rickveloper commited on
Commit
c53f3cc
·
verified ·
1 Parent(s): b4dbe0b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +148 -0
app.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from PIL import Image, ImageDraw, ImageFont
3
+ import textwrap, os
4
+
5
+ CANDIDATE_FONTS = [
6
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
7
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
8
+ ]
9
+ def get_font(size):
10
+ for p in CANDIDATE_FONTS:
11
+ if os.path.exists(p):
12
+ return ImageFont.truetype(p, size=size)
13
+ return ImageFont.load_default()
14
+
15
+ def draw_text_block(draw, text, img_w, y, font, fill, stroke_fill, stroke_width, align="center"):
16
+ lines = []
17
+ max_chars = max(12, min(30, img_w // 30))
18
+ for paragraph in (text or "").split("\n"):
19
+ wrapped = textwrap.wrap(paragraph, width=max_chars)
20
+ lines.extend(wrapped if wrapped else [""])
21
+
22
+ line_heights = []
23
+ for line in lines:
24
+ bbox = draw.textbbox((0, 0), line, font=font, stroke_width=stroke_width)
25
+ line_heights.append(bbox[3] - bbox[1])
26
+
27
+ total_h = sum(line_heights) + (len(lines) - 1) * int(font.size * 0.25)
28
+ curr_y = y
29
+ for i, line in enumerate(lines):
30
+ w = draw.textlength(line, font=font, stroke_width=stroke_width)
31
+ if align == "center":
32
+ x = (img_w - w) / 2
33
+ elif align == "left":
34
+ x = int(img_w * 0.05)
35
+ else:
36
+ x = img_w - int(img_w * 0.05) - w
37
+ draw.text((x, curr_y), line, font=font, fill=fill,
38
+ stroke_width=stroke_width, stroke_fill=stroke_fill)
39
+ curr_y += line_heights[i] + int(font.size * 0.25)
40
+ return curr_y, total_h
41
+
42
+ def make_meme(
43
+ image, top_text, bottom_text,
44
+ font_size, stroke_width, text_color, outline_color,
45
+ align, top_nudge, bottom_nudge
46
+ ):
47
+ if image is None:
48
+ return None
49
+
50
+ img = image.convert("RGB")
51
+ draw = ImageDraw.Draw(img)
52
+ w, h = img.size
53
+
54
+ base_size = max(12, int((w * font_size) / 100))
55
+ font = get_font(base_size)
56
+ stroke = int(max(0, stroke_width))
57
+
58
+ # Top
59
+ top_y = int(h * 0.03) + int(top_nudge)
60
+ draw_text_block(
61
+ draw=draw, text=(top_text or "").strip(), img_w=w, y=top_y, font=font,
62
+ fill=text_color, stroke_fill=outline_color, stroke_width=stroke, align=align
63
+ )
64
+
65
+ # Measure bottom block
66
+ tmp = Image.new("RGBA", img.size, (0, 0, 0, 0))
67
+ mdraw = ImageDraw.Draw(tmp)
68
+ lines = []
69
+ max_chars = max(12, min(30, w // 30))
70
+ for paragraph in (bottom_text or "").split("\n"):
71
+ wrapped = textwrap.wrap(paragraph, width=max_chars)
72
+ lines.extend(wrapped if wrapped else [""])
73
+ line_heights = [mdraw.textbbox((0, 0), line, font=font, stroke_width=stroke)[3] for line in lines]
74
+ total_bottom_h = sum(line_heights) + (len(lines) - 1) * int(font.size * 0.25)
75
+
76
+ bottom_y_start = int(h - total_bottom_h - h * 0.03) - int(bottom_nudge)
77
+ draw_text_block(
78
+ draw=draw, text=(bottom_text or "").strip(), img_w=w, y=bottom_y_start, font=font,
79
+ fill=text_color, stroke_fill=outline_color, stroke_width=stroke, align=align
80
+ )
81
+ return img
82
+
83
+ THEME = gr.themes.Soft(
84
+ primary_hue="indigo",
85
+ secondary_hue="violet",
86
+ neutral_hue="slate"
87
+ ).set(
88
+ button_border_width="0px",
89
+ button_large_padding="14px",
90
+ button_large_radius="14px",
91
+ radius_md="14px",
92
+ body_background_fill_dark="linear-gradient(180deg, #0b0f17 0%, #0a0920 100%)",
93
+ block_shadow="none",
94
+ )
95
+
96
+ CUSTOM_CSS = """
97
+ :root { --radius: 14px; }
98
+ * { -webkit-tap-highlight-color: transparent; }
99
+ .gradio-container { padding: 10px; max-width: 820px; margin: 0 auto; }
100
+ h2, p { text-align:center }
101
+ #stickybar { position: sticky; bottom: 0; z-index: 50; backdrop-filter: blur(8px); }
102
+ #stickybar .gr-button { width: 100%; font-weight: 700; }
103
+ @media (max-width: 640px){
104
+ .gradio-container { padding: 8px; }
105
+ .compact .gr-form{ gap:8px }
106
+ }
107
+ """
108
+
109
+ with gr.Blocks(theme=THEME, css=CUSTOM_CSS, fill_height=True, analytics_enabled=False) as demo:
110
+ gr.Markdown("<h2>📱 Mobile Meme Maker</h2><p>Upload → Type → Download. Built on iPhone, for iPhone.</p>")
111
+
112
+ with gr.Row(equal_height=False, elem_classes="compact"):
113
+ with gr.Column(scale=1):
114
+ in_img = gr.Image(type="pil", label="Upload photo", height=320)
115
+
116
+ with gr.Accordion("Text & Style", open=True):
117
+ top_text = gr.Textbox(label="Top text", value="WHEN THE WIFI JUST WORKS", autofocus=True)
118
+ bottom_text = gr.Textbox(label="Bottom text", value="CHEF'S KISS")
119
+ align = gr.Radio(choices=["left", "center", "right"], value="center", label="Alignment", inline=True)
120
+ font_size = gr.Slider(8, 22, value=12, step=1, label="Font size (% of width)")
121
+ stroke_width = gr.Slider(0, 14, value=4, step=1, label="Outline thickness")
122
+ with gr.Row():
123
+ text_color = gr.ColorPicker(value="#FFFFFF", label="Text")
124
+ outline_color = gr.ColorPicker(value="#000000", label="Outline")
125
+
126
+ with gr.Accordion("Position Nudges", open=False):
127
+ top_nudge = gr.Slider(-300, 300, value=0, step=1, label="Top text nudge (px)")
128
+ bottom_nudge = gr.Slider(-300, 300, value=0, step=1, label="Bottom text nudge (px)")
129
+
130
+ with gr.Column(scale=1):
131
+ out = gr.Image(type="pil", label="Preview / Download", height=440, show_download_button=True)
132
+ generate = gr.Button("✨ Generate", size="lg")
133
+ gr.Markdown(
134
+ "<div id='stickybar'>"
135
+ "<div class='container' style='display:flex; gap:10px; margin-top:8px;'>"
136
+ "</div></div>"
137
+ )
138
+
139
+ # Generate action
140
+ inputs = [in_img, top_text, bottom_text, font_size, stroke_width, text_color, outline_color, align, top_nudge, bottom_nudge]
141
+ generate.click(fn=make_meme, inputs=inputs, outputs=out)
142
+
143
+ # Live preview as you change things
144
+ for comp in inputs:
145
+ comp.change(fn=make_meme, inputs=inputs, outputs=out, show_progress=False)
146
+
147
+ if __name__ == "__main__":
148
+ demo.launch()