all repos — python-meme-bot @ 815dfde38783eb2c67d82b4244ad8d3fef20118f

Telegram Bot that uses PIL to compute light image processing.

python_meme_bot/effects.py (view raw)

  1from PIL import Image, ImageDraw, ImageFont
  2from PIL.ImageColor import getrgb
  3from PIL.ImageEnhance import Brightness
  4from io import BytesIO
  5import textwrap, os, random, time, math
  6
  7random.seed(time.time())
  8
  9BASE_WIDTH = 1200
 10IMPACT_FONT_FILE = os.path.join("fonts", "impact.ttf")
 11ARIAL_FONT_FILE = os.path.join("fonts", "opensans.ttf")
 12
 13k1 = 0.612123
 14k2 = 1.216428
 15k3 = 0.341428
 16k4 = 0.364576
 17
 18'''
 19k1 = 0.6
 20k2 = 2 * k1
 21k3 = k1 / 2
 22k4 = k3
 23'''
 24
 25def _get_font_size(h, w, n, letter_spacing, line_spacing):
 26
 27    font_size = (math.sqrt(4 * k1 * k2 * h * n * w + math.pow(k2, 2) * math.pow(n, 2) * math.pow(letter_spacing, 2) + ((2 * k1 * k2 * k3 - 2 * k4 * math.pow(k2, 2)) * math.pow(n, 2) - 2 * k1 * k2 * line_spacing) * letter_spacing + math.pow(k1, 2) * math.pow(n, 2) * math.pow(line_spacing, 2) + (2 * k1 * k4 * k2 - 2 * math.pow(k1, 2) * k3) * math.pow(n, 2) * line_spacing + (math.pow(k1, 2) * math.pow(k3, 2) - 2 * k1 * k4 * k2 * k3 + math.pow(k4, 2) * math.pow(k2, 2)) * math.pow(n, 2)) - k2 * n * letter_spacing - k1 * n * line_spacing + (k1 * k3 + k4 * k2) * n) / (2 * k1 * k2 * n)
 28    line_width = w / (k1 * font_size - k4 + letter_spacing)
 29    return font_size, line_width
 30
 31def img_to_bio(image):
 32    
 33    bio = BytesIO()
 34    
 35    if image.mode in ("RGBA", "P"):
 36        image = image.convert("RGB")
 37
 38    image.save(bio, 'JPEG')
 39    bio.seek(0)
 40    return bio
 41
 42def _darken_image(image: Image, amount=0.5):
 43    return Brightness(image).enhance(amount)
 44
 45def _draw_line(d: ImageDraw, x: int, y: int, line: str, font: ImageFont, letter_spacing: int = 9, fill = (255, 255, 255), stroke_width: int = 9, stroke_fill = (0, 0, 0)):
 46    
 47    for i in range(len(line)):
 48                d.text((x, y), line[i], fill=fill, stroke_width=stroke_width, font=font, stroke_fill=stroke_fill)
 49                x += font.getlength(line[i]) + letter_spacing
 50
 51def _draw_tt_bt(text: str, img: Image, bottom=False):
 52        
 53    LETTER_SPACING = 9
 54    LINE_SPACING = 10  
 55    FILL = (255, 255, 255)
 56    STROKE_WIDTH = 9
 57    STROKE_FILL = (0, 0, 0)
 58    FONT_BASE = 100
 59    MARGIN = 10
 60
 61    split_caption = textwrap.wrap(text.upper(), width=20)
 62    if split_caption == []:
 63        return
 64    
 65    font_size = FONT_BASE + 10 if len(split_caption) <= 1 else FONT_BASE
 66    font = ImageFont.truetype(font=IMPACT_FONT_FILE, size=font_size)
 67    img_width, img_height = img.size
 68
 69    d = ImageDraw.Draw(img)
 70    txt_height = d.textbbox((0, 0), split_caption[0], font=font)[3]
 71
 72    if bottom:
 73        factor = -1
 74        split_caption.reverse()
 75        y = (img_height - (img_height / MARGIN)) - (txt_height / 2)
 76    else:
 77        factor = 1
 78        y = (img_height / MARGIN) - (txt_height / 1.5)
 79
 80    for line in split_caption:
 81        txt_width = d.textbbox((0, 0), line, font=font)[2]
 82
 83        x = (img_width - txt_width - (len(line) * LETTER_SPACING)) / 2
 84
 85        _draw_line(d, x, y, line, font, LETTER_SPACING, FILL, STROKE_WIDTH, STROKE_FILL)
 86
 87        y += (txt_height + LINE_SPACING) * factor
 88
 89def bt_effect(text: str, img: Image):
 90
 91    lines = [x for x in text.split("\n") if x]
 92    bt = lines[0] if len(lines) > 0 else None
 93    
 94    img = img.resize((BASE_WIDTH, int(img.size[1] * float(BASE_WIDTH / img.size[0]))))
 95    
 96    if bt is None:
 97        return img
 98    
 99    _draw_tt_bt(bt, img, bottom=True)
100
101    img = img.resize((int(BASE_WIDTH / 2), int(float(img.size[1]) * (BASE_WIDTH / 2) / img.size[0])))
102    
103    if img.mode in ("RGBA", "P"):
104        img = img.convert("RGB")
105    
106    return img_to_bio(img)
107
108def tt_bt_effect(text: str, img: Image):
109    
110    lines = [x for x in text.split("\n") if x]
111    
112    tt = lines[0] if len(lines) > 0 else None
113    bt = lines[1] if len(lines) > 1 else None
114    
115    img = img.resize((BASE_WIDTH, int(img.size[1] * float(BASE_WIDTH / img.size[0]))))
116    
117    if tt is None and bt is None:
118        return img
119    
120    if (tt is not None):
121        _draw_tt_bt(tt, img)
122    if (bt is not None):
123        _draw_tt_bt(bt, img, bottom=True)
124        
125    img = img.resize((int(BASE_WIDTH / 2), int(float(img.size[1]) * (BASE_WIDTH / 2) / img.size[0])))
126    
127    if img.mode in ("RGBA", "P"):
128        img = img.convert("RGB")
129    
130    return img_to_bio(img)
131
132def splash_effect(input_text: str, img: Image):
133    
134    LETTER_SPACING = 1
135    LINE_SPACING = 3  
136    FILL = (255, 255, 255)
137    STROKE_WIDTH = 1
138    STROKE_FILL = (0, 0, 0)
139    
140    input_text = input_text.strip()
141    
142    img = img.resize((BASE_WIDTH, int(img.size[1] * float(BASE_WIDTH / img.size[0]))))
143    
144    img = _darken_image(img)
145    
146    img_width, img_height = img.size
147    
148    w = img_width / 2
149    h = img_height / 2
150    
151    n = len(input_text)
152    
153    try:
154        FONT_BASE, LINE_WIDTH = _get_font_size(h, w, n, LETTER_SPACING, LINE_SPACING)
155    except ValueError as e:
156        print(e)
157        FONT_BASE = 60
158        LINE_WIDTH = 30
159    
160    lines = [ x for x in input_text.split("\n") if x ]
161    first_line = lines.pop(0)
162    text = "\n".join(lines)
163    
164    FONT_BASE /= 2
165    LINE_WIDTH *= 2
166
167    text = textwrap.wrap(text.upper(), width=int(LINE_WIDTH))
168    
169    if text == []:
170        print("Splash effect needs two lines!")
171        return
172    text.insert(0, first_line)
173    n_lines = len(text)
174
175    font_first = ImageFont.truetype(font=ARIAL_FONT_FILE, size=int(FONT_BASE - (FONT_BASE / 2.5)))
176    font_base = ImageFont.truetype(font=ARIAL_FONT_FILE, size=int(FONT_BASE))
177
178    d = ImageDraw.Draw(img)
179
180    _, _, first_txt_width, first_txt_height = d.textbbox((0, 0), text[0], font=font_first)
181    _, _, max_txt_width, txt_height = d.textbbox((0, 0), text[1], font=font_base)
182
183    total_height = (txt_height + LINE_SPACING) * (n_lines - 1) + LINE_SPACING + first_txt_height
184        
185    y = (img_height - total_height) / 2
186        
187    for i in range(1, n_lines):
188        
189        temp = int(font_base.getlength(text[i]))
190        if temp > max_txt_width:
191            max_txt_width = temp
192
193    max_txt_width = max_txt_width if max_txt_width > first_txt_width else first_txt_width
194    x_start = (img_width - max_txt_width) / 2
195        
196    for i in range(n_lines):
197        
198        font = font_base if i > 0 else font_first
199        _draw_line(d=d, x=x_start, y=y, line=text[i], font=font, letter_spacing=LETTER_SPACING, fill=FILL, stroke_width=STROKE_WIDTH, stroke_fill=STROKE_FILL)
200
201        y += (txt_height if i > 0 else first_txt_height) + LINE_SPACING
202        
203    img = img.resize((int(BASE_WIDTH / 2), int(float(img.size[1]) * (BASE_WIDTH / 2) / img.size[0])))
204    
205    if img.mode in ("RGBA", "P"):
206        img = img.convert("RGB")
207    
208    return img_to_bio(img)
209
210def wot_effect(input_text: str, img: Image):
211    
212    LETTER_SPACING = 1
213    LINE_SPACING = 3  
214    FILL = (255, 255, 255)
215    STROKE_WIDTH = 1
216    STROKE_FILL = (0, 0, 0)
217    
218    img = img.resize((BASE_WIDTH, int(img.size[1] * float(BASE_WIDTH / img.size[0]))))
219    img = _darken_image(img)
220    
221    img_width, img_height = img.size
222    
223    n = len(input_text.strip())
224
225    MARGIN_H = img_height / (n / 270)
226    MARGIN_W = 0
227    
228    w = img_width - MARGIN_W
229    h = img_height - MARGIN_H
230    
231    try:
232        FONT_BASE, LINE_WIDTH = _get_font_size(h, w, n, LETTER_SPACING, LINE_SPACING)
233    except ValueError as e:
234        print(e)
235        FONT_BASE = 60
236        LINE_WIDTH = 30
237
238    text = textwrap.wrap(input_text.strip(), width=int(LINE_WIDTH))
239
240    if text == []:
241        return None
242    
243    n_lines = len(text)
244    font = ImageFont.truetype(font=ARIAL_FONT_FILE, size=int(FONT_BASE))
245    d = ImageDraw.Draw(img)
246    
247    txt_height = k2 * FONT_BASE - k3 + LINE_SPACING
248    max_text_height = txt_height * n_lines
249    y = (img_height - max_text_height) / 2
250        
251    for i in range(n_lines):
252        txt_width = d.textbbox((0, 0), text[i], font=font)[2]
253        x = (img_width - txt_width - (len(text[i]) * LETTER_SPACING)) / 2
254
255        _draw_line(d=d, x=x, y=y, line=text[i], font=font, letter_spacing=LETTER_SPACING, fill=FILL, stroke_width=STROKE_WIDTH, stroke_fill=STROKE_FILL)
256
257        y += txt_height + LINE_SPACING
258        
259    img = img.resize((int(BASE_WIDTH / 2), int(float(img.size[1]) * (BASE_WIDTH / 2) / img.size[0])))
260    
261    if img.mode in ("RGBA", "P"):
262        img = img.convert("RGB")
263    
264    return img_to_bio(img)
265
266def text_effect(text: str, img: Image):
267    LETTER_SPACING = 1
268    LINE_SPACING = 3
269    STROKE_WIDTH = 1
270    STROKE_FILL = (0, 0, 0)
271    FONT_BASE = 75
272    MARGIN = 10
273    LINE_WIDTH = 20
274    
275    img = img.resize((BASE_WIDTH, int(img.size[1] * float(BASE_WIDTH / img.size[0]))))
276    
277    text = textwrap.wrap(text.strip(), width=LINE_WIDTH)
278    if text == []:
279        return
280    n_lines = len(text)
281    
282    font = ImageFont.truetype(font=ARIAL_FONT_FILE, size=FONT_BASE)
283    
284    img_width, img_height = img.size
285    d = ImageDraw.Draw(img)
286
287    _, _, _, txt_height = d.textbbox((0, 0), text[0], font=font)
288    
289    max_length = len(text[0])
290    choice = 0
291    if n_lines > 1:
292        for i in range(1, n_lines):
293            length = len(text[i])
294            if length > max_length:
295                choice = i
296                max_length = length
297            
298    max_txt_width = int(font.getlength(text[choice]))
299    
300    total_height = int((txt_height + LINE_SPACING) * n_lines)
301    
302    y_inf = 0
303    y_sup = max(0, img_height - total_height)
304    x_inf = 0
305    x_sup = max(0, img_width - max_txt_width)
306    
307    fill = getrgb(f"hsl({random.randint(0, 359)},100%,{random.randint(60, 75)}%)") # hue [0:359], saturation, lightness
308    #print(f"x-> {x_inf}:{x_sup}; y ->{y_inf}:{y_sup}; color: {fill}")
309
310    x = random.randint(x_inf, x_sup)
311    y = random.randint(y_inf, y_sup)
312    
313    for i in range(n_lines):
314        
315        _draw_line(d=d, x=x, y=y, line=text[i], font=font, letter_spacing=LETTER_SPACING, fill=fill, stroke_width=STROKE_WIDTH, stroke_fill=STROKE_FILL)
316
317        y += txt_height + LINE_SPACING
318        
319    img = img.resize((int(BASE_WIDTH / 2), int(float(img.size[1]) * (BASE_WIDTH / 2) / img.size[0])))
320    
321    if img.mode in ("RGBA", "P"):
322        img = img.convert("RGB")
323    
324    return img_to_bio(img)
325
326def test_multiple(text, effect, modifier=""):
327
328        imgs = os.listdir("test")
329        for i in range(len(imgs)):
330            image = effect(text, Image.open(os.path.join("test", imgs[i])))
331            if image is None:
332                return print("Image test failed!")
333            
334            Image.open(image).save(os.path.join("test_output", f'output{modifier}{i}.jpg'), optimize=True, quality=80)
335        return print("Image test successful")
336
337def test(text, effect, modifier=""):
338
339        image = effect(text, Image.open("image.jpg"))
340        Image.open(image).save('output.jpg', optimize=True, quality=80)
341
342        print("Image test successful")
343
344def main():
345    input_text = '''
346    BiRabittoh (@BiRabittoh)
347Godo godo godo godo godo godo godo godo godo godo godo
348'''
349    
350    #test(input_text, splash_effect)
351    test_multiple(input_text, splash_effect)
352    #test_multiple(input_text, splash_effect, "_long")
353    
354if __name__ ==  "__main__":
355    main()