1+ /*
2+ * Licensed to the Apache Software Foundation (ASF) under one or more
3+ * contributor license agreements. See the NOTICE file distributed with
4+ * this work for additional information regarding copyright ownership.
5+ * The ASF licenses this file to You under the Apache License, Version 2.0
6+ * (the "License"); you may not use this file except in compliance with
7+ * the License. You may obtain a copy of the License at
8+ *
9+ * http://www.apache.org/licenses/LICENSE-2.0
10+ *
11+ * Unless required by applicable law or agreed to in writing, software
12+ * distributed under the License is distributed on an "AS IS" BASIS,
13+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+ * See the License for the specific language governing permissions and
15+ * limitations under the License.
16+ */
17+ package org .apache .pdfbox .printing ;
18+
19+ import static org .junit .jupiter .api .Assertions .assertEquals ;
20+ import static org .junit .jupiter .api .Assertions .assertTrue ;
21+
22+ import java .awt .BasicStroke ;
23+ import java .awt .Color ;
24+ import java .awt .Graphics2D ;
25+ import java .awt .Shape ;
26+ import java .awt .Stroke ;
27+ import java .awt .geom .AffineTransform ;
28+ import java .awt .geom .Rectangle2D ;
29+ import java .awt .image .BufferedImage ;
30+ import java .awt .print .PageFormat ;
31+ import java .awt .print .Paper ;
32+ import java .awt .print .Printable ;
33+
34+ import org .apache .pdfbox .pdmodel .PDDocument ;
35+ import org .apache .pdfbox .pdmodel .PDPage ;
36+ import org .apache .pdfbox .pdmodel .common .PDRectangle ;
37+
38+ import org .junit .jupiter .api .Test ;
39+
40+
41+ /**
42+ * Tests for {@link PDFPrintable}.
43+ */
44+ class TestPDFPrintable
45+ {
46+ private final int IMAGE_WIDTH = 100 ;
47+ private final int IMAGE_HEIGHT = 100 ;
48+
49+ /**
50+ * Tests that the page border is drawn with Color.GRAY when showPageBorder is true.
51+ *
52+ * Without rasterization, {@code graphics} and {@code graphics2D} are the same object,
53+ * so setColor(GRAY) and drawRect() both act on the same Graphics2D. The border is drawn
54+ * correctly.
55+ */
56+ @ Test
57+ void testShowPageBorderIsGrayWithoutRasterization () throws Exception
58+ {
59+ testShowPageBorderIsGray (PDFPrintable .RASTERIZE_OFF );
60+ }
61+
62+ /**
63+ * Tests that the page border is drawn with Color.GRAY when rasterizing.
64+ *
65+ * <p>When rasterizing (dpi > 0), the border is drawn on a raster image that is
66+ * larger than the page (scaled by dpiScale), then blitted down to the output.
67+ * The {@code setClip} in the showPageBorder block must use raster-pixel dimensions
68+ * (imageableWidth * scale), not raw point dimensions (imageableWidth). Otherwise
69+ * the clip is too small, the border is drawn in only a fraction of the raster image,
70+ * and the thin border line gets lost during the scale-down blit to the output.</p>
71+ */
72+ @ Test
73+ void testShowPageBorderIsGrayWithRasterization () throws Exception
74+ {
75+ testShowPageBorderIsGray (150f );
76+ }
77+
78+ /**
79+ * {@code print()} would otherwise mutate the caller's Graphics2D in several places:
80+ * translate() for imageable area and centering, scale() during rasterization,
81+ * setBackground() before the raster blit, and setColor / setStroke / setClip / setTransform
82+ * in the showPageBorder block. To isolate the caller, {@code print()} works on a private
83+ * copy obtained via {@code graphics.create()} and disposes it in the finally block, so none
84+ * of those mutations reach the caller. This test verifies that isolation by setting
85+ * distinctive state on the caller's Graphics2D before {@code print()} and asserting it is
86+ * unchanged afterwards.
87+ */
88+ @ Test
89+ void testPrinterGraphicsStateIsUnchangedAfterPrint () throws Exception
90+ {
91+ assertPrinterGraphicsStateUnchanged (PDFPrintable .RASTERIZE_OFF );
92+ }
93+
94+ @ Test
95+ void testPrinterGraphicsStateIsUnchangedAfterPrintWhenRasterizing () throws Exception
96+ {
97+ assertPrinterGraphicsStateUnchanged (150f );
98+ }
99+
100+ private void assertPrinterGraphicsStateUnchanged (float dpi ) throws Exception
101+ {
102+ try (PDDocument doc = new PDDocument ())
103+ {
104+ doc .addPage (new PDPage (new PDRectangle (IMAGE_WIDTH , IMAGE_HEIGHT )));
105+
106+ PDFPrintable printable = new PDFPrintable (doc , Scaling .ACTUAL_SIZE , true , dpi );
107+
108+ BufferedImage output = new BufferedImage (IMAGE_WIDTH , IMAGE_HEIGHT , BufferedImage .TYPE_INT_ARGB );
109+ Graphics2D g2d = output .createGraphics ();
110+
111+ // set a distinctive transform so we can detect leaks from internal translate()/scale() calls
112+ g2d .translate (7.0 , 11.0 );
113+ g2d .scale (1.3 , 1.3 );
114+
115+ Color originalColor = Color .RED ;
116+ Color originalBackground = Color .BLUE ;
117+ Stroke originalStroke = new BasicStroke (3.7f );
118+ g2d .setColor (originalColor );
119+ g2d .setBackground (originalBackground );
120+ g2d .setStroke (originalStroke );
121+ AffineTransform originalTransform = g2d .getTransform ();
122+ Rectangle2D originalClipDeviceBounds = deviceClipBounds (g2d );
123+
124+ PageFormat pf = createPageFormat (IMAGE_WIDTH , IMAGE_HEIGHT );
125+ int result = printable .print (g2d , pf , 0 );
126+
127+ assertEquals (Printable .PAGE_EXISTS , result );
128+ assertEquals (originalColor , g2d .getColor (),
129+ "color should be unchanged after print()" );
130+ assertEquals (originalBackground , g2d .getBackground (),
131+ "background should be unchanged after print()" );
132+ assertEquals (originalStroke , g2d .getStroke (),
133+ "stroke should be unchanged after print()" );
134+ assertEquals (originalTransform , g2d .getTransform (),
135+ "transform should be unchanged after print() (translate/scale inside print() must not leak)" );
136+ // device-space comparison — invariant under transform changes on the same Graphics2D
137+ assertEquals (originalClipDeviceBounds , deviceClipBounds (g2d ),
138+ "clip should be unchanged after print()" );
139+
140+ g2d .dispose ();
141+ }
142+ }
143+
144+ /**
145+ * Returns the bounds of the current clip projected into device space via the current transform.
146+ * This is stable across transform changes on the same Graphics2D (unlike getClip().getBounds2D(),
147+ * which is in current user space).
148+ */
149+ private static Rectangle2D deviceClipBounds (Graphics2D g2d )
150+ {
151+ Shape clip = g2d .getClip ();
152+ if (clip == null )
153+ {
154+ return null ;
155+ }
156+ return g2d .getTransform ().createTransformedShape (clip ).getBounds2D ();
157+ }
158+
159+ @ Test
160+ void testPrintReturnsNoSuchPageForInvalidIndex () throws Exception
161+ {
162+ try (PDDocument doc = new PDDocument ())
163+ {
164+ doc .addPage (new PDPage ());
165+
166+ PDFPrintable printable = new PDFPrintable (doc );
167+
168+ BufferedImage output = new BufferedImage (IMAGE_WIDTH , IMAGE_HEIGHT , BufferedImage .TYPE_INT_ARGB );
169+ Graphics2D g2d = output .createGraphics ();
170+ PageFormat pf = createPageFormat (IMAGE_WIDTH , IMAGE_HEIGHT );
171+
172+ assertEquals (Printable .NO_SUCH_PAGE , printable .print (g2d , pf , -1 ));
173+ assertEquals (Printable .NO_SUCH_PAGE , printable .print (g2d , pf , 1 ));
174+ assertEquals (Printable .PAGE_EXISTS , printable .print (g2d , pf , 0 ));
175+
176+ g2d .dispose ();
177+ }
178+ }
179+
180+ private void testShowPageBorderIsGray (float dpi ) throws Exception
181+ {
182+ try (PDDocument doc = new PDDocument ())
183+ {
184+ PDPage page = new PDPage (new PDRectangle (IMAGE_WIDTH , IMAGE_HEIGHT ));
185+ doc .addPage (page );
186+
187+ PDFPrintable printable = new PDFPrintable (doc , Scaling .ACTUAL_SIZE , true , dpi );
188+
189+ BufferedImage output = new BufferedImage (IMAGE_WIDTH , IMAGE_HEIGHT , BufferedImage .TYPE_INT_ARGB );
190+ Graphics2D g2d = output .createGraphics ();
191+ // fill with white so we can detect gray border pixels
192+ g2d .setColor (Color .WHITE );
193+ g2d .fillRect (0 , 0 , IMAGE_WIDTH , IMAGE_HEIGHT );
194+
195+ PageFormat pf = createPageFormat (IMAGE_WIDTH , IMAGE_HEIGHT );
196+ int result = printable .print (g2d , pf , 0 );
197+ g2d .dispose ();
198+
199+ assertEquals (Printable .PAGE_EXISTS , result );
200+ assertBorderPixelIsGray (output );
201+ }
202+ }
203+
204+ /**
205+ * Asserts that at least one pixel along the top edge of the image is gray
206+ * (R == G == B, not white and not black), proving the border was drawn with Color.GRAY.
207+ */
208+ private static void assertBorderPixelIsGray (BufferedImage image )
209+ {
210+ boolean foundGray = false ;
211+ int width = image .getWidth ();
212+ // scan top row and left column where the border rect starts
213+ for (int x = 0 ; x < width ; x ++)
214+ {
215+ if (isGray (image .getRGB (x , 0 )))
216+ {
217+ foundGray = true ;
218+ break ;
219+ }
220+ }
221+ if (!foundGray )
222+ {
223+ int height = image .getHeight ();
224+ for (int y = 0 ; y < height ; y ++)
225+ {
226+ if (isGray (image .getRGB (0 , y )))
227+ {
228+ foundGray = true ;
229+ break ;
230+ }
231+ }
232+ }
233+ assertTrue (foundGray ,
234+ "Expected a gray border pixel in the top-left corner. " +
235+ "If this fails, drawRect may be called on the wrong Graphics object." );
236+ }
237+
238+ private static boolean isGray (int argb )
239+ {
240+ int a = (argb >> 24 ) & 0xFF ;
241+ int r = (argb >> 16 ) & 0xFF ;
242+ int g = (argb >> 8 ) & 0xFF ;
243+ int b = argb & 0xFF ;
244+ // Color.GRAY is (128, 128, 128) — allow some tolerance for antialiasing
245+ return a > 0 && r == g && g == b && r > 50 && r < 200 ;
246+ }
247+
248+ private static PageFormat createPageFormat (double width , double height )
249+ {
250+ Paper paper = new Paper ();
251+ paper .setSize (width , height );
252+ paper .setImageableArea (0 , 0 , width , height );
253+ PageFormat pf = new PageFormat ();
254+ pf .setPaper (paper );
255+ return pf ;
256+ }
257+ }
0 commit comments