227 lines
7.9 KiB
C#
227 lines
7.9 KiB
C#
|
|
using CloudPrint.Client.Models;
|
|||
|
|
using PdfSharp.Drawing;
|
|||
|
|
using PdfSharp.Pdf;
|
|||
|
|
using PdfSharp.Pdf.IO;
|
|||
|
|
using System.IO;
|
|||
|
|
|
|||
|
|
namespace CloudPrint.Client.Helpers;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 基于 PDFsharp 的 PDF 处理工具,负责打印前的页码范围裁剪、水印和合并。
|
|||
|
|
/// </summary>
|
|||
|
|
public sealed class PdfProcessor
|
|||
|
|
{
|
|||
|
|
public Task<string> AddWatermarkAsync(string pdfPath, string text, CancellationToken cancellationToken = default)
|
|||
|
|
{
|
|||
|
|
ValidatePdfPath(pdfPath);
|
|||
|
|
if (string.IsNullOrWhiteSpace(text))
|
|||
|
|
{
|
|||
|
|
return Task.FromResult(pdfPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|||
|
|
var outputPath = CreateSiblingOutputPath(pdfPath, "watermarked");
|
|||
|
|
using var document = PdfReader.Open(pdfPath, PdfDocumentOpenMode.Modify);
|
|||
|
|
|
|||
|
|
foreach (var page in document.Pages)
|
|||
|
|
{
|
|||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|||
|
|
using var graphics = XGraphics.FromPdfPage(page, XGraphicsPdfPageOptions.Append);
|
|||
|
|
var font = new XFont("Arial", 36, XFontStyleEx.Bold);
|
|||
|
|
var size = graphics.MeasureString(text, font);
|
|||
|
|
var centerX = page.Width.Point / 2;
|
|||
|
|
var centerY = page.Height.Point / 2;
|
|||
|
|
|
|||
|
|
graphics.TranslateTransform(centerX, centerY);
|
|||
|
|
graphics.RotateTransform(-35);
|
|||
|
|
graphics.DrawString(
|
|||
|
|
text,
|
|||
|
|
font,
|
|||
|
|
new XSolidBrush(XColor.FromArgb(80, 120, 120, 120)),
|
|||
|
|
new XRect(-size.Width / 2, -size.Height / 2, size.Width, size.Height),
|
|||
|
|
XStringFormats.Center);
|
|||
|
|
graphics.RotateTransform(35);
|
|||
|
|
graphics.TranslateTransform(-centerX, -centerY);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.Save(outputPath);
|
|||
|
|
return Task.FromResult(outputPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public Task<string> ExtractPageRangeAsync(string pdfPath, string pageRange, string outputDirectory, CancellationToken cancellationToken = default)
|
|||
|
|
{
|
|||
|
|
ValidatePdfPath(pdfPath);
|
|||
|
|
if (string.IsNullOrWhiteSpace(pageRange) || pageRange.Equals("all", StringComparison.OrdinalIgnoreCase))
|
|||
|
|
{
|
|||
|
|
return Task.FromResult(pdfPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|||
|
|
Directory.CreateDirectory(outputDirectory);
|
|||
|
|
using var input = PdfReader.Open(pdfPath, PdfDocumentOpenMode.Import);
|
|||
|
|
var pageIndexes = ParsePageRange(pageRange, input.PageCount);
|
|||
|
|
if (pageIndexes.Count == input.PageCount && pageIndexes.SequenceEqual(Enumerable.Range(0, input.PageCount)))
|
|||
|
|
{
|
|||
|
|
return Task.FromResult(pdfPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
using var output = new PdfDocument();
|
|||
|
|
output.Info.Title = input.Info.Title;
|
|||
|
|
output.Info.Author = input.Info.Author;
|
|||
|
|
output.Info.Subject = input.Info.Subject;
|
|||
|
|
output.Info.Keywords = input.Info.Keywords;
|
|||
|
|
|
|||
|
|
foreach (var index in pageIndexes)
|
|||
|
|
{
|
|||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|||
|
|
output.AddPage(input.Pages[index]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var outputPath = CreateUniquePath(Path.Combine(outputDirectory, $"{Path.GetFileNameWithoutExtension(pdfPath)}.pages-{NormalizePageRangeForFileName(pageRange)}.pdf"));
|
|||
|
|
output.Save(outputPath);
|
|||
|
|
return Task.FromResult(outputPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public Task<string> MergeAsync(IEnumerable<CloudPrintDocument> documents, string outputPath, CancellationToken cancellationToken = default)
|
|||
|
|
{
|
|||
|
|
ArgumentNullException.ThrowIfNull(documents);
|
|||
|
|
if (string.IsNullOrWhiteSpace(outputPath))
|
|||
|
|
{
|
|||
|
|
throw new ArgumentException("输出路径不能为空", nameof(outputPath));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? Directory.GetCurrentDirectory());
|
|||
|
|
using var output = new PdfDocument();
|
|||
|
|
|
|||
|
|
foreach (var document in documents)
|
|||
|
|
{
|
|||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|||
|
|
if (!IsPdf(document.FilePath))
|
|||
|
|
{
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
using var input = PdfReader.Open(document.FilePath, PdfDocumentOpenMode.Import);
|
|||
|
|
foreach (var page in input.Pages)
|
|||
|
|
{
|
|||
|
|
output.AddPage(page);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (output.PageCount == 0)
|
|||
|
|
{
|
|||
|
|
throw new InvalidOperationException("没有可合并的 PDF 文件");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var uniqueOutputPath = CreateUniquePath(outputPath);
|
|||
|
|
output.Save(uniqueOutputPath);
|
|||
|
|
return Task.FromResult(uniqueOutputPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static IReadOnlyList<int> ParsePageRange(string pageRange, int pageCount)
|
|||
|
|
{
|
|||
|
|
if (pageCount <= 0)
|
|||
|
|
{
|
|||
|
|
throw new InvalidOperationException("PDF 没有可打印页面");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var pages = new SortedSet<int>();
|
|||
|
|
foreach (var rawPart in pageRange.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
|||
|
|
{
|
|||
|
|
var part = rawPart.Replace('-', '-').Replace('—', '-');
|
|||
|
|
if (part.Contains('-', StringComparison.Ordinal))
|
|||
|
|
{
|
|||
|
|
var bounds = part.Split('-', 2, StringSplitOptions.TrimEntries);
|
|||
|
|
var start = ParseOneBasedPageNumber(bounds[0], pageCount);
|
|||
|
|
var end = ParseOneBasedPageNumber(bounds[1], pageCount);
|
|||
|
|
if (start > end)
|
|||
|
|
{
|
|||
|
|
throw new ArgumentException($"页码范围无效:{rawPart}", nameof(pageRange));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (var page = start; page <= end; page++)
|
|||
|
|
{
|
|||
|
|
pages.Add(page - 1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
pages.Add(ParseOneBasedPageNumber(part, pageCount) - 1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (pages.Count == 0)
|
|||
|
|
{
|
|||
|
|
throw new ArgumentException("页码范围不能为空", nameof(pageRange));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return pages.ToArray();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static int ParseOneBasedPageNumber(string value, int pageCount)
|
|||
|
|
{
|
|||
|
|
if (!int.TryParse(value, out var page) || page < 1 || page > pageCount)
|
|||
|
|
{
|
|||
|
|
throw new ArgumentOutOfRangeException(nameof(value), $"页码 {value} 超出有效范围 1-{pageCount}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return page;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void ValidatePdfPath(string pdfPath)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrWhiteSpace(pdfPath))
|
|||
|
|
{
|
|||
|
|
throw new ArgumentException("PDF 路径不能为空", nameof(pdfPath));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!File.Exists(pdfPath))
|
|||
|
|
{
|
|||
|
|
throw new FileNotFoundException("PDF 文件不存在", pdfPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!IsPdf(pdfPath))
|
|||
|
|
{
|
|||
|
|
throw new ArgumentException("文件不是 PDF", nameof(pdfPath));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static bool IsPdf(string path)
|
|||
|
|
{
|
|||
|
|
return Path.GetExtension(path).Equals(".pdf", StringComparison.OrdinalIgnoreCase);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string CreateSiblingOutputPath(string sourcePath, string suffix)
|
|||
|
|
{
|
|||
|
|
var directory = Path.GetDirectoryName(sourcePath) ?? Directory.GetCurrentDirectory();
|
|||
|
|
var outputPath = Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(sourcePath)}.{suffix}.pdf");
|
|||
|
|
return CreateUniquePath(outputPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string CreateUniquePath(string path)
|
|||
|
|
{
|
|||
|
|
if (!File.Exists(path))
|
|||
|
|
{
|
|||
|
|
return path;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var directory = Path.GetDirectoryName(path) ?? Directory.GetCurrentDirectory();
|
|||
|
|
var fileName = Path.GetFileNameWithoutExtension(path);
|
|||
|
|
var extension = Path.GetExtension(path);
|
|||
|
|
for (var index = 1; index < 10_000; index++)
|
|||
|
|
{
|
|||
|
|
var candidate = Path.Combine(directory, $"{fileName}-{index}{extension}");
|
|||
|
|
if (!File.Exists(candidate))
|
|||
|
|
{
|
|||
|
|
return candidate;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw new IOException($"无法创建唯一文件路径:{path}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string NormalizePageRangeForFileName(string pageRange)
|
|||
|
|
{
|
|||
|
|
return string.Concat(pageRange.Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')).Trim('-');
|
|||
|
|
}
|
|||
|
|
}
|