Files
client/Helpers/PdfProcessor.cs

227 lines
7.9 KiB
C#
Raw Normal View History

2026-05-23 18:16:50 +08:00
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('-');
}
}