vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace KillerPDF
|
||||
{
|
||||
public enum EditTool { Select, Text, Highlight, Strikethrough, Underline, Draw, Signature, Image, Crop, Line, Rotate, Shape }
|
||||
|
||||
/// <summary>Sub-mode of the Shapes tool (#127 Phase 3): drag a rectangle or ellipse, or click
|
||||
/// out a free-form polygon vertex by vertex.</summary>
|
||||
public enum ShapeKind { Rectangle, Ellipse, Polygon }
|
||||
|
||||
/// <summary>How a HighlightAnnotation paints over its bounds.</summary>
|
||||
public enum HighlightStyle { Fill, Strikethrough, Underline }
|
||||
|
||||
public abstract class PageAnnotation
|
||||
{
|
||||
public int PageIndex { get; set; }
|
||||
// Links a text-edit cover to its replacement text (same non-empty id on both). A cover with a
|
||||
// PairId renders dashed (it's "paired"); when the partner text is deleted the cover's PairId is
|
||||
// cleared and it renders as a solid box. Empty for everything else.
|
||||
public string PairId { get; set; } = "";
|
||||
|
||||
// Groups arbitrary annotations so they select and move together (same non-empty id on every
|
||||
// member). Independent of PairId. Empty when the annotation isn't grouped.
|
||||
public string GroupId { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for placed/resizable annotations (signature, image).
|
||||
/// Carries the shared position, scale, and source-dimension properties used by the resize handle.
|
||||
/// </summary>
|
||||
public abstract class PlacedAnnotation : PageAnnotation
|
||||
{
|
||||
public Point Position { get; set; }
|
||||
public double Scale { get; set; } = 0.5;
|
||||
public double SourceWidth { get; set; } = 400;
|
||||
public double SourceHeight { get; set; } = 150;
|
||||
|
||||
// Runtime-only cache of the decoded image (for image signatures / placed images). Held in
|
||||
// memory so a resize-drag doesn't re-decode the Base64 on every mouse tick. Not serialized;
|
||||
// the immutable ImageData stays the source of truth.
|
||||
public System.Windows.Media.Imaging.BitmapSource? CachedBitmap;
|
||||
}
|
||||
|
||||
public class TextAnnotation : PageAnnotation
|
||||
{
|
||||
public Point Position { get; set; }
|
||||
public string Content { get; set; } = "";
|
||||
public double FontSize { get; set; } = 14;
|
||||
// Typeface and style. FontName is a font-family name (any installed system font). Bold/Italic/Strike
|
||||
// apply to the whole box. Defaults keep text placed before these existed rendering as plain Segoe UI.
|
||||
public string FontName { get; set; } = "Segoe UI";
|
||||
public bool Bold { get; set; }
|
||||
public bool Italic { get; set; }
|
||||
public bool Strike { get; set; }
|
||||
public bool Underline { get; set; }
|
||||
public byte ColorR { get; set; } = 0;
|
||||
public byte ColorG { get; set; } = 0;
|
||||
public byte ColorB { get; set; } = 0;
|
||||
public byte ColorA { get; set; } = 255;
|
||||
|
||||
// Box geometry. Width is fixed (text wraps to it); Height auto-grows to fit the wrapped text.
|
||||
public double Width { get; set; } = 200;
|
||||
public double Height { get; set; } = 28;
|
||||
|
||||
// Optional background fill (the "whiteout"/highlight behind the text). BgA == 0 means no fill.
|
||||
public byte BgR { get; set; } = 255;
|
||||
public byte BgG { get; set; } = 255;
|
||||
public byte BgB { get; set; } = 255;
|
||||
public byte BgA { get; set; } = 0;
|
||||
|
||||
public Color GetColor() => Color.FromArgb(ColorA, ColorR, ColorG, ColorB);
|
||||
public void SetColor(Color c) { ColorR = c.R; ColorG = c.G; ColorB = c.B; ColorA = c.A; }
|
||||
|
||||
public Color GetFill() => Color.FromArgb(BgA, BgR, BgG, BgB);
|
||||
public void SetFill(Color c) { BgR = c.R; BgG = c.G; BgB = c.B; BgA = c.A; }
|
||||
public bool HasFill => BgA > 0;
|
||||
}
|
||||
|
||||
public class InkAnnotation : PageAnnotation
|
||||
{
|
||||
public List<Point> Points { get; set; } = [];
|
||||
public double StrokeWidth { get; set; } = 2;
|
||||
public byte ColorR { get; set; } = 255;
|
||||
public byte ColorG { get; set; } = 0;
|
||||
public byte ColorB { get; set; } = 0;
|
||||
public byte ColorA { get; set; } = 255;
|
||||
|
||||
public Color GetColor() => Color.FromArgb(ColorA, ColorR, ColorG, ColorB);
|
||||
public void SetColor(Color c) { ColorR = c.R; ColorG = c.G; ColorB = c.B; ColorA = c.A; }
|
||||
|
||||
// Shapes (#127 Phase 3): a non-zero alpha fills the region enclosed by the stroke (the
|
||||
// Shapes tool commits closed outlines - the last point repeats the first). Plain ink
|
||||
// strokes and lines leave FillA = 0 and render exactly as before.
|
||||
public byte FillR { get; set; }
|
||||
public byte FillG { get; set; }
|
||||
public byte FillB { get; set; }
|
||||
public byte FillA { get; set; }
|
||||
|
||||
public bool HasFill => FillA > 0;
|
||||
public Color GetFillColor() => Color.FromArgb(FillA, FillR, FillG, FillB);
|
||||
public void SetFillColor(Color c) { FillR = c.R; FillG = c.G; FillB = c.B; FillA = c.A; }
|
||||
}
|
||||
|
||||
// One brush-eraser pass over a highlight: a stroke (canvas-space points) of the given radius. The
|
||||
// highlight renders as its rectangle MINUS the union of these widened strokes - one anti-aliased
|
||||
// geometry, so the erased edges are smooth curves, not blocky steps or seamed strips.
|
||||
public sealed class HighlightErase
|
||||
{
|
||||
public System.Collections.Generic.List<Point> Points { get; set; } = [];
|
||||
public double Radius { get; set; }
|
||||
}
|
||||
|
||||
public class HighlightAnnotation : PageAnnotation
|
||||
{
|
||||
public Rect Bounds { get; set; }
|
||||
// Brush-eraser passes carved out of this highlight (null = untouched solid rect). Only Fill-style
|
||||
// highlights are ever carved.
|
||||
public System.Collections.Generic.List<HighlightErase>? Erases { get; set; }
|
||||
public HighlightStyle Style { get; set; } = HighlightStyle.Fill;
|
||||
public byte ColorR { get; set; } = 255;
|
||||
public byte ColorG { get; set; } = 255;
|
||||
public byte ColorB { get; set; } = 0;
|
||||
public byte ColorA { get; set; } = 80;
|
||||
|
||||
public Color GetColor() => Color.FromArgb(ColorA, ColorR, ColorG, ColorB);
|
||||
public virtual void SetColor(Color c) { ColorR = c.R; ColorG = c.G; ColorB = c.B; ColorA = c.A; }
|
||||
|
||||
/// <summary>
|
||||
/// The actual rectangle painted for this annotation. Fill uses the whole bounds;
|
||||
/// strikethrough is a thin band at the vertical center; underline sits at the bottom.
|
||||
/// </summary>
|
||||
public Rect DrawRect()
|
||||
{
|
||||
double t = Math.Max(2.0, Bounds.Height * 0.10);
|
||||
switch (Style)
|
||||
{
|
||||
case HighlightStyle.Strikethrough:
|
||||
return new Rect(Bounds.X, Bounds.Y + Bounds.Height / 2 - t / 2, Bounds.Width, t);
|
||||
case HighlightStyle.Underline:
|
||||
return new Rect(Bounds.X, Bounds.Y + Bounds.Height - t, Bounds.Width, t);
|
||||
default:
|
||||
return Bounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An opaque filled rectangle that covers ("erases") existing PDF content - the background half
|
||||
/// of a text edit. Subclasses HighlightAnnotation so it inherits all rect plumbing (render, drag,
|
||||
/// corner-resize, hit-test, export) for free; the only differences are an opaque default fill and
|
||||
/// a SetColor that can never go translucent (a see-through cover would let the old text ghost
|
||||
/// through, the exact bug this feature exists to avoid). The paired replacement text is a normal
|
||||
/// TextAnnotation placed on top, so it is independently editable, movable, and recolorable.
|
||||
/// </summary>
|
||||
public class CoverAnnotation : HighlightAnnotation
|
||||
{
|
||||
public CoverAnnotation()
|
||||
{
|
||||
ColorR = 255; ColorG = 255; ColorB = 255; ColorA = 255; // opaque white by default
|
||||
Style = HighlightStyle.Fill;
|
||||
}
|
||||
|
||||
/// <summary>Recolor the cover but keep it fully opaque - drop any alpha the caller passed.</summary>
|
||||
public override void SetColor(Color c) => base.SetColor(Color.FromArgb(255, c.R, c.G, c.B));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A signature placed on a PDF page: either ink strokes or an imported image.
|
||||
/// </summary>
|
||||
public class SignatureAnnotation : PlacedAnnotation
|
||||
{
|
||||
public List<List<Point>> Strokes { get; set; } = [];
|
||||
/// <summary>Pen thickness (DIPs at source scale); multiplied by Scale when rendered.</summary>
|
||||
public double StrokeWidth { get; set; } = 2.5;
|
||||
/// <summary>Base-64 encoded PNG. Non-null = image sig; null = drawn strokes.</summary>
|
||||
public string? ImageData { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An image placed on a PDF page as a resizable annotation.
|
||||
/// </summary>
|
||||
public class ImageAnnotation : PlacedAnnotation
|
||||
{
|
||||
/// <summary>Base-64 encoded image bytes (PNG, JPG, BMP, etc.).</summary>
|
||||
public string ImageData { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A point that can be serialized to JSON (WPF Point doesn't serialize well).
|
||||
/// </summary>
|
||||
public class SerializablePoint
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A saved signature stored in the user's AppData for reuse.
|
||||
/// </summary>
|
||||
/// <summary>Distinguishes a full signature from a (smaller) initials stamp. Default is Signature
|
||||
/// so signatures saved before this field existed still deserialize correctly.</summary>
|
||||
public enum SignatureKind { Signature, Initials }
|
||||
|
||||
public class SavedSignature
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string Name { get; set; } = "Signature";
|
||||
/// <summary>Whether this is a full signature or an initials stamp. Drives which popup section
|
||||
/// it appears in and the default placement scale.</summary>
|
||||
public SignatureKind Kind { get; set; } = SignatureKind.Signature;
|
||||
/// <summary>Pen thickness the signature was drawn with (DIPs at CanvasWidth/Height scale).</summary>
|
||||
public double StrokeWidth { get; set; } = 2.5;
|
||||
public List<List<SerializablePoint>> Strokes { get; set; } = [];
|
||||
public double CanvasWidth { get; set; } = 400;
|
||||
public double CanvasHeight { get; set; } = 150;
|
||||
/// <summary>Base-64 encoded PNG for imported image signatures. Null = drawn strokes.</summary>
|
||||
public string? ImageData { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace KillerPDF
|
||||
{
|
||||
/// <summary>
|
||||
/// One link rectangle on a page, in render-dim coordinates.
|
||||
///
|
||||
/// Used by the tiled views (continuous, grid, two-page), where a per-link overlay would swallow
|
||||
/// the click without its own handler ever firing - so clicks and the hover cursor are resolved
|
||||
/// by bounds-testing these rects instead. That makes them the source of truth for links outside
|
||||
/// single-page view.
|
||||
///
|
||||
/// TOP-LEVEL, not nested. Links.cs lives in the viewer control while ContextMenu.cs lives on the
|
||||
/// window and also bounds-tests these rects to build the right-click menu, so neither class can
|
||||
/// own the type.
|
||||
/// </summary>
|
||||
internal readonly record struct LinkInfo(
|
||||
double Cx, double Cy, double Cw, double Ch, object Tag, string Tip, int AnnotIndex);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Docnet.Core;
|
||||
using Docnet.Core.Models;
|
||||
|
||||
namespace KillerPDF
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for a single page thumbnail in the sidebar PageList.
|
||||
/// Thumbnail is loaded lazily on a background thread; the UI binds to
|
||||
/// the <see cref="Thumbnail"/> property and updates via PropertyChanged.
|
||||
/// </summary>
|
||||
internal sealed class PageThumbnailVm(int pageIndex, string filePath, int rotation = 0) : INotifyPropertyChanged
|
||||
{
|
||||
// Limit concurrent pdfium doc-reader opens to avoid contention
|
||||
private static readonly SemaphoreSlim _loadSem = new(2, 2);
|
||||
|
||||
private BitmapSource? _thumb;
|
||||
private bool _loadRequested;
|
||||
|
||||
public int PageIndex { get; } = pageIndex;
|
||||
public string Label => string.Format(
|
||||
Application.Current?.TryFindResource("Str_PageLabel") as string ?? "Page {0}", PageIndex + 1);
|
||||
|
||||
private readonly string _filePath = filePath;
|
||||
private readonly int _rotation = ((rotation % 360) + 360) % 360; // degrees: 0, 90, 180, 270
|
||||
|
||||
public BitmapSource? Thumbnail => _thumb;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>Called when the ListBox item becomes visible (via binding getter trigger).</summary>
|
||||
public void RequestLoad()
|
||||
{
|
||||
if (_loadRequested) return;
|
||||
_loadRequested = true;
|
||||
System.Threading.Tasks.Task.Run(LoadAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seed the thumbnail before ItemsSource is set - no dispatch needed because
|
||||
/// no binding exists yet. Used to carry old thumbnails across a RefreshPageList
|
||||
/// call so the list never flashes blank.
|
||||
/// </summary>
|
||||
internal void SetThumbnailDirect(BitmapSource src) => _thumb = src;
|
||||
|
||||
/// <summary>Called by RefreshPageList's bulk background loader.</summary>
|
||||
internal void SetThumbnail(BitmapSource src)
|
||||
{
|
||||
// A background load can finish after the app has begun shutting down (or between
|
||||
// tab switches), when Application.Current is briefly null. The UI is going away in
|
||||
// that case, so just drop the update instead of throwing.
|
||||
var app = Application.Current;
|
||||
if (app == null) return;
|
||||
app.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
_thumb = src;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Thumbnail)));
|
||||
}));
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task LoadAsync()
|
||||
{
|
||||
await _loadSem.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var src = BuildThumb(_filePath, PageIndex, _rotation);
|
||||
if (src != null) SetThumbnail(src);
|
||||
}
|
||||
catch { /* thumbnail not critical */ }
|
||||
finally { _loadSem.Release(); }
|
||||
}
|
||||
|
||||
internal static BitmapSource? BuildThumb(string filePath, int pageIndex, int rotation = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Render thumbnails at a higher resolution than they're usually shown so the page list
|
||||
// stays crisp when the sidebar is dragged wider (thumbnails scale to the sidebar width).
|
||||
// 288px covers the ~240px the page can show at the widest sidebar; raise for sharper,
|
||||
// lower to save memory (each loaded thumbnail is kept in RAM).
|
||||
using var docReader = DocLib.Instance.GetDocReader(filePath, new PageDimensions(288, 576));
|
||||
using var pr = docReader.GetPageReader(pageIndex);
|
||||
int tw = pr.GetPageWidth();
|
||||
int th = pr.GetPageHeight();
|
||||
var raw = KillerPDF.Services.PdfiumInterop.RenderPageWithAnnotations(filePath, pageIndex, tw, th)
|
||||
?? pr.GetImage(); // #141
|
||||
if (tw <= 0 || th <= 0 || raw == null || raw.Length < tw * th * 4)
|
||||
return null;
|
||||
// Apply in-memory rotation (temp file stores /Rotate=0; _pageRotations holds true angle)
|
||||
if (rotation != 0)
|
||||
(raw, tw, th) = Services.BitmapHelpers.RotateBitmap(raw, tw, th, rotation);
|
||||
return EncodeToBitmapSource(raw, tw, th);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode already-decoded BGRA pixels (with rotation applied) to a frozen BitmapFrame.
|
||||
/// Called by the RefreshPageList bulk loader which manages its own doc reader.
|
||||
/// </summary>
|
||||
internal static BitmapSource? BuildThumbFromRaw(byte[] bgra, int width, int height)
|
||||
=> EncodeToBitmapSource(bgra, width, height);
|
||||
|
||||
/// <summary>
|
||||
/// Encode raw BGRA (pdfium) → PNG → frozen BitmapFrame entirely on the calling thread.
|
||||
/// GDI+ Format32bppArgb is BGRA in memory, matching pdfium output exactly.
|
||||
/// </summary>
|
||||
private static BitmapSource? EncodeToBitmapSource(byte[] bgra, int width, int height)
|
||||
{
|
||||
var pin = GCHandle.Alloc(bgra, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
using var bmp = new System.Drawing.Bitmap(
|
||||
width, height, width * 4,
|
||||
System.Drawing.Imaging.PixelFormat.Format32bppArgb,
|
||||
pin.AddrOfPinnedObject());
|
||||
using var ms = new MemoryStream();
|
||||
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
|
||||
ms.Position = 0;
|
||||
var src = BitmapFrame.Create(ms, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
|
||||
src.Freeze();
|
||||
return src;
|
||||
}
|
||||
finally { pin.Free(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace KillerPDF
|
||||
{
|
||||
internal enum StampKind { PageNumber, Watermark }
|
||||
|
||||
// The full configuration produced/edited by the Stamp window. One spec can drive page numbers,
|
||||
// a watermark, or both, each over its own page range. A spec is the unit that gets re-opened when
|
||||
// the user double-clicks a placed stamp, so it carries everything needed to recreate the stamps.
|
||||
internal sealed class StampSpec
|
||||
{
|
||||
// ---- Page numbers ----
|
||||
public bool NumbersEnabled;
|
||||
public int StartNumber = 1;
|
||||
public string Format = "{n}"; // {n} = this page's number, {N} = total
|
||||
public int NumPosH = 1; // 0 left, 1 center, 2 right
|
||||
public int NumPosV = 2; // 0 top, 1 middle, 2 bottom
|
||||
public double NumFontPt = 12;
|
||||
public Color NumColor = Colors.Black;
|
||||
public string NumRange = ""; // "" = all pages; else "1-3,5"
|
||||
public bool NumMirror; // flip left/right each page so numbers sit on the outer edge
|
||||
public double NumCustomX = 0.5; // used when NumPosH == -1 (Custom): center as a fraction of page
|
||||
public double NumCustomY = 0.92;
|
||||
|
||||
// ---- Watermark ----
|
||||
public bool WmEnabled;
|
||||
public bool WmIsImage; // false = text, true = image
|
||||
// Localized default (falls back to DRAFT); resolved at model construction on the UI thread.
|
||||
public string WmText = System.Windows.Application.Current?.TryFindResource("Str_Stamp_DefaultText") as string ?? "DRAFT";
|
||||
public string WmFont = "Segoe UI";
|
||||
public double WmFontPt = 64;
|
||||
public Color WmColor = Color.FromRgb(0x88, 0x88, 0x88);
|
||||
public double WmOpacity = 0.25; // 0..1
|
||||
public double WmAngle = 45; // degrees, counter-clockwise
|
||||
public int WmPosH = 1; // 0 left, 1 center, 2 right
|
||||
public int WmPosV = 1; // 0 top, 1 middle, 2 bottom
|
||||
public string? WmImagePath; // source image when WmIsImage
|
||||
public double WmScale = 1.0; // multiplier on the natural placement size
|
||||
public string WmRange = ""; // "" = all pages
|
||||
public double WmCustomX = 0.5; // used when WmPosH == -1 (Custom): center as a fraction of page
|
||||
public double WmCustomY = 0.5;
|
||||
|
||||
public StampSpec Clone() => (StampSpec)MemberwiseClone();
|
||||
}
|
||||
|
||||
// A single placed stamp on one page. It points back at the spec that created it so a double-click
|
||||
// on the page can re-open the Stamp window with the original settings. The concrete text/position is
|
||||
// derived from Spec + page geometry at render/burn time, so nothing here needs the resolved layout.
|
||||
internal sealed class StampInstance
|
||||
{
|
||||
public int PageIndex;
|
||||
public StampKind Kind;
|
||||
public StampSpec Spec = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace KillerPDF
|
||||
{
|
||||
// The undo stack's entry type. Each entry is either an annotation removal or a full document
|
||||
// snapshot; AnnotationGroup removes a specific set in one step (a text edit = cover + text).
|
||||
//
|
||||
// TOP-LEVEL, not nested in MainWindow. The code that pushes undo entries - Annotations.cs and
|
||||
// TextEditing.cs - lives in KillerPDF.Controls, where a type nested in MainWindow only spells
|
||||
// as MainWindow.UndoEntry; that would mean qualifying roughly 30 call sites for no gain. As
|
||||
// top-level types in KillerPDF they resolve unqualified from the child namespace too.
|
||||
//
|
||||
// This also retires the CS0052 chain that made them internal in the first place: DocumentSession
|
||||
// had to be internal for the render cache, its UndoStack field is Stack<UndoEntry>, and a field
|
||||
// cannot be more accessible than its type.
|
||||
|
||||
internal enum UndoKind { Annotation, Document, StampBatch, ClearAnnotations, AnnotationGroup, PageSnapshot }
|
||||
|
||||
internal readonly record struct UndoEntry(
|
||||
UndoKind Kind,
|
||||
int PageIdx = -1,
|
||||
byte[]? DocBytes = null,
|
||||
bool WasDirty = false,
|
||||
int[]? Pages = null,
|
||||
PageAnnotation? Annot = null,
|
||||
Dictionary<int, List<PageAnnotation>>? AnnotSnapshot = null,
|
||||
List<PageAnnotation>? AnnotGroup = null);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace KillerPDF
|
||||
{
|
||||
// How a document view lays its pages out, and how it fits them to the viewport.
|
||||
//
|
||||
// TOP-LEVEL, not nested in MainWindow. The viewer is a UserControl in KillerPDF.Controls, and
|
||||
// from there a type nested in MainWindow only spells as MainWindow.ViewMode - which would mean
|
||||
// qualifying 91 references for no gain. As top-level types in KillerPDF they resolve
|
||||
// unqualified from KillerPDF.Controls too (a namespace declaration puts its parent namespaces
|
||||
// in scope), so every call site compiles untouched.
|
||||
//
|
||||
// internal, not public: nothing outside the assembly has any business with either.
|
||||
|
||||
/// <summary>Page layout for a document view. RenderPage is Single/TwoPage/Grid only and is
|
||||
/// guarded to no-op in Continuous - see the render pipeline's notes on why the two pipelines
|
||||
/// cannot be mixed.</summary>
|
||||
internal enum ViewMode { Single, Continuous, TwoPage, Grid }
|
||||
|
||||
/// <summary>Automatic fit applied on resize, or None when the user has set a zoom.</summary>
|
||||
internal enum FitMode { None, Width, Page }
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace KillerPDF
|
||||
{
|
||||
/// <summary>
|
||||
/// Everything ONE document view owns. Split pane needs two of these; each PdfViewer control
|
||||
/// owns one, and the window reads the active one back through its `_view` property, so the ~500
|
||||
/// call sites behind the window's forwarding properties are untouched.
|
||||
///
|
||||
/// This is deliberately the PER-VIEW cut, not the per-document one. Per-document state
|
||||
/// (annotations, undo, form values, search hits) already travels in DocumentSession, which
|
||||
/// tab switching swaps by reference - see the comment above _annotations in
|
||||
/// MainWindow.xaml.cs. A second pane needs its own live visual maps and its own view mode
|
||||
/// and zoom; it does NOT need a second copy of the per-document machinery, because each
|
||||
/// pane will simply own its own set of sessions.
|
||||
///
|
||||
/// TOP-LEVEL, not nested in MainWindow. The viewer lives in KillerPDF.Controls and cannot own a
|
||||
/// type nested in the window without every reference spelling out MainWindow.ViewerState.
|
||||
/// ViewMode and FitMode live in Models/ViewTypes.cs for the same reason.
|
||||
/// </summary>
|
||||
internal sealed class ViewerState
|
||||
{
|
||||
/// <summary>Unified page -> overlay map covering EVERY rendered page, the primary
|
||||
/// included. The single source of truth the canvas accessors read from.</summary>
|
||||
public readonly Dictionary<int, Canvas> Pages = [];
|
||||
|
||||
/// <summary>Per-page overlay canvases for the multi-page tile systems (continuous
|
||||
/// overlays, or grid / two-page secondaries). Holds only secondary tiles and is driven
|
||||
/// by the tile-recycling machinery.</summary>
|
||||
public readonly Dictionary<int, Canvas> ContinuousCanvases = [];
|
||||
|
||||
/// <summary>The page this view is showing (0-based; -1 = no document).
|
||||
///
|
||||
/// This exists because reading the SIDEBAR's selected thumbnail,
|
||||
/// `PageList.SelectedIndex` (118 times across 24 files), works with one pane and cannot
|
||||
/// work with two - there is one sidebar and two current pages, so a viewer inside the
|
||||
/// control has nothing to ask.
|
||||
///
|
||||
/// This is the storage; the sidebar FOLLOWS it. Kept in sync in exactly two places,
|
||||
/// which between them cover every write:
|
||||
/// - PageList_SelectionChanged (PageSelection.cs) mirrors the sidebar back into here,
|
||||
/// unconditionally and before its own >= 0 guard, so clearing the list to -1 (tab
|
||||
/// close, document close) is mirrored too.
|
||||
/// - SyncCurrentPageTo (Viewport.cs), which detaches that handler to avoid re-entry
|
||||
/// and so would otherwise slip past the mirror.
|
||||
/// Everything that sets PageList.SelectedIndex directly still routes through the
|
||||
/// handler, so those need no change.
|
||||
///
|
||||
/// The 118 call sites are deliberately NOT repointed at this field: the render pipeline
|
||||
/// switches over to reading it as it moves into the control.</summary>
|
||||
public int CurrentPage = -1;
|
||||
|
||||
/// <summary>Current view mode for this view.</summary>
|
||||
public ViewMode Mode = ViewMode.Continuous;
|
||||
|
||||
/// <summary>Mode a fade is transitioning to, if one is in flight. Reads that need the
|
||||
/// destination rather than the current mode use `Pending ?? Mode` - the fade takes
|
||||
/// ~90ms and Mode lags behind it, which is what made wheel-cycling need several
|
||||
/// notches before it was fixed.</summary>
|
||||
public ViewMode? Pending;
|
||||
|
||||
// ── Zoom / fit ──────────────────────────────────────────────────────────────────
|
||||
public double ZoomLevel = 1.0;
|
||||
/// <summary>Zoom the current bitmaps were rasterized at, so the re-sharpen pass knows
|
||||
/// whether what is on screen is still crisp enough.</summary>
|
||||
public double LastRenderZoom = 1.0;
|
||||
/// <summary>Primary (spread-left) page currently rasterized.</summary>
|
||||
public int RenderedPrimaryPage = -1;
|
||||
public FitMode Fit = FitMode.None;
|
||||
|
||||
// ── In-flight render work ───────────────────────────────────────────────────────
|
||||
// Each view cancels and reschedules its own rendering, so two panes must not share
|
||||
// these - one pane's mode switch would otherwise cancel the other's render.
|
||||
public System.Windows.Threading.DispatcherTimer? RerenderTimer;
|
||||
public System.Threading.CancellationTokenSource? SecondaryRenderCts;
|
||||
public System.Threading.CancellationTokenSource? ContinuousRenderCts;
|
||||
/// <summary>#85 visible-page re-sharpen.</summary>
|
||||
public System.Threading.CancellationTokenSource? ContinuousSharpenCts;
|
||||
|
||||
// ── Continuous-view bookkeeping ─────────────────────────────────────────────────
|
||||
/// <summary>Slots currently holding a hi-res bitmap.</summary>
|
||||
public readonly HashSet<int> ContinuousSharpPages = [];
|
||||
/// <summary>Budget those slots were sharpened at.</summary>
|
||||
public int ContinuousSharpW;
|
||||
public readonly List<double> ContinuousTops = [];
|
||||
/// <summary>Page to scroll to once its grid tile streams in (-1 = none).</summary>
|
||||
public int GridScrollToPage = -1;
|
||||
/// <summary>Re-scroll here once its true height is known.</summary>
|
||||
public int ContinuousScrollTarget = -1;
|
||||
public double ContinuousPageW;
|
||||
|
||||
// ── Gesture routing ─────────────────────────────────────────────────────────────
|
||||
/// <summary>The page surface a pointer gesture started on, captured on mouse-down.
|
||||
/// Kept separate from the active canvas because RenderAllAnnotations reuses that as its
|
||||
/// render target, and in Grid view tiles stream in asynchronously and re-point it
|
||||
/// mid-gesture - which committed annotations to the wrong page.</summary>
|
||||
public Canvas? GestureCanvas;
|
||||
public int GesturePage = -1;
|
||||
|
||||
// ── Visual hosts ────────────────────────────────────────────────────────────────
|
||||
// References only - the window still creates and owns the actual elements. Today
|
||||
// ContinuousPanel / PageContentPanel / PageContentGrid come from FindName in the
|
||||
// window ctor and are the ONE window's XAML; AnnotationCanvas / PageImage are the
|
||||
// code-built primary tile (Viewport.BuildPrimaryTile) and ActiveCanvas is re-pointed
|
||||
// on mouse-down. Holding them here is what lets the next stage hand each viewer its
|
||||
// own tile tree without touching any of the ~250 call sites that use them.
|
||||
public StackPanel ContinuousPanel = null!;
|
||||
public WrapPanel PageContentPanel = null!;
|
||||
public Grid PageContentGrid = null!;
|
||||
/// <summary>The hardcoded primary tile's overlay, shown in Single/Grid/TwoPage.</summary>
|
||||
public Canvas AnnotationCanvas = null!;
|
||||
public Image PageImage = null!;
|
||||
/// <summary>Active annotation surface. Single view: always AnnotationCanvas.
|
||||
/// Continuous: set on mouse-down to the clicked page's overlay.</summary>
|
||||
public Canvas ActiveCanvas = null!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user