diff --git a/pdfbox/pom.xml b/pdfbox/pom.xml index d0e187db690..71b4f0e4263 100644 --- a/pdfbox/pom.xml +++ b/pdfbox/pom.xml @@ -996,6 +996,32 @@ 2ee83beb6658792c6b963010462515ff1187aafe64930c1a7465dc26e86ab5236dc98cb006986e5fcd60876bedef7f00e4e1cb86bbf590bba000a00187af0ee3 + + PDFBOX-6077-1 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/13078553/example.pdf + ${project.build.directory}/pdfs + PDFBOX-6077-example.pdf + f4faaa68062073ffdfd48119563f1e1a6eff6d9a063188dc2ec59ccc747277fcaa6139936cebf2f3f7b1b99b80ca13934fb4568658a169e0bf25bf4e748de101 + + + + PDFBOX-6077-2 + generate-test-resources + + wget + + + https://issues.apache.org/jira/secure/attachment/13078557/PDFBOX-5842-reduced.pdf + ${project.build.directory}/pdfs + PDFBOX-5842-reduced.pdf + c66b13230d343d6b0a2de81680f5d1ae7f8d92713acbc453ff36fe085831fa89127e0428d297e7494b87fc932ca8a721744db1fe79a41cc5c3cf7d631fc209fc + + diff --git a/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java b/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java index 7374b404b5f..93a50e380f9 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java @@ -1135,6 +1135,15 @@ public void drawImage(PDImage pdImage) throws IOException // draw the paint Paint paint = getNonStrokingPaint(); + // PDFBOX-6077: a soft mask's Paint/PaintContext machinery (see + // applySoftMaskToPaint()) assumes it is asked to render directly onto the real + // page raster, using its own cached absolute page-device coordinates. This + // stencil-mask-with-pattern case instead renders into an isolated scratch image + // (see the note above about "device scale is not used"), so unwrap any soft mask + // here, fill with the plain underlying paint below, and apply the soft mask's own + // alpha afterwards by directly looking up its backing raster (applySoftMaskAlpha). + SoftMask softMask = paint instanceof SoftMask ? (SoftMask) paint : null; + Paint innerPaint = softMask != null ? softMask.getPaint() : paint; Rectangle2D unitRect = new Rectangle2D.Float(0, 0, 1, 1); Rectangle2D bounds = at.createTransformedShape(unitRect).getBounds2D(); int w = (int) Math.ceil(bounds.getWidth()); @@ -1142,11 +1151,16 @@ public void drawImage(PDImage pdImage) throws IOException BufferedImage renderedPaint = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) renderedPaint.getGraphics(); g.translate(-bounds.getMinX(), -bounds.getMinY()); - g.setPaint(paint); + g.setPaint(innerPaint); g.setRenderingHints(graphics.getRenderingHints()); g.fill(bounds); g.dispose(); + if (softMask != null) + { + applySoftMaskAlpha(renderedPaint, bounds, softMask); + } + // draw the mask BufferedImage mask = pdImage.getImage(); AffineTransform imageTransform = new AffineTransform(at); @@ -1225,7 +1239,11 @@ else if (scaleX != 0 && scaleY != 0) { alphaPixel = alpha.getPixel(x, y, alphaPixel); rasterPixel = raster.getPixel(x, y, rasterPixel); - rasterPixel[3] = alphaPixel[0]; + // PDFBOX-6077: combine with the paint's own alpha instead of + // overwriting it, so gaps the paint never drew into (e.g. between + // tiles of a tiling pattern) stay transparent instead of turning + // into opaque black. + rasterPixel[3] = rasterPixel[3] * alphaPixel[0] / 255; raster.setPixel(x, y, rasterPixel); } } @@ -1267,6 +1285,78 @@ else if (scaleX != 0 && scaleY != 0) } } + /** + * PDFBOX-6077: applies a soft mask's alpha directly to "image", which was filled with the + * soft mask's underlying paint but not yet masked by it. This re-implements + * {@link SoftMask}'s own alpha lookup (see its SoftPaintContext.getRaster()) rather than + * relying on the Paint/PaintContext machinery, because "image" is a small scratch buffer - + * not the real page raster that the soft mask's absolute device coordinates are relative to + * - and unlike a plain coordinate offset, transforming each pixel individually stays correct + * even though this scratch buffer isn't rendered at the page's actual device scale (see the + * "device scale is not used" note where this method is called from). + * + * @param image the ARGB image to apply the soft mask's alpha to, in place. + * @param bounds the device-independent bounds (see "at" in drawImage()) that image's pixel + * (0, 0) to (image.getWidth(), image.getHeight()) covers. + * @param softMask the soft mask to apply. + */ + private void applySoftMaskAlpha(BufferedImage image, Rectangle2D bounds, SoftMask softMask) throws IOException + { + AffineTransform deviceTransform = graphics.getTransform(); + Raster maskRaster = softMask.getMask().getRaster(); + Rectangle2D bboxDevice = softMask.getBBoxDevice(); + int backdropColorValue = softMask.getBackdropColorValue(); + PDFunction transferFunction = softMask.getTransferFunction(); + Float[] map = transferFunction != null ? new Float[256] : null; + float[] input = transferFunction != null ? new float[1] : null; + + WritableRaster raster = image.getRaster(); + int width = image.getWidth(); + int height = image.getHeight(); + Point2D.Double point = new Point2D.Double(); + int[] gray = new int[1]; + int[] rasterPixel = null; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + point.setLocation(bounds.getMinX() + x, bounds.getMinY() + y); + deviceTransform.transform(point, point); + int maskX = (int) Math.floor(point.getX() - bboxDevice.getX()); + int maskY = (int) Math.floor(point.getY() - bboxDevice.getY()); + + int alphaScale; + if (maskX >= 0 && maskY >= 0 && maskX < maskRaster.getWidth() && maskY < maskRaster.getHeight()) + { + maskRaster.getPixel(maskX, maskY, gray); + if (transferFunction != null) + { + Float f = map[gray[0]]; + if (f == null) + { + input[0] = gray[0] / 255f; + f = transferFunction.eval(input)[0]; + map[gray[0]] = f; + } + alphaScale = Math.round(255 * f); + } + else + { + alphaScale = gray[0]; + } + } + else + { + alphaScale = backdropColorValue; + } + + rasterPixel = raster.getPixel(x, y, rasterPixel); + rasterPixel[3] = rasterPixel[3] * alphaScale / 255; + raster.setPixel(x, y, rasterPixel); + } + } + } + /** * Calculates the subsampling frequency for a given PDImage based on the current transformation * and its calculated transform. Extend this method if you want to use your own strategy. diff --git a/pdfbox/src/main/java/org/apache/pdfbox/rendering/SoftMask.java b/pdfbox/src/main/java/org/apache/pdfbox/rendering/SoftMask.java index 4dbc5da8c1e..3ddc031faee 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/rendering/SoftMask.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/rendering/SoftMask.java @@ -98,6 +98,35 @@ class SoftMask implements Paint } } + // PDFBOX-6077: accessors used by PageDrawer to re-implement this soft mask's alpha lookup + // directly (with its own, correctly-scaled device coordinates) when this Paint is rendered + // into a scratch image that isn't the real page raster, e.g. for a stencil mask filled with + // a pattern. + Paint getPaint() + { + return paint; + } + + BufferedImage getMask() + { + return mask; + } + + Rectangle2D getBBoxDevice() + { + return bboxDevice; + } + + int getBackdropColorValue() + { + return bc; + } + + PDFunction getTransferFunction() + { + return transferFunction; + } + @Override public PaintContext createContext(ColorModel cm, Rectangle deviceBounds, Rectangle2D userBounds, AffineTransform xform, 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 8a9b18cc277..a0a56f96759 100644 --- a/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java +++ b/pdfbox/src/test/java/org/apache/pdfbox/rendering/TestQuality.java @@ -56,4 +56,48 @@ void testPDFBox4831() throws IOException ValidateXImage.checkIdent(extractedImage, renderedImage); } } + + /** + * PDFBOX-6077: a stencil mask filled with a pattern must not paint the gaps between the + * pattern's own tiles as opaque black. Before the fix, the stencil mask's alpha overwrote + * the pattern paint's own alpha instead of being combined with it, so any pixel the pattern + * didn't itself draw into turned solid black instead of staying transparent. + * + * @throws IOException + */ + @Test + void testPDFBox6077() throws IOException + { + File file = new File(TARGET_PDF_DIR, "PDFBOX-6077-example.pdf"); + try (PDDocument doc = Loader.loadPDF(file)) + { + PDFRenderer renderer = new PDFRenderer(doc); + BufferedImage renderedImage = renderer.renderImageWithDPI(0, 100); + // a gap between the tiling pattern's own painted tiles, which must stay transparent + // (i.e. show the white page background) instead of turning opaque black + Assertions.assertEquals(0xFFFFFFFF, renderedImage.getRGB(280, 23)); + } + } + + /** + * PDFBOX-6077: a soft mask applied to a pattern that is used as a stencil mask fill must + * still be visible. Such a pattern is rendered into a separate scratch image rather than + * directly onto the page, and the soft mask's own alpha lookup is keyed to absolute + * page-device pixel coordinates, so a naive implementation renders it as fully transparent. + * + * @throws IOException + */ + @Test + void testPDFBox5842() throws IOException + { + File file = new File(TARGET_PDF_DIR, "PDFBOX-5842-reduced.pdf"); + try (PDDocument doc = Loader.loadPDF(file)) + { + PDFRenderer renderer = new PDFRenderer(doc); + BufferedImage renderedImage = renderer.renderImageWithDPI(0, 100); + // a pixel within the soft-masked pattern's map marker icon; if the soft mask's alpha + // lookup is broken, this whole region renders as blank white instead + Assertions.assertNotEquals(0xFFFFFFFF, renderedImage.getRGB(267, 1329)); + } + } }