You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by ti...@apache.org on 2026/05/30 10:24:21 UTC
svn commit: r1934768 - pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/printing
Author: tilman
Date: Sat May 30 10:24:21 2026
New Revision: 1934768
Log:
PDFBOX-5660: add test, as suggested by Valery Bokov; closes #448
Added:
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/printing/
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/printing/TestPDFPrintable.java (contents, props changed)
Added: pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/printing/TestPDFPrintable.java
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/printing/TestPDFPrintable.java Sat May 30 10:24:21 2026 (r1934768)
@@ -0,0 +1,257 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+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;
+
+
+/**
+ * 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.
+ *
+ * <p>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.</p>
+ */
+ @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;
+ }
+}
\ No newline at end of file