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; namespace PDFPatcher.Processor { sealed class PdfContentExport { readonly ExporterOptions _options; readonly Dictionary _resolvedReferences = new Dictionary(); readonly Stack _tags = new Stack(); readonly Stack _dictionaries = new Stack(); 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) == false) { 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; } } /// /// 导出 PDFDictionary。 /// /// /// void ExportPdfDictionary(XmlWriter writer, PdfDictionary dict) { _dictionaries.Push(dict); foreach (var item in dict) { ExportPdfDictionaryItem(item, writer); } _dictionaries.Pop(); } void ExportPdfDictionaryItem(KeyValuePair 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(" array, XmlWriter writer) { writer.WriteStartElement(Constants.ContentPrefix, "array", Constants.ContentNamespace); ExportArrayContent(array, writer, false); writer.WriteEndElement(); } void ExportArrayContent(List 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) == false || 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 == false) { _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 == false) { 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 == false) { 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 _textContainer; readonly List _operands = new List(); int _writerLevel; float _textWidth; public ExportProcessor(PdfContentExport export, XmlWriter writer, ExporterOptions options) { _export = export; _writer = writer; _writeOperators = options.ExportContentOperators; _textContainer = options.ExportDecodedText ? new List() : null; PopulateOperators(); RegisterContentOperator("TJ", new AccumulatedShowTextArray()); } public void End() { if (_writeOperators == false) { return; } while (_writerLevel > 0) { _writer.WriteEndElement(); _writerLevel--; } } internal List 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 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 == false) { return; } _writer.WriteStartElement(fn ?? "未知操作符"); _writer.WriteAttributeString("name", o); foreach (var item in _operands) { _writer.WriteAttributeString(item.Key, PdfHelper.GetValidXmlString(item.Value)); } break; } if (hasDescriptiveOperands == false) { 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 == false && _writerLevel >= 0) { _writer.WriteEndElement(); } } static string GetOperandsTextValue(List 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 }); } } } } }