fix(pdf): draw tracked text with the spacing in the glyph widths - #688
Merged
Merged
Conversation
A tracked run drawn with Tc places every glyph correctly, but the added space sits between the glyph boxes a reader derives from the font's widths. Readers that ignore ActualText build words from those boxes, so pdf.js (past about a tenth of the font size) and pdfplumber (past three points) read a tracked heading as single letters. Across the CV presets that track their names and headings, three resume parsers built on those readers found 100 split words in 12 presets; drawn this way they find none. A positively tracked run now draws with a second Type 0 resource over the same embedded font program. It shares its base's FontFile2, ToUnicode, CIDToGIDMap and descriptor by reference and raises every width by the tracking, in whole thousandths of an em because PDFium truncates fractional CID widths; the remainder, at most half a thousandth of the font size, stays in Tc. Glyphs land where Tc put them, each box reaches the next, and no font program is duplicated or modified. - PdfTrackedFontResources hands out one resource per face and tracking per em for the whole document, across pages and sections, and decides what qualifies: a subset-embedded horizontal Type 0 face, tracking of at least half a thousandth of an em, and text the face's GSUB substitutions leave alone. Negative tracking, Standard 14 faces, text drawn in visual order and substituted text keep Tc and ActualText. - PdfTrackedFontView is that resource. Its widths are completed after a first save has built the subsets, so a document carrying one saves twice (PdfSubsetAwareSave), with protection applied on the real save. - Paragraph runs, highlight chips and table cells draw through it. A table cell decides once for all its lines, and a line drawn in visual order goes back to Tc. - PdfRenderEnvironment.letterSpacedFont and its LetterSpacedFont record are the seam a custom handler uses, marked @beta: what they draw is settled, the shape of the call may still move. api-stability.md names them and the knowledge pack is regenerated. PdfLetterSpacedFontTest holds glyph origins to the Tc picture to 0.01pt and reader gaps to 0.005pt in paragraphs, table cells and chips, and pins resource sharing, protection and sections; PdfLetterSpacedFontFallbackTest pins every run that keeps Tc. Ten sabotages of the real sources each turn the tests that describe them red. The qa tracking tests read a PDF's tracking as the width raise plus Tc. render-pdf (274), templates (127) and qa (1759) pass.
The committed previews that draw positively tracked text in an embedded face now carry letter-spaced font resources, and these 31 no longer matched what their examples render. Against the files they replace, every page matches in PDFium at 100 dpi with no pixel changed, page and glyph counts and the extracted text are unchanged, and no font program is added: the new resources reuse the programs already embedded. The files grow by 0.1% to 2.9%. CommittedAssetDriftTest and ExampleContentGuardTest pass.
| * {@code c [w1 ... wn]} and the {@code cFirst cLast w} forms. The widths stay integers: | ||
| * PDFium reads CID widths as integers, so a fractional width would move glyphs there only. | ||
| */ | ||
| private COSArray widened(COSArray widths) { |
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.
Why tracked text read as single letters
Letter spacing (#676) reaches the PDF as
Tcon a run wrapped inActualTextmarked content.Tcmoves the pen after each glyph, but each glyph's width in the font's/Warray stays the program's own width. So the glyph box an extractor derives from/Wstops short of the next glyph by the whole tracking.ActualText(PDFBox, Acrobat) get the right text, which is why no PDFBox-based test ever showed the problem.JANE DOEcomes out asJ A N E D O E.Tracked caps set the name and section headings of most CV presets. One CV was rendered through the 24 CV presets that can lay it out, and the PDFs were read by three resume parsers:
On develop the three together found 100 words split into letters, across 12 presets. Six of those presets had their name found by only 1 of the 3 parsers.
What changed
A positively tracked run now draws with a second font resource whose glyph widths carry the tracking, over the same embedded font program.
PdfTrackedFontViewis that resource.FontFile2,ToUnicode,CIDToGIDMapandFontDescriptorby reference, and writes/Wand/DWwith every width raised by the tracking. Each glyph lands whereTcput it, and its box reaches the next glyph.Tc.PdfTrackedFontResourceshands out one resource per face and tracking per em, reused across the document's pages and sections. A run qualifies when:PDFBox applies a face's substitutions when it draws, but a letter-spaced resource encodes through the character map. So each run is checked with PDFBox's own GSUB worker, split on whitespace the way PDFBox splits it. Poppins keeps its Devanagari substitutions: a Latin heading qualifies, a Devanagari conjunct does not.
Fallbacks keep develop's
Tc+ActualTextunchanged:PdfSubsetAwareSave: documents with a letter-spaced resource save twice.PdfShapedGlyphUnicode, whose reordered-text correction runs at the same point.Handlers.
PdfParagraphFragmentRenderHandler(lines and highlight chips) andPdfTableRowFragmentRenderHandlerdraw through the new resources.Tf/rg/Tcorder and leaves untracked cells byte-identical.Tc, and a single line the face cannot serve keeps the whole cell onTc.Unchanged: tracked runs still carry
ActualText, layout measurement is untouched (only the PDF representation of the same advance changes), and DOCX and PPTX are unchanged.PDF/A and PDF/UA. The raised widths differ from the program's own advances on purpose. ISO 32000 allows that; PDF/A and PDF/UA do not. The backend has no output mode that claims either, and one added later must switch these resources off. The CHANGELOG says so.
@Betaon the new seamPdfRenderEnvironment.letterSpacedFont(font, fontSize, letterSpacing, text)and its result recordPdfRenderEnvironment.LetterSpacedFontlet a custom paragraph or table handler draw tracked text the same way.@Beta, asPdfFixedLayoutBackend.renderSections/writeSectionsandBuilder.deterministicalready do.docs/api-stability.mdnames them, and the knowledge pack is regenerated (onlybackends.jsonandbackends.mdchange).Before → after
Develop
88eb90edagainst this branch, on the same CV and the same 24 presets:ProfessionalSummary,ARTEMDEMCHYSHYN), not letters.Tcruns its own way; there PDFium's glyph boxes agree within 4.6e-5 pt.Committed previews
31 previews under
assets/readme/examplesdraw positively tracked text in an embedded face, and they are re-rendered here: 8 cover letters, 18 CVs, 2 invoices, 2 proposals andletter-spacing.receipt-moderndoes not move, because its tracked text is Standard 14 Helvetica, which keepsTc.Known limits and follow-up
No CV template is certified here. This PR changes how the PDF backend represents tracked text; it does not make any CV template ATS-certified. On the branch, the parsers still miss some fields on the same CV, and those misses come from:
None of them comes from the renderer. Certifying and tuning the CV templates is follow-up work.
Trailing tracking unit. The last tracking unit of a word now sits inside its last glyph box, so the gap from that box to whatever follows is one tracking unit narrower than under
Tc. The space glyph is drawn either way.B A C K E N D E N G I N E E R I N G); the branch reads the words, still without that gap (BACKENDENGINEERING).Right-to-left tracked text still draws with
Tc, so pdf.js still splits it.PDFBox upgrades. The substitution check follows PDFBox 3.0.8's GSUB word split, so a PDFBox upgrade should re-run
PdfLetterSpacedFontFallbackTest.Verification
./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose,:graph-compose-bundle,:graph-compose-qa,:graph-compose-coverage -am(CI's build-and-test command) → BUILD SUCCESS../mvnw -B -ntp install javadoc:javadoc -pl :graph-compose-render-pdf→ BUILD SUCCESS. Javadoc's 5 warnings are all inPdfFont.java, which this branch does not touch../mvnw -B -ntp -f examples/pom.xml test→ BUILD SUCCESS, 77 tests, includingCommittedAssetDriftTestandExampleContentGuardTeston the re-rendered previews. Before the re-render, the drift test named exactly these 31 files../mvnw -B -ntp -f benchmarks/pom.xml test→ BUILD SUCCESS, 44 deterministic benchmark gates, including the exact render-operator counts.extract-api.mjs --from-reactor --checkreports the surfaces current;check-stability-doc.mjsconfirms every@Betais named (7 members);check-claims.mjs --check(46 claims, 10 proofs),check-routes.mjsand the five fixture suites pass.New tests
PdfLetterSpacedFontTest(12):Tc. Cases cover 0.18 em, 0.02 em and point tracking at 10, 14 and 24 pt, trailing advance included.Tcreference leaves the full 1.73 pt.ToUnicodeandCIDToGIDMap, with every width raised.PdfLetterSpacedFontFallbackTest(10):Tcand draws no letter-spaced resource: negative tracking, a Standard 14 face (which keepsActualText), a right-to-left run, a bidi chip, tracking below 0.0005 em, and a Devanagari run that Poppins' substitutions rewrite.Tc, and a reordered line switches back inside a letter-spaced cell.LetterSpacedPdfrecognises a letter-spaced resource by what it states: widths above those its own embedded program gives the same glyphs. It does not look for any key the backend writes, so a fallback assertion cannot pass by accident.LetterSpacingAcrossBackendsTestandTrackingFixedLayoutParityTestread the tracking a PDF declares as the width raise over its program plus theTcin force (PdfDeclaredTracking), instead of readingTcalone.Sabotage. Each change was made to the real sources, the tests were run, and the sources were restored:
TcTcTcover widened widthsTcTcover widened widthsTcdroppedWith the sources restored, everything is green again.
Lane: shared-engine — PDF backend (
render-pdf) only; DOCX, PPTX and layout measurement unchanged.Pre-merge checklist
develop; branch isfix/pdf-letter-spacing-parser-compat../mvnw -B -ntp clean verifymodule list passes locally — the Verification proof above.CHANGELOG.mdentries under## v2.4.0 — Planned(Public API and Tests);docs/api-stability.mdand the knowledge pack updated.