Skip to content

fix(og): surface and prevent the missing libraqm text shaping - #10813

Merged
MarkusNeusinger merged 3 commits into
mainfrom
claude/anyplot-og-image-monalisa-oaf8zz
Aug 30, 2026
Merged

fix(og): surface and prevent the missing libraqm text shaping#10813
MarkusNeusinger merged 3 commits into
mainfrom
claude/anyplot-og-image-monalisa-oaf8zz

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Summary

  • The live OG card at api.anyplot.ai/og/home.png is pixel-identical to a local render forced onto Pillow's BASIC layout engine (0 differing pixels; against the normal RAQM render it differs on every text line). The deployed container's Pillow therefore has no libraqm: the ss02 stylistic set that turns MonoLisa Italic into its script forms is dropped from the — any library. line, and no text is kerned at all — that part also affects the spec and collage cards, which request no features. MonoLisa itself loads fine; both TTFs in gs://anyplot-static/fonts/ return 200 and the wordmark metrics match MonoLisa, not the DejaVu fallback.
  • _draw_text_with_features caught Exception and silently redrew plain text, so the endpoint kept returning a valid 200 PNG and nothing reached the logs — the only symptom was an off-brand image. The fallback now catches the exceptions Pillow actually raises, logs them, and warns once per process (not per text run) when shaping is missing entirely.
  • Adds two guards so this cannot ship unnoticed again: api/Dockerfile fails the build when the installed Pillow lacks libraqm, and the API logs the capability at startup via the new core.images.has_text_shaping().

Note: this makes the regression visible and prevents a future build from reintroducing it — it does not by itself repaint the currently deployed revision. The same lock installs a Pillow 12.3.0 manylinux wheel that does bundle libraqm (verified locally: features.check('raqm') → True), so the running image is most likely older than the current lock; the next API deploy should restore the swashes, and the new startup log line says whether it did.

Test plan

  • /verify-corepytest tests/unit/api tests/unit/core → 1005 passed; ruff check . clean; mypy core api clean
  • New TestTextShaping covers: feature pass-through when shaping is available, single warning + plain fallback when it is not, fallback when the font rejects the feature, and a render comparison proving the home card differs with vs. without ss02 (skipped where MonoLisa is not cached locally, since the DejaVu fallback has no ss02)
  • Dockerfile guard executed against the project venv — passes on a libraqm build, and the assert message names the remedy
  • Reproduced the production diff: downloaded api.anyplot.ai/og/home.png and compared it against create_home_og_image() under RAQM (differs) and under BASIC (identical), which is what identifies the root cause
  • Not verified: the actual Cloud Build of api/Dockerfile (no deploy from this session)

Checklist

  • CHANGELOG.md updated under [Unreleased] (### Fixed, bold-titled bullet, PR ref)
  • Related documentation updated — docs/reference/seo.md gains a "Text shaping (libraqm)" section under Branded OG images, plus the italic font variant in the font list

Generated by Claude Code

The live OG card at api.anyplot.ai/og/home.png is pixel-identical to a
render forced onto Pillow's BASIC layout engine, i.e. the deployed
container's Pillow has no libraqm. Two consequences: the `ss02` stylistic
set that turns MonoLisa Italic into its script forms is dropped from the
`— any library.` line, and no text is kerned at all — including on the
spec and collage cards, which use no features.

`_draw_text_with_features` caught every exception and redrew plain text,
so the endpoint kept returning a valid 200 PNG and nothing reached the
logs. The only symptom was an off-brand image.

- Narrow the fallback to the exceptions Pillow actually raises and log it;
  the missing-shaping case warns once per process instead of per text run.
- Expose `has_text_shaping()` and log the capability at API startup.
- Fail the API image build when Pillow lands without libraqm, so a wheel
  that loses it cannot ship silently.
- Cover both paths in tests, including a render comparison that proves the
  swashes land when the feature is applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLkhDnKHdaqxT5pMwC38GX
Copilot AI lite review requested due to automatic review settings August 30, 2026 20:50
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLkhDnKHdaqxT5pMwC38GX

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new “swashes land” test currently compares PNG bytes rather than asserting a pixel-level diff, which can allow false positives and weakens the regression guard this PR is meant to provide.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR prevents and surfaces a production regression where OG cards were rendered without kerning and without MonoLisa Italic ss02 swashes due to Pillow missing libraqm, by adding capability detection/logging and a Docker build-time guard.

Changes:

  • Add core.images.has_text_shaping() plus a logged, once-per-process warning path in _draw_text_with_features() when shaping/features are unavailable.
  • Log text shaping capability at API startup and fail the API Docker build if Pillow lacks libraqm.
  • Add unit tests and documentation/changelog entries describing the shaping requirement and safeguards.
File summaries
File Description
core/images.py Adds libraqm capability detection and non-silent fallback behavior for OpenType feature rendering.
api/main.py Logs shaping capability once during app startup for fast diagnosis in Cloud Run logs.
api/Dockerfile Adds a build-time assertion to prevent shipping an image where Pillow lacks libraqm.
tests/unit/core/test_images.py Adds tests covering shaping detection, warning behavior, and home OG rendering differences.
docs/reference/seo.md Documents the libraqm dependency and how to validate it.
CHANGELOG.md Records the production OG-card regression and the safeguards added in this PR.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/unit/core/test_images.py Outdated
@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
api/main.py 25.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot review: comparing encoded PNG bytes would pass on an encoder
change even if the swashes were gone. Decode both renders and require a
non-empty ImageChops difference, anchored below the eyebrow row so the
diff has to sit in the tagline block the feature actually touches.

Verified: diff box (308, 341, 953, 430) with ss02 applied, and None when
both renders drop the feature — so the assertion fails on the regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLkhDnKHdaqxT5pMwC38GX
Copilot AI review requested due to automatic review settings August 30, 2026 20:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The updated SEO documentation slightly overstates the “warn once” behavior and should be aligned with the actual logging semantics implemented in core.images.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

docs/reference/seo.md:345

  • The docs say core.images “logs a warning the first time a feature run is dropped”, but the code only warns once-per-process when text shaping (libraqm) is missing; when Pillow rejects a requested feature for a specific font, it logs a warning on each occurrence. Tighten the wording so it matches the actual behavior (and keeps the focus on the missing-libraqm guard).
- `api/Dockerfile` fails the build when the installed Pillow lacks libraqm.
- The API logs the capability at startup (`Text shaping (libraqm): available`,
  or an error line when it is missing), and `core.images` logs a warning the
  first time a feature run is dropped.
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@MarkusNeusinger
MarkusNeusinger merged commit 9464278 into main Aug 30, 2026
14 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the claude/anyplot-og-image-monalisa-oaf8zz branch August 30, 2026 21:31
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.

3 participants