Files
mmd-pdf/Controls/SignDocumentDialog.cs
kua-agent 6610acfa44 feat: rebrand to MMD PDF, single corporate theme, and remove original text under an edit
- Services/PdfRedactText.cs: strip text whose origin falls inside a CoverAnnotation
  from the page content stream at save time, hooked into PdfBurn.DrawAnnotationsIntoDoc.
  Fixes edited values staying recoverable by text extraction.
- Themes/MMD.xaml replaces all thirteen themes; picker and accent strip removed; no dark mode.
- Rename KillerPDF -> MMD PDF across code, resources, packaging and locale strings; new icon.
- Remove the upstream author credit and the in-app install button.
2026-08-27 07:37:09 +02:00

303 lines
15 KiB
C#

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 MmdPdf.Services.Signing;
using Microsoft.Win32;
namespace MmdPdf
{
/// <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 MmdPdf 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 = "MmdPdf - " + 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, "MmdPdf - " + 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 MmdPdf.Controls.FileDialog(MmdPdf.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 MmdPdf.Controls.FileDialog(MmdPdf.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);
}
}