feat: 初始功能
This commit is contained in:
227
Helpers/PdfProcessor.cs
Normal file
227
Helpers/PdfProcessor.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
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('-');
|
||||
}
|
||||
}
|
||||
117
Helpers/WindowsPrintApi.cs
Normal file
117
Helpers/WindowsPrintApi.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CloudPrint.Client.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Windows Print Spooler API P/Invoke 声明,用于 RAW 打印、队列枚举和任务取消。
|
||||
/// </summary>
|
||||
public static class WindowsPrintApi
|
||||
{
|
||||
public const int JobControlCancel = 3;
|
||||
public const int JobControlDelete = 5;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public sealed class DocInfo
|
||||
{
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string DocumentName = string.Empty;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string? OutputFile;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string DataType = "RAW";
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SystemTime
|
||||
{
|
||||
public short Year;
|
||||
public short Month;
|
||||
public short DayOfWeek;
|
||||
public short Day;
|
||||
public short Hour;
|
||||
public short Minute;
|
||||
public short Second;
|
||||
public short Milliseconds;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public struct JobInfo1
|
||||
{
|
||||
public int JobId;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string? PrinterName;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string? MachineName;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string? UserName;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string? DocumentName;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string? DataType;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string? StatusText;
|
||||
|
||||
public int Status;
|
||||
public int Priority;
|
||||
public int Position;
|
||||
public int TotalPages;
|
||||
public int PagesPrinted;
|
||||
public SystemTime Submitted;
|
||||
}
|
||||
|
||||
[DllImport("winspool.drv", EntryPoint = "OpenPrinterW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool OpenPrinter(string printerName, out IntPtr printerHandle, IntPtr printerDefaults);
|
||||
|
||||
[DllImport("winspool.drv", EntryPoint = "ClosePrinter", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool ClosePrinter(IntPtr printerHandle);
|
||||
|
||||
[DllImport("winspool.drv", EntryPoint = "StartDocPrinterW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern int StartDocPrinter(IntPtr printerHandle, int level, [In] DocInfo documentInfo);
|
||||
|
||||
[DllImport("winspool.drv", EntryPoint = "EndDocPrinter", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EndDocPrinter(IntPtr printerHandle);
|
||||
|
||||
[DllImport("winspool.drv", EntryPoint = "StartPagePrinter", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool StartPagePrinter(IntPtr printerHandle);
|
||||
|
||||
[DllImport("winspool.drv", EntryPoint = "EndPagePrinter", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EndPagePrinter(IntPtr printerHandle);
|
||||
|
||||
[DllImport("winspool.drv", EntryPoint = "WritePrinter", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool WritePrinter(IntPtr printerHandle, byte[] data, int count, out int written);
|
||||
|
||||
[DllImport("winspool.drv", EntryPoint = "EnumJobsW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EnumJobs(
|
||||
IntPtr printerHandle,
|
||||
int firstJob,
|
||||
int numberOfJobs,
|
||||
int level,
|
||||
IntPtr jobBuffer,
|
||||
int bufferSize,
|
||||
out int bytesNeeded,
|
||||
out int jobsReturned);
|
||||
|
||||
[DllImport("winspool.drv", EntryPoint = "SetJobW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool SetJob(
|
||||
IntPtr printerHandle,
|
||||
int jobId,
|
||||
int level,
|
||||
IntPtr jobInfo,
|
||||
int command);
|
||||
}
|
||||
Reference in New Issue
Block a user