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); + } }