From 3b4ebbe71fb70a2593f07ca129d16fea9adc53fb Mon Sep 17 00:00:00 2001 From: Bundo Lee Date: Mon, 31 Aug 2026 13:24:40 +0900 Subject: [PATCH] Stop throwing NPE on Type3 char proc metrics with no glyph bbox getAscent() and getDescent() returned primitive double while the backing fields are Double and legitimately stay null: a char proc beginning with the d0 operator specifies the glyph width only (ISO 32000-1, 9.6.5.3) and declares no glyph bounding box, so parse() leaves both unset. Reading either metric for such a glyph threw NullPointerException while unboxing, and a single d0 glyph anywhere in a font was enough to trigger it. Both getters shipped in v1.31.48, so their descriptors are kept intact to stay binary compatible: they now substitute 0 for an undefined metric instead of throwing, and are deprecated in favour of getAscentOrNull() / getDescentOrNull(), which tell "no glyph bounding box" from a real 0. PDType3Font reads the nullable accessors; without that the unboxing NPE would simply move up one frame, since those methods are declared Double and already return null when the char proc cannot be parsed. 0 is only a safe substitute for the deprecated path because it keeps existing callers running, not because it is a correct metric: ascent and descent are aggregated by maximum and minimum across a font's glyphs, so a 0 descent wins the minimum against the real negative descents around it and flattens the text line box. Add Type3CharProcParserTest covering the d1, d0, deprecated-getter and corrupted char proc cases; the d0 case reproduces the NullPointerException against the original getters. Co-Authored-By: Claude Opus 5 (1M context) --- .../verapdf/pd/font/type3/PDType3Font.java | 4 +- .../pd/font/type3/Type3CharProcParser.java | 36 ++++++- .../font/type3/Type3CharProcParserTest.java | 102 ++++++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/verapdf/pd/font/type3/Type3CharProcParserTest.java diff --git a/src/main/java/org/verapdf/pd/font/type3/PDType3Font.java b/src/main/java/org/verapdf/pd/font/type3/PDType3Font.java index 6d48b4f6..972381ed 100644 --- a/src/main/java/org/verapdf/pd/font/type3/PDType3Font.java +++ b/src/main/java/org/verapdf/pd/font/type3/PDType3Font.java @@ -163,7 +163,7 @@ public Double getAscentFromProgram(int code) { if (charProc.getType() == COSObjType.COS_STREAM) { try (Type3CharProcParser parser = new Type3CharProcParser(charProc.getData(COSStream.FilterFlags.DECODE))) { parser.parse(); - return parser.getAscent(); + return parser.getAscentOrNull(); } catch (IOException e) { LOGGER.log(Level.FINE, "Can't get ascent from type 3 char proc"); } @@ -176,7 +176,7 @@ public Double getDescentFromProgram(int code) { if (charProc.getType() == COSObjType.COS_STREAM) { try (Type3CharProcParser parser = new Type3CharProcParser(charProc.getData(COSStream.FilterFlags.DECODE))) { parser.parse(); - return parser.getDescent(); + return parser.getDescentOrNull(); } catch (IOException e) { LOGGER.log(Level.FINE, "Can't get descent from type 3 char proc"); } diff --git a/src/main/java/org/verapdf/pd/font/type3/Type3CharProcParser.java b/src/main/java/org/verapdf/pd/font/type3/Type3CharProcParser.java index 8e711b57..dee6ead1 100644 --- a/src/main/java/org/verapdf/pd/font/type3/Type3CharProcParser.java +++ b/src/main/java/org/verapdf/pd/font/type3/Type3CharProcParser.java @@ -92,11 +92,45 @@ public double getWidth() { return width; } + /** + * @return ascent of the glyph presented by given char proc, or 0 if the char + * proc declares no glyph bounding box. + * @deprecated a char proc that begins with the d0 operator specifies the + * glyph width only (ISO 32000-1, 9.6.5.3) and declares no glyph bounding + * box, so its ascent is undefined rather than 0. This method cannot express + * that; use {@link #getAscentOrNull()} instead. + */ + @Deprecated public double getAscent() { - return ascent; + return ascent == null ? 0 : ascent; } + /** + * @return descent of the glyph presented by given char proc, or 0 if the + * char proc declares no glyph bounding box. + * @deprecated undefined descent cannot be distinguished from 0; use + * {@link #getDescentOrNull()} instead. See {@link #getAscent()}. + */ + @Deprecated public double getDescent() { + return descent == null ? 0 : descent; + } + + /** + * @return ascent of the glyph presented by given char proc, or null if the + * char proc declares no glyph bounding box. A char proc that begins with + * the d0 operator specifies the glyph width only (ISO 32000-1, 9.6.5.3), + * so its ascent is undefined rather than 0. + */ + public Double getAscentOrNull() { + return ascent; + } + + /** + * @return descent of the glyph presented by given char proc, or null if the + * char proc declares no glyph bounding box. See {@link #getAscentOrNull()}. + */ + public Double getDescentOrNull() { return descent; } } diff --git a/src/test/java/org/verapdf/pd/font/type3/Type3CharProcParserTest.java b/src/test/java/org/verapdf/pd/font/type3/Type3CharProcParserTest.java new file mode 100644 index 00000000..4aa10bb3 --- /dev/null +++ b/src/test/java/org/verapdf/pd/font/type3/Type3CharProcParserTest.java @@ -0,0 +1,102 @@ +/* + * 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.type3; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.verapdf.as.io.ASMemoryInStream; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +class Type3CharProcParserTest { + + private static final double EPSILON = 1.0e-6; + + private static Type3CharProcParser parse(String charProc) throws IOException { + Type3CharProcParser parser = new Type3CharProcParser( + new ASMemoryInStream(charProc.getBytes(StandardCharsets.ISO_8859_1))); + parser.parse(); + return parser; + } + + /** + * A char proc that starts with d1 declares a glyph bounding box, so both + * metrics are available. + */ + @Test + void d1CharProcReportsMetrics() throws IOException { + try (Type3CharProcParser parser = parse("1000 0 0 -200 750 800 d1\n")) { + Assertions.assertEquals(1000, parser.getWidth(), EPSILON); + Assertions.assertEquals(800, parser.getAscentOrNull(), EPSILON); + Assertions.assertEquals(-200, parser.getDescentOrNull(), EPSILON); + } + } + + /** + * A char proc that starts with d0 specifies the glyph width only + * (ISO 32000-1, 9.6.5.3). Its ascent and descent are undefined, so they + * must be reported as null rather than defaulted to zero: a zero descent + * would win a minimum comparison against the real descents of the other + * glyphs in the same font. + */ + @Test + void d0CharProcReportsNullMetrics() throws IOException { + try (Type3CharProcParser parser = parse("1000 0 d0\n")) { + Assertions.assertEquals(1000, parser.getWidth(), EPSILON); + Assertions.assertNull(parser.getAscentOrNull()); + Assertions.assertNull(parser.getDescentOrNull()); + } + } + + /** + * The pre-existing primitive getters keep their released signatures and + * substitute 0 for an undefined metric instead of throwing. + */ + @Test + @SuppressWarnings("deprecation") + void deprecatedPrimitiveGettersSubstituteZeroForUndefinedMetrics() throws IOException { + try (Type3CharProcParser parser = parse("1000 0 d0\n")) { + Assertions.assertEquals(0, parser.getAscent(), EPSILON); + Assertions.assertEquals(0, parser.getDescent(), EPSILON); + } + try (Type3CharProcParser parser = parse("1000 0 0 -200 750 800 d1\n")) { + Assertions.assertEquals(800, parser.getAscent(), EPSILON); + Assertions.assertEquals(-200, parser.getDescent(), EPSILON); + } + } + + /** + * A char proc whose bounding box is not terminated by d1 is corrupted; + * parse() reports it and leaves no metrics behind. + */ + @Test + void corruptedCharProcThrowsAndClearsMetrics() throws IOException { + Type3CharProcParser parser = new Type3CharProcParser( + new ASMemoryInStream("1000 0 0 -200 750 800 xx\n".getBytes(StandardCharsets.ISO_8859_1))); + try (Type3CharProcParser closeable = parser) { + Assertions.assertThrows(IOException.class, closeable::parse); + } + Assertions.assertEquals(-1, parser.getWidth(), EPSILON); + Assertions.assertNull(parser.getAscentOrNull()); + Assertions.assertNull(parser.getDescentOrNull()); + } +}