Annotations

Ajouter la prise en charge des annotations au visualiseur

Les annotations dans Doconut fonctionnent dans deux sens : les utilisateurs les dessinent dans le widget du navigateur et le serveur les conserve par page, ou votre code les crée et les charge dans une session ouverte. Dans les deux cas, elles s’affichent sur les pages et peuvent être intégrées dans les exportations PDF ou PNG.

Le support des annotations est conditionné par la capacité de licence Annotation (accordée automatiquement avec une licence Temporaire active).

Activer l’interface d’annotation

L’annotation est un module du Viewer, pas une barre d’outils autonome. La page doit inclure les ressources du Viewer, la barre d’outils du Viewer, le montage du Viewer, et un objViewer initialisé ; le Ruban d’annotation est alors monté et attaché à cette même instance.

Émettez les fichiers d’annotation avec les fichiers du viewer. Ils sont conditionnés par la licence, de sorte que leurs balises n’apparaissent que lorsque la capacité est accordée. Dans MVC 5, construisez les balises dans le contrôleur (les vues n’ont pas de @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   // le ruban d'annotation intégré
});
ViewBag.IsAnnotationEnabled = viewer.IsAnnotationEnabled;

Conservez la composition complète du Viewer dans le balisage :

html
<nav id="toolbar" aria-label="Document viewer controls">
    <!-- Contrôles du visualiseur, y compris le bouton qui ouvre Annotation -->
</nav>
<div id="annBarMount"></div>
<div id="divDocViewer"><div id="div_ctlDoc"></div></div>

Les fichiers d’annotation génèrent le Ruban à l’intérieur de annBarMount ; vous ne copiez pas ses boutons ni ses dialogues. Initialisez d’abord docViewer, puis créez le Ruban uniquement lorsque le serveur confirme que l’annotation est sous licence :

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>

Sans la capacité, ReferenceScripts omet le script du Ruban, de sorte que le if côté serveur empêche qu’un appel non protégé à doconutAnnotationBar(...) ne casse la page. En Web Forms, écrivez la même protection sous la forme <% if (IsAnnotationEnabled) { %> … <% } %>.

L’enregistrement depuis le Ruban envoie les données à DocImage.axd (AnnSave), qui les stocke dans la session du document par page. Le chargement (AnnLoad) se produit automatiquement lorsqu’une page contenant des annotations est rendue.

Ouvrez et fermez le Ruban depuis votre propre barre d’outils avec annBar.open() et annBar.close(). Son API publique :

MéthodeObjectif
attach(objViewer)Connecter le Ruban au viewer initialisé ; requis une fois
open() / close()Entrer ou quitter le mode d’édition d’annotation
reset()Retourner le Ruban à son état fermé, non‑édition
isOpen() / annotating()Lire l’état du Ruban / l’état d’édition d’annotation du viewer
reopenEditable()Recharger les annotations de la page actuelle en tant qu’objets éditables
updateActionState()Rafraîchir les contrôles d’enregistrement et de suppression après les changements d’hôte
headerSlot()L’emplacement d’en‑tête optionnel pour les contrôles appartenant à l’hôte

onStatus, onToast, onLayout, onEditStart et onEditEnd sont des rappels d’hôte optionnels. L’objet endpoints peut également fournir exportPdf, exportPng, imageUpload et imageList ; les contrôles sans point de terminaison configuré restent cachés.

Deux types de rappel, câblés de deux façons

Le code d'annotation utilise deux styles de rappel, et chacun échoue silencieusement lorsqu'il est câblé de l'autre manière :

  • onAnnLoaded, onAnnSaved, onAnnSaveError, onAnnClosed sont options de docViewer({...}), comme chaque rappel du visualiseur.
  • Created, Changed, Deleted, et Properties sont des fonctions globales trouvées par leur nom : l'identifiant du visualiseur préfixé à l'événement. Pour le visualiseur dans #div_ctlDoc, l'identifiant est ctlDoc, donc les fonctions sont ctlDoc_Created, ctlDoc_Changed, ctlDoc_Deleted et ctlDoc_Properties. Une fonction mal orthographiée ou renommée n'est simplement jamais appelée.

SaveAnnotations() ne renvoie pas si l'enregistrement a réussi : le résultat arrive via onAnnSaved ou onAnnSaveError. Le code qui lit sa valeur de retour ne supprime jamais son indicateur « modifications non enregistrées ».

Créer des annotations en C#

Obtenez un gestionnaire lié à la session ouverte, ajoutez des annotations, et chargez‑les (avec using Doconut.Annotations; pour les types et using System.Drawing; pour Rectangle et Color) :

csharp
// POST /Annotations/LoadSample?token=...
[HttpPost]
public ActionResult LoadSample(string token)
{
    var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();

    // Lié aux dimensions de page de la session ouverte
    var manager   = viewer.GetAnnotationManager(token);
    var pageCount = viewer.GetPageCount(token);

    // Un tampon par 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));

    // Charger dans la session - le visualiseur les récupère, et le moteur de rendu les intègre
    // Exportations image/PDF.
    viewer.LoadAnnotationData(token, manager);
    return new HttpStatusCodeResult(200);
}

Types d'annotation

Tous les types résident dans Doconut.Annotations et héritent de BaseAnnotation (numéro de page plus la boîte englobante Rectangle) :

TypeNotes
StampAnnotationTampon de texte avec taille de police, bordure, couleur ; prend en charge Opacity, Rotate
NoteAnnotationNote autocollante avec texte, couleur d'arrière‑plan, taille de police, TitleColor
RectangleAnnotationCouleurs de bordure et de remplissage, Title/ShowTitle
CircleAnnotationBordure et remplissage, ShowBorder
EllipseAnnotationBordure et remplissage, ShowBorder
TriangleAnnotationCouleur de bordure, BackColor, ShowBorder
LineAnnotationLigne droite avec épaisseur et couleur
ArrowAnnotationLigne avec pointe de flèche ; Direction réglable (ArrowDirection, points cardinaux, défaut E)
FreehandAnnotationTrait libre à partir de points FreehandData encodés
ImageAnnotationImage depuis une URL. Une URL relative est résolue par rapport à l'hôte de la requête courante lorsque l'annotation est ajoutée — voir la note d'exportation ci‑dessous

L'API AnnotationManager

MemberPurpose
Add(BaseAnnotation)Mettre en file d'attente une annotation
GetAnnotations() / GetAnnotations(int page)Inspecter ce que le gestionnaire contient
ClearAnnotations() / ClearAnnotations(int page)Supprimer tout / par page
GetAnnotationData() / GetAnnotationData(int page)Données d'annotation encodées — l'enveloppe consommée par le widget
GetAnnotationXml()Forme XML

Viewer reflète les opérations de chargement et de lecture contre une session : LoadAnnotationData(token, manager) ou LoadAnnotationData(token, encodedData), LoadAnnotationXML(token, xml), et GetAnnotationXML(token).

Exporter avec les annotations intégrées

csharp
// PDF de toutes les pages avec les annotations rendues dessus
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");
}

// Ou un ZIP de PNGs par page
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");
}

Les exportations utilisent le même brûleur que le rendu à l'écran, ainsi que la même licence et la même décision de filigrane personnalisé.

Annotations d'image sur un hôte intranet exportées comme « Image bloquée »

Le visualiseur et l'export récupèrent l'image d'un ImageAnnotation depuis des emplacements différents :

SurfaceQui télécharge l'imageSur un hôte intranet ou de développement
Le visualiseur dans le navigateurLe navigateur de l'utilisateurL'image apparaît
ExportAnnotationsToPdfAsync / …PngZipAsyncLe serveurUn espace réservé rouge "Image bloquée"

Lorsque le serveur récupère une image pour l'intégrer, il refuse les adresses qui renvoient vers des réseaux privés : boucle locale (localhost, 127.0.0.1), 0.0.0.0, lien‑local (169.254.x.x), les plages privées 10.x, 172.16–31.x, et 192.168.x, NAT de type carrier‑grade (100.64.0.0/10), ainsi que les adresses IPv6 lien‑local et unique‑local. Cela protège le serveur contre une utilisation pour atteindre des systèmes internes via une URL d'image manipulée, et il n'existe aucun commutateur pour le désactiver.

Une URL d'image relative se résout par rapport à l'hôte de la requête en cours, donc sur localhost, sur un serveur intranet, ou sur tout site atteint via une adresse privée, l'image s'affiche dans le visualiseur et l'export contient à la place l'espace réservé. C'est la protection qui fonctionne, pas un échec de rendu. Si les exports doivent contenir l'image, servez‑la depuis un hôte avec une adresse publique et utilisez une URL absolue. Une URL d'image qui ne peut pas être résolue du tout fait disparaître cette annotation sans erreur — vérifiez les pixels rendus ou le journal réseau, pas le code d'état.

Flux de persistance

  1. Ouvrez le document et obtenez son jeton.
  2. Chargez le XML ou les données encodées précédemment stockées dans ce jeton.
  3. Laissez le widget lire et modifier les annotations de la session.
  4. Récupérez le XML avec GetAnnotationXML(token) lorsque votre application décide de persister.
  5. Exportez en PDF ou PNG lorsqu'un livrable aplati est requis.
  6. Fermez la session du document.

N'utilisez pas le jeton du visualiseur comme identifiant d'annotation permanent — il disparaît avec la session, et un recyclage du pool d'applications termine chaque session sur un serveur unique. Stockez les données d'annotation avec vos propres identifiants de document et de version.

Notes de sécurité et de rendu

  • Les requêtes d'annotation utilisent la même sécurité de session et de jeton que les requêtes de page.
  • Validez et contrôlez toute URL d'image fournie par l'utilisateur.
  • Les charges utiles libres importantes et les exports haute résolution augmentent l'utilisation de la mémoire ; testez des documents réalistes et des valeurs de zoom.

Dépannage

SymptômeVérification
Le ruban d'annotation est manquantLa capacité Annotation et les quatre indicateurs CSS/script d'annotation
L'enregistrement signale une erreurExpiration du jeton ou de la session, et le BasePath du widget
ctlDoc_Created (ou une autre fonction d'événement) ne s'exécute jamaisLe nom de la fonction : id du visualiseur + _ + événement, en tant que fonction globale
Les annotations C# n'apparaissent pasLes numéros de page commencent à 1, et les données ont été chargées dans le jeton actif
Une annotation d'image s'affiche à l'écran mais pas dans l'exportL'hôte de l'image possède une adresse privée ou de boucle locale — voir ci‑dessus
Un document rouvert n'a aucune annotationPersistez le XML ou les données en dehors de la session du visualiseur, puis chargez‑les dans le nouveau jeton

Cette page était-elle utile ?