Source code for memeengine.meme_generator

"""Meme generators overlay text(body, author from `quoteengine`  image files."""
import tempfile
from pathlib import Path
from typing import Tuple

import numpy as np
from PIL import Image, ImageDraw, ImageOps
from PIL.ImageFont import FreeTypeFont


[docs]class MemeGenerator: """MemeGenerator generates a meme from an image and text. Geenrates a meme from either (1) initialized arguments or (2) by setting output_dir only during construction then calling make_make to get back a path of the processed meme image. However all methods are checked and will raise a value error if not set/supplied required attributes are missing. All have defaults except quote_body and quote_author. """ def __init__(self, output_path: str = './output', input_image_path: str = None, quote_body: str = '', quote_author: str = '', output_width: int = None, text_width_percent: float = 0.7, font_path: str = 'memeengine/fonts/Montserrat-SemiBold.ttf', font_size: int = None, ): """Creation of a Meme Image. All params have defaults so they can be applied(or not) and any desired fashion. :param output_path: Destination directory for saving :param input_image_path: Path and filename for image load :param quote_body: Quote body text :param quote_author: Quote Author text :param output_width: Desired image output width :param text_width_percent: Percentage of image with for text to use :param font_path: Path to font file that will be used to generate text :param font_size: Set font size, overridden when using the `scale_text=True` param in overlay_text method """ self.output_path = Path(output_path) self.quote_body = quote_body self._quote_author = quote_author self.output_width = output_width self.text_width_percent = text_width_percent self.font_path = font_path self.font_size = font_size self.font_body = None self.font_author = None self.image = None self.input_image_path = input_image_path # Load image now if supplied to constructor if self.input_image_path: self.load_image(self.input_image_path) @property def quote_author(self): """Return quote author in the format of choice for rendering.""" return f'- {self._quote_author}' @quote_author.setter def quote_author(self, value): self._quote_author = value
[docs] def load_image(self, image_path:str) -> None: """Return None after loading an image from path into class attribute.""" self.image = Image.open(image_path)
[docs] def overlay_text(self, position: Tuple[int, int] = None, output_width: int = None, scale_text: bool = True, random_text_position=True): """Overlays text on loaded image. Applies optional args then applies previously loaded quote_body and quote_author to an previously loaded image. To access the processed image use the `image` class attribute. """ if not position and not random_text_position: raise ValueError("One of position, random_text_position must be provided") elif position and random_text_position: raise ValueError("Only one of position, random_text_position can be provided") if output_width: self.crop_image(output_width=output_width) if scale_text: self.set_fonts_image_scale() if random_text_position: position = self.random_image_position() d1 = ImageDraw.Draw(self.image) d1.text(position, self.quote_body, font=self.font_body, anchor='rb', fill=(255, 255, 255)) d1.text(position, self.quote_author, font=self.font_author, anchor='rt', fill=(255, 255, 255))
[docs] def save_image(self): """Return path of saved image file.""" self.output_path.mkdir(parents=True, exist_ok=True) full_output_path = tempfile.NamedTemporaryFile(dir=self.output_path.name, prefix='meme-generator-', suffix='.jpg', delete=False).name self.image.save(full_output_path) return str(self.output_path) + "/" + str(Path(full_output_path).name)
[docs] def crop_image(self, output_width: int = None) -> None: """Return an image crop cropped to single_dim keeping aspect ratio.""" if not output_width and not self.output_width: raise ValueError("output_width must be supplied or set on instance before calling fit_image") if output_width and self.output_width != output_width: self.output_width = output_width scale = output_width/self.image.width w,h = self.image.size[0]*scale,self.image.size[0]*scale self.image = ImageOps.fit(self.image, (w,h))
[docs] def set_font_size(self, size: int, author_decrease_pct=0.20) -> None: """Return a font for author based on class attributes.""" if 0.1 > author_decrease_pct > .9: raise ValueError("author_decrease_pct must be a percent in range from 0.10 - 0.90") self.font_body = FreeTypeFont(self.font_path, size) self.font_author = FreeTypeFont(self.font_path, int(size * (1+-author_decrease_pct)))
[docs] def set_fonts_image_scale(self, text_width_pct: float = None) -> None: """Return two fonts for body and author text scaled to percentage of width.""" if text_width_pct and self.text_width_percent != text_width_pct: self.text_width_percent = text_width_pct self.set_font_size(1) while self.font_body.getlength(self.quote_body) < self.image.width * self.text_width_percent: self.set_font_size(self.font_body.size + 1)
[docs] def random_image_position(self) -> Tuple[int, int]: """Return random position for text placement.""" box_padding = np.append(self.font_body.getbbox(self.quote_body, anchor='rb')[:2], np.array(self.font_author.getbbox(self.quote_author, anchor='rt')[2:])) * -1 x_min, y_min, x_max, y_max = tuple(np.array(((0, 0), self.image.size)).flatten() + box_padding) return np.random.random_integers(x_min, x_max), np.random.random_integers(y_min, y_max)
[docs] def make_meme(self, img_path: str, quote_body: str, quote_author: str, width: int = 500) -> str: """Override previously supplied params and saves image. Created for a specific use case: meme = MemeGenerator('./tmp') path = meme.make_meme(img, quote.body, quote.author) :param img_path: Loads image from path, overriding and previously stored image :param quote_body: Loads quote_body from supplied text, overriding quote_body a previously set quote_body :param quote_author: Same as quote_body but for quote_author :param width: Scaled image keeping :return: location of saved file as a str """ self.load_image(img_path) self.quote_body = quote_body self.quote_author = quote_author self.overlay_text(output_width=width, scale_text=True) return self.save_image()