如何在 C# 中將 PuppeteerSharp 遷移到 IronPDF
從 傀儡師夏普 遷移到 IronPDF 會將您的 PDF 生成工作流程從一個具有 300MB 以上依賴項的瀏覽器自動化工具轉變為具有自動記憶體管理功能的專用 PDF 庫。 本指南提供完整的、逐步的遷移路徑,消除 Chromium 下載,解決記憶體洩漏問題,並提供全面的 PDF 操作功能。
為什麼要從 傀儡師夏普 遷移到 IronPDF
理解木偶師夏普
PuppeteerSharp 是 Google Puppeteer 的 .NET 移植版,將瀏覽器自動化功能引入 C#。 它使用 Chrome 內建的列印到 PDF 功能來產生 PDF 文件——就像在瀏覽器中按下 Ctrl+P 一樣。 這樣就能產生針對紙張優化的可列印輸出,這與你在螢幕上看到的內容有所不同。
PuppeteerSharp 的設計用途是網頁測試和資料抓取,而不是文件產生。 雖然 傀儡師夏普 能夠產生 PDF 文件,但它會帶來重大的生產挑戰。
瀏覽器自動化問題
PuppeteerSharp 的設計初衷是用於瀏覽器自動化,而不是文件產生。 這會為使用 PDF 文件帶來根本性問題:
- 首次使用前需下載 300MB 以上的 Chromium 。 傀儡師夏普 的一個顯著缺點是其部署體積龐大,這主要是由於它捆綁了 Chromium 二進位。 這種巨大的體積會導致 Docker 映像膨脹,並在無伺服器環境中造成冷啟動問題。
2.高負載下出現記憶體洩漏,需要手動回收瀏覽器。 在高負載情況下,PuppeteerSharp 會出現記憶體洩漏問題。 瀏覽器實例不斷累積內存,需要人工幹預進行進程管理和回收。
- 具有瀏覽器生命週期管理的複雜非同步模式。
4.列印到 PDF 輸出(相當於 Ctrl+P,而不是螢幕截圖)。 佈局可能會重新排版,背景預設可能會省略,並且輸出會分頁以便列印,而不是與瀏覽器視窗相符。
5.不支援 PDF/A 或 PDF/UA合規性要求。 傀儡師夏普 無法產生符合 PDF/A(存檔)或 PDF/UA(無障礙)標準的文件。
6.不允許對 PDF 進行任何操作- 僅產生 PDF,不允許合併/分割/編輯。 雖然 傀儡師夏普 能夠有效率地產生 PDF 文件,但它缺乏對 PDF 文件進行進一步操作(例如合併、分割、保護或編輯 PDF 文件)的功能。
傀儡師夏普 與 IronPDF 的比較
| 方面 | 傀儡師夏普 | IronPDF |
|---|---|---|
| 主要目的 | 瀏覽器自動化 | PDF生成 |
| 鉻依賴性 | 300MB+ 單獨下載 | 內建優化引擎 |
| API複雜度 | 非同步瀏覽器/頁面生命週期 | 同步單句 |
| 初始化 | BrowserFetcher.DownloadAsync() + LaunchAsync | new ChromePdfRenderer() |
| 記憶體管理 | 需要手動回收瀏覽器 | 自動的 |
| 記憶體負載 | 500MB以上,有洩漏 | 約50MB穩定版 |
| 冷啟動 | 45秒以上 | 約20秒 |
| PDF/A 支持 | 無法使用 | 全力支持 |
| PDF/UA 無障礙訪問 | 無法使用 | 全力支持 |
| PDF編輯 | 無法使用 | 合併、拆分、蓋章、編輯 |
| 數位簽名 | 無法使用 | 全力支持 |
| 螺紋安全 | 有限的 | 滿的 |
| 專業支援 | 社群 | 商業服務水準協議 |
平台支援
| 圖書館 | .NET Framework 4.7.2 | .NET Core 3.1 | .NET 6-8 | .NET 10 |
|---|---|---|---|---|
| IronPDF | 滿的 | 滿的 | 滿的 | 滿的 |
| 傀儡師夏普 | 有限的 | 滿的 | 滿的 | 待辦的 |
IronPDF 對 .NET 平台的廣泛支援確保開發人員可以在各種環境中利用它而不會遇到相容性問題,為現代 .NET 應用程式提供靈活的選擇,直至 2025 年和 2026 年。
開始之前
先決條件
- .NET 環境: .NET Framework 4.6.2+ 或 .NET Core 3.1+ / .NET 5/6/7/8/9+
- NuGet 存取權限:能夠安裝 NuGet 套件
- IronPDF 許可證:請從ironpdf.com取得您的許可證密鑰。
NuGet 套件變更
# Remove PuppeteerSharp
dotnet remove package PuppeteerSharp
# Remove downloaded Chromium binaries (~300MB recovered)
# Delete the .local-chromium folder
# Add IronPDF
dotnet add package IronPdf# Remove PuppeteerSharp
dotnet remove package PuppeteerSharp
# Remove downloaded Chromium binaries (~300MB recovered)
# Delete the .local-chromium folder
# Add IronPDF
dotnet add package IronPdf使用 IronPDF 不需要BrowserFetcher.DownloadAsync() - 渲染引擎會自動打包。
許可證配置
// Add at application startup
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";// Add at application startup
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";完整 API 參考
命名空間變更
// Before: PuppeteerSharp
using PuppeteerSharp;
using PuppeteerSharp.Media;
using System.Threading.Tasks;
// After: IronPDF
using IronPdf;
using IronPdf.Rendering;// Before: PuppeteerSharp
using PuppeteerSharp;
using PuppeteerSharp.Media;
using System.Threading.Tasks;
// After: IronPDF
using IronPdf;
using IronPdf.Rendering;核心 API 映射
| 傀儡師夏普 API | IronPDF API | 筆記 |
|---|---|---|
new BrowserFetcher().DownloadAsync() | 不需要 | 無需下載瀏覽器 |
Puppeteer.LaunchAsync(options) | 不需要 | 無瀏覽器管理 |
browser.NewPageAsync() | 不需要 | 無頁面上下文 |
page.GoToAsync(url) | renderer.RenderUrlAsPdf(url) | 直接渲染 |
page.SetContentAsync(html) | renderer.RenderHtmlAsPdf(html) | 直接渲染 |
page.PdfAsync(path) | pdf.SaveAs(path) | 渲染後 |
await page.CloseAsync() | 不需要 | 自動清理 |
await browser.CloseAsync() | 不需要 | 自動清理 |
PdfOptions.Format | RenderingOptions.PaperSize | 紙張尺寸 |
PdfOptions.Landscape | RenderingOptions.PaperOrientation | 方向 |
PdfOptions.MarginOptions | RenderingOptions.MarginTop/Bottom/Left/Right | 個人利潤 |
PdfOptions.PrintBackground | RenderingOptions.PrintHtmlBackgrounds | 背景印刷 |
PdfOptions.HeaderTemplate | RenderingOptions.HtmlHeader | HTML 頭部 |
PdfOptions.FooterTemplate | RenderingOptions.HtmlFooter | HTML頁腳 |
page.WaitForSelectorAsync() | RenderingOptions.WaitFor.HtmlElementId | 等待元素 |
程式碼遷移範例
範例 1:基本的 HTML 轉 PDF 轉換
之前(PuppeteerSharp):
// NuGet: Install-Package PuppeteerSharp
using PuppeteerSharp;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true
});
await using var page = await browser.NewPageAsync();
await page.SetContentAsync("<h1>Hello World</h1><p>This is a PDF document.</p>");
await page.PdfAsync("output.pdf");
}
}// NuGet: Install-Package PuppeteerSharp
using PuppeteerSharp;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true
});
await using var page = await browser.NewPageAsync();
await page.SetContentAsync("<h1>Hello World</h1><p>This is a PDF document.</p>");
await page.PdfAsync("output.pdf");
}
}(IronPDF 之後):
// NuGet: Install-Package IronPdf
using IronPdf;
class Program
{
static void Main(string[] args)
{
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><p>This is a PDF document.</p>");
pdf.SaveAs("output.pdf");
}
}// NuGet: Install-Package IronPdf
using IronPdf;
class Program
{
static void Main(string[] args)
{
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><p>This is a PDF document.</p>");
pdf.SaveAs("output.pdf");
}
}這個例子展示了架構上的根本差異。 傀儡師夏普 需要六個非同步操作: BrowserFetcher.DownloadAsync() (下載 300MB 以上的 Chromium 內容)、 Puppeteer.LaunchAsync() 、 browser.NewPageAsync() 、 page.SetContentAsync() 、 browser.NewPageAsync() 、 page.PdfAsync()和await using 。
IronPDF 消除了所有這些複雜性:建立一個ChromePdfRenderer ,呼叫RenderHtmlAsPdf() ,然後SaveAs() 。 沒有非同步模式,沒有瀏覽器生命週期,沒有 Chromium 下載。 IronPDF 的方法提供了更簡潔的語法和與現代 .NET 應用程式更好的整合。 請參閱HTML 轉 PDF 文件以取得完整範例。
範例 2:URL 轉 PDF
之前(PuppeteerSharp):
// NuGet: Install-Package PuppeteerSharp
using PuppeteerSharp;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true
});
await using var page = await browser.NewPageAsync();
await page.GoToAsync("https://www.example.com");
await page.PdfAsync("webpage.pdf");
}
}// NuGet: Install-Package PuppeteerSharp
using PuppeteerSharp;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true
});
await using var page = await browser.NewPageAsync();
await page.GoToAsync("https://www.example.com");
await page.PdfAsync("webpage.pdf");
}
}(IronPDF 之後):
// NuGet: Install-Package IronPdf
using IronPdf;
class Program
{
static void Main(string[] args)
{
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderUrlAsPdf("https://www.example.com");
pdf.SaveAs("webpage.pdf");
}
}// NuGet: Install-Package IronPdf
using IronPdf;
class Program
{
static void Main(string[] args)
{
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderUrlAsPdf("https://www.example.com");
pdf.SaveAs("webpage.pdf");
}
}PuppeteerSharp 使用GoToAsync()導覽至 URL,然後使用PdfAsync() 。 IronPDF 提供了一個RenderUrlAsPdf()方法,該方法可在一個呼叫中處理導覽和 PDF 產生。 了解更多信息,請閱讀我們的教程。
範例 3:自訂頁面設定(含頁邊距)
之前(PuppeteerSharp):
// NuGet: Install-Package PuppeteerSharp
using PuppeteerSharp;
using PuppeteerSharp.Media;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true
});
await using var page = await browser.NewPageAsync();
await page.SetContentAsync("<h1>Custom PDF</h1><p>With landscape orientation and margins.</p>");
await page.PdfAsync("custom.pdf", new PdfOptions
{
Format = PaperFormat.A4,
Landscape = true,
MarginOptions = new MarginOptions
{
Top = "20mm",
Bottom = "20mm",
Left = "20mm",
Right = "20mm"
}
});
}
}// NuGet: Install-Package PuppeteerSharp
using PuppeteerSharp;
using PuppeteerSharp.Media;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true
});
await using var page = await browser.NewPageAsync();
await page.SetContentAsync("<h1>Custom PDF</h1><p>With landscape orientation and margins.</p>");
await page.PdfAsync("custom.pdf", new PdfOptions
{
Format = PaperFormat.A4,
Landscape = true,
MarginOptions = new MarginOptions
{
Top = "20mm",
Bottom = "20mm",
Left = "20mm",
Right = "20mm"
}
});
}
}(IronPDF 之後):
// NuGet: Install-Package IronPdf
using IronPdf;
using IronPdf.Rendering;
class Program
{
static void Main(string[] args)
{
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;
renderer.RenderingOptions.PaperOrientation = PdfPaperOrientation.Landscape;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;
renderer.RenderingOptions.MarginLeft = 20;
renderer.RenderingOptions.MarginRight = 20;
var pdf = renderer.RenderHtmlAsPdf("<h1>Custom PDF</h1><p>With landscape orientation and margins.</p>");
pdf.SaveAs("custom.pdf");
}
}// NuGet: Install-Package IronPdf
using IronPdf;
using IronPdf.Rendering;
class Program
{
static void Main(string[] args)
{
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;
renderer.RenderingOptions.PaperOrientation = PdfPaperOrientation.Landscape;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;
renderer.RenderingOptions.MarginLeft = 20;
renderer.RenderingOptions.MarginRight = 20;
var pdf = renderer.RenderHtmlAsPdf("<h1>Custom PDF</h1><p>With landscape orientation and margins.</p>");
pdf.SaveAs("custom.pdf");
}
}此範例展示了兩個庫之間的 PDF 選項映射關係。 傀儡師夏普 使用PdfOptions其中Format 、 Landscape和MarginOptions包含字串值( "20mm" )。 IronPDF 使用RenderingOptions屬性,其中包含直接的紙張尺寸枚舉、方向枚舉和以毫米為單位的數值邊距值。
關鍵映射:
Format = PaperFormat.A4→PaperSize = PdfPaperSize.A4Landscape = true→PaperOrientation = PdfPaperOrientation.LandscapeMarginOptions.Top = "20mm"→MarginTop = 20(數值毫米)
內存洩漏問題
PuppeteerSharp 在持續高負載下會大量佔用內存,這是其臭名昭著的問題:
// ❌ 傀儡師夏普 - Memory grows with each operation
// Requires explicit browser recycling every N operations
for (int i = 0; i < 1000; i++)
{
var page = await browser.NewPageAsync();
await page.SetContentAsync($"<h1>Document {i}</h1>");
await page.PdfAsync($"doc_{i}.pdf");
await page.CloseAsync(); // Memory still accumulates!
}
// Must periodically: await browser.CloseAsync(); and re-launch
// ✅ IronPDF - Stable memory, reuse renderer
var renderer = new ChromePdfRenderer();
for (int i = 0; i < 1000; i++)
{
var pdf = renderer.RenderHtmlAsPdf($"<h1>Document {i}</h1>");
pdf.SaveAs($"doc_{i}.pdf");
// Memory managed automatically
}// ❌ 傀儡師夏普 - Memory grows with each operation
// Requires explicit browser recycling every N operations
for (int i = 0; i < 1000; i++)
{
var page = await browser.NewPageAsync();
await page.SetContentAsync($"<h1>Document {i}</h1>");
await page.PdfAsync($"doc_{i}.pdf");
await page.CloseAsync(); // Memory still accumulates!
}
// Must periodically: await browser.CloseAsync(); and re-launch
// ✅ IronPDF - Stable memory, reuse renderer
var renderer = new ChromePdfRenderer();
for (int i = 0; i < 1000; i++)
{
var pdf = renderer.RenderHtmlAsPdf($"<h1>Document {i}</h1>");
pdf.SaveAs($"doc_{i}.pdf");
// Memory managed automatically
}IronPDF 無需像 傀儡師夏普 那樣使用瀏覽器池基礎架構:
// Before (PuppeteerSharp - delete entire class)
public class PuppeteerBrowserPool
{
private readonly ConcurrentBag<IBrowser> _browsers;
private readonly SemaphoreSlim _semaphore;
private int _operationCount;
// ... recycling logic ...
}
// After (IronPDF - simple reuse)
public class PdfService
{
private readonly ChromePdfRenderer _renderer = new();
public byte[] Generate(string html)
{
return _renderer.RenderHtmlAsPdf(html).BinaryData;
}
}// Before (PuppeteerSharp - delete entire class)
public class PuppeteerBrowserPool
{
private readonly ConcurrentBag<IBrowser> _browsers;
private readonly SemaphoreSlim _semaphore;
private int _operationCount;
// ... recycling logic ...
}
// After (IronPDF - simple reuse)
public class PdfService
{
private readonly ChromePdfRenderer _renderer = new();
public byte[] Generate(string html)
{
return _renderer.RenderHtmlAsPdf(html).BinaryData;
}
}關鍵遷移說明
異步到同步轉換
PuppeteerSharp 全程需要 async/await; IronPDF 支援同步操作:
// PuppeteerSharp: Async required
public async Task<byte[]> GeneratePdfAsync(string html)
{
await new BrowserFetcher().DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(...);
await using var page = await browser.NewPageAsync();
await page.SetContentAsync(html);
return await page.PdfDataAsync();
}
// IronPDF: Sync default
public byte[] GeneratePdf(string html)
{
var renderer = new ChromePdfRenderer();
return renderer.RenderHtmlAsPdf(html).BinaryData;
}
// Or async when needed
public async Task<byte[]> GeneratePdfAsync(string html)
{
var renderer = new ChromePdfRenderer();
var pdf = await renderer.RenderHtmlAsPdfAsync(html);
return pdf.BinaryData;
}// PuppeteerSharp: Async required
public async Task<byte[]> GeneratePdfAsync(string html)
{
await new BrowserFetcher().DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(...);
await using var page = await browser.NewPageAsync();
await page.SetContentAsync(html);
return await page.PdfDataAsync();
}
// IronPDF: Sync default
public byte[] GeneratePdf(string html)
{
var renderer = new ChromePdfRenderer();
return renderer.RenderHtmlAsPdf(html).BinaryData;
}
// Or async when needed
public async Task<byte[]> GeneratePdfAsync(string html)
{
var renderer = new ChromePdfRenderer();
var pdf = await renderer.RenderHtmlAsPdfAsync(html);
return pdf.BinaryData;
}邊際單位轉換
PuppeteerSharp 使用字串單元; IronPDF 使用數值毫米:
// 傀儡師夏普 - string units
MarginOptions = new MarginOptions
{
Top = "1in", // 25.4mm
Bottom = "0.75in", // 19mm
Left = "1cm", // 10mm
Right = "20px" // ~7.5mm at 96dpi
}
// IronPDF - numeric millimeters
renderer.RenderingOptions.MarginTop = 25; // mm
renderer.RenderingOptions.MarginBottom = 19;
renderer.RenderingOptions.MarginLeft = 10;
renderer.RenderingOptions.MarginRight = 8;// 傀儡師夏普 - string units
MarginOptions = new MarginOptions
{
Top = "1in", // 25.4mm
Bottom = "0.75in", // 19mm
Left = "1cm", // 10mm
Right = "20px" // ~7.5mm at 96dpi
}
// IronPDF - numeric millimeters
renderer.RenderingOptions.MarginTop = 25; // mm
renderer.RenderingOptions.MarginBottom = 19;
renderer.RenderingOptions.MarginLeft = 10;
renderer.RenderingOptions.MarginRight = 8;頁首/頁尾佔位符轉換
| 傀儡師夏普 課程 | IronPDF佔位符 |
|---|---|
<span> class="pageNumber"> | {page} |
<span> class="totalPages"> | {total-pages} |
<span> class="date"> | {date} |
<span> class="title"> | {html-title} |
遷移後的新功能
遷移到 IronPDF 後,您將獲得 傀儡師夏普 無法提供的功能:
PDF合併
var pdf1 = renderer.RenderHtmlAsPdf(html1);
var pdf2 = renderer.RenderHtmlAsPdf(html2);
var merged = PdfDocument.Merge(pdf1, pdf2);
merged.SaveAs("merged.pdf");var pdf1 = renderer.RenderHtmlAsPdf(html1);
var pdf2 = renderer.RenderHtmlAsPdf(html2);
var merged = PdfDocument.Merge(pdf1, pdf2);
merged.SaveAs("merged.pdf");水印
var watermark = new TextStamper
{
Text = "CONFIDENTIAL",
FontSize = 48,
Opacity = 30,
Rotation = -45
};
pdf.ApplyStamp(watermark);var watermark = new TextStamper
{
Text = "CONFIDENTIAL",
FontSize = 48,
Opacity = 30,
Rotation = -45
};
pdf.ApplyStamp(watermark);密碼保護
pdf.SecuritySettings.OwnerPassword = "admin";
pdf.SecuritySettings.UserPassword = "readonly";
pdf.SecuritySettings.AllowUserCopyPasteContent = false;pdf.SecuritySettings.OwnerPassword = "admin";
pdf.SecuritySettings.UserPassword = "readonly";
pdf.SecuritySettings.AllowUserCopyPasteContent = false;數位簽名
var signature = new PdfSignature("certificate.pfx", "password");
pdf.Sign(signature);var signature = new PdfSignature("certificate.pfx", "password");
pdf.Sign(signature);PDF/A 合規性
pdf.SaveAsPdfA("archive.pdf", PdfAVersions.PdfA3b);pdf.SaveAsPdfA("archive.pdf", PdfAVersions.PdfA3b);性能比較摘要
| 指標 | 傀儡師夏普 | IronPDF | 改進 |
|---|---|---|---|
| 第一個 PDF(冷啟動) | 45歲以上 | 約20秒 | 速度提升 55% 以上 |
| 後續PDF | 多變的 | 持續的 | 可預測的 |
| 記憶體使用情況 | 500MB+(持續成長) | 約50MB(穩定版) | 記憶體減少 90% |
| 磁碟空間(Chromium) | 300MB+ | 0 | 取消下載 |
| 瀏覽器下載 | 必需的 | 不需要 | 零設定 |
| 螺紋安全 | 有限的 | 滿的 | 可靠的並發性 |
| PDF產生時間 | 45秒 | 20多歲 | 速度提升 55% |
遷移清單
遷移前
- 識別程式碼庫中所有 傀儡師夏普 的使用情況
- 文檔邊距值(將字串轉換為毫米)
- 注意頁首/頁尾佔位符語法以進行轉換
- 刪除瀏覽器池/回收基礎設施
- 從ironpdf.com取得 IronPDF 許可證金鑰
軟體包變更
- 刪除
PuppeteerSharpNuGet 套件 - 刪除
.local-chromium資料夾以回收約 300MB 磁碟空間 安裝IronPdfNuGet 套件:dotnet add package IronPdf
程式碼更改
- 更新命名空間匯入
- 移除
BrowserFetcher.DownloadAsync()調用 - 移除
Puppeteer.LaunchAsync()和瀏覽器管理 - 將
page.SetContentAsync()+page.PdfAsync()替換為RenderHtmlAsPdf() - 將
page.GoToAsync()+page.PdfAsync()替換為RenderUrlAsPdf() - 將邊距字串轉換為毫米值
- 轉換頁首/頁尾佔位符語法
- 刪除所有瀏覽器/頁面銷毀程式碼
- 刪除瀏覽器池基礎架構
- 在應用程式啟動時新增許可證初始化
移民後
- PDF 輸出的視覺比較
- 記憶體穩定性負載測試(應保持穩定,無需重新啟動)
- 驗證頁首/頁尾的顯示是否正確,並顯示頁碼
- 根據需要新增功能(安全性、浮水印、合併)






