Author: tilman
Date: Tue Sep 1 14:36:14 2026
New Revision: 1937725
Log:
PDFBOX-6250: substitute Adobe-Identity-0 CID-keyed fonts for non-embedded
legacy-ROS fonts + prefer a regular weight when the descriptor has no weight
information, by Tim Allison and 🤖
Added:
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/CIDCharSetMatchTest.java
(contents, props changed)
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0SubstituteTest.java
(contents, props changed)
Modified:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java
Modified:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java
==============================================================================
---
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java
Tue Sep 1 12:45:05 2026 (r1937724)
+++
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java
Tue Sep 1 14:36:14 2026 (r1937725)
@@ -636,6 +636,11 @@ final class FontMapperImpl implements Fo
float dist = Math.abs(fontDescriptor.getFontWeight() -
info.getWeightClass());
match.score += 1 - (dist / 100) * 0.5;
}
+ else if (info.getWeightClass() > 0)
+ {
+ // no weight information in the descriptor: prefer a regular
weight
+ match.score += 1 - (Math.abs(info.getWeightClass() - 400) /
100) * 0.5;
+ }
// todo: italic
// ...
@@ -664,7 +669,7 @@ final class FontMapperImpl implements Fo
* Returns true if the character set described by CIDSystemInfo is present
in the given font.
* Only applies to Adobe-GB1, Adobe-CNS1, Adobe-Japan1, Adobe-Korea1, as
per the PDF spec.
*/
- private boolean isCharSetMatch(PDCIDSystemInfo cidSystemInfo, FontInfo
info)
+ boolean isCharSetMatch(PDCIDSystemInfo cidSystemInfo, FontInfo info)
{
String ordering = cidSystemInfo.getOrdering();
if (ordering == null)
@@ -673,10 +678,18 @@ final class FontMapperImpl implements Fo
}
if (info.getCIDSystemInfo() != null)
{
- return
info.getCIDSystemInfo().getRegistry().equals(cidSystemInfo.getRegistry()) &&
- info.getCIDSystemInfo().getOrdering().equals(ordering);
+ if
(info.getCIDSystemInfo().getRegistry().equals(cidSystemInfo.getRegistry()) &&
+ info.getCIDSystemInfo().getOrdering().equals(ordering))
+ {
+ return true;
+ }
+ if (!"Identity".equals(info.getCIDSystemInfo().getOrdering()))
+ {
+ return false;
+ }
+ // PDFBOX-6249: Adobe-Identity-0 fonts (Noto CJK, Source Han)
never match a ROS by
+ // name; fall through to the code page bits
}
- else
{
long codePageRange = info.getCodePageRange();
Modified:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java
==============================================================================
---
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java
Tue Sep 1 12:45:05 2026 (r1937724)
+++
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java
Tue Sep 1 14:36:14 2026 (r1937725)
@@ -31,6 +31,7 @@ import org.apache.fontbox.cff.CFFFont;
import org.apache.fontbox.cff.CFFParser;
import org.apache.fontbox.cff.CFFType1Font;
import org.apache.fontbox.cff.Type2CharString;
+import org.apache.fontbox.ttf.CmapLookup;
import org.apache.fontbox.util.BoundingBox;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.io.RandomAccessRead;
@@ -54,6 +55,10 @@ public class PDCIDFontType0 extends PDCI
private final CFFCIDFont cidFont; // Top DICT that uses CIDFont operators
private final FontBoxFont t1Font; // Top DICT that does not use CIDFont
operators
+
+ // substitute is CID-keyed with a different ROS: this font's CIDs are
meaningless in it,
+ // resolve glyphs via Unicode instead (PDFBOX-6249)
+ private final CmapLookup substituteUnicodeCmap;
private final Map<Integer, Float> glyphHeights = new HashMap<>();
private final AffineTransform fontMatrixTransform;
@@ -76,6 +81,7 @@ public class PDCIDFontType0 extends PDCI
super(fontDictionary, resourceCache);
boolean fontIsDamaged = false;
+ CmapLookup substituteCmap = null;
CFFFont cffFont = null;
PDFontDescriptor fd = getFontDescriptor();
if (fd != null)
@@ -157,9 +163,22 @@ public class PDCIDFontType0 extends PDCI
{
LOG.warn("Using fallback {} for CID-keyed font {}",
font.getName(), getBaseFont());
}
+ if (cidFont != null && mapping.isCIDFont() &&
!isCharacterCollectionMatch(cidFont) &&
+ "Identity".equals(cidFont.getOrdering()))
+ {
+ try
+ {
+ substituteCmap = mapping.getFont().getUnicodeCmapLookup();
+ }
+ catch (IOException e)
+ {
+ LOG.warn("Could not read cmap of the substitute for font
{}", getBaseFont(), e);
+ }
+ }
isEmbedded = false;
isDamaged = fontIsDamaged;
}
+ substituteUnicodeCmap = substituteCmap;
fontMatrixTransform = getFontMatrix().createAffineTransform();
fontMatrixTransform.scale(1000, 1000);
}
@@ -299,6 +318,27 @@ public class PDCIDFontType0 extends PDCI
}
}
+ private boolean isCharacterCollectionMatch(CFFCIDFont substitute) throws
IOException
+ {
+ PDCIDSystemInfo ros = getCIDSystemInfo();
+ return ros != null &&
ros.getRegistry().equals(substitute.getRegistry()) &&
+ ros.getOrdering().equals(substitute.getOrdering());
+ }
+
+ /**
+ * GID in the substitute for the given code, via Unicode; -1 if unmapped.
The substitute's
+ * Identity charset makes GIDs address its charstrings directly.
+ */
+ private int codeToSubstituteGID(int code, PDType0Font parent)
+ {
+ String unicodes = parent.toUnicode(code);
+ if (unicodes == null)
+ {
+ return -1;
+ }
+ return substituteUnicodeCmap.getGlyphId(unicodes.codePointAt(0));
+ }
+
/**
* Returns the name of the glyph with the given character code. This is
done by looking up the
* code in the parent font's ToUnicode map and generating a glyph name
from that.
@@ -317,6 +357,11 @@ public class PDCIDFontType0 extends PDCI
protected GeneralPath getPath(int code, PDType0Font parent) throws
IOException
{
int cid = codeToCID(code, parent);
+ if (substituteUnicodeCmap != null)
+ {
+ int gid = codeToSubstituteGID(code, parent);
+ return getType2CharString(Math.max(gid, 0)).getPath();
+ }
if (cid2gid != null && isEmbedded)
{
// PDFBOX-4093: despite being a type 0 font, there is a CIDToGIDMap
@@ -347,6 +392,10 @@ public class PDCIDFontType0 extends PDCI
protected boolean hasGlyph(int code, PDType0Font parent) throws IOException
{
int cid = codeToCID(code, parent);
+ if (substituteUnicodeCmap != null)
+ {
+ return codeToSubstituteGID(code, parent) > 0;
+ }
Type2CharString charstring = getType2CharString(cid);
if (charstring != null)
{
@@ -378,6 +427,10 @@ public class PDCIDFontType0 extends PDCI
protected int codeToGID(int code, PDType0Font parent)
{
int cid = codeToCID(code, parent);
+ if (substituteUnicodeCmap != null)
+ {
+ return Math.max(codeToSubstituteGID(code, parent), 0);
+ }
if (cidFont != null)
{
// The CIDs shall be used to determine the GID value for the glyph
procedure using the
@@ -402,7 +455,11 @@ public class PDCIDFontType0 extends PDCI
{
int cid = codeToCID(code, parent);
float width;
- if (cidFont != null)
+ if (substituteUnicodeCmap != null)
+ {
+ width = getType2CharString(Math.max(codeToSubstituteGID(code,
parent), 0)).getWidth();
+ }
+ else if (cidFont != null)
{
width = getType2CharString(cid).getWidth();
}
Added:
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/CIDCharSetMatchTest.java
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/CIDCharSetMatchTest.java
Tue Sep 1 14:36:14 2026 (r1937725)
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+package org.apache.pdfbox.pdmodel.font;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.fontbox.FontBoxFont;
+import org.junit.jupiter.api.Test;
+
+/**
+ * CIDSystemInfo-based candidate filtering for CID font substitution.
+ */
+class CIDCharSetMatchTest
+{
+ private static final long CHINESE_TRADITIONAL = 1 << 20;
+ private static final long CHINESE_SIMPLIFIED = 1 << 18;
+
+ private static FontInfo info(final CIDSystemInfo ros, final int
codePageRange1)
+ {
+ return new FontInfo()
+ {
+ @Override
+ public String getPostScriptName()
+ {
+ return "TestFont";
+ }
+
+ @Override
+ public FontFormat getFormat()
+ {
+ return FontFormat.OTF;
+ }
+
+ @Override
+ public CIDSystemInfo getCIDSystemInfo()
+ {
+ return ros;
+ }
+
+ @Override
+ public FontBoxFont getFont()
+ {
+ return null;
+ }
+
+ @Override
+ public int getFamilyClass()
+ {
+ return 0;
+ }
+
+ @Override
+ public int getWeightClass()
+ {
+ return 0;
+ }
+
+ @Override
+ public int getCodePageRange1()
+ {
+ return codePageRange1;
+ }
+
+ @Override
+ public int getCodePageRange2()
+ {
+ return 0;
+ }
+
+ @Override
+ public int getMacStyle()
+ {
+ return 0;
+ }
+
+ @Override
+ public PDPanoseClassification getPanose()
+ {
+ return null;
+ }
+ };
+ }
+
+ @Test
+ void testCharSetMatch()
+ {
+ FontMapperImpl mapper = new FontMapperImpl();
+ PDCIDSystemInfo cns1 = new PDCIDSystemInfo("Adobe", "CNS1", 0);
+
+ // exact ROS match
+ assertTrue(mapper.isCharSetMatch(cns1,
+ info(new CIDSystemInfo("Adobe", "CNS1", 0), 0)));
+
+ // a different legacy ROS never matches
+ assertFalse(mapper.isCharSetMatch(cns1,
+ info(new CIDSystemInfo("Adobe", "Japan1", 0), (int)
CHINESE_TRADITIONAL)));
+
+ // Adobe-Identity-0 (Noto CJK, Source Han) matches via its OS/2 code
page bits
+ assertTrue(mapper.isCharSetMatch(cns1,
+ info(new CIDSystemInfo("Adobe", "Identity", 0), (int)
CHINESE_TRADITIONAL)));
+ assertFalse(mapper.isCharSetMatch(cns1,
+ info(new CIDSystemInfo("Adobe", "Identity", 0), (int)
CHINESE_SIMPLIFIED)));
+
+ // ROS-less TrueType fonts keep matching via code page bits
+ assertTrue(mapper.isCharSetMatch(cns1, info(null, (int)
CHINESE_TRADITIONAL)));
+ assertFalse(mapper.isCharSetMatch(cns1, info(null, 0)));
+ }
+}
Added:
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0SubstituteTest.java
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0SubstituteTest.java
Tue Sep 1 14:36:14 2026 (r1937725)
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+package org.apache.pdfbox.pdmodel.font;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.awt.geom.GeneralPath;
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.fontbox.cff.CFFCIDFont;
+import org.apache.fontbox.cff.CFFFont;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.junit.jupiter.api.Test;
+
+/**
+ * A non-embedded Adobe-CNS1 CIDFontType0 must render real glyphs when a
CID-keyed substitute is
+ * available, even when the substitute uses a different character collection
(modern CJK fonts
+ * such as Noto CJK are Adobe-Identity-0). PDFBOX-6249.
+ */
+class PDCIDFontType0SubstituteTest
+{
+ @Test
+ void testLatinViaCns1NonEmbedded() throws IOException
+ {
+ File file = new
File("src/test/resources/org/apache/pdfbox/pdmodel/font",
+ "PDFBOX-6249-cns1-nonembedded-latin.pdf");
+ try (PDDocument doc = Loader.loadPDF(file))
+ {
+ PDPage page = doc.getPage(0);
+ PDType0Font font = (PDType0Font)
page.getResources().getFont(COSName.getPDFName("F1"));
+ PDCIDFontType0 cidFont = (PDCIDFontType0) font.getDescendantFont();
+
+ CIDFontMapping mapping =
FontMappers.instance().getCIDFont(cidFont.getBaseFont(),
+ cidFont.getFontDescriptor(), cidFont.getCIDSystemInfo());
+ assumeTrue(mapping.isCIDFont(),
+ "no CID-keyed substitute for Adobe-CNS1 installed, can't
test");
+
+ // 0x48 is "H": the CMap maps it to CID 41, which is not a valid
glyph index in an
+ // Adobe-Identity-0 substitute; it must be resolved via Unicode
+ assertTrue(cidFont.hasGlyph(0x48, font), "glyph for 'H' not found
in substitute");
+ GeneralPath path = cidFont.getPath(0x48, font);
+ assertFalse(path.getPathIterator(null).isDone(), "glyph for 'H'
has an empty path");
+
+ // the repro descriptor carries no Panose or FontWeight: a regular
weight must
+ // outrank Bold among otherwise-tied candidates
+ assertFalse(mapping.getFont().getName().endsWith("-Bold"),
+ "regular weight should be preferred over Bold on a
weightless descriptor");
+
+ CFFFont cff = mapping.getFont().getCFF().getFont();
+ if (cff instanceof CFFCIDFont && "Identity".equals(((CFFCIDFont)
cff).getOrdering()))
+ {
+ // ASCII GIDs happen to line up with Adobe CIDs in Noto/Source
Han, so assert on
+ // an ideograph, where using the CID as a GID yields the wrong
glyph
+ int expected =
mapping.getFont().getUnicodeCmapLookup().getGlyphId(0x4E2D);
+ assertTrue(expected > 0);
+ assertEquals(expected, cidFont.codeToGID(0x4E2D, font),
+ "GID must be resolved via Unicode, not used as a CID");
+ }
+ }
+ }
+}