Skip to content

PDFs: Switch to DomPDF & use Noto Sans (plus KR, SC, and Arabic) for Invoices & Docs. - #1552

Merged
bor0 merged 23 commits into
productionfrom
utf8-invoices
Oct 15, 2026
Merged

bor0 merged 23 commits into
productionfrom
utf8-invoices

Conversation

@dd32

@dd32 dd32 commented Oct 31, 2025

Copy link
Copy Markdown
Member

Fixes #1433

It seems that the only thing missing on Invoices to make non-western characters work is that a) The system doesn't have any chinese/korean/japanese fonts installed and b) isn't specifying a webfont that supports these laguages.

Screenshots

Attempting to render #1433 (comment)

Before After
Screenshot 2025-10-31 at 5 09 18 pm Screenshot 2025-10-31 at 5 01 08 pm

The Simplified Chinese still has a few missing characters, but much better than before. I'm not sure why these are missing, as the font appears to support those unicode characters.. It may be something in how I'm testing it.

@dd32 dd32 added [Type] Bug [Component] Docs Generate official docs for WordCamps (visa letters, sponsorship agreements) [Component] CampTix Attendee Invoices labels Oct 31, 2025
@dd32

dd32 commented Oct 31, 2025

Copy link
Copy Markdown
Member Author

Noting that there's also Noto Sans Arabic. Perhaps we should also switch to Noto Sans for western characters too..

@dd32

dd32 commented Oct 31, 2025 •

Copy link
Copy Markdown
Member Author

Maybe Arabic works fine as-is, with this PR: (Those chinese characters are still annoying me, and that's why this isn't being merged at 6pm on a Friday)

Screenshot 2025-10-31 at 5 31 37 pm

<meta charset="UTF-8">
<link href="http://fonts.googleapis.com/css?family=Open+Sans:300,600,700" rel="stylesheet" type="text/css" />
<?php // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- This is rendered by wkhtmltopdf, so additional libraries are included directly. ?>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300..700&family=Noto+Sans+KR:wght@300..700&family=Noto+Sans+SC:wght@300..700" rel="stylesheet" type="text/css" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@dd32

Thanks for your PR to resolve my issue!

How about swap font resolve order?

/* Current... */
font-family: 'Noto Sans KR', 'Noto Sans SC', 'Open Sans', sans-serif;

/* to this */
font-family: 'Noto Sans SC', 'Noto Sans KR', 'Open Sans', sans-serif;

KR might use the same glyph points as of Chinese.

Note: I cannot reproduce same character missing(we call it "TOFU").

@ryelle

ryelle commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

It looks like this isn't loading Open Sans at all now, because the Notos have latin characters, so it never falls back to Open Sans. I'd recommend updating the font stack to 'Noto Sans', 'Noto Sans SC', 'Noto Sans KR', sans-serif.

… Actually, that won't work either. I don't think wkhtmltopdf supports multiple fonts. If you do that, everything is "tofu". The tofu in Simplified Chinese goes away if you put Noto Sans SC first, but then Korean breaks. So it appears to only load the first font.

Noto Sans first Noto Sans SC first Noto Sans KR first
Screenshot 2025-10-31 at 11 04 48 AM Screenshot 2025-10-31 at 11 04 56 AM Screenshot 2025-10-31 at 11 06 28 AM

Maybe switching to something like dompdf would fix that, they have addressed CJK font support in this issue.

@fumikito

fumikito commented Nov 1, 2025 •

Copy link
Copy Markdown

I've tried on my local machine, but because of network error, wkhtmltopdf failed to download google fonts.

QSslSocket: cannot resolve SSL_load_error_strings
Exit with code 1 due to network error: UnknownNetworkError

I also tried another approach — installing fonts-noto-cjk inside the Docker container — and that resolved the garbled character issue.

sudo apt install fonts-noto-cjk
スクリーンショット 2025-11-01 17 11 30

The complete diff of docker file is below:

before__Dockerfile.php-fpm | Dockerfile.php-fpm – 2 Additions.pdf

@dd32

dd32 commented Nov 4, 2025

Copy link
Copy Markdown
Member Author

It looks like this isn't loading Open Sans at all now, because the Notos have latin characters,

I thought that might be the case...

… Actually, that won't work either. I don't think wkhtmltopdf supports multiple fonts.

This too, except that as far as I could tell it definitely does because KR doesn't work without the KR font, and Chinese didn't work without the SC font, but with both they did load...

Maybe switching to something like dompdf would fix that,

I did consider that, but I was hopeful that a quick font-change here would suffice :)

The complete diff of docker file is below:

@fumikito Unfortunately we don't use the included Dockerfile in production, so it's a little more involved than just the change here :(
While I can get systems alterations made, I was thinking if that's the case, might be worth moving to one of the PHP-based projects linked above instead.

@ryelle

ryelle commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

This too, except that as far as I could tell it definitely does because KR doesn't work without the KR font, and Chinese didn't work without the SC font, but with both they did load...

Both SC and KR fonts have Latin characters included, and KR probably has a decent set of glyphs for Chinese characters (they're used in Korean, but rarely). That's why most of the SC text loads when KR font is first. But the SC font, while it includes Latin, does not include the Hangul (Korean characters) — so the KR text does not load.

This explains the random "tofu" in SC — this is a screenshot of the KR font, and the characters in blue are Noto Sans KR, the rest are system fallbacks (not included in the font).

Screenshot 2025-11-03 at 8 33 01 PM

(I'm not sure why 区 works when it shouldn't, maybe a font version difference)

@dd32

dd32 commented Nov 4, 2025

Copy link
Copy Markdown
Member Author

🤷 I guess I was very wrong, or improperly tested :)

Let's try dompdf and if that doesn't work maybe we can look at these alternatives..

@ryelle

ryelle commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

After the subsetting work, I'm pretty familiar with these fonts 😅

@dd32 dd32 changed the title Load Noto Sans KR and Noto Sans SC for Invoices & Docs. PDFs: Switch to DomPDF & use Noto Sans (plus KR, SC, and Arabic) for Invoices & Docs. Nov 4, 2025
@dd32 dd32 changed the title PDFs: Switch to DomPDF & use Noto Sans (plus KR, SC, and Arabic) for Invoices & Docs. PDFs: Switch to DomPDF & use Noto Sans (plus KR, SC, JP, and Arabic) for Invoices & Docs. Nov 4, 2025
@dd32 dd32 changed the title PDFs: Switch to DomPDF & use Noto Sans (plus KR, SC, JP, and Arabic) for Invoices & Docs. PDFs: Switch to DomPDF & use Noto Sans (plus KR, SC, and Arabic) for Invoices & Docs. Nov 4, 2025
@dd32

dd32 commented Nov 4, 2025

Copy link
Copy Markdown
Member Author

With the PR as it is...

Screenshot 2025-11-04 at 3 48 44 pm

I've found that with DomPDF anything non-ascii really needs a font loaded, as the default font will ? everything non-western-latin.

DomPDF does bundle the DejaVu Sans font, but that didn't seem to provide any benefits here.

Initially I also had the JP and Devanagari fonts loaded, but I realised I had done so incorrectly.. Those fonts might still be worth loading.

DomPDF relies upon a local font-cache path, webfonts aren't being fetched on every PDF render, only the first one.. But as a downside, those fonts are cached locally even if it's not used in the document.

@dd32

dd32 commented Nov 17, 2025

Copy link
Copy Markdown
Member Author

Took forever to figure out why this wasn't working with invoices.. Turns out that if an embedded image is 404 / broken, then DomPdf seems to enter an endless forever loop. It seems to be that the broken-image fallback is well broken.

Additionally; Before merging some performance profiling should be done to figure out if this is going to be too slow..
For Docs this is fine, since it's generated interactively. For tickets, this happens during the checkout flow.

For invoices, I suspect the better solution will be to remove invoice generation from during the checkout, and moving it to be able to be downloaded from the Successful purchase landing page (ala #1555)

bor0 and others added 2 commits October 13, 2026 11:35
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… invoice layout

Follow-up to the DomPDF switch, from rendering real invoices and every WordCamp Docs
template with DomPDF 3.1.6.

- Arabic came out as disconnected letters in reverse order: DomPDF draws characters
  one at a time, left to right. A small shaper now swaps in the presentation form for
  each letter's position, joins lam-alef, and reverses each Arabic run into visual order.
  Persian letters too. ar-php does the same but requires ext-calendar, which our
  containers don't have.
- DomPDF runs in the web process, unlike the sandboxed wkhtmltopdf it replaces. Remote
  fetches are limited to the two Google Fonts hosts, and local reads to the working
  folder and the font cache. The invoice logo is copied in as an asset rather than
  fetched by URL, which also fixes it rendering as a broken image.
- The font cache moves from /tmp to wp-content/uploads/wordcamp-docs-fonts, and CampTix's
  daily cron warms it. A cold cache downloads about 80MB, taking ~15s, which the first
  invoice after a reboot would otherwise spend in checkout. Warm renders take ~0.4s.
- Bold (700) isn't loaded, and DomPDF falls back to Helvetica rather than the nearest
  weight, so CJK in table headers would have been blank. Bold maps to 600, and the font
  family is set on body, not just @page.
- The From/To addresses were floats in an overflow: hidden box, which DomPDF collapsed
  to nothing. They're a two-cell table now.
- A rendering exception leaves no PDF, which the invoice code already logs as a failure,
  now with the reason, instead of erroring out of checkout.

The broken-image hang reported earlier doesn't reproduce on 3.1.6 with a 404, a missing
local file, or an unresolvable host.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bor0

bor0 commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

I picked this up. I merged production in (no rebase, so your history is intact) and pushed a5ab7a0 and e00028f on top.

What I found rendering real invoices and every Docs template with DomPDF 3.1.6:

  • CJK and Korean work. Arabic didn't: DomPDF draws letters one by one, left to right, so names came out disconnected and backwards. Added a small shaper (presentation forms, lam-alef, Persian letters, runs reversed). I skipped ar-php because it requires ext-calendar.
  • The invoice logo was a broken image either way: a URL can't be fetched reliably, and a local path is outside the chroot. It's now copied in as an asset.
  • From/To addresses were invisible: floats inside overflow: hidden, which DomPDF collapses. Now a two-cell table.
  • th is bold (700), which isn't loaded, so DomPDF fell back to Helvetica. Bold now maps to 600.
  • The broken-image hang doesn't reproduce on 3.1.6 (404, missing local file, bad host).

Security: DomPDF runs as the web user, not the sandboxed wkhtmltopdf user from #1796. Remote fetches are limited to fonts.googleapis.com and fonts.gstatic.com, and local reads to the working folder and font cache. There's a test for both.

Font cache (e00028f): a cold cache downloads ~80MB (5-15s), a warm render takes ~0.5s, and CampTix's daily cron warms it. With ms-files, wp-content/uploads is on each web server's local disk, so the cron only warmed the server it ran on. The cache now lives in wp-content/blogs.dir/wordcamp-docs-fonts, which is the shared mount, so one warm-up covers every server. It sits beside the <id>/files folders, not inside one. Without ms-files it falls back to uploads, and the wordcamp_docs_font_cache_folder filter can still move it.

One thing to know: blogs.dir files are served directly, so installed-fonts.json (which lists the server paths of the font files) will be public. The fonts are Google's anyway. I think that's fine, but say if you'd rather deny the folder.

DomPDF missing (e00028f): if a deploy skips Composer, new Dompdf() was a fatal error at checkout. Now it's a logged invoice failure.

Deploy needs: the Composer lock used on deploy must include dompdf/dompdf. The lock isn't in this repo, and composer install refuses to run against a lock that doesn't have it. Also GD (DomPDF needs it for PNGs).

Testing

  • PHPUnit: full suite passes; 9 new tests in CampTix Invoices (shaper, sandbox, warm-up, layout). Each fails with its fix reverted.
  • phpcs clean on changed lines.
  • Locally: bought a ticket with an invoice requested and Arabic, Chinese and Korean name and address, then checked the PDF (logo, From/To, glyph order). Generated a visa letter from the Docs screen in the browser.
  • On a production-like sandbox: DomPDF loads, GD/mbstring/dom are there, both font hosts are reachable, cold render 5.5s, warm 0.5s, all four Noto fonts embedded. The new cache path resolves to the shared mount.
  • Without DomPDF the render returns no PDF and sets "DomPDF is not installed." instead of a fatal.

To test: composer install, then generate an invoice or a visa letter with a name like محمد أحمد / 山田太郎 / 김민준.

@obenland could you review, especially the blogs.dir location for the font cache?

With ms-files, wp-content/uploads is on each web server's local disk, so
one warm-up only covered one server. blogs.dir is the shared mount.

A deploy that skipped Composer would make every invoice a fatal error at
checkout. Now it's a logged failure instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bor0
bor0 requested a review from obenland October 13, 2026 18:41
@bor0
bor0 merged commit d0a7c2b into production Oct 15, 2026
5 checks passed
@bor0
bor0 deleted the utf8-invoices branch October 15, 2026 15:14
bor0 added a commit that referenced this pull request Oct 15, 2026
The visa letter tests assumed WordCamp_Docs_PDF_Generator is never loaded.
Since #1552 the invoice PDF tests load it, and PHPUnit includes every test
file before running any, so seven visa letter tests started generating real
PDFs and failed. Production's CI has been red since #1552 merged.

Add a ctx_vl_pdf_generator_available filter and turn it off in the suite's
bootstrap, so the tests no longer depend on which other suites ran.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bor0 added a commit that referenced this pull request Oct 15, 2026
…#2198)

* PDFs: fall back to the temp folder when the font cache isn't writable

The font cache from #1552 lives in blogs.dir. On a server that can't write
there (the sandbox's NFS mount refuses it), DomPDF failed to save the font
metrics and rendered nothing, so every invoice and Docs PDF broke. Now the
cache moves to the temp folder instead, per server and lost on reboot, but
PDFs keep working.

Also stop serve_pdf_to_browser() from reading a PDF that doesn't exist after
it has already said "File not found.", which added PHP warnings to the 404.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Visa letters: switch the PDF generator off in tests with a filter

The visa letter tests assumed WordCamp_Docs_PDF_Generator is never loaded.
Since #1552 the invoice PDF tests load it, and PHPUnit includes every test
file before running any, so seven visa letter tests started generating real
PDFs and failed. Production's CI has been red since #1552 merged.

Add a ctx_vl_pdf_generator_available filter and turn it off in the suite's
bootstrap, so the tests no longer depend on which other suites ran.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bor0

bor0 commented Oct 15, 2026

Copy link
Copy Markdown
Member

Deployed to prod, rendering seems okay

Screenshot 2026-10-15 at 17 44 07

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Component] CampTix Attendee Invoices [Component] Docs Generate official docs for WordCamps (visa letters, sponsorship agreements) [Type] Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invoice PDF drops non-western characters

4 participants