建立文件審閱系統: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:

  1. 包含所有評論的完整版本。
  2. 僅供審閱者的版本(過濾內部備註)。
  3. 用於最終批准的乾淨版本(僅保留高亮)。

情境 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。

大檔案處理策略

  1. 逐頁處理 – 逐頁載入、標註並保存。
  2. 串流輸出 – 將 Save 方法導向 MemoryStream,以避免中間寫入磁碟。
  3. 暫存檔清理 – 每次操作後刪除 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 提供標註功能時,請遵循以下模式:

  1. Controller 從客戶端接收 PDF 串流。
  2. 驗證檔案大小(若超過 200 MB 且未有特殊處理則拒絕)。
  3. 在 using 區塊內實例化 Annotator,以確保釋放。
  4. 根據描述標註類型、座標與文字的 JSON 載荷 套用標註。
  5. 保存至暫存位置,然後以適當的 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.Annotation for .NET 建置 文件審閱系統。您已學會如何設定函式庫、加入區域、橢圓與文字標註、過濾保存,並在處理大型 PDF 時保持低記憶體使用量。

您今天可以採取的下一步行動:

  1. 實驗 其他標註類型,例如 ArrowAnnotation 與 StampAnnotation。
  2. 整合 此工作流程至現有的 ASP.NET Core API 或桌面 WPF 應用程式。
  3. 探索 完整的 API 參考,發掘如標註版本管理與自訂中繼資料等進階功能。
  4. 加入 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);
}

相關教學