using CloudPrint.Client.Models; using PdfSharp.Drawing; using PdfSharp.Pdf; using PdfSharp.Pdf.IO; using System.IO; namespace CloudPrint.Client.Helpers; /// /// 基于 PDFsharp 的 PDF 处理工具,负责打印前的页码范围裁剪、水印和合并。 /// public sealed class PdfProcessor { public Task 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 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 MergeAsync(IEnumerable 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 ParsePageRange(string pageRange, int pageCount) { if (pageCount <= 0) { throw new InvalidOperationException("PDF 没有可打印页面"); } var pages = new SortedSet(); 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('-'); } }