[
https://issues.apache.org/jira/browse/FOP-3341?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18124651#comment-18124651
]
Jason Harrop commented on FOP-3341:
-----------------------------------
An FO, with Carlito as the distributions package it (OFL): attached
FOP-3341-language-fallback.fo and distro-fonts.xconf (adjust the font path).
{noformat}
fop -c distro-fonts.xconf -fo FOP-3341-language-fallback.fo -pdf out.pdf
pdftotext -bbox out.pdf -
{noformat}
Two blocks of {{office AVATAR}} in Carlito, 12pt, the second with
{{language="en"}}. Carlito has latn, grek and cyrl script tables and no DFLT,
and no legacy kern table.
On main at 5be8c69b6: the first block ligates (pdftotext gives {{office}},
U+FB03) and AVATAR is 37.02pt wide; the second block has no ligature
({{office}}, six letters) and AVATAR is 39.96pt, the bare advances:
language="en" matches no language system, the fallback looks for DFLT, and the
font has none, so nothing is shaped or kerned.
With the pull request's change both blocks read {{office}} and AVATAR is 37.02pt
in both.
> A font whose GSUB or GPOS has no DFLT script table silently gets no
> substitution and no kerning under a default script, with no warning
> ---------------------------------------------------------------------------------------------------------------------------------------
>
> Key: FOP-3341
> URL: https://issues.apache.org/jira/browse/FOP-3341
> Project: FOP
> Issue Type: Bug
> Components: font/opentype
> Affects Versions: 2.11
> Reporter: Jason Harrop
> Priority: Minor
> Attachments: FOP-3341-language-fallback.fo, distro-fonts.xconf
>
>
> GlyphTable.matchLookups falls back to the default script when a lookup match
> comes up empty:
> {noformat}
> if (lm.isEmpty() && !OTFScript.isDefault(script) &&
> !OTFScript.isWildCard(script)) {
> return matchLookups(OTFScript.DEFAULT, OTFLanguage.DEFAULT, feature);
> }
> {noformat}
> The fallback targets DFLT. So for a font that has no DFLT entry in its script
> list, a script of DFLT finds nothing, and the fallback looks for DFLT again
> and finds nothing again. Nothing is applied and nothing is reported. The font
> is loaded, its tables are read, performsSubstitution and performsPositioning
> both return true, and every lookup silently misses.
> Fonts without a DFLT script table are not unusual. Carlito, the
> metric-compatible Calibri substitute packaged by most Linux distributions,
> declares cyrl, grek and latn and no DFLT. DejaVu declares DFLT alongside
> them, which is why the two behave differently on the same code path and why
> this is easy to mistake for a font-file or a loader problem.
> Measured, on the same font objects with only the script argument changing:
> {noformat}
> script Carlito 1.104 DejaVuLGCSerif
> latn 'fi' -> U+FB01 'fi' -> U+FB01
> * 'fi' -> U+FB01 'fi' -> U+FB01
> DFLT 'fi' -> 'f','i' 'fi' -> U+FB01
> zyyy 'fi' -> 'f','i' 'fi' -> U+FB01
> auto 'fi' -> 'f','i' 'fi' -> U+FB01
> {noformat}
> The language argument opens the same hole. A language with no langsys entry
> under the script makes the (script, language) match come up empty, and the
> fallback then goes to (DFLT, dflt), skipping the script's own default
> langsys. Measured 2026-09-27 through the command line, one fo:block, only the
> language attribute changing, read back with pdftotext:
> {noformat}
> attributes Carlito DejaVu Sans
> script=latn, no language ffi ffl fi fl ft ti ligated ffi ffl fi fl
> ligated
> script=latn language="en" nothing ligated nothing ligated
> {noformat}
> And the language never matches in the first place. The FO language property
> carries an ISO 639 code ("en", "tr"); the font's language systems are
> OpenType tags ("ENG", "TRK"); FOP passes the code through untranslated. So a
> language the font does distinguish is treated like one it does not:
> DejaVuLGCSerif's TRK system omits liga, and performSubstitution("fi", "latn",
> "tr") ligates while ("fi", "latn", "TRK") does not.
> Neither font has an ENG langsys under latn. DejaVu Sans does have DFLT, but
> its DFLT langsys carries case, ccmp, dlig and kern and no liga, so the
> fallback lands on a script table that lacks the feature. DejaVu Serif's DFLT
> langsys does carry liga, and we measured that it ligates under the same
> attributes. So whether a run ligates today is font-accidental: it depends on
> which features the font's author happened to put in DFLT, not on what the
> font offers for the script.
> This is not a corner case for a consumer. docx4j writes language="en"
> country="US" on every block and inline it produces, from w:lang and the
> document defaults, and it cannot drop the attribute because FOP selects
> hyphenation patterns by it. The first point of FOP-2092 (Thai shapes only
> under language="dflt") is this language half. The fix belongs in the fallback
> order, as OpenType layout engines do it: (script, language), then (script,
> dflt), then (DFLT, dflt). GlyphTable.matchLookups goes straight from the
> first to the third.
> It is not only substitution. The same script matching governs GPOS, so
> kerning disappears too, and that is the wider harm because it reaches every
> letter pair rather than a handful of ligature sequences:
> {noformat}
> Carlito legacy kern table: absent, 0 entries
> GPOS 'AV' latn kerned * kerned DFLT null
> DejaVu legacy kern table: present, 101 entries
> GPOS 'AV' latn kerned * kerned DFLT kerned
> {noformat}
> Carlito has no legacy kern table at all, so under a default script it gets no
> kerning from either mechanism. A consumer that sets kerning="true" on such a
> font gets nothing and is told nothing.
> Note that the wildcard script "*" works, because the wildcard matches the
> font's real script tables. GlyphMapping maps "auto" and "zyyy" to "*", so the
> common FO path is safe; it is a caller that passes DFLT, or "auto"/"zyyy"
> unnormalised, that loses everything.
> h3. Why it matters
> The failure is silent and invisible in rendering: the text is drawn, just
> unshaped and unkerned. Nobody notices until they compare advance widths
> against another renderer. It also produces exactly the misleading symptom
> this was first mistaken for: "FOP reads this font's tables but not that
> one's".
> h3. Proposed fix
> Done on the fork, branch CR-003-script-fallback (GlyphTable, OTFLanguage,
> OTFScript; tests GlyphTableFallbackTestCase on synthetic tables shaped like
> Carlito and DejaVu Sans, and LanguageSystemTestCase on the DejaVuLGCSerif in
> the test tree, whose TRK system omits liga):
> # Map the language to its OpenType tag first (a table of ISO 639 codes
> against the tags OTFLanguage already names: "tr" to TRK, "en-US" to ENG,
> "zh-TW" to ZHT; a tag is accepted as given; unknown codes match nothing and
> take the next step), then fall back in the order OpenType layout engines use:
> (script, language), then (script, dflt), then (DFLT, dflt). Each step is
> taken only when the one before found nothing.
> # OTFScript.isWildCard compares with the wildcard rather than DEFAULT
> (harmless today, corrected alongside).
> Measured after the change, same command line: Carlito under language="en"
> ligates (ffi fi fl ft ti, 26 glyphs to 21) and kerns (AVATAR 43.19 pt, was
> 46.62); DejaVu Sans under "en" ligates; DejaVuLGCSerif
> performSubstitution("fi", "latn", "tr") no longer ligates, which is what its
> TRK system says; Noto Sans Arabic under "ar" unchanged.
> This changes shaping and kerning for existing documents wherever a language
> was set and the font had no system for it, so it wants the release note. A
> safer first step, if preferred: an event when a font with GSUB or GPOS
> present yields no lookups for the requested script, turning the silent miss
> into a diagnosable one.
> Note that the kerning half is also gated by the font configuration's kerning
> flag, which did not reach GPOS at all (a separate report, FOP-3343), and by a
> reader defect that drops a script's default language system when a named
> language shares its table (FOP-3342): DejaVu Sans needs all three before it
> kerns under "en".
> h3. Provenance
> Found while implementing the docx4j FO renderer's gsub-features hook, tracing
> why one font shaped and another did not through the same pipeline. All the
> numbers above are from loading each font directly with FontLoader and calling
> performSubstitution and performPositioning, so no renderer or consumer
> pipeline is involved.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)