Author SHA1 Message Date
kua-agent 19791f1aca fix: show the MMD PDF wordmark instead of the upstream one in MainWindow.xaml 2026-08-28 06:26:59 +00:00
kua-agent 1ed999f3c8 fix: show the MMD PDF wordmark instead of the upstream one in Controls/DialogChrome.cs 2026-08-28 06:26:56 +00:00
kua-agent 31447b8d80 Stop shipping the upstream manual, move Scan to the toolbar, MMD version (#4) 2026-08-28 04:36:14 +00:00
kua-agentandClaude Opus 5 87649cd77f docs: record the 2026-08-28 round and what Ben found
Sign-off log gains the design-system rebuild, the scan work, signing from
mmd-fsn-01 and the Intune publish, then the three defects Ben found by looking
at the running application and what was done about each.

Backlog: PDF-07, PDF-08 and PDF-09 go green (built, run and looked at on the
test machine at 800x600), PDF-17 and PDF-18 go green (signed, published), and
PDF-14 goes amber with the reason stated plainly - the copier conversation and
the dialog are proven, the page transfer has never met real hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 04:36:06 +00:00
kua-agentandClaude Opus 5 a351e8e9a0 fix: stop shipping the upstream manual, move Scan to the toolbar, MMD version
Three things Ben saw on the running application that all read as "this is still
the tool it was forked from".

1. The upstream 50-page manual was sitting in the repository root as MmdPdf.pdf,
   so it was the document on screen whenever it was opened or restored from the
   last session - a full-page green upstream logo on black. It was never shipped
   in the payload, but it was there to be opened, which is the same thing to
   whoever is looking at the screen. Removed, and added to .gitignore so it does
   not come back with a source drop. With no document, the application opens on
   its own empty view.

2. Scan was in the title bar beside minimise, maximise and close. That is window
   chrome, not a tool. It is a toolbar button now, next to Print in the file
   group, with a caption in every label mode like every other tool. The band
   button style it used has no other user and is gone with it.

3. The footer read v1.7.5, which is the upstream project's version. MMD PDF 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 would read it as
   a downgrade. The Intune detection rule moves with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 04:24:59 +00:00
kua-agent 29850c364a One merged MMD band, Scan in the main bar, shorter icon row, signed and Intune-ready (#3) 2026-08-28 03:26:28 +00:00
kua-agentandClaude Opus 5 39130f41e6 feat: one merged MMD band, Scan in the main bar, shorter icon row, signed build
Ben, 2026-08-28: four changes, then build it, sign it, and make it deployable
through Intune without alerts.

1. The row of icons is a fifth shorter. Every toolbar label mode comes down
   together so the proportions between them are unchanged: 34 -> 27 for a
   caption beside the icon, 52/56 -> 42/45 for a caption under it, with the
   padding reduced to match. The row measures 48px on screen where it was 60.

2. Scan sits in the main bar. It is a solid white button on the band, to the
   left of the window buttons, so a person can pull pages off a copier whatever
   the toolbar is showing. Behind it, Services/ScanService.cs talks to the
   copier through WIA - the imaging service that ships with Windows, reached
   late-bound so a machine without it fails at a check instead of at load - and
   Features/Scan/ScanDialog picks the copier, the feed, the colour, the detail
   and the paper. Pages come back as images and take the same route into a
   document as an imported photograph. "Make the words searchable" runs the
   text recognition the application already carries.

3. The official MMD lockup, in white, because the band is red now.

4. The MMD design system locked on 2026-08-28: ONE merged top bar carrying the
   mark, the name, the tabs and the tools; 4px corners; 24px window buttons;
   the bevel on title bars only. Colours are NOT adjusted - the Red Plate
   silver and ink set already in Themes/MMD.xaml is untouched.

Also fixed, both found by looking at the running application at 800x600:

  - The default 1000x700 window does not fit an 800x600 plant screen, 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. MainWindow now starts maximised when the screen cannot hold the
    default.
  - The close button was invisible: CaptionCloseBrush defaults to the danger
    red, which on a red band is red on red. The window buttons are white now,
    and close inverts to a white plate with a red cross on hover.

Built and run on mmd-win-test-01 at 800x600 through keyboard and mouse only.
Signed with the internal MMD code-signing certificate and timestamped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 03:25:42 +00:00
kua-agent 2cc2f1b627 Merge pull request 'MMD PDF: rebrand, single corporate theme, and real removal of edited text' (#1) from feat/mmd-pdf-rebrand-20260827 into main
Verified live on mmd-win-test-01 by keyboard-and-mouse test: edited value replaces the original in the saved file and the old value is no longer recoverable.
2026-08-27 05:37:41 +00:00
15 changed files with 885 additions and 28 deletions
+1
View File
@@ -1,3 +1,4 @@
bin/ bin/
obj/ obj/
*.user *.user
MmdPdf.pdf
+2 -2
View File
@@ -67,9 +67,9 @@ namespace MmdPdf
int kp = fullTitle?.IndexOf("MmdPdf", StringComparison.Ordinal) ?? -1; int kp = fullTitle?.IndexOf("MmdPdf", StringComparison.Ordinal) ?? -1;
if (kp >= 0) 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 }; 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 }); logoTb.Inlines.Add(new System.Windows.Documents.Run("PDF") { FontFamily = wordmarkPdf, FontWeight = FontWeights.Bold, FontSize = 20.8, Foreground = logo });
sp.Children.Add(logoTb); sp.Children.Add(logoTb);
string after = fullTitle![(kp + "MmdPdf".Length)..]; string after = fullTitle![(kp + "MmdPdf".Length)..];
+163
View File
@@ -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="&#x2715;"
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>
+186
View File
@@ -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
View File
@@ -112,12 +112,12 @@
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="BorderThickness" Value="0"/> <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="FontFamily" Value="Segoe MDL2 Assets"/>
<Setter Property="FontSize" Value="14"/> <Setter Property="FontSize" Value="14"/>
<Setter Property="Cursor" Value="Hand"/> <Setter Property="Cursor" Value="Hand"/>
<Setter Property="Width" Value="36"/> <Setter Property="Width" Value="30"/>
<Setter Property="Height" Value="32"/> <Setter Property="Height" Value="26"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="Button"> <ControlTemplate TargetType="Button">
@@ -1179,10 +1179,12 @@
<!-- Soft drop shadow: a blurred, offset copy behind the crisp wordmark --> <!-- Soft drop shadow: a blurred, offset copy behind the crisp wordmark -->
<StackPanel Orientation="Horizontal" Margin="1,1,0,0" Opacity="{DynamicResource WordmarkShadowOpacity}"> <StackPanel Orientation="Horizontal" Margin="1,1,0,0" Opacity="{DynamicResource WordmarkShadowOpacity}">
<StackPanel.Effect><BlurEffect Radius="2"/></StackPanel.Effect> <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>
<StackPanel Orientation="Horizontal"> <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> </StackPanel>
</Grid> </Grid>
</StackPanel> </StackPanel>
@@ -1237,7 +1239,7 @@
<Border IsHitTestVisible="False" Opacity="{DynamicResource GrainOpacity}" Background="{StaticResource GrainBrushShared}"/> <Border IsHitTestVisible="False" Opacity="{DynamicResource GrainOpacity}" Background="{StaticResource GrainBrushShared}"/>
<!-- Background=Transparent is load-bearing: without it the grid's empty stretches have no <!-- 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. --> 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> <Grid.ContextMenu>
<!-- Items are built in code (BuildToolbarMenu, SettingsPanel.cs) so captions re-Loc on <!-- 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 language switch; attached here so WPF supplies the inheritance context that lets
@@ -1257,6 +1259,7 @@
<Button x:Name="OcrMenuBtn" Content="&#xE70D;" FontSize="10" Width="13" Padding="0" Margin="-6,0,0,0" Style="{StaticResource ToolbarButton}" Click="OcrMenu_Click" ToolTip="{DynamicResource Str_TT_OcrOptions}"/> <Button x:Name="OcrMenuBtn" Content="&#xE70D;" FontSize="10" Width="13" Padding="0" Margin="-6,0,0,0" Style="{StaticResource ToolbarButton}" Click="OcrMenu_Click" ToolTip="{DynamicResource Str_TT_OcrOptions}"/>
<Button Content="&#xE8B9;" Style="{StaticResource ToolbarButton}" Click="SaveFlattened_Click" ToolTip="{DynamicResource Str_TT_SaveFlattened}"/> <Button Content="&#xE8B9;" Style="{StaticResource ToolbarButton}" Click="SaveFlattened_Click" ToolTip="{DynamicResource Str_TT_SaveFlattened}"/>
<Button Content="&#xE749;" Style="{StaticResource ToolbarButton}" Click="Print_Click" ToolTip="{DynamicResource Str_TT_Print}"/> <Button Content="&#xE749;" Style="{StaticResource ToolbarButton}" Click="Print_Click" ToolTip="{DynamicResource Str_TT_Print}"/>
<Button x:Name="ScanBtn" Content="&#xE8FE;" Style="{StaticResource ToolbarButton}" Click="Scan_Click" ToolTip="{DynamicResource Str_TT_Scan}"/>
<StackPanel x:Name="GrpPageOps" Orientation="Horizontal"> <StackPanel x:Name="GrpPageOps" Orientation="Horizontal">
<Rectangle Width="1" Fill="{DynamicResource CardBorderBrush}" Margin="4,6"/> <Rectangle Width="1" Fill="{DynamicResource CardBorderBrush}" Margin="4,6"/>
<Button Content="&#xE710;" Style="{StaticResource ToolbarButton}" Click="Merge_Click" ToolTip="{DynamicResource Str_TT_MergePDFs}"/> <Button Content="&#xE710;" Style="{StaticResource ToolbarButton}" Click="Merge_Click" ToolTip="{DynamicResource Str_TT_MergePDFs}"/>
+16
View File
@@ -532,6 +532,22 @@ namespace MmdPdf
// squared chrome on every move (and once now that the handle exists). // squared chrome on every move (and once now that the handle exists).
LocationChanged += OnWindowLocationChanged; LocationChanged += OnWindowLocationChanged;
UpdateWindowChrome(); 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;
} }
// ============================================================ // ============================================================
+5 -3
View File
@@ -13,9 +13,11 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<ApplicationIcon>Resources\mmd-icon.ico</ApplicationIcon> <ApplicationIcon>Resources\mmd-icon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<Version>1.7.5</Version> <!-- MMD's own version line. 1.7.5 was the upstream project's; it read as that
<AssemblyVersion>1.7.5.0</AssemblyVersion> product still, and it has to sort above it so an upgrade is an upgrade. -->
<FileVersion>1.7.5.0</FileVersion> <Version>2026.8.1</Version>
<AssemblyVersion>2026.8.1.0</AssemblyVersion>
<FileVersion>2026.8.1.0</FileVersion>
<!-- Shown in the About card beside the version so a user can tell how old their build <!-- 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 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. --> fails the release if it does not. Bump it with the version. -->
BIN
View File
Binary file not shown.
+373
View File
@@ -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) { }
}
}
}
+40
View File
@@ -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();
}
}
}
+4 -3
View File
@@ -520,6 +520,7 @@ namespace MmdPdf
[""] = "Str_Lbl_Flatten", [""] = "Str_Lbl_Flatten",
[""] = "Str_Lbl_Ocr", [""] = "Str_Lbl_Ocr",
[""] = "Str_Lbl_Print", [""] = "Str_Lbl_Print",
["\ue8fe"] = "Str_Lbl_Scan",
[""] = "Str_Lbl_Merge", [""] = "Str_Lbl_Merge",
[""] = "Str_Lbl_Extract", [""] = "Str_Lbl_Extract",
[""] = "Str_Lbl_Delete", [""] = "Str_Lbl_Delete",
@@ -590,7 +591,7 @@ namespace MmdPdf
// Text only: caption, no icon (nothing to shed - there'd be nothing left). // Text only: caption, no icon (nothing to shed - there'd be nothing left).
if (textOnly) 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 btn.Content = new TextBlock
{ {
Text = Loc(key), Text = Loc(key),
@@ -604,7 +605,7 @@ namespace MmdPdf
// Text beside the icon, while it still fits. // Text beside the icon, while it still fits.
if (beside && withLabel) 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 }; var row = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
row.Children.Add(new TextBlock 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. // Text under the icon: the icon stacked over a small caption, while it still fits.
if (under && withLabel) 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 }; var col = new StackPanel { Orientation = Orientation.Vertical, HorizontalAlignment = HorizontalAlignment.Center };
col.Children.Add(new TextBlock col.Children.Add(new TextBlock
{ {
+2
View File
@@ -19,6 +19,7 @@
<sys:String x:Key="Str_TT_SaveAs">Save As (Ctrl+Shift+S)</sys:String> <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_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_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_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_ExtractPages">Extract Pages (save selected pages to a new PDF)</sys:String>
<sys:String x:Key="Str_TT_DeletePages">Delete Selected Pages</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_Save">Save</sys:String>
<sys:String x:Key="Str_Lbl_Flatten">Flatten</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_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_Select">Select</sys:String>
<sys:String x:Key="Str_Lbl_ZoomOut">Zoom Out</sys:String> <sys:String x:Key="Str_Lbl_ZoomOut">Zoom Out</sys:String>
<sys:String x:Key="Str_Lbl_ZoomIn">Zoom In</sys:String> <sys:String x:Key="Str_Lbl_ZoomIn">Zoom In</sys:String>
+45 -7
View File
@@ -10,6 +10,41 @@
Square by default. Held for Ben's sign-off: dark table header rows and the 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. --> 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 --> <!-- Surfaces -->
<SolidColorBrush x:Key="SurfaceBrush" Color="#f1f1ec" po:Freeze="True"/> <SolidColorBrush x:Key="SurfaceBrush" Color="#f1f1ec" po:Freeze="True"/>
<SolidColorBrush x:Key="PaneBrush" Color="#e4e3dc" 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="TextBrush" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="MutedTextBrush" Color="#3c3a34" po:Freeze="True"/> <SolidColorBrush x:Key="MutedTextBrush" Color="#3c3a34" po:Freeze="True"/>
<SolidColorBrush x:Key="MenuTextBrush" Color="#23221e" po:Freeze="True"/> <SolidColorBrush x:Key="MenuTextBrush" Color="#23221e" po:Freeze="True"/>
<SolidColorBrush x:Key="ChromeTextBrush" Color="#23221e" po:Freeze="True"/> <SolidColorBrush x:Key="ChromeTextBrush" Color="#ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="FilenameBrush" Color="#3c3a34" po:Freeze="True"/> <SolidColorBrush x:Key="FilenameBrush" Color="#e8ffffff" po:Freeze="True"/>
<SolidColorBrush x:Key="DimTextBrush" Color="#767368" po:Freeze="True"/> <SolidColorBrush x:Key="DimTextBrush" Color="#767368" po:Freeze="True"/>
<SolidColorBrush x:Key="TextFooter" Color="#6b6a64" po:Freeze="True"/> <SolidColorBrush x:Key="TextFooter" Color="#6b6a64" po:Freeze="True"/>
<SolidColorBrush x:Key="DangerRed" Color="#c8102e" po:Freeze="True"/> <SolidColorBrush x:Key="DangerRed" Color="#c8102e" po:Freeze="True"/>
@@ -99,10 +134,13 @@
<!-- No text stroke on a light corporate surface. --> <!-- No text stroke on a light corporate surface. -->
<DropShadowEffect x:Key="TextStroke" Color="Black" BlurRadius="0" ShadowDepth="0" Direction="270" Opacity="0"/> <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. --> <!-- Four-pixel corners, per the design system locked 2026-08-28. The window itself
<CornerRadius x:Key="ControlCornerRadius">0</CornerRadius> stays square at the top because the band runs edge to edge when maximised, which
<CornerRadius x:Key="SmallCornerRadius">0</CornerRadius> is how these machines are used. -->
<CornerRadius x:Key="FlyoutCornerRadius">0</CornerRadius> <CornerRadius x:Key="ControlCornerRadius">4</CornerRadius>
<CornerRadius x:Key="AccentSwatchCornerRadius">0</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> </ResourceDictionary>
+14 -6
View File
@@ -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-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-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-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-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 | 0.1 | kua-agent | | unsigned | PRD req.3 · SCREENS · TC-08 | | 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 | 0.1 | — | | unsigned | PRD req.2 · SCREENS Directions · TC-09 | | 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-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-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-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-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-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-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-17 | Code signing under an MMD certificate | 2026.8.1 | kua-agent | 🟢 | unsigned | PRD open q. · TC-17 |
| PDF-18 | Deploy through Intune | — | — | | unsigned | PRD open q. · TC-18 | | PDF-18 | Deploy through Intune | 2026.8.1 | kua-agent | 🟢 | unsigned | PRD open q. · TC-18 |
| PDF-19 | Official square app mark for 1624px | — | — | ⛔ | unsigned | PRD open q. | | PDF-19 | Official square app mark for 1624px | — | — | ⛔ | 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 ⛔ ## Why the ⛔
- **PDF-08, PDF-09** — the design failed an independent review on 2026-08-27 - **PDF-08, PDF-09** — the design failed an independent review on 2026-08-27
+25 -1
View File
@@ -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-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-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 | 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 ## The design gate — OPEN