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]