Migrate mail printing to WinUI print preview
This commit is contained in:
@@ -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<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
||||
Task<List<PickedFileMetadata>> PickFilesMetadataAsync(params object[] typeFilters);
|
||||
Task<string> PickFilePathAsync(string saveFileName);
|
||||
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
||||
}
|
||||
|
||||
@@ -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<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;
|
||||
|
||||
// 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<WebView2PrintSettingsModel, Task<PrintingResult>> DirectPrintFuncAsync { get; set; }
|
||||
public Func<WebView2PrintSettingsModel, Task<Stream>> RenderPdfStreamFuncAsync { get; set; }
|
||||
public Func<string, Task> RenderHtmlAsyncFunc { get; set; }
|
||||
public Func<Task> 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<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.
|
||||
private async Task<string> SaveAttachmentInternalAsync(MailAttachmentViewModel attachmentViewModel, string saveFolderPath)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Updates a WebView2PrintSettingsModel from a CoreWebView2PrintSettings object.
|
||||
/// </summary>
|
||||
@@ -209,4 +236,4 @@ public static class PrintSettingsExtensions
|
||||
model.PagesPerSide = settings.PagesPerSide;
|
||||
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.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<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!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
|
||||
public class PrintService : IPrintService
|
||||
{
|
||||
private const float PdfRenderDpi = 300f;
|
||||
private const float DefaultDpi = 96f;
|
||||
|
||||
private TaskCompletionSource<PrintingResult>? _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<WebView2PrintSettingsModel, Task<Stream>>? _renderPdfStreamAsync;
|
||||
private WebView2PrintSettingsModel _currentRenderSettings = new();
|
||||
private string _printTitle = string.Empty;
|
||||
|
||||
private List<CanvasBitmap> bitmaps = new();
|
||||
private Vector2 largestBitmap;
|
||||
private Vector2 pageSize;
|
||||
private Vector2 imagePadding = new Vector2(64, 64);
|
||||
private Vector2 cellSize;
|
||||
private readonly List<CanvasBitmap> _bitmaps = new();
|
||||
private readonly List<int> _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<PrintingResult> PrintPdfFileAsync(string pdfFilePath, string printTitle)
|
||||
public async Task<PrintingResult> PrintAsync(nint windowHandle, string printTitle, Func<WebView2PrintSettingsModel, Task<Stream>> renderPdfStreamAsync)
|
||||
{
|
||||
if (windowHandle == IntPtr.Zero)
|
||||
return PrintingResult.Failed;
|
||||
|
||||
if (_taskCompletionSource != null)
|
||||
{
|
||||
_taskCompletionSource.TrySetResult(PrintingResult.Abandoned);
|
||||
_taskCompletionSource = new TaskCompletionSource<PrintingResult>();
|
||||
CleanupPrintSession();
|
||||
}
|
||||
|
||||
// Load the PDF file
|
||||
var file = await Windows.Storage.StorageFile.GetFileFromPathAsync(pdfFilePath);
|
||||
pdfDocument = await PdfDocument.LoadFromFileAsync(file);
|
||||
_taskCompletionSource = new TaskCompletionSource<PrintingResult>();
|
||||
_renderPdfStreamAsync = renderPdfStreamAsync ?? throw new ArgumentNullException(nameof(renderPdfStreamAsync));
|
||||
_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);
|
||||
|
||||
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<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);
|
||||
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<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.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<string, Task<bool>>((path) =>
|
||||
{
|
||||
@@ -92,25 +93,14 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
||||
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.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;
|
||||
|
||||
Reference in New Issue
Block a user