vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using KillerPDF.Services.Signing;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace KillerPDF
|
||||
{
|
||||
/// <summary>
|
||||
/// Themed modal dialog that cryptographically signs the open PDF with a certificate (a .pfx/.p12
|
||||
/// file, or one from the Windows store) and writes a NEW signed copy. This is the real digital
|
||||
/// signature - distinct from the drawn "Signature" stamp tool, which only places a picture.
|
||||
/// Chrome and colors mirror PrintPreviewWindow so every KillerPDF dialog looks identical.
|
||||
/// </summary>
|
||||
internal sealed class SignDocumentDialog : Window
|
||||
{
|
||||
private readonly string _sourcePdf;
|
||||
|
||||
private RadioButton _fileRadio = null!;
|
||||
private RadioButton _storeRadio = null!;
|
||||
private TextBox _pfxBox = null!;
|
||||
private PasswordBox _pwBox = null!;
|
||||
private Button _browsePfx = null!;
|
||||
private ComboBox _storeCombo = null!;
|
||||
private TextBox _reasonBox = null!;
|
||||
private TextBox _locationBox = null!;
|
||||
private TextBox _contactBox = null!;
|
||||
private TextBox _outputBox = null!;
|
||||
private readonly List<X509Certificate2> _storeCerts = [];
|
||||
|
||||
// Segoe MDL2 Assets close glyph, matching the main window + print dialog chrome.
|
||||
private const string CloseGlyph = "";
|
||||
|
||||
private static SolidColorBrush R(string key) => (SolidColorBrush)Application.Current.Resources[key];
|
||||
|
||||
// Localized string from the active locale dictionary (falls back to the key if missing).
|
||||
private static string L(string key) => Application.Current.TryFindResource(key) as string ?? key;
|
||||
|
||||
public SignDocumentDialog(Window? owner, string sourcePdf)
|
||||
{
|
||||
_sourcePdf = sourcePdf;
|
||||
Title = "KillerPDF - " + L("Str_Sign_Name");
|
||||
Width = 470;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
UseLayoutRounding = true;
|
||||
DialogChrome.Configure(this, owner);
|
||||
BuildUi();
|
||||
}
|
||||
|
||||
private void BuildUi()
|
||||
{
|
||||
var body = new StackPanel { Margin = new Thickness(20, 6, 20, 18) };
|
||||
|
||||
body.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.Format(L("Str_Sign_Desc"), Path.GetFileName(_sourcePdf)),
|
||||
Foreground = R("MutedTextBrush"), FontSize = 11, TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 0, 0, 14)
|
||||
});
|
||||
|
||||
// --- Certificate source --------------------------------------------------------------
|
||||
body.Children.Add(Label(L("Str_Sign_Certificate")));
|
||||
|
||||
_fileRadio = Radio(L("Str_Sign_FromFile"), true);
|
||||
_storeRadio = Radio(L("Str_Sign_FromStore"), false);
|
||||
_fileRadio.Checked += (_, _) => SyncSource();
|
||||
_storeRadio.Checked += (_, _) => SyncSource();
|
||||
body.Children.Add(_fileRadio);
|
||||
|
||||
var fileRow = new Grid { Margin = new Thickness(20, 2, 0, 4) };
|
||||
fileRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
fileRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
_pfxBox = Field("");
|
||||
_pfxBox.Margin = new Thickness(0, 0, 6, 0);
|
||||
Grid.SetColumn(_pfxBox, 0);
|
||||
_browsePfx = MakeButton(L("Str_Sign_Browse"), false);
|
||||
_browsePfx.Click += (_, _) => BrowsePfx();
|
||||
Grid.SetColumn(_browsePfx, 1);
|
||||
fileRow.Children.Add(_pfxBox);
|
||||
fileRow.Children.Add(_browsePfx);
|
||||
body.Children.Add(fileRow);
|
||||
|
||||
body.Children.Add(new TextBlock { Text = L("Str_Sign_Password"), Foreground = R("MutedTextBrush"), FontSize = 11, Margin = new Thickness(20, 4, 0, 2) });
|
||||
_pwBox = new PasswordBox
|
||||
{
|
||||
Margin = new Thickness(20, 0, 0, 10),
|
||||
Background = R("BgCanvas"), Foreground = R("TextBrush"),
|
||||
BorderBrush = R("CardBorderBrush"), BorderThickness = new Thickness(1),
|
||||
CaretBrush = R("TextBrush"), Template = MakePasswordTemplate()
|
||||
};
|
||||
body.Children.Add(_pwBox);
|
||||
|
||||
body.Children.Add(_storeRadio);
|
||||
_storeCombo = new ComboBox { Margin = new Thickness(20, 2, 0, 10), Height = 26 };
|
||||
ApplyComboStyle(_storeCombo);
|
||||
try
|
||||
{
|
||||
foreach (var c in WindowsCertificateStore.ListSigningCertificates())
|
||||
{
|
||||
_storeCerts.Add(c);
|
||||
_storeCombo.Items.Add(new StoreCertificateProvider(c).DisplayName);
|
||||
}
|
||||
}
|
||||
catch { /* store unavailable - leave empty */ }
|
||||
if (_storeCombo.Items.Count > 0) _storeCombo.SelectedIndex = 0;
|
||||
body.Children.Add(_storeCombo);
|
||||
|
||||
// --- Metadata ------------------------------------------------------------------------
|
||||
body.Children.Add(Label(L("Str_Sign_Reason")));
|
||||
_reasonBox = Field(""); body.Children.Add(_reasonBox);
|
||||
body.Children.Add(Label(L("Str_Sign_Location")));
|
||||
_locationBox = Field(""); body.Children.Add(_locationBox);
|
||||
body.Children.Add(Label(L("Str_Sign_Contact")));
|
||||
_contactBox = Field(""); body.Children.Add(_contactBox);
|
||||
|
||||
// --- Output --------------------------------------------------------------------------
|
||||
body.Children.Add(Label(L("Str_Sign_SaveAs")));
|
||||
var outRow = new Grid();
|
||||
outRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
outRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
_outputBox = Field(DefaultOutputPath());
|
||||
_outputBox.Margin = new Thickness(0, 0, 6, 0);
|
||||
Grid.SetColumn(_outputBox, 0);
|
||||
var browseOut = MakeButton(L("Str_Sign_Browse"), false);
|
||||
browseOut.Click += (_, _) => BrowseOutput();
|
||||
Grid.SetColumn(browseOut, 1);
|
||||
outRow.Children.Add(_outputBox);
|
||||
outRow.Children.Add(browseOut);
|
||||
body.Children.Add(outRow);
|
||||
|
||||
// --- Buttons -------------------------------------------------------------------------
|
||||
var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) };
|
||||
var sign = MakeButton(L("Str_Sign_Sign"), true);
|
||||
sign.Click += (_, _) => DoSign();
|
||||
sign.IsDefault = true; // Enter
|
||||
var cancel = MakeButton(L("Str_Sign_Cancel"), false);
|
||||
cancel.Margin = new Thickness(8, 0, 0, 0);
|
||||
cancel.Click += (_, _) => { DialogResult = false; Close(); };
|
||||
cancel.IsCancel = true; // Esc
|
||||
btnRow.Children.Add(sign);
|
||||
btnRow.Children.Add(cancel);
|
||||
body.Children.Add(btnRow);
|
||||
|
||||
SyncSource();
|
||||
|
||||
Content = DialogChrome.Frame(this, Owner, "KillerPDF - " + L("Str_Sign_TitleSuffix"),
|
||||
() => { DialogResult = false; Close(); }, body);
|
||||
}
|
||||
|
||||
private string DefaultOutputPath()
|
||||
{
|
||||
string dir = Path.GetDirectoryName(_sourcePdf) ?? "";
|
||||
string name = Path.GetFileNameWithoutExtension(_sourcePdf);
|
||||
return Path.Combine(dir, name + "-signed.pdf");
|
||||
}
|
||||
|
||||
// Enable only the inputs for the selected certificate source.
|
||||
private void SyncSource()
|
||||
{
|
||||
bool file = _fileRadio.IsChecked == true;
|
||||
_pfxBox.IsEnabled = _browsePfx.IsEnabled = _pwBox.IsEnabled = file;
|
||||
_storeCombo.IsEnabled = !file;
|
||||
}
|
||||
|
||||
private void BrowsePfx()
|
||||
{
|
||||
var dlg = new KillerPDF.Controls.FileDialog(KillerPDF.Controls.FileDialogMode.Open)
|
||||
{ Filter = L("Str_Filter_Cert") + "|*.pfx;*.p12|" + L("Str_Filter_AllFiles") + "|*.*", Title = L("Str_Sign_ChooseCert") };
|
||||
if (dlg.ShowDialog(this) == true) _pfxBox.Text = dlg.FileName;
|
||||
}
|
||||
|
||||
private void BrowseOutput()
|
||||
{
|
||||
var dlg = new KillerPDF.Controls.FileDialog(KillerPDF.Controls.FileDialogMode.Save)
|
||||
{ Filter = L("Str_Filter_Pdf") + "|*.pdf", Title = L("Str_Sign_SaveAs"), FileName = Path.GetFileName(_outputBox.Text) };
|
||||
if (dlg.ShowDialog(this) == true) _outputBox.Text = dlg.FileName;
|
||||
}
|
||||
|
||||
private void DoSign()
|
||||
{
|
||||
ICertificateProvider provider;
|
||||
if (_fileRadio.IsChecked == true)
|
||||
{
|
||||
string pfx = _pfxBox.Text?.Trim() ?? "";
|
||||
if (!File.Exists(pfx)) { Warn(L("Str_Sign_NeedCertFile")); return; }
|
||||
provider = new PfxFileCertificateProvider(pfx, _pwBox.Password);
|
||||
}
|
||||
else
|
||||
{
|
||||
int i = _storeCombo.SelectedIndex;
|
||||
if (i < 0 || i >= _storeCerts.Count) { Warn(L("Str_Sign_NoStoreCert")); return; }
|
||||
provider = new StoreCertificateProvider(_storeCerts[i]);
|
||||
}
|
||||
|
||||
string output = _outputBox.Text?.Trim() ?? "";
|
||||
if (string.IsNullOrEmpty(output)) { Warn(L("Str_Sign_NeedOutput")); return; }
|
||||
|
||||
X509Certificate2 cert;
|
||||
try { cert = provider.GetCertificate(); }
|
||||
catch (System.Security.Cryptography.CryptographicException)
|
||||
{
|
||||
// The raw Win32 text ("The specified network password is not correct.") is misleading -
|
||||
// nothing networked is involved. Almost always a wrong password or a non-.pfx file.
|
||||
Warn(L("Str_Sign_BadCert"));
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) { Warn(L("Str_Sign_CertLoadFailed") + "\n\n" + ex.Message); return; }
|
||||
|
||||
try
|
||||
{
|
||||
new PdfSigner().Sign(_sourcePdf, output, cert,
|
||||
new PdfSigner.SignInfo(_reasonBox.Text ?? "", _locationBox.Text ?? "", _contactBox.Text ?? ""));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Warn(L("Str_Sign_Failed") + "\n\n" + ex.GetType().Name + ": " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
KillerDialog.Show(this, L("Str_Dlg_SignedSavedTo") + "\n" + output, L("Str_Sign_Name"), MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Warn(string msg) => KillerDialog.Show(this, msg, L("Str_Sign_Name"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
|
||||
// ---- themed control helpers (mirroring PrintPreviewWindow) -------------------------------
|
||||
private Style? FindOwnerStyle(string key) => Owner?.TryFindResource(key) as Style;
|
||||
|
||||
private static TextBlock Label(string text) => new()
|
||||
{ Text = text, Foreground = R("TextBrush"), FontSize = 12, FontWeight = FontWeights.SemiBold, Margin = new Thickness(0, 6, 0, 2) };
|
||||
|
||||
private RadioButton Radio(string text, bool isChecked)
|
||||
{
|
||||
var r = new RadioButton { Content = text, IsChecked = isChecked, GroupName = "CertSource", FontSize = 12, Margin = new Thickness(0, 4, 0, 2) };
|
||||
if (FindOwnerStyle("ThemeRadio") is Style s) r.Style = s; else r.Foreground = R("TextBrush");
|
||||
return r;
|
||||
}
|
||||
|
||||
private void ApplyComboStyle(ComboBox combo)
|
||||
{
|
||||
if (FindOwnerStyle("DarkComboBox") is Style s) combo.Style = s;
|
||||
else { combo.Foreground = R("TextBrush"); combo.BorderBrush = R("CardBorderBrush"); }
|
||||
combo.Background = R("BgCanvas");
|
||||
}
|
||||
|
||||
private TextBox Field(string text)
|
||||
{
|
||||
var tb = new TextBox
|
||||
{
|
||||
Text = text, Margin = new Thickness(0, 0, 0, 4),
|
||||
Background = R("BgCanvas"), Foreground = R("TextBrush"),
|
||||
BorderBrush = R("CardBorderBrush"), BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(6, 4, 6, 4), CaretBrush = R("TextBrush"),
|
||||
SelectionBrush = R("RowSelectedBrush"), SelectionTextBrush = R("TextBrush"),
|
||||
Template = MakeTextBoxTemplate()
|
||||
};
|
||||
return tb;
|
||||
}
|
||||
|
||||
private static ControlTemplate MakeTextBoxTemplate()
|
||||
{
|
||||
var b = new FrameworkElementFactory(typeof(Border));
|
||||
b.SetBinding(Border.BackgroundProperty, new Binding("Background") { RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent) });
|
||||
b.SetBinding(Border.BorderBrushProperty, new Binding("BorderBrush") { RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent) });
|
||||
b.SetBinding(Border.BorderThicknessProperty, new Binding("BorderThickness") { RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent) });
|
||||
b.SetValue(Border.CornerRadiusProperty, new CornerRadius(3));
|
||||
var sv = new FrameworkElementFactory(typeof(ScrollViewer)) { Name = "PART_ContentHost" };
|
||||
b.AppendChild(sv);
|
||||
var ct = new ControlTemplate(typeof(TextBox)) { VisualTree = b };
|
||||
var disabled = new Trigger { Property = UIElement.IsEnabledProperty, Value = false };
|
||||
disabled.Setters.Add(new Setter(UIElement.OpacityProperty, 0.4));
|
||||
ct.Triggers.Add(disabled);
|
||||
return ct;
|
||||
}
|
||||
|
||||
private static ControlTemplate MakePasswordTemplate()
|
||||
{
|
||||
var b = new FrameworkElementFactory(typeof(Border));
|
||||
b.SetBinding(Border.BackgroundProperty, new Binding("Background") { RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent) });
|
||||
b.SetBinding(Border.BorderBrushProperty, new Binding("BorderBrush") { RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent) });
|
||||
b.SetBinding(Border.BorderThicknessProperty, new Binding("BorderThickness") { RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent) });
|
||||
b.SetValue(Border.CornerRadiusProperty, new CornerRadius(3));
|
||||
b.SetValue(Border.PaddingProperty, new Thickness(6, 4, 6, 4));
|
||||
var sv = new FrameworkElementFactory(typeof(ScrollViewer)) { Name = "PART_ContentHost" };
|
||||
b.AppendChild(sv);
|
||||
var ct = new ControlTemplate(typeof(PasswordBox)) { VisualTree = b };
|
||||
var disabled = new Trigger { Property = UIElement.IsEnabledProperty, Value = false };
|
||||
disabled.Setters.Add(new Setter(UIElement.OpacityProperty, 0.4));
|
||||
ct.Triggers.Add(disabled);
|
||||
return ct;
|
||||
}
|
||||
|
||||
private static Button MakeButton(string label, bool accent) => UiKit.Make(label, accent);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user