fix(render-pdf): draw Latin text as letters rather than ligatures - #576
Open
DemchaAV wants to merge 1 commit into
Open
fix(render-pdf): draw Latin text as letters rather than ligatures#576DemchaAV wants to merge 1 commit into
DemchaAV wants to merge 1 commit into
Conversation
PDFBox applies a font's GSUB substitutions itself: a PDType0Font carrying them rewrites every shown string from characters into glyph identifiers, and in most of the bundled families `ti`, `tf` and `ft` each became one glyph. The ToUnicode map is built by reading the font's character map backwards, where a ligature is reachable from no character at all, so the entry was absent and both letters were lost on extraction — `Platform` came back as `Pla orm`. The page looked right, which is why nothing showed it, and the text layer is what search, copy-and-paste, screen readers and applicant tracking systems read. A Latin face is now handed to PDFBox with nothing to substitute. That is what the engine already assumed: layout measures a string ligature-blind, so a line drawn with ligatures was narrower than the box measured for it, and the DOCX and PPTX backends never substituted. Faces whose active script is one PDFBox shapes — Devanagari, Bengali, Gujarati — keep their substitutions, because there they are how the script renders. PDFBox applies ccmp, liga and clig together with no way to keep one without the others; in the bundled families the Latin ccmp changes nothing, so only the ligature pairs move. Tests: PdfLatinLigatureTextLayerTest sweeps all 35 bundled families and CvPresetTextLayerTest all 16 CV presets, both asserting the probe words come back whole; without the fix 10 of the 16 presets go red. Full reactor gate green (762 qa tests); the layered CV and cover-letter visual baselines and the 25 drifted committed previews were re-recorded.
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
Text set in a bundled TrueType family lost letters from its text layer.
Platformextracted as
Pla orm,certificationascer fica on,retiredasre red.Nothing on the page showed it, and the text layer is what search, copy-and-paste,
screen readers and applicant tracking systems read — so a CV rendered through one of
these presets did not contain the words printed on it.
PDFBox applies a font's
GSUBsubstitutions itself:PDAbstractContentStream.setFontregisters a
GsubWorkerfor anyPDType0Fontwhose active script islatn, and fromthen on every string shown through it is rewritten from characters into glyph
identifiers. In most of the bundled families
ti,tfandfteach become one glyph.The
ToUnicodemap a subset font carries is built by reading the font's character mapbackwards, and a ligature is reachable from no character at all, so the entry is simply
absent and both letters are lost. The families whose ligatures happen to have code
points of their own (
fi,fl) survived, which is why the damage looked arbitrary.What changed
PdfFontLoaderhands a Latin face to PDFBox with nothing to substitute(
TrueTypeFont.setEnableGsub(false)), at the single seam both the render and themeasurement pipelines resolve their faces through.
That is also what the engine already assumed. Layout measures a string with
PDFont.getStringWidth, which knows nothing of ligatures, so a line drawn with them wasnarrower than the box measured for it — Lato's
tfligature is 64 font units short oft+f. The DOCX and PPTX backends never substituted either.Only Latin is silenced. PDFBox also shapes Devanagari, Bengali and Gujarati through the
same mechanism, and there the substitutions are how the script renders rather than a
flourish on top of it, so a face whose active script is one of those keeps them
(
Poppinscarries Devanagari and is pinned as the negative case).PDFBox applies
ccmp,ligaandcligtogether with no way to keep one without theothers. In the bundled families the Latin
ccmpchanges nothing: decomposed combiningsequences (
e+ U+0301) and precomposed letters are drawn byte-identically with GSUBoff — only the ligature pairs move.
Visible consequence: text in a bundled family no longer forms ligatures, so
fiandflare drawn as two letters.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-qa,:graph-compose-coverage -am— BUILD SUCCESS, 762 qa tests../mvnw -B -ntp javadoc:javadoc -pl :graph-compose-core— BUILD SUCCESS.PdfLatinLigatureTextLayerTest(render-pdf, 4 tests) renders a probe sentence throughall 35 bundled families and asserts the whole sentence comes back from
PDFTextStripper; it also pins that a Latin face reports no substitutions and that aDevanagari-carrying face keeps its own.
CvPresetTextLayerTest(qa, 16 tests) does the same through every layered CV preset.With the fix reverted, 10 of the 16 go red — the same count as the presets reported
broken.
previews the drift gate named were re-recorded; every other baseline and preview is
byte-unchanged.