Source code for faker_file.providers.image.pil_generator

import logging
import textwrap
from io import BytesIO
from pathlib import Path
from typing import Any, Dict, List, Type, Union

from faker import Faker
from faker.generator import Generator
from faker.providers.python import Provider
from PIL import Image, ImageDraw, ImageFont

from ...base import DynamicTemplate
from ...constants import DEFAULT_FILE_ENCODING
from ..base.image_generator import BaseImageGenerator

__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
__copyright__ = "2022-2023 Artur Barseghyan"
__license__ = "MIT"
__all__ = ("PilImageGenerator",)

LOGGER = logging.getLogger(__name__)


[docs]class PilImageGenerator(BaseImageGenerator): """PIL image generator. Usage example: .. code-block:: python from faker import Faker from faker_file.providers.png_file import PngFileProvider from faker_file.providers.image.pil_generator import PilImageGenerator FAKER = Faker() FAKER.add_provider(PngFileProvider) file = FAKER.png_file( image_generator_cls=PilImageGenerator, ) With options: .. code-block:: python file = FAKER.png_file( image_generator_cls=PilImageGenerator, image_generator_kwargs={ "spacing": 6, }, wrap_chars_after=119, ) With dynamic content: .. code-block:: python from faker import Faker from faker_file.base import DynamicTemplate from faker_file.contrib.image.pil_snippets import * from faker_file.providers.image.pil_generator import PilImageGenerator from faker_file.providers.png_file import PngFileProvider FAKER = Faker() FAKER.add_provider(PngFileProvider) file = FAKER.png_file( image_generator_cls=PilImageGenerator, content=DynamicTemplate( [ (add_h1_heading, {}), (add_paragraph, {"max_nb_chars": 500}), (add_paragraph, {"max_nb_chars": 500}), (add_paragraph, {"max_nb_chars": 500}), (add_paragraph, {"max_nb_chars": 500}), ] ) ) file = FAKER.png_file( image_generator_cls=PilImageGenerator, content=DynamicTemplate( [ (add_h1_heading, {}), (add_paragraph, {}), (add_picture, {}), (add_paragraph, {}), (add_picture, {}), (add_paragraph, {}), (add_picture, {}), (add_paragraph, {}), ] ) ) file = FAKER.png_file( image_generator_cls=PilImageGenerator, content=DynamicTemplate( [ (add_h1_heading, {}), (add_picture, {}), (add_paragraph, {"max_nb_chars": 500}), (add_picture, {}), (add_paragraph, {"max_nb_chars": 500}), (add_picture, {}), (add_paragraph, {"max_nb_chars": 500}), (add_picture, {}), (add_paragraph, {"max_nb_chars": 500}), ] ) ) file = FAKER.png_file( image_generator_cls=PilImageGenerator, content=DynamicTemplate( [ (add_h1_heading, {}), (add_picture, {}), (add_paragraph, {"max_nb_chars": 500}), (add_table, {"rows": 5, "cols": 4}), ] ) ) file = FAKER.png_file( image_generator_cls=PilImageGenerator, content=DynamicTemplate( [ (add_h1_heading, {"margin": (2, 2)}), (add_picture, {"margin": (2, 2)}), (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), (add_picture, {"margin": (2, 2)}), (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), (add_picture, {"margin": (2, 2)}), (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), (add_picture, {"margin": (2, 2)}), (add_paragraph, {"max_nb_chars": 500, "margin": (2, 2)}), ] ) ) """ encoding: str = DEFAULT_FILE_ENCODING font: str = str(Path("Pillow") / "Tests" / "fonts" / "DejaVuSans.ttf") font_size: int = 12 page_width: int = 794 # A4 size at 96 DPI page_height: int = 1123 # A4 size at 96 DPI line_height: int = 14 spacing: int = 6 def __init__(self: "PilImageGenerator", **kwargs) -> None: super().__init__(**kwargs) self.pages = [] self.img = None self.draw = None self.image_mode = "RGB"
[docs] @classmethod def find_max_fit_for_multi_line_text( cls: Type["PilImageGenerator"], draw: ImageDraw, lines: List[str], font: ImageFont, max_width: int, ): # Find the longest line longest_line = str(max(lines, key=len)) return cls.find_max_fit_for_single_line_text( draw, longest_line, font, max_width )
[docs] @classmethod def find_max_fit_for_single_line_text( cls: Type["PilImageGenerator"], draw: "ImageDraw", text: str, font: ImageFont, max_width: int, ) -> int: low, high = 0, len(text) while low < high: mid = (high + low) // 2 text_width, _ = draw.textsize(text[:mid], font=font) if text_width > max_width: high = mid else: low = mid + 1 return low - 1
[docs] def handle_kwargs(self: "PilImageGenerator", **kwargs) -> None: """Handle kwargs.""" if "encoding" in kwargs: self.encoding = kwargs["encoding"] if "font_size" in kwargs: self.font_size = kwargs["font_size"] if "page_width" in kwargs: self.page_width = kwargs["page_width"] if "page_height" in kwargs: self.page_height = kwargs["page_height"] if "line_height" in kwargs: self.line_height = kwargs["line_height"] if "spacing" in kwargs: self.spacing = kwargs["spacing"] if "image_mode" in kwargs: self.image_mode = kwargs["image_mode"]
[docs] def create_image_instance(self, height: Union[int, None] = None) -> Image: return Image.new( self.image_mode, (self.page_width, height or self.page_height), (255, 255, 255), )
[docs] def start_new_page(self): self.img = self.create_image_instance() self.draw = ImageDraw.Draw(self.img)
[docs] def save_and_start_new_page(self): self.pages.append(self.img.copy()) self.start_new_page()
[docs] def combine_images_vertically(self): # Calculate total width and height total_width = max(image.width for image in self.pages) total_height = sum(image.height for image in self.pages) # Create a new, white canvas to paste images onto new_image = Image.new("RGB", (total_width, total_height), "white") # Paste each image y_offset = 0 for image in self.pages: new_image.paste(image, (0, y_offset)) y_offset += image.height return new_image
[docs] def generate( self: "PilImageGenerator", content: str, data: Dict[str, Any], provider: Union[Faker, Generator, Provider], **kwargs, ) -> bytes: """Generate image.""" position = (0, 0) if isinstance(content, DynamicTemplate): self.start_new_page() for counter, (ct_modifier, ct_modifier_kwargs) in enumerate( content.content_modifiers ): ct_modifier_kwargs["position"] = position # LOGGER.debug(f"ct_modifier_kwargs: {ct_modifier_kwargs}") add_page, position = ct_modifier( provider, self, data, counter, **ct_modifier_kwargs, ) self.pages.append(self.img.copy()) # Add as a new page else: self.img = self.create_image_instance() self.draw = ImageDraw.Draw(self.img) font = ImageFont.truetype(self.font, self.font_size) # The `content_specs` is a dictionary that holds two keys: # `max_nb_chars` and `wrap_chars_after`. Those are the same values # passed to the `PdfFileProvider`. content_specs = kwargs.get("content_specs", {}) lines = content.split("\n") line_max_num_chars = self.find_max_fit_for_multi_line_text( self.draw, lines, font, self.page_width, ) wrap_chars_after = content_specs.get("wrap_chars_after") if ( not wrap_chars_after or wrap_chars_after and (wrap_chars_after > line_max_num_chars) ): lines = textwrap.wrap(content, line_max_num_chars) y_text = 0 for counter, line in enumerate(lines): text_width, text_height = self.draw.textsize( line, font=font, spacing=self.spacing ) if y_text + text_height > self.page_height: self.save_and_start_new_page() y_text = 0 self.draw.text( (0, y_text), line, fill=(0, 0, 0), spacing=self.spacing, font=font, ) y_text += text_height + self.line_height self.pages.append(self.img.copy()) # Add as a new page buffer = BytesIO() # Combine images together vertically combined_image = self.combine_images_vertically() combined_image.save( buffer, resolution=100.0, quality=95, format=provider.image_format, ) return buffer.getvalue()