diff --git a/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs b/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs index c915d5b9..a4ae780b 100644 --- a/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs +++ b/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Common; -using Wino.Core.Domain.Models.Printing; namespace Wino.Core.Domain.Interfaces; @@ -30,5 +29,4 @@ public interface IDialogServiceBase Task> PickFilesAsync(params object[] typeFilters); Task> PickFilesMetadataAsync(params object[] typeFilters); Task PickFilePathAsync(string saveFileName); - Task ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null); } diff --git a/Wino.Core.Domain/Interfaces/IPrintService.cs b/Wino.Core.Domain/Interfaces/IPrintService.cs index 4d8cb33d..4b92d839 100644 --- a/Wino.Core.Domain/Interfaces/IPrintService.cs +++ b/Wino.Core.Domain/Interfaces/IPrintService.cs @@ -1,9 +1,12 @@ -using System.Threading.Tasks; +using System; +using System.IO; +using System.Threading.Tasks; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Printing; namespace Wino.Core.Domain.Interfaces; public interface IPrintService { - Task PrintPdfFileAsync(string pdfFilePath, string printTitle); + Task PrintAsync(nint windowHandle, string printTitle, Func> renderPdfStreamAsync); } diff --git a/Wino.Core.Domain/Models/Printing/PrintInformation.cs b/Wino.Core.Domain/Models/Printing/PrintInformation.cs deleted file mode 100644 index 2d5d91b9..00000000 --- a/Wino.Core.Domain/Models/Printing/PrintInformation.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Wino.Core.Domain.Models.Printing; - -public class PrintInformation -{ - public PrintInformation(string pDFFilePath, string pDFTitle) - { - PDFFilePath = pDFFilePath ?? throw new ArgumentNullException(nameof(pDFFilePath)); - PDFTitle = pDFTitle ?? throw new ArgumentNullException(nameof(pDFTitle)); - } - - public string PDFFilePath { get; } - public string PDFTitle { get; } -} diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 7e6aed25..f895667e 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -58,9 +58,9 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, private MimeMessageInformation initializedMimeMessageInformation = null; // Func to get WebView2 to save current HTML as PDF to given location. - // Used in 'Save as' and 'Print' functionality. + // Used in 'Save as' functionality. public Func> SaveHTMLasPDFFunc { get; set; } - public Func> DirectPrintFuncAsync { get; set; } + public Func> RenderPdfStreamFuncAsync { get; set; } public Func RenderHtmlAsyncFunc { get; set; } public Func ClearRenderedHtmlAsyncFunc { get; set; } @@ -275,17 +275,17 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, } else if (operation == MailOperation.Print) { - var settings = await _dialogService.ShowPrintDialogAsync(); - - if (settings == null) return; - - var printingResult = await DirectPrintFuncAsync.Invoke(settings); + var printingResult = await PrintAsync(); // TODO: More detailed printing result handling. if (printingResult == PrintingResult.Submitted) { _dialogService.InfoBarMessage(Translator.DialogMessage_PrintingSuccessTitle, Translator.DialogMessage_PrintingSuccessMessage, InfoBarMessageType.Success); } + else if (printingResult == PrintingResult.Canceled) + { + return; + } else if (printingResult == PrintingResult.Failed) { _dialogService.InfoBarMessage(Translator.DialogMessage_PrintingFailedTitle, Translator.DialogMessage_PrintingFailedMessage, InfoBarMessageType.Error); @@ -784,6 +784,22 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, } } + private async Task PrintAsync() + { + if (RenderPdfStreamFuncAsync == null) + return PrintingResult.Failed; + + var windowHandle = NativeAppService.GetCoreWindowHwnd(); + if (windowHandle == IntPtr.Zero) + return PrintingResult.Failed; + + var printTitle = string.IsNullOrWhiteSpace(Subject) + ? Translator.MailItemNoSubject + : Subject; + + return await PrintService.PrintAsync(windowHandle, printTitle, RenderPdfStreamFuncAsync); + } + // Returns created file path. private async Task SaveAttachmentInternalAsync(MailAttachmentViewModel attachmentViewModel, string saveFolderPath) { diff --git a/Wino.Mail.WinUI/Dialogs/PrintDialog.xaml b/Wino.Mail.WinUI/Dialogs/PrintDialog.xaml deleted file mode 100644 index 750782ae..00000000 --- a/Wino.Mail.WinUI/Dialogs/PrintDialog.xaml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wino.Mail.WinUI/Dialogs/PrintDialog.xaml.cs b/Wino.Mail.WinUI/Dialogs/PrintDialog.xaml.cs deleted file mode 100644 index 13b9cfb2..00000000 --- a/Wino.Mail.WinUI/Dialogs/PrintDialog.xaml.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.UI.Xaml.Controls; -using Serilog; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.Printing; -using Wino.Mail.WinUI.Helpers; - -namespace Wino.Mail.WinUI.Dialogs; - -/// -/// Custom print dialog for configuring WebView2 print settings. -/// -public sealed partial class PrintDialog : ContentDialog -{ - public WebView2PrintSettingsModel PrintSettings { get; set; } = new WebView2PrintSettingsModel(); - - public PrintDialog() - { - this.InitializeComponent(); - } - - /// - /// Initializes the dialog with existing print settings. - /// - /// The initial print settings to load. - public PrintDialog(WebView2PrintSettingsModel printSettings = default!) - { - if (printSettings != null) PrintSettings = printSettings; - - this.InitializeComponent(); - } - - private void PrintDialog_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => LoadSettingsToUI(PrintSettings); - - private void OrientationRadio_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is RadioButtons radioButtons) - { - PrintSettings.Orientation = (PrintOrientation)radioButtons.SelectedIndex; - } - } - - private void PrinterComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is ComboBox comboBox && comboBox.SelectedItem != null) - { - PrintSettings.PrinterName = comboBox.SelectedItem.ToString(); - } - } - - /// - /// Sets the list of available printers for the dialog. - /// - /// List of available printer names. - public void SetAvailablePrinters(IEnumerable printers) - { - var printerList = printers?.ToList() ?? new List(); - - if (this.FindName("PrinterComboBox") is ComboBox printerComboBox) - { - printerComboBox.ItemsSource = printerList; - - if (printerList.Any()) - { - // Set to first printer or to the one in settings - var targetPrinter = !string.IsNullOrEmpty(PrintSettings.PrinterName) - ? PrintSettings.PrinterName - : printerList.First(); - - var index = printerList.IndexOf(targetPrinter); - printerComboBox.SelectedIndex = index >= 0 ? index : 0; - - // Update the settings model with the selected printer - PrintSettings.PrinterName = printerComboBox.SelectedItem?.ToString() ?? string.Empty; - } - } - } - - /// - /// Loads available printers asynchronously and sets them in the dialog. - /// - public async Task LoadAvailablePrintersAsync() - { - try - { - var printers = await Task.Run(() => - { - return InstalledPrinterHelper.GetInstalledPrinters().AsEnumerable(); - }); - - SetAvailablePrinters(printers); - } - catch (System.Exception ex) - { - // Log the exception if logging is available - Log.Error(ex, "Error getting available printers"); - - // Set empty list if printer discovery fails - SetAvailablePrinters(Enumerable.Empty()); - } - } - - private void LoadSettingsToUI(WebView2PrintSettingsModel settings) - { - if (settings == null) return; - - // Only handle orientation manually since other properties are bound via x:Bind - if (this.FindName("OrientationRadioButtons") is RadioButtons orientationRadio) - { - orientationRadio.SelectedIndex = (int)settings.Orientation; - } - } - - private void UpdateSettingsFromUI() - { - // Most properties are bound via x:Bind, only handle orientation manually - if (this.FindName("OrientationRadioButtons") is RadioButtons orientationRadio) - { - PrintSettings.Orientation = (PrintOrientation)orientationRadio.SelectedIndex; - } - - // Also update printer name from ComboBox since it uses ItemsSource binding - if (this.FindName("PrinterComboBox") is ComboBox printerComboBox && - printerComboBox.SelectedItem != null) - { - PrintSettings.PrinterName = printerComboBox.SelectedItem.ToString(); - } - } - - /// - /// Validates the current print settings before closing the dialog. - /// - /// True if settings are valid, false otherwise. - private bool ValidateSettings() - { - // Check if a printer is selected - if (this.FindName("PrinterComboBox") is ComboBox printerComboBox && - printerComboBox.SelectedItem == null) - { - return false; - } - - // Copies validation is handled by the bound property with validation in the model - if (PrintSettings.Copies <= 0) - { - return false; - } - - return true; - } - - private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) - { - // Update settings from UI before validation - UpdateSettingsFromUI(); - - // Validate settings before closing - if (!ValidateSettings()) - { - args.Cancel = true; - } - } - - private void ContentDialog_SecondaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) - { - // Cancel was clicked, no validation needed - } -} diff --git a/Wino.Mail.WinUI/Extensions/PrintSettingsExtensions.cs b/Wino.Mail.WinUI/Extensions/PrintSettingsExtensions.cs index db40d939..9057d6ff 100644 --- a/Wino.Mail.WinUI/Extensions/PrintSettingsExtensions.cs +++ b/Wino.Mail.WinUI/Extensions/PrintSettingsExtensions.cs @@ -178,6 +178,33 @@ public static class PrintSettingsExtensions return settings; } + /// + /// Creates a CoreWebView2PrintSettings object containing only document-safe settings for PDF generation. + /// Printer job options such as copies, duplex, printer name, N-up, and page ranges stay in the WinRT print session. + /// + public static CoreWebView2PrintSettings ToCoreWebView2PdfRenderSettings( + this WebView2PrintSettingsModel model, + CoreWebView2Environment environment) + { + var settings = environment.CreatePrintSettings(); + + settings.Orientation = model.Orientation.ToCoreWebView2Orientation(); + settings.ColorMode = model.ColorMode.ToCoreWebView2ColorMode(); + settings.MediaSize = model.MediaSize.ToCoreWebView2MediaSize(); + settings.MarginTop = model.MarginTop; + settings.MarginBottom = model.MarginBottom; + settings.MarginLeft = model.MarginLeft; + settings.MarginRight = model.MarginRight; + settings.ShouldPrintBackgrounds = model.ShouldPrintBackgrounds; + settings.ShouldPrintSelectionOnly = model.ShouldPrintSelectionOnly; + settings.ShouldPrintHeaderAndFooter = model.ShouldPrintHeaderAndFooter; + settings.HeaderTitle = model.HeaderTitle; + settings.FooterUri = model.FooterUri; + settings.ScaleFactor = model.ScaleFactor; + + return settings; + } + /// /// Updates a WebView2PrintSettingsModel from a CoreWebView2PrintSettings object. /// @@ -209,4 +236,4 @@ public static class PrintSettingsExtensions model.PagesPerSide = settings.PagesPerSide; model.PageRanges = settings.PageRanges ?? string.Empty; } -} \ No newline at end of file +} diff --git a/Wino.Mail.WinUI/Helpers/InstalledPrinterHelper.cs b/Wino.Mail.WinUI/Helpers/InstalledPrinterHelper.cs deleted file mode 100644 index 34d629ed..00000000 --- a/Wino.Mail.WinUI/Helpers/InstalledPrinterHelper.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace Wino.Mail.WinUI.Helpers; - -internal static partial class InstalledPrinterHelper -{ - private const int PrinterEnumLocal = 0x00000002; - private const int PrinterEnumConnections = 0x00000004; - - public static IReadOnlyList GetInstalledPrinters() - { - var flags = PrinterEnumLocal | PrinterEnumConnections; - - if (!EnumPrinters(flags, null, 4, IntPtr.Zero, 0, out var bytesNeeded, out _)) - { - var error = Marshal.GetLastWin32Error(); - if (error != 122 || bytesNeeded <= 0) - { - throw new InvalidOperationException($"EnumPrinters failed to query buffer size. Win32 error: {error}."); - } - } - - var printerBuffer = Marshal.AllocHGlobal(bytesNeeded); - try - { - if (!EnumPrinters(flags, null, 4, printerBuffer, bytesNeeded, out _, out var printerCount)) - { - throw new InvalidOperationException($"EnumPrinters failed to enumerate printers. Win32 error: {Marshal.GetLastWin32Error()}."); - } - - var printers = new List(printerCount); - var structSize = Marshal.SizeOf(); - - for (var i = 0; i < printerCount; i++) - { - var current = IntPtr.Add(printerBuffer, i * structSize); - var info = Marshal.PtrToStructure(current); - if (!string.IsNullOrWhiteSpace(info.PrinterName)) - { - printers.Add(info.PrinterName); - } - } - - return printers; - } - finally - { - Marshal.FreeHGlobal(printerBuffer); - } - } - - [LibraryImport("winspool.drv", EntryPoint = "EnumPrintersW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool EnumPrinters( - int flags, - string? name, - int level, - IntPtr pPrinterEnum, - int cbBuf, - out int pcbNeeded, - out int pcReturned); - - [StructLayout(LayoutKind.Sequential)] - private struct PrinterInfo4 - { - public nint PrinterNamePointer; - public nint ServerNamePointer; - public int Attributes; - - public string? PrinterName => Marshal.PtrToStringUni(PrinterNamePointer); - } -} diff --git a/Wino.Mail.WinUI/Models/PrintDialogViewModel.cs b/Wino.Mail.WinUI/Models/PrintDialogViewModel.cs deleted file mode 100644 index 5f4498ac..00000000 --- a/Wino.Mail.WinUI/Models/PrintDialogViewModel.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using Microsoft.UI.Xaml.Controls; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.Printing; - -namespace Wino.Mail.WinUI.Dialogs; - -/// -/// ViewModel for the PrintDialog that handles data binding and state management. -/// -public class PrintDialogViewModel : INotifyPropertyChanged -{ - private List _availablePrinters = new(); - private bool _isCustomPageRange = false; - private WebView2PrintSettingsModel _printSettings = new(); - - public event PropertyChangedEventHandler? PropertyChanged; - - public PrintDialogViewModel() - { - // Initialize default values - PrintSettings.PropertyChanged += OnPrintSettingsChanged; - } - - /// - /// The print settings model that will be configured by the dialog. - /// - public WebView2PrintSettingsModel PrintSettings - { - get => _printSettings; - set - { - if (_printSettings != value) - { - if (_printSettings != null) - _printSettings.PropertyChanged -= OnPrintSettingsChanged; - - _printSettings = value; - - if (_printSettings != null) - _printSettings.PropertyChanged += OnPrintSettingsChanged; - - OnPropertyChanged(nameof(PrintSettings)); - UpdateDerivedProperties(); - } - } - } - - /// - /// List of available printers. - /// - public List AvailablePrinters - { - get => _availablePrinters; - set - { - if (_availablePrinters != value) - { - _availablePrinters = value ?? new List(); - OnPropertyChanged(nameof(AvailablePrinters)); - } - } - } - - /// - /// Index for the orientation radio buttons. - /// - public int OrientationIndex - { - get => (int)PrintSettings.Orientation; - set - { - if (value >= 0 && value <= 1) - { - PrintSettings.Orientation = (PrintOrientation)value; - OnPropertyChanged(nameof(OrientationIndex)); - } - } - } - - /// - /// Index for the color mode radio buttons. - /// - public int ColorModeIndex - { - get => (int)PrintSettings.ColorMode; - set - { - if (value >= 0 && value <= 2) - { - PrintSettings.ColorMode = (PrintColorMode)value; - OnPropertyChanged(nameof(ColorModeIndex)); - } - } - } - - /// - /// Index for the collation radio buttons. - /// - public int CollationIndex - { - get => (int)PrintSettings.Collation; - set - { - if (value >= 0 && value <= 2) - { - PrintSettings.Collation = (PrintCollation)value; - OnPropertyChanged(nameof(CollationIndex)); - } - } - } - - /// - /// Index for the duplex radio buttons. - /// - public int DuplexIndex - { - get => (int)PrintSettings.Duplex; - set - { - if (value >= 0 && value <= 3) - { - PrintSettings.Duplex = (PrintDuplex)value; - OnPropertyChanged(nameof(DuplexIndex)); - } - } - } - - /// - /// Index for the media size combo box. - /// - public int MediaSizeIndex - { - get => (int)PrintSettings.MediaSize; - set - { - if (value >= 0 && value <= 9) - { - PrintSettings.MediaSize = (PrintMediaSize)value; - OnPropertyChanged(nameof(MediaSizeIndex)); - } - } - } - - /// - /// Index for the pages per side combo box. - /// - public int PagesPerSideIndex - { - get - { - var validValues = new[] { 1, 2, 4, 6, 9, 16 }; - return Array.IndexOf(validValues, PrintSettings.PagesPerSide); - } - set - { - var validValues = new[] { 1, 2, 4, 6, 9, 16 }; - if (value >= 0 && value < validValues.Length) - { - PrintSettings.PagesPerSide = validValues[value]; - OnPropertyChanged(nameof(PagesPerSideIndex)); - } - } - } - - /// - /// Index for the page range option (0 = All pages, 1 = Custom range). - /// - public int PageRangeOptionIndex - { - get => IsCustomPageRange ? 1 : 0; - set - { - IsCustomPageRange = value == 1; - if (!IsCustomPageRange) - { - PrintSettings.PageRanges = string.Empty; - } - OnPropertyChanged(nameof(PageRangeOptionIndex)); - } - } - - /// - /// Whether custom page range is selected. - /// - public bool IsCustomPageRange - { - get => _isCustomPageRange; - private set - { - if (_isCustomPageRange != value) - { - _isCustomPageRange = value; - OnPropertyChanged(nameof(IsCustomPageRange)); - } - } - } - - /// - /// Scale factor as percentage text for display. - /// - public string ScalePercentageText => $"{(int)(PrintSettings.ScaleFactor * 100)}%"; - - /// - /// Initializes the dialog with the provided print settings. - /// - /// The initial print settings. - public void Initialize(WebView2PrintSettingsModel printSettings = default!) - { - if (printSettings != null) - { - PrintSettings = printSettings; - } - else - { - PrintSettings = new WebView2PrintSettingsModel(); - } - - UpdateDerivedProperties(); - } - - /// - /// Sets the list of available printers. - /// - /// List of printer names. - public void SetAvailablePrinters(IEnumerable printers) - { - AvailablePrinters = printers?.ToList() ?? new List(); - - // If current printer is not in the list, select the first one - if (AvailablePrinters.Any() && !AvailablePrinters.Contains(PrintSettings.PrinterName)) - { - PrintSettings.PrinterName = AvailablePrinters.First(); - } - } - - private void OnPrintSettingsChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(WebView2PrintSettingsModel.ScaleFactor)) - { - OnPropertyChanged(nameof(ScalePercentageText)); - } - else if (e.PropertyName == nameof(WebView2PrintSettingsModel.PageRanges)) - { - // Update custom page range flag based on whether page ranges is empty - if (!string.IsNullOrWhiteSpace(PrintSettings.PageRanges)) - { - IsCustomPageRange = true; - OnPropertyChanged(nameof(PageRangeOptionIndex)); - } - } - } - - private void UpdateDerivedProperties() - { - OnPropertyChanged(nameof(OrientationIndex)); - OnPropertyChanged(nameof(ColorModeIndex)); - OnPropertyChanged(nameof(CollationIndex)); - OnPropertyChanged(nameof(DuplexIndex)); - OnPropertyChanged(nameof(MediaSizeIndex)); - OnPropertyChanged(nameof(PagesPerSideIndex)); - OnPropertyChanged(nameof(PageRangeOptionIndex)); - OnPropertyChanged(nameof(ScalePercentageText)); - - // Update custom page range based on current page ranges value - IsCustomPageRange = !string.IsNullOrWhiteSpace(PrintSettings.PageRanges); - } - - protected virtual void OnPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } -} diff --git a/Wino.Mail.WinUI/Services/DialogServiceBase.cs b/Wino.Mail.WinUI/Services/DialogServiceBase.cs index f097abc5..f066b864 100644 --- a/Wino.Mail.WinUI/Services/DialogServiceBase.cs +++ b/Wino.Mail.WinUI/Services/DialogServiceBase.cs @@ -15,7 +15,6 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Common; -using Wino.Core.Domain.Models.Printing; using Wino.Dialogs; using Wino.Mail.WinUI.Dialogs; using Wino.Mail.WinUI.Extensions; @@ -355,37 +354,4 @@ public class DialogServiceBase : IDialogServiceBase return dialog.Result ?? null!; } - public async Task ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = default!) - { - try - { - // Create the print dialog - var dialog = initialSettings != null - ? new PrintDialog(initialSettings) - : new PrintDialog(); - - // Set the XamlRoot for proper display - dialog.XamlRoot = GetXamlRoot(); - - // Get available printers asynchronously when the dialog is loaded - dialog.Loaded += async (sender, e) => - { - await dialog.LoadAvailablePrintersAsync(); - }; - - // Show the dialog - var result = await HandleDialogPresentationAsync(dialog); - - // Return the settings if user clicked Print, otherwise null - return result == ContentDialogResult.Primary - ? dialog.PrintSettings - : null!; - } - catch (Exception ex) - { - // Log the exception if logging is available - Log.Error(ex, "Error showing print dialog"); - return null!; - } - } } diff --git a/Wino.Mail.WinUI/Services/PrintService.cs b/Wino.Mail.WinUI/Services/PrintService.cs index 513ee582..7f69ccca 100644 --- a/Wino.Mail.WinUI/Services/PrintService.cs +++ b/Wino.Mail.WinUI/Services/PrintService.cs @@ -1,141 +1,212 @@ using System; using System.Collections.Generic; +using System.IO; using System.Numerics; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas.Printing; using Windows.Data.Pdf; using Windows.Graphics.Printing; using Windows.Graphics.Printing.OptionDetails; -using Windows.Storage.Streams; +using WinRT.Interop; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Printing; +using DomainPrintCollation = Wino.Core.Domain.Enums.PrintCollation; +using DomainPrintDuplex = Wino.Core.Domain.Enums.PrintDuplex; +using DomainPrintMediaSize = Wino.Core.Domain.Enums.PrintMediaSize; +using DomainPrintOrientation = Wino.Core.Domain.Enums.PrintOrientation; namespace Wino.Mail.WinUI.Services; /// -/// Printer service that uses WinRT APIs to print PDF files. -/// Used modified version of the code here: -/// https://github.com/microsoft/Win2D-Samples/blob/reunion_master/ExampleGallery/PrintingExample.xaml.cs -/// HTML file is saved as PDF to temporary location. -/// Then PDF is loaded as PdfDocument and printed using CanvasBitmap for each page. +/// Printer service that uses the WinRT print preview UI with a WebView2-backed PDF render callback. /// - public class PrintService : IPrintService { + private const float PdfRenderDpi = 300f; + private const float DefaultDpi = 96f; + private TaskCompletionSource? _taskCompletionSource; - private CanvasPrintDocument? printDocument; - private PrintTask? printTask; - private PdfDocument? pdfDocument; + private CanvasPrintDocument? _printDocument; + private PrintTask? _printTask; + private PrintTaskOptionDetails? _printTaskOptionDetails; + private PrintManager? _printManager; + private PdfDocument? _pdfDocument; + private Func>? _renderPdfStreamAsync; + private WebView2PrintSettingsModel _currentRenderSettings = new(); + private string _printTitle = string.Empty; - private List bitmaps = new(); - private Vector2 largestBitmap; - private Vector2 pageSize; - private Vector2 imagePadding = new Vector2(64, 64); - private Vector2 cellSize; + private readonly List _bitmaps = new(); + private readonly List _pageIndexesToPrint = new(); + private Vector2 _pageSize; + private Windows.Foundation.Rect _imageableRect; + private int _pagesPerSheet = 1; + private int _columns = 1; + private int _rows = 1; + private int _sheetCount; - private int bitmapCount; - private int columns; - private int rows; - private int bitmapsPerPage; - private int pageCount = -1; - - private PrintInformation? _currentPrintInformation; - - public async Task PrintPdfFileAsync(string pdfFilePath, string printTitle) + public async Task PrintAsync(nint windowHandle, string printTitle, Func> renderPdfStreamAsync) { + if (windowHandle == IntPtr.Zero) + return PrintingResult.Failed; + if (_taskCompletionSource != null) { _taskCompletionSource.TrySetResult(PrintingResult.Abandoned); - _taskCompletionSource = new TaskCompletionSource(); + CleanupPrintSession(); } - // Load the PDF file - var file = await Windows.Storage.StorageFile.GetFileFromPathAsync(pdfFilePath); - pdfDocument = await PdfDocument.LoadFromFileAsync(file); + _taskCompletionSource = new TaskCompletionSource(); + _renderPdfStreamAsync = renderPdfStreamAsync ?? throw new ArgumentNullException(nameof(renderPdfStreamAsync)); + _printTitle = printTitle ?? throw new ArgumentNullException(nameof(printTitle)); + _currentRenderSettings = new WebView2PrintSettingsModel(); - _taskCompletionSource ??= new TaskCompletionSource(); + _printDocument = new CanvasPrintDocument(); + _printDocument.PrintTaskOptionsChanged += OnDocumentTaskOptionsChanged; + _printDocument.Preview += OnDocumentPreview; + _printDocument.Print += OnDocumentPrint; - _currentPrintInformation = new PrintInformation(pdfFilePath, printTitle); - - printDocument = new CanvasPrintDocument(); - printDocument.PrintTaskOptionsChanged += OnDocumentTaskOptionsChanged; - printDocument.Preview += OnDocumentPreview; - printDocument.Print += OnDocumentPrint; - - var printManager = PrintManager.GetForCurrentView(); - printManager.PrintTaskRequested += PrintingExample_PrintTaskRequested; + _printManager = PrintManagerInterop.GetForWindow(windowHandle); + _printManager.PrintTaskRequested += OnPrintTaskRequested; try { - await PrintManager.ShowPrintUIAsync(); - - var result = await _taskCompletionSource.Task; - - return result; + await ReloadPdfDocumentAsync(_currentRenderSettings); + await PrintManagerInterop.ShowPrintUIForWindowAsync(windowHandle); + return await _taskCompletionSource.Task; } finally { - // Dispose everything. - UnregisterPrintManager(printManager); - ClearBitmaps(); - UnregisterTask(); - DisposePDFDocument(); - - _taskCompletionSource = null; + CleanupPrintSession(); } } - private void DisposePDFDocument() + private void CleanupPrintSession() { - if (pdfDocument != null) + var printManager = _printManager; + _printManager = null; + if (printManager != null) { - pdfDocument = null; + try + { + printManager.PrintTaskRequested -= OnPrintTaskRequested; + } + catch (ObjectDisposedException) + { + } } - } - private void UnregisterTask() - { + var printTaskOptionDetails = _printTaskOptionDetails; + _printTaskOptionDetails = null; + if (printTaskOptionDetails != null) + { + try + { + printTaskOptionDetails.OptionChanged -= OnPrintTaskOptionChanged; + } + catch (ObjectDisposedException) + { + } + } + + var printTask = _printTask; + _printTask = null; if (printTask != null) { - printTask.Completed -= TaskCompleted; - printTask = null; + try + { + printTask.Completed -= OnPrintTaskCompleted; + } + catch (ObjectDisposedException) + { + } } - } - private void UnregisterPrintManager(PrintManager manager) - { - manager.PrintTaskRequested -= PrintingExample_PrintTaskRequested; + var printDocument = _printDocument; + _printDocument = null; + if (printDocument != null) + { + try + { + printDocument.PrintTaskOptionsChanged -= OnDocumentTaskOptionsChanged; + } + catch (ObjectDisposedException) + { + } + + try + { + printDocument.Preview -= OnDocumentPreview; + } + catch (ObjectDisposedException) + { + } + + try + { + printDocument.Print -= OnDocumentPrint; + } + catch (ObjectDisposedException) + { + } + } + + _pdfDocument = null; + ClearBitmaps(); + _pageIndexesToPrint.Clear(); + _taskCompletionSource = null; + _renderPdfStreamAsync = null; + _printTitle = string.Empty; } private void ClearBitmaps() { - foreach (var bitmap in bitmaps) + foreach (var bitmap in _bitmaps) { bitmap.Dispose(); } - bitmaps.Clear(); + _bitmaps.Clear(); } - private void PrintingExample_PrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args) + private void OnPrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args) { - if (_currentPrintInformation == null) return; - - printTask = args.Request.CreatePrintTask(_currentPrintInformation.PDFTitle, (createPrintTaskArgs) => + _printTask = args.Request.CreatePrintTask(_printTitle, createPrintTaskArgs => { - if (printDocument == null) + if (_printDocument == null) return; - createPrintTaskArgs.SetSource(printDocument); + createPrintTaskArgs.SetSource(_printDocument); }); - printTask.Completed += TaskCompleted; + _printTask.Completed += OnPrintTaskCompleted; + + _printTaskOptionDetails = PrintTaskOptionDetails.GetFromPrintTaskOptions(_printTask.Options); + _printTaskOptionDetails.DisplayedOptions.Clear(); + TryAddDisplayedOption(StandardPrintTaskOptions.Copies); + TryAddDisplayedOption(StandardPrintTaskOptions.Orientation); + TryAddDisplayedOption(StandardPrintTaskOptions.MediaSize); + TryAddDisplayedOption(StandardPrintTaskOptions.Collation); + TryAddDisplayedOption(StandardPrintTaskOptions.Duplex); + TryAddDisplayedOption(StandardPrintTaskOptions.CustomPageRanges); + TryAddDisplayedOption(StandardPrintTaskOptions.NUp); + _printTaskOptionDetails.OptionChanged += OnPrintTaskOptionChanged; } - private void TaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args) - => _taskCompletionSource?.TrySetResult((PrintingResult)args.Completion); + private void OnPrintTaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args) + => _taskCompletionSource?.TrySetResult(args.Completion switch + { + PrintTaskCompletion.Submitted => PrintingResult.Submitted, + PrintTaskCompletion.Canceled => PrintingResult.Canceled, + PrintTaskCompletion.Failed => PrintingResult.Failed, + _ => PrintingResult.Abandoned + }); + + private void OnPrintTaskOptionChanged(PrintTaskOptionDetails sender, PrintTaskOptionChangedEventArgs args) + => _printDocument?.InvalidatePreview(); private async void OnDocumentTaskOptionsChanged(CanvasPrintDocument sender, CanvasPrintTaskOptionsChangedEventArgs args) { @@ -143,47 +214,19 @@ public class PrintService : IPrintService try { - await LoadPDFPageBitmapsAsync(sender); + var newSettings = CreateRenderSettings(args.PrintTaskOptions); - var pageDesc = args.PrintTaskOptions.GetPageDescription(1); - var newPageSize = pageDesc.PageSize.ToVector2(); - - if (pageSize == newPageSize && pageCount != -1) + if (ShouldReloadPdf(newSettings)) { - // We've already figured out the pages and the page size hasn't changed, so there's nothing left for us to do here. - return; + await ReloadPdfDocumentAsync(newSettings); + } + else + { + _currentRenderSettings = newSettings; } - pageSize = newPageSize; + UpdatePreviewLayout(args.PrintTaskOptions); sender.InvalidatePreview(); - - // Figure out the bitmap index at the top of the current preview page. We'll request that the preview defaults to showing - // the page that still has this bitmap on it in the new layout. - int indexOnCurrentPage = 0; - if (pageCount != -1) - { - indexOnCurrentPage = (int)(args.CurrentPreviewPageNumber - 1) * bitmapsPerPage; - } - - // Calculate the new layout - var printablePageSize = pageSize * 0.9f; - - cellSize = largestBitmap + imagePadding; - - var cellsPerPage = printablePageSize / cellSize; - - columns = Math.Max(1, (int)Math.Floor(cellsPerPage.X)); - rows = Math.Max(1, (int)Math.Floor(cellsPerPage.Y)); - - bitmapsPerPage = columns * rows; - - // Calculate the page count - bitmapCount = bitmaps.Count; - pageCount = (int)Math.Ceiling(bitmapCount / (double)bitmapsPerPage); - sender.SetPageCount((uint)pageCount); - - // Set the preview page to the one that has the item that was currently displayed in the last preview - args.NewPreviewPageNumber = (uint)(indexOnCurrentPage / bitmapsPerPage) + 1; } finally { @@ -191,85 +234,380 @@ public class PrintService : IPrintService } } - - private async Task LoadPDFPageBitmapsAsync(CanvasPrintDocument sender) + private async Task ReloadPdfDocumentAsync(WebView2PrintSettingsModel settings) { - if (pdfDocument == null) - return; + if (_renderPdfStreamAsync == null) + throw new InvalidOperationException("No PDF render callback is registered."); + + await using var pdfStream = await _renderPdfStreamAsync(settings); + var randomAccessStream = pdfStream.AsRandomAccessStream(); + + _pdfDocument = await PdfDocument.LoadFromStreamAsync(randomAccessStream); + _currentRenderSettings = settings; ClearBitmaps(); - bitmaps ??= new List(); + if (_printDocument == null || _pdfDocument == null) + return; - for (int i = 0; i < pdfDocument.PageCount; i++) + for (var i = 0; i < _pdfDocument.PageCount; i++) { - var page = pdfDocument.GetPage((uint)i); - var stream = new InMemoryRandomAccessStream(); - await page.RenderToStreamAsync(stream); - var bitmap = await CanvasBitmap.LoadAsync(sender, stream); - bitmaps.Add(bitmap); - } - - largestBitmap = Vector2.Zero; - - foreach (var bitmap in bitmaps) - { - largestBitmap.X = Math.Max(largestBitmap.X, (float)bitmap.Size.Width); - largestBitmap.Y = Math.Max(largestBitmap.Y, (float)bitmap.Size.Height); + using var page = _pdfDocument.GetPage((uint)i); + using var stream = new Windows.Storage.Streams.InMemoryRandomAccessStream(); + var renderOptions = CreateRenderOptions(page); + await page.RenderToStreamAsync(stream, renderOptions); + stream.Seek(0); + var bitmap = await CanvasBitmap.LoadAsync(_printDocument, stream); + _bitmaps.Add(bitmap); } } + private static PdfPageRenderOptions CreateRenderOptions(PdfPage page) + { + var scale = PdfRenderDpi / DefaultDpi; + var destinationWidth = Math.Max(1u, (uint)Math.Ceiling(page.Size.Width * scale)); + var destinationHeight = Math.Max(1u, (uint)Math.Ceiling(page.Size.Height * scale)); + + return new PdfPageRenderOptions + { + DestinationWidth = destinationWidth, + DestinationHeight = destinationHeight + }; + } + + private void UpdatePreviewLayout(PrintTaskOptions printTaskOptions) + { + if (_pdfDocument == null) + { + _sheetCount = 0; + return; + } + + var pageDescription = printTaskOptions.GetPageDescription(1); + _pageSize = pageDescription.PageSize.ToVector2(); + _imageableRect = pageDescription.ImageableRect; + + _pagesPerSheet = GetPagesPerSheet(); + (_columns, _rows) = GetGrid(_pagesPerSheet); + + _pageIndexesToPrint.Clear(); + _pageIndexesToPrint.AddRange(GetPageIndexesToPrint((int)_pdfDocument.PageCount)); + _sheetCount = Math.Max(1, (int)Math.Ceiling(_pageIndexesToPrint.Count / (double)_pagesPerSheet)); + + _printDocument?.SetPageCount((uint)_sheetCount); + } private void OnDocumentPreview(CanvasPrintDocument sender, CanvasPreviewEventArgs args) - { - var ds = args.DrawingSession; - var pageNumber = args.PageNumber; - - DrawPdfPage(sender, ds, pageNumber); - } + => DrawSheet(args.DrawingSession, args.PageNumber); private void OnDocumentPrint(CanvasPrintDocument sender, CanvasPrintEventArgs args) { - var detailedOptions = PrintTaskOptionDetails.GetFromPrintTaskOptions(args.PrintTaskOptions); - - if (pdfDocument == null) + if (_pdfDocument == null || _sheetCount == 0) return; - int pageCountToPrint = (int)pdfDocument.PageCount; - - for (uint i = 1; i <= pageCountToPrint; ++i) + for (uint i = 1; i <= _sheetCount; i++) { - using var ds = args.CreateDrawingSession(); - var imageableRect = args.PrintTaskOptions.GetPageDescription(i).ImageableRect; - - DrawPdfPage(sender, ds, i); + using var drawingSession = args.CreateDrawingSession(); + DrawSheet(drawingSession, i); } } - private void DrawPdfPage(CanvasPrintDocument sender, CanvasDrawingSession ds, uint pageNumber) + private void DrawSheet(CanvasDrawingSession drawingSession, uint pageNumber) { - if (bitmaps.Count == 0) return; + if (_bitmaps.Count == 0 || _pageIndexesToPrint.Count == 0) + return; - var cellAcross = new Vector2(cellSize.X, 0); - var cellDown = new Vector2(0, cellSize.Y); + var printableSize = new Vector2((float)_imageableRect.Width, (float)_imageableRect.Height); + var cellWidth = printableSize.X / _columns; + var cellHeight = printableSize.Y / _rows; + var topLeft = new Vector2((float)_imageableRect.X, (float)_imageableRect.Y); + var pageIndex = ((int)pageNumber - 1) * _pagesPerSheet; - var totalSize = cellAcross * columns + cellDown * rows; - Vector2 topLeft = (pageSize - totalSize) / 2; - - int bitmapIndex = ((int)pageNumber - 1) * bitmapsPerPage; - - for (int row = 0; row < rows; ++row) + for (var row = 0; row < _rows; row++) { - for (int column = 0; column < columns; ++column) + for (var column = 0; column < _columns; column++) { - var cellTopLeft = topLeft + cellAcross * column + cellDown * row; - var bitmapInfo = bitmaps[bitmapIndex % bitmaps.Count]; - var bitmapPos = cellTopLeft + (cellSize - bitmapInfo.Size.ToVector2()) / 2; + if (pageIndex >= _pageIndexesToPrint.Count) + return; - ds.DrawImage(bitmapInfo, bitmapPos); - - bitmapIndex++; + var bitmap = _bitmaps[_pageIndexesToPrint[pageIndex]]; + var cellTopLeft = topLeft + new Vector2(cellWidth * column, cellHeight * row); + DrawBitmapInCell(drawingSession, bitmap, cellTopLeft, new Vector2(cellWidth, cellHeight)); + pageIndex++; } } } + + private static void DrawBitmapInCell(CanvasDrawingSession drawingSession, CanvasBitmap bitmap, Vector2 cellTopLeft, Vector2 cellSize) + { + var bitmapSize = bitmap.Size.ToVector2(); + var scale = Math.Min(cellSize.X / bitmapSize.X, cellSize.Y / bitmapSize.Y); + var targetSize = bitmapSize * scale; + var targetOffset = cellTopLeft + (cellSize - targetSize) / 2; + drawingSession.DrawImage(bitmap, new Windows.Foundation.Rect(targetOffset.X, targetOffset.Y, targetSize.X, targetSize.Y)); + } + + private WebView2PrintSettingsModel CreateRenderSettings(PrintTaskOptions printTaskOptions) + => new() + { + Orientation = GetOrientation(printTaskOptions), + MediaSize = GetMediaSize(), + PageRanges = GetPageRanges(), + MarginTop = _currentRenderSettings.MarginTop, + MarginBottom = _currentRenderSettings.MarginBottom, + MarginLeft = _currentRenderSettings.MarginLeft, + MarginRight = _currentRenderSettings.MarginRight, + ShouldPrintBackgrounds = _currentRenderSettings.ShouldPrintBackgrounds, + ShouldPrintSelectionOnly = _currentRenderSettings.ShouldPrintSelectionOnly, + ShouldPrintHeaderAndFooter = _currentRenderSettings.ShouldPrintHeaderAndFooter, + HeaderTitle = _currentRenderSettings.HeaderTitle, + FooterUri = _currentRenderSettings.FooterUri, + ScaleFactor = _currentRenderSettings.ScaleFactor + }; + + private bool ShouldReloadPdf(WebView2PrintSettingsModel newSettings) + => newSettings.Orientation != _currentRenderSettings.Orientation + || newSettings.MediaSize != _currentRenderSettings.MediaSize + || newSettings.ShouldPrintBackgrounds != _currentRenderSettings.ShouldPrintBackgrounds + || newSettings.ShouldPrintHeaderAndFooter != _currentRenderSettings.ShouldPrintHeaderAndFooter + || !string.Equals(newSettings.HeaderTitle, _currentRenderSettings.HeaderTitle, StringComparison.Ordinal) + || !string.Equals(newSettings.FooterUri, _currentRenderSettings.FooterUri, StringComparison.Ordinal); + + private int GetCopies() + => GetOptionValue(StandardPrintTaskOptions.Copies) as int? ?? 1; + + private DomainPrintOrientation GetOrientation(PrintTaskOptions printTaskOptions) + { + var optionValue = GetOptionValue(StandardPrintTaskOptions.Orientation); + if (optionValue is DomainPrintOrientation orientation) + return orientation; + + if (optionValue is string orientationString + && Enum.TryParse(orientationString, true, out var parsedOrientation)) + { + return parsedOrientation; + } + + var pageDescription = printTaskOptions.GetPageDescription(1); + return pageDescription.PageSize.Width >= pageDescription.PageSize.Height + ? DomainPrintOrientation.Landscape + : DomainPrintOrientation.Portrait; + } + + private DomainPrintMediaSize GetMediaSize() + { + var optionValue = GetOptionValue(StandardPrintTaskOptions.MediaSize); + if (optionValue is DomainPrintMediaSize mediaSize) + return mediaSize; + + if (optionValue is string mediaSizeString + && Enum.TryParse(mediaSizeString, true, out var parsedMediaSize)) + { + return parsedMediaSize; + } + + return DomainPrintMediaSize.Default; + } + + private DomainPrintCollation GetCollation() + { + var optionValue = GetOptionValue(StandardPrintTaskOptions.Collation); + if (optionValue is DomainPrintCollation collation) + return collation; + + if (optionValue is string collationString + && Enum.TryParse(collationString, true, out var parsedCollation)) + { + return parsedCollation; + } + + return DomainPrintCollation.Default; + } + + private DomainPrintDuplex GetDuplex() + { + var optionValue = GetOptionValue(StandardPrintTaskOptions.Duplex); + if (optionValue is DomainPrintDuplex duplex) + return duplex; + + if (optionValue is string duplexString) + { + return duplexString switch + { + nameof(DomainPrintDuplex.Simplex) => DomainPrintDuplex.Simplex, + nameof(DomainPrintDuplex.DuplexShortEdge) => DomainPrintDuplex.DuplexShortEdge, + nameof(DomainPrintDuplex.DuplexLongEdge) => DomainPrintDuplex.DuplexLongEdge, + _ => DomainPrintDuplex.Default + }; + } + + return DomainPrintDuplex.Default; + } + + private int GetPagesPerSheet() + { + var optionValue = GetOptionValue(StandardPrintTaskOptions.NUp); + if (optionValue is int pagesPerSheet) + return NormalizePagesPerSheet(pagesPerSheet); + + if (optionValue is string valueString) + { + if (int.TryParse(valueString, out var parsedPagesPerSheet)) + return NormalizePagesPerSheet(parsedPagesPerSheet); + + return valueString switch + { + "TwoUp" => 2, + "FourUp" => 4, + "SixUp" => 6, + "NineUp" => 9, + "SixteenUp" => 16, + _ => 1 + }; + } + + return 1; + } + + private string GetPageRanges() + { + var optionValue = GetOptionValue(StandardPrintTaskOptions.CustomPageRanges); + return optionValue?.ToString() ?? string.Empty; + } + + private object? GetOptionValue(string optionId) + { + if (_printTaskOptionDetails == null) + return null; + + if (!TryGetOption(optionId, out var option)) + return null; + + try + { + return option switch + { + PrintCopiesOptionDetails copies => copies.Value, + PrintPageRangeOptionDetails pageRanges => pageRanges.Value, + IPrintItemListOptionDetails itemList => itemList.Value, + IPrintNumberOptionDetails number => number.Value, + _ => null + }; + } + catch (COMException) + { + return null; + } + } + + private void TryAddDisplayedOption(string optionId) + { + if (_printTaskOptionDetails == null) + return; + + if (TryGetOption(optionId, out _)) + { + _printTaskOptionDetails.DisplayedOptions.Add(optionId); + } + } + + private bool TryGetOption(string optionId, out IPrintOptionDetails? option) + { + option = null; + + if (_printTaskOptionDetails == null) + return false; + + try + { + if (_printTaskOptionDetails.Options.TryGetValue(optionId, out option)) + return option != null; + } + catch (COMException) + { + return false; + } + catch (KeyNotFoundException) + { + return false; + } + + return false; + } + + private IEnumerable GetPageIndexesToPrint(int totalPageCount) + { + if (string.IsNullOrWhiteSpace(_currentRenderSettings.PageRanges)) + return GetAllPages(totalPageCount); + + var pageIndexes = new List(); + var tokens = _currentRenderSettings.PageRanges.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (var token in tokens) + { + if (token.Contains('-')) + { + var bounds = token.Split('-', StringSplitOptions.TrimEntries); + if (bounds.Length != 2 + || !int.TryParse(bounds[0], out var start) + || !int.TryParse(bounds[1], out var end)) + { + continue; + } + + if (end < start) + { + (start, end) = (end, start); + } + + for (var i = start; i <= end; i++) + { + AddPageIndex(pageIndexes, i, totalPageCount); + } + } + else if (int.TryParse(token, out var pageNumber)) + { + AddPageIndex(pageIndexes, pageNumber, totalPageCount); + } + } + + return pageIndexes.Count > 0 + ? pageIndexes + : GetAllPages(totalPageCount); + } + + private static IEnumerable GetAllPages(int totalPageCount) + { + for (var i = 0; i < totalPageCount; i++) + { + yield return i; + } + } + + private static void AddPageIndex(ICollection pageIndexes, int pageNumber, int totalPageCount) + { + var zeroBasedIndex = pageNumber - 1; + if (zeroBasedIndex >= 0 && zeroBasedIndex < totalPageCount) + { + pageIndexes.Add(zeroBasedIndex); + } + } + + private static int NormalizePagesPerSheet(int pagesPerSheet) + => pagesPerSheet switch + { + 2 or 4 or 6 or 9 or 16 => pagesPerSheet, + _ => 1 + }; + + private static (int Columns, int Rows) GetGrid(int pagesPerSheet) + => pagesPerSheet switch + { + 2 => (1, 2), + 4 => (2, 2), + 6 => (2, 3), + 9 => (3, 3), + 16 => (4, 4), + _ => (1, 1) + }; } diff --git a/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml.cs b/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml.cs index 6db2e604..31b7fcdb 100644 --- a/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Runtime.InteropServices.WindowsRuntime; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -58,7 +59,7 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, WebViewExtensions.EnsureWebView2Environment(); - ViewModel.DirectPrintFuncAsync = DirectPrintAsync; + ViewModel.RenderPdfStreamFuncAsync = RenderPdfStreamAsync; ViewModel.SaveHTMLasPDFFunc = new Func>((path) => { @@ -92,25 +93,14 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, RendererCommandBar.InvalidateCommands(); } - private async Task DirectPrintAsync(WebView2PrintSettingsModel settings) + private async Task RenderPdfStreamAsync(WebView2PrintSettingsModel settings) { - if (Chromium.CoreWebView2 == null) return PrintingResult.Failed; + if (Chromium.CoreWebView2 == null) + throw new InvalidOperationException("WebView2 is not initialized for printing."); - try - { - var nativeSettings = settings.ToCoreWebView2PrintSettings(Chromium.CoreWebView2.Environment); - var res = await Chromium.CoreWebView2.PrintAsync(nativeSettings); - - return res switch - { - CoreWebView2PrintStatus.Succeeded => PrintingResult.Submitted, - _ => PrintingResult.Failed, - }; - } - catch (Exception) - { - return PrintingResult.Failed; - } + var nativeSettings = settings.ToCoreWebView2PdfRenderSettings(Chromium.CoreWebView2.Environment); + var pdfStream = await Chromium.CoreWebView2.PrintToPdfStreamAsync(nativeSettings); + return pdfStream.AsStreamForRead(); } public override async void OnEditorThemeChanged() @@ -182,7 +172,7 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract, // Make sure the WebView2 is disposed properly. ViewModel.SaveHTMLasPDFFunc = null; - ViewModel.DirectPrintFuncAsync = null; + ViewModel.RenderPdfStreamFuncAsync = null; ViewModel.RenderHtmlAsyncFunc = null; ViewModel.ClearRenderedHtmlAsyncFunc = null; _currentRenderedHtml = string.Empty;