diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java index ff98cdda4cd..e71fa761384 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java @@ -636,6 +636,11 @@ else if (fontDescriptor.getFontWeight() > 0 && info.getWeightClass() > 0) 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 @@ private boolean probablyBarcodeFont(PDFontDescriptor fontDescriptor) * 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 @@ private boolean isCharSetMatch(PDCIDSystemInfo cidSystemInfo, FontInfo info) } 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(); diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java index 77d09733ec4..f4a6d0514ca 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0.java @@ -31,6 +31,7 @@ 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 PDCIDFont 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 glyphHeights = new HashMap<>(); private final AffineTransform fontMatrixTransform; @@ -76,6 +81,7 @@ public PDCIDFontType0(COSDictionary fontDictionary, ResourceCache resourceCache) super(fontDictionary, resourceCache); boolean fontIsDamaged = false; + CmapLookup substituteCmap = null; CFFFont cffFont = null; PDFontDescriptor fd = getFontDescriptor(); if (fd != null) @@ -157,9 +163,22 @@ public PDCIDFontType0(COSDictionary fontDictionary, ResourceCache resourceCache) { 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 @@ else if (t1Font instanceof CFFType1Font) } } + 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 @@ private String getGlyphName(int code, PDType0Font parent) 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 @@ protected GeneralPath getNormalizedPath(int code, PDType0Font parent) throws IOE 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 @@ protected int codeToCID(int code, PDType0Font parent) 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 @@ protected float getWidthFromFont(int code, PDType0Font parent) throws IOExceptio { 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(); } diff --git a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/CIDCharSetMatchTest.java b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/CIDCharSetMatchTest.java new file mode 100644 index 00000000000..0c61d79a12a --- /dev/null +++ b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/CIDCharSetMatchTest.java @@ -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))); + } +} diff --git a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0SubstituteTest.java b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0SubstituteTest.java new file mode 100644 index 00000000000..c4d614105e7 --- /dev/null +++ b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType0SubstituteTest.java @@ -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"); + } + } + } +} diff --git a/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/font/PDFBOX-6249-cns1-nonembedded-latin.pdf b/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/font/PDFBOX-6249-cns1-nonembedded-latin.pdf new file mode 100644 index 00000000000..058b3d267a0 Binary files /dev/null and b/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/font/PDFBOX-6249-cns1-nonembedded-latin.pdf differ