Author: tilman
Date: Thu Sep 10 19:33:24 2026
New Revision: 1938083

Log:
PDFBOX-6259: hasBlendMode() now recurses into the resources of form XObjects + 
test, by Daniel Persson and Claude Fable 5.1

Added:
   
pdfbox/branches/2.0/examples/src/test/java/org/apache/pdfbox/examples/rendering/TestPDFRendererBlendMode.java
   (contents, props changed)
Modified:
   
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/rendering/PDFRenderer.java

Added: 
pdfbox/branches/2.0/examples/src/test/java/org/apache/pdfbox/examples/rendering/TestPDFRendererBlendMode.java
==============================================================================
--- /dev/null   00:00:00 1970   (empty, because file is newly added)
+++ 
pdfbox/branches/2.0/examples/src/test/java/org/apache/pdfbox/examples/rendering/TestPDFRendererBlendMode.java
       Thu Sep 10 19:33:24 2026        (r1938083)
@@ -0,0 +1,183 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDFormContentStream;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
+import org.apache.pdfbox.pdmodel.PDResources;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
+import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
+import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests that blend modes are detected wherever they are used on a page, so 
that the page is
+ * rendered on a transparent backdrop and composited onto white afterwards 
(PDFBOX-4095). A blend
+ * mode such as ColorBurn or Multiply applied directly onto an opaque white 
backdrop would make
+ * the blended content disappear.
+ */
+class TestPDFRendererBlendMode
+{
+    private static final int BLUE = 0xFF0000FF;
+    private static final int WHITE = 0xFFFFFFFF;
+
+    /**
+     * Blend mode set in the resources of the page itself.
+     */
+    @Test
+    void testBlendModeInPageResources() throws IOException
+    {
+        try (PDDocument doc = new PDDocument())
+        {
+            PDPage page = new PDPage(new PDRectangle(200, 200));
+            doc.addPage(page);
+            try (PDPageContentStream cs = new PDPageContentStream(doc, page))
+            {
+                drawBlueSquareWithColorBurn(cs);
+            }
+            assertBlueSquareVisible(doc);
+        }
+    }
+
+    /**
+     * Blend mode set in the resources of a form XObject that is drawn by the 
page. The page
+     * resources themselves don't have any blend mode.
+     */
+    @Test
+    void testBlendModeInFormXObject() throws IOException
+    {
+        try (PDDocument doc = new PDDocument())
+        {
+            PDPage page = new PDPage(new PDRectangle(200, 200));
+            doc.addPage(page);
+
+            PDFormXObject form = createForm(doc);
+            try (PDFormContentStream cs = new PDFormContentStream(form))
+            {
+                drawBlueSquareWithColorBurn(cs);
+            }
+            try (PDPageContentStream cs = new PDPageContentStream(doc, page))
+            {
+                cs.drawForm(form);
+            }
+            assertBlueSquareVisible(doc);
+        }
+    }
+
+    /**
+     * Blend mode set in the resources of a form XObject that is nested in 
another form XObject,
+     * as done by layout applications placing artwork from illustration 
applications.
+     */
+    @Test
+    void testBlendModeInNestedFormXObject() throws IOException
+    {
+        try (PDDocument doc = new PDDocument())
+        {
+            PDPage page = new PDPage(new PDRectangle(200, 200));
+            doc.addPage(page);
+
+            PDFormXObject inner = createForm(doc);
+            try (PDFormContentStream cs = new PDFormContentStream(inner))
+            {
+                drawBlueSquareWithColorBurn(cs);
+            }
+            PDFormXObject outer = createForm(doc);
+            try (PDFormContentStream cs = new PDFormContentStream(outer))
+            {
+                cs.drawForm(inner);
+            }
+            try (PDPageContentStream cs = new PDPageContentStream(doc, page))
+            {
+                cs.drawForm(outer);
+            }
+            assertBlueSquareVisible(doc);
+        }
+    }
+
+    /**
+     * A form XObject referencing itself must not cause endless recursion in 
the blend mode
+     * detection.
+     */
+    @Test
+    void testSelfReferencingFormXObject() throws IOException
+    {
+        try (PDDocument doc = new PDDocument())
+        {
+            PDPage page = new PDPage(new PDRectangle(200, 200));
+            doc.addPage(page);
+
+            PDFormXObject form = createForm(doc);
+            form.getResources().add(form);
+            try (PDFormContentStream cs = new PDFormContentStream(form))
+            {
+                cs.setNonStrokingColor(0f, 0f, 1f);
+                cs.addRect(50, 50, 100, 100);
+                cs.fill();
+            }
+            try (PDPageContentStream cs = new PDPageContentStream(doc, page))
+            {
+                cs.drawForm(form);
+            }
+            assertBlueSquareVisible(doc);
+        }
+    }
+
+    private static PDFormXObject createForm(PDDocument doc)
+    {
+        PDFormXObject form = new PDFormXObject(doc);
+        form.setBBox(new PDRectangle(200, 200));
+        form.setResources(new PDResources());
+        return form;
+    }
+
+    private static void drawBlueSquareWithColorBurn(PDPageContentStream cs) 
throws IOException
+    {
+        PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
+        gs.setBlendMode(BlendMode.COLOR_BURN);
+        cs.setGraphicsStateParameters(gs);
+        cs.setNonStrokingColor(0f, 0f, 1f);
+        cs.addRect(50, 50, 100, 100);
+        cs.fill();
+    }
+
+    private static void drawBlueSquareWithColorBurn(PDFormContentStream cs) 
throws IOException
+    {
+        PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
+        gs.setBlendMode(BlendMode.COLOR_BURN);
+        cs.setGraphicsStateParameters(gs);
+        cs.setNonStrokingColor(0f, 0f, 1f);
+        cs.addRect(50, 50, 100, 100);
+        cs.fill();
+    }
+
+    private static void assertBlueSquareVisible(PDDocument doc) throws 
IOException
+    {
+        BufferedImage image = new PDFRenderer(doc).renderImage(0, 1, 
ImageType.RGB);
+        assertEquals(BufferedImage.TYPE_INT_RGB, image.getType());
+        assertEquals(BLUE, image.getRGB(100, 100), "blended content must be 
visible on white");
+        assertEquals(WHITE, image.getRGB(25, 25), "background must stay 
white");
+    }
+}

Modified: 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/rendering/PDFRenderer.java
==============================================================================
--- 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/rendering/PDFRenderer.java
       Thu Sep 10 18:45:03 2026        (r1938082)
+++ 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/rendering/PDFRenderer.java
       Thu Sep 10 19:33:24 2026        (r1938083)
@@ -24,18 +24,23 @@ import java.awt.GraphicsDevice;
 import java.awt.RenderingHints;
 import java.awt.image.BufferedImage;
 import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.StringTokenizer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.cos.COSDictionary;
 import org.apache.pdfbox.cos.COSName;
 import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.pdmodel.PDPage;
 import org.apache.pdfbox.pdmodel.PDPageTree;
 import org.apache.pdfbox.pdmodel.PDResources;
 import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.graphics.PDXObject;
 import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
+import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
 import 
org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup;
 import 
org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties;
 import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
@@ -578,9 +583,25 @@ public class PDFRenderer
 
     private boolean hasBlendMode(PDPage page)
     {
-        // check the current resources for blend modes
-        PDResources resources = page.getResources();
-        if (resources == null)
+        // check the page resources and the resources of nested form XObjects
+        return hasBlendMode(page.getResources(), new HashSet<COSDictionary>());
+    }
+
+    /**
+     * Checks whether the given resources, or the resources of any form 
XObject reachable from
+     * them, contain an extended graphics state with a blend mode other than 
Normal.
+     * Blend modes are often set inside form XObjects (e.g. artwork placed 
into a layout), so
+     * checking only the page level resources is not enough: a blend mode like 
ColorBurn or
+     * Multiply applied to an opaque white backdrop would make the content 
disappear.
+     *
+     * @param resources the resources to check, may be null.
+     * @param visited the resource dictionaries that have already been 
checked, to avoid endless
+     * recursion for self-referencing forms.
+     * @return true if a blend mode other than Normal was found.
+     */
+    private boolean hasBlendMode(PDResources resources, Set<COSDictionary> 
visited)
+    {
+        if (resources == null || !visited.add(resources.getCOSObject()))
         {
             return false;
         }
@@ -598,6 +619,30 @@ public class PDFRenderer
                 }
             }
         }
+        for (COSName name : resources.getXObjectNames())
+        {
+            if (resources.isImageXObject(name))
+            {
+                // avoid creating image XObjects, they can't have resources
+                continue;
+            }
+            PDXObject xObject;
+            try
+            {
+                xObject = resources.getXObject(name);
+            }
+            catch (IOException ex)
+            {
+                // broken XObject, will be reported again when the page is 
drawn
+                LOG.debug("Can't check XObject " + name + " for blend modes", 
ex);
+                continue;
+            }
+            if (xObject instanceof PDFormXObject
+                    && hasBlendMode(((PDFormXObject) xObject).getResources(), 
visited))
+            {
+                return true;
+            }
+        }
         return false;
     }
 

Reply via email to