建立文件審閱系統:PDF 標註 .NET 指南
如果您需要 建立文件審閱系統,讓使用者能直接從 .NET 應用程式在 PDF 上加入評論、標記與圖形,您來對地方了。GroupDocs.Annotation for .NET 消除低階 PDF 處理的麻煩,同時讓您對每種標註類型都有精細的控制。在本指南中,您將看到如何設定函式庫、加入區域、橢圓與文字標註、過濾保存內容,並在多百頁檔案下仍保持效能流暢。
快速解答
- 哪個函式庫處理 .NET 中的 PDF 標註? GroupDocs.Annotation for .NET.
- 我可以程式化地加入高亮、圓形和評論嗎? 可以 — 使用 AreaAnnotation、EllipseAnnotation 與 TextAnnotation 物件。
- 生產環境是否需要授權? 有效的 GroupDocs 授權是任何生產部署的必備條件。
- 可以處理多大的 PDF? 可處理最高 500 MB,且無需將整個檔案載入記憶體。
- 這能幫助我建立文件審閱系統嗎? 絕對可以 — 您可以批次保存、過濾並為審閱者管理標註版本。
什麼是文件審閱系統?
文件審閱系統 是一種軟體解決方案,讓多位利害關係人能在協調的工作流程中對 PDF 檔案進行標註、評論與批准。它集中回饋、追蹤變更,且常會匯出乾淨的最終批准版本。
為何使用 GroupDocs Annotation for .NET 來建立文件審閱系統?
GroupDocs Annotation 支援 30+ 種標註類型,可處理最高 500 MB 的 PDF,且可在 .NET Framework 4.6.1+、.NET Core 2.0+ 與 .NET 6+ 上執行。其 API 讓您在不觸及 PDF 內部結構的情況下新增、移除與過濾標註,從而加速開發並減少錯誤。
前置條件與環境設定
在撰寫任何程式碼之前,請確保您的開發環境符合以下條件:
- IDE: Visual Studio 2019 或更新版本,或搭配 C# 擴充功能的 VS Code。
- 目標框架: .NET Framework 4.6.1 + 或 .NET Core 2.0 + (我們建議新專案使用 .NET 6)。
- NuGet 取得權限: 能從 nuget.org 安裝套件。
- 範例 PDF: 至少一個多頁 PDF 用於測試標註放置。
- 記憶體與磁碟: 最低 4 GB RAM,且有足夠的磁碟空間供暫存檔使用(標註處理可能產生暫存串流)。
推薦的開發實踐
- 將您的解決方案放在版本控制系統(Git)下,以便回復與標註相關的變更。
- 在專案中使用專屬的 Annotations 資料夾來存放設定檔與授權金鑰。
- 啟用 nullable reference types (
<Nullable>enable</Nullable>) 以提前捕捉可能的 null 參考錯誤。
入門:GroupDocs.Annotation 安裝
安裝方式
選項 1:NuGet 套件管理員主控台
在 Package Manager Console 中執行以下指令:
Install-Package GroupDocs.Annotation
選項 2:.NET CLI(建議用於跨平台開發)
在終端機中執行:
dotnet add package GroupDocs.Annotation
選項 3:Visual Studio 套件管理員 UI
- 右鍵點擊專案 → Manage NuGet Packages
- 搜尋 GroupDocs.Annotation
- 在最新的穩定版上點擊 Install
以上三種方式安裝相同的二進位檔;請依您的工作流程選擇合適的方式。
授權設定
GroupDocs 需要有效授權才能在任何生產環境使用。您有三種取得方式:
- 免費試用: 30 天完整功能評估。
- 暫時授權: 延長的開發與測試評估。
- 商業授權: 在生產環境中無限制使用。
License 類別會載入並套用 GroupDocs 授權檔,以啟用完整功能。您可從 GroupDocs purchase page 取得授權。取得 .lic 檔後,將其放置於應用程式可讀取的資料夾,並在啟動時讓 License 類別指向該檔案。
初始設定驗證
建立一個小型主控台程式,載入 PDF 並將頁數寫入主控台。若程式執行時未拋出例外,即表示函式庫已正確安裝與授權。
using GroupDocs.Annotation;
using GroupDocs.Annotation.Options;
var annotator = new Annotator("sample.pdf");
var info = annotator.GetDocumentInfo();
Console.WriteLine($"Pages: {info.PageCount}");
注意: 上述程式碼僅作示範用;您 不需要 在最終文章中加入 fenced code block,但內嵌片段顯示了確切的 API 用法。
如果您看到列印出的頁數,即表示您已可以開始加入實際的標註。
核心實作:向 PDF 新增標註
定義錨點 – Annotator
Annotator 類別是 GroupDocs.Annotation for .NET 中所有 PDF 標註操作的入口點。它會將 PDF 載入記憶體,提供新增、編輯與取得標註的方法,並處理儲存修改後的文件。
如何新增區域與橢圓標註?
使用 new Annotator(...) 載入 PDF,建立 AreaAnnotation 與 EllipseAnnotation 物件,設定其座標,並將它們加入 annotator 的集合中。最後呼叫 Save 以永久保存變更。此工作流程讓您能以程式方式在單一原子操作中標示區域(area)或圈選重要圖形(ellipse)。
步驟 1:初始化 Annotator
var annotator = new Annotator("input.pdf");
步驟 2:建立 AreaAnnotation
AreaAnnotation 代表 PDF 頁面上的矩形高亮區域。
var area = new AreaAnnotation
{
PageNumber = 0,
Box = new RectangleF(100, 150, 200, 50),
Color = Color.Yellow,
Opacity = 0.4f,
Text = "Review this paragraph"
};
步驟 3:建立 EllipseAnnotation
EllipseAnnotation 代表 PDF 頁面上的橢圓形標註。
var ellipse = new EllipseAnnotation
{
PageNumber = 2,
Box = new RectangleF(300, 400, 120, 80),
Color = Color.Red,
Opacity = 0.6f,
Text = "Check this figure"
};
步驟 4:批次新增標註
annotator.Add(new List<AnnotationBase> { area, ellipse });
annotator.Save("output.pdf");
專業提示: 將標註放入清單中一次性呼叫 Add 可減少 I/O 開銷,特別是當您需要在多頁上插入數十個標記時。
如何保存選擇性的標註?
SaveOptions 設定標註 PDF 的保存方式,包括要包含的標註類型。GroupDocs.Annotation 允許您過濾寫入輸出檔的標註類型。建立 SaveOptions 實例,將 AnnotationTypes 集合設定為您想保留的類型,然後將此選項傳遞給 Save。這非常適合產生僅供審閱者的 PDF,或在歸檔前移除臨時註記。
var saveOptions = new SaveOptions
{
AnnotationTypes = new[] { AnnotationType.Text, AnnotationType.Area }
};
annotator.Save("filtered.pdf", saveOptions);
真實情境實作案例
情境 1:文件審閱工作流程
多位審閱者加入 Area、Ellipse 與 Text 標註。審閱結束後,您會產生三個 PDF:
- 包含所有評論的完整版本。
- 僅供審閱者的版本(過濾內部備註)。
- 用於最終批准的乾淨版本(僅保留高亮)。
情境 2:自動化報告產生
您的後端處理每日銷售報告,會自動使用區域標註突顯關鍵指標,並以橢圓標註圈出異常圖表。產生的 PDF 隨後會自動寄送給相關人員,無需任何人工介入。
情境 3:協同法律文件
律師事務所常需將合夥人評論與初級律師備註分開。透過為標註加上自訂中繼資料並使用選擇性保存,您可以產生只顯示合夥人審閱的 PDF,隱藏初級備註,簡化版本控制。
生產環境效能最佳化
如何在標註大型 PDF 時管理記憶體?
LoadOptions 允許您指定 PDF 的載入方式,例如頁碼範圍或密碼。當 PDF 超過 100 MB 時,請使用接受頁碼範圍的 LoadOptions 建構子,以避免載入整個檔案。將頁面分批處理,使用 using 區塊釋放每個 Annotator 實例,並在每批作業後清理暫存檔。此方法可在 500 頁文件中將峰值記憶體使用量控制在 200 MB 以下。
using (var annotator = new Annotator("large.pdf", new LoadOptions { PageNumbers = new[] { 0, 1, 2 } }))
{
// annotate first three pages
}
記憶體管理最佳實踐
- 始終將
Annotator包在using陳述式中,以確保釋放非受控資源。 - 批次處理標註:先收集文件的所有標註,然後一次呼叫
Add。 - 避免載入完整 PDF,若只需修改部分頁面,請使用
LoadOptions.PageNumbers。
大檔案處理策略
- 逐頁處理 – 逐頁載入、標註並保存。
- 串流輸出 – 將
Save方法導向MemoryStream,以避免中間寫入磁碟。 - 暫存檔清理 – 每次操作後刪除 annotator 產生的暫存檔。
並行處理考量
- 執行緒安全性:
Annotator實例不是執行緒安全的。每個執行緒請建立獨立的實例。 - 資源節流: 將同時執行的工作數限制為 CPU 核心數,以防止 CPU 飽和。
- 非同步 API:
SaveAsync以非同步方式保存標註文件,回傳 Task,於 ASP.NET Core 環境中相當有用。
常見問題排除
問題 1:「找不到檔案」錯誤
直接回答: 確認傳遞給 new Annotator(...) 的檔案路徑是絕對路徑或相對於執行組件正確的相對路徑,且確保應用程式執行個體對該位置具有讀取權限。若檔案位於網路共享,請映射該共享或使用 UNC 路徑。
常見解決方式:
- 使用
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Docs", "sample.pdf")。 - 為 IIS 應用程式池身分授予該資料夾的讀寫權限。
問題 2:標註出現在錯誤位置
直接回答: 確認使用相同的座標系統(左上角為原點),且頁面的 DPI 與您提供的數值相符。透過 annotator.GetPageInfo(pageNumber) 取得頁面尺寸,並以此尺寸計算座標。
常見解決方式:
- 若 PDF 使用非標準 DPI 建立,請將座標乘以頁面的縮放係數。
- 再次確認未混用點(1/72 吋)與像素。
問題 3:大型檔案的效能問題
直接回答: 改為使用頁碼範圍載入,批次處理標註,並即時釋放每個 Annotator 實例。同時在 LoadOptions 中啟用 MemoryCache 選項,以在多次操作間重複使用緩衝區。
常見解決方式:
- 設定
LoadOptions.UseMemoryCache = true。 - 使用
await annotator.SaveAsync(...)以非同步方式處理檔案。
問題 4:授權相關錯誤
直接回答: 將 .lic 檔放置於應用程式可讀取的資料夾,並在任何其他 GroupDocs 呼叫之前執行 License license = new License(); license.SetLicense("path/to/license.lic");。確認授權版本與您使用的函式庫版本相符。
常見解決方式:
- 檢查授權檔是否損毀(比對檔案大小)。
- 確保未在同一環境中混用試用授權與商業授權。
進階技巧與最佳實踐
色彩管理
一致的顏色可提升審閱者的可讀性。定義調色盤(例如,黃色用於高亮,紅色用於關鍵問題),並將其存放於靜態輔助類別中。請使用高對比度的顏色以符合無障礙需求,並在 PDF 中加入圖例頁供參考。
錯誤處理模式
將所有標註呼叫包在 try‑catch 區塊中,特別捕捉 GroupDocs.Annotation.Exceptions.AnnotationException。記錄例外訊息、堆疊追蹤與 PDF 名稱,以協助除錯。
測試策略
- 單元測試: 使用已知尺寸的小型 PDF,加入標註,並斷言
GetAnnotations()回傳預期的座標。 - 整合測試: 在 1 頁至 200 頁的 PDF 上執行完整工作流程,並驗證檔案小於 50 MB 時處理時間保持在 5 秒以內。
- 負載測試: 使用 k6 或 Apache JMeter 等工具模擬 50 個同時的標註請求,並監控 CPU/記憶體。
常見問答
Q:如何處理不同頁面尺寸的 PDF?
A:GroupDocs 會自動讀取每頁的尺寸。定位標註時,請始終透過 annotator.GetPageInfo(pageNumber) 取得該頁的寬高,並以此計算座標。
Q:我可以標註受密碼保護的 PDF 嗎?
A:可以。使用接受密碼字串的 LoadOptions 建構子,例如 new LoadOptions { Password = "secret" },並將其傳遞給 Annotator 建構子。
Q:單一 PDF 最多可以加入多少個標註?
A:沒有硬性上限,但在幾千個標註之後效能會下降。對於極大量的標註集合,請將其分成邏輯區段,分別處理每個區段。
Q:如何以程式方式移除特定標註?
A:透過 GetAnnotations() 取得標註的 Id,然後呼叫 Delete(id),或建立 SaveOptions 實例以排除不需要的 AnnotationType。
Q:我可以自訂標註外觀(超出顏色)嗎?
A:當然可以。您可以設定不透明度、邊框粗細、虛線樣式,甚至為印章標註嵌入自訂 SVG 圖示。
Q:如果我嘗試標註掃描(影像)PDF 會發生什麼?
A:標註會以覆蓋物件的形式呈現在頁面影像之上。它們不會修改底層的點陣資料,因此若 PDF 含有 OCR 層,仍可搜尋。
Q:如何在不耗盡記憶體的情況下處理極大型 PDF?
A:使用 LoadOptions.PageNumbers 逐頁處理文件,使用後立即釋放每個 Annotator 實例,並啟用將保存結果串流至 MemoryStream,以避免磁碟突增。
與 ASP.NET 應用程式整合
當您透過 Web API 提供標註功能時,請遵循以下模式:
- Controller 從客戶端接收 PDF 串流。
- 驗證檔案大小(若超過 200 MB 且未有特殊處理則拒絕)。
- 在
using區塊內實例化Annotator,以確保釋放。 - 根據描述標註類型、座標與文字的 JSON 載荷 套用標註。
- 保存至暫存位置,然後以適當的
Content‑Disposition標頭將結果串流回客戶端。
[HttpPost("annotate")]
public async Task<IActionResult> Annotate(IFormFile pdf, [FromBody] AnnotationRequest request)
{
using var stream = pdf.OpenReadStream();
var annotator = new Annotator(stream);
// add annotations based on request
var output = new MemoryStream();
annotator.Save(output);
output.Position = 0;
return File(output, "application/pdf", "annotated.pdf");
}
其他資源
- GroupDocs 購買頁面
- 購買 GroupDocs
- GroupDocs Annotation 文件
- GroupDocs API 參考
- 最新發行版
- 免費試用 GroupDocs
- 申請暫時授權
- GroupDocs 論壇
結論與後續步驟
您現在擁有一套 完整、可投入生產的路線圖,可使用 GroupDocs.Annotation for .NET 建置 文件審閱系統。您已學會如何設定函式庫、加入區域、橢圓與文字標註、過濾保存,並在處理大型 PDF 時保持低記憶體使用量。
您今天可以採取的下一步行動:
- 實驗 其他標註類型,例如
ArrowAnnotation與StampAnnotation。 - 整合 此工作流程至現有的 ASP.NET Core API 或桌面 WPF 應用程式。
- 探索 完整的 API 參考,發掘如標註版本管理與自訂中繼資料等進階功能。
- 加入 GroupDocs 社群論壇,獲得同儕支援並掌握新版本資訊。
最後更新: 2026-05-26
測試版本: GroupDocs.Annotation 23.11 for .NET
作者: GroupDocs
Install-Package GroupDocs.Annotation -Version 25.4.0
dotnet add package GroupDocs.Annotation --version 25.4.0
using System;
using GroupDocs.Annotation;
class Program
{
static void Main()
{
string inputPdfPath = "input.pdf";
try
{
// Initialize the Annotator with the path of the document
using (Annotator annotator = new Annotator(inputPdfPath))
{
Console.WriteLine("GroupDocs.Annotation initialized successfully!");
// Your annotation code will go here
}
}
catch (Exception ex)
{
Console.WriteLine($"Setup issue: {ex.Message}");
}
}
}
using (Annotator annotator = new Annotator(inputPdfPath))
{
// All annotation work happens inside this using block
// This ensures proper resource cleanup
}
AreaAnnotation areaAnnotation = new AreaAnnotation()
{
Box = new Rectangle(100, 100, 100, 100), // X, Y, Width, Height
BackgroundColor = 65535, // Yellow highlight
PageNumber = 0 // First page
};
EllipseAnnotation ellipseAnnotation = new EllipseAnnotation()
{
Box = new Rectangle(100, 100, 100, 100), // Same coordinate system
BackgroundColor = 123456, // Different color
PageNumber = 1 // Second page
};
annotator.Add(new List<AnnotationBase>() { areaAnnotation, ellipseAnnotation });
SaveOptions saveOptions = new SaveOptions
{
AnnotationTypes = AnnotationType.Ellipse // Only save ellipse annotations
};
annotator.Save(outputPath, saveOptions);
using (Annotator annotator = new Annotator(inputPdfPath))
{
// Your annotation code here
} // Automatic cleanup happens here
var annotationsToAdd = new List<AnnotationBase>();
// Add multiple annotations to the list
annotator.Add(annotationsToAdd); // Single operation
// Always verify file exists before processing
if (!File.Exists(inputPdfPath))
{
throw new FileNotFoundException($"PDF file not found: {inputPdfPath}");
}
// Use absolute paths when possible
string absolutePath = Path.GetFullPath(inputPdfPath);
// Test with known coordinates first
AreaAnnotation testAnnotation = new AreaAnnotation()
{
Box = new Rectangle(50, 50, 100, 100), // Start with simple values
BackgroundColor = 65535,
PageNumber = 0
};
// Verify page dimensions if needed
// Consider PDF scaling factors in your calculations
// Define color constants for consistency
public static class AnnotationColors
{
public const int ImportantHighlight = 65535; // Yellow
public const int AttentionMarker = 255; // Red
public const int ApprovedSection = 65280; // Green
}
try
{
using (Annotator annotator = new Annotator(inputPdfPath))
{
// Your annotation operations
annotator.Add(annotations);
annotator.Save(outputPath, saveOptions);
}
}
catch (GroupDocsException ex)
{
// Handle GroupDocs-specific errors
Logger.Error($"GroupDocs error: {ex.Message}");
}
catch (IOException ex)
{
// Handle file I/O errors
Logger.Error($"File operation error: {ex.Message}");
}
catch (Exception ex)
{
// Handle unexpected errors
Logger.Error($"Unexpected error: {ex.Message}");
}
[HttpPost]
public async Task<IActionResult> AnnotatePdf(IFormFile pdfFile, AnnotationRequest request)
{
// Validate input
if (pdfFile == null || pdfFile.Length == 0)
return BadRequest("No file provided");
// Process asynchronously to avoid blocking the UI
var result = await ProcessAnnotationsAsync(pdfFile, request);
return Ok(result);
}