diff --git a/Elwig/Elwig.csproj b/Elwig/Elwig.csproj index 9d6f450..5d7fa5c 100644 --- a/Elwig/Elwig.csproj +++ b/Elwig/Elwig.csproj @@ -23,7 +23,7 @@ - + diff --git a/Elwig/Helpers/Printing/Pdf.cs b/Elwig/Helpers/Printing/Pdf.cs index b282af3..994465b 100644 --- a/Elwig/Helpers/Printing/Pdf.cs +++ b/Elwig/Helpers/Printing/Pdf.cs @@ -1,7 +1,6 @@ using Elwig.Services; using Elwig.Windows; using System; -using System.Drawing.Printing; using System.Threading.Tasks; namespace Elwig.Helpers.Printing { @@ -32,20 +31,10 @@ namespace Elwig.Helpers.Printing { }); } - public static async Task Print(string path, int copies = 1, bool doublePaged = false) { - await Print(path, new() { - Copies = (short)copies, - Collate = true, - Duplex = doublePaged ? Duplex.Vertical : Duplex.Simplex, - }); - } - - public static Task Print(string path, PrinterSettings settings) { + public static Task Print(string path, int copies = 1, bool doublePaged = false) { try { - using var printDoc = new PdfPrintDocument(path) { - PrinterSettings = settings, - }; - printDoc.Print(); + var printer = new PdfPrinter(); + printer.Print(path, copies, doublePaged); } catch (Exception exc) { InteractionService.ShowException("Fehler beim Drucken", "Beim Drucken ist ein Fehler aufgetreten", exc); } diff --git a/Elwig/Helpers/Printing/PdfPrintDocument.cs b/Elwig/Helpers/Printing/PdfPrintDocument.cs deleted file mode 100644 index 59c2f64..0000000 --- a/Elwig/Helpers/Printing/PdfPrintDocument.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Drawing; -using System.Drawing.Imaging; -using System.Drawing.Printing; -using System.Runtime.InteropServices; - -namespace Elwig.Helpers.Printing { - public class PdfPrintDocument : PrintDocument { - - private readonly IntPtr _handle; - private readonly int _pageCount; - - private int _currentPage; - - public PdfPrintDocument(string path, string? password = null) : - base() { - _handle = PdfiumNative.FPDF_LoadDocument(path, password); - _pageCount = PdfiumNative.FPDF_GetPageCount(_handle); - } - - protected override void Dispose(bool disposing) { - PdfiumNative.FPDF_CloseDocument(_handle); - base.Dispose(disposing); - } - - protected override void OnBeginPrint(PrintEventArgs evt) { - _currentPage = (PrinterSettings.FromPage != 0) ? (PrinterSettings.FromPage - 1) : 0; - base.OnBeginPrint(evt); - } - - protected override void OnPrintPage(PrintPageEventArgs evt) { - if (_currentPage < _pageCount) { - IntPtr page = PdfiumNative.FPDF_LoadPage(_handle, _currentPage); - double width = PdfiumNative.FPDF_GetPageWidth(page); - double height = PdfiumNative.FPDF_GetPageHeight(page); - int pixelWidth = (int)(width / 72.0 * (evt.Graphics?.DpiX ?? 300.0)); - int pixelHeight = (int)(height / 72.0 * (evt.Graphics?.DpiY ?? 300.0)); - - IntPtr bitmap = PdfiumNative.FPDFBitmap_Create(pixelWidth, pixelHeight, 1); - PdfiumNative.FPDF_RenderPageBitmap(bitmap, page, 0, 0, pixelWidth, pixelHeight, 0, 0); - - IntPtr buffer = PdfiumNative.FPDFBitmap_GetBuffer(bitmap); - int stride = PdfiumNative.FPDFBitmap_GetStride(bitmap); - using (var bmp = new Bitmap(pixelWidth, pixelHeight, stride, PixelFormat.Format32bppArgb, buffer)) { - evt.Graphics?.TranslateTransform( - -evt.PageSettings.HardMarginX, - -evt.PageSettings.HardMarginY); - evt.Graphics?.DrawImage(bmp, new RectangleF(0, 0, (float)(width / 72.0 * 100.0), (float)(height / 72.0 * 100.0))); - } - _currentPage++; - - PdfiumNative.FPDFBitmap_Destroy(bitmap); - PdfiumNative.FPDF_ClosePage(page); - } - evt.HasMorePages = _currentPage < ((PrinterSettings.ToPage == 0) ? _pageCount : Math.Min(PrinterSettings.ToPage, _pageCount)); - base.OnPrintPage(evt); - } - } - - internal partial class PdfiumNative { - [LibraryImport("pdfium.dll")] - public static partial void FPDF_InitLibrary(); - - [LibraryImport("pdfium.dll")] - public static partial void FPDF_DestroyLibrary(); - - [LibraryImport("pdfium.dll", StringMarshalling = StringMarshalling.Utf8)] - public static partial IntPtr FPDF_LoadDocument(string filePath, string? password); - - [LibraryImport("pdfium.dll")] - public static partial void FPDF_CloseDocument(IntPtr document); - - [LibraryImport("pdfium.dll")] - public static partial int FPDF_GetPageCount(IntPtr document); - - [LibraryImport("pdfium.dll")] - public static partial IntPtr FPDF_LoadPage(IntPtr document, int pageIndex); - - [LibraryImport("pdfium.dll")] - public static partial void FPDF_ClosePage(IntPtr page); - - [LibraryImport("pdfium.dll")] - public static partial double FPDF_GetPageWidth(IntPtr page); - - [LibraryImport("pdfium.dll")] - public static partial double FPDF_GetPageHeight(IntPtr page); - - [LibraryImport("pdfium.dll")] - public static partial IntPtr FPDFBitmap_Create(int width, int height, int alpha); - - [LibraryImport("pdfium.dll")] - public static partial void FPDFBitmap_Destroy(IntPtr bitmap); - - [LibraryImport("pdfium.dll")] - public static partial IntPtr FPDFBitmap_GetBuffer(IntPtr bitmap); - - [LibraryImport("pdfium.dll")] - public static partial int FPDFBitmap_GetStride(IntPtr bitmap); - - [LibraryImport("pdfium.dll")] - public static partial void FPDF_RenderPageBitmap(IntPtr bitmap, IntPtr page, int start_x, int start_y, int size_x, int size_y, int rotate, int flags); - } -} diff --git a/Elwig/Helpers/Printing/PdfPrinter.cs b/Elwig/Helpers/Printing/PdfPrinter.cs new file mode 100644 index 0000000..2cdbee5 --- /dev/null +++ b/Elwig/Helpers/Printing/PdfPrinter.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.Printing; + +namespace Elwig.Helpers.Printing { + public class PdfPrinter { + + public string PrinterName { get; init; } + + public PdfPrinter(string? printerName = null) { + PrinterName = printerName ?? GetDefaultPrinterName() ?? throw new InvalidOperationException("No default Windows printer is configured."); + } + + public void Print(string pdfPath, int copies = 1, bool doublePaged = false) { + if (!File.Exists(pdfPath)) throw new FileNotFoundException("PDF file not found.", pdfPath); + + IntPtr document = PdfiumNative.FPDF_LoadDocument(pdfPath, null); + if (document == IntPtr.Zero) throw new InvalidOperationException("PDFium could not open the PDF."); + + try { + int pageCount = PdfiumNative.FPDF_GetPageCount(document); + if (pageCount <= 0) + return; + + using var printer = new Win32Printer.Printer(PrinterName); + printer.TrySetDuplex(doublePaged ? Duplexing.TwoSidedLongEdge : Duplexing.OneSided); + for (int i = 0; i < copies; i++) { + printer.StartDocument(Path.GetFileName(pdfPath)); + bool documentStarted = true; + try { + for (int j = 0; j < pageCount; j++) { + PrintPage(document, printer, j); + } + printer.EndDocument(); + documentStarted = false; + } finally { + if (documentStarted) printer.AbortDocument(); + } + } + } finally { + PdfiumNative.FPDF_CloseDocument(document); + } + } + + private static void PrintPage(IntPtr document, Win32Printer.Printer printer, int pageIndex) { + if (!PdfiumNative.FPDF_GetPageSizeByIndex(document, pageIndex, out double pageWidthPt, out double pageHeightPt)) { + throw new InvalidOperationException($"Unable to obtain PDF page {pageIndex} size."); + } + + IntPtr page = PdfiumNative.FPDF_LoadPage(document, pageIndex); + if (page == IntPtr.Zero) + throw new InvalidOperationException($"Unable to load PDF page {pageIndex}."); + + try { + // PDF coordinates are points (1/72 inch). + // + // HORZRES/VERTRES are the printable area in + // device pixels for the current printer DC. + int widthPx = printer.PixelWidth; + int heightPx = printer.PixelHeight; + + // Preserve aspect ratio. + double scaleX = widthPx / pageWidthPt; + double scaleY = heightPx / pageHeightPt; + double scale = Math.Min(scaleX, scaleY); + int renderWidth = Math.Max(1, (int)Math.Round(pageWidthPt * scale)); + int renderHeight = Math.Max(1, (int)Math.Round(pageHeightPt * scale)); + int offsetX = (widthPx - renderWidth) / 2; + int offsetY = (heightPx - renderHeight) / 2; + + printer.StartPage(); + try { + PdfiumNative.FPDF_RenderPage(printer.Hdc, page, offsetX, offsetY, renderWidth, renderHeight, 0, PdfiumNative.FPDF_PRINTING | PdfiumNative.FPDF_ANNOT); + } finally { + printer.EndPage(); + } + } finally { + PdfiumNative.FPDF_ClosePage(page); + } + } + + private static string? GetDefaultPrinterName() { + using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion\Windows"); + string? printer = key?.GetValue("Device") as string; + if (string.IsNullOrWhiteSpace(printer)) + return null; + + // Device has the form: + // Printer Name,winspool,PORT + int comma = printer.IndexOf(','); + return comma >= 0 ? printer[..comma] : printer; + } + } +} diff --git a/Elwig/Helpers/Printing/PdfiumNative.cs b/Elwig/Helpers/Printing/PdfiumNative.cs new file mode 100644 index 0000000..6412227 --- /dev/null +++ b/Elwig/Helpers/Printing/PdfiumNative.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.InteropServices; + +namespace Elwig.Helpers.Printing { + internal partial class PdfiumNative { + + public const int FPDF_ANNOT = 0x01; + public const int FPDF_PRINTING = 0x800; + + [LibraryImport("pdfium.dll")] + public static partial void FPDF_InitLibrary(); + + [LibraryImport("pdfium.dll")] + public static partial void FPDF_DestroyLibrary(); + + [LibraryImport("pdfium.dll", StringMarshalling = StringMarshalling.Utf8)] + public static partial IntPtr FPDF_LoadDocument(string filePath, string? password); + + [LibraryImport("pdfium.dll")] + public static partial void FPDF_CloseDocument(IntPtr document); + + [LibraryImport("pdfium.dll")] + public static partial int FPDF_GetPageCount(IntPtr document); + + [LibraryImport("pdfium.dll")] + public static partial IntPtr FPDF_LoadPage(IntPtr document, int pageIndex); + + [LibraryImport("pdfium.dll")] + public static partial void FPDF_ClosePage(IntPtr page); + + [LibraryImport("pdfium.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool FPDF_GetPageSizeByIndex(IntPtr document, int pageIndex, out double width, out double height); + + [LibraryImport("pdfium.dll")] + public static partial void FPDF_RenderPage(IntPtr hdc, IntPtr page, int startX, int startY, int sizeX, int sizeY, int rotate, int flags); + } +} diff --git a/Elwig/Helpers/Printing/Win32Printer.cs b/Elwig/Helpers/Printing/Win32Printer.cs new file mode 100644 index 0000000..dcd4a58 --- /dev/null +++ b/Elwig/Helpers/Printing/Win32Printer.cs @@ -0,0 +1,229 @@ +using System; +using System.ComponentModel; +using System.Printing; +using System.Runtime.InteropServices; + +namespace Elwig.Helpers.Printing { + public partial class Win32Printer { + + private const int DM_OUT_BUFFER = 2; + + private const int HORZRES = 8; + private const int VERTRES = 10; + private const int LOGPIXELSX = 88; + private const int LOGPIXELSY = 90; + + private const int DM_DUPLEX = 0x1000; + private const short DMDUP_SIMPLEX = 1; + private const short DMDUP_VERTICAL = 2; + private const short DMDUP_HORIZONTAL = 3; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct DOCINFO { + public int cbSize; + + [MarshalAs(UnmanagedType.LPWStr)] + public string? lpszDocName; + + [MarshalAs(UnmanagedType.LPWStr)] + public string? lpszOutput; + + [MarshalAs(UnmanagedType.LPWStr)] + public string? lpszDatatype; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct DEVMODE { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string dmDeviceName; + + public short dmSpecVersion; + public short dmDriverVersion; + public short dmSize; + public short dmDriverExtra; + public int dmFields; + + public short dmOrientation; + public short dmPaperSize; + public short dmPaperLength; + public short dmPaperWidth; + public short dmScale; + public short dmCopies; + public short dmDefaultSource; + public short dmPrintQuality; + public short dmColor; + public short dmDuplex; + public short dmYResolution; + public short dmTTOption; + public short dmCollate; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string dmFormName; + + public short dmLogPixels; + public int dmBitsPerPel; + public int dmPelsWidth; + public int dmPelsHeight; + public int dmDisplayFlags; + public int dmDisplayFrequency; + public int dmICMMethod; + public int dmICMIntent; + public int dmMediaType; + public int dmDitherType; + public int dmReserved1; + public int dmReserved2; + public int dmPanningWidth; + public int dmPanningHeight; + } + + [DllImport("winspool.drv", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool OpenPrinter(string pPrinterName, out IntPtr phPrinter, IntPtr pDefault); + + [DllImport("winspool.drv", SetLastError = true)] + private static extern bool ClosePrinter(IntPtr hPrinter); + + [DllImport("winspool.drv", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern int DocumentProperties(IntPtr hwnd, IntPtr hPrinter, string pDeviceName, IntPtr pDevModeOutput, IntPtr pDevModeInput, int fMode); + + [DllImport("gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr CreateDC(string? pDriver, string? pDevice, string? pOutput, IntPtr pInitData); + + [DllImport("gdi32.dll", SetLastError = true)] + private static extern bool DeleteDC(IntPtr hdc); + + [DllImport("gdi32.dll", SetLastError = true)] + private static extern int GetDeviceCaps(IntPtr hdc, int index); + + [DllImport("gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern int StartDoc(IntPtr hdc, ref DOCINFO lpdi); + + [DllImport("gdi32.dll", SetLastError = true)] + private static extern int EndDoc(IntPtr hdc); + + [DllImport("gdi32.dll", SetLastError = true)] + private static extern int AbortDoc(IntPtr hdc); + + [DllImport("gdi32.dll", SetLastError = true)] + private static extern int StartPage(IntPtr hdc); + + [DllImport("gdi32.dll", SetLastError = true)] + private static extern int EndPage(IntPtr hdc); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GlobalFree(IntPtr hMem); + + [DllImport("kernel32.dll")] + private static extern IntPtr GlobalLock(IntPtr hMem); + + [DllImport("kernel32.dll")] + private static extern bool GlobalUnlock(IntPtr hMem); + + public class Printer : IDisposable { + private readonly string _printerName; + private readonly IntPtr _printerHandle; + private readonly IntPtr _devMode; + private readonly IntPtr _hdc; + + public IntPtr Hdc => _hdc; + + public int PixelWidth => GetDeviceCaps(_hdc, HORZRES); + public int PixelHeight => GetDeviceCaps(_hdc, VERTRES); + public int DpiX => GetDeviceCaps(_hdc, LOGPIXELSX); + public int DpiY => GetDeviceCaps(_hdc, LOGPIXELSY); + + public Printer(string printerName) { + _printerName = printerName; + + if (!OpenPrinter(_printerName, out _printerHandle, IntPtr.Zero)) { + throw new Win32Exception(Marshal.GetLastWin32Error(), $"Unable to open printer '{printerName}'."); + } + + try { + _devMode = CreateDevMode(); + _hdc = CreateDC(null, _printerName, null, _devMode); + if (_hdc == IntPtr.Zero) + throw new Win32Exception(Marshal.GetLastWin32Error(), $"Unable to create printer DC for '{_printerName}'."); + } catch { + if (_devMode != IntPtr.Zero) Marshal.FreeHGlobal(_devMode); + ClosePrinter(_printerHandle); + throw; + } + } + + public bool TrySetDuplex(Duplexing duplex) { + var devMode = Marshal.PtrToStructure(_devMode); + if ((devMode.dmFields & DM_DUPLEX) != 0) { + switch (duplex) { + case Duplexing.OneSided: devMode.dmDuplex = DMDUP_SIMPLEX; break; + case Duplexing.TwoSidedShortEdge: devMode.dmDuplex = DMDUP_HORIZONTAL; break; + case Duplexing.TwoSidedLongEdge: devMode.dmDuplex = DMDUP_VERTICAL; break; + } + Marshal.StructureToPtr(devMode, _devMode, false); + return true; + } else { + return false; + } + } + + private IntPtr CreateDevMode() { + int size = DocumentProperties(IntPtr.Zero, _printerHandle, _printerName, IntPtr.Zero, IntPtr.Zero, 0); + if (size <= 0) + throw new Win32Exception(Marshal.GetLastWin32Error(), "DocumentProperties failed."); + + IntPtr devMode = Marshal.AllocHGlobal(size); + int result = DocumentProperties(IntPtr.Zero, _printerHandle, _printerName, devMode, IntPtr.Zero, DM_OUT_BUFFER); + if (result < 0) { + Marshal.FreeHGlobal(devMode); + throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to obtain printer DEVMODE."); + } + + return devMode; + } + + public void StartDocument(string documentName) { + var docInfo = new DOCINFO { + cbSize = Marshal.SizeOf(), + lpszDocName = documentName, + lpszDatatype = null, + lpszOutput = null + }; + + if (StartDoc(_hdc, ref docInfo) <= 0) + throw new Win32Exception(Marshal.GetLastWin32Error(), "StartDoc failed."); + } + + public void StartPage() { + if (Win32Printer.StartPage(_hdc) <= 0) + throw new Win32Exception(Marshal.GetLastWin32Error(), "StartPage failed."); + } + + public void EndPage() { + if (Win32Printer.EndPage(_hdc) <= 0) + throw new Win32Exception(Marshal.GetLastWin32Error(), "EndPage failed."); + } + + public void EndDocument() { + if (EndDoc(_hdc) <= 0) + throw new Win32Exception( Marshal.GetLastWin32Error(), "EndDoc failed."); + } + + public void AbortDocument() { + AbortDoc(_hdc); + } + + public void Dispose() { + if (_hdc != IntPtr.Zero) + DeleteDC(_hdc); + + if (_devMode != IntPtr.Zero) + Marshal.FreeHGlobal(_devMode); + + if (_printerHandle != IntPtr.Zero) + ClosePrinter(_printerHandle); + } + } + } +}