大多數對此問題的回答已經五年之久,只支援 PDF,且一開始就要求你測量看不見的頁面座標。這裡是 2026 版:七個呼叫、兩個決定標記實際落點的慣例,以及 Linux 容器所需的單行指令。
以下所有程式碼皆可在 .NET 8 與 .NET 10、Windows 與 Linux 上執行,且不需要安裝 Adobe Acrobat 或 Microsoft Office——相同程式碼同樣適用於 DOCX、XLSX、PPTX 與影像。只要更改檔案路徑即可。
可執行的範例放在 GitHub: annotate-pdf-csharp-net。
安裝
dotnet add package GroupDocs.Annotation
若未提供授權,函式庫會以評估模式執行,輸出會帶有評估標記。使用 temporary licence 可移除這些標記:
new License().SetLicense("GroupDocs.Annotation.lic");
開啟文件
using (Annotator annotator = new Annotator("quarterly-review.pdf"))
{
// 在此加入、讀取或移除標記
}
先說明一件事:Annotator 會保持檔案開啟直到釋放,且無法直接覆寫正在讀取的檔案。請另存至不同路徑,或先存成暫存檔再搬移至目標位置。
不要測量座標——搜尋文字
這是大多數教學跳過的部分,也是讓標註程式碼在文件升版後仍能正常運作的關鍵。GetDocumentInfo() 會回傳每一頁的尺寸與文字行,每行都有一個框:
IDocumentInfo info = annotator.Document.GetDocumentInfo();
foreach (PageInfo page in info.PagesInfo)
{
foreach (TextLineInfo line in page.TextLines)
{
if (line.Text.Contains("Hosting costs"))
{
// line.LeftIndent、line.TopIndent、line.Width、line.Height —— 以左上角為原點的點數
}
}
}
如此一來,標註會依照文件今天的內容放置,而不是依照去年文件的舊位置。
加入帶有評論與回覆的醒目標記
float left = (float)line.LeftIndent;
float right = (float)(line.LeftIndent + line.Width);
float top = (float)(page.Height - line.TopIndent);
float bottom = (float)(page.Height - line.TopIndent - line.Height);
annotator.Add(new HighlightAnnotation
{
PageNumber = page.PageNumber - 1,
BackgroundColor = 0xFFE699,
Opacity = 0.5,
Message = "Check this against the invoice",
CreatedOn = DateTime.Now,
User = new User { Id = 1, Name = "Reviewer", Role = Role.Editor },
Points = new List<Point>
{
new Point(left, top), new Point(right, top),
new Point(left, bottom), new Point(right, bottom)
},
Replies = new List<Reply>
{
new Reply { Comment = "Confirmed with finance.", RepliedOn = DateTime.Now }
}
});
annotator.Save("result.pdf");
Message 為評論內容。Replies 會啟動討論串——在 PDF 閱讀器中顯示為帶回覆的註解,在 Word 中則是帶回覆的評論。User 讓後續程式能精確找出並移除特定審閱者的標記,同時不影響其他人的標記。
兩個讓所有人都會踩到的慣例
仔細觀察計算 top 與 bottom 的兩行程式碼,以及 PageNumber 的使用方式。
頁碼計算兩次。 PageInfo.PageNumber 為 1 起始。AnnotationBase.PageNumber 為 0 起始。若混用兩者,所有標記都會錯位一頁——最後一頁的標記會因指向不存在的頁面而在儲存時被直接丟棄,且不會拋出例外或警告。若發現標記「沒有出現」,請先檢查頁碼索引。
兩種原點。 TextLineInfo 與 AreaAnnotation.Box 的測量基準是 左上角。文字標記(醒目、底線、刪除線、波浪線)則是以 左下角 為基準,這也是 PDF 本身的座標系統。
| 成員 | 原點 | 頁碼編號 |
|---|---|---|
PageInfo、TextLineInfo |
左上角 | 1 起始 |
AnnotationBase.PageNumber |
— | 0 起始 |
AreaAnnotation.Box |
左上角 | — |
HighlightAnnotation.Points |
左下角 | — |
PreviewOptions.PageNumbers |
— | 1 起始 |
四個點構成一個矩形,順序為:左上、右上、左下、右下。若同一個評論需要跨越三行文字,可在同一列表中依序加入多個矩形。
讀回標記內容
using (Annotator annotator = new Annotator("result.pdf"))
{
foreach (AnnotationBase a in annotator.Get())
{
Console.WriteLine($"#{a.Id} {a.Type} on page {a.PageNumber + 1}: {a.Message}");
foreach (Reply reply in a.Replies)
Console.WriteLine($" reply: {reply.Comment}");
}
}
Get() 會讀取文件實際包含的內容——包括使用 Word 或 Acrobat 加入的標記,這正是讓標記在不同工具間往返的關鍵。
需要留意的一點是:Id 是在讀取文件時即時分配的,並不會寫入檔案。它足夠穩定,可在同一工作階段內刪除或更新標記,但不適合作為資料庫的主鍵。若要將標記儲存於檔案之外,請使用匯入與匯出功能。
再次移除標記
annotator.Remove(1); // 依 Id 移除
annotator.Remove(annotator.Get()
.Where(a => a.User?.Name == "Reviewer").ToList()); // 依作者移除
渲染頁面
PreviewOptions previewOptions = new PreviewOptions(n => File.Create($"page_{n}.png"));
previewOptions.PreviewFormat = PreviewFormats.PNG;
previewOptions.PageNumbers = new[] { 1, 2 }; // 1 起始
previewOptions.RenderAnnotations = true;
previewOptions.Resolution = 144;
annotator.Document.GeneratePreview(previewOptions);
此功能在三種情境下特別有用:在 Web UI 中顯示文件、在測試中驗證標記是否正確落在預期位置,以及在自動化代理需要檢視自身工作成果時。
在 Linux 上
程式碼本身不變,容器需要變更:
FROM mcr.microsoft.com/dotnet/runtime:8.0
RUN apt-get update \
&& apt-get install -y --no-install-recommends fontconfig fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
.NET 執行時映像檔預設不含字型,而文字渲染必須依賴字型。這一行指令就是正確輸出與空白頁面之間的關鍵,也是最常見的 Linux 文件函式庫相關問題。
接下來可以做什麼
git clone https://github.com/groupdocs-annotation/annotate-pdf-csharp-net.git
cd annotate-pdf-csharp-net
dotnet run --project src/AnnotatePdf -- highlight samples/quarterly-review.pdf --text "Hosting costs"
- 文件說明:Annotate a PDF in C#
- 讓 LLM 審閱合約並留下真實評論 — 使用相同呼叫,由模型驅動
- 建置瀏覽器端標註 UI — 使用相同呼叫,由使用者在瀏覽器中操作