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) :
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 :
<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.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éthode | Objectif |
|---|---|
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,onAnnClosedsont options dedocViewer({...}), comme chaque rappel du visualiseur.Created,Changed,Deleted, etPropertiessont 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 estctlDoc, donc les fonctions sontctlDoc_Created,ctlDoc_Changed,ctlDoc_DeletedetctlDoc_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) :
// 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) :
| Type | Notes |
|---|---|
StampAnnotation | Tampon de texte avec taille de police, bordure, couleur ; prend en charge Opacity, Rotate |
NoteAnnotation | Note autocollante avec texte, couleur d'arrière‑plan, taille de police, TitleColor |
RectangleAnnotation | Couleurs de bordure et de remplissage, Title/ShowTitle |
CircleAnnotation | Bordure et remplissage, ShowBorder |
EllipseAnnotation | Bordure et remplissage, ShowBorder |
TriangleAnnotation | Couleur de bordure, BackColor, ShowBorder |
LineAnnotation | Ligne droite avec épaisseur et couleur |
ArrowAnnotation | Ligne avec pointe de flèche ; Direction réglable (ArrowDirection, points cardinaux, défaut E) |
FreehandAnnotation | Trait libre à partir de points FreehandData encodés |
ImageAnnotation | Image 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
| Member | Purpose |
|---|---|
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
// 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 :
| Surface | Qui télécharge l'image | Sur un hôte intranet ou de développement |
|---|---|---|
| Le visualiseur dans le navigateur | Le navigateur de l'utilisateur | L'image apparaît |
ExportAnnotationsToPdfAsync / …PngZipAsync | Le serveur | Un 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
- Ouvrez le document et obtenez son jeton.
- Chargez le XML ou les données encodées précédemment stockées dans ce jeton.
- Laissez le widget lire et modifier les annotations de la session.
- Récupérez le XML avec
GetAnnotationXML(token)lorsque votre application décide de persister. - Exportez en PDF ou PNG lorsqu'un livrable aplati est requis.
- 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ôme | Vérification |
|---|---|
| Le ruban d'annotation est manquant | La capacité Annotation et les quatre indicateurs CSS/script d'annotation |
| L'enregistrement signale une erreur | Expiration du jeton ou de la session, et le BasePath du widget |
ctlDoc_Created (ou une autre fonction d'événement) ne s'exécute jamais | Le nom de la fonction : id du visualiseur + _ + événement, en tant que fonction globale |
| Les annotations C# n'apparaissent pas | Les 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'export | L'hôte de l'image possède une adresse privée ou de boucle locale — voir ci‑dessus |
| Un document rouvert n'a aucune annotation | Persistez 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 ?