Author: tilman
Date: Tue Sep  1 18:13:27 2026
New Revision: 1937736

Log:
PDFBOX-6249: 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/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/CIDCharSetMatchTest.java
   (contents, props changed)
   
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0SubstituteTest.java
   (contents, props changed)
Modified:
   
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java
   
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java

Modified: 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java
==============================================================================
--- 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java
 Tue Sep  1 17:27:39 2026        (r1937735)
+++ 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java
 Tue Sep  1 18:13:27 2026        (r1937736)
@@ -662,6 +662,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
             // ...
 
@@ -690,7 +695,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)
@@ -699,10 +704,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/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java
==============================================================================
--- 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java
 Tue Sep  1 17:27:39 2026        (r1937735)
+++ 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java
 Tue Sep  1 18:13:27 2026        (r1937736)
@@ -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.pdmodel.common.PDRectangle;
@@ -53,6 +54,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<Integer, 
Float>();
     private final boolean isEmbedded;
     private final boolean isDamaged;
@@ -73,6 +78,7 @@ public class PDCIDFontType0 extends PDCI
     {
         super(fontDictionary, parent);
 
+        CmapLookup substituteCmap = null;
         PDFontDescriptor fd = getFontDescriptor();
         byte[] bytes = null;
         if (fd != null)
@@ -160,9 +166,22 @@ public class PDCIDFontType0 extends PDCI
                 LOG.warn("Using fallback " + font.getName() + " for CID-keyed 
font " +
                          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);
     }
@@ -306,6 +325,27 @@ public class PDCIDFontType0 extends PDCI
         }
     }
 
+    private boolean isCharacterCollectionMatch(CFFCIDFont substitute)
+    {
+        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) throws 
IOException
+    {
+        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.
@@ -324,6 +364,11 @@ public class PDCIDFontType0 extends PDCI
     public GeneralPath getPath(int code) throws IOException
     {
         int cid = codeToCID(code);
+        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
@@ -348,6 +393,10 @@ public class PDCIDFontType0 extends PDCI
     public boolean hasGlyph(int code) throws IOException
     {
         int cid = codeToCID(code);
+        if (substituteUnicodeCmap != null)
+        {
+            return codeToSubstituteGID(code, parent) > 0;
+        }
         Type2CharString charstring = getType2CharString(cid);
         if (charstring != null)
         {
@@ -376,9 +425,13 @@ public class PDCIDFontType0 extends PDCI
     }
 
     @Override
-    public int codeToGID(int code)
+    public int codeToGID(int code) throws IOException
     {
         int cid = codeToCID(code);
+        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
@@ -405,7 +458,11 @@ public class PDCIDFontType0 extends PDCI
     {
         int cid = codeToCID(code);
         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/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/CIDCharSetMatchTest.java
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ 
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/CIDCharSetMatchTest.java
    Tue Sep  1 18:13:27 2026        (r1937736)
@@ -0,0 +1,122 @@
+/*
+ * 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 org.apache.fontbox.FontBoxFont;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import org.junit.Test;
+
+/**
+ * CIDSystemInfo-based candidate filtering for CID font substitution.
+ */
+public 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
+    public 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/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0SubstituteTest.java
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ 
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0SubstituteTest.java
   Tue Sep  1 18:13:27 2026        (r1937736)
@@ -0,0 +1,81 @@
+/*
+ * 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 java.awt.geom.GeneralPath;
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import org.apache.fontbox.cff.CFFCIDFont;
+import org.apache.fontbox.cff.CFFFont;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+
+import org.junit.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.
+ */
+public class PDCIDFontType0SubstituteTest
+{
+    @Test
+    public void testLatinViaCns1NonEmbedded() throws IOException
+    {
+        File file = new 
File("src/test/resources/org/apache/pdfbox/pdmodel/font",
+                "PDFBOX-6249-cns1-nonembedded-latin.pdf");
+        PDDocument doc = PDDocument.load(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("no CID-keyed substitute for Adobe-CNS1 installed, can't 
test", mapping.isCIDFont());
+                
+
+        // 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("glyph for 'H' not found in substitute", 
cidFont.hasGlyph(0x48));
+        GeneralPath path = cidFont.getPath(0x48);
+        assertFalse( "glyph for 'H' has an empty path", 
path.getPathIterator(null).isDone());
+
+        // the repro descriptor carries no Panose or FontWeight: a regular 
weight must
+        // outrank Bold among otherwise-tied candidates
+        assertFalse("regular weight should be preferred over Bold on a 
weightless descriptor",
+                mapping.getFont().getName().endsWith("-Bold"));
+
+        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("GID must be resolved via Unicode, not used as a CID",
+                    expected, cidFont.codeToGID(0x4E2D));
+        }
+        doc.close();
+    }
+}

Reply via email to