mirror of
https://github.com/wmjordan/PDFPatcher.git
synced 2026-10-18 13:42:28 +08:00
625 lines
18 KiB
C#
625 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using PDFPatcher.Common;
|
|
|
|
namespace PDFPatcher.Model
|
|
{
|
|
public abstract class SourceItem
|
|
{
|
|
List<SourceItem> _Items;
|
|
|
|
public FilePath FilePath { get; }
|
|
public string FileName { get; }
|
|
public string FolderName { get; }
|
|
public BookmarkSettings Bookmark { get; set; }
|
|
public int PageCount { get; private set; }
|
|
public abstract int FileSize { get; }
|
|
public abstract DateTime FileTime { get; }
|
|
public List<SourceItem> Items => _Items ??= [];
|
|
|
|
public bool HasSubItems => _Items.HasContent();
|
|
public virtual bool HasContent => _Items?.Any(i => i.HasContent) == true;
|
|
|
|
public abstract ItemType Type { get; }
|
|
public static void SortFileList(string[] fileList) {
|
|
if (AppContext.Merger.CajSort && CajSort(fileList)) {
|
|
return;
|
|
}
|
|
|
|
if (AppContext.Merger.NumericAwareSort) {
|
|
Array.Sort(fileList, FileHelper.NumericAwareComparePath);
|
|
}
|
|
else {
|
|
Array.Sort(fileList, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
|
|
public void SortItems(SortType options, bool recursive) {
|
|
if (_Items.HasContent() == false) {
|
|
return;
|
|
}
|
|
switch (options) {
|
|
case SortType.Literal:
|
|
_Items.Sort((x, y) => String.Compare(x.FilePath, y.FilePath, StringComparison.OrdinalIgnoreCase));
|
|
break;
|
|
case SortType.NumericAwareSort:
|
|
_Items.Sort((x, y) => FileHelper.NumericAwareComparePath(x.FilePath, y.FilePath));
|
|
break;
|
|
case SortType.CajSort:
|
|
if (CajSort(_Items) == false) {
|
|
goto case SortType.NumericAwareSort;
|
|
}
|
|
break;
|
|
case SortType.FileTime:
|
|
_Items.Sort((x, y) => x.FileTime.CompareTo(y.FileTime));
|
|
break;
|
|
case SortType.Reverse:
|
|
_Items.Reverse();
|
|
break;
|
|
}
|
|
if (recursive == false) {
|
|
return;
|
|
}
|
|
foreach (var item in _Items) {
|
|
if (item._Items.HasContent()) {
|
|
item.SortItems(options, recursive);
|
|
}
|
|
}
|
|
}
|
|
|
|
public abstract SourceItem Clone();
|
|
|
|
public override string ToString() {
|
|
return FilePath.ToString();
|
|
}
|
|
|
|
public enum ItemType
|
|
{
|
|
Empty, Pdf, Image, Folder
|
|
}
|
|
public enum SortType {
|
|
Undefined,
|
|
Literal,
|
|
NumericAwareSort,
|
|
CajSort,
|
|
FileTime,
|
|
Reverse
|
|
}
|
|
|
|
/// <summary>
|
|
/// 创建新的空白页。
|
|
/// </summary>
|
|
/// <returns>空白页实例。</returns>
|
|
internal static Empty Create() {
|
|
return new Empty();
|
|
}
|
|
|
|
internal static SourceItem Create(string path) {
|
|
return Create(path, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 根据传入的文件路径创建 <see cref="SourceItem"/> 实例。
|
|
/// </summary>
|
|
/// <param name="path">文件或文件夹路径。</param>
|
|
/// <param name="refresh">是否强制更新。</param>
|
|
/// <returns><see cref="SourceItem"/> 实例。</returns>
|
|
internal static SourceItem Create(FilePath path, bool refresh) {
|
|
if (((string)path).IsNullOrWhiteSpace()) {
|
|
return Create();
|
|
}
|
|
if (path.ExistsDirectory) {
|
|
return new Folder(path.ToString(), refresh);
|
|
}
|
|
if (path.HasExtension(Constants.FileExtensions.Pdf)) {
|
|
try {
|
|
var reader = Processor.PdfHelper.OpenPdfFile(path.ToString(), true, false);
|
|
var c = reader.NumberOfPages;
|
|
string r = refresh ? new PageRange(1, c).ToString() : null;
|
|
var info = Processor.DocInfoExporter.RewriteDocInfoWithEncoding(reader, AppContext.Encodings.DocInfoEncoding);
|
|
reader.Close();
|
|
return new Pdf(path, r, c, info);
|
|
}
|
|
catch (FileNotFoundException) {
|
|
FormHelper.ErrorBox($"找不到文件:“{path}”。");
|
|
}
|
|
catch (Exception) {
|
|
FormHelper.ErrorBox($"打开 PDF 文件“{path}”时出错。");
|
|
// ignore corrupted
|
|
}
|
|
return Create();
|
|
}
|
|
if (path.HasExtension(Constants.FileExtensions.AllSupportedImageExtension)) {
|
|
return new Image(path);
|
|
}
|
|
FormHelper.ErrorBox($"不支持文件“{path}”。");
|
|
return Create();
|
|
}
|
|
|
|
internal string GetInfoFileName() {
|
|
// 优先采用与输入文件同名的 XML 信息文件
|
|
var f = new FilePath(FileHelper.CombinePath(FolderName, Path.ChangeExtension(FileName, Constants.FileExtensions.Xml)));
|
|
if (f.ExistsFile == false) {
|
|
// 次之采用与输入文件同名的 TXT 信息文件
|
|
f = f.ChangeExtension(Constants.FileExtensions.Txt);
|
|
if (f.ExistsFile == false) {
|
|
// 次之采用同一个信息文件
|
|
f = FilePath.ChangeExtension(Constants.FileExtensions.Xml);
|
|
if (f.ExistsFile == false) {
|
|
f = FilePath.Empty;
|
|
}
|
|
}
|
|
}
|
|
return f.ToString();
|
|
}
|
|
|
|
internal string GetTargetPdfFileName(string targetPath) {
|
|
return FileHelper.HasFileNameMacro(targetPath)
|
|
? targetPath
|
|
: FileHelper.CombinePath(Path.GetDirectoryName(targetPath), FilePath.FileName);
|
|
}
|
|
|
|
internal sealed class Empty : SourceItem
|
|
{
|
|
readonly DateTime _Time = DateTime.Now;
|
|
|
|
public override ItemType Type => ItemType.Empty;
|
|
|
|
public override int FileSize => 0;
|
|
public override DateTime FileTime => _Time;
|
|
|
|
public void SetPageCount(int pageCount) {
|
|
PageCount = pageCount;
|
|
}
|
|
|
|
public override string ToString() {
|
|
return "<空白页>";
|
|
}
|
|
|
|
public override SourceItem Clone() {
|
|
var n = new Empty();
|
|
CopyProperties(n);
|
|
return n;
|
|
}
|
|
|
|
internal Empty() : base(null, 1) { }
|
|
internal Empty(int pageCount) : base(null, pageCount) { }
|
|
}
|
|
|
|
internal sealed class CropOptions
|
|
{
|
|
public int Left { get; set; }
|
|
public int Right { get; set; }
|
|
public int Top { get; set; }
|
|
public int Bottom { get; set; }
|
|
public int MinHeight { get; set; }
|
|
public int MinWidth { get; set; }
|
|
|
|
public bool NeedCropping => Left > 0 || Right > 0 || Top > 0 || Bottom > 0;
|
|
|
|
public bool Equals(CropOptions i) {
|
|
return Top == i.Top && Bottom == i.Bottom && Left == i.Left && Right == i.Right &&
|
|
MinHeight == i.MinHeight && MinWidth == i.MinWidth;
|
|
}
|
|
public CropOptions Clone() {
|
|
return (CropOptions)MemberwiseClone();
|
|
}
|
|
}
|
|
|
|
internal sealed class Image : SourceItem
|
|
{
|
|
readonly int _FileSize = -1;
|
|
readonly DateTime _FileTime;
|
|
|
|
public Image(FilePath path)
|
|
: base(path, 0) {
|
|
Cropping = new CropOptions();
|
|
GetFileInfo(path, out _FileSize, out _FileTime);
|
|
}
|
|
|
|
public CropOptions Cropping { get; set; }
|
|
public override ItemType Type => ItemType.Image;
|
|
public override int FileSize => _FileSize;
|
|
public override DateTime FileTime => _FileTime;
|
|
public override bool HasContent => true;
|
|
|
|
public override SourceItem Clone() {
|
|
return new Image(FilePath) {
|
|
Cropping = Cropping.Clone()
|
|
};
|
|
}
|
|
}
|
|
|
|
internal sealed class Pdf : SourceItem
|
|
{
|
|
int _FileSize = -1;
|
|
DateTime _FileTime;
|
|
|
|
public Pdf(FilePath path, string pageRanges, int pageCount, Model.GeneralInfo docInfo)
|
|
: base(path, pageCount) {
|
|
PageRanges = pageRanges;
|
|
DocInfo = docInfo;
|
|
ExtractImageOptions = new ImageExtracterOptions() {
|
|
OutputPath = Path.GetDirectoryName(path.ToString()),
|
|
ExtractAnnotationImages = false,
|
|
MergeJpgToPng = true,
|
|
MergeImages = true,
|
|
MinWidth = 50,
|
|
MinHeight = 50
|
|
};
|
|
GetFileInfo(path, out _FileSize, out _FileTime);
|
|
}
|
|
|
|
public Pdf(FilePath path) : base(path, 0) {
|
|
Refresh(path.ToString(), AppContext.Encodings.DocInfoEncoding);
|
|
}
|
|
|
|
public string PageRanges { get; set; }
|
|
public bool ImportImagesOnly { get; set; }
|
|
public ImageExtracterOptions ExtractImageOptions { get; private set; }
|
|
public Model.GeneralInfo DocInfo { get; private set; }
|
|
public override ItemType Type => ItemType.Pdf;
|
|
public override int FileSize => _FileSize;
|
|
public override DateTime FileTime => _FileTime;
|
|
public override bool HasContent => true;
|
|
|
|
public void Refresh(Encoding encoding) {
|
|
Refresh(FilePath.ToString(), encoding);
|
|
}
|
|
|
|
public override string ToString() {
|
|
return FilePath.IsEmpty ? String.Empty :
|
|
String.IsNullOrEmpty(PageRanges) ? (string)FilePath :
|
|
String.Concat(FilePath, "::", PageRanges);
|
|
}
|
|
|
|
public override SourceItem Clone() {
|
|
var n = new Pdf(FilePath, PageRanges, PageCount, DocInfo) {
|
|
ImportImagesOnly = ImportImagesOnly
|
|
};
|
|
CopyProperties(n);
|
|
return n;
|
|
}
|
|
|
|
private void Refresh(string path, Encoding encoding) {
|
|
try {
|
|
GetFileInfo(path, out _FileSize, out _FileTime);
|
|
if (_FileSize > 0) {
|
|
using (var reader = Processor.PdfHelper.OpenPdfFile(path, true, false)) {
|
|
DocInfo = Processor.DocInfoExporter.RewriteDocInfoWithEncoding(reader, encoding);
|
|
PageCount = reader.NumberOfPages;
|
|
PageRanges = new PageRange(1, PageCount).ToString();
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex) {
|
|
AppContext.MainForm.ErrorBox($"打开 PDF 文件“{path}”时出错", ex);
|
|
// ignore corrupted
|
|
}
|
|
}
|
|
}
|
|
|
|
internal sealed class Folder : SourceItem
|
|
{
|
|
readonly DateTime _FolderTime;
|
|
|
|
public Folder(string path) : base(path, 0) {
|
|
var p = new FilePath(path);
|
|
if (p.ExistsDirectory) {
|
|
_FolderTime = p.ToDirectoryInfo().LastWriteTime;
|
|
}
|
|
}
|
|
public Folder(string path, bool loadSubItems)
|
|
: this(path) {
|
|
if (loadSubItems) {
|
|
Reload();
|
|
}
|
|
}
|
|
|
|
public override ItemType Type => ItemType.Folder;
|
|
|
|
public override int FileSize => 0;
|
|
public override DateTime FileTime => _FolderTime;
|
|
|
|
public void Reload() {
|
|
Items.Clear();
|
|
if (!FilePath.ExistsDirectory) {
|
|
return;
|
|
}
|
|
|
|
var p = FilePath.ToString();
|
|
var l = Items;
|
|
switch (AppContext.Merger.SubFolder) {
|
|
case MergerOptions.SubFolderPosition.BeforeFiles:
|
|
AddSubDirectories(p, l);
|
|
AddFiles(p, l);
|
|
break;
|
|
case MergerOptions.SubFolderPosition.WithFiles:
|
|
AddSubDirectoriesAndFiles(p, l);
|
|
break;
|
|
case MergerOptions.SubFolderPosition.Exclude:
|
|
AddFiles(p, l);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public override SourceItem Clone() {
|
|
var n = new Folder(FilePath.ToString());
|
|
CopyProperties(n);
|
|
return n;
|
|
}
|
|
|
|
static void AddSubDirectoriesAndFiles(string folderPath, List<SourceItem> list) {
|
|
var fl = Array.FindAll(Directory.GetFiles(folderPath), (i) => {
|
|
var ext = Path.GetExtension(i).ToLowerInvariant();
|
|
return Constants.FileExtensions.Pdf == ext
|
|
|| Constants.FileExtensions.AllSupportedImageExtension.Contains(ext);
|
|
});
|
|
var d = Array.ConvertAll(Directory.GetDirectories(folderPath), (i) => i + "\\");
|
|
var s = new string[fl.Length + d.Length];
|
|
Array.Copy(fl, s, fl.Length);
|
|
Array.Copy(d, 0, s, fl.Length, d.Length);
|
|
SortFileList(s);
|
|
foreach (var item in s) {
|
|
if (item[item.Length - 1] == '\\') {
|
|
list.Add(new Folder(item.Substring(0, item.Length - 1), true));
|
|
}
|
|
else {
|
|
list.Add(Create(item));
|
|
}
|
|
}
|
|
}
|
|
|
|
static void AddFiles(string folderPath, List<SourceItem> list) {
|
|
try {
|
|
var fl = Directory.GetFiles(folderPath);
|
|
SortFileList(fl);
|
|
foreach (var item in fl) {
|
|
var ext = Path.GetExtension(item).ToLowerInvariant();
|
|
if (Constants.FileExtensions.Pdf == ext
|
|
|| Constants.FileExtensions.AllSupportedImageExtension.Contains(ext)) {
|
|
list.Add(Create(item));
|
|
}
|
|
}
|
|
}
|
|
catch (UnauthorizedAccessException) { }
|
|
catch (IOException) { }
|
|
}
|
|
|
|
static void AddSubDirectories(string folderPath, List<SourceItem> list) {
|
|
try {
|
|
foreach (var item in Directory.EnumerateDirectories(folderPath)) {
|
|
var f = new Folder(item, true);
|
|
list.Add(f);
|
|
}
|
|
}
|
|
catch (UnauthorizedAccessException) { }
|
|
catch (IOException) { }
|
|
}
|
|
}
|
|
|
|
protected SourceItem(FilePath path, int pageCount) {
|
|
PageCount = pageCount;
|
|
if (!path.IsValidPath) {
|
|
return;
|
|
}
|
|
|
|
FilePath = path;
|
|
FileName = path.FileName;
|
|
FolderName = path.Directory;
|
|
if (AppContext.Merger.AutoBookmarkTitle == false) {
|
|
return;
|
|
}
|
|
var t = path.ExistsDirectory ? FileName : path.FileNameWithoutExtension;
|
|
if (t.Length > 0) {
|
|
Bookmark = CreateBookmarkSettings(t);
|
|
}
|
|
}
|
|
|
|
protected static void GetFileInfo(FilePath fileName, out int kilobytes, out DateTime fileTime) {
|
|
if (fileName.ExistsFile == false) {
|
|
kilobytes = 0;
|
|
fileTime = DateTime.MinValue;
|
|
}
|
|
else {
|
|
var f = fileName.ToFileInfo();
|
|
kilobytes = (int)(f.Length >> 10);
|
|
fileTime = f.LastWriteTime;
|
|
}
|
|
}
|
|
|
|
protected virtual void CopyProperties(SourceItem target) {
|
|
target._Items = new List<SourceItem>(HasSubItems ? Items.Count : 0);
|
|
if (HasSubItems) {
|
|
foreach (var item in _Items) {
|
|
target._Items.Add(item.Clone());
|
|
}
|
|
}
|
|
if (Bookmark != null) {
|
|
target.Bookmark = Bookmark.Clone();
|
|
}
|
|
target.PageCount = PageCount;
|
|
}
|
|
static BookmarkSettings CreateBookmarkSettings(string t) {
|
|
if (AppContext.Merger.CajSort && t.Length == 6) {
|
|
if (MatchCajPattern(t, Constants.CajNaming.Cover)) {
|
|
return t.EndsWith("001", StringComparison.Ordinal) ? new BookmarkSettings("封面")
|
|
: t.EndsWith("002", StringComparison.Ordinal) ? new BookmarkSettings("封底")
|
|
: null; // 超过2页的,只为第一页和第二页生成书签
|
|
}
|
|
else if (MatchCajPattern(t, Constants.CajNaming.TitlePage)) {
|
|
return t.EndsWith("001", StringComparison.Ordinal) ? new BookmarkSettings("书名") : null;
|
|
}
|
|
else if (MatchCajPattern(t, Constants.CajNaming.CopyrightPage)) {
|
|
return t.EndsWith("001", StringComparison.Ordinal) ? new BookmarkSettings("版权") : null;
|
|
}
|
|
else if (MatchCajPattern(t, Constants.CajNaming.Foreword)) {
|
|
return t.EndsWith("001", StringComparison.Ordinal) ? new BookmarkSettings("前言") : null;
|
|
}
|
|
else if (MatchCajPattern(t, Constants.CajNaming.Contents)) {
|
|
return t.EndsWith("00001", StringComparison.Ordinal) ? new BookmarkSettings("目录") : null;
|
|
}
|
|
else if (MatchCajPattern(t, String.Empty) && t == "000001") {
|
|
return new BookmarkSettings("正文");
|
|
}
|
|
}
|
|
if (AppContext.Merger.IgnoreLeadingNumbers) {
|
|
int i;
|
|
for (i = 0; i < t.Length; i++) {
|
|
if (t[i] > '9' || t[i] < '0') {
|
|
break;
|
|
}
|
|
}
|
|
t = t.Substring(i);
|
|
}
|
|
return new BookmarkSettings(t);
|
|
}
|
|
static bool CajSort(string[] fileList) {
|
|
var m = false; // match Caj naming
|
|
var cov = new List<string>(1);
|
|
var bok = new List<string>(2);
|
|
var leg = new List<string>(1);
|
|
var fow = new List<string>(3);
|
|
var cnt = new List<string>(5);
|
|
var body = new List<string>(fileList.Length);
|
|
foreach (var path in fileList) {
|
|
var f = Path.GetFileNameWithoutExtension(path);
|
|
if (f.Length == 6) {
|
|
if (MatchCajPatternAddPath(path, f, Constants.CajNaming.Cover, cov)
|
|
|| MatchCajPatternAddPath(path, f, Constants.CajNaming.TitlePage, bok)
|
|
|| MatchCajPatternAddPath(path, f, Constants.CajNaming.CopyrightPage, leg)
|
|
|| MatchCajPatternAddPath(path, f, Constants.CajNaming.Foreword, fow)
|
|
|| MatchCajPatternAddPath(path, f, Constants.CajNaming.Contents, cnt)
|
|
) {
|
|
m = true;
|
|
continue;
|
|
}
|
|
}
|
|
body.Add(path);
|
|
}
|
|
if (m == false) {
|
|
return false;
|
|
}
|
|
cov.Sort(StringComparer.OrdinalIgnoreCase);
|
|
bok.Sort(StringComparer.OrdinalIgnoreCase);
|
|
leg.Sort(StringComparer.OrdinalIgnoreCase);
|
|
fow.Sort(StringComparer.OrdinalIgnoreCase);
|
|
cnt.Sort(StringComparer.OrdinalIgnoreCase);
|
|
body.Sort(StringComparer.OrdinalIgnoreCase);
|
|
int p = 0;
|
|
if (cov.Count == 2) {
|
|
fileList[0] = cov[0];
|
|
++p;
|
|
}
|
|
else {
|
|
p = CopyItem(fileList, cov, p);
|
|
}
|
|
p = CopyItem(fileList, bok, p);
|
|
p = CopyItem(fileList, leg, p);
|
|
p = CopyItem(fileList, fow, p);
|
|
p = CopyItem(fileList, cnt, p);
|
|
p = CopyItem(fileList, body, p);
|
|
if (cov.Count == 2) {
|
|
fileList[p] = cov[1];
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static int CopyItem(string[] fileList, List<string> list, int position) {
|
|
list.CopyTo(fileList, position);
|
|
position += list.Count;
|
|
return position;
|
|
}
|
|
|
|
static bool MatchCajPatternAddPath(string path, string text, string pattern, List<string> container) {
|
|
if (MatchCajPattern(text, pattern)) {
|
|
container.Add(path);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool CajSort(List<SourceItem> fileList) {
|
|
var m = false; // match Caj naming
|
|
var cov = new List<SourceItem>(1);
|
|
var bok = new List<SourceItem>(2);
|
|
var leg = new List<SourceItem>(1);
|
|
var fow = new List<SourceItem>(3);
|
|
var cnt = new List<SourceItem>(5);
|
|
var body = new List<SourceItem>(fileList.Count);
|
|
foreach (var file in fileList) {
|
|
var path = file.FilePath;
|
|
var f = Path.GetFileNameWithoutExtension(path);
|
|
if (f.Length == 6) {
|
|
if (MatchCajPatternAddPath(file, f, Constants.CajNaming.Cover, cov)
|
|
|| MatchCajPatternAddPath(file, f, Constants.CajNaming.TitlePage, bok)
|
|
|| MatchCajPatternAddPath(file, f, Constants.CajNaming.CopyrightPage, leg)
|
|
|| MatchCajPatternAddPath(file, f, Constants.CajNaming.Foreword, fow)
|
|
|| MatchCajPatternAddPath(file, f, Constants.CajNaming.Contents, cnt)
|
|
) {
|
|
m = true;
|
|
continue;
|
|
}
|
|
}
|
|
body.Add(file);
|
|
}
|
|
if (m == false) {
|
|
return false;
|
|
}
|
|
cov.Sort(CompareFilePath);
|
|
bok.Sort(CompareFilePath);
|
|
leg.Sort(CompareFilePath);
|
|
fow.Sort(CompareFilePath);
|
|
cnt.Sort(CompareFilePath);
|
|
body.Sort(CompareFilePath);
|
|
fileList.Clear();
|
|
if (cov.Count == 2) {
|
|
fileList.Add(cov[0]);
|
|
}
|
|
else {
|
|
fileList.AddRange(cov);
|
|
}
|
|
fileList.AddRange(bok);
|
|
fileList.AddRange(leg);
|
|
fileList.AddRange(fow);
|
|
fileList.AddRange(cnt);
|
|
fileList.AddRange(body);
|
|
if (cov.Count == 2) {
|
|
fileList.Add(cov[1]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool MatchCajPatternAddPath(SourceItem item, string text, string pattern, List<SourceItem> container) {
|
|
if (MatchCajPattern(text, pattern)) {
|
|
container.Add(item);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int CompareFilePath(SourceItem x, SourceItem y) {
|
|
return String.Compare(x.FilePath, y.FilePath, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static bool MatchCajPattern(string text, string pattern) {
|
|
if (!text.HasCaseInsensitivePrefix(pattern)) {
|
|
return false;
|
|
}
|
|
int l = pattern.Length;
|
|
if (text.Length == l) {
|
|
return false;
|
|
}
|
|
foreach (var ch in text.Substring(l)) {
|
|
if (ch < '0' || ch > '9') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|