Migrate mail printing to WinUI print preview

This commit is contained in:
Burak Kaan Köse
2026-04-11 15:07:22 +02:00
parent 24626d1c31
commit e206368801
12 changed files with 565 additions and 828 deletions
@@ -1,10 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Common; using Wino.Core.Domain.Models.Common;
using Wino.Core.Domain.Models.Printing;
namespace Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Interfaces;
@@ -30,5 +29,4 @@ public interface IDialogServiceBase
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters); Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
Task<List<PickedFileMetadata>> PickFilesMetadataAsync(params object[] typeFilters); Task<List<PickedFileMetadata>> PickFilesMetadataAsync(params object[] typeFilters);
Task<string> PickFilePathAsync(string saveFileName); Task<string> PickFilePathAsync(string saveFileName);
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
} }
+5 -2
View File
@@ -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.Enums;
using Wino.Core.Domain.Models.Printing;
namespace Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Interfaces;
public interface IPrintService public interface IPrintService
{ {
Task<PrintingResult> PrintPdfFileAsync(string pdfFilePath, string printTitle); Task<PrintingResult> PrintAsync(nint windowHandle, string printTitle, Func<WebView2PrintSettingsModel, Task<Stream>> renderPdfStreamAsync);
} }
@@ -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; }
}
@@ -58,9 +58,9 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
private MimeMessageInformation initializedMimeMessageInformation = null; private MimeMessageInformation initializedMimeMessageInformation = null;
// Func to get WebView2 to save current HTML as PDF to given location. // 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<string, Task<bool>> SaveHTMLasPDFFunc { get; set; } public Func<string, Task<bool>> SaveHTMLasPDFFunc { get; set; }
public Func<WebView2PrintSettingsModel, Task<PrintingResult>> DirectPrintFuncAsync { get; set; } public Func<WebView2PrintSettingsModel, Task<Stream>> RenderPdfStreamFuncAsync { get; set; }
public Func<string, Task> RenderHtmlAsyncFunc { get; set; } public Func<string, Task> RenderHtmlAsyncFunc { get; set; }
public Func<Task> ClearRenderedHtmlAsyncFunc { get; set; } public Func<Task> ClearRenderedHtmlAsyncFunc { get; set; }
@@ -275,17 +275,17 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
} }
else if (operation == MailOperation.Print) else if (operation == MailOperation.Print)
{ {
var settings = await _dialogService.ShowPrintDialogAsync(); var printingResult = await PrintAsync();
if (settings == null) return;
var printingResult = await DirectPrintFuncAsync.Invoke(settings);
// TODO: More detailed printing result handling. // TODO: More detailed printing result handling.
if (printingResult == PrintingResult.Submitted) if (printingResult == PrintingResult.Submitted)
{ {
_dialogService.InfoBarMessage(Translator.DialogMessage_PrintingSuccessTitle, Translator.DialogMessage_PrintingSuccessMessage, InfoBarMessageType.Success); _dialogService.InfoBarMessage(Translator.DialogMessage_PrintingSuccessTitle, Translator.DialogMessage_PrintingSuccessMessage, InfoBarMessageType.Success);
} }
else if (printingResult == PrintingResult.Canceled)
{
return;
}
else if (printingResult == PrintingResult.Failed) else if (printingResult == PrintingResult.Failed)
{ {
_dialogService.InfoBarMessage(Translator.DialogMessage_PrintingFailedTitle, Translator.DialogMessage_PrintingFailedMessage, InfoBarMessageType.Error); _dialogService.InfoBarMessage(Translator.DialogMessage_PrintingFailedTitle, Translator.DialogMessage_PrintingFailedMessage, InfoBarMessageType.Error);
@@ -784,6 +784,22 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
} }
} }
private async Task<PrintingResult> 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. // Returns created file path.
private async Task<string> SaveAttachmentInternalAsync(MailAttachmentViewModel attachmentViewModel, string saveFolderPath) private async Task<string> SaveAttachmentInternalAsync(MailAttachmentViewModel attachmentViewModel, string saveFolderPath)
{ {
-66
View File
@@ -1,66 +0,0 @@
<ContentDialog
x:Class="Wino.Mail.WinUI.Dialogs.PrintDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:printing="using:Wino.Core.Domain.Models.Printing"
Title="Print Settings"
MinWidth="400"
MinHeight="300"
DefaultButton="Primary"
Loaded="PrintDialog_Loaded"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
PrimaryButtonText="Print"
SecondaryButtonClick="ContentDialog_SecondaryButtonClick"
SecondaryButtonText="Cancel"
Style="{StaticResource WinoDialogStyle}"
mc:Ignorable="d">
<StackPanel Spacing="20">
<!-- Printer Selection -->
<ComboBox
x:Name="PrinterComboBox"
HorizontalAlignment="Stretch"
Header="Printer"
SelectedItem="{x:Bind PrintSettings.PrinterName, Mode=TwoWay}"
SelectionChanged="PrinterComboBox_SelectionChanged" />
<!-- Copies -->
<NumberBox
Header="Copies"
Maximum="999"
Minimum="1"
SpinButtonPlacementMode="Inline"
Value="{x:Bind PrintSettings.Copies, Mode=TwoWay}" />
<!-- Orientation -->
<RadioButtons
x:Name="OrientationRadioButtons"
Header="Orientation"
SelectionChanged="OrientationRadio_SelectionChanged">
<RadioButton Content="Portrait" />
<RadioButton Content="Landscape" />
</RadioButtons>
<!-- Print Options -->
<StackPanel Spacing="8">
<TextBlock
Margin="0,0,0,8"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="Options" />
<Border
Padding="16,12"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="6">
<StackPanel Spacing="8">
<CheckBox Content="Print backgrounds" IsChecked="{x:Bind PrintSettings.ShouldPrintBackgrounds, Mode=TwoWay}" />
<CheckBox Content="Print headers and footers" IsChecked="{x:Bind PrintSettings.ShouldPrintHeaderAndFooter, Mode=TwoWay}" />
</StackPanel>
</Border>
</StackPanel>
</StackPanel>
</ContentDialog>
-170
View File
@@ -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;
/// <summary>
/// Custom print dialog for configuring WebView2 print settings.
/// </summary>
public sealed partial class PrintDialog : ContentDialog
{
public WebView2PrintSettingsModel PrintSettings { get; set; } = new WebView2PrintSettingsModel();
public PrintDialog()
{
this.InitializeComponent();
}
/// <summary>
/// Initializes the dialog with existing print settings.
/// </summary>
/// <param name="printSettings">The initial print settings to load.</param>
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();
}
}
/// <summary>
/// Sets the list of available printers for the dialog.
/// </summary>
/// <param name="printers">List of available printer names.</param>
public void SetAvailablePrinters(IEnumerable<string> printers)
{
var printerList = printers?.ToList() ?? new List<string>();
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;
}
}
}
/// <summary>
/// Loads available printers asynchronously and sets them in the dialog.
/// </summary>
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<string>());
}
}
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();
}
}
/// <summary>
/// Validates the current print settings before closing the dialog.
/// </summary>
/// <returns>True if settings are valid, false otherwise.</returns>
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
}
}
@@ -178,6 +178,33 @@ public static class PrintSettingsExtensions
return settings; return settings;
} }
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Updates a WebView2PrintSettingsModel from a CoreWebView2PrintSettings object. /// Updates a WebView2PrintSettingsModel from a CoreWebView2PrintSettings object.
/// </summary> /// </summary>
@@ -209,4 +236,4 @@ public static class PrintSettingsExtensions
model.PagesPerSide = settings.PagesPerSide; model.PagesPerSide = settings.PagesPerSide;
model.PageRanges = settings.PageRanges ?? string.Empty; model.PageRanges = settings.PageRanges ?? string.Empty;
} }
} }
@@ -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<string> 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<string>(printerCount);
var structSize = Marshal.SizeOf<PrinterInfo4>();
for (var i = 0; i < printerCount; i++)
{
var current = IntPtr.Add(printerBuffer, i * structSize);
var info = Marshal.PtrToStructure<PrinterInfo4>(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);
}
}
@@ -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;
/// <summary>
/// ViewModel for the PrintDialog that handles data binding and state management.
/// </summary>
public class PrintDialogViewModel : INotifyPropertyChanged
{
private List<string> _availablePrinters = new();
private bool _isCustomPageRange = false;
private WebView2PrintSettingsModel _printSettings = new();
public event PropertyChangedEventHandler? PropertyChanged;
public PrintDialogViewModel()
{
// Initialize default values
PrintSettings.PropertyChanged += OnPrintSettingsChanged;
}
/// <summary>
/// The print settings model that will be configured by the dialog.
/// </summary>
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();
}
}
}
/// <summary>
/// List of available printers.
/// </summary>
public List<string> AvailablePrinters
{
get => _availablePrinters;
set
{
if (_availablePrinters != value)
{
_availablePrinters = value ?? new List<string>();
OnPropertyChanged(nameof(AvailablePrinters));
}
}
}
/// <summary>
/// Index for the orientation radio buttons.
/// </summary>
public int OrientationIndex
{
get => (int)PrintSettings.Orientation;
set
{
if (value >= 0 && value <= 1)
{
PrintSettings.Orientation = (PrintOrientation)value;
OnPropertyChanged(nameof(OrientationIndex));
}
}
}
/// <summary>
/// Index for the color mode radio buttons.
/// </summary>
public int ColorModeIndex
{
get => (int)PrintSettings.ColorMode;
set
{
if (value >= 0 && value <= 2)
{
PrintSettings.ColorMode = (PrintColorMode)value;
OnPropertyChanged(nameof(ColorModeIndex));
}
}
}
/// <summary>
/// Index for the collation radio buttons.
/// </summary>
public int CollationIndex
{
get => (int)PrintSettings.Collation;
set
{
if (value >= 0 && value <= 2)
{
PrintSettings.Collation = (PrintCollation)value;
OnPropertyChanged(nameof(CollationIndex));
}
}
}
/// <summary>
/// Index for the duplex radio buttons.
/// </summary>
public int DuplexIndex
{
get => (int)PrintSettings.Duplex;
set
{
if (value >= 0 && value <= 3)
{
PrintSettings.Duplex = (PrintDuplex)value;
OnPropertyChanged(nameof(DuplexIndex));
}
}
}
/// <summary>
/// Index for the media size combo box.
/// </summary>
public int MediaSizeIndex
{
get => (int)PrintSettings.MediaSize;
set
{
if (value >= 0 && value <= 9)
{
PrintSettings.MediaSize = (PrintMediaSize)value;
OnPropertyChanged(nameof(MediaSizeIndex));
}
}
}
/// <summary>
/// Index for the pages per side combo box.
/// </summary>
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));
}
}
}
/// <summary>
/// Index for the page range option (0 = All pages, 1 = Custom range).
/// </summary>
public int PageRangeOptionIndex
{
get => IsCustomPageRange ? 1 : 0;
set
{
IsCustomPageRange = value == 1;
if (!IsCustomPageRange)
{
PrintSettings.PageRanges = string.Empty;
}
OnPropertyChanged(nameof(PageRangeOptionIndex));
}
}
/// <summary>
/// Whether custom page range is selected.
/// </summary>
public bool IsCustomPageRange
{
get => _isCustomPageRange;
private set
{
if (_isCustomPageRange != value)
{
_isCustomPageRange = value;
OnPropertyChanged(nameof(IsCustomPageRange));
}
}
}
/// <summary>
/// Scale factor as percentage text for display.
/// </summary>
public string ScalePercentageText => $"{(int)(PrintSettings.ScaleFactor * 100)}%";
/// <summary>
/// Initializes the dialog with the provided print settings.
/// </summary>
/// <param name="printSettings">The initial print settings.</param>
public void Initialize(WebView2PrintSettingsModel printSettings = default!)
{
if (printSettings != null)
{
PrintSettings = printSettings;
}
else
{
PrintSettings = new WebView2PrintSettingsModel();
}
UpdateDerivedProperties();
}
/// <summary>
/// Sets the list of available printers.
/// </summary>
/// <param name="printers">List of printer names.</param>
public void SetAvailablePrinters(IEnumerable<string> printers)
{
AvailablePrinters = printers?.ToList() ?? new List<string>();
// 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));
}
}
@@ -15,7 +15,6 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Common; using Wino.Core.Domain.Models.Common;
using Wino.Core.Domain.Models.Printing;
using Wino.Dialogs; using Wino.Dialogs;
using Wino.Mail.WinUI.Dialogs; using Wino.Mail.WinUI.Dialogs;
using Wino.Mail.WinUI.Extensions; using Wino.Mail.WinUI.Extensions;
@@ -355,37 +354,4 @@ public class DialogServiceBase : IDialogServiceBase
return dialog.Result ?? null!; return dialog.Result ?? null!;
} }
public async Task<WebView2PrintSettingsModel> 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!;
}
}
} }
+499 -161
View File
@@ -1,141 +1,212 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Printing; using Microsoft.Graphics.Canvas.Printing;
using Windows.Data.Pdf; using Windows.Data.Pdf;
using Windows.Graphics.Printing; using Windows.Graphics.Printing;
using Windows.Graphics.Printing.OptionDetails; using Windows.Graphics.Printing.OptionDetails;
using Windows.Storage.Streams; using WinRT.Interop;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Printing; 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; namespace Wino.Mail.WinUI.Services;
/// <summary> /// <summary>
/// Printer service that uses WinRT APIs to print PDF files. /// Printer service that uses the WinRT print preview UI with a WebView2-backed PDF render callback.
/// 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.
/// </summary> /// </summary>
public class PrintService : IPrintService public class PrintService : IPrintService
{ {
private const float PdfRenderDpi = 300f;
private const float DefaultDpi = 96f;
private TaskCompletionSource<PrintingResult>? _taskCompletionSource; private TaskCompletionSource<PrintingResult>? _taskCompletionSource;
private CanvasPrintDocument? printDocument; private CanvasPrintDocument? _printDocument;
private PrintTask? printTask; private PrintTask? _printTask;
private PdfDocument? pdfDocument; private PrintTaskOptionDetails? _printTaskOptionDetails;
private PrintManager? _printManager;
private PdfDocument? _pdfDocument;
private Func<WebView2PrintSettingsModel, Task<Stream>>? _renderPdfStreamAsync;
private WebView2PrintSettingsModel _currentRenderSettings = new();
private string _printTitle = string.Empty;
private List<CanvasBitmap> bitmaps = new(); private readonly List<CanvasBitmap> _bitmaps = new();
private Vector2 largestBitmap; private readonly List<int> _pageIndexesToPrint = new();
private Vector2 pageSize; private Vector2 _pageSize;
private Vector2 imagePadding = new Vector2(64, 64); private Windows.Foundation.Rect _imageableRect;
private Vector2 cellSize; private int _pagesPerSheet = 1;
private int _columns = 1;
private int _rows = 1;
private int _sheetCount;
private int bitmapCount; public async Task<PrintingResult> PrintAsync(nint windowHandle, string printTitle, Func<WebView2PrintSettingsModel, Task<Stream>> renderPdfStreamAsync)
private int columns;
private int rows;
private int bitmapsPerPage;
private int pageCount = -1;
private PrintInformation? _currentPrintInformation;
public async Task<PrintingResult> PrintPdfFileAsync(string pdfFilePath, string printTitle)
{ {
if (windowHandle == IntPtr.Zero)
return PrintingResult.Failed;
if (_taskCompletionSource != null) if (_taskCompletionSource != null)
{ {
_taskCompletionSource.TrySetResult(PrintingResult.Abandoned); _taskCompletionSource.TrySetResult(PrintingResult.Abandoned);
_taskCompletionSource = new TaskCompletionSource<PrintingResult>(); CleanupPrintSession();
} }
// Load the PDF file _taskCompletionSource = new TaskCompletionSource<PrintingResult>();
var file = await Windows.Storage.StorageFile.GetFileFromPathAsync(pdfFilePath); _renderPdfStreamAsync = renderPdfStreamAsync ?? throw new ArgumentNullException(nameof(renderPdfStreamAsync));
pdfDocument = await PdfDocument.LoadFromFileAsync(file); _printTitle = printTitle ?? throw new ArgumentNullException(nameof(printTitle));
_currentRenderSettings = new WebView2PrintSettingsModel();
_taskCompletionSource ??= new TaskCompletionSource<PrintingResult>(); _printDocument = new CanvasPrintDocument();
_printDocument.PrintTaskOptionsChanged += OnDocumentTaskOptionsChanged;
_printDocument.Preview += OnDocumentPreview;
_printDocument.Print += OnDocumentPrint;
_currentPrintInformation = new PrintInformation(pdfFilePath, printTitle); _printManager = PrintManagerInterop.GetForWindow(windowHandle);
_printManager.PrintTaskRequested += OnPrintTaskRequested;
printDocument = new CanvasPrintDocument();
printDocument.PrintTaskOptionsChanged += OnDocumentTaskOptionsChanged;
printDocument.Preview += OnDocumentPreview;
printDocument.Print += OnDocumentPrint;
var printManager = PrintManager.GetForCurrentView();
printManager.PrintTaskRequested += PrintingExample_PrintTaskRequested;
try try
{ {
await PrintManager.ShowPrintUIAsync(); await ReloadPdfDocumentAsync(_currentRenderSettings);
await PrintManagerInterop.ShowPrintUIForWindowAsync(windowHandle);
var result = await _taskCompletionSource.Task; return await _taskCompletionSource.Task;
return result;
} }
finally finally
{ {
// Dispose everything. CleanupPrintSession();
UnregisterPrintManager(printManager);
ClearBitmaps();
UnregisterTask();
DisposePDFDocument();
_taskCompletionSource = null;
} }
} }
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) if (printTask != null)
{ {
printTask.Completed -= TaskCompleted; try
printTask = null; {
printTask.Completed -= OnPrintTaskCompleted;
}
catch (ObjectDisposedException)
{
}
} }
}
private void UnregisterPrintManager(PrintManager manager) var printDocument = _printDocument;
{ _printDocument = null;
manager.PrintTaskRequested -= PrintingExample_PrintTaskRequested; 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() private void ClearBitmaps()
{ {
foreach (var bitmap in bitmaps) foreach (var bitmap in _bitmaps)
{ {
bitmap.Dispose(); 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(_printTitle, createPrintTaskArgs =>
printTask = args.Request.CreatePrintTask(_currentPrintInformation.PDFTitle, (createPrintTaskArgs) =>
{ {
if (printDocument == null) if (_printDocument == null)
return; 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) private void OnPrintTaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args)
=> _taskCompletionSource?.TrySetResult((PrintingResult)args.Completion); => _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) private async void OnDocumentTaskOptionsChanged(CanvasPrintDocument sender, CanvasPrintTaskOptionsChangedEventArgs args)
{ {
@@ -143,47 +214,19 @@ public class PrintService : IPrintService
try try
{ {
await LoadPDFPageBitmapsAsync(sender); var newSettings = CreateRenderSettings(args.PrintTaskOptions);
var pageDesc = args.PrintTaskOptions.GetPageDescription(1); if (ShouldReloadPdf(newSettings))
var newPageSize = pageDesc.PageSize.ToVector2();
if (pageSize == newPageSize && pageCount != -1)
{ {
// We've already figured out the pages and the page size hasn't changed, so there's nothing left for us to do here. await ReloadPdfDocumentAsync(newSettings);
return; }
else
{
_currentRenderSettings = newSettings;
} }
pageSize = newPageSize; UpdatePreviewLayout(args.PrintTaskOptions);
sender.InvalidatePreview(); 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 finally
{ {
@@ -191,85 +234,380 @@ public class PrintService : IPrintService
} }
} }
private async Task ReloadPdfDocumentAsync(WebView2PrintSettingsModel settings)
private async Task LoadPDFPageBitmapsAsync(CanvasPrintDocument sender)
{ {
if (pdfDocument == null) if (_renderPdfStreamAsync == null)
return; 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(); ClearBitmaps();
bitmaps ??= new List<CanvasBitmap>(); 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); using var page = _pdfDocument.GetPage((uint)i);
var stream = new InMemoryRandomAccessStream(); using var stream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
await page.RenderToStreamAsync(stream); var renderOptions = CreateRenderOptions(page);
var bitmap = await CanvasBitmap.LoadAsync(sender, stream); await page.RenderToStreamAsync(stream, renderOptions);
bitmaps.Add(bitmap); stream.Seek(0);
} var bitmap = await CanvasBitmap.LoadAsync(_printDocument, 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);
} }
} }
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) private void OnDocumentPreview(CanvasPrintDocument sender, CanvasPreviewEventArgs args)
{ => DrawSheet(args.DrawingSession, args.PageNumber);
var ds = args.DrawingSession;
var pageNumber = args.PageNumber;
DrawPdfPage(sender, ds, pageNumber);
}
private void OnDocumentPrint(CanvasPrintDocument sender, CanvasPrintEventArgs args) private void OnDocumentPrint(CanvasPrintDocument sender, CanvasPrintEventArgs args)
{ {
var detailedOptions = PrintTaskOptionDetails.GetFromPrintTaskOptions(args.PrintTaskOptions); if (_pdfDocument == null || _sheetCount == 0)
if (pdfDocument == null)
return; return;
int pageCountToPrint = (int)pdfDocument.PageCount; for (uint i = 1; i <= _sheetCount; i++)
for (uint i = 1; i <= pageCountToPrint; ++i)
{ {
using var ds = args.CreateDrawingSession(); using var drawingSession = args.CreateDrawingSession();
var imageableRect = args.PrintTaskOptions.GetPageDescription(i).ImageableRect; DrawSheet(drawingSession, i);
DrawPdfPage(sender, ds, 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 printableSize = new Vector2((float)_imageableRect.Width, (float)_imageableRect.Height);
var cellDown = new Vector2(0, cellSize.Y); 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; for (var row = 0; row < _rows; row++)
Vector2 topLeft = (pageSize - totalSize) / 2;
int bitmapIndex = ((int)pageNumber - 1) * bitmapsPerPage;
for (int 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; if (pageIndex >= _pageIndexesToPrint.Count)
var bitmapInfo = bitmaps[bitmapIndex % bitmaps.Count]; return;
var bitmapPos = cellTopLeft + (cellSize - bitmapInfo.Size.ToVector2()) / 2;
ds.DrawImage(bitmapInfo, bitmapPos); var bitmap = _bitmaps[_pageIndexesToPrint[pageIndex]];
var cellTopLeft = topLeft + new Vector2(cellWidth * column, cellHeight * row);
bitmapIndex++; 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<DomainPrintOrientation>(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<DomainPrintMediaSize>(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<DomainPrintCollation>(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<int> GetPageIndexesToPrint(int totalPageCount)
{
if (string.IsNullOrWhiteSpace(_currentRenderSettings.PageRanges))
return GetAllPages(totalPageCount);
var pageIndexes = new List<int>();
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<int> GetAllPages(int totalPageCount)
{
for (var i = 0; i < totalPageCount; i++)
{
yield return i;
}
}
private static void AddPageIndex(ICollection<int> 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)
};
} }
@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -58,7 +59,7 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
WebViewExtensions.EnsureWebView2Environment(); WebViewExtensions.EnsureWebView2Environment();
ViewModel.DirectPrintFuncAsync = DirectPrintAsync; ViewModel.RenderPdfStreamFuncAsync = RenderPdfStreamAsync;
ViewModel.SaveHTMLasPDFFunc = new Func<string, Task<bool>>((path) => ViewModel.SaveHTMLasPDFFunc = new Func<string, Task<bool>>((path) =>
{ {
@@ -92,25 +93,14 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
RendererCommandBar.InvalidateCommands(); RendererCommandBar.InvalidateCommands();
} }
private async Task<PrintingResult> DirectPrintAsync(WebView2PrintSettingsModel settings) private async Task<Stream> 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.ToCoreWebView2PdfRenderSettings(Chromium.CoreWebView2.Environment);
{ var pdfStream = await Chromium.CoreWebView2.PrintToPdfStreamAsync(nativeSettings);
var nativeSettings = settings.ToCoreWebView2PrintSettings(Chromium.CoreWebView2.Environment); return pdfStream.AsStreamForRead();
var res = await Chromium.CoreWebView2.PrintAsync(nativeSettings);
return res switch
{
CoreWebView2PrintStatus.Succeeded => PrintingResult.Submitted,
_ => PrintingResult.Failed,
};
}
catch (Exception)
{
return PrintingResult.Failed;
}
} }
public override async void OnEditorThemeChanged() public override async void OnEditorThemeChanged()
@@ -182,7 +172,7 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
// Make sure the WebView2 is disposed properly. // Make sure the WebView2 is disposed properly.
ViewModel.SaveHTMLasPDFFunc = null; ViewModel.SaveHTMLasPDFFunc = null;
ViewModel.DirectPrintFuncAsync = null; ViewModel.RenderPdfStreamFuncAsync = null;
ViewModel.RenderHtmlAsyncFunc = null; ViewModel.RenderHtmlAsyncFunc = null;
ViewModel.ClearRenderedHtmlAsyncFunc = null; ViewModel.ClearRenderedHtmlAsyncFunc = null;
_currentRenderedHtml = string.Empty; _currentRenderedHtml = string.Empty;