Files
mmd-pdf/MmdPdf.Tests/OutlineDestinationTests.cs
T
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

107 lines
4.5 KiB
C#

using System.Diagnostics;
using PdfSharpCore.Pdf;
using PdfSharpCore.Pdf.IO;
using Xunit;
namespace MmdPdf.Tests
{
/// <summary>
/// Regression cover for bookmarks whose /Dest is a *named destination* rather than a literal
/// array. wkhtmltopdf writes /Dest /__WKANCHOR_n plus a flat catalog /Dests dictionary, and
/// since most HTML-to-PDF invoice generators are wkhtmltopdf underneath, that shape is common.
/// PdfSharpCore's PdfOutline.Initialize() used to hit Debug.Assert(false, "See what to do when
/// this happened.") on it - a modal dialog in Debug, a silently dead bookmark in Release.
/// </summary>
public class OutlineDestinationTests
{
const string DestName = "/__WKANCHOR_2";
/// <summary>
/// Debug.Assert writes to a trace listener and shows a modal box - it never throws, so an
/// assert regression would HANG this test rather than fail it. Swap in a listener that
/// turns Fail into an exception.
/// </summary>
sealed class ThrowingListener : TraceListener
{
public override void Write(string? message) { }
public override void WriteLine(string? message) { }
public override void Fail(string? message) => throw new Xunit.Sdk.XunitException("Debug.Assert fired: " + message);
public override void Fail(string? message, string? detail) => throw new Xunit.Sdk.XunitException("Debug.Assert fired: " + message + " | " + detail);
}
[Fact]
public void OpenOutlines_NamedDestination_DoesNotAssert()
{
var dir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString("N"));
System.IO.Directory.CreateDirectory(dir);
var file = System.IO.Path.Combine(dir, "named_dest.pdf");
var original = new TraceListener[Trace.Listeners.Count];
Trace.Listeners.CopyTo(original, 0);
Trace.Listeners.Clear();
Trace.Listeners.Add(new ThrowingListener());
try
{
WriteNamedDestPdf(file);
var doc = PdfReader.Open(file, PdfDocumentOpenMode.Modify);
var outlines = doc.Outlines; // threw before the fix
Assert.Single(outlines);
Assert.Equal("Tax Invoice", outlines[0].Title);
// Guard the fixture itself: if a future PdfSharpCore writer rewrote the name into a
// literal array on save, this test would still pass while no longer covering the
// regression at all.
Assert.IsType<PdfName>(outlines[0].Elements.GetValue("/Dest"));
}
finally
{
Trace.Listeners.Clear();
Trace.Listeners.AddRange(original);
System.IO.Directory.Delete(dir, recursive: true);
}
}
/// <summary>
/// Builds a one-page PDF with a single bookmark pointing at a named destination.
/// ponytail: the /Dests entry holds the destination array DIRECTLY; the invoice that
/// surfaced this held it indirectly. Both take the same /Dest-is-a-PdfName branch, which is
/// the regression under test. Make it indirect if that path ever breaks on its own.
/// </summary>
static void WriteNamedDestPdf(string path)
{
var doc = new PdfDocument();
doc.AddPage();
// [page /XYZ left top zoom] - integer page number, as wkhtmltopdf emits.
var dest = new PdfArray(doc,
new PdfInteger(0), new PdfName("/XYZ"),
new PdfInteger(0), new PdfInteger(800), new PdfInteger(0));
var dests = new PdfDictionary(doc);
dests.Elements[DestName] = dest;
doc.Internals.AddObject(dests);
var root = new PdfDictionary(doc);
root.Elements["/Type"] = new PdfName("/Outlines");
doc.Internals.AddObject(root);
var item = new PdfDictionary(doc);
item.Elements["/Title"] = new PdfString("Tax Invoice");
item.Elements["/Dest"] = new PdfName(DestName);
item.Elements.SetReference("/Parent", root);
doc.Internals.AddObject(item);
root.Elements.SetReference("/First", item);
root.Elements.SetReference("/Last", item);
doc.Internals.Catalog.Elements.SetReference("/Outlines", root);
doc.Internals.Catalog.Elements.SetReference("/Dests", dests);
doc.Save(path);
}
}
}