Source code for todays_paper_web.widgets.core.text

import textwrap
from typing import Optional

import webcolors
from PIL import Image, ImageDraw, ImageFont
from pydantic import Field

from todays_paper_web.fonts import Fonts
from todays_paper_web.widgets import registerView, View
from todays_paper_web.widgets.params import Color
from todays_paper_web.widgets.view import ViewParams


[docs] class TextParams(ViewParams): size: int = Field(18, description="Font size in points.") font: Optional[str] = Field( None, description="Path to a `.ttf`/`.otf` font file, resolved the same way as any other file " "the config references. Falls back to the bundled FiraSans at `size` if unset or " "unloadable.", ) width: Optional[int] = Field( None, description="If set, wraps text to this pixel width using greedy word-wrapping before " "measuring/drawing. Without it, only explicit newlines in the content break lines.", ) color: Color = Field( default_factory=lambda: webcolors.name_to_rgb("black"), description="Text color." )
@registerView("text") class TextView(View): Params = TextParams def render(self): # Format the text text = self.innerText # Get bounding box img = Image.new("1", (0, 0)) draw = ImageDraw.Draw(img) font = Fonts.load("FiraSans", self.params.size) if self.params.font: try: font = ImageFont.truetype(self.openFile(self.params.font), self.params.size) except: print("Could not load font", self.params.font) if self.params.width: # If width is specified, then word wrap the text within the bounds lines = TextView.wordwrap(text, self.params.width, draw, font) text = "\n".join(lines) _, _, width, height = draw.multiline_textbbox( (0, 0), text, font=font, ) # Draw the text img = Image.new("RGBA", (width, height), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) draw.multiline_text( (0, 0), text, font=font, fill=(self.params.color.red, self.params.color.green, self.params.color.blue, 255), ) return img @staticmethod def wordwrap( text: str, width: int, draw: ImageDraw.ImageDraw, font: ImageFont.FreeTypeFont, ): wrapper = textwrap.TextWrapper() chunks = wrapper._split(text) words = {} lines = [] line = [] lineWidth = 0 for word in chunks: # Cache the size if the word is used more than once if word not in words: _, _, wordWidth, _ = draw.textbbox((0, 0), word, font=font) words[word] = wordWidth if lineWidth + words[word] > width: lines.append(line) line = [] lineWidth = 0 line.append(word) lineWidth += words[word] lines.append(line) return ["".join(line).strip() for line in lines]