Files
mmd-pdf/Services/PdfOutlines.cs
kua-agent 6610acfa44 feat: rebrand to MMD PDF, single corporate theme, and remove original text under an edit
- 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.
2026-08-27 07:37:09 +02:00

93 lines
4.5 KiB
C#

using PdfSharpCore.Pdf;
namespace MmdPdf.Services
{
// ============================================================
// Outline/bookmark document helpers - pure functions over the
// PdfSharpCore outline tree, no window state. Split out of
// SidebarOutline.cs (MmdPdfUI refactor); the TreeView panel and
// bookmark editing UI stay in the shell.
// ============================================================
internal static class PdfOutlines
{
/// <summary>
/// #133: PdfSharpCore's lexer decodes UTF-16 bookmark titles by their BOM, but strings it
/// decrypts AFTER parsing (owner-password protected files) never get that BOM re-check, so
/// the title arrives as raw bytes widened to chars: a U+00FE U+00FF prefix (the BOM bytes)
/// followed by one char per byte (mojibake).
/// Detect the widened BOM, re-pack the chars into bytes, and decode as UTF-16. Titles that
/// parsed correctly don't start with those two chars and pass through untouched.
/// </summary>
internal static string FixRawUnicodeTitle(string s)
{
if (s.Length < 2) return s;
bool be = s[0] == '\u00FE' && s[1] == '\u00FF'; // UTF-16BE BOM as raw chars
bool le = s[0] == '\u00FF' && s[1] == '\u00FE'; // UTF-16LE (Adobe tolerance)
if (!be && !le) return s;
foreach (char c in s)
if (c > '\u00FF') return s; // not byte-widened data - a real (odd) title, leave it
var sb = new System.Text.StringBuilder((s.Length - 2) / 2);
for (int i = 2; i + 1 < s.Length; i += 2) // a trailing odd byte is dropped rather than corrupting the pairs
sb.Append(be ? (char)((s[i] << 8) | s[i + 1])
: (char)((s[i + 1] << 8) | s[i]));
return sb.ToString();
}
internal static int CountOutlines(PdfSharpCore.Pdf.PdfOutlineCollection col)
{
int n = 0;
foreach (PdfSharpCore.Pdf.PdfOutline o in col) n += 1 + CountOutlines(o.Outlines);
return n;
}
// Bottom-up: Collection.Remove() drops the removed object from the document's reference
// table, so deleting the whole branch leaf-first leaves no orphaned outline objects (with
// dangling /Parent refs) behind in the saved file.
internal static void RemoveOutlineRecursive(PdfSharpCore.Pdf.PdfOutlineCollection parent,
PdfSharpCore.Pdf.PdfOutline outline)
{
while (outline.Outlines.Count > 0)
RemoveOutlineRecursive(outline.Outlines, outline.Outlines[outline.Outlines.Count - 1]);
parent.Remove(outline);
}
// PdfSharpCore's PrepareForSave rewrites outline linkage keys (/First /Last /Next /Prev
// /Parent /Count) from the in-memory collections but never REMOVES entries that no longer
// apply: an item that became last keeps its old /Next, a parent whose children were all
// deleted keeps /First /Last, and an emptied root would dangle (ScrubEmptyOutlines only
// drops the catalog entry when /First is gone). After any bookmark edit, strip the linkage
// keys everywhere - the writer rebuilds all of them from the collections on save.
internal static void ScrubStaleOutlineLinkKeys(PdfDocument? doc)
{
if (doc is null) return;
try
{
var item = doc.Internals.Catalog.Elements["/Outlines"];
if (item is null) return;
if (PdfScrub.DerefItemStatic(item) is PdfDictionary root)
{
root.Elements.Remove("/First");
root.Elements.Remove("/Last");
root.Elements.Remove("/Count");
}
ScrubOutlineLinkKeys(doc.Outlines);
}
catch { /* malformed outline tree - the save-time scrubs are the backstop */ }
}
private static void ScrubOutlineLinkKeys(PdfSharpCore.Pdf.PdfOutlineCollection col)
{
foreach (PdfSharpCore.Pdf.PdfOutline o in col)
{
o.Elements.Remove("/First");
o.Elements.Remove("/Last");
o.Elements.Remove("/Next");
o.Elements.Remove("/Prev");
o.Elements.Remove("/Parent");
o.Elements.Remove("/Count");
ScrubOutlineLinkKeys(o.Outlines);
}
}
}
}