Annotations

Add annotation support to the viewer

Annotations in Doconut work in two directions: users draw them in the browser widget and the server keeps them per page, or your code builds them and loads them into an open session. Either way they render on the pages and can be burned into PDF or 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 page must include the Viewer resources, the Viewer toolbar, the Viewer mount, and an initialized objViewer; the Annotation Ribbon is then mounted and attached to that same instance.

Emit the annotation files with the viewer files. They are license-gated, so their tags appear only when the capability is granted. In MVC 5, build the tags in the controller (views have no @inject):

csharp
var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();
ViewBag.ViewerCss = viewer.ReferenceCss(new CssConfig
{
    IncludeViewerCss     = true,
    IncludeAnnotationCss = true    // jquery-ui.min.css + annotationBar.css
});
ViewBag.ViewerScripts = viewer.ReferenceScripts(new ScriptConfig
{
    IncludeJQuery            = true,
    IncludeViewerScripts     = true,
    IncludeAnnotationScripts = true,  // jquery-ui, raphael.js, annotation.js
    IncludeAnnotationBar     = true   // the embedded annotation ribbon
});
ViewBag.IsAnnotationEnabled = viewer.IsAnnotationEnabled;

Keep the complete Viewer composition in the markup:

html
<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 files generate the Ribbon inside annBarMount; you do not copy its buttons or dialogs. Initialize docViewer first, then create the Ribbon only when the server confirms that Annotation is licensed:

html
@Html.Raw(ViewBag.ViewerScripts)
<script>
    var annBar = null;
    var currentToken = '';

    var objViewer = $('#div_ctlDoc').docViewer({
        BasePath: '/',
        ResPath:  'doconut-res/images',
        onAnnLoaded:    function () { if (annBar) annBar.handleAnnLoaded(); },
        onAnnSaved:     function () { if (annBar) annBar.handleAnnSaved(); },
        onAnnSaveError: function () { if (annBar) annBar.handleAnnSaveError(); },
        onAnnClosed:    function () { if (annBar) annBar.handleAnnClosed(); },
        onError:        function (message) { console.error('Viewer error:', message); }
    });

    @if ((bool)ViewBag.IsAnnotationEnabled)
    {
        <text>
    annBar = $('#annBarMount').doconutAnnotationBar({
        docId: 'ctlDoc',
        getRequestParams: function () { return { token: currentToken }; },
        onLayout: function () { requestAnimationFrame(function () { objViewer.Refit(); }); }
    });
    annBar.attach(objViewer);
        </text>
    }
</script>

Without the capability, ReferenceScripts leaves out the Ribbon's script, so the server-side if keeps an unguarded doconutAnnotationBar(...) call from breaking the page. In Web Forms, write the same guard as <% if (IsAnnotationEnabled) { %> … <% } %>.

Saving from the Ribbon posts the data to DocImage.axd (AnnSave), which stores it in the document session per page. Loading (AnnLoad) happens automatically when a page with annotations renders.

Open and close the Ribbon from your own toolbar with annBar.open() and annBar.close(). Its public API:

MethodPurpose
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 the Ribbon state / the viewer's annotation-editing state
reopenEditable()Reload the current page's annotations as editable objects
updateActionState()Refresh the save and delete controls after host changes
headerSlot()The optional header slot for host-owned controls

onStatus, onToast, onLayout, onEditStart, and onEditEnd are optional host callbacks. The endpoints object can also provide exportPdf, exportPng, imageUpload, and imageList; controls without a configured endpoint stay hidden.

Two kinds of callback, wired two ways

The annotation code uses two callback styles, and each fails silently when wired the other way:

  • onAnnLoaded, onAnnSaved, onAnnSaveError, onAnnClosed are options of docViewer({...}), like every viewer callback.
  • Created, Changed, Deleted, and Properties are global functions found by name: the viewer id prefixed to the event. For the viewer in #div_ctlDoc the id is ctlDoc, so the functions are ctlDoc_Created, ctlDoc_Changed, ctlDoc_Deleted, and ctlDoc_Properties. A misspelled or renamed function is simply never called.

SaveAnnotations() does not return whether it saved: the result arrives through onAnnSaved or onAnnSaveError. Code that reads its return value never clears its "unsaved changes" flag.

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 and Color):

csharp
// POST /Annotations/LoadSample?token=...
[HttpPost]
public ActionResult LoadSample(string token)
{
    var viewer = Doconut.DoconutHost.Services.GetRequiredService<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 viewer fetches them, and the renderer burns them into
    // image/PDF exports.
    viewer.LoadAnnotationData(token, manager);
    return new HttpStatusCodeResult(200);
}

Annotation types

All types live in Doconut.Annotations and inherit from BaseAnnotation (page number plus bounding Rectangle):

TypeNotes
StampAnnotationText stamp with font size, border, color; supports Opacity, Rotate
NoteAnnotationSticky note with text, background color, font size, TitleColor
RectangleAnnotationBorder and fill colors, Title/ShowTitle
CircleAnnotationBorder and fill, ShowBorder
EllipseAnnotationBorder and fill, ShowBorder
TriangleAnnotationBorder color, BackColor, ShowBorder
LineAnnotationStraight line with width and color
ArrowAnnotationLine with arrowhead; settable Direction (ArrowDirection, compass points, default E)
FreehandAnnotationFree stroke from encoded FreehandData points
ImageAnnotationImage from a URL. A relative URL is resolved against the current request's host when the annotation is added — see the export note below

The AnnotationManager API

MemberPurpose
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 — the envelope the widget consumes
GetAnnotationXml()XML form

Viewer mirrors the load and read operations against a session: LoadAnnotationData(token, manager) or LoadAnnotationData(token, encodedData), LoadAnnotationXML(token, xml), and GetAnnotationXML(token).

Export with annotations burned in

csharp
// PDF of all pages with annotations rendered onto them
public async Task<ActionResult> ExportPdf(string token)
{
    var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();
    byte[] pdf = await viewer.ExportAnnotationsToPdfAsync(token, zoom: 100, ct: Response.ClientDisconnectedToken);
    return File(pdf, "application/pdf", "export.pdf");
}

// Or a ZIP of per-page PNGs
public async Task<ActionResult> ExportPngZip(string token)
{
    var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();
    byte[] zip = await viewer.ExportAnnotationsToPngZipAsync(token, zoom: 100, ct: Response.ClientDisconnectedToken);
    return File(zip, "application/zip", "annotations-png.zip");
}

Exports use the same burner as on-screen rendering, and the same license and custom-watermark decision.

Image annotations on an intranet host export as "Image blocked"

The viewer and the export fetch an ImageAnnotation's image from different places:

SurfaceWho downloads the imageOn an intranet or development host
The viewer in the browserThe user's browserThe image appears
ExportAnnotationsToPdfAsync / …PngZipAsyncThe serverA red "Image blocked" placeholder

When the server fetches an image to burn it in, it refuses addresses that point back into private networks: loopback (localhost, 127.0.0.1), 0.0.0.0, link-local (169.254.x.x), the private ranges 10.x, 172.16–31.x, and 192.168.x, carrier-grade NAT (100.64.0.0/10), and IPv6 link-local and unique-local addresses. This protects the server from being used to reach internal systems through a crafted image URL, and there is no switch to turn it off.

A relative image URL resolves against the host of the current request, so on localhost, on an intranet server, or on any site reached through a private address, the image shows in the viewer and the export contains the placeholder instead. That is the guard working, not a rendering failure. If exports must contain the image, serve it from a host with a public address and use an absolute URL. An image URL that cannot be resolved at all makes that one annotation disappear without an error — check the rendered pixels or the network log, not a status code.

Persistence workflow

  1. Open the document and obtain its token.
  2. Load previously stored XML or encoded data into that token.
  3. Let the widget read and edit the session annotations.
  4. Retrieve XML with GetAnnotationXML(token) when your application decides to persist.
  5. Export PDF or PNG when a flattened deliverable is required.
  6. Close the document session.

Do not use the viewer token as a permanent annotation identifier — it dies with the session, and a recycle of the application pool ends every session on a single server. Store annotation data against your own document and version identifiers.

Security and rendering notes

  • Annotation requests use the same session and token security as page requests.
  • Validate and control any user-supplied image URL.
  • Large freehand payloads and high-resolution exports increase memory use; test realistic documents and zoom values.

Troubleshooting

SymptomCheck
The Annotation Ribbon is missingThe Annotation capability and the four annotation CSS/script flags
Saving reports an errorToken or session expiration, and the widget's BasePath
ctlDoc_Created (or another event function) never runsThe function name: viewer id + _ + event, as a global function
C# annotations do not appearPage numbers are one-based, and the data was loaded into the active token
An image annotation shows on screen but not in the exportThe image's host has a private or loopback address — see above
A reopened document has no annotationsPersist XML or data outside the viewer session, then load it into the new token

Apakah halaman ini membantu?