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
{
///
/// Blanks text drawn with its origin inside any of .
/// Rectangles are in PDF user space (origin bottom-left, y up), points.
/// Returns the number of show-text operators blanked.
///
internal static int RemoveTextUnder(PdfPage page, IReadOnlyList 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 CtmStack = new Stack();
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 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 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;
}
}
}