diff --git a/src/main/java/org/verapdf/pd/font/type1/BaseCharStringParser.java b/src/main/java/org/verapdf/pd/font/type1/BaseCharStringParser.java index 1b6cdf56..df49b8c3 100644 --- a/src/main/java/org/verapdf/pd/font/type1/BaseCharStringParser.java +++ b/src/main/java/org/verapdf/pd/font/type1/BaseCharStringParser.java @@ -25,7 +25,6 @@ import org.verapdf.pd.font.cff.CFFIndex; import java.io.IOException; -import java.util.Map; import java.util.Stack; /** @@ -43,7 +42,7 @@ public abstract class BaseCharStringParser { protected final CFFIndex localSubrs; protected final int bias; protected final int gBias; - protected final Map subrWidths; + protected final Type1Subroutines subroutines; /** * Constructor that calls method parse(), so width is extracted right after @@ -56,8 +55,8 @@ protected BaseCharStringParser(ASInputStream stream) throws IOException { this(stream, null, 0, null, 0); } - protected BaseCharStringParser(ASInputStream stream, Map subrWidths) throws IOException { - this(stream, null, 0, null, 0, subrWidths); + protected BaseCharStringParser(ASInputStream stream, Type1Subroutines subroutines) throws IOException { + this(stream, null, 0, null, 0, subroutines); } /** @@ -77,8 +76,9 @@ protected BaseCharStringParser(ASInputStream stream, CFFIndex localSubrs, this(stream, localSubrs, bias, globalSubrs, gBias, null); } - protected BaseCharStringParser(ASInputStream stream, CFFIndex localSubrs, - int bias, CFFIndex globalSubrs, int gBias, Map subrWidths) throws IOException { + private BaseCharStringParser(ASInputStream stream, CFFIndex localSubrs, int bias, + CFFIndex globalSubrs, int gBias, Type1Subroutines subroutines) + throws IOException { this.streams = new Stack<>(); this.streams.push(stream); this.stack = new Stack<>(); @@ -87,7 +87,7 @@ protected BaseCharStringParser(ASInputStream stream, CFFIndex localSubrs, this.localSubrs = localSubrs == null ? CFFIndex.getEmptyIndex() : localSubrs; this.bias = bias; this.gBias = gBias; - this.subrWidths = subrWidths; + this.subroutines = subroutines; parse(); } diff --git a/src/main/java/org/verapdf/pd/font/type1/Type1CharStringParser.java b/src/main/java/org/verapdf/pd/font/type1/Type1CharStringParser.java index 5989e8f0..d1070ca0 100644 --- a/src/main/java/org/verapdf/pd/font/type1/Type1CharStringParser.java +++ b/src/main/java/org/verapdf/pd/font/type1/Type1CharStringParser.java @@ -24,7 +24,6 @@ import org.verapdf.pd.font.CFFNumber; import java.io.IOException; -import java.util.Map; /** * This class parses charstring data in font Type 1 files after it was @@ -41,8 +40,9 @@ public Type1CharStringParser(ASInputStream decodedCharString) throws IOException super(decodedCharString); } - public Type1CharStringParser(ASInputStream decodedCharString, Map subrWidths) throws IOException { - super(decodedCharString, subrWidths); + Type1CharStringParser(ASInputStream decodedCharString, Type1Subroutines subroutines) + throws IOException { + super(decodedCharString, subroutines); } /** @@ -83,9 +83,9 @@ protected boolean processNextOperator(int firstByte) throws IOException { return true; case 10: // callsubr if (!this.stack.empty()) { - CFFNumber number = this.stack.pop(); - if (subrWidths != null) { - CFFNumber width = subrWidths.get((int) number.getInteger()); + int subrNumber = (int) this.stack.pop().getInteger(); + if (subroutines != null) { + CFFNumber width = subroutines.getWidth(subrNumber); if (width != null) { this.setWidth(width); return true; diff --git a/src/main/java/org/verapdf/pd/font/type1/Type1PrivateParser.java b/src/main/java/org/verapdf/pd/font/type1/Type1PrivateParser.java index 4c40f855..65f0f159 100644 --- a/src/main/java/org/verapdf/pd/font/type1/Type1PrivateParser.java +++ b/src/main/java/org/verapdf/pd/font/type1/Type1PrivateParser.java @@ -26,7 +26,6 @@ import org.verapdf.cos.COSKey; import org.verapdf.parser.SeekableBaseParser; import org.verapdf.parser.Token; -import org.verapdf.pd.font.CFFNumber; import java.io.IOException; import java.io.InputStream; @@ -55,7 +54,7 @@ class Type1PrivateParser extends SeekableBaseParser { private final double[] fontMatrix; private final boolean isDefaultFontMatrix; private boolean charStringsFound; - private Map subrWidths; + private Type1Subroutines subroutines; private final COSKey key; @@ -129,10 +128,8 @@ private void processToken() throws IOException { break; case Type1StringConstants.SUBRS: nextToken(); - if (subrWidths == null) { - subrWidths = new HashMap<>(); - } int amountOfSubrs = (int) this.getToken().integer; + subroutines = new Type1Subroutines(this.getSource()); nextToken(); // reading "array" for (int i = 0; i < amountOfSubrs; ++i) { nextToken(); // reading "dup" @@ -147,14 +144,7 @@ private void processToken() throws IOException { this.skipSpaces(); long beginOffset = this.getSource().getOffset(); this.getSource().skip(toSkip); - try (ASInputStream chunk = this.getSource().getStream(beginOffset, toSkip); - ASInputStream eexecDecode = new EexecFilterDecode( - chunk, true, this.lenIV); ASInputStream decodedCharString = new ASMemoryInStream(eexecDecode)) { - Type1CharStringParser parser = new Type1CharStringParser(decodedCharString, subrWidths); - if (parser.getWidth() != null) { - subrWidths.put((int) number, parser.getWidth()); - } - } + subroutines.addSubroutine((int) number, beginOffset, toSkip, this.lenIV); this.nextToken(); // reading "NP" // some fonts have 'noaccess put' instead of 'NP'. Supporting this case as well if (this.getToken().getValue().equals(Type1StringConstants.NOACCESS)) { @@ -196,7 +186,7 @@ private boolean decodeCharString() throws IOException { try (ASInputStream chunk = this.getSource().getStream(beginOffset, charstringLength); ASInputStream eexecDecode = new EexecFilterDecode( chunk, true, this.lenIV); ASInputStream decodedCharString = new ASMemoryInStream(eexecDecode)) { - Type1CharStringParser parser = new Type1CharStringParser(decodedCharString, subrWidths); + Type1CharStringParser parser = new Type1CharStringParser(decodedCharString, subroutines); if (parser.getWidth() != null) { if (!isDefaultFontMatrix) { glyphWidths.put(glyphName, applyFontMatrix(parser.getWidth().getReal())); diff --git a/src/main/java/org/verapdf/pd/font/type1/Type1Subroutines.java b/src/main/java/org/verapdf/pd/font/type1/Type1Subroutines.java new file mode 100644 index 00000000..1aff8c11 --- /dev/null +++ b/src/main/java/org/verapdf/pd/font/type1/Type1Subroutines.java @@ -0,0 +1,92 @@ +/* + * 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 this program. If not, see https://www.gnu.org/licenses/gpl-3.0.en.html. + * + * The Mozilla Public License MPLv2+. + * You should have received a copy of the MPL license along with this program. + */ +package org.verapdf.pd.font.type1; + +import org.verapdf.as.io.ASInputStream; +import org.verapdf.io.SeekableInputStream; +import org.verapdf.pd.font.CFFNumber; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +final class Type1Subroutines { + private static final int MAX_SUBR_DEPTH = 100; + + private final SeekableInputStream source; + private final Map subroutines; + private final Map widths; + private final Set subrsWithoutWidth; + private final Set resolvingSubrs; + private int depth; + + Type1Subroutines(SeekableInputStream source) { + this.source = source; + this.subroutines = new HashMap<>(); + this.widths = new HashMap<>(); + this.subrsWithoutWidth = new HashSet<>(); + this.resolvingSubrs = new HashSet<>(); + } + + void addSubroutine(int number, long offset, long length, int lenIV) { + subroutines.put(number, new Subroutine(offset, length, lenIV)); + } + + CFFNumber getWidth(int subrNumber) throws IOException { + CFFNumber width = widths.get(subrNumber); + if (width != null || subrsWithoutWidth.contains(subrNumber) || + resolvingSubrs.contains(subrNumber) || depth >= MAX_SUBR_DEPTH) { + return width; + } + Subroutine subroutine = subroutines.get(subrNumber); + if (subroutine == null) { + return null; + } + resolvingSubrs.add(subrNumber); + depth++; + try { + Type1CharStringParser parser = new Type1CharStringParser(subroutine.open(source), this); + width = parser.getWidth(); + if (width != null) { + widths.put(subrNumber, width); + } else { + subrsWithoutWidth.add(subrNumber); + } + return width; + } finally { + depth--; + resolvingSubrs.remove(subrNumber); + } + } + + static final class Subroutine { + private final long offset; + private final long length; + private final int lenIV; + + Subroutine(long offset, long length, int lenIV) { + this.offset = offset; + this.length = length; + this.lenIV = lenIV; + } + + private ASInputStream open(SeekableInputStream source) throws IOException { + return new EexecFilterDecode(source.getStream(offset, length), true, lenIV); + } + } +} diff --git a/src/test/java/org/verapdf/pd/font/type1/Type1CharStringParserTest.java b/src/test/java/org/verapdf/pd/font/type1/Type1CharStringParserTest.java new file mode 100644 index 00000000..d4f56d3f --- /dev/null +++ b/src/test/java/org/verapdf/pd/font/type1/Type1CharStringParserTest.java @@ -0,0 +1,89 @@ +/* + * 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 this program. If not, see https://www.gnu.org/licenses/gpl-3.0.en.html. + * + * The Mozilla Public License MPLv2+. + * You should have received a copy of the MPL license along with this program. + */ +package org.verapdf.pd.font.type1; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.verapdf.as.io.ASMemoryInStream; +import org.verapdf.io.SeekableInputStream; + +import java.io.IOException; + +class Type1CharStringParserTest { + + @Test + void resolvesWidthThroughNestedSubrsRegardlessOfSubrOrder() throws IOException { + byte[] firstSubr = encrypt(charstring(number(2245), operator(10), operator(11))); + byte[] secondSubr = encrypt(charstring(number(0), number(239), operator(13), operator(11))); + byte[] sourceBytes = new byte[1 + firstSubr.length + secondSubr.length]; + System.arraycopy(firstSubr, 0, sourceBytes, 1, firstSubr.length); + System.arraycopy(secondSubr, 0, sourceBytes, 1 + firstSubr.length, secondSubr.length); + try (SeekableInputStream source = new ASMemoryInStream(sourceBytes)) { + Type1Subroutines subroutines = new Type1Subroutines(source); + subroutines.addSubroutine(2240, 1, firstSubr.length, 0); + subroutines.addSubroutine(2245, 1 + firstSubr.length, secondSubr.length, 0); + + Type1CharStringParser parser = new Type1CharStringParser( + new ASMemoryInStream(charstring(number(2240), operator(10), operator(14))), + subroutines); + + Assertions.assertEquals(239, parser.getWidth().getInteger()); + Type1CharStringParser cachedParser = new Type1CharStringParser( + new ASMemoryInStream(charstring(number(2240), operator(10), operator(14))), + subroutines); + Assertions.assertEquals(239, cachedParser.getWidth().getInteger()); + } + } + + private static byte[] charstring(byte[]... parts) { + int length = 0; + for (byte[] part : parts) { + length += part.length; + } + byte[] result = new byte[length]; + int offset = 0; + for (byte[] part : parts) { + System.arraycopy(part, 0, result, offset, part.length); + offset += part.length; + } + return result; + } + + private static byte[] number(int value) { + if (value >= -107 && value <= 107) { + return new byte[]{(byte) (value + 139)}; + } + return new byte[]{ + (byte) 255, (byte) (value >>> 24), (byte) (value >>> 16), + (byte) (value >>> 8), (byte) value + }; + } + + private static byte[] operator(int value) { + return new byte[]{(byte) value}; + } + + private static byte[] encrypt(byte[] cleartext) { + byte[] ciphertext = new byte[cleartext.length]; + int r = 4330; + for (int i = 0; i < cleartext.length; i++) { + int encoded = (cleartext[i] & 0xFF) ^ (r >> 8); + ciphertext[i] = (byte) encoded; + r = (encoded + r) * 52845 + 22719 & 0xFFFF; + } + return ciphertext; + } +}