From 6d30395ecea61c25b90e8d9683ad2f6790391db4 Mon Sep 17 00:00:00 2001 From: Jason Harrop Date: Sat, 3 Oct 2026 07:17:50 +1000 Subject: [PATCH] FOP-3342: A script's default language system shared with a named one is kept under dflt OTFAdvancedTypographicTableReader.readScriptTable compared each LangSys record's offset with the default LangSys offset and, when they were equal, dropped the default and made that language the script's default tag: if (dl == lo) { dl = 0; dt = lt; } constructLookups then registered the features under (script, that language) only and nothing under (script, "dflt"). Font builders share the table on purpose: FontForge writes one LangSys table for the default and for every language whose feature list equals it. So a request for the script's default language system found nothing and fell back to the DFLT script, which lists whatever the font's author put there. DejaVu Sans is the visible case. Its GPOS latn default is the table latn/ROM and the Sami systems point at, listing kern lookups 14 and 15; DFLT/dflt lists lookup 15 alone, a 20-glyph subtable with no Latin letter. Measured on this branch, 14pt "AVATAR To Ye", kerning on, line width from mutool draw -F stext: no language language="ROM" language="en" before 99.68 pt 91.55 pt 99.68 pt after 91.55 pt 91.55 pt 99.68 pt Before, the font kerned only when the language named the record that shares the table. (language="en" is the separate FOP-3341: the FO language code is never translated to the font's tag and the fallback skips the script's default.) Its arab default is shared with KUR in GSUB and GPOS both, and DFLT lists no contextual form and no mark lookup, so Arabic set in DejaVu Sans under a default language was drawn as isolated letters. Of 1356 OpenType fonts installed on one Linux machine, 252 share a default this way. The default LangSys table is now read and registered under "dflt" whether or not a named record points at the same table; a named language sharing it is still read under its own tag. SharedDefaultLanguageSystemTestCase uses the DejaVuLGCSerif already in the test tree, whose GPOS latn default is shared with AZE and whose GPOS has no DFLT script: (latn, dflt, kern) must match and "AV" must kern. Its first case fails without the change. Co-Authored-By: Claude Fable 5.1 --- .../OTFAdvancedTypographicTableReader.java | 10 +-- .../SharedDefaultLanguageSystemTestCase.java | 71 +++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 fop-core/src/test/java/org/apache/fop/fonts/SharedDefaultLanguageSystemTestCase.java diff --git a/fop-core/src/main/java/org/apache/fop/complexscripts/fonts/OTFAdvancedTypographicTableReader.java b/fop-core/src/main/java/org/apache/fop/complexscripts/fonts/OTFAdvancedTypographicTableReader.java index 386766e4dac..11633bff48d 100644 --- a/fop-core/src/main/java/org/apache/fop/complexscripts/fonts/OTFAdvancedTypographicTableReader.java +++ b/fop-core/src/main/java/org/apache/fop/complexscripts/fonts/OTFAdvancedTypographicTableReader.java @@ -197,10 +197,12 @@ private void readScriptTable(OFTableName tableTag, long scriptTable, String scri } lta[i] = lt; loa[i] = lo; - if (dl == lo) { - dl = 0; - dt = lt; - } + // The default language system is read under "dflt" even when its table is the one a + // named language system record points at. A font builder shares the table whenever a + // language's features equal the default's (FontForge does, for DejaVu and others), and + // aliasing the default to that language left the script with no default language + // system at all, so a caller asking for (script, dflt) fell through to DFLT and lost + // the script's features. ll.add(lt); } // read non-default language system tables diff --git a/fop-core/src/test/java/org/apache/fop/fonts/SharedDefaultLanguageSystemTestCase.java b/fop-core/src/test/java/org/apache/fop/fonts/SharedDefaultLanguageSystemTestCase.java new file mode 100644 index 00000000000..725a8528d4c --- /dev/null +++ b/fop-core/src/test/java/org/apache/fop/fonts/SharedDefaultLanguageSystemTestCase.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.fonts; + +import java.io.File; + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.apache.fop.apps.io.InternalResourceResolver; +import org.apache.fop.apps.io.ResourceResolverFactory; +import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; +import org.apache.fop.complexscripts.fonts.GlyphTable; + +/** + * DejaVuLGCSerif's GPOS {@code latn} script points its default language system and its {@code AZE} + * system at one table, as FontForge does for every language whose features equal the default's, + * and GPOS has no {@code DFLT} script. The reader used to alias the default to the named language + * and register nothing under {@code dflt}, so a request for the script's default found no kern + * feature and the font was never kerned under a default language (FOP-3342). The GSUB side of the same font + * does not share its table, which is why ligatures never showed the defect. + */ +public class SharedDefaultLanguageSystemTestCase { + + private MultiByteFont font; + + @Before + public void setUp() throws Exception { + InternalResourceResolver resolver = + ResourceResolverFactory.createDefaultInternalResourceResolver(new File(".").toURI()); + File file = new File("test/resources/fonts/ttf/DejaVuLGCSerif.ttf"); + CustomFont loaded = FontLoader.loadFont(new FontUris(file.toURI(), null), "", true, + EmbeddingMode.AUTO, EncodingMode.AUTO, true, true, resolver, false, false, true); + assertTrue(loaded instanceof MultiByteFont); + font = (MultiByteFont) loaded; + assertTrue(font.performsPositioning()); + } + + @Test + public void testSharedDefaultLanguageSystemIsRegisteredUnderDflt() { + assertTrue(font.getGPOS().matchLookupSpecs("latn", "dflt", "kern").size() > 0); + assertTrue(font.getGPOS().matchLookupSpecs("latn", "AZE", "kern").size() > 0); + assertTrue(font.hasFeature(GlyphTable.GLYPH_TABLE_TYPE_POSITIONING, "latn", "dflt", "kern")); + } + + @Test + public void testSharedDefaultLanguageSystemKerns() { + int[][] gpa = font.performPositioning("AV", "latn", "dflt", 1000); + assertNotNull(gpa); + assertTrue(gpa[0][GlyphPositioningTable.Value.IDX_X_ADVANCE] < 0); + } +}