diff --git a/pdfbox/src/test/java/org/apache/pdfbox/printing/TestPDFPrintable.java b/pdfbox/src/test/java/org/apache/pdfbox/printing/TestPDFPrintable.java new file mode 100644 index 00000000000..fad98e669af --- /dev/null +++ b/pdfbox/src/test/java/org/apache/pdfbox/printing/TestPDFPrintable.java @@ -0,0 +1,254 @@ +/* + * 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.printing; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.print.PageFormat; +import java.awt.print.Paper; +import java.awt.print.Printable; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link PDFPrintable}. + */ +class TestPDFPrintable +{ + private final int IMAGE_WIDTH = 100; + private final int IMAGE_HEIGHT = 100; + + /** + * Tests that the page border is drawn with Color.GRAY when showPageBorder is true. + * + * Without rasterization, {@code graphics} and {@code graphics2D} are the same object, + * so setColor(GRAY) and drawRect() both act on the same Graphics2D. The border is drawn + * correctly. + */ + @Test + void testShowPageBorderIsGrayWithoutRasterization() throws Exception + { + testShowPageBorderIsGray(PDFPrintable.RASTERIZE_OFF); + } + + /** + * Tests that the page border is drawn with Color.GRAY when rasterizing. + * + *

When rasterizing (dpi > 0), the border is drawn on a raster image that is + * larger than the page (scaled by dpiScale), then blitted down to the output. + * The {@code setClip} in the showPageBorder block must use raster-pixel dimensions + * (imageableWidth * scale), not raw point dimensions (imageableWidth). Otherwise + * the clip is too small, the border is drawn in only a fraction of the raster image, + * and the thin border line gets lost during the scale-down blit to the output.

+ */ + @Test + void testShowPageBorderIsGrayWithRasterization() throws Exception + { + testShowPageBorderIsGray(150f); + } + + /** + * {@code print()} would otherwise mutate the caller's Graphics2D in several places: + * translate() for imageable area and centering, scale() during rasterization, + * setBackground() before the raster blit, and setColor / setStroke / setClip / setTransform + * in the showPageBorder block. To isolate the caller, {@code print()} works on a private + * copy obtained via {@code graphics.create()} and disposes it in the finally block, so none + * of those mutations reach the caller. This test verifies that isolation by setting + * distinctive state on the caller's Graphics2D before {@code print()} and asserting it is + * unchanged afterwards. + */ + @Test + void testPrinterGraphicsStateIsUnchangedAfterPrint() throws Exception + { + assertPrinterGraphicsStateUnchanged(PDFPrintable.RASTERIZE_OFF); + } + + @Test + void testPrinterGraphicsStateIsUnchangedAfterPrintWhenRasterizing() throws Exception + { + assertPrinterGraphicsStateUnchanged(150f); + } + + private void assertPrinterGraphicsStateUnchanged(float dpi) throws Exception + { + try (PDDocument doc = new PDDocument()) + { + doc.addPage(new PDPage(new PDRectangle(IMAGE_WIDTH, IMAGE_HEIGHT))); + + PDFPrintable printable = new PDFPrintable(doc, Scaling.ACTUAL_SIZE, true, dpi); + + BufferedImage output = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = output.createGraphics(); + + // set a distinctive transform so we can detect leaks from internal translate()/scale() calls + g2d.translate(7.0, 11.0); + g2d.scale(1.3, 1.3); + + Color originalColor = Color.RED; + Color originalBackground = Color.BLUE; + Stroke originalStroke = new BasicStroke(3.7f); + g2d.setColor(originalColor); + g2d.setBackground(originalBackground); + g2d.setStroke(originalStroke); + AffineTransform originalTransform = g2d.getTransform(); + Rectangle2D originalClipDeviceBounds = deviceClipBounds(g2d); + + PageFormat pf = createPageFormat(IMAGE_WIDTH, IMAGE_HEIGHT); + int result = printable.print(g2d, pf, 0); + + assertEquals(Printable.PAGE_EXISTS, result); + assertEquals(originalColor, g2d.getColor(), + "color should be unchanged after print()"); + assertEquals(originalBackground, g2d.getBackground(), + "background should be unchanged after print()"); + assertEquals(originalStroke, g2d.getStroke(), + "stroke should be unchanged after print()"); + assertEquals(originalTransform, g2d.getTransform(), + "transform should be unchanged after print() (translate/scale inside print() must not leak)"); + // device-space comparison — invariant under transform changes on the same Graphics2D + assertEquals(originalClipDeviceBounds, deviceClipBounds(g2d), + "clip should be unchanged after print()"); + + g2d.dispose(); + } + } + + /** + * Returns the bounds of the current clip projected into device space via the current transform. + * This is stable across transform changes on the same Graphics2D (unlike getClip().getBounds2D(), + * which is in current user space). + */ + private static Rectangle2D deviceClipBounds(Graphics2D g2d) + { + Shape clip = g2d.getClip(); + if (clip == null) + { + return null; + } + return g2d.getTransform().createTransformedShape(clip).getBounds2D(); + } + + @Test + void testPrintReturnsNoSuchPageForInvalidIndex() throws Exception + { + try (PDDocument doc = new PDDocument()) + { + doc.addPage(new PDPage()); + + PDFPrintable printable = new PDFPrintable(doc); + + BufferedImage output = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = output.createGraphics(); + PageFormat pf = createPageFormat(IMAGE_WIDTH, IMAGE_HEIGHT); + + assertEquals(Printable.NO_SUCH_PAGE, printable.print(g2d, pf, -1)); + assertEquals(Printable.NO_SUCH_PAGE, printable.print(g2d, pf, 1)); + assertEquals(Printable.PAGE_EXISTS, printable.print(g2d, pf, 0)); + + g2d.dispose(); + } + } + + private void testShowPageBorderIsGray(float dpi) throws Exception + { + try (PDDocument doc = new PDDocument()) + { + PDPage page = new PDPage(new PDRectangle(IMAGE_WIDTH, IMAGE_HEIGHT)); + doc.addPage(page); + + PDFPrintable printable = new PDFPrintable(doc, Scaling.ACTUAL_SIZE, true, dpi); + + BufferedImage output = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = output.createGraphics(); + // fill with white so we can detect gray border pixels + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT); + + PageFormat pf = createPageFormat(IMAGE_WIDTH, IMAGE_HEIGHT); + int result = printable.print(g2d, pf, 0); + g2d.dispose(); + + assertEquals(Printable.PAGE_EXISTS, result); + assertBorderPixelIsGray(output); + } + } + + /** + * Asserts that at least one pixel along the top edge of the image is gray + * (R == G == B, not white and not black), proving the border was drawn with Color.GRAY. + */ + private static void assertBorderPixelIsGray(BufferedImage image) + { + boolean foundGray = false; + int width = image.getWidth(); + // scan top row and left column where the border rect starts + for (int x = 0; x < width; x++) + { + if (isGray(image.getRGB(x, 0))) + { + foundGray = true; + break; + } + } + if (!foundGray) + { + int height = image.getHeight(); + for (int y = 0; y < height; y++) + { + if (isGray(image.getRGB(0, y))) + { + foundGray = true; + break; + } + } + } + assertTrue(foundGray, + "Expected a gray border pixel in the top-left corner. " + + "If this fails, drawRect may be called on the wrong Graphics object."); + } + + private static boolean isGray(int argb) + { + int a = (argb >> 24) & 0xFF; + int r = (argb >> 16) & 0xFF; + int g = (argb >> 8) & 0xFF; + int b = argb & 0xFF; + // Color.GRAY is (128, 128, 128) — allow some tolerance for antialiasing + return a > 0 && r == g && g == b && r > 50 && r < 200; + } + + private static PageFormat createPageFormat(double width, double height) + { + Paper paper = new Paper(); + paper.setSize(width, height); + paper.setImageableArea(0, 0, width, height); + PageFormat pf = new PageFormat(); + pf.setPaper(paper); + return pf; + } +}