Files
client/Helpers/PdfProcessor.cs
2026-05-23 18:16:50 +08:00

227 lines
7.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('-');
}
}