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); + } + +}