Author: tilman
Date: Tue Sep  8 12:02:47 2026
New Revision: 1937993

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/2.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/JPXLowMemoryRenderMain.java
   (contents, props changed)
Modified:
   
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
   
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java

Modified: 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
==============================================================================
--- 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
       Tue Sep  8 11:14:31 2026        (r1937992)
+++ 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/PDImageXObject.java
       Tue Sep  8 12:02:47 2026        (r1937993)
@@ -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)
      */
@@ -751,7 +754,12 @@ public final class PDImageXObject extend
         COSInputStream is = null;
         try
         {
-            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.
+            is = mayHaveJPXSMask()
+                ? stream.createInputStream()
+                : stream.createInputStream(new 
DecodeOptions(JPX_METADATA_SUBSAMPLING));
             DecodeResult decodeResult = is.getDecodeResult();
             stream.getCOSObject().addAll(decodeResult.getParameters());
             if (colorSpace == null)
@@ -772,6 +780,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/2.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/2.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/JPXLowMemoryRenderMain.java
    Tue Sep  8 12:02:47 2026        (r1937993)
@@ -0,0 +1,43 @@
+/*
+ * 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.io.MemoryUsageSetting;
+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]);
+        PDDocument doc = PDDocument.load(file, 
MemoryUsageSetting.setupTempFileOnly());
+        PDFRenderer renderer = new PDFRenderer(doc);
+        renderer.setSubsamplingAllowed(true);
+        renderer.renderImage(0, 0.5f);
+        doc.close();
+    }
+}

Modified: 
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java
==============================================================================
--- 
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java
       Tue Sep  8 11:14:31 2026        (r1937992)
+++ 
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java
       Tue Sep  8 12:02:47 2026        (r1937993)
@@ -19,10 +19,18 @@ package org.apache.pdfbox.rendering;
 import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
-import org.apache.pdfbox.pdmodel.PDDocument;
+import java.util.concurrent.TimeUnit;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
+
+import org.apache.pdfbox.io.IOUtils;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.util.Charsets;
+
+import static org.junit.Assume.assumeTrue;
+
 import org.junit.Test;
 
 /**
@@ -97,4 +105,36 @@ public class TestQuality
         assertTrue("expected a dark text pixel but was too light: " + 
Integer.toHexString(rgb), red < 100);
         doc.close();
     }
+
+    /**
+     * 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
+    public void testPDFBox5876() throws IOException, InterruptedException
+    {
+        String featureFlag = System.getProperty("TestOOM");
+        assumeTrue("true".equals(featureFlag));
+        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()), Charsets.UTF_8);
+        boolean finished = process.waitFor(120, TimeUnit.SECONDS);
+        assertTrue("subprocess timed out", finished);
+        assertEquals("subprocess failed:\n" + output, 0, process.exitValue());
+    }
 }

Reply via email to