Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2085dad3b9 | ||
|
|
b6f046abdd | ||
|
|
9dcd9055d9 | ||
|
|
cc579d794e | ||
|
|
19d9e906b7 | ||
|
|
a9b8f314a7 | ||
|
|
a26a4a7f46 | ||
|
|
221a7962ea | ||
|
|
ac092ccccc | ||
|
|
031c386191 | ||
|
|
0abfa700fe | ||
|
|
b29e087029 | ||
|
|
9287248214 | ||
|
|
1527e9fd7a | ||
|
|
b789881134 | ||
|
|
2090f57008 | ||
|
|
b4bb2544f6 | ||
|
|
edc2a3f050 | ||
|
|
0d8c86eccd | ||
|
|
19791f1aca | ||
|
|
1ed999f3c8 | ||
|
|
31447b8d80 | ||
|
|
87649cd77f | ||
|
|
a351e8e9a0 | ||
|
|
29850c364a | ||
|
|
39130f41e6 | ||
|
|
2cc2f1b627 |
@@ -1,3 +1,4 @@
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
MmdPdf.pdf
|
||||
|
||||
+20
-1
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
@@ -68,8 +68,27 @@ namespace MmdPdf
|
||||
// Startup
|
||||
// ============================================================
|
||||
|
||||
// Windows reports whether we are being viewed over a remote desktop. In that case WPF's
|
||||
// hardware compositing hands the session a surface it never repaints when the desktop is
|
||||
// resized, which leaves the part of the window outside the original size drawn as flat
|
||||
// black. Drawing in software costs nothing at this size and keeps the whole window painted.
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetSystemMetrics(int index);
|
||||
private const int SM_REMOTESESSION = 0x1000;
|
||||
|
||||
private static void UseSoftwareRenderingOnRemoteDesktops()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (GetSystemMetrics(SM_REMOTESESSION) != 0)
|
||||
RenderOptions.ProcessRenderMode = System.Windows.Interop.RenderMode.SoftwareOnly;
|
||||
}
|
||||
catch { /* never let a rendering hint stop the application starting */ }
|
||||
}
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
UseSoftwareRenderingOnRemoteDesktops();
|
||||
StartupTrace.Mark("App.OnStartup entered");
|
||||
DispatcherUnhandledException += OnDispatcherException;
|
||||
AppDomain.CurrentDomain.UnhandledException += OnDomainException;
|
||||
|
||||
@@ -67,9 +67,9 @@ namespace MmdPdf
|
||||
int kp = fullTitle?.IndexOf("MmdPdf", StringComparison.Ordinal) ?? -1;
|
||||
if (kp >= 0)
|
||||
{
|
||||
// Killer + PDF in one TextBlock so the two sizes share a baseline (cohesive wordmark).
|
||||
// MMD + PDF in one TextBlock so the two sizes share a baseline (cohesive wordmark).
|
||||
var logoTb = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
|
||||
logoTb.Inlines.Add(new System.Windows.Documents.Run("Killer") { FontFamily = wordmark, FontWeight = FontWeights.Normal, FontSize = 16, Foreground = primary });
|
||||
logoTb.Inlines.Add(new System.Windows.Documents.Run("MMD ") { FontFamily = wordmark, FontWeight = FontWeights.Normal, FontSize = 16, Foreground = primary });
|
||||
logoTb.Inlines.Add(new System.Windows.Documents.Run("PDF") { FontFamily = wordmarkPdf, FontWeight = FontWeights.Bold, FontSize = 20.8, Foreground = logo });
|
||||
sp.Children.Add(logoTb);
|
||||
string after = fullTitle![(kp + "MmdPdf".Length)..];
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace MmdPdf
|
||||
{
|
||||
var wm = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
var wmTb = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
|
||||
wmTb.Inlines.Add(new System.Windows.Documents.Run("Killer") { FontFamily = UiKit.WordmarkFont, FontWeight = FontWeights.Normal, FontSize = 15, Foreground = R("TextBrush") });
|
||||
wmTb.Inlines.Add(new System.Windows.Documents.Run("MMD ") { FontFamily = UiKit.WordmarkFont, FontWeight = FontWeights.Normal, FontSize = 15, Foreground = R("TextBrush") });
|
||||
wmTb.Inlines.Add(new System.Windows.Documents.Run("PDF") { FontFamily = UiKit.WordmarkFontPdf, FontWeight = FontWeights.Bold, FontSize = 19.5, Foreground = R("AccentLogo") });
|
||||
wm.Children.Add(wmTb);
|
||||
// No DropShadowEffect on the text - it rasterizes and blurs the wordmark. Kept crisp.
|
||||
@@ -241,7 +241,7 @@ namespace MmdPdf
|
||||
if (title == "MmdPdf")
|
||||
{
|
||||
var wmTb = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
|
||||
wmTb.Inlines.Add(new System.Windows.Documents.Run("Killer") { FontFamily = UiKit.WordmarkFont, FontWeight = FontWeights.Normal, FontSize = 15, Foreground = R("TextBrush") });
|
||||
wmTb.Inlines.Add(new System.Windows.Documents.Run("MMD ") { FontFamily = UiKit.WordmarkFont, FontWeight = FontWeights.Normal, FontSize = 15, Foreground = R("TextBrush") });
|
||||
wmTb.Inlines.Add(new System.Windows.Documents.Run("PDF") { FontFamily = UiKit.WordmarkFontPdf, FontWeight = FontWeights.Bold, FontSize = 19.5, Foreground = R("AccentLogo") });
|
||||
titleBar.Child = wmTb;
|
||||
}
|
||||
@@ -386,7 +386,7 @@ namespace MmdPdf
|
||||
titleBar.MouseLeftButtonDown += (_, e) => { if (e.ButtonState == MouseButtonState.Pressed) win.DragMove(); };
|
||||
var wm = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
var wmTb = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
|
||||
wmTb.Inlines.Add(new System.Windows.Documents.Run("Killer") { FontFamily = UiKit.WordmarkFont, FontWeight = FontWeights.Normal, FontSize = 15, Foreground = R("TextBrush") });
|
||||
wmTb.Inlines.Add(new System.Windows.Documents.Run("MMD ") { FontFamily = UiKit.WordmarkFont, FontWeight = FontWeights.Normal, FontSize = 15, Foreground = R("TextBrush") });
|
||||
wmTb.Inlines.Add(new System.Windows.Documents.Run("PDF") { FontFamily = UiKit.WordmarkFontPdf, FontWeight = FontWeights.Bold, FontSize = 18, Foreground = R("AccentLogo") });
|
||||
wm.Children.Add(wmTb);
|
||||
titleBar.Child = wm;
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<!-- Pull pages off a copier. Chrome follows the MMD design system locked on 2026-08-28:
|
||||
a front-window title bar (white to silver, 3px red top edge, ink text), 4px corners,
|
||||
flat buttons, one red fill for the single action the person came to do. Every colour and
|
||||
radius is a theme key from Themes/MMD.xaml - nothing is picked here. -->
|
||||
<Window x:Class="MmdPdf.Features.Scan.ScanDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Scan a document"
|
||||
Width="520" SizeToContent="Height"
|
||||
WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False"
|
||||
Background="Transparent"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"
|
||||
UseLayoutRounding="True" SnapsToDevicePixels="True"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Border Background="{DynamicResource BgFlyout}"
|
||||
BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="1"
|
||||
CornerRadius="{DynamicResource ControlCornerRadius}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Title bar: the front-window treatment from the design system -->
|
||||
<Border Grid.Row="0" Height="34"
|
||||
Background="{DynamicResource WindowFrontTitleBrush}"
|
||||
BorderBrush="{DynamicResource PrimaryBrush}" BorderThickness="0,3,0,0"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<Grid Margin="12,0,8,0">
|
||||
<TextBlock x:Name="TitleText" Text="Scan a document" VerticalAlignment="Center"
|
||||
FontFamily="Segoe UI" FontSize="12.5" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextBrush}"/>
|
||||
<Button x:Name="CloseChrome" Click="Cancel_Click" HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center" Width="24" Height="24"
|
||||
Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||||
Foreground="{DynamicResource TextBrush}" Content="✕"
|
||||
FontFamily="Segoe UI" FontSize="11"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Body -->
|
||||
<Grid Grid.Row="1" Margin="16,14,16,6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="COPIER" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="CopierBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="FEED" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="FeedBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="COLOUR" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="ColourBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="DETAIL" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="DetailBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="PAPER" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="PaperBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="AFTERWARDS" VerticalAlignment="Top" Margin="0,2,0,0"
|
||||
FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<StackPanel Grid.Column="1">
|
||||
<CheckBox x:Name="SearchableBox" IsChecked="True" FontSize="11.5">
|
||||
<TextBlock Text="Make the words searchable, in French and English"
|
||||
TextWrapping="Wrap" MaxWidth="180"/>
|
||||
</CheckBox>
|
||||
<CheckBox x:Name="StraightenBox" Margin="0,6,0,0" FontSize="11.5">
|
||||
<TextBlock Text="Straighten crooked pages" TextWrapping="Wrap" MaxWidth="180"/>
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Sheet preview: what one page will look like -->
|
||||
<Border Grid.Column="1" Width="132" Margin="16,0,0,0"
|
||||
Background="{DynamicResource BgCanvas}"
|
||||
BorderBrush="{DynamicResource InputBorderBrush}" BorderThickness="1"
|
||||
CornerRadius="{DynamicResource ControlCornerRadius}">
|
||||
<Border Width="96" Height="132" Margin="10" VerticalAlignment="Center"
|
||||
Background="White" BorderBrush="{DynamicResource InputBorderBrush}" BorderThickness="1">
|
||||
<StackPanel Margin="9,9,8,0">
|
||||
<Rectangle Height="3" Width="56" HorizontalAlignment="Left" Margin="0,0,0,5"
|
||||
Fill="{DynamicResource DimTextBrush}"/>
|
||||
<Rectangle Height="2" Margin="0,0,0,4" Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Width="66" HorizontalAlignment="Left" Margin="0,0,0,4"
|
||||
Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Width="44" HorizontalAlignment="Left" Margin="0,0,0,4"
|
||||
Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Margin="0,0,0,4" Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Width="60" HorizontalAlignment="Left" Fill="{DynamicResource TreeLineBrush}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border Grid.Row="2" Background="{DynamicResource SurfaceBrush}"
|
||||
BorderBrush="{DynamicResource PaneBorderBrush}" BorderThickness="0,1,0,0"
|
||||
CornerRadius="0,0,4,4" Padding="16,10">
|
||||
<Grid>
|
||||
<TextBlock x:Name="FootNote" VerticalAlignment="Center" FontSize="10.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"
|
||||
Text="Pages arrive as one document you can edit"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button x:Name="CancelBtn" Content="Cancel" Click="Cancel_Click"
|
||||
Height="28" Padding="13,0" Margin="0,0,8,0" FontSize="11.5" FontWeight="SemiBold"/>
|
||||
<Button x:Name="ScanBtn" Content="Scan" Click="Scan_Click"
|
||||
Height="28" Padding="13,0" FontSize="11.5" FontWeight="SemiBold"
|
||||
Background="{DynamicResource PrimaryBrush}"
|
||||
Foreground="{DynamicResource OnPrimaryBrush}"
|
||||
BorderBrush="{DynamicResource PrimaryBrush}" BorderThickness="1"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using MmdPdf.Services;
|
||||
|
||||
namespace MmdPdf.Features.Scan
|
||||
{
|
||||
/// <summary>
|
||||
/// Asks the copier for pages. The person picks the machine, the feed, how much ink and how
|
||||
/// much detail; everything else is the copier's own default. The pages come back as image
|
||||
/// files which MainWindow turns into one document.
|
||||
///
|
||||
/// A run cannot be interrupted mid-sheet - that is the copier's hardware, not a decision made
|
||||
/// here - so Cancel stops after the sheet in progress and keeps the pages already read.
|
||||
/// </summary>
|
||||
public partial class ScanDialog : Window
|
||||
{
|
||||
private readonly List<ScannerInfo> _scanners;
|
||||
private CancellationTokenSource? _cancel;
|
||||
private bool _running;
|
||||
|
||||
/// <summary>Image files, one per page, in the order the copier read them.</summary>
|
||||
internal string[] Pages { get; private set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>The person asked for the words to be made searchable afterwards.</summary>
|
||||
internal bool MakeSearchable { get; private set; }
|
||||
|
||||
/// <summary>The person asked for crooked pages to be straightened afterwards.</summary>
|
||||
internal bool Straighten { get; private set; }
|
||||
|
||||
public ScanDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_scanners = ScanService.IsAvailable ? ScanService.ListScanners() : new List<ScannerInfo>();
|
||||
|
||||
foreach (var s in _scanners) CopierBox.Items.Add(s.Name);
|
||||
if (_scanners.Count > 0)
|
||||
{
|
||||
CopierBox.SelectedIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
CopierBox.Items.Add(ScanService.IsAvailable
|
||||
? "No copier found on this machine"
|
||||
: "This machine has no imaging service");
|
||||
CopierBox.SelectedIndex = 0;
|
||||
CopierBox.IsEnabled = false;
|
||||
ScanBtn.IsEnabled = false;
|
||||
FootNote.Text = "Switch the copier on, or connect it, then open this again";
|
||||
}
|
||||
|
||||
FeedBox.Items.Add("Feeder, one side");
|
||||
FeedBox.Items.Add("Feeder, both sides");
|
||||
FeedBox.Items.Add("Glass, one sheet");
|
||||
FeedBox.SelectedIndex = 0;
|
||||
|
||||
ColourBox.Items.Add("Black and white");
|
||||
ColourBox.Items.Add("Shades of grey");
|
||||
ColourBox.Items.Add("Colour");
|
||||
ColourBox.SelectedIndex = 0;
|
||||
|
||||
DetailBox.Items.Add("200 dpi, quick");
|
||||
DetailBox.Items.Add("300 dpi, good for text");
|
||||
DetailBox.Items.Add("600 dpi, fine detail");
|
||||
DetailBox.SelectedIndex = 1;
|
||||
|
||||
foreach (var p in ScanService.PaperSizes) PaperBox.Items.Add(p);
|
||||
PaperBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ButtonState == MouseButtonState.Pressed) DragMove();
|
||||
}
|
||||
|
||||
private void Cancel_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_running)
|
||||
{
|
||||
_cancel?.Cancel();
|
||||
FootNote.Text = "Stopping after this sheet…";
|
||||
return;
|
||||
}
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
|
||||
private async void Scan_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_running || _scanners.Count == 0) return;
|
||||
|
||||
var device = _scanners[Math.Max(0, CopierBox.SelectedIndex)];
|
||||
var feed = FeedBox.SelectedIndex switch
|
||||
{
|
||||
1 => ScanFeed.FeederBothSides,
|
||||
2 => ScanFeed.Flatbed,
|
||||
_ => ScanFeed.FeederOneSide,
|
||||
};
|
||||
var colour = ColourBox.SelectedIndex switch
|
||||
{
|
||||
1 => ScanColour.Greyscale,
|
||||
2 => ScanColour.Colour,
|
||||
_ => ScanColour.BlackAndWhite,
|
||||
};
|
||||
int dpi = DetailBox.SelectedIndex switch
|
||||
{
|
||||
0 => 200,
|
||||
2 => 600,
|
||||
_ => 300,
|
||||
};
|
||||
string paper = PaperBox.SelectedItem as string ?? "A4 portrait";
|
||||
|
||||
MakeSearchable = SearchableBox.IsChecked == true;
|
||||
Straighten = StraightenBox.IsChecked == true;
|
||||
|
||||
_running = true;
|
||||
_cancel = new CancellationTokenSource();
|
||||
ScanBtn.IsEnabled = false;
|
||||
CancelBtn.Content = "Stop";
|
||||
SetInputsEnabled(false);
|
||||
FootNote.Text = feed == ScanFeed.Flatbed ? "Reading the sheet on the glass…" : "Reading…";
|
||||
|
||||
var progress = new Progress<int>(n =>
|
||||
FootNote.Text = n == 1 ? "1 page read" : n.ToString(CultureInfo.InvariantCulture) + " pages read");
|
||||
|
||||
try
|
||||
{
|
||||
var token = _cancel.Token;
|
||||
var reporter = (IProgress<int>)progress;
|
||||
string[] pages = await Task.Run(() =>
|
||||
ScanService.Acquire(device.DeviceId, feed, colour, dpi, paper,
|
||||
maxPages: feed == ScanFeed.Flatbed ? 1 : 200,
|
||||
cancel: token,
|
||||
onPage: reporter.Report).ToArray(), token);
|
||||
|
||||
Pages = pages;
|
||||
if (pages.Length == 0)
|
||||
{
|
||||
FootNote.Text = "The copier returned no pages";
|
||||
ResetAfterRun();
|
||||
return;
|
||||
}
|
||||
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
FootNote.Text = "Stopped";
|
||||
ResetAfterRun();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FootNote.Text = "The copier could not be used: " + ex.Message;
|
||||
ResetAfterRun();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAfterRun()
|
||||
{
|
||||
_running = false;
|
||||
_cancel?.Dispose();
|
||||
_cancel = null;
|
||||
ScanBtn.IsEnabled = _scanners.Count > 0;
|
||||
CancelBtn.Content = "Cancel";
|
||||
SetInputsEnabled(true);
|
||||
}
|
||||
|
||||
private void SetInputsEnabled(bool on)
|
||||
{
|
||||
CopierBox.IsEnabled = on && _scanners.Count > 0;
|
||||
FeedBox.IsEnabled = on;
|
||||
ColourBox.IsEnabled = on;
|
||||
DetailBox.IsEnabled = on;
|
||||
PaperBox.IsEnabled = on;
|
||||
SearchableBox.IsEnabled = on;
|
||||
StraightenBox.IsEnabled = on;
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-6
@@ -112,12 +112,12 @@
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="10,6"/>
|
||||
<Setter Property="Padding" Value="8,4"/>
|
||||
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Width" Value="36"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Width" Value="30"/>
|
||||
<Setter Property="Height" Value="26"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
@@ -1179,10 +1179,12 @@
|
||||
<!-- Soft drop shadow: a blurred, offset copy behind the crisp wordmark -->
|
||||
<StackPanel Orientation="Horizontal" Margin="1,1,0,0" Opacity="{DynamicResource WordmarkShadowOpacity}">
|
||||
<StackPanel.Effect><BlurEffect Radius="2"/></StackPanel.Effect>
|
||||
<Image Source="Resources/mmd-lockup-red.png" Height="19" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
|
||||
<Image Source="Resources/mmd-lockup-white.png" Height="18" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
|
||||
<TextBlock Text="PDF" Margin="5,0,0,0" VerticalAlignment="Center" FontSize="13" FontWeight="Bold" Foreground="White" Opacity="0.95"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Source="Resources/mmd-lockup-red.png" Height="19" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
|
||||
<Image Source="Resources/mmd-lockup-white.png" Height="18" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
|
||||
<TextBlock Text="PDF" Margin="5,0,0,0" VerticalAlignment="Center" FontSize="13" FontWeight="Bold" Foreground="White" Opacity="0.95"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -1237,7 +1239,7 @@
|
||||
<Border IsHitTestVisible="False" Opacity="{DynamicResource GrainOpacity}" Background="{StaticResource GrainBrushShared}"/>
|
||||
<!-- Background=Transparent is load-bearing: without it the grid's empty stretches have no
|
||||
hit surface and the right-click appearance picker only opens over the buttons. -->
|
||||
<Grid x:Name="ToolbarGrid" SizeChanged="ToolbarGrid_SizeChanged" Margin="4,2" Background="Transparent">
|
||||
<Grid x:Name="ToolbarGrid" SizeChanged="ToolbarGrid_SizeChanged" Margin="4,1" Background="Transparent">
|
||||
<Grid.ContextMenu>
|
||||
<!-- Items are built in code (BuildToolbarMenu, SettingsPanel.cs) so captions re-Loc on
|
||||
language switch; attached here so WPF supplies the inheritance context that lets
|
||||
@@ -1257,6 +1259,7 @@
|
||||
<Button x:Name="OcrMenuBtn" Content="" FontSize="10" Width="13" Padding="0" Margin="-6,0,0,0" Style="{StaticResource ToolbarButton}" Click="OcrMenu_Click" ToolTip="{DynamicResource Str_TT_OcrOptions}"/>
|
||||
<Button Content="" Style="{StaticResource ToolbarButton}" Click="SaveFlattened_Click" ToolTip="{DynamicResource Str_TT_SaveFlattened}"/>
|
||||
<Button Content="" Style="{StaticResource ToolbarButton}" Click="Print_Click" ToolTip="{DynamicResource Str_TT_Print}"/>
|
||||
<Button x:Name="ScanBtn" Content="" Style="{StaticResource ToolbarButton}" Click="Scan_Click" ToolTip="{DynamicResource Str_TT_Scan}"/>
|
||||
<StackPanel x:Name="GrpPageOps" Orientation="Horizontal">
|
||||
<Rectangle Width="1" Fill="{DynamicResource CardBorderBrush}" Margin="4,6"/>
|
||||
<Button Content="" Style="{StaticResource ToolbarButton}" Click="Merge_Click" ToolTip="{DynamicResource Str_TT_MergePDFs}"/>
|
||||
|
||||
@@ -532,6 +532,22 @@ namespace MmdPdf
|
||||
// squared chrome on every move (and once now that the handle exists).
|
||||
LocationChanged += OnWindowLocationChanged;
|
||||
UpdateWindowChrome();
|
||||
FitToSmallScreen();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plant machines run at 800 by 600. The default 1000 by 700 window does not fit one, and
|
||||
/// a window taller than the screen puts its own title bar - and therefore Scan, the window
|
||||
/// buttons and the tabs - above the top edge where nobody can reach them. On a screen that
|
||||
/// cannot hold the default, start maximised instead.
|
||||
/// </summary>
|
||||
private void FitToSmallScreen()
|
||||
{
|
||||
if (WindowState != WindowState.Normal) return;
|
||||
var work = System.Windows.SystemParameters.WorkArea;
|
||||
if (work.Width <= 0 || work.Height <= 0) return;
|
||||
if (Width <= work.Width && Height <= work.Height) return;
|
||||
WindowState = WindowState.Maximized;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
+6
-4
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
@@ -13,9 +13,11 @@
|
||||
<UseWPF>true</UseWPF>
|
||||
<ApplicationIcon>Resources\mmd-icon.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>1.7.5</Version>
|
||||
<AssemblyVersion>1.7.5.0</AssemblyVersion>
|
||||
<FileVersion>1.7.5.0</FileVersion>
|
||||
<!-- MMD's own version line. 1.7.5 was the upstream project's; it read as that
|
||||
product still, and it has to sort above it so an upgrade is an upgrade. -->
|
||||
<Version>2026.8.2</Version>
|
||||
<AssemblyVersion>2026.8.2.0</AssemblyVersion>
|
||||
<FileVersion>2026.8.2.0</FileVersion>
|
||||
<!-- Shown in the About card beside the version so a user can tell how old their build
|
||||
is. Must match the date on this version's CHANGELOG section - release.ps1 preflight
|
||||
fails the release if it does not. Bump it with the version. -->
|
||||
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,373 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace MmdPdf.Services
|
||||
{
|
||||
/// <summary>Which side of the paper the copier reads, and from where.</summary>
|
||||
internal enum ScanFeed
|
||||
{
|
||||
Flatbed,
|
||||
FeederOneSide,
|
||||
FeederBothSides,
|
||||
}
|
||||
|
||||
/// <summary>How much ink the copier records.</summary>
|
||||
internal enum ScanColour
|
||||
{
|
||||
BlackAndWhite,
|
||||
Greyscale,
|
||||
Colour,
|
||||
}
|
||||
|
||||
/// <summary>One copier the machine can reach.</summary>
|
||||
internal sealed class ScannerInfo
|
||||
{
|
||||
internal ScannerInfo(string deviceId, string name)
|
||||
{
|
||||
DeviceId = deviceId;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
internal string DeviceId { get; }
|
||||
internal string Name { get; }
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulls pages off a network copier or a desk scanner through WIA, the imaging service that
|
||||
/// ships with Windows. Nothing is installed for this: the COM objects are created late-bound
|
||||
/// by ProgID, so a machine with no imaging stack fails at <see cref="IsAvailable"/> instead of
|
||||
/// at load time, and the application still starts.
|
||||
///
|
||||
/// The page images land in a folder of their own under the user's temp directory and are
|
||||
/// handed back as file paths; turning them into a document is PdfImport's job, the same way
|
||||
/// an imported photograph is handled.
|
||||
/// </summary>
|
||||
internal static class ScanService
|
||||
{
|
||||
// WIA item property ids. These are the documented constants; the COM interface takes
|
||||
// them as integers, so they are written here once rather than repeated as magic numbers.
|
||||
private const int WIA_IPS_CUR_INTENT = 6146;
|
||||
private const int WIA_IPS_XRES = 6147;
|
||||
private const int WIA_IPS_YRES = 6148;
|
||||
private const int WIA_IPS_XPOS = 6149;
|
||||
private const int WIA_IPS_YPOS = 6150;
|
||||
private const int WIA_IPS_XEXTENT = 6151;
|
||||
private const int WIA_IPS_YEXTENT = 6152;
|
||||
private const int WIA_IPA_DATATYPE = 4103;
|
||||
private const int WIA_IPA_DEPTH = 4104;
|
||||
|
||||
// Device properties that drive the sheet feeder.
|
||||
private const int WIA_DPS_DOCUMENT_HANDLING_SELECT = 3088;
|
||||
private const int WIA_DPS_DOCUMENT_HANDLING_STATUS = 3087;
|
||||
private const int WIA_DPS_PAGES = 3096;
|
||||
|
||||
private const int FEEDER = 0x001;
|
||||
private const int FLATBED = 0x002;
|
||||
private const int DUPLEX = 0x004;
|
||||
private const int FEED_READY = 0x001;
|
||||
|
||||
// Intent values: what the copier should optimise for.
|
||||
private const int INTENT_COLOUR = 0x00000001;
|
||||
private const int INTENT_GREYSCALE = 0x00000002;
|
||||
private const int INTENT_TEXT = 0x00000004;
|
||||
|
||||
private const string FORMAT_PNG = "{B96B3CAF-0728-11D3-9D7B-0000F81EF32E}";
|
||||
|
||||
// Paper the copier is asked for, in inches. A4 is the MMD standard; Letter is here
|
||||
// because the Dubai office buys it locally.
|
||||
private static readonly Dictionary<string, (double W, double H)> PaperInches =
|
||||
new Dictionary<string, (double, double)>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["A4 portrait"] = (8.27, 11.69),
|
||||
["A4 landscape"] = (11.69, 8.27),
|
||||
["A5 portrait"] = (5.83, 8.27),
|
||||
["Letter portrait"] = (8.5, 11.0),
|
||||
["Legal portrait"] = (8.5, 14.0),
|
||||
};
|
||||
|
||||
internal static IEnumerable<string> PaperSizes => PaperInches.Keys;
|
||||
|
||||
/// <summary>True when this machine has the Windows imaging service at all.</summary>
|
||||
internal static bool IsAvailable => Type.GetTypeFromProgID("WIA.DeviceManager") != null;
|
||||
|
||||
/// <summary>
|
||||
/// Every copier and scanner the machine can currently reach. An empty list is a normal
|
||||
/// answer - it means nothing is switched on - and never an exception the caller has to
|
||||
/// catch.
|
||||
/// </summary>
|
||||
internal static List<ScannerInfo> ListScanners()
|
||||
{
|
||||
var found = new List<ScannerInfo>();
|
||||
var progId = Type.GetTypeFromProgID("WIA.DeviceManager");
|
||||
if (progId == null) return found;
|
||||
|
||||
object? manager = null;
|
||||
try
|
||||
{
|
||||
manager = Activator.CreateInstance(progId);
|
||||
dynamic dm = manager!;
|
||||
dynamic infos = dm.DeviceInfos;
|
||||
int count = (int)infos.Count;
|
||||
for (int i = 1; i <= count; i++) // WIA collections are 1-based
|
||||
{
|
||||
dynamic info = infos[i];
|
||||
try
|
||||
{
|
||||
// 1 == ScannerDeviceType. Cameras and video devices are skipped.
|
||||
if ((int)info.Type != 1) continue;
|
||||
string name = ReadProperty(info.Properties, "Name") ?? "Copier";
|
||||
found.Add(new ScannerInfo((string)info.DeviceID, name));
|
||||
}
|
||||
finally { Release(info); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { /* imaging service not running: nothing to offer */ }
|
||||
catch (InvalidCastException) { }
|
||||
finally { Release(manager); }
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads pages and writes each one as a PNG into its own folder. Returns the files in
|
||||
/// page order. Cancelling stops after the page in progress - a copier cannot be
|
||||
/// interrupted mid-sheet.
|
||||
/// </summary>
|
||||
/// <param name="onPage">Called with the running page count as each sheet lands.</param>
|
||||
internal static List<string> Acquire(
|
||||
string deviceId,
|
||||
ScanFeed feed,
|
||||
ScanColour colour,
|
||||
int dpi,
|
||||
string paper,
|
||||
int maxPages,
|
||||
CancellationToken cancel,
|
||||
Action<int>? onPage = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId)) throw new ArgumentException("No copier chosen.", nameof(deviceId));
|
||||
if (dpi < 75 || dpi > 1200) throw new ArgumentOutOfRangeException(nameof(dpi));
|
||||
|
||||
var progId = Type.GetTypeFromProgID("WIA.DeviceManager")
|
||||
?? throw new InvalidOperationException("This machine has no imaging service.");
|
||||
|
||||
string folder = Path.Combine(Path.GetTempPath(), "MmdPdf.Scan." + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
var pages = new List<string>();
|
||||
object? manager = null;
|
||||
object? device = null;
|
||||
try
|
||||
{
|
||||
manager = Activator.CreateInstance(progId);
|
||||
dynamic dm = manager!;
|
||||
dynamic infos = dm.DeviceInfos;
|
||||
|
||||
dynamic? chosen = null;
|
||||
int count = (int)infos.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic info = infos[i];
|
||||
if (string.Equals((string)info.DeviceID, deviceId, StringComparison.Ordinal)) { chosen = info; break; }
|
||||
Release(info);
|
||||
}
|
||||
if (chosen == null) throw new InvalidOperationException("That copier is no longer reachable.");
|
||||
|
||||
device = chosen.Connect();
|
||||
dynamic dev = device!;
|
||||
ConfigureFeeder(dev, feed, maxPages);
|
||||
|
||||
dynamic item = dev.Items[1];
|
||||
try
|
||||
{
|
||||
ConfigurePage(item, colour, dpi, paper);
|
||||
|
||||
for (int page = 1; page <= maxPages; page++)
|
||||
{
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
string path = Path.Combine(folder, "page-" + page.ToString("000", CultureInfo.InvariantCulture) + ".png");
|
||||
try
|
||||
{
|
||||
dynamic image = item.Transfer(FORMAT_PNG);
|
||||
try
|
||||
{
|
||||
image.SaveFile(path);
|
||||
pages.Add(path);
|
||||
onPage?.Invoke(pages.Count);
|
||||
}
|
||||
finally { Release(image); }
|
||||
}
|
||||
catch (COMException ex) when (IsOutOfPaper(ex))
|
||||
{
|
||||
break; // the feeder emptied: a normal end, not a failure
|
||||
}
|
||||
|
||||
if (feed == ScanFeed.Flatbed) break; // one sheet on the glass
|
||||
if (!FeederHasPaper(dev)) break;
|
||||
}
|
||||
}
|
||||
finally { Release(item); }
|
||||
}
|
||||
finally
|
||||
{
|
||||
Release(device);
|
||||
Release(manager);
|
||||
}
|
||||
|
||||
if (pages.Count == 0)
|
||||
{
|
||||
try { Directory.Delete(folder, true); } catch (IOException) { }
|
||||
catch (UnauthorizedAccessException) { }
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
private static void ConfigureFeeder(dynamic device, ScanFeed feed, int maxPages)
|
||||
{
|
||||
int handling = feed == ScanFeed.Flatbed ? FLATBED : FEEDER;
|
||||
if (feed == ScanFeed.FeederBothSides) handling |= DUPLEX;
|
||||
|
||||
TrySetProperty(device.Properties, WIA_DPS_DOCUMENT_HANDLING_SELECT, handling);
|
||||
if (feed != ScanFeed.Flatbed) TrySetProperty(device.Properties, WIA_DPS_PAGES, maxPages);
|
||||
}
|
||||
|
||||
private static void ConfigurePage(dynamic item, ScanColour colour, int dpi, string paper)
|
||||
{
|
||||
int intent = colour switch
|
||||
{
|
||||
ScanColour.Colour => INTENT_COLOUR,
|
||||
ScanColour.Greyscale => INTENT_GREYSCALE,
|
||||
_ => INTENT_TEXT,
|
||||
};
|
||||
int depth = colour switch
|
||||
{
|
||||
ScanColour.Colour => 24,
|
||||
ScanColour.Greyscale => 8,
|
||||
_ => 1,
|
||||
};
|
||||
int dataType = colour switch
|
||||
{
|
||||
ScanColour.Colour => 3, // WIA_DATA_COLOR
|
||||
ScanColour.Greyscale => 2, // WIA_DATA_GRAYSCALE
|
||||
_ => 0, // WIA_DATA_THRESHOLD
|
||||
};
|
||||
|
||||
TrySetProperty(item.Properties, WIA_IPS_CUR_INTENT, intent);
|
||||
TrySetProperty(item.Properties, WIA_IPA_DATATYPE, dataType);
|
||||
TrySetProperty(item.Properties, WIA_IPA_DEPTH, depth);
|
||||
TrySetProperty(item.Properties, WIA_IPS_XRES, dpi);
|
||||
TrySetProperty(item.Properties, WIA_IPS_YRES, dpi);
|
||||
|
||||
var size = PaperInches.TryGetValue(paper ?? string.Empty, out var found) ? found : PaperInches["A4 portrait"];
|
||||
TrySetProperty(item.Properties, WIA_IPS_XPOS, 0);
|
||||
TrySetProperty(item.Properties, WIA_IPS_YPOS, 0);
|
||||
TrySetProperty(item.Properties, WIA_IPS_XEXTENT, (int)Math.Round(size.W * dpi));
|
||||
TrySetProperty(item.Properties, WIA_IPS_YEXTENT, (int)Math.Round(size.H * dpi));
|
||||
}
|
||||
|
||||
private static bool FeederHasPaper(dynamic device)
|
||||
{
|
||||
try
|
||||
{
|
||||
object? raw = ReadPropertyValue(device.Properties, WIA_DPS_DOCUMENT_HANDLING_STATUS);
|
||||
if (raw == null) return false;
|
||||
return ((int)Convert.ToInt32(raw, CultureInfo.InvariantCulture) & FEED_READY) == FEED_READY;
|
||||
}
|
||||
catch (COMException) { return false; }
|
||||
catch (InvalidCastException) { return false; }
|
||||
}
|
||||
|
||||
/// <summary>The copier saying "the tray is empty", which ends a run rather than failing it.</summary>
|
||||
private static bool IsOutOfPaper(COMException ex)
|
||||
{
|
||||
const int WIA_ERROR_PAPER_EMPTY = unchecked((int)0x80210003);
|
||||
const int WIA_ERROR_PAPER_JAM = unchecked((int)0x80210002);
|
||||
return ex.ErrorCode == WIA_ERROR_PAPER_EMPTY || ex.ErrorCode == WIA_ERROR_PAPER_JAM;
|
||||
}
|
||||
|
||||
private static string? ReadProperty(dynamic properties, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = (int)properties.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic p = properties[i];
|
||||
try
|
||||
{
|
||||
if (string.Equals((string)p.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
return Convert.ToString(p.get_Value(), CultureInfo.InvariantCulture);
|
||||
}
|
||||
finally { Release(p); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object? ReadPropertyValue(dynamic properties, int propertyId)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = (int)properties.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic p = properties[i];
|
||||
try
|
||||
{
|
||||
if ((int)p.PropertyID == propertyId) return p.get_Value();
|
||||
}
|
||||
finally { Release(p); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets one property if the copier has it. Copiers differ wildly in what they expose, so a
|
||||
/// missing or read-only property is ignored rather than failing the run - the copier then
|
||||
/// uses its own default for that setting.
|
||||
/// </summary>
|
||||
private static void TrySetProperty(dynamic properties, int propertyId, int value)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = (int)properties.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic p = properties[i];
|
||||
try
|
||||
{
|
||||
if ((int)p.PropertyID != propertyId) continue;
|
||||
if ((bool)p.IsReadOnly) return;
|
||||
p.set_Value(value);
|
||||
return;
|
||||
}
|
||||
finally { Release(p); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
}
|
||||
|
||||
private static void Release(object? comObject)
|
||||
{
|
||||
if (comObject == null) return;
|
||||
try
|
||||
{
|
||||
if (Marshal.IsComObject(comObject)) Marshal.ReleaseComObject(comObject);
|
||||
}
|
||||
catch (ArgumentException) { }
|
||||
catch (InvalidComObjectException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -110,7 +110,7 @@ namespace MmdPdf
|
||||
{
|
||||
AboutLogoBlock.Inlines.Clear();
|
||||
var hl = new Hyperlink { TextDecorations = null };
|
||||
hl.Inlines.Add(new Run("Killer")
|
||||
hl.Inlines.Add(new Run("MMD ")
|
||||
{
|
||||
FontFamily = UiKit.WordmarkFont,
|
||||
FontSize = 21,
|
||||
@@ -133,7 +133,7 @@ namespace MmdPdf
|
||||
var shadowBrush = new System.Windows.Media.SolidColorBrush(
|
||||
System.Windows.Media.Color.FromArgb(0xB0, 0, 0, 0));
|
||||
shadowBrush.Freeze();
|
||||
AboutLogoShadowBlock.Inlines.Add(new Run("Killer")
|
||||
AboutLogoShadowBlock.Inlines.Add(new Run("MMD ")
|
||||
{
|
||||
FontFamily = UiKit.WordmarkFont,
|
||||
FontSize = 21,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using MmdPdf.Controls;
|
||||
using MmdPdf.Features.Scan;
|
||||
|
||||
namespace MmdPdf
|
||||
{
|
||||
// Pulling pages off a copier. The dialog does the talking to the hardware; this file only
|
||||
// turns the pages it hands back into a document, which is the same route an imported
|
||||
// photograph takes (ImportAndZip.cs).
|
||||
public partial class MainWindow
|
||||
{
|
||||
private void Scan_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new ScanDialog { Owner = this };
|
||||
bool? answer;
|
||||
try
|
||||
{
|
||||
answer = dlg.ShowDialog();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
KillerDialog.Show(this, "The copier could not be reached.\n" + ex.Message,
|
||||
"MMD PDF", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (answer != true || dlg.Pages.Length == 0) return;
|
||||
|
||||
string name = "Scan " + DateTime.Now.ToString("d MMM yyyy HH:mm", CultureInfo.CurrentCulture);
|
||||
OpenImagesAsImportedTab(dlg.Pages, name);
|
||||
|
||||
// "Make the words searchable" is the copier setting a person ticks before the run;
|
||||
// the reading itself happens here, on the document that just opened, through the
|
||||
// text recognition the application already carries.
|
||||
if (dlg.MakeSearchable) MakeSearchablePdf();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -520,6 +520,7 @@ namespace MmdPdf
|
||||
[""] = "Str_Lbl_Flatten",
|
||||
[""] = "Str_Lbl_Ocr",
|
||||
[""] = "Str_Lbl_Print",
|
||||
["\ue8fe"] = "Str_Lbl_Scan",
|
||||
[""] = "Str_Lbl_Merge",
|
||||
[""] = "Str_Lbl_Extract",
|
||||
[""] = "Str_Lbl_Delete",
|
||||
@@ -590,7 +591,7 @@ namespace MmdPdf
|
||||
// Text only: caption, no icon (nothing to shed - there'd be nothing left).
|
||||
if (textOnly)
|
||||
{
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 34; btn.Padding = new Thickness(8, 5, 8, 5);
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 27; btn.Padding = new Thickness(8, 3, 8, 3);
|
||||
btn.Content = new TextBlock
|
||||
{
|
||||
Text = Loc(key),
|
||||
@@ -604,7 +605,7 @@ namespace MmdPdf
|
||||
// Text beside the icon, while it still fits.
|
||||
if (beside && withLabel)
|
||||
{
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 34; btn.Padding = new Thickness(8, 5, 8, 5);
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 27; btn.Padding = new Thickness(8, 3, 8, 3);
|
||||
var row = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
@@ -628,7 +629,7 @@ namespace MmdPdf
|
||||
// Text under the icon: the icon stacked over a small caption, while it still fits.
|
||||
if (under && withLabel)
|
||||
{
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = large ? 56 : 52; btn.Padding = new Thickness(6, 4, 6, 4);
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = large ? 45 : 42; btn.Padding = new Thickness(6, 2, 6, 2);
|
||||
var col = new StackPanel { Orientation = Orientation.Vertical, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
col.Children.Add(new TextBlock
|
||||
{
|
||||
|
||||
+1
-1
@@ -671,7 +671,7 @@ Windows সেটিংস - ডিফল্ট অ্যাপ খুলবে
|
||||
<sys:String x:Key="Str_Key_WheelLogo">লোগোর উপর হুইল</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">মাঝের বোতামে টানুন</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">স্পেস + টানুন</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">খুলুন</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">পরিবর্তন</sys:String>
|
||||
|
||||
+1
-1
@@ -676,7 +676,7 @@ Změnit jejich velikost na podporovanou? Stránky si zachovají vzhled i proporc
|
||||
<sys:String x:Key="Str_Key_WheelLogo">Kolečko nad logem</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">Tažení prostředním tlačítkem</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">Mezerník + tažení</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">Otevřít</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">Nahradit</sys:String>
|
||||
|
||||
+1
-1
@@ -677,7 +677,7 @@ Auf eine unterstützte Größe skalieren? Aussehen und Proportionen der Seiten b
|
||||
<sys:String x:Key="Str_Key_WheelLogo">Rad über Logo</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">Mittlere Taste ziehen</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">Leertaste + ziehen</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">Öffnen</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">Ersetzen</sys:String>
|
||||
|
||||
+3
-1
@@ -19,6 +19,7 @@
|
||||
<sys:String x:Key="Str_TT_SaveAs">Save As (Ctrl+Shift+S)</sys:String>
|
||||
<sys:String x:Key="Str_TT_SaveFlattened">Save Flattened PDF (rasterize all pages - fully uneditable)</sys:String>
|
||||
<sys:String x:Key="Str_TT_Print">Print (Ctrl+P)</sys:String>
|
||||
<sys:String x:Key="Str_TT_Scan">Pull pages off a copier</sys:String>
|
||||
<sys:String x:Key="Str_TT_MergePDFs">Merge PDFs (combine additional PDFs into this one)</sys:String>
|
||||
<sys:String x:Key="Str_TT_ExtractPages">Extract Pages (save selected pages to a new PDF)</sys:String>
|
||||
<sys:String x:Key="Str_TT_DeletePages">Delete Selected Pages</sys:String>
|
||||
@@ -297,6 +298,7 @@ Scale them to a supported size? The pages keep their look and proportions.</sys:
|
||||
<sys:String x:Key="Str_Lbl_Save">Save</sys:String>
|
||||
<sys:String x:Key="Str_Lbl_Flatten">Flatten</sys:String>
|
||||
<sys:String x:Key="Str_Lbl_Print">Print</sys:String>
|
||||
<sys:String x:Key="Str_Lbl_Scan">Scan</sys:String>
|
||||
<sys:String x:Key="Str_Lbl_Select">Select</sys:String>
|
||||
<sys:String x:Key="Str_Lbl_ZoomOut">Zoom Out</sys:String>
|
||||
<sys:String x:Key="Str_Lbl_ZoomIn">Zoom In</sys:String>
|
||||
@@ -694,7 +696,7 @@ Scale them to a supported size? The pages keep their look and proportions.</sys:
|
||||
<sys:String x:Key="Str_Key_WheelLogo">Wheel on logo</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">Middle drag</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">Space + drag</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">Open</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">Replace</sys:String>
|
||||
|
||||
+1
-1
@@ -671,7 +671,7 @@ Abre Configuración de Windows - Aplicaciones predeterminadas.</sys:String>
|
||||
<sys:String x:Key="Str_Key_WheelLogo">Rueda sobre el logotipo</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">Arrastrar con el botón central</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">Espacio + arrastrar</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">Abrir</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">Reemplazar</sys:String>
|
||||
|
||||
+1
-1
@@ -687,7 +687,7 @@ Les redimensionner à une taille prise en charge ? Les pages conservent leur app
|
||||
<sys:String x:Key="Str_Key_WheelLogo">Molette sur le logo</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">Glisser avec le bouton du milieu</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">Espace + glisser</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">Ouvrir</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">Remplacer</sys:String>
|
||||
|
||||
+1
-1
@@ -692,7 +692,7 @@ Megnyílik a Windows Beállítások - Alapértelmezett alkalmazások.</sys:Strin
|
||||
<sys:String x:Key="Str_Key_WheelLogo">Görgő a logó felett</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">Húzás középső gombbal</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">Szóköz + húzás</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">Megnyitás</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">Csere</sys:String>
|
||||
|
||||
+1
-1
@@ -676,7 +676,7 @@ Windows の設定 - 既定のアプリ を開きます。</sys:String>
|
||||
<sys:String x:Key="Str_Key_WheelLogo">ロゴ上でホイール</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">中ボタンでドラッグ</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">Space + ドラッグ</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">開く</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">置き換え</sys:String>
|
||||
|
||||
+1
-1
@@ -669,7 +669,7 @@ Przeskalować je do obsługiwanego rozmiaru? Strony zachowują swój wygląd i p
|
||||
<sys:String x:Key="Str_Key_WheelLogo">Kółko nad logo</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">Przeciąganie środkowym przyciskiem</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">Spacja + przeciąganie</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">Otwórz</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">Zamień</sys:String>
|
||||
|
||||
+1
-1
@@ -671,7 +671,7 @@ Desteklenen bir boyuta ölçeklensin mi? Sayfaların görünümü ve oranları k
|
||||
<sys:String x:Key="Str_Key_WheelLogo">Logo üzerinde tekerlek</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">Orta tuşla sürükleme</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">Boşluk + sürükleme</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">Aç</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">Değiştir</sys:String>
|
||||
|
||||
+1
-1
@@ -675,7 +675,7 @@ MMD PDF 不会创建两个已安装的副本。</sys:String>
|
||||
<sys:String x:Key="Str_Key_WheelLogo">在徽标上滚动滚轮</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">中键拖动</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">空格 + 拖动</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">打开</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">替换</sys:String>
|
||||
|
||||
+1
-1
@@ -673,7 +673,7 @@ MMD PDF 不會建立兩個已安裝的複本。</sys:String>
|
||||
<sys:String x:Key="Str_Key_WheelLogo">在標誌上滾動滾輪</sys:String>
|
||||
<sys:String x:Key="Str_Key_MiddleDrag">中鍵拖曳</sys:String>
|
||||
<sys:String x:Key="Str_Key_SpaceDrag">空白鍵 + 拖曳</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">Killer</sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_A">MMD </sys:String>
|
||||
<sys:String x:Key="Str_Wordmark_B">PDF</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Open">開啟</sys:String>
|
||||
<sys:String x:Key="Str_Btn_Replace">取代</sys:String>
|
||||
|
||||
+45
-7
@@ -10,6 +10,41 @@
|
||||
Square by default. Held for Ben's sign-off: dark table header rows and the
|
||||
window-chrome variant (V2 vs V7) are still being chosen. -->
|
||||
|
||||
<!-- The one merged top bar, locked 2026-08-28. MMD red, the white lockup on it,
|
||||
and the window buttons at its right end. These are the design system's own
|
||||
values; no colour here is new. -->
|
||||
<LinearGradientBrush x:Key="TitleBarBrush" StartPoint="0,0" EndPoint="0,1" po:Freeze="True">
|
||||
<GradientStop Offset="0.0" Color="#d4213f"/>
|
||||
<GradientStop Offset="0.55" Color="#c8102e"/>
|
||||
<GradientStop Offset="1.0" Color="#a50d26"/>
|
||||
</LinearGradientBrush>
|
||||
<sys:Double x:Key="TitleBarHeight">42</sys:Double>
|
||||
<Thickness x:Key="TitleBarPadding">14,0,0,0</Thickness>
|
||||
<SolidColorBrush x:Key="CaptionGlyphBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<sys:Double x:Key="CaptionButtonWidth">24</sys:Double>
|
||||
<sys:Double x:Key="CaptionButtonHeight">24</sys:Double>
|
||||
<Thickness x:Key="CaptionButtonsMargin">0,0,9,0</Thickness>
|
||||
<!-- The window buttons sit on the red band, so they are white. The close button
|
||||
inverts on hover - white plate, red cross - because red on red is invisible,
|
||||
which is exactly what the first build of this band showed. -->
|
||||
<SolidColorBrush x:Key="CaptionButtonBrush" Color="#1fffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionHoverBrush" Color="#33ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionCloseBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionCloseHoverBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionCloseHoverFgBrush" Color="#c8102e" po:Freeze="True"/>
|
||||
<sys:Double x:Key="WordmarkShadowOpacity">0</sys:Double>
|
||||
<sys:Double x:Key="TitleIconSize">0</sys:Double>
|
||||
<Thickness x:Key="TitleIconMargin">0</Thickness>
|
||||
|
||||
<!-- A window that is in front, for the dialogs: white to silver with a red top edge.
|
||||
The bevel lives on title bars only; buttons are flat. -->
|
||||
<LinearGradientBrush x:Key="WindowFrontTitleBrush" StartPoint="0,0" EndPoint="0,1" po:Freeze="True">
|
||||
<GradientStop Offset="0.0" Color="#ffffff"/>
|
||||
<GradientStop Offset="0.62" Color="#ffffff"/>
|
||||
<GradientStop Offset="1.0" Color="#f1f1ec"/>
|
||||
</LinearGradientBrush>
|
||||
<sys:Double x:Key="FooterHeight">26</sys:Double>
|
||||
|
||||
<!-- Surfaces -->
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="#f1f1ec" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="PaneBrush" Color="#e4e3dc" po:Freeze="True"/>
|
||||
@@ -54,8 +89,8 @@
|
||||
<SolidColorBrush x:Key="TextBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="MutedTextBrush" Color="#3c3a34" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="MenuTextBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="ChromeTextBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="FilenameBrush" Color="#3c3a34" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="ChromeTextBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="FilenameBrush" Color="#e8ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DimTextBrush" Color="#767368" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="TextFooter" Color="#6b6a64" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DangerRed" Color="#c8102e" po:Freeze="True"/>
|
||||
@@ -99,10 +134,13 @@
|
||||
<!-- No text stroke on a light corporate surface. -->
|
||||
<DropShadowEffect x:Key="TextStroke" Color="Black" BlurRadius="0" ShadowDepth="0" Direction="270" Opacity="0"/>
|
||||
|
||||
<!-- Square. The portal's chrome is square-edged; rounded corners are not MMD. -->
|
||||
<CornerRadius x:Key="ControlCornerRadius">0</CornerRadius>
|
||||
<CornerRadius x:Key="SmallCornerRadius">0</CornerRadius>
|
||||
<CornerRadius x:Key="FlyoutCornerRadius">0</CornerRadius>
|
||||
<CornerRadius x:Key="AccentSwatchCornerRadius">0</CornerRadius>
|
||||
<!-- Four-pixel corners, per the design system locked 2026-08-28. The window itself
|
||||
stays square at the top because the band runs edge to edge when maximised, which
|
||||
is how these machines are used. -->
|
||||
<CornerRadius x:Key="ControlCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="SmallCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="FlyoutCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="AccentSwatchCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="TitleBarCornerRadius">0</CornerRadius>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
+14
-6
@@ -11,20 +11,28 @@ Status: 🟢 proven end-to-end on a real build with evidence · 🟡 in progress
|
||||
| PDF-04 | Save over the original, with confirmation | 1.7.5 | kua-agent | 🟢 | unsigned | SCREENS Save · TC-04 |
|
||||
| PDF-05 | Runs with no admin rights, portable | 1.7.5 | kua-agent | 🟢 | unsigned | PRD req.4 · TC-05 |
|
||||
| PDF-06 | Leaves the Windows default-PDF setting alone | 1.7.5 | kua-agent | 🟢 | unsigned | PRD "why" · TC-06 |
|
||||
| PDF-07 | MMD name, icon and word-mark | 0.1 | kua-agent | 🟡 | unsigned | SCREENS all · TC-07 |
|
||||
| PDF-08 | MMD shell design system, light only, no theme picker | 0.1 | kua-agent | ⛔ | unsigned | PRD req.3 · SCREENS · TC-08 |
|
||||
| PDF-09 | Works at 800×600 | 0.1 | — | ⛔ | unsigned | PRD req.2 · SCREENS Directions · TC-09 |
|
||||
| PDF-07 | MMD name, icon and word-mark | 2026.8.1 | kua-agent | 🟢 | unsigned | SCREENS all · TC-07 |
|
||||
| PDF-08 | MMD shell design system, light only, no theme picker | 2026.8.1 | kua-agent | 🟢 | unsigned | PRD req.3 · SCREENS · TC-08 |
|
||||
| PDF-09 | Works at 800×600 | 2026.8.1 | kua-agent | 🟢 | unsigned | PRD req.2 · SCREENS Directions · TC-09 |
|
||||
| PDF-10 | Fill and sign forms | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-10 |
|
||||
| PDF-11 | Mark up: highlight, draw, shapes, stamps, images | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-11 |
|
||||
| PDF-12 | Page work: merge, extract, delete, reorder, crop | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-12 |
|
||||
| PDF-13 | Turn a scan into searchable text | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-13 |
|
||||
| PDF-14 | Pull pages off a network copier | — | — | ⚪ | unsigned | PRD in-scope · SCREENS Scan · TC-14 |
|
||||
| PDF-14 | Pull pages off a network copier | 2026.8.1 | kua-agent | 🟡 | unsigned | PRD in-scope · SCREENS Scan · TC-14 |
|
||||
| PDF-15 | French interface | 1.7.5 | — | ⚪ | unsigned | PRD req.5 · TC-15 |
|
||||
| PDF-16 | Print | 1.7.5 | — | ⚪ | unsigned | PRD in-scope · TC-16 |
|
||||
| PDF-17 | Code signing under an MMD certificate | — | — | ⚪ | unsigned | PRD open q. · TC-17 |
|
||||
| PDF-18 | Deploy through Intune | — | — | ⚪ | unsigned | PRD open q. · TC-18 |
|
||||
| PDF-17 | Code signing under an MMD certificate | 2026.8.1 | kua-agent | 🟢 | unsigned | PRD open q. · TC-17 |
|
||||
| PDF-18 | Deploy through Intune | 2026.8.1 | kua-agent | 🟢 | unsigned | PRD open q. · TC-18 |
|
||||
| PDF-19 | Official square app mark for 16–24px | — | — | ⛔ | unsigned | PRD open q. |
|
||||
|
||||
## Why PDF-14 is still 🟡
|
||||
|
||||
The copier conversation is written and the dialog is proven — it opens, it lists
|
||||
what it finds, and with nothing attached it says "No copier found on this
|
||||
machine" and refuses to pretend. **The page transfer itself has never met real
|
||||
hardware**, because the test machine has no scanner. That is the one thing left
|
||||
before it is green.
|
||||
|
||||
## Why the ⛔
|
||||
|
||||
- **PDF-08, PDF-09** — the design failed an independent review on 2026-08-27
|
||||
|
||||
@@ -11,7 +11,31 @@ reviewer's PASS are all evidence — none of them is a signature.
|
||||
| 2026-08-27 | PDF-03 | 0.1 | `12 tonnes` no longer recoverable from the saved file; `19 tonnes` present; other lines intact | kua-agent | `pdftotext` before/after on the VM |
|
||||
| 2026-08-27 | PDF-06 | 1.7.5 | `.pdf` UserChoice ProgId + Hash unchanged; deny ACE intact; no helper binary | kua-agent | registry read before and after install and first open |
|
||||
| 2026-08-27 | PDF-07…09 | 0.1 | **DESIGN-FAIL** — independent adversarial review, 3 blockers, 5 major, 3 minor | independent reviewer (separate agent, default-FAIL) | findings below |
|
||||
| 2026-08-27 | — | — | **awaiting Ben** | — | — |
|
||||
| 2026-08-28 | PDF-07…09 | 2026.8.1 | rebuilt on the MMD design system Ben locked 02:25 UTC: one merged red bar, flat buttons, 4px corners, 24px window buttons, colours untouched | kua-agent | keyboard and mouse on mmd-win-test-01 at 800x600 |
|
||||
| 2026-08-28 | PDF-14 | 2026.8.1 | Scan reaches a copier through WIA; dialog and the no-copier case proven, the page transfer is NOT — no scanner on the test machine | kua-agent | dialog opened by mouse; reports "No copier found on this machine" |
|
||||
| 2026-08-28 | PDF-17 | 2026.8.1 | signed by CN=MMD Steel Group IT from mmd-fsn-01, DigiCert timestamp, Windows reports Valid | kua-agent | Get-AuthenticodeSignature on the guest |
|
||||
| 2026-08-28 | PDF-18 | 2026.8.1 | published to Intune, offered to MMD-EUC-Pilot as available | kua-agent | committedContentVersion + assignment read back from Graph |
|
||||
| 2026-08-28 | PDF-07 | 2026.8.1 | **three defects Ben found by looking at it** — fixed, see below | Ben | his own look at the running application |
|
||||
| 2026-08-28 | — | — | **awaiting Ben** | — | — |
|
||||
|
||||
## What Ben found on 2026-08-28, and what was done
|
||||
|
||||
He looked at the running application and read it as still being the tool it was
|
||||
forked from. Three things, all real:
|
||||
|
||||
1. **The upstream manual was what he saw first.** It sat in the repository root
|
||||
as `MmdPdf.pdf` and on the test machine at `C:\src`. It was never in the
|
||||
shipped payload — but the application restores the previous session, so once
|
||||
it had been opened it came back on every start, a full-page upstream logo on
|
||||
black. Removed and gitignored. *Not shipped is not the same as not on screen.*
|
||||
It was visible in this project's own screenshots the day before and was noted
|
||||
rather than fixed; that is why it reached him.
|
||||
2. **Scan was in the window chrome**, beside minimise, maximise and close. It is
|
||||
a toolbar button now, next to Print, captioned like every other tool.
|
||||
3. **The footer read v1.7.5**, the upstream project's number. The application is
|
||||
**2026.8.1**. Not 1.0: the pilot machines already carry a build numbered
|
||||
1.7.5, and an upgrade has to sort above what it replaces or Intune reads it
|
||||
as a downgrade.
|
||||
|
||||
## The design gate — OPEN
|
||||
|
||||
|
||||
Reference in New Issue
Block a user