diff --git a/pdfbox/pom.xml b/pdfbox/pom.xml
index 9938c9e894b..985fb5192c1 100644
--- a/pdfbox/pom.xml
+++ b/pdfbox/pom.xml
@@ -1046,6 +1046,19 @@
cd6d92c643108cdcf2b4c6a4ae4cc5a15d848e3335f6c8a135a5438d691eb982d11cfd8f26d11839321e06a364126d9b0c9732b4a19785821b844fe0ec94d525
+
+ PDFBOX-5876
+ generate-test-resources
+
+ wget
+
+
+ https://issues.apache.org/jira/secure/attachment/13071244/jpeg2000.pdf
+ ${project.build.directory}/pdfs
+ PDFBOX-5876-jpeg2000.pdf
+ 5eb020282f3998c7673983625303fb7634a3ca2a2fc65efc7bd123241a7facae999610bbd38d8a6be8fc26752c7241d806d46252448ba600e2beb118f4567cf3
+
+
diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
index c1cc131bf6e..ffda68ea56e 100644
--- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
+++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
@@ -84,6 +84,9 @@ public final class PDImageXObject extends PDXObject implements PDImage
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)
*/
@@ -739,7 +742,9 @@ private void initJPXValues()
// 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: bound the subsampling of this metadata-only read, see field javadoc.
+ DecodeOptions options = new DecodeOptions(JPX_METADATA_SUBSAMPLING);
+ try (COSInputStream is = stream.createInputStream(options))
{
DecodeResult decodeResult = is.getDecodeResult();
stream.getCOSObject().addAll(decodeResult.getParameters());
diff --git a/pdfbox/src/test/java/org/apache/pdfbox/rendering/JPXLowMemoryRenderMain.java b/pdfbox/src/test/java/org/apache/pdfbox/rendering/JPXLowMemoryRenderMain.java
new file mode 100644
index 00000000000..d24e181777c
--- /dev/null
+++ b/pdfbox/src/test/java/org/apache/pdfbox/rendering/JPXLowMemoryRenderMain.java
@@ -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);
+ }
+ }
+}
diff --git a/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java b/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java
index af18a07ebbc..f61be5232f6 100644
--- a/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java
+++ b/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java
@@ -19,6 +19,8 @@
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.pdmodel.PDDocument;
@@ -126,4 +128,34 @@ void testPDFBox5403() throws IOException
"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
+ 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(process.getInputStream().readAllBytes(), 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);
+ }
}