vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
using PdfSharpCore.Pdf;
|
||||
|
||||
namespace KillerPDF.Services
|
||||
{
|
||||
// ============================================================
|
||||
// Pre-save document scrubs - pure functions over a PdfDocument,
|
||||
// no window state. Split out of FileOperations.cs and Links.cs
|
||||
// (KillerUI refactor); shared by the GUI save paths, TempReload,
|
||||
// the CLI runner and the batch runner.
|
||||
// ============================================================
|
||||
internal static class PdfScrub
|
||||
{
|
||||
/// <summary>
|
||||
/// Dereferences a PdfItem if it is an indirect reference (PdfReference is internal to
|
||||
/// PdfSharpCore; we detect it by looking for a public "Value" property returning
|
||||
/// PdfObject). Null-tolerant: absent dictionary keys arrive here as null and mean
|
||||
/// "not there".
|
||||
/// </summary>
|
||||
internal static PdfItem? DerefItemStatic(PdfItem? item)
|
||||
{
|
||||
// Absent dictionary keys arrive here as null (Elements["/X"] on a fresh document is
|
||||
// null for /AcroForm, /Kids, ...). The scrubs' pattern matches treat null as "not
|
||||
// there", which is correct - dereferencing it here just tripped an NRE first.
|
||||
if (item is null) return null;
|
||||
var valueProp = item.GetType().GetProperty("Value",
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
if (valueProp?.GetValue(item) is PdfObject resolved) return resolved;
|
||||
return item;
|
||||
}
|
||||
|
||||
internal static double RectNum(PdfItem item) =>
|
||||
item is PdfReal r ? r.Value : item is PdfInteger n ? n.Value : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the PDF object number of a PdfItem that is an indirect reference, or -1.
|
||||
/// Handles the internal PdfReference type via reflection, like DerefItemStatic above.
|
||||
/// </summary>
|
||||
internal static int GetObjectNumber(PdfItem? item)
|
||||
{
|
||||
if (item is null) return -1;
|
||||
var prop = item.GetType().GetProperty("ObjectNumber",
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
return prop?.GetValue(item) is int n2 ? n2 : -1;
|
||||
}
|
||||
|
||||
// #103: PdfSharpCore's writer can emit the catalog's /Outlines reference without ever
|
||||
// writing the (empty, lazily created) outlines object itself - a dangling xref entry
|
||||
// that strict parsers, including PdfSharpCore on reopen, refuse. An outlines dictionary
|
||||
// with no /First contains no bookmarks, so dropping the entry is a semantic no-op that
|
||||
// keeps the file consistent. Real bookmark trees (/First present) are left untouched.
|
||||
// Called before every save of the working document.
|
||||
internal static void ScrubEmptyOutlines(PdfDocument doc)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cat = doc.Internals.Catalog;
|
||||
var item = cat.Elements["/Outlines"];
|
||||
if (item == null) return;
|
||||
var resolved = DerefItemStatic(item);
|
||||
if (resolved is not PdfDictionary o || o.Elements["/First"] == null)
|
||||
cat.Elements.Remove("/Outlines");
|
||||
}
|
||||
catch { /* malformed catalog - leave the save as-is */ }
|
||||
}
|
||||
|
||||
// PdfSharpCore's PdfPage.MediaBox/CropBox property GETTERS have create-on-read semantics:
|
||||
// touching page.CropBox on a page that has none plants an empty /CropBox [0 0 0 0] into
|
||||
// the page dictionary (the same lazy-getter trap as the phantom /Outlines above). A
|
||||
// zero-size page box saves to disk and Adobe then rejects the page as "dimensions
|
||||
// out-of-range" even though the MediaBox is fine (Chrome falls back to the MediaBox,
|
||||
// which is why such files still open there). Dropping a degenerate CropBox is a semantic
|
||||
// no-op - the page falls back to its MediaBox - and it also HEALS files written by
|
||||
// affected versions (1.6.x up to 1.6.2) when they are re-saved. Real crops are untouched.
|
||||
// Called before every save of the working document.
|
||||
internal static void ScrubDegenerateCropBoxes(PdfDocument doc)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < doc.PageCount; i++)
|
||||
{
|
||||
var elements = doc.Pages[i].Elements;
|
||||
var item = elements["/CropBox"];
|
||||
if (item is null) continue;
|
||||
var resolved = DerefItemStatic(item);
|
||||
|
||||
// The box can be a parsed PdfArray (loaded from disk) or a PdfRectangle
|
||||
// (planted in memory by the lazy getter) - handle both, like ScaleRectValue.
|
||||
bool cropReadable = TryReadPageBox(resolved, out double cx1, out double cy1,
|
||||
out double cx2, out double cy2);
|
||||
double w = cropReadable ? cx2 - cx1 : -1;
|
||||
double h = cropReadable ? cy2 - cy1 : -1;
|
||||
|
||||
// Remove only when we could read the box AND it is degenerate; anything we
|
||||
// cannot interpret is left alone rather than destroyed.
|
||||
if (w >= 0 && (w < 1 || h < 1))
|
||||
{
|
||||
elements.Remove("/CropBox");
|
||||
continue;
|
||||
}
|
||||
|
||||
// PDF requires CropBox to stay inside MediaBox. A rotated page could previously
|
||||
// be saved with portrait MediaBox dimensions and a landscape CropBox, producing
|
||||
// a malformed page that strict validators reject. Removing that invalid crop is
|
||||
// lossless: the page falls back to its complete MediaBox instead of clipping data.
|
||||
if (cropReadable && TryReadInheritedPageBox(doc.Pages[i], "/MediaBox",
|
||||
out double mx1, out double my1, out double mx2, out double my2) &&
|
||||
(cx1 < mx1 - 0.01 || cy1 < my1 - 0.01 ||
|
||||
cx2 > mx2 + 0.01 || cy2 > my2 + 0.01))
|
||||
elements.Remove("/CropBox");
|
||||
}
|
||||
}
|
||||
catch { /* malformed page tree - leave the save as-is */ }
|
||||
}
|
||||
|
||||
private static bool TryReadInheritedPageBox(PdfPage page, string key,
|
||||
out double x1, out double y1, out double x2, out double y2)
|
||||
{
|
||||
PdfDictionary? node = page;
|
||||
for (int depth = 0; node is not null && depth < 32; depth++)
|
||||
{
|
||||
var item = node.Elements[key];
|
||||
if (item is not null && TryReadPageBox(DerefItemStatic(item), out x1, out y1, out x2, out y2))
|
||||
return true;
|
||||
node = DerefItemStatic(node.Elements["/Parent"]) as PdfDictionary;
|
||||
}
|
||||
x1 = y1 = x2 = y2 = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryReadPageBox(PdfItem? item,
|
||||
out double x1, out double y1, out double x2, out double y2)
|
||||
{
|
||||
if (item is PdfRectangle rect)
|
||||
{
|
||||
x1 = Math.Min(rect.X1, rect.X2);
|
||||
y1 = Math.Min(rect.Y1, rect.Y2);
|
||||
x2 = Math.Max(rect.X1, rect.X2);
|
||||
y2 = Math.Max(rect.Y1, rect.Y2);
|
||||
return true;
|
||||
}
|
||||
if (item is PdfArray arr && arr.Elements.Count == 4 &&
|
||||
arr.Elements[0] is PdfReal or PdfInteger && arr.Elements[1] is PdfReal or PdfInteger &&
|
||||
arr.Elements[2] is PdfReal or PdfInteger && arr.Elements[3] is PdfReal or PdfInteger)
|
||||
{
|
||||
double ax = RectNum(arr.Elements[0]), ay = RectNum(arr.Elements[1]);
|
||||
double bx = RectNum(arr.Elements[2]), by = RectNum(arr.Elements[3]);
|
||||
x1 = Math.Min(ax, bx);
|
||||
y1 = Math.Min(ay, by);
|
||||
x2 = Math.Max(ax, bx);
|
||||
y2 = Math.Max(ay, by);
|
||||
return true;
|
||||
}
|
||||
x1 = y1 = x2 = y2 = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// A KillerPDF save fully REWRITES the file, which mathematically invalidates any existing
|
||||
// digital signature: its /ByteRange and digest describe the old bytes (ISO 19005-2, 6.4.3
|
||||
// requires the digest to cover the entire file). Carrying the dead signature forward
|
||||
// misleads viewers and fails PDF/A validation, so strip signature VALUES (/V) from
|
||||
// signature fields and the catalog's /Perms certification (DocMDP / usage rights) that
|
||||
// references them. The empty fields stay and can be re-signed via Sign Document.
|
||||
// Called before every save of the working document.
|
||||
internal static void ScrubDeadSignatures(PdfDocument doc)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cat = doc.Internals.Catalog;
|
||||
cat.Elements.Remove("/Perms");
|
||||
if (DerefItemStatic(cat.Elements["/AcroForm"]) is not PdfDictionary acro) return;
|
||||
if (DerefItemStatic(acro.Elements["/Fields"]) is PdfArray fields)
|
||||
ScrubSigFieldValues(fields, 0);
|
||||
}
|
||||
catch { /* malformed catalog - leave the save as-is */ }
|
||||
}
|
||||
|
||||
private static void ScrubSigFieldValues(PdfArray fields, int depth)
|
||||
{
|
||||
if (depth > 8) return; // defensive: malformed circular /Kids
|
||||
foreach (var item in fields.Elements)
|
||||
{
|
||||
if (DerefItemStatic(item) is not PdfDictionary field) continue;
|
||||
if (field.Elements.GetName("/FT") == "/Sig" && field.Elements["/V"] != null)
|
||||
field.Elements.Remove("/V");
|
||||
if (DerefItemStatic(field.Elements["/Kids"]) is PdfArray kids)
|
||||
ScrubSigFieldValues(kids, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips visual styling (border, color, appearance stream) from all Link annotations
|
||||
/// in the document so they render as invisible clickable areas rather than colored
|
||||
/// rectangles that can look like strikethroughs in other PDF viewers.
|
||||
/// </summary>
|
||||
internal static void StripLinkAnnotationBorders(PdfDocument doc)
|
||||
{
|
||||
foreach (var pdfPage in doc.Pages)
|
||||
{
|
||||
var annotsArr = pdfPage.Elements.GetArray("/Annots");
|
||||
if (annotsArr is null) continue;
|
||||
for (int i = 0; i < annotsArr.Elements.Count; i++)
|
||||
{
|
||||
PdfItem? elem = annotsArr.Elements[i];
|
||||
PdfDictionary? ann = elem as PdfDictionary ?? DerefItemStatic(elem) as PdfDictionary;
|
||||
if (ann is null) continue;
|
||||
|
||||
// Dereference subtype in case it is an indirect name.
|
||||
var subtypeItem = ann.Elements["/Subtype"];
|
||||
var subtype = (subtypeItem as PdfDictionary ?? DerefItemStatic(subtypeItem) as PdfDictionary) is null
|
||||
? subtypeItem?.ToString() ?? ""
|
||||
: "";
|
||||
if (!subtype.Contains("Link")) continue;
|
||||
|
||||
// Remove appearance stream and color.
|
||||
ann.Elements.Remove("/AP");
|
||||
ann.Elements.Remove("/C");
|
||||
|
||||
// /BS (border style dict) takes precedence over /Border in PDF spec;
|
||||
// set W=0 explicitly. Also set /Border [0 0 0] for older viewers.
|
||||
var bs = new PdfDictionary();
|
||||
bs.Elements["/W"] = new PdfInteger(0);
|
||||
ann.Elements["/BS"] = bs;
|
||||
|
||||
var borderArr = new PdfArray();
|
||||
borderArr.Elements.Add(new PdfInteger(0));
|
||||
borderArr.Elements.Add(new PdfInteger(0));
|
||||
borderArr.Elements.Add(new PdfInteger(0));
|
||||
ann.Elements["/Border"] = borderArr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user