using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using PDFPatcher.Common;
using PDFPatcher.Model;
namespace PDFPatcher.Processor
{
/// A class which manages outlines (bookmarks) of PDF documents.
static partial class OutlineManager
{
static readonly char[] __fullWidthNumbers = "0123456789".ToCharArray();
static readonly char[] __halfWidthNumbers = "0123456789".ToCharArray();
static readonly char[] __cmdIdentifiers = ['=', '﹦', '=', ':', ':'];
static readonly char[] __pageLabelSeparators = [';', ';', ',', ',', ' '];
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($"缩进标记改为“{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) || 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)) {
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) {
continue;
}
title = m.Groups[1].Value;
pnText = m.Groups[2].Value;
if (pnText.Length == 0) {
pageNum = 0;
}
else {
if (pnText.IndexOfAny(__fullWidthNumbers) != -1) {
digits = Array.ConvertAll(m.Groups[2].Value.ToCharArray(), d => ValueHelper.MapValue(d, __fullWidthNumbers, __halfWidthNumbers, 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($"在简易书签第 {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) {
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.HasPrefix(VersionString) || s.HasPrefix(VersionString2)) {
return item;
}
}
return Encoding.Default;
}
}
}