Annotations
Add annotation support to the viewer
Annotations in Doconut work in two directions: users draw them in the browser widget and the server persists them per page, or your code builds them programmatically and loads them into an open session. Either way they render on the pages and can be burned into PDF/PNG exports.
Annotation support is gated by the Annotation license capability (granted automatically under an active Temporary license).
Enable the annotation UI
Annotation is a Viewer module, not a standalone toolbar. The complete page must include
the Viewer resources, Viewer toolbar, Viewer mount, and initialized objViewer; the
Annotation Ribbon is then mounted and attached to that same instance.
Emit the annotation bundles alongside the viewer bundles — they are license-gated, so the tags only appear when the capability is available:
@Html.Raw(Viewer.ReferenceCss(new CssConfig
{
IncludeViewerCss = true,
IncludeAnnotationCss = true // jquery-ui.min.css + annotationBar.css
}))
@Html.Raw(Viewer.ReferenceScripts(new ScriptConfig
{
IncludeJQuery = true,
IncludeViewerScripts = true,
IncludeAnnotationScripts = true, // jquery-ui, raphael.js, annotation.js
IncludeAnnotationBar = true // the embedded annotation ribbon
}))Keep the complete Viewer composition visible in the markup:
<nav id="toolbar" aria-label="Document viewer controls">
<!-- Viewer controls, including the button that opens Annotation -->
</nav>
<div id="annBarMount"></div>
<div id="divDocViewer"><div id="div_ctlDoc"></div></div>The Annotation bundle generates the Ribbon DOM inside annBarMount; you do not need to
copy its buttons or dialog markup. Initialize docViewer first, then create the Ribbon
only when the server confirms that Annotation is licensed:
<script>
let annBar = null;
let currentToken = '';
const objViewer = $('#div_ctlDoc').docViewer({
BasePath: '/doconut',
ResPath: '/doconut-res/images',
onAnnLoaded: () => annBar?.handleAnnLoaded(),
onAnnSaved: () => annBar?.handleAnnSaved(),
onAnnSaveError: () => annBar?.handleAnnSaveError(),
onAnnClosed: () => annBar?.handleAnnClosed(),
onError: (message) => console.error('Viewer error:', message)
});
@if (Viewer.IsAnnotationEnabled)
{
<text>
annBar = $('#annBarMount').doconutAnnotationBar({
docId: 'ctlDoc',
getRequestParams: () => ({ token: currentToken }),
onStatus: (message) => console.log(message),
onToast: (message, type) => console.log(type, message),
onLayout: () => requestAnimationFrame(() => objViewer.Refit())
});
annBar.attach(objViewer);
</text>
}
</script>Saving from the Ribbon posts data through the middleware (AnnSave), which stores it in
the document session per page. Loading (AnnLoad) happens automatically when a page with
annotations renders. The four onAnn* callbacks keep the Ribbon synchronized with the
viewer lifecycle.
Open and close it from any host-owned Viewer toolbar:
annBar.open();
annBar.close();The public Ribbon API is:
| Method | Purpose |
|---|---|
attach(objViewer) | Connect the Ribbon to the initialized viewer; required once |
open() / close() | Enter or leave annotation editing |
reset() | Return the Ribbon to its closed, non-editing state |
isOpen() / annotating() | Read Ribbon state / the viewer's annotation-editing state |
reopenEditable() | Reload the current page annotations as editable objects |
updateActionState() | Refresh save/delete control availability after host changes |
headerSlot() | Get the optional header extension slot for host-owned controls |
onStatus, onToast, onLayout, onEditStart, and onEditEnd are optional host
callbacks. The endpoints object can additionally provide exportPdf, exportPng,
imageUpload, and imageList; controls without a configured endpoint remain hidden.
For the combined Viewer, Search, and Annotation startup sequence, see
Quick Start.
The annotation bundle adds the browser authoring tools, but the data still belongs to the server-side document session identified by the token. Reopening the source creates a new session; persist the XML or encoded annotation envelope in your application if annotations must survive beyond the session lifetime.
Build annotations in C#
Get a manager bound to the open session, add annotations, and load them (with using Doconut.Annotations; for the types and using System.Drawing; for Rectangle/Color):
app.MapPost("/api/annotations/load-sample", (string token, Viewer viewer) =>
{
// Bound to the open session's page dimensions
var manager = viewer.GetAnnotationManager(token);
var pageCount = viewer.GetPageCount(token);
// One stamp per page
for (int page = 1; page <= pageCount; page++)
{
manager.Add(new StampAnnotation(page, new Rectangle(30, 20, 240, 90),
$"PAGE {page}", 28, 4, Color.Maroon)
{
Opacity = 60,
Rotate = -8
});
}
manager.Add(new NoteAnnotation(1, new Rectangle(420, 150, 220, 120),
"Loaded from C# code.", Color.FromArgb(255, 255, 255, 170), 14));
// Load into the session — the widget fetches them via AnnLoad and the
// renderer burns them into image/PDF exports.
viewer.LoadAnnotationData(token, manager);
return Results.Ok();
});Annotation types
All types live in Doconut.Annotations and inherit from BaseAnnotation (page number + bounding Rectangle):
| Type | Notes |
|---|---|
StampAnnotation | Text stamp with font size, border, color; supports Opacity, Rotate |
NoteAnnotation | Sticky note with text, background color, font size, TitleColor |
RectangleAnnotation | Border + fill colors, Title/ShowTitle |
CircleAnnotation | Border + fill, ShowBorder |
EllipseAnnotation | Border + fill, ShowBorder |
TriangleAnnotation | Border color, BackColor, ShowBorder |
LineAnnotation | Straight line with width and color |
ArrowAnnotation | Line with arrowhead; settable Direction (type ArrowDirection, compass points, default E) |
FreehandAnnotation | Free stroke from encoded FreehandData points |
ImageAnnotation | Image from a URL. A relative URL is resolved against the request host when the annotation is added (only the image fetch happens at burn time) — it must be reachable from the server (e.g. a file under wwwroot served by UseStaticFiles) |
The AnnotationManager API
| Member | Purpose |
|---|---|
Add(BaseAnnotation) | Queue an annotation |
GetAnnotations() / GetAnnotations(int page) | Inspect what the manager holds |
ClearAnnotations() / ClearAnnotations(int page) | Remove all / per-page |
GetAnnotationData() / GetAnnotationData(int page) | Encoded annotation-data string — a Base64 wire envelope (what the widget consumes) |
GetAnnotationXml() | XML form |
Viewer mirrors the load/read operations against a session: LoadAnnotationData(token, manager) or LoadAnnotationData(token, encodedData) (the Base64 wire envelope from GetAnnotationData()), LoadAnnotationXML(token, xml), GetAnnotationXML(token).
Export with annotations burned in
// PDF of all pages with annotations rendered onto them
app.MapGet("/api/annotations/export-pdf", async (string token, Viewer viewer) =>
{
byte[] pdf = await viewer.ExportAnnotationsToPdfAsync(token, zoom: 100);
return Results.File(pdf, "application/pdf", "export.pdf");
});
// Or a ZIP of per-page PNGs
app.MapGet("/api/annotations/export-png-zip", async (string token, Viewer viewer) =>
{
byte[] zip = await viewer.ExportAnnotationsToPngZipAsync(token, zoom: 100);
return Results.File(zip, "application/zip", "annotations-png.zip");
});Exports use the same burner as on-screen rendering, so what users see is what the file contains.
Persistence workflow
- Open the document and obtain its token.
- Load previously stored XML or encoded data into that token.
- Let the widget read and edit the session annotations.
- Retrieve XML with
GetAnnotationXML(token)when your application decides to persist. - Export PDF/PNG when a flattened deliverable is required.
- Close the document session.
Do not use the opaque viewer token as a permanent annotation identifier. Associate persisted annotation data with your own document and version identifiers.
Security and rendering notes
- Annotation requests use the same session/token security as page requests.
- A relative
ImageAnnotationURL is resolved from the request host and must remain reachable to the server at burn time. - Validate and control any user-supplied image URL to avoid server-side request forgery.
- Exports apply the same license/custom-watermark decision as on-screen page rendering.
- Large freehand payloads and high-resolution exports increase memory use; test realistic documents and zoom values.
Troubleshooting
| Symptom | Check |
|---|---|
| Annotation ribbon is missing | Annotation capability and the four annotation CSS/script flags |
| Save callback reports an error | Token/session expiration and middleware BasePath |
| C# annotations do not appear | Page numbering is one-based and data was loaded into the active token |
| Image annotation appears on screen but not in export | The server can reach the image URL during burn-in |
| Reopened document has no annotations | Persist XML/data outside the viewer session, then load it into the new token |
Was this page helpful?