mirror of
https://github.com/wmjordan/PDFPatcher.git
synced 2026-10-18 13:42:28 +08:00
545 lines
17 KiB
C#
545 lines
17 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using iTextSharp.text.pdf;
|
||
using PDFPatcher.Common;
|
||
using PDFPatcher.Processor;
|
||
|
||
namespace PDFPatcher.Model
|
||
{
|
||
[System.Diagnostics.DebuggerDisplay("Name = {Name}({FriendlyName}); Value = {Value}; {HasChildren}")]
|
||
public sealed class DocumentObject : IHierarchicalObject<DocumentObject>
|
||
{
|
||
static readonly string[] __ReversalRefNames = ["Parent", "Prev", "First", "Last", "P"];
|
||
static readonly int[] __CompoundTypes = [PdfObject.DICTIONARY, PdfObject.ARRAY, PdfObject.STREAM];
|
||
static readonly DocumentObject[] __Leaf = [];
|
||
|
||
internal PdfPathDocument OwnerDocument { get; }
|
||
internal DocumentObject Parent { get; }
|
||
internal string Name { get; set; }
|
||
internal PdfObject Value { get; set; }
|
||
internal string Description { get; set; }
|
||
internal object ExtensiveObject { get; set; }
|
||
internal PdfObjectType Type { get; private set; }
|
||
internal bool IsKeyObject { get; set; }
|
||
internal object ImageKey { get; set; }
|
||
/// <summary>
|
||
/// 获取友好形式的名称。
|
||
/// </summary>
|
||
internal string FriendlyName { get; set; }
|
||
/// <summary>
|
||
/// 获取友好形式的值。
|
||
/// </summary>
|
||
internal string FriendlyValue { get; set; }
|
||
|
||
public string LiteralValue => GetItemValueText(Value, ExtensiveObject as PdfObject);
|
||
public bool HasChildren {
|
||
get {
|
||
if (Type != PdfObjectType.Normal
|
||
&& (Type == PdfObjectType.Trailer
|
||
|| Type == PdfObjectType.Pages
|
||
|| Type == PdfObjectType.Page
|
||
|| Type == PdfObjectType.PageCommands
|
||
|| Type == PdfObjectType.Hidden
|
||
|| Type == PdfObjectType.PageCommand && Children.Count > 0)) {
|
||
return true;
|
||
}
|
||
var po = Value ?? ExtensiveObject as PdfObject;
|
||
if (po == null) {
|
||
return false;
|
||
}
|
||
if (PdfHelper.CompoundTypes.Contains(po.Type)) {
|
||
return true;
|
||
}
|
||
else if (po.Type == PdfObject.INDIRECT) {
|
||
if (Type == PdfObjectType.GoToPage) {
|
||
return false;
|
||
}
|
||
if (ExtensiveObject is not PdfObject r) {
|
||
return false;
|
||
}
|
||
if (r.Type == PdfObject.DICTIONARY && Parent.Type == PdfObjectType.Outline && Name == "Next") {
|
||
return false;
|
||
}
|
||
return r.Type == PdfObject.DICTIONARY && __ReversalRefNames.Contains(Name) == false
|
||
|| r.Type == PdfObject.ARRAY
|
||
|| r.Type == PdfObject.STREAM
|
||
;
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
IList<DocumentObject> _Children;
|
||
public ICollection<DocumentObject> Children {
|
||
get {
|
||
if (_Children == null) {
|
||
PopulateChildren(false);
|
||
_Children ??= __Leaf;
|
||
}
|
||
return _Children;
|
||
}
|
||
}
|
||
|
||
internal DocumentObject(PdfPathDocument ownerDocument, DocumentObject parent, string name, PdfObject value) :
|
||
this(ownerDocument, parent, name, value, PdfObjectType.Normal) {
|
||
}
|
||
internal DocumentObject(PdfPathDocument ownerDocument, DocumentObject parent, string name, PdfObject value, PdfObjectType type) {
|
||
OwnerDocument = ownerDocument;
|
||
Parent = parent;
|
||
if (value != null) {
|
||
if (value.Type == PdfObject.INDIRECT) {
|
||
var r = PdfReader.GetPdfObjectRelease(value);
|
||
if (r != null) {
|
||
ExtensiveObject = r;
|
||
if (r.Type == PdfObject.DICTIONARY) {
|
||
int page = ownerDocument.GetPageNumber(value as PdfIndirectReference);
|
||
if (page > 0) {
|
||
Description = $"指向第 {page} 页";
|
||
type = PdfObjectType.GoToPage;
|
||
}
|
||
else if (PdfName.FONT.Equals(((PdfDictionary)r).GetAsName(PdfName.TYPE)) && parent.Name == "Font") {
|
||
var fontName = ((PdfDictionary)r).GetAsName(PdfName.BASEFONT);
|
||
if (fontName != null) {
|
||
Description = PdfName.DecodeName(fontName.ToString());
|
||
}
|
||
}
|
||
}
|
||
else if (r.Type == PdfObject.STREAM) {
|
||
var subType = ((PdfDictionary)r).GetAsName(PdfName.SUBTYPE);
|
||
if (PdfName.IMAGE.Equals(subType)) {
|
||
type = PdfObjectType.Image;
|
||
}
|
||
else if (PdfName.FORM.Equals(subType)) {
|
||
type = PdfObjectType.Form;
|
||
}
|
||
else if (parent?.Name == "AP"
|
||
&& PdfName.ANNOT.Equals((PdfReader.GetPdfObjectRelease(parent.Parent?.Value) as PdfDictionary)?.GetAsName(PdfName.TYPE))) {
|
||
type = PdfObjectType.Form;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else if (value.Type == PdfObject.DICTIONARY) {
|
||
if (parent != null && (parent.Type == PdfObjectType.Page || parent.Type == PdfObjectType.Form) && name == "Resources") {
|
||
type = PdfObjectType.Resources;
|
||
}
|
||
}
|
||
}
|
||
Name = name; Value = value; Type = type;
|
||
}
|
||
|
||
internal bool RemoveChildByName(string name) {
|
||
if (!HasChildren) {
|
||
return false;
|
||
}
|
||
for (int i = _Children.Count - 1; i >= 0; i--) {
|
||
if (_Children[i].Name == name) {
|
||
if (_Children is Array) {
|
||
_Children = new List<DocumentObject>(_Children);
|
||
}
|
||
_Children.RemoveAt(i);
|
||
var po = Value;
|
||
if (po != null) {
|
||
while (po.Type == PdfObject.INDIRECT) {
|
||
po = PdfReader.GetPdfObject(po);
|
||
}
|
||
if (po.Type == PdfObject.ARRAY) {
|
||
((PdfArray)po).Remove(i);
|
||
}
|
||
else if (po.Type == PdfObject.DICTIONARY || po.Type == PdfObject.STREAM) {
|
||
((PdfDictionary)po).Remove(new PdfName(name));
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
internal DocumentObject FindReferenceAncestor() {
|
||
var d = this;
|
||
do {
|
||
if (d.Value?.Type == PdfObject.INDIRECT) {
|
||
return d;
|
||
}
|
||
} while ((d = d.Parent) != null);
|
||
return null;
|
||
}
|
||
|
||
internal bool UpdateDocumentObject(object value) {
|
||
if (Value is not PdfObject po) {
|
||
return false;
|
||
}
|
||
switch (po.Type) {
|
||
case PdfObject.STRING:
|
||
var s = value as string;
|
||
if (s == ((PdfString)po).ToUnicodeString()) {
|
||
break;
|
||
}
|
||
Value = s.ToPdfString();
|
||
break;
|
||
case PdfObject.NUMBER:
|
||
double n;
|
||
if (((string)value).TryParse(out n)) {
|
||
Value = new PdfNumber(n);
|
||
break;
|
||
}
|
||
return false;
|
||
case PdfObject.NAME:
|
||
Value = new PdfName((string)value); break;
|
||
case PdfObject.BOOLEAN:
|
||
Value = new PdfBoolean((bool)value); break;
|
||
}
|
||
if (Parent != null) {
|
||
if ((Parent.ExtensiveObject ?? Parent.Value) is PdfDictionary pd) {
|
||
pd.Put(new PdfName(Name), Value);
|
||
_Children = null;
|
||
return true;
|
||
}
|
||
if ((Parent.ExtensiveObject ?? Parent.Value) is PdfArray pa) {
|
||
pa.ArrayList[Int32.Parse(Name) - 1] = Value;
|
||
_Children = null;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
DocumentObject GetPageObject() {
|
||
var p = this;
|
||
while (p?.Type != PdfObjectType.Page) {
|
||
p = p.Parent;
|
||
}
|
||
return p;
|
||
}
|
||
|
||
private static string GetItemValueText(PdfObject po, PdfObject eo) {
|
||
if (po == null && eo == null) {
|
||
goto Exit;
|
||
}
|
||
if (po == null) {
|
||
po = eo;
|
||
eo = null;
|
||
}
|
||
switch (po.Type) {
|
||
case PdfObject.DICTIONARY: return $"<<{((PdfDictionary)po).Size} 子项>>";
|
||
case PdfObject.INDIRECT:
|
||
return eo == null || __CompoundTypes.Contains(eo.Type)
|
||
? ((PdfIndirectReference)po).ToString()
|
||
: $"{(PdfIndirectReference)po}→{GetItemValueText(null, eo)}";
|
||
case PdfObject.NAME: return PdfHelper.DecodeKeyName(po);
|
||
case PdfObject.NUMBER: return ((PdfNumber)po).DoubleValue.ToText();
|
||
case PdfObject.STRING: return ((PdfString)po).Decode(null);
|
||
case PdfObject.STREAM: goto case PdfObject.DICTIONARY;
|
||
case PdfObject.ARRAY: return PdfHelper.GetArrayString((PdfArray)po);
|
||
case PdfObject.BOOLEAN: return ((PdfBoolean)po).ToString();
|
||
case PdfObject.NULL: return "Null";
|
||
}
|
||
Exit:
|
||
return null;
|
||
}
|
||
|
||
internal string GetContextName() {
|
||
var d = this;
|
||
string contextName = null;
|
||
if (d.Type != PdfObjectType.Normal) {
|
||
switch (d.Type) {
|
||
case PdfObjectType.Page:
|
||
return "Page";
|
||
case PdfObjectType.Image:
|
||
return "Image";
|
||
case PdfObjectType.Form:
|
||
return "Form";
|
||
}
|
||
}
|
||
while ((!d.IsKeyObject || String.IsNullOrEmpty(contextName = d.Name)) && (d = d.Parent) != null) {
|
||
}
|
||
return contextName;
|
||
}
|
||
|
||
internal IList<DocumentObject> PopulateChildren(bool refresh) {
|
||
if (refresh) {
|
||
_Children = null;
|
||
}
|
||
if (_Children == null) {
|
||
if (Type == PdfObjectType.Page && Value == null) {
|
||
Value = OwnerDocument.Document.GetPageN((int)ExtensiveObject);
|
||
}
|
||
else if (Type != PdfObjectType.Normal) {
|
||
PopulateChildrenForSpecialObject();
|
||
}
|
||
if (_Children == null) {
|
||
PopulateChildrenForNormalObject();
|
||
}
|
||
}
|
||
return _Children;
|
||
}
|
||
|
||
private void PopulateChildrenForNormalObject() {
|
||
var po = (ExtensiveObject as PdfObject) ?? Value;
|
||
_Children = __Leaf;
|
||
if (po == null) {
|
||
return;
|
||
}
|
||
if (po.Type == PdfObject.DICTIONARY || po.Type == PdfObject.STREAM) {
|
||
var pd = po as PdfDictionary;
|
||
var cs = Type == PdfObjectType.Page || Type == PdfObjectType.Form; // 是否有 content stream
|
||
var r = new DocumentObject[pd.Size + (cs ? 1 : 0)];
|
||
var n = 0;
|
||
foreach (var item in pd) {
|
||
var d = new DocumentObject(OwnerDocument, this, PdfHelper.DecodeKeyName(item.Key), item.Value);
|
||
r[n++] = d;
|
||
var i = PdfStructInfo.GetInfo(GetContextName(), d.Name);
|
||
if (i.Name != null && i.IsKeyObject) {
|
||
d.IsKeyObject = true;
|
||
}
|
||
if (String.IsNullOrEmpty(i.ImageKey) == false) {
|
||
d.ImageKey = i.ImageKey;
|
||
}
|
||
}
|
||
if (cs) {
|
||
r[n++] = new DocumentObject(OwnerDocument, this, Constants.Content.Operators, null, PdfObjectType.PageCommands) { IsKeyObject = true };
|
||
}
|
||
else {
|
||
switch (Type) {
|
||
case PdfObjectType.Trailer: {
|
||
var d = Array.Find(r, (o) => o.Name == "Root");
|
||
if (d != null) {
|
||
d.Type = PdfObjectType.Root;
|
||
}
|
||
}
|
||
break;
|
||
case PdfObjectType.Root: {
|
||
var d = Array.Find(r, (o) => o.Name == "Outlines");
|
||
if (d != null) {
|
||
d.Type = PdfObjectType.Outline;
|
||
}
|
||
}
|
||
break;
|
||
case PdfObjectType.Outline: {
|
||
var o = new List<DocumentObject>(r);
|
||
var or = pd.Get(PdfName.FIRST);
|
||
pd = PdfReader.GetPdfObject(or) as PdfDictionary;
|
||
if (pd != null) {
|
||
o.Add(new DocumentObject(OwnerDocument, this, Constants.Bookmark, or, PdfObjectType.Outline) {
|
||
Description = pd.Contains(PdfName.TITLE) ? pd.GetAsString(PdfName.TITLE).ToUnicodeString() : null
|
||
});
|
||
while ((or = pd.Get(PdfName.NEXT)) != null
|
||
&& (pd = PdfReader.GetPdfObject(or) as PdfDictionary) != null) {
|
||
o.Add(new DocumentObject(OwnerDocument, this, Constants.Bookmark, or, PdfObjectType.Outline) {
|
||
Description = pd.Contains(PdfName.TITLE) ? pd.GetAsString(PdfName.TITLE).ToUnicodeString() : null
|
||
});
|
||
}
|
||
}
|
||
_Children = o;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
_Children = r;
|
||
}
|
||
else if (po.Type == PdfObject.ARRAY) {
|
||
var pd = (PdfArray)po;
|
||
var r = new DocumentObject[pd.Size];
|
||
var n = 0;
|
||
foreach (var item in pd.ArrayList) {
|
||
var d = new DocumentObject(OwnerDocument, this, (++n).ToText(), item);
|
||
r[n - 1] = d;
|
||
var i = PdfStructInfo.GetInfo(GetContextName(), d.Name);
|
||
if (i.Name != null && i.IsKeyObject) {
|
||
d.IsKeyObject = true;
|
||
}
|
||
if (String.IsNullOrEmpty(i.ImageKey) == false) {
|
||
d.ImageKey = i.ImageKey;
|
||
}
|
||
}
|
||
_Children = r;
|
||
}
|
||
}
|
||
|
||
private void PopulateChildrenForSpecialObject() {
|
||
var pdf = OwnerDocument.Document;
|
||
switch (Type) {
|
||
case PdfObjectType.Pages: {
|
||
if (pdf.NumberOfPages == 0) {
|
||
return;
|
||
}
|
||
var r = PageRangeCollection.Parse(ExtensiveObject as string, 1, pdf.NumberOfPages, true);
|
||
var pn = new DocumentObject[r.TotalPages];
|
||
var i = 0;
|
||
foreach (var item in r) {
|
||
foreach (var p in item) {
|
||
pn[i++] = new DocumentObject(OwnerDocument, this, $"第{p}页", null, PdfObjectType.Page) { ExtensiveObject = p };
|
||
}
|
||
}
|
||
_Children = pn;
|
||
break;
|
||
}
|
||
case PdfObjectType.PageCommands: {
|
||
// 解释页面指令
|
||
var cp = new PdfPageCommandProcessor();
|
||
if (Parent.Type == PdfObjectType.Page) {
|
||
var pn = (int)Parent.ExtensiveObject;
|
||
cp.ProcessContent(pdf.GetPageContent(pn), pdf.GetPageN(pn).GetAsDict(PdfName.RESOURCES));
|
||
}
|
||
else if (Parent.Type == PdfObjectType.Form) {
|
||
var form = PdfReader.GetPdfObjectRelease(Parent.Value) as PRStream;
|
||
cp.ProcessContent(PdfReader.GetStreamBytes(form),
|
||
new CompositePdfDictionary(form.GetAsDict(PdfName.RESOURCES),
|
||
pdf.GetPageN((int)GetPageObject().ExtensiveObject).GetAsDict(PdfName.RESOURCES))
|
||
);
|
||
}
|
||
foreach (var item in cp.Commands) {
|
||
PopulatePageCommand(item);
|
||
}
|
||
if (cp.LastError != null) {
|
||
Description = cp.LastError;
|
||
}
|
||
break;
|
||
}
|
||
case PdfObjectType.PageCommand:
|
||
_Children = __Leaf;
|
||
break;
|
||
case PdfObjectType.Hidden: {
|
||
var ul = PdfHelper.ListUnusedObjects(pdf, AppContext.LoadPartialPdfFile);
|
||
ExtensiveObject = ul;
|
||
var uo = new List<DocumentObject>();
|
||
foreach (var item in ul) {
|
||
var u = pdf.GetPdfObjectRelease(item);
|
||
if (u != null) {
|
||
uo.Add(new DocumentObject(OwnerDocument, this, item.ToText(), u));
|
||
}
|
||
}
|
||
_Children = uo;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void PopulatePageCommand(PdfPageCommand item) {
|
||
string fn;
|
||
var op = item.Name.ToString();
|
||
if (!PdfPageCommand.GetFriendlyCommandName(op, out fn)) {
|
||
fn = "未知操作符";
|
||
}
|
||
var o = new DocumentObject(OwnerDocument, this, fn, null, PdfObjectType.PageCommand) {
|
||
FriendlyName = fn + "(" + op + ")",
|
||
ExtensiveObject = op
|
||
};
|
||
if (item.Type == PdfPageCommandType.Text) {
|
||
var t = item as TextCommand;
|
||
o.FriendlyValue = t.TextInfo.PdfString.GetFriendlyValue();
|
||
o.Description = t.TextInfo.Text;
|
||
if (item.Name.ToString() == "TJ") {
|
||
var a = item.Operands[0] as PdfArray;
|
||
if (a.Size > 0) {
|
||
var pt = item as PaceAndTextCommand;
|
||
var i = 0;
|
||
CreateChildrenList(ref o._Children);
|
||
foreach (var ti in a.ArrayList) {
|
||
var d = new DocumentObject(OwnerDocument, o, (++i).ToText(), ti);
|
||
if (ti.Type == PdfObject.STRING) {
|
||
d.FriendlyValue = ((PdfString)ti).GetFriendlyValue();
|
||
d.Description = pt.DecodedTexts[i - 1];
|
||
}
|
||
o._Children.Add(d);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else if (item.Type == PdfPageCommandType.Font) {
|
||
var f = item as FontCommand;
|
||
o.FriendlyValue = String.Concat(
|
||
Constants.Content.OperandNames.ResourceName, ":", f.ResourceName.ToString(), "; ",
|
||
Constants.Content.OperandNames.Size, ":", f.FontSize.DoubleValue.ToText()
|
||
);
|
||
o.Description = f.FontName;
|
||
}
|
||
else if (item.Type == PdfPageCommandType.Enclosure) {
|
||
if (item.Operands.HasContent()) {
|
||
var i = 0;
|
||
CreateChildrenList(ref o._Children);
|
||
foreach (var t in item.Operands) {
|
||
o._Children.Add(new DocumentObject(OwnerDocument, o, (++i).ToText(), t));
|
||
}
|
||
}
|
||
var e = (EnclosingCommand)item;
|
||
if (!e.HasCommand) {
|
||
return;
|
||
}
|
||
foreach (var cmd in e.Commands) {
|
||
o.PopulatePageCommand(cmd);
|
||
}
|
||
}
|
||
else if (item.Type == PdfPageCommandType.InlineImage) {
|
||
var s = (PdfImageData)item.Operands[0];
|
||
CreateChildrenList(ref o._Children);
|
||
foreach (var ii in s) {
|
||
o._Children.Add(new DocumentObject(OwnerDocument, o, PdfHelper.DecodeKeyName(ii.Key), ii.Value));
|
||
}
|
||
}
|
||
else if (item.Type == PdfPageCommandType.Invalid) {
|
||
o.Description = ((InvalidCommand)item).Error;
|
||
o.ExtensiveObject = "?";
|
||
if (item.Operands.HasContent()) {
|
||
var i = 0;
|
||
CreateChildrenList(ref o._Children);
|
||
foreach (var t in item.Operands) {
|
||
o._Children.Add(new DocumentObject(OwnerDocument, o, (++i).ToText(), t));
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
o.FriendlyValue = item.GetOperandsText();
|
||
}
|
||
CreateChildrenList(ref _Children);
|
||
_Children.Add(o);
|
||
}
|
||
|
||
static void CreateChildrenList(ref IList<DocumentObject> list) {
|
||
if (list == null || list == __Leaf) {
|
||
list = new List<DocumentObject>();
|
||
}
|
||
}
|
||
|
||
sealed class CompositePdfDictionary(PdfDictionary primary, PdfDictionary auxiliary) : PdfDictionary
|
||
{
|
||
public new IEnumerable<PdfName> Keys => GetKeyValues().Select(i => i.Key);
|
||
public override int Size => (primary?.Size ?? 0) + (auxiliary?.Size ?? 0);
|
||
public override PdfObject GetDirectObject(PdfName key) {
|
||
var po = primary?.GetDirectObject(key);
|
||
if (po is PdfDictionary pd) {
|
||
if (auxiliary != null) {
|
||
var ao = auxiliary.GetDirectObject(key);
|
||
return ao is PdfDictionary ad ? new CompositePdfDictionary(pd, ad) : pd;
|
||
}
|
||
else {
|
||
return pd;
|
||
}
|
||
}
|
||
return primary?.GetDirectObject(key) ?? auxiliary?.GetDirectObject(key);
|
||
}
|
||
public override bool Contains(PdfName key) {
|
||
return primary?.Contains(key) ?? auxiliary?.Contains(key) ?? false;
|
||
}
|
||
public new IEnumerator<KeyValuePair<PdfName, PdfObject>> GetEnumerator() {
|
||
return GetKeyValues().GetEnumerator();
|
||
}
|
||
public IEnumerable<KeyValuePair<PdfName, PdfObject>> GetKeyValues() {
|
||
if (primary != null) {
|
||
foreach (var item in primary) {
|
||
yield return new KeyValuePair<PdfName, PdfObject>(item.Key, item.Value);
|
||
}
|
||
}
|
||
if (auxiliary != null) {
|
||
foreach (var item in auxiliary) {
|
||
if (primary?.Contains(item.Key) == false) {
|
||
yield return new KeyValuePair<PdfName, PdfObject>(item.Key, item.Value);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|