From 2ca4109cce1ae0bf4edf61d686296da42100a70d Mon Sep 17 00:00:00 2001 From: Brandon Weeks Date: Fri, 4 Sep 2026 02:15:25 -0700 Subject: [PATCH] Add a regression test for FontMatrix right after ROS in a CFF Top DICT #723 fixed this but didn't add a test. Two Libertinus subsets from Typst, identical except Copyright and FontMatrix are swapped. Widths must match /W for both. --- .../pd/font/cff/CFFCIDFontProgramTest.java | 97 ++++++++++++++++++ .../LibertinusSerif-subset-ros-fontmatrix.cff | Bin 0 -> 3573 bytes .../pd/font/cff/LibertinusSerif-subset.cff | Bin 0 -> 3573 bytes 3 files changed, 97 insertions(+) create mode 100644 src/test/java/org/verapdf/pd/font/cff/CFFCIDFontProgramTest.java create mode 100644 src/test/resources/org/verapdf/pd/font/cff/LibertinusSerif-subset-ros-fontmatrix.cff create mode 100644 src/test/resources/org/verapdf/pd/font/cff/LibertinusSerif-subset.cff diff --git a/src/test/java/org/verapdf/pd/font/cff/CFFCIDFontProgramTest.java b/src/test/java/org/verapdf/pd/font/cff/CFFCIDFontProgramTest.java new file mode 100644 index 00000000..39528460 --- /dev/null +++ b/src/test/java/org/verapdf/pd/font/cff/CFFCIDFontProgramTest.java @@ -0,0 +1,97 @@ +/* + * This file is part of veraPDF Parser, a module of the veraPDF project. + * Copyright (c) 2015-2026, veraPDF Consortium + * All rights reserved. + * + * veraPDF Parser is free software: you can redistribute it and/or modify + * it under the terms of either: + * + * The GNU General public license GPLv3+. + * You should have received a copy of the GNU General Public License + * along with veraPDF Parser as the LICENSE.GPL file in the root of the source + * tree. If not, see http://www.gnu.org/licenses/ or + * https://www.gnu.org/licenses/gpl-3.0.en.html. + * + * The Mozilla Public License MPLv2+. + * You should have received a copy of the Mozilla Public License along with + * veraPDF Parser as the LICENSE.MPL file in the root of the source tree. + * If a copy of the MPL was not distributed with this file, you can obtain one at + * http://mozilla.org/MPL/2.0/. + */ +package org.verapdf.pd.font.cff; + +import org.junit.Test; +import org.verapdf.as.io.ASInputStream; +import org.verapdf.io.InternalInputStream; +import org.verapdf.tools.StaticResources; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Regression test for CID-keyed CFF Top DICT parsing. + *

+ * Both fixtures are the same subset of Libertinus Serif (SIL OFL 1.1), + * embedded by Typst as a CIDFontType0 with an Identity charset (cid == gid). + * They differ only in Top DICT operator order: + *

+ * The second ordering used to make the parser pick up the ROS Registry SID + * (391) as FontMatrix[0], scaling every glyph width by 391000. + */ +public class CFFCIDFontProgramTest { + + private static final String RESOURCE_DIR = "src/test/resources/org/verapdf/pd/font/cff/"; + private static final String ROS_COPYRIGHT_FONTMATRIX = RESOURCE_DIR + "LibertinusSerif-subset.cff"; + private static final String ROS_FONTMATRIX_COPYRIGHT = RESOURCE_DIR + "LibertinusSerif-subset-ros-fontmatrix.cff"; + + /** + * Advance widths from the /W array of the CIDFont dictionary the fixtures + * were embedded with, indexed by CID (== GID). + */ + private static final float[] EXPECTED_WIDTHS = { + 500, 730, 447, 264, 504, 250, 747, 372, 505.99997f, 220, 316, 390, 271, + 542, 500, 541, 701, 485, 323, 695, 428, 310, 790, 457, 220 + }; + + private static CFFCIDFontProgram parse(String path) throws IOException { + StaticResources.clear(); + ASInputStream stream = new InternalInputStream(path, 2); + CFFFontProgram font = new CFFFontProgram(stream, null, true); + font.parseFont(); + assertTrue(font.isCIDFont()); + assertTrue(font.getFont() instanceof CFFCIDFontProgram); + return (CFFCIDFontProgram) font.getFont(); + } + + private static void assertWidthsMatchFontDictionary(CFFCIDFontProgram font) { + assertEquals(EXPECTED_WIDTHS.length, font.getNGlyphs()); + for (int gid = 0; gid < EXPECTED_WIDTHS.length; ++gid) { + assertEquals("width of gid " + gid, EXPECTED_WIDTHS[gid], font.widths.getWidth(gid), 0.01f); + } + } + + @Test + public void testWidthsWhenCopyrightSeparatesROSAndFontMatrix() throws IOException { + assertWidthsMatchFontDictionary(parse(ROS_COPYRIGHT_FONTMATRIX)); + } + + @Test + public void testWidthsWhenFontMatrixDirectlyFollowsROS() throws IOException { + assertWidthsMatchFontDictionary(parse(ROS_FONTMATRIX_COPYRIGHT)); + } + + @Test + public void testTopDictOperatorOrderDoesNotChangeWidths() throws IOException { + CFFCIDFontProgram a = parse(ROS_COPYRIGHT_FONTMATRIX); + CFFCIDFontProgram b = parse(ROS_FONTMATRIX_COPYRIGHT); + assertEquals(a.getNGlyphs(), b.getNGlyphs()); + for (int gid = 0; gid < a.getNGlyphs(); ++gid) { + assertEquals("width of gid " + gid, a.widths.getWidth(gid), b.widths.getWidth(gid), 0f); + } + } +} diff --git a/src/test/resources/org/verapdf/pd/font/cff/LibertinusSerif-subset-ros-fontmatrix.cff b/src/test/resources/org/verapdf/pd/font/cff/LibertinusSerif-subset-ros-fontmatrix.cff new file mode 100644 index 0000000000000000000000000000000000000000..cf8221ae8d9202381b57a4a85316fccc7796d582 GIT binary patch literal 3573 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