using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Xml; using iTextSharp.text.pdf; using PDFPatcher.Common; using PDFPatcher.Model; namespace PDFPatcher.Processor { /// A class which manages outlines (bookmarks) of PDF documents. static class OutlineManager { // modifed: added split array for action parameters static readonly char[] __ActionSplitters = new char[] { ' ', '\t', '\r', '\n' }; static readonly char[] __fullWidthNums = "0123456789".ToCharArray(); static readonly char[] __halfWidthNums = "0123456789".ToCharArray(); static readonly char[] __cmdIdentifiers = new char[] { '=', '﹦', '=', ':', ':' }; static readonly char[] __pageLabelSeparators = new char[] { ';', ';', ',', ',', ' ' }; private static void BookmarkDepth(PdfReader reader, PdfActionExporter exporter, PdfDictionary outline, Dictionary pageRefMap, XmlWriter target) { while (outline != null) { target.WriteStartElement(Constants.Bookmark); target.WriteAttributeString(Constants.BookmarkAttributes.Title, StringHelper.ReplaceControlAndBomCharacters(outline.GetAsString(PdfName.TITLE).Decode(AppContext.Encodings.BookmarkEncoding)) ); var color = outline.Locate(PdfName.C); DocInfoExporter.ExportColor(color, target); var style = outline.Locate(PdfName.F); if (style != null) { int f = style.IntValue & 0x03; if (f > 0) { target.WriteAttributeString(Constants.BookmarkAttributes.Style, Constants.BookmarkAttributes.StyleType.Names[f]); } } var count = outline.Get(PdfName.COUNT) as PdfNumber; if (count != null) { target.WriteAttributeString(Constants.BookmarkAttributes.Open, count.IntValue < 0 ? Constants.Boolean.False : Constants.Boolean.True); } var dest = outline.Locate(PdfName.DEST); if (dest != null) { exporter.ExportGotoAction(dest, target, pageRefMap); } else { exporter.ExportAction(outline.Locate(PdfName.A), pageRefMap, target); } var first = outline.Locate(PdfName.FIRST); if (first != null) { BookmarkDepth(reader, exporter, first, pageRefMap, target); } outline = outline.Locate(PdfName.NEXT); target.WriteEndElement(); } } /// /// 从 PDF 导出书签为 XML 元素。 /// public static XmlElement GetBookmark(PdfReader reader, UnitConverter unitConverter) { var catalog = reader.Catalog; var outlines = catalog.Locate(PdfName.OUTLINES); if (outlines == null) return null; if (unitConverter == null) { throw new NullReferenceException("unitConverter"); } var pages = reader.GetPageRefMapper(); var doc = new XmlDocument(); doc.AppendElement(Constants.DocumentBookmark); using (var w = doc.DocumentElement.CreateNavigator().AppendChild()) { var a = new PdfActionExporter(unitConverter); BookmarkDepth( reader, a, (PdfDictionary)PdfReader.GetPdfObjectRelease(outlines.Get(PdfName.FIRST)), pages, w); } return doc.DocumentElement; } private static Object[] CreateOutlines(PdfWriter writer, PdfIndirectReference parent, XmlElement kids, int maxPageNumber, bool namedAsNames) { var bookmarks = kids.SelectNodes(Constants.Bookmark); var refs = new PdfIndirectReference[bookmarks.Count]; for (int k = 0; k < refs.Length; ++k) refs[k] = writer.PdfIndirectReference; int ptr = 0; int count = 0; foreach (XmlElement child in bookmarks) { Object[] lower = null; if (child.SelectSingleNode(Constants.Bookmark) != null) lower = CreateOutlines(writer, refs[ptr], child, maxPageNumber, namedAsNames); var outline = new PdfDictionary(); ++count; if (lower != null) { outline.Put(PdfName.FIRST, (PdfIndirectReference)lower[0]); outline.Put(PdfName.LAST, (PdfIndirectReference)lower[1]); int n = (int)lower[2]; // 默认关闭书签 if (child.GetAttribute(Constants.BookmarkAttributes.Open) != Constants.Boolean.True) { outline.Put(PdfName.COUNT, -n); } else { outline.Put(PdfName.COUNT, n); count += n; } } outline.Put(PdfName.PARENT, parent); if (ptr > 0) outline.Put(PdfName.PREV, refs[ptr - 1]); if (ptr < refs.Length - 1) outline.Put(PdfName.NEXT, refs[ptr + 1]); outline.Put(PdfName.TITLE, child.GetAttribute(Constants.BookmarkAttributes.Title)); DocInfoImporter.ImportColor(child, outline); var style = child.GetAttribute(Constants.BookmarkAttributes.Style); if (String.IsNullOrEmpty(style) == false) { int bits = Array.IndexOf(Constants.BookmarkAttributes.StyleType.Names, style); if (bits == -1) { bits = 0; } if (bits != 0) outline.Put(PdfName.F, bits); } DocInfoImporter.ImportAction(writer, outline, child, maxPageNumber, namedAsNames); writer.AddToBody(outline, refs[ptr]); ++ptr; } return new Object[] { refs[0], refs[refs.Length - 1], count }; } internal static PdfIndirectReference WriteOutline(PdfWriter writer, XmlElement bookmarks, int maxPageNumber) { if (bookmarks == null || bookmarks.SelectSingleNode(Constants.Bookmark) == null) { return null; } var top = new PdfDictionary(); var topRef = writer.PdfIndirectReference; var kids = CreateOutlines(writer, topRef, bookmarks, maxPageNumber, false); top.Put(PdfName.TYPE, PdfName.OUTLINES); top.Put(PdfName.FIRST, (PdfIndirectReference)kids[0]); top.Put(PdfName.LAST, (PdfIndirectReference)kids[1]); top.Put(PdfName.COUNT, (int)kids[2]); writer.AddToBody(top, topRef); writer.ExtraCatalog.Put(PdfName.OUTLINES, topRef); return topRef; } internal static void KillOutline(PdfReader source) { var catalog = source.Catalog; var o = catalog.Get(PdfName.OUTLINES); if (o == null) { return; } if (o != null) { var outlines = o as PRIndirectReference; OutlineTravel(outlines); PdfReader.KillIndirect(outlines); } catalog.Remove(PdfName.OUTLINES); PdfReader.KillIndirect(catalog.Get(PdfName.OUTLINES)); if (PdfName.USEOUTLINES.Equals(catalog.GetAsName(PdfName.PAGEMODE))) { catalog.Remove(PdfName.PAGEMODE); } } private static void OutlineTravel(PRIndirectReference outline) { while (outline != null) { var outlineR = (PdfDictionary)PdfReader.GetPdfObjectRelease(outline); PdfReader.KillIndirect(outline); if (outlineR != null) { var first = (PRIndirectReference)outlineR.Get(PdfName.FIRST); if (first != null) { OutlineTravel(first); } PdfReader.KillIndirect(outlineR.Get(PdfName.DEST)); PdfReader.KillIndirect(outlineR.Get(PdfName.A)); outline = (PRIndirectReference)outlineR.Get(PdfName.NEXT); } else { outline = null; } } } public static string EscapeBinaryString(String s) { var buf = StringBuilderCache.Acquire(); var cc = s.ToCharArray(); int len = cc.Length; for (int k = 0; k < len; ++k) { char c = cc[k]; if (c < ' ') { buf.Append('\\'); int v = c; var octal = ""; do { int x = v % 8; octal = x.ToText() + octal; v /= 8; } while (v > 0); buf.Append(octal.PadLeft(3, '0')); } else if (c == '\\') { buf.Append("\\\\"); } else { buf.Append(c); } } return StringBuilderCache.GetStringAndRelease(buf); } public static string UnescapeBinaryString(String s) { var buf = StringBuilderCache.Acquire(); var cc = s.ToCharArray(); int len = cc.Length; for (int k = 0; k < len; ++k) { char c = cc[k]; if (c != '\\') { buf.Append(c); continue; } if (++k >= len) { buf.Append('\\'); break; } c = cc[k]; if (c < '0' || c > '7') { buf.Append(c); continue; } int n = c - '0'; ++k; for (int j = 0; j < 2 && k < len; ++j) { c = cc[k]; if (c < '0' || c > '7') { break; } ++k; n = n * 8 + c - '0'; } --k; buf.Append((char)n); } return StringBuilderCache.GetStringAndRelease(buf); } internal static void ImportSimpleBookmarks(TextReader source, PdfInfoXmlDocument target) { string cmd, cmdData, s, title, indentString = "\t", pnText; bool isOpen = false; // 书签是否默认打开 int pageOffset = 0, pageNum; int currentIndent = -1, indent, p; int lineNum = 0; char[] digits; var pattern = new Regex(@"(.+?)[\s\.… \-_]*(-?[0-90123456789]+)?\s*$", RegexOptions.Compiled); var docInfo = target.InfoNode; var root = target.BookmarkRoot; var pageLabels = target.PageLabelRoot; BookmarkContainer currentBookmark = root; BookmarkElement bookmark; while (source.Peek() != -1) { s = source.ReadLine(); lineNum++; if (s.Trim().Length == 0) { continue; } if ((s[0] == '#' || s[0] == '#') && (p = s.IndexOfAny(__cmdIdentifiers)) != -1) { cmd = s.Substring(1, p - 1); cmdData = s.Substring(p + 1); switch (cmd) { case "首页页码": if (cmdData.TryParse(out pageOffset)) { Tracker.TraceMessage("首页页码改为 " + pageOffset); pageOffset--; } break; case "缩进标记": indentString = cmdData; Tracker.TraceMessage(String.Concat("缩进标记改为“", indentString, "”")); break; case "版本": if (lineNum == 1) { var v = cmdData.Trim(); target.DocumentElement.SetAttribute(Constants.Info.ProductVersion, v); Tracker.TraceMessage("导入简易书签文件,版本为:" + v); } break; case "打开书签": cmdData = cmdData.ToLowerInvariant(); isOpen = (cmdData == "是" || cmdData == "true" || cmdData == "y" || cmdData == "yes" || cmdData == "1"); break; case Constants.Info.DocumentPath: target.PdfDocumentPath = cmdData.Trim(); break; case Constants.PageLabels: var l = cmdData.Split(__pageLabelSeparators, 3); if (l.Length < 1) { Tracker.TraceMessage(Constants.PageLabels + "格式不正确,至少应指定起始页码。"); continue; } int pn; if (l[0].TryParse(out pn) == false || pn < 1) { Tracker.TraceMessage(Constants.PageLabels + "格式不正确:起始页码应为正整数。"); continue; } var style = l[1].Length > 0 ? ValueHelper.MapValue(l[1][0], Constants.PageLabelStyles.SimpleInfoIdentifiers, Constants.PageLabelStyles.Names, Constants.PageLabelStyles.Names[1]) : Constants.PageLabelStyles.Names[1]; var prefix = l.Length > 2 ? l[2] : null; var pl = target.CreateElement(Constants.PageLabelsAttributes.Style) as XmlElement; pl.SetAttribute(Constants.PageLabelsAttributes.PageNumber, pn.ToText()); pl.SetAttribute(Constants.PageLabelsAttributes.Style, style); if (String.IsNullOrEmpty(prefix) == false) { pl.SetAttribute(Constants.PageLabelsAttributes.Prefix, prefix); } pageLabels.AppendChild(pl); continue; case Constants.Info.Title: case Constants.Info.Subject: case Constants.Info.Keywords: case Constants.Info.Author: docInfo.SetAttribute(cmd, cmdData); break; } continue; } indent = p = 0; while (s.IndexOf(indentString, p) == p) { p += indentString.Length; indent++; } var m = pattern.Match(s, p); if (m.Success == false) { continue; } title = m.Groups[1].Value; pnText = m.Groups[2].Value; if (pnText.Length == 0) { pageNum = 0; } else { if (pnText.IndexOfAny(__fullWidthNums) != -1) { digits = Array.ConvertAll(m.Groups[2].Value.ToCharArray(), d => ValueHelper.MapValue(d, __fullWidthNums, __halfWidthNums, d)); pnText = new string(digits, 0, digits.Length); } if (pnText.TryParse(out pageNum)) { pageNum += pageOffset; } } bookmark = target.CreateBookmark(); if (indent == currentIndent) { currentBookmark.ParentNode.AppendChild(bookmark); } else if (indent > currentIndent) { currentBookmark.AppendChild(bookmark); if (indent - currentIndent > 1) { throw new FormatException(String.Concat("在简易书签第 ", lineNum, " 行的缩进格式不正确。\n\n说明:下级书签最多只能比上级书签多一个缩进标记。")); } currentIndent++; } else /* indent < currentIndent */ { while (currentIndent > indent && currentBookmark.ParentNode != root) { currentBookmark = currentBookmark.ParentNode as BookmarkContainer; currentIndent--; } currentBookmark.ParentNode.AppendChild(bookmark); } bookmark.Title = title; if (isOpen == false) { bookmark.IsOpen = false; } if (pageNum > 0) { bookmark.Page = pageNum; } currentBookmark = bookmark; } } internal static void ImportSimpleBookmarks(string path, PdfInfoXmlDocument target) { using (TextReader r = new StreamReader(path, DetectEncoding(path))) { ImportSimpleBookmarks(r, target); } } public static void WriteSimpleBookmarkInstruction(TextWriter writer, string item, string value) { if (String.IsNullOrEmpty(value)) { return; } writer.Write("#"); writer.Write(item); writer.Write("="); writer.WriteLine(value); } /// /// 将 XML 书签输出为简易书签。 /// /// 输出目标。 /// 书签节点。 /// 缩进量。 /// 缩进字符串。 public static void WriteSimpleBookmark(TextWriter writer, BookmarkContainer container, int indent, string indentChar) { foreach (BookmarkElement item in container.SubBookmarks) { for (int i = 0; i < indent; i++) { writer.Write(indentChar); } writer.Write(item.Title); writer.Write("\t\t"); writer.Write(item.Page.ToText()); writer.WriteLine(); WriteSimpleBookmark(writer, item, indent + 1, indentChar); } } private static Encoding DetectEncoding(string path) { const string VersionString = "#版本"; const string VersionString2 = "#版本"; var b = new byte[20]; using (var r = new FileStream(path, FileMode.Open)) { if (r.Length < b.Length) { throw new FormatException("简易书签文件内容不足。"); } r.Read(b, 0, b.Length); } foreach (var item in Constants.Encoding.Encodings) { if (item == null) { continue; } var s = item.GetString(b); if (s.StartsWith(VersionString, StringComparison.Ordinal) || s.StartsWith(VersionString2, StringComparison.Ordinal)) { return item; } } return Encoding.Default; } } }