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 } /// Sub-mode of the Shapes tool (#127 Phase 3): drag a rectangle or ellipse, or click /// out a free-form polygon vertex by vertex. public enum ShapeKind { Rectangle, Ellipse, Polygon } /// How a HighlightAnnotation paints over its bounds. 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; } = ""; } /// /// Base class for placed/resizable annotations (signature, image). /// Carries the shared position, scale, and source-dimension properties used by the resize handle. /// 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 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 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? 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; } /// /// 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. /// 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; } } } /// /// 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. /// public class CoverAnnotation : HighlightAnnotation { public CoverAnnotation() { ColorR = 255; ColorG = 255; ColorB = 255; ColorA = 255; // opaque white by default Style = HighlightStyle.Fill; } /// Recolor the cover but keep it fully opaque - drop any alpha the caller passed. public override void SetColor(Color c) => base.SetColor(Color.FromArgb(255, c.R, c.G, c.B)); } /// /// A signature placed on a PDF page: either ink strokes or an imported image. /// public class SignatureAnnotation : PlacedAnnotation { public List> Strokes { get; set; } = []; /// Pen thickness (DIPs at source scale); multiplied by Scale when rendered. public double StrokeWidth { get; set; } = 2.5; /// Base-64 encoded PNG. Non-null = image sig; null = drawn strokes. public string? ImageData { get; set; } } /// /// An image placed on a PDF page as a resizable annotation. /// public class ImageAnnotation : PlacedAnnotation { /// Base-64 encoded image bytes (PNG, JPG, BMP, etc.). public string ImageData { get; set; } = ""; } /// /// A point that can be serialized to JSON (WPF Point doesn't serialize well). /// public class SerializablePoint { public double X { get; set; } public double Y { get; set; } } /// /// A saved signature stored in the user's AppData for reuse. /// /// Distinguishes a full signature from a (smaller) initials stamp. Default is Signature /// so signatures saved before this field existed still deserialize correctly. public enum SignatureKind { Signature, Initials } public class SavedSignature { public string Id { get; set; } = Guid.NewGuid().ToString("N"); public string Name { get; set; } = "Signature"; /// Whether this is a full signature or an initials stamp. Drives which popup section /// it appears in and the default placement scale. public SignatureKind Kind { get; set; } = SignatureKind.Signature; /// Pen thickness the signature was drawn with (DIPs at CanvasWidth/Height scale). public double StrokeWidth { get; set; } = 2.5; public List> Strokes { get; set; } = []; public double CanvasWidth { get; set; } = 400; public double CanvasHeight { get; set; } = 150; /// Base-64 encoded PNG for imported image signatures. Null = drawn strokes. public string? ImageData { get; set; } } }