diff --git a/pdfbox/src/main/appended-resources/META-INF/LICENSE b/pdfbox/src/main/appended-resources/META-INF/LICENSE index 41c00b7d114..6bb2b7a1a97 100644 --- a/pdfbox/src/main/appended-resources/META-INF/LICENSE +++ b/pdfbox/src/main/appended-resources/META-INF/LICENSE @@ -942,6 +942,100 @@ Noto Sans Devanagari font (https://fonts.google.com/noto/specimen/Noto+Sans+Deva This license becomes null and void if any of the above conditions are not met. + DISCLAIMER + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT + OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE + COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL + DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM + OTHER DEALINGS IN THE FONT SOFTWARE. + +Noto Sans CJK SC font, test subset (https://github.com/notofonts/noto-cjk): + + Copyright 2014-2021 Adobe (http://www.adobe.com/) + + This Font Software is licensed under the SIL Open Font License, Version 1.1 . + This license is copied below, and is also available with a FAQ at: https://openfontlicense.org + + ----------------------------------------------------------- + SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + ----------------------------------------------------------- + + PREAMBLE + The goals of the Open Font License (OFL) are to stimulate worldwide + development of collaborative font projects, to support the font creation + efforts of academic and linguistic communities, and to provide a free and + open framework in which fonts may be shared and improved in partnership + with others. + + The OFL allows the licensed fonts to be used, studied, modified and + redistributed freely as long as they are not sold by themselves. The + fonts, including any derivative works, can be bundled, embedded, + redistributed and/or sold with any software provided that any reserved + names are not used by derivative works. The fonts and derivatives, + however, cannot be released under any other type of license. The + requirement for fonts to remain under this license does not apply + to any document created using the fonts or their derivatives. + + DEFINITIONS + "Font Software" refers to the set of files released by the Copyright + Holder(s) under this license and clearly marked as such. This may + include source files, build scripts and documentation. + + "Reserved Font Name" refers to any names specified as such after the + copyright statement(s). + + "Original Version" refers to the collection of Font Software components as + distributed by the Copyright Holder(s). + + "Modified Version" refers to any derivative made by adding to, deleting, + or substituting -- in part or in whole -- any of the components of the + Original Version, by changing formats or by porting the Font Software to a + new environment. + + "Author" refers to any designer, engineer, programmer, technical + writer or other person who contributed to the Font Software. + + PERMISSION & CONDITIONS + Permission is hereby granted, free of charge, to any person obtaining + a copy of the Font Software, to use, study, copy, merge, embed, modify, + redistribute, and sell modified and unmodified copies of the Font + Software, subject to the following conditions: + + 1) Neither the Font Software nor any of its individual components, + in Original or Modified Versions, may be sold by itself. + + 2) Original or Modified Versions of the Font Software may be bundled, + redistributed and/or sold with any software, provided that each copy + contains the above copyright notice and this license. These can be + included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or + binary files as long as those fields can be easily viewed by the user. + + 3) No Modified Version of the Font Software may use the Reserved Font + Name(s) unless explicit written permission is granted by the corresponding + Copyright Holder. This restriction only applies to the primary font name as + presented to the users. + + 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + Software shall not be used to promote, endorse or advertise any + Modified Version, except to acknowledge the contribution(s) of the + Copyright Holder(s) and the Author(s) or with their explicit written + permission. + + 5) The Font Software, modified or unmodified, in part or in whole, + must be distributed entirely under this license, and must not be + distributed under any other license. The requirement for fonts to + remain under this license does not apply to any document created + using the Font Software. + + TERMINATION + This license becomes null and void if any of the above conditions are + not met. + DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType2.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType2.java index 45f4e73294b..248e2d6411b 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType2.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDCIDFontType2.java @@ -25,6 +25,7 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.fontbox.cff.CFFCIDFont; import org.apache.fontbox.cff.CFFFont; import org.apache.fontbox.cff.Type2CharString; import org.apache.fontbox.cmap.CMap; @@ -55,6 +56,9 @@ public class PDCIDFontType2 extends PDCIDFont private final OpenTypeFont otf; private final int[] cid2gid; private final CmapLookup cmap; // may be null + // substitute is CID-keyed with a different ROS: this font's CIDs are meaningless in it, + // resolve glyphs via Unicode instead (PDFBOX-6293) + private final boolean isForeignCIDSubstitute; private Matrix fontMatrix; private BoundingBox fontBBox; private final Set noMapping = new HashSet<>(); @@ -153,6 +157,21 @@ public PDCIDFontType2(COSDictionary fontDictionary, TrueTypeFont trueTypeFont, } cmap = ttf.getUnicodeCmapLookup(false); cid2gid = readCIDToGIDMap(); + isForeignCIDSubstitute = !isEmbedded && isForeignCIDFont(otf); + } + + private boolean isForeignCIDFont(OpenTypeFont substitute) throws IOException + { + if (substitute == null || !substitute.isPostScript() || + !(substitute.getCFF().getFont() instanceof CFFCIDFont)) + { + return false; + } + CFFCIDFont cff = (CFFCIDFont) substitute.getCFF().getFont(); + PDCIDSystemInfo ros = getCIDSystemInfo(); + return "Identity".equals(cff.getOrdering()) && (ros == null || + !ros.getRegistry().equals(cff.getRegistry()) || + !ros.getOrdering().equals(cff.getOrdering())); } private TrueTypeFont findFontOrSubstitute() throws IOException @@ -291,7 +310,7 @@ protected int codeToGID(int code, PDType0Font parent) throws IOException // see PDFBOX-2599 return codeToCID(code, parent); } - else if (otf != null && otf.isPostScript()) + else if (otf != null && otf.isPostScript() && !isForeignCIDSubstitute) { // "Identity" is the default for CFF-based OpenTypeFonts // (copied from segment below where font is embedded) diff --git a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/IdentityCJKFixture.java b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/IdentityCJKFixture.java new file mode 100644 index 00000000000..3f8e17a32ee --- /dev/null +++ b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/IdentityCJKFixture.java @@ -0,0 +1,168 @@ +/* + * 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.io.File; +import java.io.IOException; + +import org.apache.fontbox.FontBoxFont; +import org.apache.fontbox.cff.CFFCIDFont; +import org.apache.fontbox.ttf.OTFParser; +import org.apache.fontbox.ttf.OpenTypeFont; +import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSInteger; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSString; +import org.apache.pdfbox.io.RandomAccessReadBufferedFile; + +/** + * Installs an Adobe-Identity-0 CID-keyed font as the substitute for every non-embedded CID font, + * standing in for an installed Noto CJK / Source Han font, and restores the previous mapper on + * {@link #close()}. {@link FontMappers} is JVM-wide, so test classes using this must be + * {@code @Isolated}. + *

+ * The substitute is Noto Sans CJK SC (SIL OFL 1.1) cut down to space and U+4E2D with glyph IDs + * kept, so that CIDs still equal GIDs as in the full font; every other glyph is empty. + * Regenerate with fontTools: + *

+ * pyftsubset NotoSansCJK-Regular.ttc --font-number=2 --unicodes=U+0020,U+4E2D --retain-gids \
+ *     --layout-features='' --no-hinting --desubroutinize --name-IDs='*' --notdef-outline \
+ *     --output-file=NotoSansCJKsc-Regular-subset.otf
+ * 
+ */ +final class IdentityCJKFixture implements AutoCloseable +{ + private static final File FIXTURE = new File( + "src/test/resources/org/apache/pdfbox/pdmodel/font/NotoSansCJKsc-Regular-subset.otf"); + + // U+4E2D: GID/CID 9544 in the substitute, CID 4559 in Adobe-GB1 + static final int ZHONG = 0x4E2D; + static final int ZHONG_GID = 9544; + + private final FontMapper original; + private final OpenTypeFont substitute; + + IdentityCJKFixture() throws IOException + { + substitute = new OTFParser().parse(new RandomAccessReadBufferedFile(FIXTURE)); + CFFCIDFont cff = (CFFCIDFont) substitute.getCFF().getFont(); + if (!"Identity".equals(cff.getOrdering())) + { + throw new IllegalStateException("fixture is not Adobe-Identity-0"); + } + original = FontMappers.instance(); + FontMappers.set(new FixtureMapper(original, substitute)); + } + + OpenTypeFont getSubstitute() + { + return substitute; + } + + @Override + public void close() throws IOException + { + FontMappers.set(original); + substitute.close(); + } + + /** + * A non-embedded Adobe-GB1 Type0 font dictionary. + */ + static COSDictionary type0(String baseFont, COSName subtype, String encoding) + { + COSDictionary ros = new COSDictionary(); + ros.setItem(COSName.REGISTRY, new COSString("Adobe")); + ros.setItem(COSName.ORDERING, new COSString("GB1")); + ros.setInt(COSName.SUPPLEMENT, 2); + + COSDictionary fd = new COSDictionary(); + fd.setItem(COSName.TYPE, COSName.FONT_DESC); + fd.setName(COSName.FONT_NAME, baseFont); + fd.setInt(COSName.FLAGS, 6); + COSArray bbox = new COSArray(); + bbox.add(COSInteger.get(0)); + bbox.add(COSInteger.get(-141)); + bbox.add(COSInteger.get(996)); + bbox.add(COSInteger.get(855)); + fd.setItem(COSName.FONT_BBOX, bbox); + fd.setInt(COSName.ITALIC_ANGLE, 0); + fd.setInt(COSName.ASCENT, 859); + fd.setInt(COSName.DESCENT, -141); + fd.setInt(COSName.CAP_HEIGHT, 859); + fd.setInt(COSName.STEM_V, 80); + + COSDictionary cid = new COSDictionary(); + cid.setItem(COSName.TYPE, COSName.FONT); + cid.setItem(COSName.SUBTYPE, subtype); + cid.setName(COSName.BASE_FONT, baseFont); + cid.setItem(COSName.CIDSYSTEMINFO, ros); + cid.setItem(COSName.FONT_DESC, fd); + cid.setInt(COSName.DW, 1000); + + COSArray descendants = new COSArray(); + descendants.add(cid); + COSDictionary type0 = new COSDictionary(); + type0.setItem(COSName.TYPE, COSName.FONT); + type0.setItem(COSName.SUBTYPE, COSName.TYPE0); + type0.setName(COSName.BASE_FONT, baseFont); + type0.setName(COSName.ENCODING, encoding); + type0.setItem(COSName.DESCENDANT_FONTS, descendants); + return type0; + } + + static COSDictionary descendant(COSDictionary type0) + { + return (COSDictionary) ((COSArray) type0.getDictionaryObject(COSName.DESCENDANT_FONTS)) + .getObject(0); + } + + private static final class FixtureMapper implements FontMapper + { + private final FontMapper delegate; + private final OpenTypeFont substitute; + + FixtureMapper(FontMapper delegate, OpenTypeFont substitute) + { + this.delegate = delegate; + this.substitute = substitute; + } + + @Override + public FontMapping getTrueTypeFont(String baseFont, + PDFontDescriptor fontDescriptor) + { + return delegate.getTrueTypeFont(baseFont, fontDescriptor); + } + + @Override + public FontMapping getFontBoxFont(String baseFont, + PDFontDescriptor fontDescriptor) + { + return delegate.getFontBoxFont(baseFont, fontDescriptor); + } + + @Override + public CIDFontMapping getCIDFont(String baseFont, PDFontDescriptor fontDescriptor, + PDCIDSystemInfo cidSystemInfo) + { + return new CIDFontMapping(substitute, null, true); + } + } +} diff --git a/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/IdentityCJKSubstituteTest.java b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/IdentityCJKSubstituteTest.java new file mode 100644 index 00000000000..3101a76bd37 --- /dev/null +++ b/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/font/IdentityCJKSubstituteTest.java @@ -0,0 +1,90 @@ +/* + * 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.apache.pdfbox.pdmodel.font.IdentityCJKFixture.ZHONG; +import static org.apache.pdfbox.pdmodel.font.IdentityCJKFixture.ZHONG_GID; +import static org.apache.pdfbox.pdmodel.font.IdentityCJKFixture.type0; +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 java.awt.geom.GeneralPath; +import java.io.IOException; + +import org.apache.pdfbox.cos.COSName; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; + +/** + * Non-embedded Adobe-GB1 CID fonts substituted by an Adobe-Identity-0 CID-keyed font (Noto CJK, + * Source Han). The PDF's CIDs are not glyph indices in such a substitute, so glyphs must be + * resolved via Unicode: PDFBOX-6249 for CIDFontType0, PDFBOX-6293 for CIDFontType2. + */ +@Isolated +class IdentityCJKSubstituteTest +{ + private IdentityCJKFixture fixture; + + @BeforeEach + void installSubstitute() throws IOException + { + fixture = new IdentityCJKFixture(); + } + + @AfterEach + void restore() throws IOException + { + fixture.close(); + } + + @Test + void testCIDFontType2ResolvesViaUnicode() throws IOException + { + PDType0Font font = new PDType0Font(type0("SimSun", COSName.CID_FONT_TYPE2, "UniGB-UCS2-H"), + null); + PDCIDFontType2 cidFont = (PDCIDFontType2) font.getDescendantFont(); + + assertEquals(ZHONG_GID, cidFont.codeToGID(ZHONG, font), + "GID must be resolved via Unicode, not used as a CID"); + assertSameGlyph(cidFont.getPath(ZHONG, font)); + } + + @Test + void testCIDFontType0ResolvesViaUnicode() throws IOException + { + PDType0Font font = new PDType0Font( + type0("AdobeSongStd-Light", COSName.CID_FONT_TYPE0, "UniGB-UCS2-H"), null); + PDCIDFontType0 cidFont = (PDCIDFontType0) font.getDescendantFont(); + + assertEquals(ZHONG_GID, cidFont.codeToGID(ZHONG, font), + "GID must be resolved via Unicode, not used as a CID"); + assertSameGlyph(cidFont.getPath(ZHONG, font)); + } + + private void assertSameGlyph(GeneralPath path) throws IOException + { + // with the CID used as a GID, the fixture yields an empty glyph + assertFalse(path.getPathIterator(null).isDone(), "glyph for U+4E2D has an empty path"); + GeneralPath expected = fixture.getSubstitute().getCFF().getFont() + .getType2CharString(ZHONG_GID).getPath(); + assertEquals(expected.getBounds2D(), path.getBounds2D()); + assertTrue(path.getBounds2D().getWidth() > 0); + } +} diff --git a/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/font/NotoSansCJKsc-Regular-subset.otf b/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/font/NotoSansCJKsc-Regular-subset.otf new file mode 100644 index 00000000000..2e9d3d0add3 Binary files /dev/null and b/pdfbox/src/test/resources/org/apache/pdfbox/pdmodel/font/NotoSansCJKsc-Regular-subset.otf differ