Author: tilman
Date: Tue Sep 8 12:02:52 2026
New Revision: 1937994
Log:
PDFBOX-5876: use upper bound subsampling during initJPXValues() except when
smask in data + add OOM test by Valery Bokov assisted by Claude; closes #527
Added:
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/JPXLowMemoryRenderMain.java
(contents, props changed)
Modified:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java
Modified:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
==============================================================================
---
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
Tue Sep 8 12:02:47 2026 (r1937993)
+++
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
Tue Sep 8 12:02:52 2026 (r1937994)
@@ -85,6 +85,9 @@ public final class PDImageXObject extend
private boolean jpxValuesInitialized = false;
private BufferedImage jpxSMask = null;
+ // PDFBOX-5876: upper bound for the subsampling used by initJPXValues
method.
+ private static final int JPX_METADATA_SUBSAMPLING = 8;
+
/**
* current resource dictionary (has color spaces)
*/
@@ -745,7 +748,12 @@ public final class PDImageXObject extend
// bits per component
// the colorspace of the image is used if the dictionary doesn't
provide any value
PDStream stream = getStream();
- try (COSInputStream is = stream.createInputStream())
+ // PDFBOX-5876: subsample this metadata-only read, see the
JPX_METADATA_SUBSAMPLING field.
+ // Not when a soft mask may be extracted from the image data, because
that mask is kept
+ // and used later at its own resolution, so it must not be subsampled.
+ try (COSInputStream is = mayHaveJPXSMask()
+ ? stream.createInputStream()
+ : stream.createInputStream(new
DecodeOptions(JPX_METADATA_SUBSAMPLING)))
{
DecodeResult decodeResult = is.getDecodeResult();
stream.getCOSObject().addAll(decodeResult.getParameters());
@@ -763,6 +771,19 @@ public final class PDImageXObject extend
}
/**
+ * Tells whether decoding this image may produce a soft mask taken from
the image data
+ * (PDFBOX-5657). {@code JPXFilter} only extracts such a mask when the
image dictionary has no
+ * /ColorSpace entry and a positive /SMaskInData entry, so both are
checked here to mirror it.
+ *
+ * @return true if a soft mask may be extracted from the image data.
+ */
+ private boolean mayHaveJPXSMask()
+ {
+ COSDictionary dict = getCOSObject();
+ return !dict.containsKey(COSName.COLORSPACE) &&
dict.getInt(COSName.SMASK_IN_DATA) > 0;
+ }
+
+ /**
* High-quality image scaling.
*/
private static BufferedImage scaleImage(BufferedImage image, int width,
int height, int type, boolean interpolate)
Added:
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/JPXLowMemoryRenderMain.java
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/JPXLowMemoryRenderMain.java
Tue Sep 8 12:02:52 2026 (r1937994)
@@ -0,0 +1,45 @@
+/*
+ * 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.rendering;
+
+import java.io.File;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.io.IOUtils;
+import org.apache.pdfbox.pdmodel.PDDocument;
+
+/**
+ * Renders the first page of a PDF at half scale, the same way as reported in
PDFBOX-5876. Run in
+ * its own JVM with a constrained heap by {@link
TestQuality#testPDFBox5876()}, since the heap size
+ * of the JVM already running the test suite can't be changed after the fact.
+ */
+public final class JPXLowMemoryRenderMain
+{
+ private JPXLowMemoryRenderMain()
+ {
+ }
+
+ public static void main(String[] args) throws Exception
+ {
+ File file = new File(args[0]);
+ try (PDDocument doc = Loader.loadPDF(file,
IOUtils.createTempFileOnlyStreamCache()))
+ {
+ PDFRenderer renderer = new PDFRenderer(doc);
+ renderer.setSubsamplingAllowed(true);
+ renderer.renderImage(0, 0.5f);
+ }
+ }
+}
Modified:
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java
==============================================================================
---
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java
Tue Sep 8 12:02:47 2026 (r1937993)
+++
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java
Tue Sep 8 12:02:52 2026 (r1937994)
@@ -19,13 +19,19 @@ package org.apache.pdfbox.rendering;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.image.ValidateXImage;
+
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
/**
*
@@ -126,4 +132,35 @@ class TestQuality
"expected a dark text pixel but was too light: " +
Integer.toHexString(rgb));
}
}
+
+ /**
+ * PDFBOX-5876: rendering a page containing a very large JPEG 2000 (JPX)
image at reduced
+ * scale must not decode the image at full resolution first just to read
its width, height
+ * and color space. Before the fix, {@code PDImageXObject.initJPXValues()}
did exactly that,
+ * on top of the properly subsampled decode done afterwards for the actual
rendering, so
+ * memory usage was driven by the full image size regardless of how small
the rendered output
+ * was. This must run in a separate, heap-constrained JVM, since the heap
size of the JVM
+ * already running the test suite can't be changed after the fact, and the
failure (an
+ * OutOfMemoryError) only reproduces below a certain heap size.
+ *
+ * @throws IOException
+ * @throws InterruptedException
+ */
+ @Test
+ @EnabledIfSystemProperty(named = "TestOOM", matches = "true")
+ void testPDFBox5876() throws IOException, InterruptedException
+ {
+ File file = new File(TARGET_PDF_DIR, "PDFBOX-5876-jpeg2000.pdf");
+ String javaBin = System.getProperty("java.home") + File.separator +
"bin" +
+ File.separator + "java";
+ ProcessBuilder builder = new ProcessBuilder(javaBin, "-Xmx600m",
+ "-cp", System.getProperty("java.class.path"),
+ JPXLowMemoryRenderMain.class.getName(),
file.getAbsolutePath());
+ builder.redirectErrorStream(true);
+ Process process = builder.start();
+ String output = new
String(IOUtils.toByteArray(process.getInputStream()), StandardCharsets.UTF_8);
+ boolean finished = process.waitFor(120, TimeUnit.SECONDS);
+ Assertions.assertTrue(finished, "subprocess timed out");
+ Assertions.assertEquals(0, process.exitValue(), "subprocess failed:\n"
+ output);
+ }
}