mirror of
https://github.com/wmjordan/PDFPatcher.git
synced 2026-10-19 06:21:06 +08:00
727 lines
23 KiB
C#
727 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Xml;
|
|
using iTextSharp.text.pdf;
|
|
using PDFPatcher.Common;
|
|
using PDFPatcher.Model;
|
|
using Matrix = iTextSharp.text.pdf.parser.Matrix;
|
|
using NameValuePair = System.Collections.Generic.KeyValuePair<string, string>;
|
|
|
|
namespace PDFPatcher.Processor
|
|
{
|
|
sealed class PdfContentExport
|
|
{
|
|
readonly ExporterOptions _options;
|
|
readonly Dictionary<string, string> _resolvedReferences = [];
|
|
readonly Stack<string> _tags = new Stack<string>();
|
|
readonly Stack<PdfDictionary> _dictionaries = new Stack<PdfDictionary>();
|
|
bool _streamFolderExists;
|
|
ImageExtractor _imageExporter;
|
|
FilePath _exportStreamPath;
|
|
|
|
public PdfContentExport(ExporterOptions options) {
|
|
_options = options;
|
|
}
|
|
|
|
public FilePath BinaryStreamPath {
|
|
get => _exportStreamPath;
|
|
set {
|
|
_exportStreamPath = value;
|
|
_streamFolderExists = value.ExistsDirectory;
|
|
}
|
|
}
|
|
|
|
bool AddReferenceRecord(PdfIndirectReference r, string type) {
|
|
var k = String.Concat(r.Number, ' ', r.Generation);
|
|
if (_resolvedReferences.ContainsKey(k)) {
|
|
return false;
|
|
}
|
|
|
|
_resolvedReferences.Add(k, type);
|
|
return true;
|
|
}
|
|
|
|
internal void ExportTrailer(XmlWriter writer, PdfReader reader) {
|
|
Tracker.TraceMessage("导出文档总索引。");
|
|
writer.WriteStartElement("Trailer");
|
|
writer.WriteAttributeString(Constants.ContentPrefix, "http://www.w3.org/2000/xmlns/", Constants.ContentNamespace);
|
|
foreach (var item in reader.Trailer) {
|
|
if (!PdfName.ROOT.Equals(item.Key)) {
|
|
ExportPdfDictionaryItem(item, writer);
|
|
}
|
|
}
|
|
ExportCatalog(writer, reader);
|
|
writer.WriteEndElement();
|
|
}
|
|
|
|
void ExportCatalog(XmlWriter writer, PdfReader reader) {
|
|
Tracker.TraceMessage("导出文档编录。");
|
|
writer.WriteStartElement(Constants.Catalog);
|
|
writer.WriteAttributeString(Constants.ContentPrefix, "http://www.w3.org/2000/xmlns/", Constants.ContentNamespace);
|
|
foreach (var item in reader.Catalog) {
|
|
if (PdfName.OUTLINES.Equals(item.Key)) {
|
|
if (PdfReader.GetPdfObjectRelease(item.Value) is PdfDictionary o) {
|
|
ExportPdfOutline(o, writer);
|
|
}
|
|
}
|
|
else {
|
|
ExportPdfDictionaryItem(item, writer);
|
|
}
|
|
}
|
|
writer.WriteEndElement();
|
|
}
|
|
|
|
internal void ExportContents(XmlWriter writer, PdfReader reader) {
|
|
if (_options.ExtractImages) {
|
|
_imageExporter = new ImageExtractor(_options.Images);
|
|
}
|
|
writer.WriteStartElement(Constants.Body);
|
|
writer.WriteAttributeString(Constants.ContentPrefix, "http://www.w3.org/2000/xmlns/", Constants.ContentNamespace);
|
|
ExportPages(reader, PageRangeCollection.Parse(_options.ExtractPageRange, 1, reader.NumberOfPages, true), writer);
|
|
writer.WriteEndElement();
|
|
}
|
|
|
|
void ExportPages(PdfReader reader, PageRangeCollection ranges, XmlWriter writer) {
|
|
var p = new ExportProcessor(this, writer, _options);
|
|
foreach (PageRange r in ranges) {
|
|
foreach (var i in r) {
|
|
Tracker.TraceMessage(String.Concat("导出第 ", i, " 页。"));
|
|
ExportPage(i, reader, writer, p);
|
|
Tracker.IncrementProgress(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExportPdfOutline(PdfDictionary outline, XmlWriter writer) {
|
|
while (outline != null) {
|
|
writer.WriteStartElement(Constants.ContentPrefix, "Outline", Constants.ContentNamespace);
|
|
foreach (var i in outline) {
|
|
switch (i.Key.ToString()) {
|
|
case "/First":
|
|
case "/Last":
|
|
case "/Next":
|
|
case "/Parent":
|
|
case "/Prev":
|
|
continue;
|
|
default:
|
|
ExportPdfDictionaryItem(i, writer);
|
|
break;
|
|
}
|
|
}
|
|
if (PdfReader.GetPdfObjectRelease(outline.Get(PdfName.FIRST)) is PdfDictionary f) {
|
|
writer.WriteStartElement(Constants.ContentPrefix, "Outlines", Constants.ContentNamespace);
|
|
ExportPdfOutline(f, writer);
|
|
writer.WriteEndElement();
|
|
}
|
|
writer.WriteEndElement();
|
|
outline = PdfReader.GetPdfObjectRelease(outline.Get(PdfName.NEXT)) as PdfDictionary;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 导出 PDFDictionary。
|
|
/// </summary>
|
|
/// <param name="writer"></param>
|
|
/// <param name="dict"></param>
|
|
void ExportPdfDictionary(XmlWriter writer, PdfDictionary dict) {
|
|
_dictionaries.Push(dict);
|
|
foreach (var item in dict) {
|
|
ExportPdfDictionaryItem(item, writer);
|
|
}
|
|
_dictionaries.Pop();
|
|
}
|
|
|
|
void ExportPdfDictionaryItem(KeyValuePair<PdfName, PdfObject> item, XmlWriter writer) {
|
|
var key = PdfHelper.DecodeKeyName(item.Key);
|
|
var value = item.Value;
|
|
try {
|
|
writer.WriteStartElement(XmlConvert.VerifyNCName(key));
|
|
}
|
|
catch (XmlException) {
|
|
writer.WriteStartElement(Constants.Content.Item, Constants.ContentNamespace);
|
|
writer.WriteAttributeString(Constants.Content.Name, Constants.ContentNamespace, key);
|
|
}
|
|
_tags.Push(key);
|
|
if (value == null) {
|
|
writer.WriteAttributeString(PdfHelper.GetTypeName(value.Type), "null");
|
|
goto EndElement;
|
|
}
|
|
switch (value.Type) {
|
|
case PdfObject.ARRAY:
|
|
var a = value as PdfArray;
|
|
if (key == "ColorSpace" && PdfName.INDEXED.Equals(a.GetAsName(0)) && a.ArrayList.Count == 4) {
|
|
ExportColorSpaceContent(writer, a);
|
|
}
|
|
else {
|
|
ExportArray(a.ArrayList, writer);
|
|
}
|
|
break;
|
|
case PdfObject.STRING:
|
|
var text = (value as PdfString).ToUnicodeString();
|
|
if (text.StartsWith("<?xml")) {
|
|
writer.WriteStartElement(Constants.ContentPrefix, Constants.Content.Value, Constants.ContentNamespace);
|
|
writer.WriteCData(text);
|
|
writer.WriteEndElement();
|
|
}
|
|
else {
|
|
writer.WriteAttributeString(PdfHelper.GetTypeName(value.Type), PdfHelper.GetValidXmlString(text));
|
|
}
|
|
break;
|
|
case PdfObject.INDIRECT:
|
|
writer.WriteAttributeString(Constants.Content.Type, Constants.ContentNamespace, PdfHelper.GetTypeName(PdfObject.INDIRECT));
|
|
ExportIndirectReference(value as PdfIndirectReference, writer, key);
|
|
break;
|
|
case PdfObject.DICTIONARY:
|
|
ExportPdfDictionary(writer, value as PdfDictionary);
|
|
break;
|
|
default:
|
|
writer.WriteAttributeString(PdfHelper.GetTypeName(value.Type), value.ToString());
|
|
break;
|
|
}
|
|
EndElement:
|
|
writer.WriteEndElement();
|
|
_tags.Pop();
|
|
}
|
|
|
|
void ExportColorSpaceContent(XmlWriter writer, PdfArray a) {
|
|
writer.WriteStartElement(Constants.ContentPrefix, "colorSpace", Constants.ContentNamespace);
|
|
writer.WriteAttributeString("type", a[0].ToString());
|
|
var b = a.GetAsName(1);
|
|
if (b != null) {
|
|
writer.WriteAttributeString("base", a.GetAsName(1).ToString());
|
|
}
|
|
writer.WriteAttributeString("hival", a.GetAsNumber(2).ToString());
|
|
writer.WriteStartAttribute("lookup");
|
|
var cs = a.GetDirectObject(3);
|
|
byte[] l = null;
|
|
if (cs.IsString()) {
|
|
l = (cs as PdfString).GetOriginalBytes();
|
|
}
|
|
else if (cs.IsStream()) {
|
|
l = PdfReader.GetStreamBytes(cs as PRStream);
|
|
}
|
|
if (l != null) {
|
|
int g;
|
|
if (b == null) {
|
|
g = l.Length;
|
|
}
|
|
else if (b.Equals(PdfName.DEVICERGB)) {
|
|
g = 3;
|
|
}
|
|
else if (b.Equals(PdfName.DEVICECMYK)) {
|
|
g = 4;
|
|
}
|
|
else {
|
|
g = l.Length;
|
|
}
|
|
for (int i = 0; i < l.Length; i += g) {
|
|
writer.WriteBinHex(l, i, g);
|
|
writer.WriteString(" ");
|
|
}
|
|
}
|
|
writer.WriteEndAttribute();
|
|
if (a[1].IsArray()) {
|
|
ExportArray(a.GetAsArray(1).ArrayList, writer);
|
|
}
|
|
writer.WriteEndElement();
|
|
}
|
|
|
|
void ExportArray(List<PdfObject> array, XmlWriter writer) {
|
|
writer.WriteStartElement(Constants.ContentPrefix, "array", Constants.ContentNamespace);
|
|
ExportArrayContent(array, writer, false);
|
|
writer.WriteEndElement();
|
|
}
|
|
|
|
void ExportArrayContent(List<PdfObject> array, XmlWriter writer, bool writeStringBytes) {
|
|
int iType = PdfObject.NULL;
|
|
foreach (PdfObject i in array) {
|
|
if (iType == PdfObject.NULL) {
|
|
iType = i.Type;
|
|
}
|
|
else if (iType != i.Type) {
|
|
iType = PdfObject.NULL;
|
|
break;
|
|
}
|
|
}
|
|
if (ValueHelper.IsInCollection(iType, PdfObject.NAME, PdfObject.NUMBER, PdfObject.BOOLEAN)) {
|
|
writer.WriteAttributeString(Constants.Content.Type, PdfHelper.GetTypeName(iType));
|
|
writer.WriteStartAttribute(Constants.Content.Value);
|
|
var l = array;
|
|
for (int i = 0; i < l.Count; i++) {
|
|
if (i > 0) {
|
|
writer.WriteString(" ");
|
|
}
|
|
writer.WriteString(l[i].ToString());
|
|
}
|
|
writer.WriteEndAttribute();
|
|
}
|
|
else {
|
|
foreach (PdfObject i in array) {
|
|
if (i is not PdfArray subArray) {
|
|
if (PdfHelper.GetTypeName(i.Type).Length > 0) {
|
|
writer.WriteStartElement(Constants.ContentPrefix, PdfHelper.GetTypeName(i.Type), Constants.ContentNamespace);
|
|
}
|
|
#if DEBUG
|
|
else {
|
|
writer.WriteElementString("literal", i.ToString());
|
|
continue;
|
|
}
|
|
#endif
|
|
switch (i.Type) {
|
|
case PdfObject.INDIRECT:
|
|
ExportIndirectReference(i as PdfIndirectReference, writer, String.Empty);
|
|
break;
|
|
case PdfObject.DICTIONARY:
|
|
ExportPdfDictionary(writer, i as PdfDictionary);
|
|
break;
|
|
case PdfObject.STRING:
|
|
if (writeStringBytes) {
|
|
var bytes = (i as PdfString).GetBytes();
|
|
var bl = bytes.Length;
|
|
if (bl > 0) {
|
|
int l = 2 * bl + ((bl + 1) >> 1) - 1;
|
|
var chArray = new char[l];
|
|
int num = 0, bi = 0;
|
|
foreach (byte b in bytes) {
|
|
if ((bi % 2) == 0 && bi > 0) {
|
|
chArray[num++] = ' ';
|
|
}
|
|
chArray[num++] = "0123456789ABCDEF"[b >> 4];
|
|
chArray[num++] = "0123456789ABCDEF"[b & 15];
|
|
bi++;
|
|
}
|
|
writer.WriteAttributeString(Constants.Content.Type, "字节");
|
|
writer.WriteAttributeString(Constants.Content.Value, new string(chArray));
|
|
}
|
|
break;
|
|
}
|
|
goto default;
|
|
default:
|
|
writer.WriteAttributeString(Constants.Content.Value, PdfHelper.GetValidXmlString(i.ToString()));
|
|
break;
|
|
}
|
|
writer.WriteEndElement();
|
|
}
|
|
else {
|
|
writer.WriteStartElement(Constants.ContentPrefix, "array", Constants.ContentNamespace);
|
|
ExportArrayContent(subArray.ArrayList, writer, writeStringBytes);
|
|
writer.WriteEndElement();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExportIndirectReference(PdfIndirectReference r, XmlWriter writer, string key) {
|
|
var obj = PdfReader.GetPdfObjectRelease(r);
|
|
writer.WriteAttributeString(Constants.Content.ResourceID, Constants.ContentNamespace, r.ToString());
|
|
if (!AddReferenceRecord(r, key) || obj == null) {
|
|
return;
|
|
}
|
|
switch (obj.Type) {
|
|
case PdfObject.DICTIONARY:
|
|
var type = (obj as PdfDictionary).GetAsName(PdfName.TYPE);
|
|
if (type != null) {
|
|
var n = PdfHelper.GetPdfNameString(type);
|
|
writer.WriteAttributeString(Constants.Content.RefType, Constants.ContentNamespace, n);
|
|
switch (n) {
|
|
case "Page":
|
|
case "Pages":
|
|
return;
|
|
default:
|
|
if (key == "Parent") {
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
ExportPdfDictionary(writer, obj as PdfDictionary);
|
|
break;
|
|
case PdfObject.ARRAY:
|
|
ExportArray((obj as PdfArray).ArrayList, writer);
|
|
break;
|
|
case PdfObject.STREAM:
|
|
var s = obj as PdfStream;
|
|
ExportPdfDictionary(writer, s);
|
|
if (_imageExporter != null && PdfName.IMAGE.Equals(s.GetAsName(PdfName.SUBTYPE))) {
|
|
writer.WriteStartElement(Constants.ContentPrefix, "image", Constants.ContentNamespace);
|
|
var info = _imageExporter.InfoList.Find(ii => ii.InlineImage.PdfRef == r);
|
|
if (info != null) {
|
|
writer.WriteAttributeString(Constants.Content.Path, Path.GetFileName(info.FileName));
|
|
}
|
|
writer.WriteEndElement();
|
|
break;
|
|
}
|
|
ExportStreamContent(writer, s, r);
|
|
break;
|
|
default:
|
|
writer.WriteAttributeString(PdfHelper.GetTypeName(obj.Type), Constants.ContentNamespace, obj.ToString());
|
|
break;
|
|
}
|
|
}
|
|
|
|
void ExportStreamContent(XmlWriter writer, PdfStream s, PdfIndirectReference pdfIndirect) {
|
|
if (s is not PRStream prs || writer is NullXmlWriter || _exportStreamPath.IsEmpty) {
|
|
return;
|
|
}
|
|
var key = _tags.Peek();
|
|
var dict = _dictionaries.Peek();
|
|
writer.WriteStartElement(Constants.ContentPrefix, "stream", Constants.ContentNamespace);
|
|
byte[] bs;
|
|
bool isRaw = false;
|
|
var isForm = PdfName.XOBJECT.Equals(s.GetAsName(PdfName.TYPE)) && PdfName.FORM.Equals(s.GetAsName(PdfName.SUBTYPE));
|
|
try {
|
|
bs = PdfReader.GetStreamBytes(prs);
|
|
}
|
|
catch (IOException) {
|
|
bs = PdfReader.GetStreamBytesRaw(prs);
|
|
isRaw = true;
|
|
writer.WriteAttributeString(Constants.Content.Raw, Constants.Boolean.True);
|
|
}
|
|
|
|
if (pdfIndirect != null) {
|
|
writer.WriteAttributeString(Constants.Content.Length, bs.Length.ToText());
|
|
if (bs.Length != 0) {
|
|
if (!_streamFolderExists) {
|
|
_exportStreamPath.CreateDirectory();
|
|
_streamFolderExists = true;
|
|
}
|
|
string extName;
|
|
switch (key) {
|
|
case "ToUnicode":
|
|
case "Contents":
|
|
extName = Constants.FileExtensions.Txt;
|
|
break;
|
|
case "Metadata":
|
|
extName = Constants.FileExtensions.Xml;
|
|
break;
|
|
case "FontFile2":
|
|
extName = Constants.FileExtensions.Ttf;
|
|
var ff = dict.GetAsString(PdfName.FONTFAMILY);
|
|
if (ff != null) {
|
|
key += "." + ff;
|
|
}
|
|
else {
|
|
var fn = dict.GetAsName(PdfName.FONTNAME);
|
|
if (fn != null) {
|
|
key += "." + fn.ToString().Substring(1);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
extName = isForm ? Constants.FileExtensions.Txt : Constants.FileExtensions.Dat;
|
|
break;
|
|
}
|
|
var fileName = _exportStreamPath.Combine(key + "." + pdfIndirect.Number + (pdfIndirect.Generation != 0 ? ("[" + pdfIndirect.ToString() + "]") : null) + extName);
|
|
using (FileStream f = new FileStream(fileName, FileMode.Create)) {
|
|
f.Write(bs, 0, bs.Length);
|
|
}
|
|
writer.WriteAttributeString("file", fileName.FileName);
|
|
if (isRaw) {
|
|
writer.WriteAttributeString("isRaw", "True");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isRaw) {
|
|
if (key == "Contents" || key == "ToUnicode" || isForm) {
|
|
var sb = StringBuilderCache.Acquire();
|
|
byte b;
|
|
int l = bs.Length;
|
|
int p1 = 0, p2 = 0;
|
|
for (int i = 0; i < l; i++) {
|
|
b = bs[i];
|
|
if (b == 0x0A || b == 0x0D || i + 1 == l) {
|
|
p2 = i;
|
|
if (i > 2 && bs[i - 2] == 'T' && (bs[i - 1] == 'J' || bs[i - 1] == 'j')) {
|
|
// is a text operation
|
|
ExportStreamTextContent(bs, sb, p1, p2);
|
|
sb.AppendLine();
|
|
}
|
|
else {
|
|
sb.Append(Encoding.ASCII.GetString(bs, p1, p2 - p1));
|
|
sb.AppendLine();
|
|
}
|
|
if (b == 0x0D && i + 1 < l && bs[i + 1] == 0x0A) {
|
|
i++;
|
|
}
|
|
p1 = i + 1;
|
|
}
|
|
}
|
|
|
|
if (sb.Length == 0) {
|
|
if (l > 2 && bs[l - 2] == 'T' && (bs[l - 1] == 'J' || bs[l - 1] == 'j')) {
|
|
ExportStreamTextContent(bs, sb, 0, l);
|
|
}
|
|
else {
|
|
sb.Append(Encoding.Default.GetString(bs, p1, p2));
|
|
}
|
|
}
|
|
writer.WriteCData(StringBuilderCache.GetStringAndRelease(sb));
|
|
}
|
|
else if (key == "Metadata") {
|
|
using (var ms = new MemoryStream(bs))
|
|
using (var sr = new StreamReader(ms, true)) {
|
|
// strip byte-order-marks
|
|
writer.WriteCData(sr.ReadToEnd().Replace("\uFFFE", "").Replace("\uFEFF", ""));
|
|
}
|
|
}
|
|
}
|
|
writer.WriteEndElement();
|
|
}
|
|
|
|
static void ExportStreamTextContent(byte[] bs, StringBuilder sb, int p1, int p2) {
|
|
bool inText = false;
|
|
bool escape = false;
|
|
char ch;
|
|
int beginTextPosition = 0;
|
|
for (int c = p1; c < p2; c++) {
|
|
ch = (char)bs[c];
|
|
switch (ch) {
|
|
case '\\':
|
|
if (escape) {
|
|
escape = false;
|
|
sb.Append(ch);
|
|
}
|
|
else {
|
|
escape = true;
|
|
}
|
|
continue;
|
|
case '(':
|
|
if (escape) {
|
|
sb.Append(ch);
|
|
escape = false;
|
|
}
|
|
else {
|
|
inText = true;
|
|
beginTextPosition = c;
|
|
sb.Append(ch);
|
|
}
|
|
continue;
|
|
case ')':
|
|
if (escape) {
|
|
escape = false;
|
|
}
|
|
else {
|
|
inText = false;
|
|
//sb.Append (Encoding.BigEndianUnicode.GetString (buffer, beginTextPosition + 1, c - 1 - beginTextPosition));
|
|
}
|
|
sb.Append(ch);
|
|
continue;
|
|
default:
|
|
if (escape) {
|
|
escape = false;
|
|
}
|
|
if (!inText) {
|
|
sb.Append(ch);
|
|
}
|
|
else {
|
|
sb.Append(((byte)ch).ToString("X2"));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void ExportPage(PdfReader reader, XmlWriter writer, params int[] pageNumbers) {
|
|
var p = new ExportProcessor(this, writer, _options);
|
|
foreach (var pageNum in pageNumbers) {
|
|
ExportPage(pageNum, reader, writer, p);
|
|
}
|
|
}
|
|
|
|
void ExportPage(int pageNum, PdfReader reader, XmlWriter writer, ExportProcessor exportProcessor) {
|
|
writer.WriteStartElement(Constants.Content.Page);
|
|
_tags.Push(Constants.Content.Page);
|
|
writer.WriteAttributeString(Constants.Content.PageNumber, pageNum.ToText());
|
|
writer.WriteAttributeString(Constants.Content.ResourceID, reader.GetPageOrigRef(pageNum).ToString());
|
|
_imageExporter?.ExtractPageImages(reader, pageNum);
|
|
|
|
if (_options.ExtractPageDictionary) {
|
|
ExportPdfDictionary(writer, reader.GetPageNRelease(pageNum));
|
|
}
|
|
if (_options.ExportContentOperators || _options.ExportDecodedText) {
|
|
if (_options.ExportContentOperators) {
|
|
writer.WriteStartElement(Constants.Content.Operators);
|
|
}
|
|
exportProcessor.Reset();
|
|
exportProcessor.ProcessContent(reader.GetPageContent(pageNum), reader.GetPageNRelease(pageNum).GetAsDict(PdfName.RESOURCES));
|
|
if (_options.ExportContentOperators) {
|
|
exportProcessor.End();
|
|
writer.WriteEndElement();
|
|
}
|
|
}
|
|
//if (_imageExporter != null && _imageExporter.InfoList.Count > 0) {
|
|
// writer.WriteStartElement ("images", Constants.ContentNamespace);
|
|
// foreach (var item in _imageExporter.InfoList) {
|
|
// writer.WriteStartElement ("image", Constants.ContentNamespace);
|
|
// writer.WriteAttributeString (Constants.Content.ResourceID, Constants.ContentNamespace, item.PdfRef.ToString ());
|
|
// writer.WriteAttributeString ("位置", String.Concat (item.X, ", ", item.Y, ", ", item.Z));
|
|
// writer.WriteEndElement ();
|
|
// }
|
|
// writer.WriteEndElement ();
|
|
//}
|
|
writer.WriteEndElement();
|
|
_tags.Pop();
|
|
}
|
|
|
|
sealed class ExportProcessor : PdfContentStreamProcessor
|
|
{
|
|
readonly PdfContentExport _export;
|
|
readonly XmlWriter _writer;
|
|
readonly bool _writeOperators;
|
|
readonly List<TextInfo> _textContainer;
|
|
readonly List<NameValuePair> _operands = [];
|
|
int _writerLevel;
|
|
float _textWidth;
|
|
|
|
|
|
public ExportProcessor(PdfContentExport export, XmlWriter writer, ExporterOptions options) {
|
|
_export = export;
|
|
_writer = writer;
|
|
_writeOperators = options.ExportContentOperators;
|
|
_textContainer = options.ExportDecodedText ? [] : null;
|
|
PopulateOperators();
|
|
RegisterContentOperator("TJ", new AccumulatedShowTextArray());
|
|
}
|
|
|
|
public void End() {
|
|
if (!_writeOperators) {
|
|
return;
|
|
}
|
|
while (_writerLevel > 0) {
|
|
_writer.WriteEndElement();
|
|
_writerLevel--;
|
|
}
|
|
}
|
|
|
|
internal List<TextInfo> TextInfoList => _textContainer;
|
|
protected override void DisplayPdfString(PdfString str) {
|
|
var gs = CurrentGraphicState;
|
|
var font = gs.Font;
|
|
float totalWidth = 0;
|
|
foreach (var c in font.DecodeText(str)) {
|
|
float w = font.GetWidth(c) / 1000.0f;
|
|
float wordSpacing = c == ' ' ? gs.WordSpacing : 0f;
|
|
totalWidth += (w * gs.FontSize + gs.CharacterSpacing + wordSpacing) * gs.HorizontalScaling;
|
|
}
|
|
|
|
_textWidth = totalWidth;
|
|
}
|
|
|
|
protected override void InvokeOperator(PdfLiteral oper, List<PdfObject> operands) {
|
|
base.InvokeOperator(oper, operands);
|
|
string o, fn, t = null;
|
|
bool open = false;
|
|
bool hasDescriptiveOperands = false;
|
|
o = oper.ToString();
|
|
PdfPageCommand.GetFriendlyCommandName(o, out fn);
|
|
_operands.Clear();
|
|
switch (o) {
|
|
case "BDC":
|
|
case "BMC":
|
|
case "BT":
|
|
case "BX":
|
|
case "q":
|
|
open = true;
|
|
_writerLevel++;
|
|
goto default;
|
|
case "EMC":
|
|
case "ET":
|
|
case "EX":
|
|
case "Q":
|
|
if (_writerLevel > 0 && _writeOperators) {
|
|
_writer.WriteEndElement();
|
|
_writerLevel--;
|
|
}
|
|
return;
|
|
case "TJ":
|
|
var array = (PdfArray)operands[0];
|
|
using (var ms = new MemoryStream(array.Length)) {
|
|
foreach (PdfObject item in array.ArrayList) {
|
|
if (item.Type == PdfObject.STRING) {
|
|
ms.Write((item as PdfString).GetBytes(), 0, item.Length);
|
|
}
|
|
}
|
|
t = CurrentGraphicState.Font.DecodeText(new PdfString(ms.ToArray()));
|
|
}
|
|
AddTextInfo(t);
|
|
_operands.Add(new NameValuePair(Constants.Content.OperandNames.Text, t));
|
|
goto default;
|
|
case "Tj":
|
|
case "'":
|
|
case "\"":
|
|
t = CurrentGraphicState.Font.DecodeText(operands[0] as PdfString);
|
|
AddTextInfo(t);
|
|
_operands.Add(new NameValuePair(Constants.Content.OperandNames.Text, t));
|
|
goto default;
|
|
case "Tf":
|
|
_operands.Add(new NameValuePair(Constants.Font.ThisName, CurrentGraphicState.Font.FontName));
|
|
_operands.Add(new NameValuePair(Constants.Content.OperandNames.ResourceName, operands[0].ToString()));
|
|
_operands.Add(new NameValuePair(Constants.Content.OperandNames.Size, operands[1].ToString()));
|
|
hasDescriptiveOperands = true;
|
|
goto default;
|
|
case "cm":
|
|
case "Tm":
|
|
_operands.Add(new NameValuePair(Constants.Content.Operands, GetOperandsTextValue(operands)));
|
|
hasDescriptiveOperands = true;
|
|
goto default;
|
|
case "BI":
|
|
//case "EI":
|
|
goto default;
|
|
default:
|
|
if (!_writeOperators) {
|
|
return;
|
|
}
|
|
_writer.WriteStartElement(fn ?? "未知操作符");
|
|
_writer.WriteAttributeString("name", o);
|
|
foreach (var item in _operands) {
|
|
_writer.WriteAttributeString(item.Key, PdfHelper.GetValidXmlString(item.Value));
|
|
}
|
|
break;
|
|
}
|
|
if (!hasDescriptiveOperands) {
|
|
if (operands.Count > 0) {
|
|
// 删除操作符
|
|
operands.RemoveAt(operands.Count - 1);
|
|
}
|
|
if (operands.Count > 0) {
|
|
_writer.WriteStartElement(Constants.Content.Operands);
|
|
_export.ExportArrayContent(operands, _writer, true);
|
|
_writer.WriteEndElement();
|
|
if (o == "BI") {
|
|
_writer.WriteStartElement(Constants.Content.Raw);
|
|
var bs = (operands[0] as PdfImageData).RawBytes;
|
|
_writer.WriteAttributeString(Constants.Content.Length, bs.Length.ToText());
|
|
_writer.WriteBinHex(bs, 0, bs.Length);
|
|
_writer.WriteEndElement();
|
|
}
|
|
}
|
|
}
|
|
if (!open && _writerLevel >= 0) {
|
|
_writer.WriteEndElement();
|
|
}
|
|
}
|
|
|
|
static string GetOperandsTextValue(List<PdfObject> operands) {
|
|
operands.RemoveAt(operands.Count - 1);
|
|
return String.Join(" ", operands.ConvertAll((po) => po.Type == PdfObject.NUMBER ? ((PdfNumber)po).DoubleValue.ToText() : null).ToArray());
|
|
}
|
|
|
|
void AddTextInfo(string t) {
|
|
if (_textContainer != null && t != null) {
|
|
_textContainer.Add(new TextInfo() {
|
|
Text = t,
|
|
Size = CurrentGraphicState.FontSize * TextMatrix[Matrix.I11],
|
|
Region = new Bound(TextMatrix[Matrix.I31], TextMatrix[Matrix.I32], TextMatrix[Matrix.I31] + _textWidth, 0),
|
|
// note 不要设置该属性,否则可能在文档具有大量字体时占用过高的内存
|
|
//Font = CurrentGraphicState.Font
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|