Repository navigation
Render CJK fonts that a document does not embed with fallback faces (#600) - #613
Merged
Merged
Conversation
…600) hayro asks its font resolver for a font that a document does not embed, and its default resolver answers with a Standard 14 font, which has no CJK glyphs, so such text rendered blank. A resolver ported from pylopdf 4ff4a266 (rust/src/document/fonts.rs) now answers first: the language comes from the character collection (Adobe-Japan1, Korea1, GB1, CNS1), or from the BaseFont name for Adobe-Identity, and a name that marks a serif face, such as a Mincho, takes the serif face. - pyropdf-engine: render::Fallback holds a sans and a serif face per language, a loaded face or a font file read the first time that a page needs it. Every rendering takes it (raster, batch, svg), and the thread's render cache is keyed by its faces as well as the bridge (DESIGN.md section 5). - pyropdf-python: each _Document holds its faces, which set_fallback_faces replaces; page and annotation pixmaps, batches, and SVG render with them. - Python: FallbackFont and doc.fallback_fonts, a mapping by language that holds the faces of the installed pylopdf-fonts-* packages until it is edited. The jp, ko, zh-cn, and zh-tw extras install them, and the dev group installs them for the tests. - The differential harness renders tests/assets/text/nonembedded-cjk.pdf with the extras' faces in both engines, and the corpus without. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run.py now builds frozen pylopdf with its CJK font extras, dumps the corpus without fallback fonts and tests/assets/text/nonembedded-cjk.pdf with them, and measures pdfium-ocg-invisible.pdf too. All 163 rasters match byte for byte. The run also finds what changed since 2026-10-28: the highlight of pdfium-links-highlights-annots now renders alike (#172), and page 2 of patent-us223898 keeps its headings whole (#460). compare.py reads the text of a layout's spans from their characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ield Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 tasks
render::raster and render::svg take the faces that draw CJK fonts that a document does not embed; the fuzz targets pass none, which renders as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck-faces Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since hayro 0.9's patch (#609), which only the root and fuzz manifests repeat, the probe no longer builds whatever it passes render::raster, and an evidence crate records what its measurement ran. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 tasks
Frozen pylopdf renders on hayro 0.8, and pyropdf now on hayro 0.9, whose glyph cache moves filled text by less than a pixel on 152 of the 163 rasters. With the cache off, 162 match byte for byte, so the report names it as their cause, and #622 asks the owner whether 0.2.2 keeps it. The reading outputs stay as they were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 4, 2026
This was referenced Nov 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #600
Text in a CJK font that a PDF does not embed rendered blank: hayro asks its font resolver for such a font, and the default resolver answers with a Standard 14 font, which has no CJK glyphs. This adds a resolver for those queries, ported from pylopdf
4ff4a266(v0.16.0):pick_cjk_fallback, the name hints, andinterpreter_settingsofrust/src/document/,_FONT_PROVIDER_SPECSand_bundled_font_providersofsrc/pylopdf/_fonts.py, and theFallbackFontAPI ofsrc/pylopdf/_fallback_fonts.py, with the rendering cases oftests/test_locale_fonts.py. These files are the same at the freeze commitebb01d3c.What changed
crates/pyropdf-engine/src/render/fallback.rs):Fallbackholds a sans and a serif face for each ofja,ko,zh-CN, andzh-TW. Its resolver takes the language from the character collection (Adobe-Japan1, Korea1, GB1, CNS1), or for Adobe-Identity from the BaseFont name, and the serif face for a serif name such as a Mincho; every other query goes to hayro's default resolver. A face is a loadedFontor an installed font file, which is read once, the first time that a page needs it.raster,batch, andsvgtake the faces, and the thread's render cache is keyed by them as well as by the bridge (DESIGN.md §5)._Documentholds its faces (set_fallback_faces), andPage.pixmap,Document.render,Page.svg, andAnnotation.pixmaprender with them. Unlocking keeps them.pyropdf.FallbackFont(sans=..., serif=...)anddoc.fallback_fonts, a mapping by language that holds the faces of the installedpylopdf-fonts-*packages until it is edited;{}renders without fallback fonts, anddelreturns to the installed extras. They are not saved, and text extraction does not use them (out of Render text in non-embedded CJK fonts with fallback faces #600's scope).jp,ko,zh-cn, andzh-twinstallpylopdf-fonts-*>=0.1.1,<0.2; the dev group installs them for the tests.Acceptance
pylopdf-fonts-jpinstalled, the page above draws its three glyphs inpixmap()andsvg(), and a Mincho BaseFont picks the serif face:test_the_installed_japanese_faces_draw_the_page(both descendant types and both CMaps, with no empty glyph outline; the page draws 1,441 dark pixels through hayro 0.9's glyph cache, and without it the 1,437 of the issue's pylopdf 0.16.0 withjp) andtest_a_mincho_font_takes_the_serif_face.test_each_collection_takes_the_faces_of_its_language, which also checks that the other languages' faces never draw them.test_without_installed_faces_the_text_renders_blank;docs/en/rendering.md#fonts-that-a-document-does-not-embed, its Japanese page, and Getting started.tests/assets/text/nonembedded-cjk.pdf, the three pages above, MS-Mincho, and a GB1, CNS1, and Korea1 page, inevidence/phase1-parityandtests/test_parity.py. Without hayro 0.9's glyph cache (Decision: hayro 0.9's glyph cache changes 152 of 163 corpus renders; keep it or turn it off for 0.2.2 #622), all 7 render byte for byte as frozen pylopdf with its CJK extras renders them; through it, 4 are close under the harness's tolerance and 3 differ in 0.6 to 0.8% of their pixels, as the corpus's other text pages do.Evidence
evidence/phase1-parityran again on 2026-11-03 atd2715c8, this branch merged withmainon hayro 0.9 (Windows, CPython 3.14.8), with frozen pylopdfebb01d3con hayro 0.8, built with[jp,ko,zh-cn,zh-tw]; its font files have the same SHA-256 as those of the PyPI packages that pyropdf's dev group installs.Renders changed with #609, apart from this PR. Of the 163 rasters, 11 match byte for byte, 7 are close under the tolerance, and 145 are not. The cause is
RenderSettings::glyph_caching, on by default in hayro 0.9, which #609 leaves on when it turns hinting off: withglyph_caching: false, measured and not committed, 162 match byte for byte and the last differs in 495 pixels by at most 5. Before the merge, on hayro 0.8, all 163 matched (e5b0d10). The owner chose on #18 that 0.2.2 changes no render, so #622 asks whether to keep the cache, which makes rendering about 1.8 times as fast. This PR does not change it, and records it as the causehayro-glyph-cache; whichever of this PR and #622's change lands second measures again.The run also records what changed since the run of 2026-10-28:
fallback-exact-boxes:words(exact_bboxes=True)differs on pages 2 to 7 of the fixture, by up to 7.28 points, because pylopdf's extraction measures the outlines of its fallback faces and pyropdf's extraction does not use them. The owner chose to leave this out of 0.2.2 and of this PR; Measure the exact boxes of non-embedded CJK fonts on their fallback faces #624 follows it.heading-above-columns: page 2 ofpatent-us223898.pdfkeeps its headings whole since Headings above two columns are split into the columns in page.text() #460, which landed after the previous run.pdfium-links-highlights-annots.pdfrenders alike since Draw annotations stored without an appearance at render time (#169) #172, so its cause is retired; without the glyph cache, the page matches byte for byte.pdfium-ocg-invisible.pdf, which was added after the previous run, is measured now, and matches on every output.compare.pyread a span's text fromtextonly; the spans oflayout(chars=True)hold characters, which it now joins. No difference had reached that path before.PYROPDF_PARITY_PYLOPDF=target/parity/pylopdf-env/Scripts/python.exe uv run pytest tests/test_parity.pypasses, with the redump of the fallback pages.Notes for review
pyropdf[ocr]setsOcrEngine()'s models. The owner approved the wording on Render text in non-embedded CJK fonts with fallback faces #600; it is PR DESIGN.md: output depends on the installed CJK font extras (#625) #626 (DESIGN.md: output depends on the installed CJK font extras #625), which merges with or after this PR, and this PR does not change DESIGN.md.main(hayro 0.9, Move to hayro 0.9 with LaurenzV/hayro#1734 patched in, and read streams that name a file #609): the conflicts were theInterpreterSettingsliterals ofrender/mod.rsandrender/svg.rs, which takerender_contentfrom Move to hayro 0.9 with LaurenzV/hayro#1734 patched in, and read streams that name a file #609 and..fallback.settings()from this PR, and the dev group's entry inuv.lock, whichuv lockleaves as resolved. The resolver's types have the same paths in hayro 0.9 (hayro_interpret::font::{FallbackFontQuery, FontData, FontQuery}andhayro_cmap::CidFamily). hayro 0.9 also caches resolved programs by query in itsRenderCache, which the thread's cache, keyed by the faces, already keeps apart.PyroOCR-Gothic, which the name hints read as Japanese, so withjpinstalled, rendering a page with an OCR layer reads the Japanese sans face once per process; the layer stays invisible.doc.fallback_fontsor no font extras (test_search.py,test_ocr_layer.py,test_acroform.py,test_canvas_text.py); they now say what is true.tests/corpus.py's comment onADDEDno longer names the parity evidence, which measures that file now.Checks
python tools/check.py --lockedon Windows 11 with CPython 3.14.8, at0bab51a, this branch merged withmainat614fb83: sync, pytest (6,613 passed, 691 skipped), ruff, ruff format, ty, cargo fmt, clippy, and cargo test all passed.tests/test_parity.pypasses withPYROPDF_PARITY_PYLOPDFset, which redumps the fallback pages against frozen pylopdf. The audit tools were not run: this PR changes neitherCargo.locknor the workflows.CI's Fuzz bridge job failed at
fbbc204: the fuzz targets, which are not members of the workspace thattools/check.pychecks, calledrender::rasterwithout its newfallbackargument.c0ac774passes no faces in thebridgeandinterprettargets, andcargo check --manifest-path fuzz/Cargo.toml --binsbuilds them all after the merge. Theevidence/crates stay as they were measured: since #609, whose patch table only the root andfuzz/manifests repeat,evidence/edit-render/splitdoes not build whatever it passes.Ready for the owner's review. It has no merge authorization.
🤖 Generated with Claude Code