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 @@ - - - - - - - - diff --git a/binaries/org.eclipse.swt.cocoa.macosx.x86_64/.settings/.api_filters b/binaries/org.eclipse.swt.cocoa.macosx.x86_64/.settings/.api_filters index b60addbbb8e..3dcd19b8e19 100644 --- a/binaries/org.eclipse.swt.cocoa.macosx.x86_64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.cocoa.macosx.x86_64/.settings/.api_filters @@ -196,12 +196,4 @@ - - - - - - - - diff --git a/binaries/org.eclipse.swt.gtk.linux.aarch64/.settings/.api_filters b/binaries/org.eclipse.swt.gtk.linux.aarch64/.settings/.api_filters index 74653d35cf0..74680b18800 100644 --- a/binaries/org.eclipse.swt.gtk.linux.aarch64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.gtk.linux.aarch64/.settings/.api_filters @@ -204,12 +204,4 @@ - - - - - - - - diff --git a/binaries/org.eclipse.swt.gtk.linux.loongarch64/.settings/.api_filters b/binaries/org.eclipse.swt.gtk.linux.loongarch64/.settings/.api_filters index 93bb4727d41..3a8f35fd3d1 100644 --- a/binaries/org.eclipse.swt.gtk.linux.loongarch64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.gtk.linux.loongarch64/.settings/.api_filters @@ -204,12 +204,4 @@ - - - - - - - - diff --git a/binaries/org.eclipse.swt.gtk.linux.ppc64le/.settings/.api_filters b/binaries/org.eclipse.swt.gtk.linux.ppc64le/.settings/.api_filters index 5b182ba15ae..fd5936a3709 100644 --- a/binaries/org.eclipse.swt.gtk.linux.ppc64le/.settings/.api_filters +++ b/binaries/org.eclipse.swt.gtk.linux.ppc64le/.settings/.api_filters @@ -204,12 +204,4 @@ - - - - - - - - diff --git a/binaries/org.eclipse.swt.gtk.linux.riscv64/.settings/.api_filters b/binaries/org.eclipse.swt.gtk.linux.riscv64/.settings/.api_filters index 7e7b49ce111..d6e1e5dcb9e 100644 --- a/binaries/org.eclipse.swt.gtk.linux.riscv64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.gtk.linux.riscv64/.settings/.api_filters @@ -204,12 +204,4 @@ - - - - - - - - diff --git a/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters b/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters index ff22ad7af3f..a9011033bff 100644 --- a/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters @@ -204,12 +204,4 @@ - - - - - - - - diff --git a/binaries/org.eclipse.swt.win32.win32.aarch64/.settings/.api_filters b/binaries/org.eclipse.swt.win32.win32.aarch64/.settings/.api_filters index e32b4d5cd2e..f532b07786b 100644 --- a/binaries/org.eclipse.swt.win32.win32.aarch64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.win32.win32.aarch64/.settings/.api_filters @@ -315,12 +315,4 @@ - - - - - - - - diff --git a/binaries/org.eclipse.swt.win32.win32.x86_64/.settings/.api_filters b/binaries/org.eclipse.swt.win32.win32.x86_64/.settings/.api_filters index dfb4da06adb..5460dfa4033 100644 --- a/binaries/org.eclipse.swt.win32.win32.x86_64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.win32.win32.x86_64/.settings/.api_filters @@ -315,12 +315,4 @@ - - - - - - - - diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo.c b/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo.c index 514e744c809..2bd801b9181 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo.c +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo.c @@ -15,7 +15,7 @@ * * IBM * - Binding to permit interfacing between Cairo and SWT - * - Copyright (C) 2005, 2022 IBM Corp. All Rights Reserved. + * - Copyright (C) 2005, 2025 IBM Corp. All Rights Reserved. * * ***** END LICENSE BLOCK ***** */ @@ -704,6 +704,30 @@ JNIEXPORT void JNICALL Cairo_NATIVE(cairo_1pattern_1set_1matrix) } #endif +#ifndef NO_cairo_1pdf_1surface_1create +JNIEXPORT jlong JNICALL Cairo_NATIVE(cairo_1pdf_1surface_1create) + (JNIEnv *env, jclass that, jbyteArray arg0, jdouble arg1, jdouble arg2) +{ + jbyte *lparg0=NULL; + jlong rc = 0; + Cairo_NATIVE_ENTER(env, that, cairo_1pdf_1surface_1create_FUNC); + if (arg0) if ((lparg0 = (*env)->GetByteArrayElements(env, arg0, NULL)) == NULL) goto fail; +/* + rc = (jlong)cairo_pdf_surface_create((const char *)lparg0, arg1, arg2); +*/ + { + Cairo_LOAD_FUNCTION(fp, cairo_pdf_surface_create) + if (fp) { + rc = (jlong)((jlong (CALLING_CONVENTION*)(const char *, jdouble, jdouble))fp)((const char *)lparg0, arg1, arg2); + } + } +fail: + if (arg0 && lparg0) (*env)->ReleaseByteArrayElements(env, arg0, lparg0, 0); + Cairo_NATIVE_EXIT(env, that, cairo_1pdf_1surface_1create_FUNC); + return rc; +} +#endif + #ifndef NO_cairo_1pdf_1surface_1set_1size JNIEXPORT void JNICALL Cairo_NATIVE(cairo_1pdf_1surface_1set_1size) (JNIEnv *env, jclass that, jlong arg0, jdouble arg1, jdouble arg2) diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo_custom.h b/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo_custom.h index 3d314e25886..99408ca3154 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo_custom.h +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo_custom.h @@ -30,6 +30,7 @@ #define cairo_ps_surface_set_size_LIB LIB_CAIRO #define cairo_surface_set_device_scale_LIB LIB_CAIRO #define cairo_surface_get_device_scale_LIB LIB_CAIRO +#define cairo_pdf_surface_create_LIB LIB_CAIRO #ifdef CAIRO_HAS_XLIB_SURFACE #define cairo_xlib_surface_get_height_LIB LIB_CAIRO diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo_stats.h b/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo_stats.h index 23572bbca3b..9b79992a843 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo_stats.h +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/library/cairo_stats.h @@ -15,7 +15,7 @@ * * IBM * - Binding to permit interfacing between Cairo and SWT - * - Copyright (C) 2005, 2023 IBM Corp. All Rights Reserved. + * - Copyright (C) 2005, 2025 IBM Corp. All Rights Reserved. * * ***** END LICENSE BLOCK ***** */ @@ -86,6 +86,7 @@ typedef enum { cairo_1pattern_1set_1extend_FUNC, cairo_1pattern_1set_1filter_FUNC, cairo_1pattern_1set_1matrix_FUNC, + cairo_1pdf_1surface_1create_FUNC, cairo_1pdf_1surface_1set_1size_FUNC, cairo_1pop_1group_1to_1source_FUNC, cairo_1ps_1surface_1set_1size_FUNC, diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/org/eclipse/swt/internal/cairo/Cairo.java b/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/org/eclipse/swt/internal/cairo/Cairo.java index 19ee94d0dec..bacc4d50b1d 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/org/eclipse/swt/internal/cairo/Cairo.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cairo/org/eclipse/swt/internal/cairo/Cairo.java @@ -418,4 +418,13 @@ public class Cairo extends Platform { */ public static final native void memmove(double[] dest, long src, long size); +/** Surface type constant for SVG */ +public static final int CAIRO_SURFACE_TYPE_SVG = 4; + +/** + * @method flags=dynamic + * @param filename cast=(const char *) + */ +public static final native long cairo_pdf_surface_create(byte[] filename, double width_in_points, double height_in_points); + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/library/os.c b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/library/os.c index 0dd0bd6dd11..b29812ac759 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/library/os.c +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/library/os.c @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2022 IBM Corporation and others. + * Copyright (c) 2000, 2025 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -7,9 +7,6 @@ * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * IBM Corporation - initial API and implementation *******************************************************************************/ /* Note: This file was auto-generated by org.eclipse.swt.tools.internal.JNIGenerator */ @@ -1596,6 +1593,52 @@ JNIEXPORT void JNICALL OS_NATIVE(CGImageRelease) } #endif +#ifndef NO_CGPDFContextBeginPage +JNIEXPORT void JNICALL OS_NATIVE(CGPDFContextBeginPage) + (JNIEnv *env, jclass that, jlong arg0, jlong arg1) +{ + OS_NATIVE_ENTER(env, that, CGPDFContextBeginPage_FUNC); + CGPDFContextBeginPage((CGContextRef)arg0, (CFDictionaryRef)arg1); + OS_NATIVE_EXIT(env, that, CGPDFContextBeginPage_FUNC); +} +#endif + +#ifndef NO_CGPDFContextClose +JNIEXPORT void JNICALL OS_NATIVE(CGPDFContextClose) + (JNIEnv *env, jclass that, jlong arg0) +{ + OS_NATIVE_ENTER(env, that, CGPDFContextClose_FUNC); + CGPDFContextClose((CGContextRef)arg0); + OS_NATIVE_EXIT(env, that, CGPDFContextClose_FUNC); +} +#endif + +#ifndef NO_CGPDFContextCreateWithURL +JNIEXPORT jlong JNICALL OS_NATIVE(CGPDFContextCreateWithURL) + (JNIEnv *env, jclass that, jlong arg0, jobject arg1, jlong arg2) +{ + CGRect _arg1, *lparg1=NULL; + jlong rc = 0; + OS_NATIVE_ENTER(env, that, CGPDFContextCreateWithURL_FUNC); + if (arg1) if ((lparg1 = getCGRectFields(env, arg1, &_arg1)) == NULL) goto fail; + rc = (jlong)CGPDFContextCreateWithURL((CFURLRef)arg0, (const CGRect *)lparg1, (CFDictionaryRef)arg2); +fail: + if (arg1 && lparg1) setCGRectFields(env, arg1, lparg1); + OS_NATIVE_EXIT(env, that, CGPDFContextCreateWithURL_FUNC); + return rc; +} +#endif + +#ifndef NO_CGPDFContextEndPage +JNIEXPORT void JNICALL OS_NATIVE(CGPDFContextEndPage) + (JNIEnv *env, jclass that, jlong arg0) +{ + OS_NATIVE_ENTER(env, that, CGPDFContextEndPage_FUNC); + CGPDFContextEndPage((CGContextRef)arg0); + OS_NATIVE_EXIT(env, that, CGPDFContextEndPage_FUNC); +} +#endif + #ifndef NO_CGPathAddCurveToPoint JNIEXPORT void JNICALL OS_NATIVE(CGPathAddCurveToPoint) (JNIEnv *env, jclass that, jlong arg0, jlong arg1, jdouble arg2, jdouble arg3, jdouble arg4, jdouble arg5, jdouble arg6, jdouble arg7) diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/library/os_stats.h b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/library/os_stats.h index 3391ecb8cd1..f574e737cbc 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/library/os_stats.h +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/library/os_stats.h @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2023 IBM Corporation and others. + * Copyright (c) 2000, 2025 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -7,9 +7,6 @@ * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * IBM Corporation - initial API and implementation *******************************************************************************/ /* Note: This file was auto-generated by org.eclipse.swt.tools.internal.JNIGenerator */ @@ -117,6 +114,10 @@ typedef enum { CGImageGetHeight_FUNC, CGImageGetWidth_FUNC, CGImageRelease_FUNC, + CGPDFContextBeginPage_FUNC, + CGPDFContextClose_FUNC, + CGPDFContextCreateWithURL_FUNC, + CGPDFContextEndPage_FUNC, CGPathAddCurveToPoint_FUNC, CGPathAddLineToPoint_FUNC, CGPathApply_FUNC, diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java index e125e46194e..fffa4837edc 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java @@ -3198,6 +3198,25 @@ public static Selector getSelector (long value) { * @param image cast=(CGImageRef) */ public static final native void CGImageRelease(long image); +/** + * @param url cast=(CFURLRef) + * @param mediaBox cast=(const CGRect *) + * @param auxiliaryInfo cast=(CFDictionaryRef) + */ +public static final native long CGPDFContextCreateWithURL(long url, CGRect mediaBox, long auxiliaryInfo); +/** + * @param context cast=(CGContextRef) + * @param pageInfo cast=(CFDictionaryRef) + */ +public static final native void CGPDFContextBeginPage(long context, long pageInfo); +/** + * @param context cast=(CGContextRef) + */ +public static final native void CGPDFContextEndPage(long context); +/** + * @param context cast=(CGContextRef) + */ +public static final native void CGPDFContextClose(long context); /** * @param path cast=(CGMutablePathRef) * @param m cast=(CGAffineTransform*) diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/win32/OS.java b/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/win32/OS.java index 66b285598a3..798c7bf9ec4 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/win32/OS.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/win32/OS.java @@ -337,7 +337,18 @@ public class OS extends C { public static final int DM_COPIES = 0x00000100; public static final int DM_DUPLEX = 0x00001000; public static final int DM_ORIENTATION = 0x00000001; + public static final int DM_PAPERSIZE = 0x00000002; + public static final int DM_PAPERLENGTH = 0x00000004; + public static final int DM_PAPERWIDTH = 0x00000008; public static final int DM_OUT_BUFFER = 2; + public static final short DMPAPER_LETTER = 1; + public static final short DMPAPER_LEGAL = 5; + public static final short DMPAPER_EXECUTIVE = 7; + public static final short DMPAPER_A3 = 8; + public static final short DMPAPER_A4 = 9; + public static final short DMPAPER_A5 = 11; + public static final short DMPAPER_TABLOID = 3; + public static final short DMPAPER_USER = 256; public static final short DMORIENT_PORTRAIT = 1; public static final short DMORIENT_LANDSCAPE = 2; public static final short DMDUP_SIMPLEX = 1; diff --git a/bundles/org.eclipse.swt/Eclipse SWT Printing/cocoa/org/eclipse/swt/printing/PDFDocument.java b/bundles/org.eclipse.swt/Eclipse SWT Printing/cocoa/org/eclipse/swt/printing/PDFDocument.java new file mode 100644 index 00000000000..ce7d51ae1d1 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT Printing/cocoa/org/eclipse/swt/printing/PDFDocument.java @@ -0,0 +1,393 @@ +/******************************************************************************* + * 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.cocoa.*; + +/** + * Instances of this class are used to create PDF documents. + * Applications create a GC on a PDFDocument using 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 + * @exception SWTError + * + * @see #dispose() + */ + public PDFDocument(String filename, double widthInPoints, double heightInPoints) { + this(null, filename, widthInPoints, heightInPoints); + } + + /** + * Constructs a new PDFDocument with the specified filename and page dimensions, + * associated with the given device. + *

+ * 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 + * @exception SWTError + * + * @see #dispose() + */ + public PDFDocument(Device device, String filename, double widthInPoints, double heightInPoints) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + if (widthInPoints <= 0 || heightInPoints <= 0) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + + NSAutoreleasePool pool = null; + if (!NSThread.isMainThread()) pool = (NSAutoreleasePool) new NSAutoreleasePool().alloc().init(); + try { + this.widthInPoints = widthInPoints; + this.heightInPoints = heightInPoints; + + // Get device from the current display if not provided + if (device == null) { + try { + this.device = org.eclipse.swt.widgets.Display.getDefault(); + } catch (Exception e) { + this.device = null; + } + } else { + this.device = device; + } + + // Create CFURL from the filename + NSString path = NSString.stringWith(filename); + NSURL fileURL = NSURL.fileURLWithPath(path); + + // Create the PDF context with the media box + CGRect mediaBox = createMediaBox(); + + // Use CGPDFContextCreateWithURL + pdfContext = OS.CGPDFContextCreateWithURL(fileURL.id, mediaBox, 0); + if (pdfContext == 0) SWT.error(SWT.ERROR_NO_HANDLES); + + // Create an NSGraphicsContext from the CGContext + graphicsContext = NSGraphicsContext.graphicsContextWithGraphicsPort(pdfContext, false); + if (graphicsContext == null) { + OS.CGContextRelease(pdfContext); + pdfContext = 0; + SWT.error(SWT.ERROR_NO_HANDLES); + } + graphicsContext.retain(); + } finally { + if (pool != null) pool.release(); + } + } + + /** + * Creates a CGRect for the current page dimensions + */ + private CGRect createMediaBox() { + CGRect mediaBox = new CGRect(); + mediaBox.origin.x = 0; + mediaBox.origin.y = 0; + mediaBox.size.width = widthInPoints; + mediaBox.size.height = heightInPoints; + return mediaBox; + } + + /** + * Ensures the first page has been started + */ + private void ensurePageStarted() { + if (!pageStarted) { + OS.CGPDFContextBeginPage(pdfContext, 0); + pageStarted = true; + } + } + + /** + * Starts a new page in the PDF document. + *

+ * 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 + */ + public void newPage() { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + NSAutoreleasePool pool = null; + if (!NSThread.isMainThread()) pool = (NSAutoreleasePool) new NSAutoreleasePool().alloc().init(); + try { + if (pageStarted) { + OS.CGPDFContextEndPage(pdfContext); + } + OS.CGPDFContextBeginPage(pdfContext, 0); + pageStarted = true; + } finally { + if (pool != null) pool.release(); + } + } + + /** + * Starts a new page in the PDF document with the specified dimensions. + *

+ * 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 + * @exception SWTException + */ + public void newPage(double widthInPoints, double heightInPoints) { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + if (widthInPoints <= 0 || heightInPoints <= 0) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + + this.widthInPoints = widthInPoints; + this.heightInPoints = heightInPoints; + newPage(); + } + + /** + * Returns the width of the current page in points. + * + * @return the width in points (1/72 inch) + * + * @exception SWTException + */ + public double getWidth() { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + return widthInPoints; + } + + /** + * Returns the height of the current page in points. + * + * @return the height in points (1/72 inch) + * + * @exception SWTException + */ + public double getHeight() { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + return heightInPoints; + } + + /** + * Invokes platform specific functionality to allocate a new GC handle. + *

+ * 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. + *

+ * + * @param data the platform specific GC data + * @return the platform specific GC handle + * + * @noreference This method is not intended to be referenced by clients. + */ + @Override + public long internal_new_GC(GCData data) { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + if (isGCCreated) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + + NSAutoreleasePool pool = null; + if (!NSThread.isMainThread()) pool = (NSAutoreleasePool) new NSAutoreleasePool().alloc().init(); + try { + ensurePageStarted(); + + // Set up current graphics context + NSGraphicsContext.static_saveGraphicsState(); + NSGraphicsContext.setCurrentContext(graphicsContext); + + if (data != null) { + int mask = SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT; + if ((data.style & mask) == 0) { + data.style |= SWT.LEFT_TO_RIGHT; + } + data.device = device; + data.flippedContext = graphicsContext; + data.restoreContext = true; + NSSize size = new NSSize(); + size.width = widthInPoints; + size.height = heightInPoints; + data.size = size; + if (device != null) { + data.background = device.getSystemColor(SWT.COLOR_WHITE).handle; + data.foreground = device.getSystemColor(SWT.COLOR_BLACK).handle; + data.font = device.getSystemFont(); + } + } + isGCCreated = true; + return graphicsContext.id; + } finally { + if (pool != null) pool.release(); + } + } + + /** + * Invokes platform specific functionality to dispose a GC handle. + *

+ * 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. + *

+ * + * @param hDC the platform specific GC handle + * @param data the platform specific GC data + * + * @noreference This method is not intended to be referenced by clients. + */ + @Override + public void internal_dispose_GC(long hDC, GCData data) { + NSAutoreleasePool pool = null; + if (!NSThread.isMainThread()) pool = (NSAutoreleasePool) new NSAutoreleasePool().alloc().init(); + try { + // Only restore the graphics state if it hasn't been restored yet by uncheckGC() + if (data != null && data.restoreContext) { + NSGraphicsContext.static_restoreGraphicsState(); + data.restoreContext = false; + } + if (data != null) isGCCreated = false; + } finally { + if (pool != null) pool.release(); + } + } + + /** + * @noreference This method is not intended to be referenced by clients. + */ + @Override + public boolean isAutoScalable() { + return false; + } + + /** + * Returns 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 using 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 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 + * @exception SWTError + * + * @see #dispose() + */ + public PDFDocument(String filename, double widthInPoints, double heightInPoints) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + if (widthInPoints <= 0 || heightInPoints <= 0) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + + this.widthInPoints = widthInPoints; + this.heightInPoints = heightInPoints; + + byte[] filenameBytes = Converter.wcsToMbcs(filename, true); + surface = Cairo.cairo_pdf_surface_create(filenameBytes, widthInPoints, heightInPoints); + if (surface == 0) SWT.error(SWT.ERROR_NO_HANDLES); + + cairo = Cairo.cairo_create(surface); + if (cairo == 0) { + Cairo.cairo_surface_destroy(surface); + surface = 0; + SWT.error(SWT.ERROR_NO_HANDLES); + } + + // Get device from the current display or create a temporary one + try { + device = org.eclipse.swt.widgets.Display.getDefault(); + } catch (Exception e) { + device = null; + } + } + + /** + * Constructs a new PDFDocument with the specified filename and page dimensions, + * associated with the given device. + *

+ * 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 + * @exception SWTError + * + * @see #dispose() + */ + public PDFDocument(Device device, String filename, double widthInPoints, double heightInPoints) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + if (widthInPoints <= 0 || heightInPoints <= 0) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + + this.device = device; + this.widthInPoints = widthInPoints; + this.heightInPoints = heightInPoints; + + byte[] filenameBytes = Converter.wcsToMbcs(filename, true); + surface = Cairo.cairo_pdf_surface_create(filenameBytes, widthInPoints, heightInPoints); + if (surface == 0) SWT.error(SWT.ERROR_NO_HANDLES); + + cairo = Cairo.cairo_create(surface); + if (cairo == 0) { + Cairo.cairo_surface_destroy(surface); + surface = 0; + SWT.error(SWT.ERROR_NO_HANDLES); + } + } + + /** + * Starts a new page in the PDF document. + *

+ * 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 + */ + public void newPage() { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + Cairo.cairo_show_page(cairo); + } + + /** + * Starts a new page in the PDF document with the specified dimensions. + *

+ * 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 + * @exception SWTException + */ + public void newPage(double widthInPoints, double heightInPoints) { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + if (widthInPoints <= 0 || heightInPoints <= 0) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + + Cairo.cairo_show_page(cairo); + Cairo.cairo_pdf_surface_set_size(surface, widthInPoints, heightInPoints); + this.widthInPoints = widthInPoints; + this.heightInPoints = heightInPoints; + } + + /** + * Returns the width of the current page in points. + * + * @return the width in points (1/72 inch) + * + * @exception SWTException + */ + public double getWidth() { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + return widthInPoints; + } + + /** + * Returns the height of the current page in points. + * + * @return the height in points (1/72 inch) + * + * @exception SWTException + */ + public double getHeight() { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + return heightInPoints; + } + + /** + * Invokes platform specific functionality to allocate a new GC handle. + *

+ * 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. + *

+ * + * @param data the platform specific GC data + * @return the platform specific GC handle + * + * @noreference This method is not intended to be referenced by clients. + */ + @Override + public long internal_new_GC(GCData data) { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + if (isGCCreated) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + + if (data != null) { + int mask = SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT; + if ((data.style & mask) == 0) { + data.style |= SWT.LEFT_TO_RIGHT; + } + data.device = device; + data.cairo = cairo; + data.width = (int) widthInPoints; + data.height = (int) heightInPoints; + if (device != null) { + data.foregroundRGBA = device.getSystemColor(SWT.COLOR_BLACK).handle; + data.backgroundRGBA = device.getSystemColor(SWT.COLOR_WHITE).handle; + data.font = device.getSystemFont(); + } else { + // Fallback: create default colors manually using GdkRGBA values + data.foregroundRGBA = new org.eclipse.swt.internal.gtk.GdkRGBA(); + data.foregroundRGBA.red = 0; + data.foregroundRGBA.green = 0; + data.foregroundRGBA.blue = 0; + data.foregroundRGBA.alpha = 1; + data.backgroundRGBA = new org.eclipse.swt.internal.gtk.GdkRGBA(); + data.backgroundRGBA.red = 1; + data.backgroundRGBA.green = 1; + data.backgroundRGBA.blue = 1; + data.backgroundRGBA.alpha = 1; + } + } + isGCCreated = true; + return cairo; + } + + /** + * Invokes platform specific functionality to dispose a GC handle. + *

+ * 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. + *

+ * + * @param hDC the platform specific GC handle + * @param data the platform specific GC data + * + * @noreference This method is not intended to be referenced by clients. + */ + @Override + public void internal_dispose_GC(long hDC, GCData data) { + if (data != null) isGCCreated = false; + } + + /** + * @noreference This method is not intended to be referenced by clients. + */ + @Override + public boolean isAutoScalable() { + return false; + } + + /** + * Returns 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 using 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. + *

+ *

+ * 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 + * @exception SWTError + * + * @see #dispose() + */ + public PDFDocument(String filename, double width, double height) { + this(null, filename, width, height); + } + + /** + * Constructs a new PDFDocument with the specified filename and page dimensions, + * associated with the given device. + *

+ * 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 + * @exception SWTError + * + * @see #dispose() + */ + public PDFDocument(Device device, String filename, double width, double height) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + if (width <= 0 || height <= 0) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + + this.filename = filename; + this.width = width; + this.height = height; + + // Get device from the current display if not provided + if (device == null) { + try { + this.device = org.eclipse.swt.widgets.Display.getDefault(); + } catch (SWTException e) { + this.device = null; + } + } else { + this.device = device; + } + + // Calculate physical size in inches from screen pixels + int screenDpiX = 96; + int screenDpiY = 96; + if (this.device != null) { + Point dpi = this.device.getDPI(); + screenDpiX = dpi.x; + screenDpiY = dpi.y; + } + double widthInInches = width / screenDpiX; + double heightInInches = height / screenDpiY; + + // Microsoft Print to PDF doesn't support custom page sizes + // Find the best matching standard paper size + PaperSize bestMatch = findBestPaperSize(widthInInches, heightInInches); + this.widthInPoints = bestMatch.widthInInches * 72.0; + this.heightInPoints = bestMatch.heightInInches * 72.0; + + // Create printer DC for "Microsoft Print to PDF" + TCHAR driver = new TCHAR(0, "WINSPOOL", true); + TCHAR deviceName = new TCHAR(0, PDF_PRINTER_NAME, true); + + // Get printer settings + long[] hPrinter = new long[1]; + if (OS.OpenPrinter(deviceName, hPrinter, 0)) { + int dwNeeded = OS.DocumentProperties(0, hPrinter[0], deviceName, 0, 0, 0); + if (dwNeeded >= 0) { + long hHeap = OS.GetProcessHeap(); + long lpInitData = OS.HeapAlloc(hHeap, OS.HEAP_ZERO_MEMORY, dwNeeded); + if (lpInitData != 0) { + int rc = OS.DocumentProperties(0, hPrinter[0], deviceName, lpInitData, 0, OS.DM_OUT_BUFFER); + if (rc == OS.IDOK) { + DEVMODE devmode = new DEVMODE(); + OS.MoveMemory(devmode, lpInitData, DEVMODE.sizeof); + devmode.dmPaperSize = (short) bestMatch.paperSizeConstant; + devmode.dmOrientation = (short) bestMatch.orientation; + devmode.dmFields = OS.DM_PAPERSIZE | OS.DM_ORIENTATION; + OS.MoveMemory(lpInitData, devmode, DEVMODE.sizeof); + handle = OS.CreateDC(driver, deviceName, 0, lpInitData); + } + OS.HeapFree(hHeap, 0, lpInitData); + } + } + OS.ClosePrinter(hPrinter[0]); + } + + if (handle == 0) { + SWT.error(SWT.ERROR_NO_HANDLES); + } + } + + /** + * Ensures the print job has been started + */ + private void ensureJobStarted() { + if (!jobStarted) { + DOCINFO di = new DOCINFO(); + di.cbSize = DOCINFO.sizeof; + long hHeap = OS.GetProcessHeap(); + + // Set output filename + TCHAR buffer = new TCHAR(0, filename, true); + int byteCount = buffer.length() * TCHAR.sizeof; + long lpszOutput = OS.HeapAlloc(hHeap, OS.HEAP_ZERO_MEMORY, byteCount); + OS.MoveMemory(lpszOutput, buffer, byteCount); + di.lpszOutput = lpszOutput; + + // Set document name + TCHAR docName = new TCHAR(0, "SWT PDF Document", true); + int docByteCount = docName.length() * TCHAR.sizeof; + long lpszDocName = OS.HeapAlloc(hHeap, OS.HEAP_ZERO_MEMORY, docByteCount); + OS.MoveMemory(lpszDocName, docName, docByteCount); + di.lpszDocName = lpszDocName; + + int rc = OS.StartDoc(handle, di); + + OS.HeapFree(hHeap, 0, lpszOutput); + OS.HeapFree(hHeap, 0, lpszDocName); + + if (rc <= 0) { + SWT.error(SWT.ERROR_NO_HANDLES); + } + jobStarted = true; + } + } + + /** + * Ensures the current page has been started + */ + private void ensurePageStarted() { + ensureJobStarted(); + if (!pageStarted) { + OS.StartPage(handle); + pageStarted = true; + } + } + + /** + * Starts a new page in the PDF document. + *

+ * 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 + */ + public void newPage() { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + if (pageStarted) { + OS.EndPage(handle); + pageStarted = false; + } + ensurePageStarted(); + } + + /** + * Starts a new page in the PDF document with the specified dimensions. + *

+ * 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 + * @exception SWTException + */ + public void newPage(double widthInPoints, double heightInPoints) { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + if (widthInPoints <= 0 || heightInPoints <= 0) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + + this.widthInPoints = widthInPoints; + this.heightInPoints = heightInPoints; + newPage(); + } + + /** + * Returns the width of the current page in points. + * + * @return the width in points (1/72 inch) + * + * @exception SWTException + */ + public double getWidth() { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + return widthInPoints; + } + + /** + * Returns the height of the current page in points. + * + * @return the height in points (1/72 inch) + * + * @exception SWTException + */ + public double getHeight() { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + return heightInPoints; + } + + /** + * Invokes platform specific functionality to allocate a new GC handle. + *

+ * 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. + *

+ * + * @param data the platform specific GC data + * @return the platform specific GC handle + * + * @noreference This method is not intended to be referenced by clients. + */ + @Override + public long internal_new_GC(GCData data) { + if (disposed) SWT.error(SWT.ERROR_WIDGET_DISPOSED); + if (isGCCreated) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + + ensurePageStarted(); + + if (data != null) { + int mask = SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT; + if ((data.style & mask) != 0) { + data.layout = (data.style & SWT.RIGHT_TO_LEFT) != 0 ? OS.LAYOUT_RTL : 0; + } else { + data.style |= SWT.LEFT_TO_RIGHT; + } + data.device = device; + data.nativeZoom = 100; + if (device != null) { + data.font = device.getSystemFont(); + } + } + + // Set up coordinate system scaling + // Get screen DPI + int screenDpiX = 96; + int screenDpiY = 96; + if (device != null) { + Point dpi = device.getDPI(); + screenDpiX = dpi.x; + screenDpiY = dpi.y; + } + + // Get PDF printer DPI + int pdfDpiX = OS.GetDeviceCaps(handle, OS.LOGPIXELSX); + int pdfDpiY = OS.GetDeviceCaps(handle, OS.LOGPIXELSY); + + // Calculate content size in inches (what user wanted) + double contentWidthInInches = width / screenDpiX; + double contentHeightInInches = height / screenDpiY; + + // Calculate scale factor to fit content to page + // The page size is the physical paper size we selected + double pageWidthInInches = widthInPoints / 72.0; + double pageHeightInInches = heightInPoints / 72.0; + double scaleToFitWidth = pageWidthInInches / contentWidthInInches; + double scaleToFitHeight = pageHeightInInches / contentHeightInInches; + + // Use the smaller scale to ensure both width and height fit + double scaleToFit = Math.min(scaleToFitWidth, scaleToFitHeight); + + // Combined scale: fit-to-page * DPI conversion + float scaleX = (float)(scaleToFit * pdfDpiX / screenDpiX); + float scaleY = (float)(scaleToFit * pdfDpiY / screenDpiY); + + OS.SetGraphicsMode(handle, OS.GM_ADVANCED); + float[] transform = new float[] {scaleX, 0, 0, scaleY, 0, 0}; + OS.SetWorldTransform(handle, transform); + + isGCCreated = true; + return handle; + } + + /** + * Invokes platform specific functionality to dispose a GC handle. + *

+ * 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. + *

+ * + * @param hDC the platform specific GC handle + * @param data the platform specific GC data + * + * @noreference This method is not intended to be referenced by clients. + */ + @Override + public void internal_dispose_GC(long hDC, GCData data) { + if (data != null) isGCCreated = false; + } + + /** + * @noreference This method is not intended to be referenced by clients. + */ + @Override + public boolean isAutoScalable() { + return false; + } + + /** + * Returns 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); + } + +}