From 9a8b586292bc2a2a517135179b09415c1cf9c415 Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 6 Apr 2026 18:58:42 -0400 Subject: [PATCH] feat(cover): pixel-width headline truncation and larger font MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace character-count truncation (45 chars) with Pillow textbbox pixel-width measurement. Bump headline font 14→18, line spacing 22→26. Made-with: Cursor --- src/cover.py | 27 ++++++++++++++++++++++----- tests/test_cover.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/cover.py b/src/cover.py index 9d10a72..c477a07 100644 --- a/src/cover.py +++ b/src/cover.py @@ -70,6 +70,23 @@ def _get_dominant_category(categories: list[str]) -> str: return most_common +def _truncate_to_width( + text: str, font: ImageFont.FreeTypeFont, max_width: int +) -> str: + dummy = PILImage.new("RGBA", (1, 1)) + draw = ImageDraw.Draw(dummy) + bbox = draw.textbbox((0, 0), text, font=font) + if (bbox[2] - bbox[0]) <= max_width: + return text + ellipsis = "\u2026" + for i in range(len(text) - 1, 0, -1): + candidate = text[:i] + ellipsis + bbox = draw.textbbox((0, 0), candidate, font=font) + if (bbox[2] - bbox[0]) <= max_width: + return candidate + return ellipsis + + def _draw_text_overlays( draw: ImageDraw.ImageDraw, width: int, @@ -80,7 +97,7 @@ def _draw_text_overlays( ) -> None: title_font = _get_font(38) date_font = _get_font(20) - headline_font = _get_font(14) + headline_font = _get_font(18) draw.rectangle([(0, 0), (width, 90)], fill=(0, 0, 0, 204)) draw.text( @@ -100,18 +117,18 @@ def _draw_text_overlays( if headlines: max_headlines = min(len(headlines), 5) - strip_height = 28 + max_headlines * 22 + strip_height = 28 + max_headlines * 26 strip_top = height - strip_height draw.rectangle( [(0, strip_top), (width, height)], fill=(0, 0, 0, 170) ) + max_width = width - 32 y = strip_top + 8 for headline in headlines[:max_headlines]: text = f"\u2022 {headline}" - if len(text) > 45: - text = text[:42] + "..." + text = _truncate_to_width(text, headline_font, max_width) draw.text((16, y), text, fill="white", font=headline_font) - y += 22 + y += 26 def generate_programmatic_cover( diff --git a/tests/test_cover.py b/tests/test_cover.py index 5b00e23..c0fd6af 100644 --- a/tests/test_cover.py +++ b/tests/test_cover.py @@ -3,13 +3,15 @@ from collections import Counter from datetime import date from unittest.mock import patch, MagicMock from io import BytesIO -from PIL import Image as PILImage +from PIL import Image as PILImage, ImageDraw, ImageFont from src.cover import ( generate_programmatic_cover, generate_ai_cover, generate_cover, _get_dominant_category, + _truncate_to_width, + _get_font, CATEGORY_PROMPTS, ) @@ -109,3 +111,28 @@ def test_get_dominant_category(): def test_category_prompts_include_massachusetts(): for category, prompt in CATEGORY_PROMPTS.items(): assert "Massachusetts" in prompt, f"Prompt for {category} missing 'Massachusetts'" + + +def test_truncate_to_width_respects_pixel_budget(): + font = _get_font(18) + short = "Short text" + result = _truncate_to_width(short, font, 400) + assert result == short, "Short text should not be truncated" + + long = "A" * 200 + result = _truncate_to_width(long, font, 400) + assert result.endswith("\u2026"), "Long text should end with ellipsis" + assert len(result) < len(long), "Truncated text should be shorter" + + +def test_truncate_to_width_fits_within_budget(): + font = _get_font(18) + long = "This is a really long headline that should definitely be truncated to fit" + max_width = 448 + result = _truncate_to_width(long, font, max_width) + + dummy = PILImage.new("RGBA", (1, 1)) + draw = ImageDraw.Draw(dummy) + bbox = draw.textbbox((0, 0), result, font=font) + rendered_width = bbox[2] - bbox[0] + assert rendered_width <= max_width, f"Rendered width {rendered_width} exceeds {max_width}"