Skip to content

fix: improve share image design and optimize file size#145

Closed
Hugo0 wants to merge 3 commits intomainfrom
fix/share-image-v3
Closed

fix: improve share image design and optimize file size#145
Hugo0 wants to merge 3 commits intomainfrom
fix/share-image-v3

Conversation

@Hugo0
Copy link
Owner

@Hugo0 Hugo0 commented Mar 14, 2026

Summary

  • Pixel-perfect header: Crops the WORDLE tiles + globe directly from og-image.png instead of drawing an approximation with code
  • Improved layout: 1/3 - 2/3 split for score and mini grid with proper vertical centering
  • Auto-sizing challenge text: Shrinks from 44px down to 26px for longer translations that would otherwise overflow
  • CJK font fallback: Adds NotoSansCJK for Korean, fixing missing Hangul glyphs
  • File size optimization: Palette-mode PNG conversion (64 adaptive colors) cuts total image size from ~13MB to ~7MB

Test plan

  • Verify share images render correctly on social media previews (Twitter/Facebook/WhatsApp)
  • Spot-check a few language images (English, Arabic RTL, Korean CJK)
  • Confirm file sizes are smaller than before

Summary by CodeRabbit

  • New Features

    • Redesigned share-image layout with a pixel-perfect header and a centered mini-grid representing results.
    • Language-aware font selection (CJK vs Latin) with dynamic sizing and two-line cap for challenge text.
  • Improvements

    • Better text centering, wrapping and score presentation.
    • Reduced shared-image file sizes via optimized color palette.
    • Minor logging format refinement.

@coderabbitai
Copy link

coderabbitai bot commented Mar 14, 2026

Warning

Rate limit exceeded

@Hugo0 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 10 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f37b4f25-30e2-4a11-9c1a-5f379f163641

📥 Commits

Reviewing files that changed from the base of the PR and between 883931c and 24dffc1.

📒 Files selected for processing (1)
  • scripts/generate_share_images.py
📝 Walkthrough

Walkthrough

Reworks the share-image generator: adds OG image header extraction (load_header + HEADER_STRIP), replaces per-tile Wordle rendering with a centered mini-grid, adds CJK-aware font selection and dynamic text/score layout, and updates image composition to use the pre-cropped header and 64-color palette output.

Changes

Cohort / File(s) Summary
Share image generator
scripts/generate_share_images.py
Added OG_IMAGE_PATH, load_header() and HEADER_STRIP; removed tile-specific rendering (draw_wordle_tiles) and added draw_mini_grid; updated generate_image() to compose using header strip, language-aware font selection (CJK vs DejaVu), dynamic score sizing, two-line wrapped challenge text, and final palette conversion.
Webapp logging formatting
webapp/app.py
Minor formatting-only change: adjusted print calls in load_languages() to use explicit parentheses and line breaks; no behavioral change.

Sequence Diagram(s)

sequenceDiagram
    participant Main as "scripts/generate_share_images.py::main"
    participant Loader as "load_header()"
    participant Generator as "generate_image()"
    participant Fonts as "Font selector (CJK/DejaVu)"
    participant Composer as "Image composer (header, score, mini-grid, text)"
    participant FS as "Filesystem (og-image.png / output.png)"

    Main->>Loader: ensure HEADER_STRIP initialized
    Loader->>FS: open `og-image.png`
    FS-->>Loader: raw image
    Loader-->>Main: HEADER_STRIP

    Main->>Generator: generate image for score/result/text
    Generator->>Fonts: pick fonts by language (CJK_LANGS)
    Fonts-->>Generator: font objects
    Generator->>Composer: provide HEADER_STRIP, fonts, score, result
    Composer->>Composer: draw header, center score, draw_mini_grid, wrap text
    Composer->>FS: save final image (palette reduced)
    FS-->>Main: output path
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A header cropped neat from the og-image she kept,

I drew a small grid where the old tiles slept.
Fonts for all tongues, two lines fit just right,
Palette shrunk tight, and the share looks bright! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: improve share image design and optimize file size' accurately summarizes the main changes: visual improvements to share images and file size reduction via palette conversion.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/share-image-v3
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Hugo0 added 2 commits March 14, 2026 15:34
- Pixel-perfect header cropped from og-image.png (was hand-drawn approximation)
- 1/3 - 2/3 layout for score and mini grid (proper spacing and vertical centering)
- Auto-sizing challenge text (44→26px) for longer translations
- NotoSansCJK fallback for Korean (fixes missing glyphs)
- Palette-mode PNG optimization (13MB → 7MB)
@Hugo0 Hugo0 force-pushed the fix/share-image-v3 branch from 20b785d to 883931c Compare March 14, 2026 15:36
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/generate_share_images.py (1)

202-202: ⚠️ Potential issue | 🟡 Minor

String comparison for boolean config is fragile.

The comparison lang_config.get("right_to_left", "false") == "true" assumes the JSON value is a string. If the config contains a boolean (true/false instead of "true"/"false"), RTL will silently fail.

Proposed fix for robustness
-            "is_rtl": lang_config.get("right_to_left", "false") == "true",
+            "is_rtl": lang_config.get("right_to_left", False) in (True, "true"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate_share_images.py` at line 202, The current assignment for
"is_rtl" uses a fragile string comparison lang_config.get("right_to_left",
"false") == "true" which fails if the config contains a boolean; update the
logic where "is_rtl" is set (in scripts/generate_share_images.py, referencing
lang_config and the "right_to_left" key) to normalize the value to a boolean:
retrieve the raw value, if it's a string interpret common truthy strings (e.g.,
"true","1","yes") case-insensitively, otherwise coerce booleans/numerics to
bool, and assign that boolean to "is_rtl" so both true/false and "true"/"false"
representations work robustly.
🧹 Nitpick comments (3)
scripts/generate_share_images.py (3)

39-46: Hardcoded Linux font paths limit cross-platform development.

The font paths assume a Linux environment with DejaVu and Noto fonts installed at specific system locations. This works for containerized builds but may break local development on macOS/Windows.

Consider documenting the font dependencies or adding fallback logic if cross-platform support is needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate_share_images.py` around lines 39 - 46, The hardcoded font
paths (FONT_DEJAVU, FONT_DEJAVU_BOLD, FONT_CJK, FONT_CJK_BOLD, SCORE_FONT)
assume Linux and break on macOS/Windows; update generate_share_images.py to
implement fallback/discovery: try environment variables (e.g., FONT_PATH_*),
then check a short list of common platform locations, then attempt to load
bundled fonts in the repo, and finally fall back to PIL’s default font or raise
a clear error; keep CJK_LANGS logic unchanged but ensure CJK font resolution
uses the same multi-path fallback so Hangul characters load cross-platform.

183-184: Use non-deprecated Pillow constants.

Image.ADAPTIVE is deprecated in Pillow 10+ in favor of Image.Palette.ADAPTIVE.

Proposed fix
     # Convert to palette mode for smaller file size (~6 distinct colors)
-    return img.convert("P", palette=Image.ADAPTIVE, colors=64)
+    return img.convert("P", palette=Image.Palette.ADAPTIVE, colors=64)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate_share_images.py` around lines 183 - 184, Replace the
deprecated Image.ADAPTIVE constant in the img.convert call with the
non-deprecated Image.Palette.ADAPTIVE: change the return in the image conversion
(the img.convert("P", palette=..., colors=64) call) to use
Image.Palette.ADAPTIVE instead of Image.ADAPTIVE; ensure any necessary imports
are available so Image.Palette is accessible in the scope where the conversion
occurs.

64-74: Replace Image.LANCZOS with Image.Resampling.LANCZOS for Pillow 12 compatibility.

While Image.LANCZOS is still supported in Pillow 12+, Image.Resampling.LANCZOS is the recommended constant going forward.

Optionally, add explicit error handling for the missing OG image file to provide a clearer error message before the script fails at usage.

Proposed fix
 def load_header():
     """Crop and scale the WORDLE tiles + globe from og-image.png."""
     global HEADER_STRIP
     og = Image.open(OG_IMAGE_PATH)
     # Crop the tiles+globe band (y=185..335 in the 1200x630 original)
     strip = og.crop((0, 185, 1200, 335))
     HEADER_STRIP = strip.resize(
         (int(strip.width * HEADER_HEIGHT / strip.height), HEADER_HEIGHT),
-        Image.LANCZOS,
+        Image.Resampling.LANCZOS,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate_share_images.py` around lines 64 - 74, In load_header(),
replace the deprecated Image.LANCZOS usage with Image.Resampling.LANCZOS when
calling strip.resize to ensure Pillow 12 compatibility, i.e., update the
resampling constant referenced in that call (function: load_header, globals:
HEADER_STRIP, OG_IMAGE_PATH); additionally, add a small try/except around
Image.open(OG_IMAGE_PATH) to catch FileNotFoundError (or IOError) and raise or
log a clearer message before proceeding so missing OG image errors are explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/generate_share_images.py`:
- Around line 162-181: The code assumes wrap_text(display_text, font_main, draw,
max_w) always returns at least one line; if display_text is empty it returns []
and later accesses lines[0], causing IndexError. Add a guard after computing
lines (the variable from wrap_text) to handle an empty list: if not lines, skip
the drawing block or set a safe default (e.g., no text or a placeholder) before
the existing if len(lines) >= 2 / else branches. Update references around the
draw.textbbox and draw.text calls (where font_main and font_cta are used) so
they only run when lines contains the expected elements.

---

Outside diff comments:
In `@scripts/generate_share_images.py`:
- Line 202: The current assignment for "is_rtl" uses a fragile string comparison
lang_config.get("right_to_left", "false") == "true" which fails if the config
contains a boolean; update the logic where "is_rtl" is set (in
scripts/generate_share_images.py, referencing lang_config and the
"right_to_left" key) to normalize the value to a boolean: retrieve the raw
value, if it's a string interpret common truthy strings (e.g., "true","1","yes")
case-insensitively, otherwise coerce booleans/numerics to bool, and assign that
boolean to "is_rtl" so both true/false and "true"/"false" representations work
robustly.

---

Nitpick comments:
In `@scripts/generate_share_images.py`:
- Around line 39-46: The hardcoded font paths (FONT_DEJAVU, FONT_DEJAVU_BOLD,
FONT_CJK, FONT_CJK_BOLD, SCORE_FONT) assume Linux and break on macOS/Windows;
update generate_share_images.py to implement fallback/discovery: try environment
variables (e.g., FONT_PATH_*), then check a short list of common platform
locations, then attempt to load bundled fonts in the repo, and finally fall back
to PIL’s default font or raise a clear error; keep CJK_LANGS logic unchanged but
ensure CJK font resolution uses the same multi-path fallback so Hangul
characters load cross-platform.
- Around line 183-184: Replace the deprecated Image.ADAPTIVE constant in the
img.convert call with the non-deprecated Image.Palette.ADAPTIVE: change the
return in the image conversion (the img.convert("P", palette=..., colors=64)
call) to use Image.Palette.ADAPTIVE instead of Image.ADAPTIVE; ensure any
necessary imports are available so Image.Palette is accessible in the scope
where the conversion occurs.
- Around line 64-74: In load_header(), replace the deprecated Image.LANCZOS
usage with Image.Resampling.LANCZOS when calling strip.resize to ensure Pillow
12 compatibility, i.e., update the resampling constant referenced in that call
(function: load_header, globals: HEADER_STRIP, OG_IMAGE_PATH); additionally, add
a small try/except around Image.open(OG_IMAGE_PATH) to catch FileNotFoundError
(or IOError) and raise or log a clearer message before proceeding so missing OG
image errors are explicit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3d680750-de7b-401d-bac9-78cfcd6dead8

📥 Commits

Reviewing files that changed from the base of the PR and between 7445580 and 20b785d.

⛔ Files ignored due to path filters (299)
  • webapp/static/images/share/ar_1.png is excluded by !**/*.png
  • webapp/static/images/share/ar_2.png is excluded by !**/*.png
  • webapp/static/images/share/ar_3.png is excluded by !**/*.png
  • webapp/static/images/share/ar_4.png is excluded by !**/*.png
  • webapp/static/images/share/ar_5.png is excluded by !**/*.png
  • webapp/static/images/share/ar_6.png is excluded by !**/*.png
  • webapp/static/images/share/ar_x.png is excluded by !**/*.png
  • webapp/static/images/share/az_1.png is excluded by !**/*.png
  • webapp/static/images/share/az_2.png is excluded by !**/*.png
  • webapp/static/images/share/az_3.png is excluded by !**/*.png
  • webapp/static/images/share/az_4.png is excluded by !**/*.png
  • webapp/static/images/share/az_5.png is excluded by !**/*.png
  • webapp/static/images/share/az_6.png is excluded by !**/*.png
  • webapp/static/images/share/az_x.png is excluded by !**/*.png
  • webapp/static/images/share/bg_1.png is excluded by !**/*.png
  • webapp/static/images/share/bg_2.png is excluded by !**/*.png
  • webapp/static/images/share/bg_3.png is excluded by !**/*.png
  • webapp/static/images/share/bg_4.png is excluded by !**/*.png
  • webapp/static/images/share/bg_5.png is excluded by !**/*.png
  • webapp/static/images/share/bg_6.png is excluded by !**/*.png
  • webapp/static/images/share/bg_x.png is excluded by !**/*.png
  • webapp/static/images/share/br_1.png is excluded by !**/*.png
  • webapp/static/images/share/br_2.png is excluded by !**/*.png
  • webapp/static/images/share/br_3.png is excluded by !**/*.png
  • webapp/static/images/share/br_4.png is excluded by !**/*.png
  • webapp/static/images/share/br_5.png is excluded by !**/*.png
  • webapp/static/images/share/br_6.png is excluded by !**/*.png
  • webapp/static/images/share/br_x.png is excluded by !**/*.png
  • webapp/static/images/share/ca_1.png is excluded by !**/*.png
  • webapp/static/images/share/ca_2.png is excluded by !**/*.png
  • webapp/static/images/share/ca_3.png is excluded by !**/*.png
  • webapp/static/images/share/ca_4.png is excluded by !**/*.png
  • webapp/static/images/share/ca_5.png is excluded by !**/*.png
  • webapp/static/images/share/ca_6.png is excluded by !**/*.png
  • webapp/static/images/share/ca_x.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_1.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_2.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_3.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_4.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_5.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_6.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_x.png is excluded by !**/*.png
  • webapp/static/images/share/cs_1.png is excluded by !**/*.png
  • webapp/static/images/share/cs_2.png is excluded by !**/*.png
  • webapp/static/images/share/cs_3.png is excluded by !**/*.png
  • webapp/static/images/share/cs_4.png is excluded by !**/*.png
  • webapp/static/images/share/cs_5.png is excluded by !**/*.png
  • webapp/static/images/share/cs_6.png is excluded by !**/*.png
  • webapp/static/images/share/cs_x.png is excluded by !**/*.png
  • webapp/static/images/share/da_1.png is excluded by !**/*.png
  • webapp/static/images/share/da_2.png is excluded by !**/*.png
  • webapp/static/images/share/da_3.png is excluded by !**/*.png
  • webapp/static/images/share/da_4.png is excluded by !**/*.png
  • webapp/static/images/share/da_5.png is excluded by !**/*.png
  • webapp/static/images/share/da_6.png is excluded by !**/*.png
  • webapp/static/images/share/da_x.png is excluded by !**/*.png
  • webapp/static/images/share/de_1.png is excluded by !**/*.png
  • webapp/static/images/share/de_2.png is excluded by !**/*.png
  • webapp/static/images/share/de_3.png is excluded by !**/*.png
  • webapp/static/images/share/de_4.png is excluded by !**/*.png
  • webapp/static/images/share/de_5.png is excluded by !**/*.png
  • webapp/static/images/share/de_6.png is excluded by !**/*.png
  • webapp/static/images/share/de_x.png is excluded by !**/*.png
  • webapp/static/images/share/el_1.png is excluded by !**/*.png
  • webapp/static/images/share/el_2.png is excluded by !**/*.png
  • webapp/static/images/share/el_3.png is excluded by !**/*.png
  • webapp/static/images/share/el_4.png is excluded by !**/*.png
  • webapp/static/images/share/el_5.png is excluded by !**/*.png
  • webapp/static/images/share/el_6.png is excluded by !**/*.png
  • webapp/static/images/share/el_x.png is excluded by !**/*.png
  • webapp/static/images/share/en_1.png is excluded by !**/*.png
  • webapp/static/images/share/en_2.png is excluded by !**/*.png
  • webapp/static/images/share/en_3.png is excluded by !**/*.png
  • webapp/static/images/share/en_4.png is excluded by !**/*.png
  • webapp/static/images/share/en_5.png is excluded by !**/*.png
  • webapp/static/images/share/en_6.png is excluded by !**/*.png
  • webapp/static/images/share/en_x.png is excluded by !**/*.png
  • webapp/static/images/share/eo_1.png is excluded by !**/*.png
  • webapp/static/images/share/eo_2.png is excluded by !**/*.png
  • webapp/static/images/share/eo_3.png is excluded by !**/*.png
  • webapp/static/images/share/eo_4.png is excluded by !**/*.png
  • webapp/static/images/share/eo_5.png is excluded by !**/*.png
  • webapp/static/images/share/eo_6.png is excluded by !**/*.png
  • webapp/static/images/share/eo_x.png is excluded by !**/*.png
  • webapp/static/images/share/es_1.png is excluded by !**/*.png
  • webapp/static/images/share/es_2.png is excluded by !**/*.png
  • webapp/static/images/share/es_3.png is excluded by !**/*.png
  • webapp/static/images/share/es_4.png is excluded by !**/*.png
  • webapp/static/images/share/es_5.png is excluded by !**/*.png
  • webapp/static/images/share/es_6.png is excluded by !**/*.png
  • webapp/static/images/share/es_x.png is excluded by !**/*.png
  • webapp/static/images/share/et_1.png is excluded by !**/*.png
  • webapp/static/images/share/et_2.png is excluded by !**/*.png
  • webapp/static/images/share/et_3.png is excluded by !**/*.png
  • webapp/static/images/share/et_4.png is excluded by !**/*.png
  • webapp/static/images/share/et_5.png is excluded by !**/*.png
  • webapp/static/images/share/et_6.png is excluded by !**/*.png
  • webapp/static/images/share/et_x.png is excluded by !**/*.png
  • webapp/static/images/share/eu_1.png is excluded by !**/*.png
  • webapp/static/images/share/eu_2.png is excluded by !**/*.png
  • webapp/static/images/share/eu_3.png is excluded by !**/*.png
  • webapp/static/images/share/eu_4.png is excluded by !**/*.png
  • webapp/static/images/share/eu_5.png is excluded by !**/*.png
  • webapp/static/images/share/eu_6.png is excluded by !**/*.png
  • webapp/static/images/share/eu_x.png is excluded by !**/*.png
  • webapp/static/images/share/fa_1.png is excluded by !**/*.png
  • webapp/static/images/share/fa_2.png is excluded by !**/*.png
  • webapp/static/images/share/fa_3.png is excluded by !**/*.png
  • webapp/static/images/share/fa_4.png is excluded by !**/*.png
  • webapp/static/images/share/fa_5.png is excluded by !**/*.png
  • webapp/static/images/share/fa_6.png is excluded by !**/*.png
  • webapp/static/images/share/fa_x.png is excluded by !**/*.png
  • webapp/static/images/share/fi_1.png is excluded by !**/*.png
  • webapp/static/images/share/fi_2.png is excluded by !**/*.png
  • webapp/static/images/share/fi_3.png is excluded by !**/*.png
  • webapp/static/images/share/fi_4.png is excluded by !**/*.png
  • webapp/static/images/share/fi_5.png is excluded by !**/*.png
  • webapp/static/images/share/fi_6.png is excluded by !**/*.png
  • webapp/static/images/share/fi_x.png is excluded by !**/*.png
  • webapp/static/images/share/fo_1.png is excluded by !**/*.png
  • webapp/static/images/share/fo_2.png is excluded by !**/*.png
  • webapp/static/images/share/fo_3.png is excluded by !**/*.png
  • webapp/static/images/share/fo_4.png is excluded by !**/*.png
  • webapp/static/images/share/fo_5.png is excluded by !**/*.png
  • webapp/static/images/share/fo_6.png is excluded by !**/*.png
  • webapp/static/images/share/fo_x.png is excluded by !**/*.png
  • webapp/static/images/share/fr_1.png is excluded by !**/*.png
  • webapp/static/images/share/fr_2.png is excluded by !**/*.png
  • webapp/static/images/share/fr_3.png is excluded by !**/*.png
  • webapp/static/images/share/fr_4.png is excluded by !**/*.png
  • webapp/static/images/share/fr_5.png is excluded by !**/*.png
  • webapp/static/images/share/fr_6.png is excluded by !**/*.png
  • webapp/static/images/share/fr_x.png is excluded by !**/*.png
  • webapp/static/images/share/fur_1.png is excluded by !**/*.png
  • webapp/static/images/share/fur_2.png is excluded by !**/*.png
  • webapp/static/images/share/fur_3.png is excluded by !**/*.png
  • webapp/static/images/share/fur_4.png is excluded by !**/*.png
  • webapp/static/images/share/fur_5.png is excluded by !**/*.png
  • webapp/static/images/share/fur_6.png is excluded by !**/*.png
  • webapp/static/images/share/fur_x.png is excluded by !**/*.png
  • webapp/static/images/share/fy_1.png is excluded by !**/*.png
  • webapp/static/images/share/fy_2.png is excluded by !**/*.png
  • webapp/static/images/share/fy_3.png is excluded by !**/*.png
  • webapp/static/images/share/fy_4.png is excluded by !**/*.png
  • webapp/static/images/share/fy_5.png is excluded by !**/*.png
  • webapp/static/images/share/fy_6.png is excluded by !**/*.png
  • webapp/static/images/share/fy_x.png is excluded by !**/*.png
  • webapp/static/images/share/ga_1.png is excluded by !**/*.png
  • webapp/static/images/share/ga_2.png is excluded by !**/*.png
  • webapp/static/images/share/ga_3.png is excluded by !**/*.png
  • webapp/static/images/share/ga_4.png is excluded by !**/*.png
  • webapp/static/images/share/ga_5.png is excluded by !**/*.png
  • webapp/static/images/share/ga_6.png is excluded by !**/*.png
  • webapp/static/images/share/ga_x.png is excluded by !**/*.png
  • webapp/static/images/share/gd_1.png is excluded by !**/*.png
  • webapp/static/images/share/gd_2.png is excluded by !**/*.png
  • webapp/static/images/share/gd_3.png is excluded by !**/*.png
  • webapp/static/images/share/gd_4.png is excluded by !**/*.png
  • webapp/static/images/share/gd_5.png is excluded by !**/*.png
  • webapp/static/images/share/gd_6.png is excluded by !**/*.png
  • webapp/static/images/share/gd_x.png is excluded by !**/*.png
  • webapp/static/images/share/gl_1.png is excluded by !**/*.png
  • webapp/static/images/share/gl_2.png is excluded by !**/*.png
  • webapp/static/images/share/gl_3.png is excluded by !**/*.png
  • webapp/static/images/share/gl_4.png is excluded by !**/*.png
  • webapp/static/images/share/gl_5.png is excluded by !**/*.png
  • webapp/static/images/share/gl_6.png is excluded by !**/*.png
  • webapp/static/images/share/gl_x.png is excluded by !**/*.png
  • webapp/static/images/share/he_1.png is excluded by !**/*.png
  • webapp/static/images/share/he_2.png is excluded by !**/*.png
  • webapp/static/images/share/he_3.png is excluded by !**/*.png
  • webapp/static/images/share/he_4.png is excluded by !**/*.png
  • webapp/static/images/share/he_5.png is excluded by !**/*.png
  • webapp/static/images/share/he_6.png is excluded by !**/*.png
  • webapp/static/images/share/he_x.png is excluded by !**/*.png
  • webapp/static/images/share/hr_1.png is excluded by !**/*.png
  • webapp/static/images/share/hr_2.png is excluded by !**/*.png
  • webapp/static/images/share/hr_3.png is excluded by !**/*.png
  • webapp/static/images/share/hr_4.png is excluded by !**/*.png
  • webapp/static/images/share/hr_5.png is excluded by !**/*.png
  • webapp/static/images/share/hr_6.png is excluded by !**/*.png
  • webapp/static/images/share/hr_x.png is excluded by !**/*.png
  • webapp/static/images/share/hu_1.png is excluded by !**/*.png
  • webapp/static/images/share/hu_2.png is excluded by !**/*.png
  • webapp/static/images/share/hu_3.png is excluded by !**/*.png
  • webapp/static/images/share/hu_4.png is excluded by !**/*.png
  • webapp/static/images/share/hu_5.png is excluded by !**/*.png
  • webapp/static/images/share/hu_6.png is excluded by !**/*.png
  • webapp/static/images/share/hu_x.png is excluded by !**/*.png
  • webapp/static/images/share/hy_1.png is excluded by !**/*.png
  • webapp/static/images/share/hy_2.png is excluded by !**/*.png
  • webapp/static/images/share/hy_3.png is excluded by !**/*.png
  • webapp/static/images/share/hy_4.png is excluded by !**/*.png
  • webapp/static/images/share/hy_5.png is excluded by !**/*.png
  • webapp/static/images/share/hy_6.png is excluded by !**/*.png
  • webapp/static/images/share/hy_x.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_1.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_2.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_3.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_4.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_5.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_6.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_x.png is excluded by !**/*.png
  • webapp/static/images/share/ia_1.png is excluded by !**/*.png
  • webapp/static/images/share/ia_2.png is excluded by !**/*.png
  • webapp/static/images/share/ia_3.png is excluded by !**/*.png
  • webapp/static/images/share/ia_4.png is excluded by !**/*.png
  • webapp/static/images/share/ia_5.png is excluded by !**/*.png
  • webapp/static/images/share/ia_6.png is excluded by !**/*.png
  • webapp/static/images/share/ia_x.png is excluded by !**/*.png
  • webapp/static/images/share/ie_1.png is excluded by !**/*.png
  • webapp/static/images/share/ie_2.png is excluded by !**/*.png
  • webapp/static/images/share/ie_3.png is excluded by !**/*.png
  • webapp/static/images/share/ie_4.png is excluded by !**/*.png
  • webapp/static/images/share/ie_5.png is excluded by !**/*.png
  • webapp/static/images/share/ie_6.png is excluded by !**/*.png
  • webapp/static/images/share/ie_x.png is excluded by !**/*.png
  • webapp/static/images/share/is_1.png is excluded by !**/*.png
  • webapp/static/images/share/is_2.png is excluded by !**/*.png
  • webapp/static/images/share/is_3.png is excluded by !**/*.png
  • webapp/static/images/share/is_4.png is excluded by !**/*.png
  • webapp/static/images/share/is_5.png is excluded by !**/*.png
  • webapp/static/images/share/is_6.png is excluded by !**/*.png
  • webapp/static/images/share/is_x.png is excluded by !**/*.png
  • webapp/static/images/share/it_1.png is excluded by !**/*.png
  • webapp/static/images/share/it_2.png is excluded by !**/*.png
  • webapp/static/images/share/it_3.png is excluded by !**/*.png
  • webapp/static/images/share/it_4.png is excluded by !**/*.png
  • webapp/static/images/share/it_5.png is excluded by !**/*.png
  • webapp/static/images/share/it_6.png is excluded by !**/*.png
  • webapp/static/images/share/it_x.png is excluded by !**/*.png
  • webapp/static/images/share/ka_1.png is excluded by !**/*.png
  • webapp/static/images/share/ka_2.png is excluded by !**/*.png
  • webapp/static/images/share/ka_3.png is excluded by !**/*.png
  • webapp/static/images/share/ka_4.png is excluded by !**/*.png
  • webapp/static/images/share/ka_5.png is excluded by !**/*.png
  • webapp/static/images/share/ka_6.png is excluded by !**/*.png
  • webapp/static/images/share/ka_x.png is excluded by !**/*.png
  • webapp/static/images/share/ko_1.png is excluded by !**/*.png
  • webapp/static/images/share/ko_2.png is excluded by !**/*.png
  • webapp/static/images/share/ko_3.png is excluded by !**/*.png
  • webapp/static/images/share/ko_4.png is excluded by !**/*.png
  • webapp/static/images/share/ko_5.png is excluded by !**/*.png
  • webapp/static/images/share/ko_6.png is excluded by !**/*.png
  • webapp/static/images/share/ko_x.png is excluded by !**/*.png
  • webapp/static/images/share/la_1.png is excluded by !**/*.png
  • webapp/static/images/share/la_2.png is excluded by !**/*.png
  • webapp/static/images/share/la_3.png is excluded by !**/*.png
  • webapp/static/images/share/la_4.png is excluded by !**/*.png
  • webapp/static/images/share/la_5.png is excluded by !**/*.png
  • webapp/static/images/share/la_6.png is excluded by !**/*.png
  • webapp/static/images/share/la_x.png is excluded by !**/*.png
  • webapp/static/images/share/lb_1.png is excluded by !**/*.png
  • webapp/static/images/share/lb_2.png is excluded by !**/*.png
  • webapp/static/images/share/lb_3.png is excluded by !**/*.png
  • webapp/static/images/share/lb_4.png is excluded by !**/*.png
  • webapp/static/images/share/lb_5.png is excluded by !**/*.png
  • webapp/static/images/share/lb_6.png is excluded by !**/*.png
  • webapp/static/images/share/lb_x.png is excluded by !**/*.png
  • webapp/static/images/share/lt_1.png is excluded by !**/*.png
  • webapp/static/images/share/lt_2.png is excluded by !**/*.png
  • webapp/static/images/share/lt_3.png is excluded by !**/*.png
  • webapp/static/images/share/lt_4.png is excluded by !**/*.png
  • webapp/static/images/share/lt_5.png is excluded by !**/*.png
  • webapp/static/images/share/lt_6.png is excluded by !**/*.png
  • webapp/static/images/share/lt_x.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_1.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_2.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_3.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_4.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_5.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_6.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_x.png is excluded by !**/*.png
  • webapp/static/images/share/lv_1.png is excluded by !**/*.png
  • webapp/static/images/share/lv_2.png is excluded by !**/*.png
  • webapp/static/images/share/lv_3.png is excluded by !**/*.png
  • webapp/static/images/share/lv_4.png is excluded by !**/*.png
  • webapp/static/images/share/lv_5.png is excluded by !**/*.png
  • webapp/static/images/share/lv_6.png is excluded by !**/*.png
  • webapp/static/images/share/lv_x.png is excluded by !**/*.png
  • webapp/static/images/share/mi_1.png is excluded by !**/*.png
  • webapp/static/images/share/mi_2.png is excluded by !**/*.png
  • webapp/static/images/share/mi_3.png is excluded by !**/*.png
  • webapp/static/images/share/mi_4.png is excluded by !**/*.png
  • webapp/static/images/share/mi_5.png is excluded by !**/*.png
  • webapp/static/images/share/mi_6.png is excluded by !**/*.png
  • webapp/static/images/share/mi_x.png is excluded by !**/*.png
  • webapp/static/images/share/mk_1.png is excluded by !**/*.png
  • webapp/static/images/share/mk_2.png is excluded by !**/*.png
  • webapp/static/images/share/mk_3.png is excluded by !**/*.png
  • webapp/static/images/share/mk_4.png is excluded by !**/*.png
  • webapp/static/images/share/mk_5.png is excluded by !**/*.png
  • webapp/static/images/share/mk_6.png is excluded by !**/*.png
  • webapp/static/images/share/mk_x.png is excluded by !**/*.png
  • webapp/static/images/share/mn_1.png is excluded by !**/*.png
  • webapp/static/images/share/mn_2.png is excluded by !**/*.png
  • webapp/static/images/share/mn_3.png is excluded by !**/*.png
  • webapp/static/images/share/mn_4.png is excluded by !**/*.png
  • webapp/static/images/share/mn_5.png is excluded by !**/*.png
📒 Files selected for processing (1)
  • scripts/generate_share_images.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
scripts/generate_share_images.py (1)

162-181: ⚠️ Potential issue | 🟡 Minor

Guard against empty wrapped text before indexing.

wrap_text() can return [] for empty/whitespace input, and lines[0] then raises IndexError.

Suggested fix
     for size in (44, 38, 32, 26):
         font_main = get_font(font_reg, size)
         font_cta = get_font(font_bold, size + 4)
         lines = wrap_text(display_text, font_main, draw, max_w)
         if len(lines) <= 2:
             break
 
+    if not lines:
+        lines = [""]
+
     if len(lines) >= 2:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate_share_images.py` around lines 162 - 181, The block assumes
wrap_text(display_text, font_main, draw, max_w) returns at least one line but
wrap_text can return an empty list; add a guard after computing lines to handle
an empty list before accessing lines[0] or lines[1:]. In the function where
wrap_text is called (variables: lines, display_text, font_main, font_cta, draw,
WIDTH), check if not lines: either skip drawing/return early or draw a fallback
(e.g., nothing or a default message) so subsequent accesses to lines[0] and
lines[1:] and calls to draw.textbbox do not raise IndexError. Ensure both the if
len(lines) >= 2 and the else branch only run when lines is non-empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/generate_share_images.py`:
- Around line 159-168: The loop that reduces font size using get_font and
wrap_text may still yield >2 lines at the minimum size (size 26), but the code
then joins all remaining lines into line2 which can overflow; change the
post-loop handling in the block that uses lines to enforce a hard two-line cap:
keep line1 = lines[0], construct line2 from the remaining text but truncate it
with an ellipsis so its rendered width (measured with draw.textlength or
font_cta metrics) does not exceed max_w, ensuring you measure with the chosen
font (font_main/font_cta) and trim words/characters until the width fits; update
the logic around variables lines, line1, line2 and reuse wrap_text/measurement
utilities rather than allowing an unbounded join.
- Around line 170-181: Several draw.text(...) calls (around the block using
bbox1, bbox2, WIDTH, font_main, font_cta and the else branch using bbox) exceed
the 100-char line length; run the Ruff formatter (or manually wrap the long
draw.text argument lists) so each line is <=100 characters, breaking long
argument lists across multiple lines and aligning subsequent args, and reformat
the tuple calculations ((WIDTH - (bbox[2] - bbox[0])) // 2, ...) and the
draw.text(...) calls for both the two-line branch (line1/line2 using
bbox1/bbox2) and the single-line else branch (line using bbox) to satisfy the
linter.
- Around line 64-73: The load_header() function currently opens OG_IMAGE_PATH
without a context manager and doesn't handle missing files; change
Image.open(OG_IMAGE_PATH) to use a with-statement (e.g., with
Image.open(OG_IMAGE_PATH) as og:) so the file handle is closed safely, and wrap
that block in a try/except catching FileNotFoundError/OSError to raise or log a
clear error that mentions OG_IMAGE_PATH before attempting to crop/resize and
assign HEADER_STRIP.
- Around line 39-43: Replace the hard-coded absolute font paths by implementing
resilient font resolution in scripts/generate_share_images.py: for each symbol
(FONT_DEJAVU, FONT_DEJAVU_BOLD, FONT_CJK, FONT_CJK_BOLD, SCORE_FONT) attempt to
load from a list of candidate paths and fall back to a bundled font or PIL’s
default if none exist, and log a warning when falling back; ensure functions
that call ImageFont.truetype (or equivalent) use the resolved path or fallback
object so font loading won’t crash when the specific system fonts are missing.

---

Duplicate comments:
In `@scripts/generate_share_images.py`:
- Around line 162-181: The block assumes wrap_text(display_text, font_main,
draw, max_w) returns at least one line but wrap_text can return an empty list;
add a guard after computing lines to handle an empty list before accessing
lines[0] or lines[1:]. In the function where wrap_text is called (variables:
lines, display_text, font_main, font_cta, draw, WIDTH), check if not lines:
either skip drawing/return early or draw a fallback (e.g., nothing or a default
message) so subsequent accesses to lines[0] and lines[1:] and calls to
draw.textbbox do not raise IndexError. Ensure both the if len(lines) >= 2 and
the else branch only run when lines is non-empty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e27e104-12ab-4b29-bee1-6461ff6b2223

📥 Commits

Reviewing files that changed from the base of the PR and between 20b785d and 883931c.

⛔ Files ignored due to path filters (289)
  • webapp/static/images/share/ar_1.png is excluded by !**/*.png
  • webapp/static/images/share/ar_2.png is excluded by !**/*.png
  • webapp/static/images/share/ar_3.png is excluded by !**/*.png
  • webapp/static/images/share/ar_4.png is excluded by !**/*.png
  • webapp/static/images/share/ar_5.png is excluded by !**/*.png
  • webapp/static/images/share/ar_6.png is excluded by !**/*.png
  • webapp/static/images/share/ar_x.png is excluded by !**/*.png
  • webapp/static/images/share/az_1.png is excluded by !**/*.png
  • webapp/static/images/share/az_2.png is excluded by !**/*.png
  • webapp/static/images/share/az_3.png is excluded by !**/*.png
  • webapp/static/images/share/az_4.png is excluded by !**/*.png
  • webapp/static/images/share/az_5.png is excluded by !**/*.png
  • webapp/static/images/share/az_6.png is excluded by !**/*.png
  • webapp/static/images/share/az_x.png is excluded by !**/*.png
  • webapp/static/images/share/bg_1.png is excluded by !**/*.png
  • webapp/static/images/share/bg_2.png is excluded by !**/*.png
  • webapp/static/images/share/bg_3.png is excluded by !**/*.png
  • webapp/static/images/share/bg_4.png is excluded by !**/*.png
  • webapp/static/images/share/bg_5.png is excluded by !**/*.png
  • webapp/static/images/share/bg_6.png is excluded by !**/*.png
  • webapp/static/images/share/bg_x.png is excluded by !**/*.png
  • webapp/static/images/share/br_1.png is excluded by !**/*.png
  • webapp/static/images/share/br_2.png is excluded by !**/*.png
  • webapp/static/images/share/br_3.png is excluded by !**/*.png
  • webapp/static/images/share/br_4.png is excluded by !**/*.png
  • webapp/static/images/share/br_5.png is excluded by !**/*.png
  • webapp/static/images/share/br_6.png is excluded by !**/*.png
  • webapp/static/images/share/br_x.png is excluded by !**/*.png
  • webapp/static/images/share/ca_1.png is excluded by !**/*.png
  • webapp/static/images/share/ca_2.png is excluded by !**/*.png
  • webapp/static/images/share/ca_3.png is excluded by !**/*.png
  • webapp/static/images/share/ca_4.png is excluded by !**/*.png
  • webapp/static/images/share/ca_5.png is excluded by !**/*.png
  • webapp/static/images/share/ca_6.png is excluded by !**/*.png
  • webapp/static/images/share/ca_x.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_1.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_2.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_3.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_4.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_5.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_6.png is excluded by !**/*.png
  • webapp/static/images/share/ckb_x.png is excluded by !**/*.png
  • webapp/static/images/share/cs_1.png is excluded by !**/*.png
  • webapp/static/images/share/cs_2.png is excluded by !**/*.png
  • webapp/static/images/share/cs_3.png is excluded by !**/*.png
  • webapp/static/images/share/cs_4.png is excluded by !**/*.png
  • webapp/static/images/share/cs_5.png is excluded by !**/*.png
  • webapp/static/images/share/cs_6.png is excluded by !**/*.png
  • webapp/static/images/share/cs_x.png is excluded by !**/*.png
  • webapp/static/images/share/da_1.png is excluded by !**/*.png
  • webapp/static/images/share/da_2.png is excluded by !**/*.png
  • webapp/static/images/share/da_3.png is excluded by !**/*.png
  • webapp/static/images/share/da_4.png is excluded by !**/*.png
  • webapp/static/images/share/da_5.png is excluded by !**/*.png
  • webapp/static/images/share/da_6.png is excluded by !**/*.png
  • webapp/static/images/share/da_x.png is excluded by !**/*.png
  • webapp/static/images/share/de_1.png is excluded by !**/*.png
  • webapp/static/images/share/de_2.png is excluded by !**/*.png
  • webapp/static/images/share/de_3.png is excluded by !**/*.png
  • webapp/static/images/share/de_4.png is excluded by !**/*.png
  • webapp/static/images/share/de_5.png is excluded by !**/*.png
  • webapp/static/images/share/de_6.png is excluded by !**/*.png
  • webapp/static/images/share/de_x.png is excluded by !**/*.png
  • webapp/static/images/share/el_1.png is excluded by !**/*.png
  • webapp/static/images/share/el_2.png is excluded by !**/*.png
  • webapp/static/images/share/el_3.png is excluded by !**/*.png
  • webapp/static/images/share/el_4.png is excluded by !**/*.png
  • webapp/static/images/share/el_5.png is excluded by !**/*.png
  • webapp/static/images/share/el_6.png is excluded by !**/*.png
  • webapp/static/images/share/el_x.png is excluded by !**/*.png
  • webapp/static/images/share/en_1.png is excluded by !**/*.png
  • webapp/static/images/share/en_2.png is excluded by !**/*.png
  • webapp/static/images/share/en_3.png is excluded by !**/*.png
  • webapp/static/images/share/en_4.png is excluded by !**/*.png
  • webapp/static/images/share/en_5.png is excluded by !**/*.png
  • webapp/static/images/share/en_6.png is excluded by !**/*.png
  • webapp/static/images/share/en_x.png is excluded by !**/*.png
  • webapp/static/images/share/eo_1.png is excluded by !**/*.png
  • webapp/static/images/share/eo_2.png is excluded by !**/*.png
  • webapp/static/images/share/eo_3.png is excluded by !**/*.png
  • webapp/static/images/share/eo_4.png is excluded by !**/*.png
  • webapp/static/images/share/eo_5.png is excluded by !**/*.png
  • webapp/static/images/share/eo_6.png is excluded by !**/*.png
  • webapp/static/images/share/eo_x.png is excluded by !**/*.png
  • webapp/static/images/share/es_1.png is excluded by !**/*.png
  • webapp/static/images/share/es_2.png is excluded by !**/*.png
  • webapp/static/images/share/es_3.png is excluded by !**/*.png
  • webapp/static/images/share/es_4.png is excluded by !**/*.png
  • webapp/static/images/share/es_5.png is excluded by !**/*.png
  • webapp/static/images/share/es_6.png is excluded by !**/*.png
  • webapp/static/images/share/es_x.png is excluded by !**/*.png
  • webapp/static/images/share/et_1.png is excluded by !**/*.png
  • webapp/static/images/share/et_2.png is excluded by !**/*.png
  • webapp/static/images/share/et_3.png is excluded by !**/*.png
  • webapp/static/images/share/et_4.png is excluded by !**/*.png
  • webapp/static/images/share/et_5.png is excluded by !**/*.png
  • webapp/static/images/share/et_6.png is excluded by !**/*.png
  • webapp/static/images/share/et_x.png is excluded by !**/*.png
  • webapp/static/images/share/eu_1.png is excluded by !**/*.png
  • webapp/static/images/share/eu_2.png is excluded by !**/*.png
  • webapp/static/images/share/eu_3.png is excluded by !**/*.png
  • webapp/static/images/share/eu_4.png is excluded by !**/*.png
  • webapp/static/images/share/eu_5.png is excluded by !**/*.png
  • webapp/static/images/share/eu_6.png is excluded by !**/*.png
  • webapp/static/images/share/eu_x.png is excluded by !**/*.png
  • webapp/static/images/share/fa_1.png is excluded by !**/*.png
  • webapp/static/images/share/fa_2.png is excluded by !**/*.png
  • webapp/static/images/share/fa_3.png is excluded by !**/*.png
  • webapp/static/images/share/fa_4.png is excluded by !**/*.png
  • webapp/static/images/share/fa_5.png is excluded by !**/*.png
  • webapp/static/images/share/fa_6.png is excluded by !**/*.png
  • webapp/static/images/share/fa_x.png is excluded by !**/*.png
  • webapp/static/images/share/fi_1.png is excluded by !**/*.png
  • webapp/static/images/share/fi_2.png is excluded by !**/*.png
  • webapp/static/images/share/fi_3.png is excluded by !**/*.png
  • webapp/static/images/share/fi_4.png is excluded by !**/*.png
  • webapp/static/images/share/fi_5.png is excluded by !**/*.png
  • webapp/static/images/share/fi_6.png is excluded by !**/*.png
  • webapp/static/images/share/fi_x.png is excluded by !**/*.png
  • webapp/static/images/share/fo_1.png is excluded by !**/*.png
  • webapp/static/images/share/fo_2.png is excluded by !**/*.png
  • webapp/static/images/share/fo_3.png is excluded by !**/*.png
  • webapp/static/images/share/fo_4.png is excluded by !**/*.png
  • webapp/static/images/share/fo_5.png is excluded by !**/*.png
  • webapp/static/images/share/fo_6.png is excluded by !**/*.png
  • webapp/static/images/share/fo_x.png is excluded by !**/*.png
  • webapp/static/images/share/fr_1.png is excluded by !**/*.png
  • webapp/static/images/share/fr_2.png is excluded by !**/*.png
  • webapp/static/images/share/fr_3.png is excluded by !**/*.png
  • webapp/static/images/share/fr_4.png is excluded by !**/*.png
  • webapp/static/images/share/fr_5.png is excluded by !**/*.png
  • webapp/static/images/share/fr_6.png is excluded by !**/*.png
  • webapp/static/images/share/fr_x.png is excluded by !**/*.png
  • webapp/static/images/share/fur_1.png is excluded by !**/*.png
  • webapp/static/images/share/fur_2.png is excluded by !**/*.png
  • webapp/static/images/share/fur_3.png is excluded by !**/*.png
  • webapp/static/images/share/fur_4.png is excluded by !**/*.png
  • webapp/static/images/share/fur_5.png is excluded by !**/*.png
  • webapp/static/images/share/fur_6.png is excluded by !**/*.png
  • webapp/static/images/share/fur_x.png is excluded by !**/*.png
  • webapp/static/images/share/fy_1.png is excluded by !**/*.png
  • webapp/static/images/share/fy_2.png is excluded by !**/*.png
  • webapp/static/images/share/fy_3.png is excluded by !**/*.png
  • webapp/static/images/share/fy_4.png is excluded by !**/*.png
  • webapp/static/images/share/fy_5.png is excluded by !**/*.png
  • webapp/static/images/share/fy_6.png is excluded by !**/*.png
  • webapp/static/images/share/fy_x.png is excluded by !**/*.png
  • webapp/static/images/share/ga_1.png is excluded by !**/*.png
  • webapp/static/images/share/ga_2.png is excluded by !**/*.png
  • webapp/static/images/share/ga_3.png is excluded by !**/*.png
  • webapp/static/images/share/ga_4.png is excluded by !**/*.png
  • webapp/static/images/share/ga_5.png is excluded by !**/*.png
  • webapp/static/images/share/ga_6.png is excluded by !**/*.png
  • webapp/static/images/share/ga_x.png is excluded by !**/*.png
  • webapp/static/images/share/gd_1.png is excluded by !**/*.png
  • webapp/static/images/share/gd_2.png is excluded by !**/*.png
  • webapp/static/images/share/gd_3.png is excluded by !**/*.png
  • webapp/static/images/share/gd_4.png is excluded by !**/*.png
  • webapp/static/images/share/gd_5.png is excluded by !**/*.png
  • webapp/static/images/share/gd_6.png is excluded by !**/*.png
  • webapp/static/images/share/gd_x.png is excluded by !**/*.png
  • webapp/static/images/share/gl_1.png is excluded by !**/*.png
  • webapp/static/images/share/gl_2.png is excluded by !**/*.png
  • webapp/static/images/share/gl_3.png is excluded by !**/*.png
  • webapp/static/images/share/gl_4.png is excluded by !**/*.png
  • webapp/static/images/share/gl_5.png is excluded by !**/*.png
  • webapp/static/images/share/gl_6.png is excluded by !**/*.png
  • webapp/static/images/share/gl_x.png is excluded by !**/*.png
  • webapp/static/images/share/he_1.png is excluded by !**/*.png
  • webapp/static/images/share/he_2.png is excluded by !**/*.png
  • webapp/static/images/share/he_3.png is excluded by !**/*.png
  • webapp/static/images/share/he_4.png is excluded by !**/*.png
  • webapp/static/images/share/he_5.png is excluded by !**/*.png
  • webapp/static/images/share/he_6.png is excluded by !**/*.png
  • webapp/static/images/share/he_x.png is excluded by !**/*.png
  • webapp/static/images/share/hr_1.png is excluded by !**/*.png
  • webapp/static/images/share/hr_2.png is excluded by !**/*.png
  • webapp/static/images/share/hr_3.png is excluded by !**/*.png
  • webapp/static/images/share/hr_4.png is excluded by !**/*.png
  • webapp/static/images/share/hr_5.png is excluded by !**/*.png
  • webapp/static/images/share/hr_6.png is excluded by !**/*.png
  • webapp/static/images/share/hr_x.png is excluded by !**/*.png
  • webapp/static/images/share/hu_1.png is excluded by !**/*.png
  • webapp/static/images/share/hu_2.png is excluded by !**/*.png
  • webapp/static/images/share/hu_3.png is excluded by !**/*.png
  • webapp/static/images/share/hu_4.png is excluded by !**/*.png
  • webapp/static/images/share/hu_5.png is excluded by !**/*.png
  • webapp/static/images/share/hu_6.png is excluded by !**/*.png
  • webapp/static/images/share/hu_x.png is excluded by !**/*.png
  • webapp/static/images/share/hy_1.png is excluded by !**/*.png
  • webapp/static/images/share/hy_2.png is excluded by !**/*.png
  • webapp/static/images/share/hy_3.png is excluded by !**/*.png
  • webapp/static/images/share/hy_4.png is excluded by !**/*.png
  • webapp/static/images/share/hy_5.png is excluded by !**/*.png
  • webapp/static/images/share/hy_6.png is excluded by !**/*.png
  • webapp/static/images/share/hy_x.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_1.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_2.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_3.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_4.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_5.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_6.png is excluded by !**/*.png
  • webapp/static/images/share/hyw_x.png is excluded by !**/*.png
  • webapp/static/images/share/ia_1.png is excluded by !**/*.png
  • webapp/static/images/share/ia_2.png is excluded by !**/*.png
  • webapp/static/images/share/ia_3.png is excluded by !**/*.png
  • webapp/static/images/share/ia_4.png is excluded by !**/*.png
  • webapp/static/images/share/ia_5.png is excluded by !**/*.png
  • webapp/static/images/share/ia_6.png is excluded by !**/*.png
  • webapp/static/images/share/ia_x.png is excluded by !**/*.png
  • webapp/static/images/share/ie_1.png is excluded by !**/*.png
  • webapp/static/images/share/ie_2.png is excluded by !**/*.png
  • webapp/static/images/share/ie_3.png is excluded by !**/*.png
  • webapp/static/images/share/ie_4.png is excluded by !**/*.png
  • webapp/static/images/share/ie_5.png is excluded by !**/*.png
  • webapp/static/images/share/ie_6.png is excluded by !**/*.png
  • webapp/static/images/share/ie_x.png is excluded by !**/*.png
  • webapp/static/images/share/is_1.png is excluded by !**/*.png
  • webapp/static/images/share/is_2.png is excluded by !**/*.png
  • webapp/static/images/share/is_3.png is excluded by !**/*.png
  • webapp/static/images/share/is_4.png is excluded by !**/*.png
  • webapp/static/images/share/is_5.png is excluded by !**/*.png
  • webapp/static/images/share/is_6.png is excluded by !**/*.png
  • webapp/static/images/share/is_x.png is excluded by !**/*.png
  • webapp/static/images/share/it_1.png is excluded by !**/*.png
  • webapp/static/images/share/it_2.png is excluded by !**/*.png
  • webapp/static/images/share/it_3.png is excluded by !**/*.png
  • webapp/static/images/share/it_4.png is excluded by !**/*.png
  • webapp/static/images/share/it_5.png is excluded by !**/*.png
  • webapp/static/images/share/it_6.png is excluded by !**/*.png
  • webapp/static/images/share/it_x.png is excluded by !**/*.png
  • webapp/static/images/share/ka_1.png is excluded by !**/*.png
  • webapp/static/images/share/ka_2.png is excluded by !**/*.png
  • webapp/static/images/share/ka_3.png is excluded by !**/*.png
  • webapp/static/images/share/ka_4.png is excluded by !**/*.png
  • webapp/static/images/share/ka_5.png is excluded by !**/*.png
  • webapp/static/images/share/ka_6.png is excluded by !**/*.png
  • webapp/static/images/share/ka_x.png is excluded by !**/*.png
  • webapp/static/images/share/ko_1.png is excluded by !**/*.png
  • webapp/static/images/share/ko_2.png is excluded by !**/*.png
  • webapp/static/images/share/ko_3.png is excluded by !**/*.png
  • webapp/static/images/share/ko_4.png is excluded by !**/*.png
  • webapp/static/images/share/ko_5.png is excluded by !**/*.png
  • webapp/static/images/share/ko_6.png is excluded by !**/*.png
  • webapp/static/images/share/ko_x.png is excluded by !**/*.png
  • webapp/static/images/share/la_1.png is excluded by !**/*.png
  • webapp/static/images/share/la_2.png is excluded by !**/*.png
  • webapp/static/images/share/la_3.png is excluded by !**/*.png
  • webapp/static/images/share/la_4.png is excluded by !**/*.png
  • webapp/static/images/share/la_5.png is excluded by !**/*.png
  • webapp/static/images/share/la_6.png is excluded by !**/*.png
  • webapp/static/images/share/la_x.png is excluded by !**/*.png
  • webapp/static/images/share/lb_1.png is excluded by !**/*.png
  • webapp/static/images/share/lb_2.png is excluded by !**/*.png
  • webapp/static/images/share/lb_3.png is excluded by !**/*.png
  • webapp/static/images/share/lb_4.png is excluded by !**/*.png
  • webapp/static/images/share/lb_5.png is excluded by !**/*.png
  • webapp/static/images/share/lb_6.png is excluded by !**/*.png
  • webapp/static/images/share/lb_x.png is excluded by !**/*.png
  • webapp/static/images/share/lt_1.png is excluded by !**/*.png
  • webapp/static/images/share/lt_2.png is excluded by !**/*.png
  • webapp/static/images/share/lt_3.png is excluded by !**/*.png
  • webapp/static/images/share/lt_4.png is excluded by !**/*.png
  • webapp/static/images/share/lt_5.png is excluded by !**/*.png
  • webapp/static/images/share/lt_6.png is excluded by !**/*.png
  • webapp/static/images/share/lt_x.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_1.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_2.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_3.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_4.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_5.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_6.png is excluded by !**/*.png
  • webapp/static/images/share/ltg_x.png is excluded by !**/*.png
  • webapp/static/images/share/lv_1.png is excluded by !**/*.png
  • webapp/static/images/share/lv_2.png is excluded by !**/*.png
  • webapp/static/images/share/lv_3.png is excluded by !**/*.png
  • webapp/static/images/share/lv_4.png is excluded by !**/*.png
  • webapp/static/images/share/lv_5.png is excluded by !**/*.png
  • webapp/static/images/share/lv_6.png is excluded by !**/*.png
  • webapp/static/images/share/lv_x.png is excluded by !**/*.png
  • webapp/static/images/share/mi_1.png is excluded by !**/*.png
  • webapp/static/images/share/mi_2.png is excluded by !**/*.png
  • webapp/static/images/share/mi_3.png is excluded by !**/*.png
  • webapp/static/images/share/mi_4.png is excluded by !**/*.png
  • webapp/static/images/share/mi_5.png is excluded by !**/*.png
  • webapp/static/images/share/mi_6.png is excluded by !**/*.png
  • webapp/static/images/share/mi_x.png is excluded by !**/*.png
  • webapp/static/images/share/mk_1.png is excluded by !**/*.png
  • webapp/static/images/share/mk_2.png is excluded by !**/*.png
📒 Files selected for processing (2)
  • scripts/generate_share_images.py
  • webapp/app.py
✅ Files skipped from review due to trivial changes (1)
  • webapp/app.py

Comment on lines +39 to +43
FONT_DEJAVU = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
FONT_DEJAVU_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
FONT_CJK = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
FONT_CJK_BOLD = "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"
SCORE_FONT = "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid hard-coded system font paths without fallback.

These absolute paths will fail on environments where Noto fonts are not installed, causing generation to crash.

Suggested fix
+def pick_font_path(*candidates):
+    for candidate in candidates:
+        if os.path.exists(candidate):
+            return candidate
+    raise FileNotFoundError(f"No usable font found in: {candidates}")
+
-FONT_DEJAVU = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
-FONT_DEJAVU_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
-FONT_CJK = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
-FONT_CJK_BOLD = "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"
-SCORE_FONT = "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf"
+FONT_DEJAVU = pick_font_path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
+FONT_DEJAVU_BOLD = pick_font_path("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf")
+FONT_CJK = pick_font_path(
+    "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
+    FONT_DEJAVU,
+)
+FONT_CJK_BOLD = pick_font_path(
+    "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
+    FONT_DEJAVU_BOLD,
+)
+SCORE_FONT = pick_font_path(
+    "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf",
+    FONT_DEJAVU_BOLD,
+)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate_share_images.py` around lines 39 - 43, Replace the
hard-coded absolute font paths by implementing resilient font resolution in
scripts/generate_share_images.py: for each symbol (FONT_DEJAVU,
FONT_DEJAVU_BOLD, FONT_CJK, FONT_CJK_BOLD, SCORE_FONT) attempt to load from a
list of candidate paths and fall back to a bundled font or PIL’s default if none
exist, and log a warning when falling back; ensure functions that call
ImageFont.truetype (or equivalent) use the resolved path or fallback object so
font loading won’t crash when the specific system fonts are missing.

Comment on lines +64 to +73
def load_header():
"""Crop and scale the WORDLE tiles + globe from og-image.png."""
global HEADER_STRIP
og = Image.open(OG_IMAGE_PATH)
# Crop the tiles+globe band (y=185..335 in the 1200x630 original)
strip = og.crop((0, 185, 1200, 335))
HEADER_STRIP = strip.resize(
(int(strip.width * HEADER_HEIGHT / strip.height), HEADER_HEIGHT),
Image.LANCZOS,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle missing og-image.png and close file handles safely.

Image.open() should be wrapped in a context manager, and missing file errors should be explicit for faster diagnosis.

Suggested fix
 def load_header():
     """Crop and scale the WORDLE tiles + globe from og-image.png."""
     global HEADER_STRIP
-    og = Image.open(OG_IMAGE_PATH)
-    # Crop the tiles+globe band (y=185..335 in the 1200x630 original)
-    strip = og.crop((0, 185, 1200, 335))
+    if not os.path.exists(OG_IMAGE_PATH):
+        raise FileNotFoundError(f"Required image not found: {OG_IMAGE_PATH}")
+    with Image.open(OG_IMAGE_PATH) as og:
+        # Crop the tiles+globe band (y=185..335 in the 1200x630 original)
+        strip = og.crop((0, 185, 1200, 335))
     HEADER_STRIP = strip.resize(
         (int(strip.width * HEADER_HEIGHT / strip.height), HEADER_HEIGHT),
         Image.LANCZOS,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate_share_images.py` around lines 64 - 73, The load_header()
function currently opens OG_IMAGE_PATH without a context manager and doesn't
handle missing files; change Image.open(OG_IMAGE_PATH) to use a with-statement
(e.g., with Image.open(OG_IMAGE_PATH) as og:) so the file handle is closed
safely, and wrap that block in a try/except catching FileNotFoundError/OSError
to raise or log a clear error that mentions OG_IMAGE_PATH before attempting to
crop/resize and assign HEADER_STRIP.

Comment on lines +159 to +168
for size in (44, 38, 32, 26):
font_main = get_font(font_reg, size)
font_cta = get_font(font_bold, size + 4)
lines = wrap_text(display_text, font_main, draw, max_w)
if len(lines) <= 2:
break

if len(lines) >= 2:
line1 = lines[0]
line2 = " ".join(lines[1:])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Two-line cap is not guaranteed at minimum font size.

If text still wraps to >2 lines at size 26, the current branch joins all remaining lines into line2, which can overflow the width and break the layout objective.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate_share_images.py` around lines 159 - 168, The loop that
reduces font size using get_font and wrap_text may still yield >2 lines at the
minimum size (size 26), but the code then joins all remaining lines into line2
which can overflow; change the post-loop handling in the block that uses lines
to enforce a hard two-line cap: keep line1 = lines[0], construct line2 from the
remaining text but truncate it with an ellipsis so its rendered width (measured
with draw.textlength or font_cta metrics) does not exceed max_w, ensuring you
measure with the chosen font (font_main/font_cta) and trim words/characters
until the width fits; update the logic around variables lines, line1, line2 and
reuse wrap_text/measurement utilities rather than allowing an unbounded join.

Comment on lines +170 to +181
draw.text(((WIDTH - (bbox1[2] - bbox1[0])) // 2, 430), line1, fill=WHITE, font=font_main)
bbox2 = draw.textbbox((0, 0), line2, font=font_cta)
draw.text(
((WIDTH - (bbox2[2] - bbox2[0])) // 2, 430 + size + 12),
line2,
fill=GREEN,
font=font_cta,
)
else:
line = lines[0]
bbox = draw.textbbox((0, 0), line, font=font_cta)
draw.text(((WIDTH - (bbox[2] - bbox[0])) // 2, 460), line, fill=GREEN, font=font_cta)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Run Ruff formatting to enforce max line length.

A few updated draw.text(...) lines exceed the 100-character limit.

As per coding guidelines, "Use Ruff formatter and linter with 100 character line length for Python code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate_share_images.py` around lines 170 - 181, Several
draw.text(...) calls (around the block using bbox1, bbox2, WIDTH, font_main,
font_cta and the else branch using bbox) exceed the 100-char line length; run
the Ruff formatter (or manually wrap the long draw.text argument lists) so each
line is <=100 characters, breaking long argument lists across multiple lines and
aligning subsequent args, and reformat the tuple calculations ((WIDTH - (bbox[2]
- bbox[0])) // 2, ...) and the draw.text(...) calls for both the two-line branch
(line1/line2 using bbox1/bbox2) and the single-line else branch (line using
bbox) to satisfy the linter.

@Hugo0
Copy link
Owner Author

Hugo0 commented Mar 14, 2026

Superseded by #146 which was merged first with the same changes.

@Hugo0 Hugo0 closed this Mar 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant