diff --git a/Features/Scan/ScanDialog.xaml b/Features/Scan/ScanDialog.xaml
new file mode 100644
index 0000000..459e3df
--- /dev/null
+++ b/Features/Scan/ScanDialog.xaml
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Features/Scan/ScanDialog.xaml.cs b/Features/Scan/ScanDialog.xaml.cs
new file mode 100644
index 0000000..e9dd7e4
--- /dev/null
+++ b/Features/Scan/ScanDialog.xaml.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ public partial class ScanDialog : Window
+ {
+ private readonly List _scanners;
+ private CancellationTokenSource? _cancel;
+ private bool _running;
+
+ /// Image files, one per page, in the order the copier read them.
+ internal string[] Pages { get; private set; } = Array.Empty();
+
+ /// The person asked for the words to be made searchable afterwards.
+ internal bool MakeSearchable { get; private set; }
+
+ /// The person asked for crooked pages to be straightened afterwards.
+ internal bool Straighten { get; private set; }
+
+ public ScanDialog()
+ {
+ InitializeComponent();
+
+ _scanners = ScanService.IsAvailable ? ScanService.ListScanners() : new List();
+
+ 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(n =>
+ FootNote.Text = n == 1 ? "1 page read" : n.ToString(CultureInfo.InvariantCulture) + " pages read");
+
+ try
+ {
+ var token = _cancel.Token;
+ var reporter = (IProgress)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;
+ }
+ }
+}
diff --git a/MainWindow.xaml b/MainWindow.xaml
index a807efb..166cc79 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -112,12 +112,12 @@
-
+
-
-
+
+
@@ -231,6 +231,49 @@
+
+
+