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