From 9d37b22cb44f4607679d90c5997fa3e1a3126ed7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 29 Nov 2025 12:11:01 +0000
Subject: [PATCH] Add native PDF output support
Currently if one wants to create a PDF file it requires external
libraries and as SWT does not allows an abstraction like Grahics2D in
AWT one can not export real content of SWT components (e.g. Canvas)
except exporting as an raster image or using some hacks.
This now introduce a new PDFDocument to enable direct
PDF generation from SWT widgets via Control.print(GC). This allows
applications to export widget content to PDF files using the standard
GC drawing API as well as even creating completely customized documents.
---
.../.settings/.api_filters | 8 -
.../.settings/.api_filters | 8 -
.../.settings/.api_filters | 8 -
.../.settings/.api_filters | 8 -
.../.settings/.api_filters | 8 -
.../.settings/.api_filters | 8 -
.../.settings/.api_filters | 8 -
.../.settings/.api_filters | 8 -
.../.settings/.api_filters | 8 -
.../Eclipse SWT PI/cairo/library/cairo.c | 26 +-
.../cairo/library/cairo_custom.h | 1 +
.../cairo/library/cairo_stats.h | 3 +-
.../org/eclipse/swt/internal/cairo/Cairo.java | 9 +
.../Eclipse SWT PI/cocoa/library/os.c | 51 +-
.../Eclipse SWT PI/cocoa/library/os_stats.h | 9 +-
.../org/eclipse/swt/internal/cocoa/OS.java | 19 +
.../org/eclipse/swt/internal/win32/OS.java | 11 +
.../org/eclipse/swt/printing/PDFDocument.java | 393 +++++++++++++
.../org/eclipse/swt/printing/PDFDocument.java | 336 +++++++++++
.../org/eclipse/swt/printing/PDFDocument.java | 526 ++++++++++++++++++
.../cocoa/org/eclipse/swt/widgets/Shell.java | 18 +-
.../win32/org/eclipse/swt/widgets/Shell.java | 24 +-
.../org/eclipse/swt/snippets/Snippet388.java | 240 ++++++++
.../swt/tests/junit/AllNonBrowserTests.java | 1 +
..._org_eclipse_swt_printing_PDFDocument.java | 103 ++++
25 files changed, 1758 insertions(+), 84 deletions(-)
create mode 100644 bundles/org.eclipse.swt/Eclipse SWT Printing/cocoa/org/eclipse/swt/printing/PDFDocument.java
create mode 100644 bundles/org.eclipse.swt/Eclipse SWT Printing/gtk/org/eclipse/swt/printing/PDFDocument.java
create mode 100644 bundles/org.eclipse.swt/Eclipse SWT Printing/win32/org/eclipse/swt/printing/PDFDocument.java
create mode 100644 examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet388.java
create mode 100644 tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_printing_PDFDocument.java
diff --git a/binaries/org.eclipse.swt.cocoa.macosx.aarch64/.settings/.api_filters b/binaries/org.eclipse.swt.cocoa.macosx.aarch64/.settings/.api_filters
index 10facecefd3..ca5e705a7eb 100644
--- a/binaries/org.eclipse.swt.cocoa.macosx.aarch64/.settings/.api_filters
+++ b/binaries/org.eclipse.swt.cocoa.macosx.aarch64/.settings/.api_filters
@@ -196,12 +196,4 @@
- new GC(pdfDocument)
+ * and then draw on the GC using the usual graphics calls.
+ *
+ * A PDFDocument object may be constructed by providing
+ * a filename and the page dimensions. After drawing is complete,
+ * the document must be disposed to finalize the PDF file.
+ *
+ * Application code must explicitly invoke the PDFDocument.dispose()
+ * method to release the operating system resources managed by each instance
+ * when those instances are no longer required.
+ *
+ * The following example demonstrates how to use PDFDocument: + *
+ *
+ * PDFDocument pdf = new PDFDocument("output.pdf", 612, 792); // Letter size in points
+ * GC gc = new GC(pdf);
+ * gc.drawText("Hello, PDF!", 100, 100);
+ * gc.dispose();
+ * pdf.dispose();
+ *
+ *
+ * @see GC
+ * @since 3.133
+ */
+public class PDFDocument implements Drawable {
+ Device device;
+ long pdfContext;
+ NSGraphicsContext graphicsContext;
+ boolean isGCCreated = false;
+ boolean disposed = false;
+ boolean pageStarted = false;
+
+ /**
+ * Width of the page in points (1/72 inch)
+ */
+ double widthInPoints;
+
+ /**
+ * Height of the page in points (1/72 inch)
+ */
+ double heightInPoints;
+
+ /**
+ * Constructs a new PDFDocument with the specified filename and page dimensions.
+ * + * You must dispose the PDFDocument when it is no longer required. + *
+ * + * @param filename the path to the PDF file to create + * @param widthInPoints the width of each page in points (1/72 inch) + * @param heightInPoints the height of each page in points (1/72 inch) + * + * @exception IllegalArgumentException+ * You must dispose the PDFDocument when it is no longer required. + *
+ * + * @param device the device to associate with this PDFDocument + * @param filename the path to the PDF file to create + * @param widthInPoints the width of each page in points (1/72 inch) + * @param heightInPoints the height of each page in points (1/72 inch) + * + * @exception IllegalArgumentException+ * This method should be called after completing the content of one page + * and before starting to draw on the next page. The new page will have + * the same dimensions as the initial page. + *
+ * + * @exception SWTException+ * This method should be called after completing the content of one page + * and before starting to draw on the next page. + *
+ * + * @param widthInPoints the width of the new page in points (1/72 inch) + * @param heightInPoints the height of the new page in points (1/72 inch) + * + * @exception IllegalArgumentException
+ * IMPORTANT: This method is not part of the public
+ * API for PDFDocument. It is marked public only so that it
+ * can be shared within the packages provided by SWT. It is not
+ * available on all platforms, and should never be called from
+ * application code.
+ *
+ * IMPORTANT: This method is not part of the public
+ * API for PDFDocument. It is marked public only so that it
+ * can be shared within the packages provided by SWT. It is not
+ * available on all platforms, and should never be called from
+ * application code.
+ *
true if the PDFDocument has been disposed,
+ * and false otherwise.
+ *
+ * @return true when the PDFDocument is disposed and false otherwise
+ */
+ public boolean isDisposed() {
+ return disposed;
+ }
+
+ /**
+ * Disposes of the operating system resources associated with
+ * the PDFDocument. Applications must dispose of all PDFDocuments
+ * that they allocate.
+ * + * This method finalizes the PDF file and writes it to disk. + *
+ */ + public void dispose() { + if (disposed) return; + disposed = true; + + NSAutoreleasePool pool = null; + if (!NSThread.isMainThread()) pool = (NSAutoreleasePool) new NSAutoreleasePool().alloc().init(); + try { + if (pdfContext != 0) { + if (pageStarted) { + OS.CGPDFContextEndPage(pdfContext); + } + OS.CGPDFContextClose(pdfContext); + OS.CGContextRelease(pdfContext); + pdfContext = 0; + } + if (graphicsContext != null) { + graphicsContext.release(); + graphicsContext = null; + } + } finally { + if (pool != null) pool.release(); + } + } +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT Printing/gtk/org/eclipse/swt/printing/PDFDocument.java b/bundles/org.eclipse.swt/Eclipse SWT Printing/gtk/org/eclipse/swt/printing/PDFDocument.java new file mode 100644 index 00000000000..9c76c25187b --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT Printing/gtk/org/eclipse/swt/printing/PDFDocument.java @@ -0,0 +1,336 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse Platform Contributors and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eclipse Platform Contributors - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.printing; + +import org.eclipse.swt.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.internal.*; +import org.eclipse.swt.internal.cairo.*; + +/** + * Instances of this class are used to create PDF documents. + * Applications create a GC on a PDFDocument usingnew GC(pdfDocument)
+ * and then draw on the GC using the usual graphics calls.
+ *
+ * A PDFDocument object may be constructed by providing
+ * a filename and the page dimensions. After drawing is complete,
+ * the document must be disposed to finalize the PDF file.
+ *
+ * Application code must explicitly invoke the PDFDocument.dispose()
+ * method to release the operating system resources managed by each instance
+ * when those instances are no longer required.
+ *
+ * The following example demonstrates how to use PDFDocument: + *
+ *
+ * PDFDocument pdf = new PDFDocument("output.pdf", 612, 792); // Letter size in points
+ * GC gc = new GC(pdf);
+ * gc.drawText("Hello, PDF!", 100, 100);
+ * gc.dispose();
+ * pdf.dispose();
+ *
+ *
+ * @see GC
+ * @since 3.133
+ */
+public class PDFDocument implements Drawable {
+ Device device;
+ long surface;
+ long cairo;
+ boolean isGCCreated = false;
+ boolean disposed = false;
+
+ /**
+ * Width of the page in points (1/72 inch)
+ */
+ double widthInPoints;
+
+ /**
+ * Height of the page in points (1/72 inch)
+ */
+ double heightInPoints;
+
+ /**
+ * Constructs a new PDFDocument with the specified filename and page dimensions.
+ * + * You must dispose the PDFDocument when it is no longer required. + *
+ * + * @param filename the path to the PDF file to create + * @param widthInPoints the width of each page in points (1/72 inch) + * @param heightInPoints the height of each page in points (1/72 inch) + * + * @exception IllegalArgumentException+ * You must dispose the PDFDocument when it is no longer required. + *
+ * + * @param device the device to associate with this PDFDocument + * @param filename the path to the PDF file to create + * @param widthInPoints the width of each page in points (1/72 inch) + * @param heightInPoints the height of each page in points (1/72 inch) + * + * @exception IllegalArgumentException+ * This method should be called after completing the content of one page + * and before starting to draw on the next page. The new page will have + * the same dimensions as the initial page. + *
+ * + * @exception SWTException+ * This method should be called after completing the content of one page + * and before starting to draw on the next page. + *
+ * + * @param widthInPoints the width of the new page in points (1/72 inch) + * @param heightInPoints the height of the new page in points (1/72 inch) + * + * @exception IllegalArgumentException
+ * IMPORTANT: This method is not part of the public
+ * API for PDFDocument. It is marked public only so that it
+ * can be shared within the packages provided by SWT. It is not
+ * available on all platforms, and should never be called from
+ * application code.
+ *
+ * IMPORTANT: This method is not part of the public
+ * API for PDFDocument. It is marked public only so that it
+ * can be shared within the packages provided by SWT. It is not
+ * available on all platforms, and should never be called from
+ * application code.
+ *
true if the PDFDocument has been disposed,
+ * and false otherwise.
+ *
+ * @return true when the PDFDocument is disposed and false otherwise
+ */
+ public boolean isDisposed() {
+ return disposed;
+ }
+
+ /**
+ * Disposes of the operating system resources associated with
+ * the PDFDocument. Applications must dispose of all PDFDocuments
+ * that they allocate.
+ * + * This method finalizes the PDF file and writes it to disk. + *
+ */ + public void dispose() { + if (disposed) return; + disposed = true; + + if (cairo != 0) { + Cairo.cairo_destroy(cairo); + cairo = 0; + } + if (surface != 0) { + Cairo.cairo_surface_finish(surface); + Cairo.cairo_surface_destroy(surface); + surface = 0; + } + } +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT Printing/win32/org/eclipse/swt/printing/PDFDocument.java b/bundles/org.eclipse.swt/Eclipse SWT Printing/win32/org/eclipse/swt/printing/PDFDocument.java new file mode 100644 index 00000000000..db15aa658f9 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT Printing/win32/org/eclipse/swt/printing/PDFDocument.java @@ -0,0 +1,526 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse Platform Contributors and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eclipse Platform Contributors - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.printing; + +import org.eclipse.swt.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.internal.win32.*; + +/** + * Instances of this class are used to create PDF documents. + * Applications create a GC on a PDFDocument usingnew GC(pdfDocument)
+ * and then draw on the GC using the usual graphics calls.
+ *
+ * A PDFDocument object may be constructed by providing
+ * a filename and the page dimensions. After drawing is complete,
+ * the document must be disposed to finalize the PDF file.
+ *
+ * Application code must explicitly invoke the PDFDocument.dispose()
+ * method to release the operating system resources managed by each instance
+ * when those instances are no longer required.
+ *
+ * Note: On Windows, this class uses the built-in "Microsoft Print to PDF" + * printer which is available on Windows 10 and later. + *
+ *+ * The following example demonstrates how to use PDFDocument: + *
+ *
+ * PDFDocument pdf = new PDFDocument("output.pdf", 612, 792); // Letter size in points
+ * GC gc = new GC(pdf);
+ * gc.drawText("Hello, PDF!", 100, 100);
+ * gc.dispose();
+ * pdf.dispose();
+ *
+ *
+ * @see GC
+ * @since 3.133
+ */
+public class PDFDocument implements Drawable {
+ Device device;
+ long handle;
+ boolean isGCCreated = false;
+ boolean disposed = false;
+ boolean jobStarted = false;
+ boolean pageStarted = false;
+ String filename;
+
+ /**
+ * Width of the page in device-independent units
+ */
+ double width;
+
+ /**
+ * Height of the page in device-independent units
+ */
+ double height;
+
+ /**
+ * Width of the page in points (1/72 inch)
+ */
+ double widthInPoints;
+
+ /**
+ * Height of the page in points (1/72 inch)
+ */
+ double heightInPoints;
+
+ /** The name of the Microsoft Print to PDF printer */
+ private static final String PDF_PRINTER_NAME = "Microsoft Print to PDF";
+
+ /** Helper class to represent a paper size with orientation */
+ private static class PaperSize {
+ int paperSizeConstant;
+ int orientation;
+ double widthInInches;
+ double heightInInches;
+
+ PaperSize(int paperSize, int orientation, double width, double height) {
+ this.paperSizeConstant = paperSize;
+ this.orientation = orientation;
+ this.widthInInches = width;
+ this.heightInInches = height;
+ }
+ }
+
+ /**
+ * Finds the best matching standard paper size for the given dimensions.
+ * Tries both portrait and landscape orientations and selects the one that
+ * minimizes wasted space while ensuring the content fits.
+ */
+ private static PaperSize findBestPaperSize(double widthInInches, double heightInInches) {
+ // Common paper sizes (width x height in inches, portrait orientation)
+ int[][] standardSizes = {
+ {OS.DMPAPER_LETTER, 850, 1100}, // 8.5 x 11
+ {OS.DMPAPER_LEGAL, 850, 1400}, // 8.5 x 14
+ {OS.DMPAPER_A4, 827, 1169}, // 8.27 x 11.69
+ {OS.DMPAPER_TABLOID, 1100, 1700}, // 11 x 17
+ {OS.DMPAPER_A3, 1169, 1654}, // 11.69 x 16.54
+ {OS.DMPAPER_EXECUTIVE, 725, 1050}, // 7.25 x 10.5
+ {OS.DMPAPER_A5, 583, 827}, // 5.83 x 8.27
+ };
+
+ PaperSize bestMatch = null;
+ double minWaste = Double.MAX_VALUE;
+
+ for (int[] size : standardSizes) {
+ double paperWidth = size[1] / 100.0;
+ double paperHeight = size[2] / 100.0;
+
+ // Try portrait orientation
+ if (widthInInches <= paperWidth && heightInInches <= paperHeight) {
+ double waste = (paperWidth * paperHeight) - (widthInInches * heightInInches);
+ if (waste < minWaste) {
+ minWaste = waste;
+ bestMatch = new PaperSize(size[0], OS.DMORIENT_PORTRAIT, paperWidth, paperHeight);
+ }
+ }
+
+ // Try landscape orientation (swap width and height)
+ if (widthInInches <= paperHeight && heightInInches <= paperWidth) {
+ double waste = (paperHeight * paperWidth) - (widthInInches * heightInInches);
+ if (waste < minWaste) {
+ minWaste = waste;
+ bestMatch = new PaperSize(size[0], OS.DMORIENT_LANDSCAPE, paperHeight, paperWidth);
+ }
+ }
+ }
+
+ // Default to Letter if no match found
+ if (bestMatch == null) {
+ bestMatch = new PaperSize(OS.DMPAPER_LETTER, OS.DMORIENT_PORTRAIT, 8.5, 11.0);
+ }
+
+ return bestMatch;
+ }
+
+ /**
+ * Constructs a new PDFDocument with the specified filename and page dimensions.
+ * + * You must dispose the PDFDocument when it is no longer required. + *
+ * + * @param filename the path to the PDF file to create + * @param width the width of each page in device-independent units + * @param height the height of each page in device-independent units + * + * @exception IllegalArgumentException+ * You must dispose the PDFDocument when it is no longer required. + *
+ * + * @param device the device to associate with this PDFDocument + * @param filename the path to the PDF file to create + * @param width the width of each page in device-independent units + * @param height the height of each page in device-independent units + * + * @exception IllegalArgumentException+ * This method should be called after completing the content of one page + * and before starting to draw on the next page. The new page will have + * the same dimensions as the initial page. + *
+ * + * @exception SWTException+ * This method should be called after completing the content of one page + * and before starting to draw on the next page. + *
+ *+ * Note: On Windows, changing page dimensions after the document + * has been started may not be fully supported by all printer drivers. + *
+ * + * @param widthInPoints the width of the new page in points (1/72 inch) + * @param heightInPoints the height of the new page in points (1/72 inch) + * + * @exception IllegalArgumentException
+ * IMPORTANT: This method is not part of the public
+ * API for PDFDocument. It is marked public only so that it
+ * can be shared within the packages provided by SWT. It is not
+ * available on all platforms, and should never be called from
+ * application code.
+ *
+ * IMPORTANT: This method is not part of the public
+ * API for PDFDocument. It is marked public only so that it
+ * can be shared within the packages provided by SWT. It is not
+ * available on all platforms, and should never be called from
+ * application code.
+ *
true if the PDFDocument has been disposed,
+ * and false otherwise.
+ *
+ * @return true when the PDFDocument is disposed and false otherwise
+ */
+ public boolean isDisposed() {
+ return disposed;
+ }
+
+ /**
+ * Disposes of the operating system resources associated with
+ * the PDFDocument. Applications must dispose of all PDFDocuments
+ * that they allocate.
+ * + * This method finalizes the PDF file and writes it to disk. + *
+ */ + public void dispose() { + if (disposed) return; + disposed = true; + + if (handle != 0) { + if (pageStarted) { + OS.EndPage(handle); + } + if (jobStarted) { + OS.EndDoc(handle); + } + OS.DeleteDC(handle); + handle = 0; + } + } +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Shell.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Shell.java index 5c7cc8e9936..45943a568e8 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Shell.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Shell.java @@ -1404,7 +1404,23 @@ public boolean print (GC gc) { checkWidget (); if (gc == null) error (SWT.ERROR_NULL_ARGUMENT); if (gc.isDisposed ()) error (SWT.ERROR_INVALID_ARGUMENT); - return false; + // Print only the client area (children) without shell decorations + Control [] children = _getChildren (); + for (Control child : children) { + Rectangle bounds = child.getBounds(); + // Save the graphics state before transforming + NSGraphicsContext.static_saveGraphicsState(); + NSGraphicsContext.setCurrentContext(gc.handle); + // Create and apply translation transform for child's position + NSAffineTransform transform = NSAffineTransform.transform(); + transform.translateXBy(bounds.x, bounds.y); + transform.concat(); + // Print the child control + child.print(gc); + // Restore the graphics state + NSGraphicsContext.static_restoreGraphicsState(); + } + return true; } @Override diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Shell.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Shell.java index 84c21d92193..f1005c72996 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Shell.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Shell.java @@ -1350,7 +1350,29 @@ public boolean print (GC gc) { checkWidget (); if (gc == null) error (SWT.ERROR_NULL_ARGUMENT); if (gc.isDisposed ()) error (SWT.ERROR_INVALID_ARGUMENT); - return false; + // Print only the client area (children) without shell decorations + forceResize (); + Control [] children = _getChildren (); + long gdipGraphics = gc.getGCData().gdipGraphics; + for (Control child : children) { + Rectangle bounds = child.getBounds(); + if (gdipGraphics != 0) { + // For GDI+, translate the graphics object + org.eclipse.swt.internal.gdip.Gdip.Graphics_TranslateTransform(gdipGraphics, bounds.x, bounds.y, org.eclipse.swt.internal.gdip.Gdip.MatrixOrderPrepend); + child.print(gc); + org.eclipse.swt.internal.gdip.Gdip.Graphics_TranslateTransform(gdipGraphics, -bounds.x, -bounds.y, org.eclipse.swt.internal.gdip.Gdip.MatrixOrderPrepend); + } else { + // For GDI, modify the world transform to add translation + int state = OS.SaveDC(gc.handle); + // Create a translation transform matrix + float[] translateMatrix = new float[] {1, 0, 0, 1, bounds.x, bounds.y}; + // Multiply (prepend) the translation to the existing transform + OS.ModifyWorldTransform(gc.handle, translateMatrix, OS.MWT_LEFTMULTIPLY); + child.print(gc); + OS.RestoreDC(gc.handle, state); + } + } + return true; } @Override diff --git a/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet388.java b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet388.java new file mode 100644 index 00000000000..889100d17c6 --- /dev/null +++ b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet388.java @@ -0,0 +1,240 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse Platform Contributors and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eclipse Platform Contributors - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.snippets; + +/* + * PDFDocument example snippet: create a shell with graphics and export to PDF + * + * For a list of all SWT example snippets see + * http://www.eclipse.org/swt/snippets/ + */ + +import org.eclipse.swt.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.layout.*; +import org.eclipse.swt.printing.*; +import org.eclipse.swt.program.*; +import org.eclipse.swt.widgets.*; + +public class Snippet388 { + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("PDF Export Demo"); + shell.setLayout(new BorderLayout()); + + Label titleLabel = new Label(shell, SWT.CENTER); + titleLabel.setText("SWT Graphics Demo"); + titleLabel.setLayoutData(new BorderData(SWT.TOP)); + + Canvas canvas = new Canvas(shell, SWT.BORDER); + canvas.setLayoutData(new BorderData(SWT.CENTER)); + canvas.addListener(SWT.Paint, e -> { + GC gc = e.gc; + Color red = display.getSystemColor(SWT.COLOR_RED); + Color blue = display.getSystemColor(SWT.COLOR_BLUE); + Color green = display.getSystemColor(SWT.COLOR_GREEN); + Color yellow = display.getSystemColor(SWT.COLOR_YELLOW); + Color black = display.getSystemColor(SWT.COLOR_BLACK); + Color darkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY); + + int shapeSpacing = 200; + int shapeY = 20; + int shapeWidth = 100; + int shapeHeight = 80; + int textY = shapeY + shapeHeight + 5; + + int x1 = 50; + gc.setBackground(red); + gc.fillRectangle(x1, shapeY, shapeWidth, shapeHeight); + + int x2 = x1 + shapeSpacing; + gc.setForeground(blue); + gc.setLineWidth(3); + gc.drawRectangle(x2, shapeY, shapeWidth, shapeHeight); + + int x3 = x2 + shapeSpacing; + gc.setBackground(green); + gc.fillOval(x3, shapeY, shapeWidth, shapeHeight); + + int x4 = x3 + shapeSpacing; + gc.setForeground(yellow); + gc.setLineWidth(2); + gc.drawOval(x4, shapeY, shapeWidth, shapeHeight); + + gc.setForeground(black); + gc.setLineWidth(4); + gc.drawLine(20, 150, x4 + shapeWidth, 150); + + gc.setBackground(blue); + gc.fillPolygon(new int[] { 50, 170, 100, 220, 150, 170 }); + + gc.setForeground(red); + gc.setLineWidth(2); + gc.drawPolygon(new int[] { 250, 170, 300, 220, 350, 170, 300, 200 }); + + gc.setForeground(darkGray); + String[] labels = { "Filled Rectangle", "Outlined Rectangle", "Filled Oval", "Outlined Oval" }; + int[] shapeXPositions = { x1, x2, x3, x4 }; + for (int i = 0; i < labels.length; i++) { + Point textExtent = gc.stringExtent(labels[i]); + int centeredX = shapeXPositions[i] + (shapeWidth - textExtent.x) / 2; + gc.drawString(labels[i], centeredX, textY, true); + } + + int row2Y = 240; + + Path path = new Path(display); + try { + path.moveTo(x1, row2Y + 40); + path.lineTo(x1 + 30, row2Y); + path.quadTo(x1 + 50, row2Y + 20, x1 + 70, row2Y); + path.cubicTo(x1 + 90, row2Y, x1 + 100, row2Y + 60, x1 + 50, row2Y + 70); + path.close(); + gc.setBackground(display.getSystemColor(SWT.COLOR_CYAN)); + gc.fillPath(path); + gc.setForeground(black); + gc.setLineWidth(2); + gc.drawPath(path); + } finally { + path.dispose(); + } + + Pattern gradient1 = new Pattern(display, x2, row2Y, x2 + shapeWidth, row2Y + shapeHeight, + display.getSystemColor(SWT.COLOR_MAGENTA), display.getSystemColor(SWT.COLOR_WHITE)); + try { + gc.setBackgroundPattern(gradient1); + gc.fillRoundRectangle(x2, row2Y, shapeWidth, shapeHeight, 20, 20); + } finally { + gradient1.dispose(); + } + + Pattern gradient2 = new Pattern(display, x3 + shapeWidth / 2, row2Y + shapeHeight / 2, + x3 + shapeWidth / 2, row2Y + shapeHeight / 2, red, 0, yellow, 50); + try { + gc.setBackgroundPattern(gradient2); + gc.fillOval(x3, row2Y, shapeWidth, shapeHeight); + } finally { + gradient2.dispose(); + } + + Transform transform = new Transform(display); + try { + transform.translate(x4 + shapeWidth / 2, row2Y + shapeHeight / 2); + transform.rotate(45); + gc.setTransform(transform); + gc.setBackground(green); + gc.fillRectangle(-40, -40, 80, 80); + gc.setForeground(black); + gc.setLineWidth(2); + gc.drawRectangle(-40, -40, 80, 80); + gc.setTransform(null); + } finally { + transform.dispose(); + } + + gc.setForeground(darkGray); + String[] row2Labels = { "Path with Curves", "Linear Gradient", "Radial Gradient", "45° Rotation" }; + int row2TextY = row2Y + shapeHeight + 5; + for (int i = 0; i < row2Labels.length; i++) { + Point textExtent = gc.stringExtent(row2Labels[i]); + int centeredX = shapeXPositions[i] + (shapeWidth - textExtent.x) / 2; + gc.drawString(row2Labels[i], centeredX, row2TextY, true); + } + + int row3Y = 360; + + gc.setAlpha(128); + gc.setBackground(blue); + gc.fillOval(x1, row3Y, 60, 60); + gc.setBackground(red); + gc.fillOval(x1 + 40, row3Y + 20, 60, 60); + gc.setAlpha(255); + + gc.setLineStyle(SWT.LINE_DASH); + gc.setForeground(blue); + gc.setLineWidth(3); + gc.drawRoundRectangle(x2, row3Y, shapeWidth, shapeHeight, 15, 15); + gc.setLineStyle(SWT.LINE_DOT); + gc.setForeground(red); + gc.drawRectangle(x2 + 10, row3Y + 10, shapeWidth - 20, shapeHeight - 20); + gc.setLineStyle(SWT.LINE_SOLID); + + gc.setAntialias(SWT.ON); + gc.setForeground(green); + gc.setLineWidth(3); + for (int i = 0; i < 5; i++) { + int offset = i * 20; + gc.drawLine(x3 + offset, row3Y, x3 + shapeWidth, row3Y + shapeHeight - offset); + } + gc.setAntialias(SWT.OFF); + + Font largeFont = new Font(display, "Arial", 24, SWT.BOLD); + try { + gc.setFont(largeFont); + gc.setForeground(display.getSystemColor(SWT.COLOR_DARK_BLUE)); + String text = "ABC"; + Point extent = gc.stringExtent(text); + gc.drawString(text, x4 + (shapeWidth - extent.x) / 2, row3Y + (shapeHeight - extent.y) / 2, true); + } finally { + largeFont.dispose(); + } + + gc.setForeground(darkGray); + String[] row3Labels = { "Alpha Blending", "Line Styles", "Antialiasing", "Custom Font" }; + int row3TextY = row3Y + shapeHeight + 5; + for (int i = 0; i < row3Labels.length; i++) { + Point textExtent = gc.stringExtent(row3Labels[i]); + int centeredX = shapeXPositions[i] + (shapeWidth - textExtent.x) / 2; + gc.drawString(row3Labels[i], centeredX, row3TextY, true); + } + }); + + Button exportButton = new Button(shell, SWT.PUSH); + exportButton.setText("Export to PDF"); + exportButton.setLayoutData(new BorderData(SWT.BOTTOM)); + exportButton.addListener(SWT.Selection, e -> { + try { + String tempDir = System.getProperty("java.io.tmpdir"); + String pdfPath = tempDir + "/swt_graphics_demo.pdf"; + + Rectangle clientArea = shell.getClientArea(); + PDFDocument pdf = new PDFDocument(pdfPath, clientArea.width, clientArea.height); + GC gc = new GC(pdf); + shell.print(gc); + gc.drawString("Exported to PDF...", 0, 0, true); + gc.dispose(); + pdf.dispose(); + System.out.println("PDF has been exported to:\n" + pdfPath + "\n\nOpening..."); + Program.launch(pdfPath); + + } catch (Throwable ex) { + MessageBox errorBox = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK); + errorBox.setText("Error"); + errorBox.setMessage("Failed to export PDF: " + ex.getMessage()); + errorBox.open(); + ex.printStackTrace(); + } + }); + + shell.setSize(800, 600); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + display.dispose(); + } +} diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/AllNonBrowserTests.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/AllNonBrowserTests.java index db49ac96590..69e8bb534c8 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/AllNonBrowserTests.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/AllNonBrowserTests.java @@ -71,6 +71,7 @@ Test_org_eclipse_swt_layout_BorderLayout.class, // Test_org_eclipse_swt_layout_FormAttachment.class, // Test_org_eclipse_swt_layout_GridData.class, // + Test_org_eclipse_swt_printing_PDFDocument.class, // Test_org_eclipse_swt_printing_PrintDialog.class, // Test_org_eclipse_swt_printing_Printer.class, // Test_org_eclipse_swt_printing_PrinterData.class, // diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_printing_PDFDocument.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_printing_PDFDocument.java new file mode 100644 index 00000000000..5df7dc42ada --- /dev/null +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_printing_PDFDocument.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse Platform Contributors and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eclipse Platform Contributors - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.tests.junit; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.DataFormatException; + +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.printing.PDFDocument; +import org.eclipse.swt.widgets.Display; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Automated Test Suite for class org.eclipse.swt.printing.PDFDocument + * + * @see org.eclipse.swt.printing.PDFDocument + */ +public class Test_org_eclipse_swt_printing_PDFDocument { + + Display display; + PDFDocument pdfDocument; + GC gc; + File tempFile; + + @TempDir + Path tempDir; + + @BeforeEach + public void setUp() { + display = Display.getDefault(); + } + + @AfterEach + public void tearDown() { + if (gc != null && !gc.isDisposed()) { + gc.dispose(); + } + if (pdfDocument != null && !pdfDocument.isDisposed()) { + pdfDocument.dispose(); + } + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } + } + + @Test + public void test_createPDFDocumentWithHelloWorld() throws IOException, DataFormatException { + // Create a temporary file for the PDF + tempFile = Files.createTempFile(tempDir, "test", ".pdf").toFile(); + String filename = tempFile.getAbsolutePath(); + + // Create PDF document with standard letter size (612 x 792 points) + pdfDocument = new PDFDocument(filename, 612, 792); + assertNotNull(pdfDocument, "PDFDocument should be created"); + + // Create a GC on the PDF document + gc = new GC(pdfDocument); + assertNotNull(gc, "GC should be created on PDFDocument"); + + // Draw "hello world" text + gc.drawText("hello world", 100, 100); + + // Dispose of resources to finalize the PDF + gc.dispose(); + gc = null; + pdfDocument.dispose(); + pdfDocument = null; + + // Verify the PDF file was created and is not empty + assertTrue(tempFile.exists(), "PDF file should exist"); + assertTrue(tempFile.length() > 0, "PDF file should not be empty"); + + // Verify PDF magic bytes and content + byte[] fileContent = Files.readAllBytes(tempFile.toPath()); + assertTrue(fileContent.length >= 5, "PDF file should have at least 5 bytes for header"); + + // Check for PDF magic bytes: %PDF- + String headerString = new String(fileContent, 0, Math.min(5, fileContent.length)); + assertTrue(headerString.startsWith("%PDF-"), + "PDF file should start with %PDF- magic bytes, but got: " + headerString); + } + +}