- Services/PdfRedactText.cs: strip text whose origin falls inside a CoverAnnotation from the page content stream at save time, hooked into PdfBurn.DrawAnnotationsIntoDoc. Fixes edited values staying recoverable by text extraction. - Themes/MMD.xaml replaces all thirteen themes; picker and accent strip removed; no dark mode. - Rename KillerPDF -> MMD PDF across code, resources, packaging and locale strings; new icon. - Remove the upstream author credit and the in-app install button.
212 lines
8.2 KiB
C#
212 lines
8.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using PdfSharpCore.Drawing;
|
|
using PdfSharpCore.Pdf;
|
|
using PdfSharpCore.Pdf.Content;
|
|
using PdfSharpCore.Pdf.Content.Objects;
|
|
|
|
namespace MmdPdf.Services
|
|
{
|
|
// ============================================================
|
|
// Removes original page text that sits under an opaque cover.
|
|
//
|
|
// When a user edits existing text, the editor lays an opaque CoverAnnotation
|
|
// over the original run and draws the replacement on top. The page LOOKED
|
|
// right but the original glyphs were still in the content stream, so text
|
|
// extraction and copy/paste returned the old value alongside the new one -
|
|
// an edited quantity on a delivery note stayed recoverable.
|
|
//
|
|
// This pass walks the page content stream and blanks every text-showing
|
|
// operator whose text origin falls inside a cover rectangle, so the old
|
|
// glyphs are gone from the file, not merely hidden. Positioning operators
|
|
// are left untouched, so nothing else on the page moves.
|
|
//
|
|
// Deliberate limitation: a run is removed only when its ORIGIN is inside the
|
|
// cover. A single show-text operator that starts outside the cover and runs
|
|
// into it is left alone rather than risk deleting text the user did not edit.
|
|
// The editor's own covers are generated to bound the run they replace, so
|
|
// this is exact for edits made in this application.
|
|
// ============================================================
|
|
internal static class PdfRedactText
|
|
{
|
|
/// <summary>
|
|
/// Blanks text drawn with its origin inside any of <paramref name="rectsPdf"/>.
|
|
/// Rectangles are in PDF user space (origin bottom-left, y up), points.
|
|
/// Returns the number of show-text operators blanked.
|
|
/// </summary>
|
|
internal static int RemoveTextUnder(PdfPage page, IReadOnlyList<XRect> rectsPdf)
|
|
{
|
|
if (page is null || rectsPdf is null || rectsPdf.Count == 0) return 0;
|
|
|
|
CSequence content;
|
|
try { content = ContentReader.ReadContent(page); }
|
|
catch { return 0; } // unparsable stream: leave the page exactly as it was
|
|
|
|
var state = new TextState();
|
|
int blanked = Walk(content, state, rectsPdf);
|
|
if (blanked == 0) return 0;
|
|
|
|
try { page.Contents.ReplaceContent(content); }
|
|
catch { return 0; }
|
|
return blanked;
|
|
}
|
|
|
|
// ── graphics + text state ──────────────────────────────────────────
|
|
|
|
private sealed class TextState
|
|
{
|
|
internal XMatrix Ctm = XMatrix.Identity;
|
|
internal readonly Stack<XMatrix> CtmStack = new Stack<XMatrix>();
|
|
internal XMatrix Tm = XMatrix.Identity; // text matrix
|
|
internal XMatrix Tlm = XMatrix.Identity; // text line matrix
|
|
internal double Leading; // TL
|
|
}
|
|
|
|
private static int Walk(CSequence seq, TextState st, IReadOnlyList<XRect> rects)
|
|
{
|
|
int blanked = 0;
|
|
foreach (var obj in seq)
|
|
{
|
|
if (obj is CSequence inner && obj is not CArray)
|
|
{
|
|
blanked += Walk(inner, st, rects);
|
|
continue;
|
|
}
|
|
if (obj is not COperator op) continue;
|
|
|
|
string name = op.OpCode?.Name ?? "";
|
|
switch (name)
|
|
{
|
|
case "q":
|
|
st.CtmStack.Push(st.Ctm);
|
|
break;
|
|
case "Q":
|
|
if (st.CtmStack.Count > 0) st.Ctm = st.CtmStack.Pop();
|
|
break;
|
|
case "cm":
|
|
if (TryMatrix(op.Operands, out var cm)) st.Ctm = cm * st.Ctm;
|
|
break;
|
|
|
|
case "BT":
|
|
st.Tm = st.Tlm = XMatrix.Identity;
|
|
break;
|
|
case "ET":
|
|
st.Tm = st.Tlm = XMatrix.Identity;
|
|
break;
|
|
|
|
case "Tm":
|
|
if (TryMatrix(op.Operands, out var tm)) st.Tm = st.Tlm = tm;
|
|
break;
|
|
case "TL":
|
|
st.Leading = Num(op.Operands, 0);
|
|
break;
|
|
case "Td":
|
|
NextLine(st, Num(op.Operands, 0), Num(op.Operands, 1));
|
|
break;
|
|
case "TD":
|
|
st.Leading = -Num(op.Operands, 1);
|
|
NextLine(st, Num(op.Operands, 0), Num(op.Operands, 1));
|
|
break;
|
|
case "T*":
|
|
NextLine(st, 0, -st.Leading);
|
|
break;
|
|
|
|
case "'":
|
|
NextLine(st, 0, -st.Leading);
|
|
blanked += BlankIfInside(op, st, rects);
|
|
break;
|
|
case "\"":
|
|
NextLine(st, 0, -st.Leading);
|
|
blanked += BlankIfInside(op, st, rects);
|
|
break;
|
|
case "Tj":
|
|
case "TJ":
|
|
blanked += BlankIfInside(op, st, rects);
|
|
break;
|
|
}
|
|
}
|
|
return blanked;
|
|
}
|
|
|
|
private static void NextLine(TextState st, double tx, double ty)
|
|
{
|
|
var t = new XMatrix(1, 0, 0, 1, tx, ty);
|
|
st.Tlm = t * st.Tlm;
|
|
st.Tm = st.Tlm;
|
|
}
|
|
|
|
private static int BlankIfInside(COperator op, TextState st, IReadOnlyList<XRect> rects)
|
|
{
|
|
var m = st.Tm * st.Ctm;
|
|
var origin = new XPoint(m.OffsetX, m.OffsetY);
|
|
|
|
bool hit = false;
|
|
for (int i = 0; i < rects.Count; i++)
|
|
{
|
|
var r = rects[i];
|
|
// A baseline sits slightly above the visual bottom of the glyphs, so allow a
|
|
// small pad below the cover; otherwise a run whose baseline is a point under
|
|
// the cover edge survives.
|
|
if (origin.X >= r.X - 1.0 && origin.X <= r.X + r.Width + 1.0 &&
|
|
origin.Y >= r.Y - 2.0 && origin.Y <= r.Y + r.Height + 2.0)
|
|
{
|
|
hit = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!hit) return 0;
|
|
|
|
return BlankStrings(op.Operands) ? 1 : 0;
|
|
}
|
|
|
|
private static bool BlankStrings(CSequence operands)
|
|
{
|
|
if (operands is null) return false;
|
|
bool any = false;
|
|
for (int i = 0; i < operands.Count; i++)
|
|
{
|
|
switch (operands[i])
|
|
{
|
|
case CString s when s.Value.Length > 0:
|
|
s.Value = "";
|
|
any = true;
|
|
break;
|
|
case CArray arr:
|
|
for (int j = 0; j < arr.Count; j++)
|
|
{
|
|
if (arr[j] is CString cs && cs.Value.Length > 0)
|
|
{
|
|
cs.Value = "";
|
|
any = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return any;
|
|
}
|
|
|
|
// ── operand helpers ────────────────────────────────────────────────
|
|
|
|
private static double Num(CSequence operands, int index)
|
|
{
|
|
if (operands is null || index >= operands.Count) return 0;
|
|
return operands[index] switch
|
|
{
|
|
CInteger i => i.Value,
|
|
CReal r => r.Value,
|
|
_ => 0
|
|
};
|
|
}
|
|
|
|
private static bool TryMatrix(CSequence operands, out XMatrix m)
|
|
{
|
|
m = XMatrix.Identity;
|
|
if (operands is null || operands.Count < 6) return false;
|
|
m = new XMatrix(Num(operands, 0), Num(operands, 1), Num(operands, 2),
|
|
Num(operands, 3), Num(operands, 4), Num(operands, 5));
|
|
return true;
|
|
}
|
|
}
|
|
}
|