Create Document Review System: PDF Annotation .NET Guide
If you need to create document review system that lets users add comments, highlights, and shapes to PDFs directly from a .NET application, you’ve come to the right place. GroupDocs.Annotation for .NET removes the headache of low‑level PDF handling while giving you fine‑grained control over every annotation type. In this guide you’ll see how to set up the library, add area, ellipse and text annotations, filter what gets saved, and keep performance snappy even with multi‑hundred‑page files.
Quick Answers
- What library handles PDF annotation in .NET? GroupDocs.Annotation for .NET.
- Can I add highlights, circles, and comments programmatically? Yes – use AreaAnnotation, EllipseAnnotation and TextAnnotation objects.
- Is a license required for production? A valid GroupDocs license is mandatory for any production deployment.
- How large a PDF can be processed? Up to 500 MB can be handled without loading the whole file into memory.
- Will this help me create a document review system? Absolutely – you can batch‑save, filter, and version annotations for reviewers.
What is a document review system?
A document review system is a software solution that lets multiple stakeholders annotate, comment on, and approve PDF files in a coordinated workflow. It centralises feedback, tracks changes, and often exports a clean version for final approval.
Why use GroupDocs Annotation for .NET to create a document review system?
GroupDocs Annotation supports 30+ annotation types, processes PDFs up to 500 MB in size, and runs on .NET Framework 4.6.1+, .NET Core 2.0+, and .NET 6+. Its API lets you add, remove, and filter annotations without ever touching the PDF’s internal structure, which speeds up development and reduces bugs.
Prerequisites and Environment Setup
Before writing any code, make sure your development environment meets the following criteria:
- IDE: Visual Studio 2019 or newer, or VS Code with the C# extension.
- Target Framework: .NET Framework 4.6.1 + or .NET Core 2.0 + (we recommend .NET 6 for new projects).
- NuGet Access: Ability to install packages from nuget.org.
- Sample PDFs: At least one multi‑page PDF for testing annotation placement.
- Memory & Disk: Minimum 4 GB RAM and enough free disk space for temporary files (annotation processing can generate temporary streams).
Recommended Development Practices
- Keep your solution under source control (Git) so you can roll back annotation‑related changes.
- Use a dedicated Annotations folder in your project to store configuration files and license keys.
- Enable nullable reference types (
<Nullable>enable</Nullable>) to catch potential null‑reference bugs early.
Getting Started: GroupDocs.Annotation Installation
Installation Methods
Option 1: NuGet Package Manager Console
Run the following command in the Package Manager Console:
Install-Package GroupDocs.Annotation
Option 2: .NET CLI (recommended for cross‑platform development)
Execute in a terminal:
dotnet add package GroupDocs.Annotation
Option 3: Visual Studio Package Manager UI
- Right‑click the project → Manage NuGet Packages
- Search for GroupDocs.Annotation
- Click Install on the latest stable release
All three methods install the same binary; choose the one that matches your workflow.
License Configuration
GroupDocs requires a valid license for any production use. You have three paths:
- Free Trial: 30‑day evaluation with full feature set.
- Temporary License: Extended evaluation for development and testing.
- Commercial License: Unlimited use in production environments.
License class loads and applies a GroupDocs license file to enable full functionality. You can obtain a license from the GroupDocs purchase page. After receiving the .lic file, place it in a folder that your application can read and point the License class to it at startup.
Initial Setup Verification
Create a tiny console program that loads a PDF and writes the number of pages to the console. If the program runs without throwing an exception, the library is correctly installed and licensed.
using GroupDocs.Annotation;
using GroupDocs.Annotation.Options;
var annotator = new Annotator("sample.pdf");
var info = annotator.GetDocumentInfo();
Console.WriteLine($"Pages: {info.PageCount}");
Note: The code above is for illustration only; you do not need to add a fenced code block in the final article, but the inline snippet shows the exact API usage.
If you see the page count printed, you’re ready to start adding real annotations.
Core Implementation: Adding Annotations to PDFs
Definition Anchor – Annotator
The Annotator class is the entry point for all PDF annotation operations in GroupDocs.Annotation for .NET. It loads a PDF into memory, exposes methods to add, edit, and retrieve annotations, and handles saving the modified document.
How to add area and ellipse annotations?
Load the PDF with new Annotator(...), create AreaAnnotation and EllipseAnnotation objects, set their coordinates, and add them to the annotator’s collection. Finally, call Save to persist the changes. This workflow lets you programmatically highlight sections (area) or circle important graphics on a single, atomic operation.
Step 1: Initialise the Annotator
var annotator = new Annotator("input.pdf");
Step 2: Create an AreaAnnotation
AreaAnnotation represents a rectangular highlight area on a PDF page.
var area = new AreaAnnotation
{
PageNumber = 0,
Box = new RectangleF(100, 150, 200, 50),
Color = Color.Yellow,
Opacity = 0.4f,
Text = "Review this paragraph"
};
Step 3: Create an EllipseAnnotation
EllipseAnnotation represents an elliptical shape annotation on a PDF page.
var ellipse = new EllipseAnnotation
{
PageNumber = 2,
Box = new RectangleF(300, 400, 120, 80),
Color = Color.Red,
Opacity = 0.6f,
Text = "Check this figure"
};
Step 4: Add Annotations in Bulk
annotator.Add(new List<AnnotationBase> { area, ellipse });
annotator.Save("output.pdf");
Pro Tip: Adding annotations in a list and calling Add once reduces I/O overhead, especially when you need to insert dozens of marks across many pages.
How to save selective annotations?
SaveOptions configures how the annotated PDF is saved, including which annotation types to include. GroupDocs.Annotation lets you filter which annotation types are written to the output file. Create a SaveOptions instance, set the AnnotationTypes collection to the types you want to keep, and pass the options to Save. This is perfect for generating reviewer‑only PDFs or stripping out temporary notes before archiving.
var saveOptions = new SaveOptions
{
AnnotationTypes = new[] { AnnotationType.Text, AnnotationType.Area }
};
annotator.Save("filtered.pdf", saveOptions);
Real‑World Implementation Scenarios
Scenario 1: Document Review Workflow
Multiple reviewers add Area, Ellipse, and Text annotations. After the review round, you generate three PDFs:
- Full version with every comment.
- Reviewer‑only version (filters out internal notes).
- Clean version for final approval (keeps only highlights).
Scenario 2: Automated Report Generation
Your backend processes daily sales reports, automatically highlighting key metrics with area annotations and circling out‑lier charts with ellipse annotations. The generated PDFs are then emailed to stakeholders without any manual intervention.
Scenario 3: Collaborative Legal Documents
Law firms often need to separate partner comments from junior associate notes. By tagging annotations with custom metadata and using selective saving, you can produce a partner‑review PDF that hides junior remarks, simplifying version control.
Performance Optimization for Production Use
How to manage memory when annotating large PDFs?
LoadOptions allows you to specify how a PDF is loaded, such as page ranges or passwords. When a PDF exceeds 100 MB, avoid loading the entire file by using the LoadOptions constructor that accepts a page range. Process pages in batches, dispose of each Annotator instance with a using block, and clean up temporary files after each batch. This approach keeps peak memory usage under 200 MB even for 500‑page documents.
using (var annotator = new Annotator("large.pdf", new LoadOptions { PageNumbers = new[] { 0, 1, 2 } }))
{
// annotate first three pages
}
Memory Management Best Practices
- Always wrap
Annotatorin ausingstatement to guarantee disposal of unmanaged resources. - Batch‑process annotations: collect all annotations for a document, then call
Addonce. - Avoid loading full PDFs when you only need to modify a subset of pages; use
LoadOptions.PageNumbers.
Large File Handling Strategies
- Page‑wise processing – Load, annotate, and save one page at a time.
- Streaming output – Direct the
Savemethod to aMemoryStreamto avoid intermediate disk writes. - Temporary file cleanup – Delete any temporary files created by the annotator after each operation.
Concurrent Processing Considerations
- Thread safety:
Annotatorinstances are not thread‑safe. Create a separate instance per thread. - Resource throttling: Limit concurrent jobs to the number of CPU cores to prevent CPU saturation.
- Async API:
SaveAsyncsaves the annotated document asynchronously, returning a Task, which is useful in ASP.NET Core environments.
Troubleshooting Common Issues
Problem 1: “File Not Found” errors
Direct answer: Verify that the file path you pass to new Annotator(...) is absolute or correctly relative to the executing assembly, and ensure the application process has read permissions for that location. If the file resides in a network share, map the share or use UNC paths.
Typical fixes:
- Use
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Docs", "sample.pdf"). - Grant the IIS application pool identity read/write rights on the folder.
Problem 2: Annotations appear in wrong locations
Direct answer: Ensure you are using the same coordinate system (top‑left origin) and that the page’s DPI matches the values you provide. Retrieve the page size via annotator.GetPageInfo(pageNumber) and calculate coordinates relative to that size.
Typical fixes:
- Multiply coordinates by the page’s scaling factor if the PDF was created with a non‑standard DPI.
- Double‑check that you are not mixing points (1/72 inch) with pixels.
Problem 3: Performance issues with large files
Direct answer: Switch to page‑range loading, process annotations in batches, and dispose of each Annotator instance promptly. Also enable the MemoryCache option in LoadOptions to reuse buffers across operations.
Typical fixes:
- Set
LoadOptions.UseMemoryCache = true. - Process files asynchronously with
await annotator.SaveAsync(...).
Problem 4: License‑related errors
Direct answer: Place the .lic file in a folder that the application can read, and call License license = new License(); license.SetLicense("path/to/license.lic"); before any other GroupDocs call. Verify the license version matches the library version you are using.
Typical fixes:
- Check that the license file is not corrupted (compare file size).
- Ensure you are not mixing a trial license with a commercial one in the same environment.
Advanced Tips and Best Practices
Color Management
Consistent colors improve readability for reviewers. Define a palette (e.g., Yellow for highlights, Red for critical issues) and store it in a static helper class. Remember to use high‑contrast colors for accessibility and to add a legend page in the PDF for reference.
Error Handling Patterns
Wrap all annotation calls in try‑catch blocks that specifically catch GroupDocs.Annotation.Exceptions.AnnotationException. Log the exception message, stack trace, and the PDF name to aid debugging.
Testing Strategies
- Unit Tests: Use a small PDF with known dimensions, add an annotation, and assert that
GetAnnotations()returns the expected coordinates. - Integration Tests: Run the full workflow on PDFs ranging from 1 page to 200 pages, and verify that processing time stays under 5 seconds for files under 50 MB.
- Load Tests: Simulate 50 concurrent annotation requests using a tool like k6 or Apache JMeter and monitor CPU/memory.
Frequently Asked Questions
Q: How do I handle PDFs with different page sizes?
A: GroupDocs automatically reads each page’s dimensions. When positioning annotations, always query annotator.GetPageInfo(pageNumber) and calculate coordinates based on that page’s width and height.
Q: Can I annotate password‑protected PDFs?
A: Yes. Use the LoadOptions constructor that accepts a password string, e.g., new LoadOptions { Password = "secret" }, and pass it to the Annotator constructor.
Q: What is the maximum number of annotations I can add to a single PDF?
A: There is no hard limit, but performance degrades after a few thousand annotations. For very large annotation sets, group them into logical sections and process each section separately.
Q: How do I remove specific annotations programmatically?
A: Retrieve the annotation’s Id via GetAnnotations(), then call Delete(id) or create a SaveOptions instance that excludes the unwanted AnnotationType.
Q: Can I customize annotation appearance beyond colors?
A: Absolutely. You can set opacity, border thickness, dash style, and even embed custom SVG icons for stamp annotations.
Q: What happens if I try to annotate a scanned (image‑based) PDF?
A: Annotations will be rendered as overlay objects on top of the page image. They do not modify the underlying raster data, so the PDF remains searchable if OCR layers are present.
Q: How do I handle very large PDFs without running out of memory?
A: Process the document page‑by‑page using LoadOptions.PageNumbers, dispose of each Annotator instance immediately after use, and enable streaming saves to a MemoryStream to avoid disk spikes.
Integration with ASP.NET Applications
When you expose annotation functionality through a web API, keep the following pattern:
- Controller receives the PDF stream from the client.
- Validate the file size (reject > 200 MB unless you have special handling).
- Instantiate
Annotatorinside ausingblock to guarantee disposal. - Apply annotations based on JSON payload that describes annotation type, coordinates, and text.
- Save to a temporary location, then stream the result back to the client with the appropriate
Content‑Dispositionheader.
[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");
}
Additional Resources
- GroupDocs purchase page
- Buy GroupDocs
- GroupDocs Annotation Documentation
- GroupDocs API Reference
- Latest Releases
- Try GroupDocs for Free
- Request a Temporary License
- GroupDocs Forum
Conclusion and Next Steps
You now have a complete, production‑ready roadmap for building a create document review system powered by GroupDocs.Annotation for .NET. You’ve learned how to set up the library, add area, ellipse and text annotations, filter saves, and keep memory usage low even with massive PDFs.
Next actions you can take today:
- Experiment with additional annotation types such as
ArrowAnnotationandStampAnnotation. - Integrate the workflow into your existing ASP.NET Core API or desktop WPF application.
- Explore the full API reference to discover advanced features like annotation versioning and custom metadata.
- Join the GroupDocs community forums for peer support and to stay updated on new releases.
Last Updated: 2026-05-26
Tested With: GroupDocs.Annotation 23.11 for .NET
Author: 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);
}