Anotaciones

Agregar soporte de anotaciones al visor

Las anotaciones en Doconut funcionan en dos direcciones: los usuarios las dibujan en el widget del navegador y el servidor las mantiene por página, o su código las crea y las carga en una sesión abierta. De cualquier manera se renderizan en las páginas y pueden incrustarse en exportaciones PDF o PNG.

El soporte de anotaciones está restringido por la capacidad de licencia Annotation (concedida automáticamente bajo una licencia Temporal activa).

Habilitar la interfaz de anotaciones

Annotation es un módulo del Viewer, no una barra de herramientas independiente. La página debe incluir los recursos del Viewer, la barra de herramientas del Viewer, el montaje del Viewer y un objViewer inicializado; luego la Cinta de Anotaciones se monta y se adjunta a esa misma instancia.

Emita los archivos de anotación con los archivos del visor. Están restringidos por licencia, por lo que sus etiquetas aparecen solo cuando se concede la capacidad. En MVC 5, construya las etiquetas en el controlador (las vistas no tienen @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;

Mantenga la composición completa del Viewer en el marcado:

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>

Los archivos de Annotation generan la Cinta dentro de annBarMount; no copie sus botones ni diálogos. Inicialice docViewer primero, luego cree la Cinta solo cuando el servidor confirme que Annotation está licenciado:

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>

Sin la capacidad, ReferenceScripts omite el script de la Cinta, de modo que el if del lado del servidor evita que una llamada sin protección a doconutAnnotationBar(...) rompa la página. En Web Forms, escriba la misma protección como <% if (IsAnnotationEnabled) { %> … <% } %>.

Guardar desde la Cinta envía los datos a DocImage.axd (AnnSave), que los almacena en la sesión del documento por página. La carga (AnnLoad) ocurre automáticamente cuando una página con anotaciones se renderiza.

Abra y cierre la Cinta desde su propia barra de herramientas con annBar.open() y annBar.close(). Su API pública:

MétodoPropósito
attach(objViewer)Conecta la cinta al visor inicializado; requerido una vez
open() / close()Entrar o salir de la edición de anotaciones
reset()Devuelve la cinta a su estado cerrado, sin edición
isOpen() / annotating()Leer el estado de la cinta / el estado de edición de anotaciones del visor
reopenEditable()Recargar las anotaciones de la página actual como objetos editables
updateActionState()Actualizar los controles de guardar y eliminar después de cambios en el host
headerSlot()El espacio opcional de encabezado para controles pertenecientes al host

onStatus, onToast, onLayout, onEditStart y onEditEnd son callbacks opcionales del host. El objeto endpoints también puede proporcionar exportPdf, exportPng, imageUpload y imageList; los controles sin un endpoint configurado permanecen ocultos.

Dos tipos de devolución de llamada, conectados de dos maneras

El código de anotaciones usa dos estilos de devolución de llamada, y cada uno falla silenciosamente cuando se conecta de la otra manera:

  • onAnnLoaded, onAnnSaved, onAnnSaveError, onAnnClosed son opciones de docViewer({...}), como cualquier devolución de llamada del visor.
  • Created, Changed, Deleted y Properties son funciones globales encontradas por nombre: el id del visor precede al evento. Para el visor en #div_ctlDoc el id es ctlDoc, así que las funciones son ctlDoc_Created, ctlDoc_Changed, ctlDoc_Deleted y ctlDoc_Properties. Una función mal escrita o renombrada simplemente nunca se llama.
  • SaveAnnotations() no devuelve si se guardó: el resultado llega a través de onAnnSaved o onAnnSaveError. El código que lee su valor de retorno nunca borra su bandera de "cambios no guardados".

Construir anotaciones en C#

Obtenga un gestor vinculado a la sesión abierta, añada anotaciones y cárguelas (con using Doconut.Annotations; para los tipos y using System.Drawing; para Rectangle y 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);
}

Tipos de anotación

Todos los tipos viven en Doconut.Annotations y heredan de BaseAnnotation (número de página más Rectangle delimitador):

TipoNotas
StampAnnotationSello de texto con tamaño de fuente, borde, color; admite Opacity, Rotate
NoteAnnotationNota adhesiva con texto, color de fondo, tamaño de fuente, TitleColor
RectangleAnnotationColores de borde y relleno, Title/ShowTitle
CircleAnnotationBorde y relleno, ShowBorder
EllipseAnnotationBorde y relleno, ShowBorder
TriangleAnnotationColor de borde, BackColor, ShowBorder
LineAnnotationLínea recta con ancho y color
ArrowAnnotationLínea con punta de flecha; Direction configurable (ArrowDirection, puntos cardinales, predeterminado E)
FreehandAnnotationTrazo libre a partir de puntos codificados FreehandData
ImageAnnotationImagen desde una URL. Una URL relativa se resuelve contra el host de la solicitud actual cuando se agrega la anotación — vea la nota de exportación a continuación

La API de AnnotationManager

MiembroPropósito
Add(BaseAnnotation)Encolar una anotación
GetAnnotations() / GetAnnotations(int page)Inspeccionar lo que el gestor contiene
ClearAnnotations() / ClearAnnotations(int page)Eliminar todas / por página
GetAnnotationData() / GetAnnotationData(int page)Datos de anotación codificados — el sobre que consume el widget
GetAnnotationXml()Forma XML

Viewer refleja las operaciones de carga y lectura contra una sesión: LoadAnnotationData(token, manager) o LoadAnnotationData(token, encodedData), LoadAnnotationXML(token, xml), y GetAnnotationXML(token).

Exportar con anotaciones incrustadas

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");
}

Las exportaciones usan el mismo quemador que el renderizado en pantalla, y la misma decisión de licencia y marca de agua personalizada.

Anotaciones de imagen en un host intranet exportan como "Imagen bloqueada"

El visor y la exportación obtienen la imagen de una ImageAnnotation desde diferentes lugares:

SuperficieQuién descarga la imagenEn un host intranet o de desarrollo
El visor en el navegadorEl navegador del usuarioLa imagen aparece
ExportAnnotationsToPdfAsync / …PngZipAsyncEl servidorUn marcador de posición rojo "Image blocked"

Cuando el servidor obtiene una imagen para incrustarla, rechaza direcciones que apuntan de nuevo a redes privadas: loopback (localhost, 127.0.0.1), 0.0.0.0, link-local (169.254.x.x), los rangos privados 10.x, 172.16–31.x y 192.168.x, NAT de nivel de operador (100.64.0.0/10) y direcciones IPv6 link-local y unique-local. Esto protege al servidor de ser usado para alcanzar sistemas internos mediante una URL de imagen manipulada, y no existe un interruptor para desactivarlo.

Una URL de imagen relativa se resuelve contra el host de la solicitud actual, por lo que en localhost, en un servidor intranet, o en cualquier sitio al que se acceda mediante una dirección privada, la imagen se muestra en el visor y la exportación contiene el marcador de posición en su lugar. Eso es la protección funcionando, no un error de renderizado. Si las exportaciones deben contener la imagen, sírvala desde un host con una dirección pública y use una URL absoluta. Una URL de imagen que no pueda resolverse en absoluto hace que esa anotación desaparezca sin error — revise los píxeles renderizados o el registro de red, no un código de estado.

Flujo de trabajo de persistencia

  1. Abra el documento y obtenga su token.
  2. Cargue XML o datos codificados previamente almacenados en ese token.
  3. Permita que el widget lea y edite las anotaciones de la sesión.
  4. Recupere XML con GetAnnotationXML(token) cuando su aplicación decida persistir.
  5. Exporte PDF o PNG cuando se requiera un entregable aplanado.
  6. Cierre la sesión del documento.

No utilice el token del visor como identificador permanente de anotaciones — desaparece con la sesión, y un reciclado del pool de aplicaciones finaliza cada sesión en un solo servidor. Almacene los datos de anotaciones asociados a sus propios identificadores de documento y versión.

Notas de seguridad y renderizado

  • Las solicitudes de anotaciones usan la misma seguridad de sesión y token que las solicitudes de página.
  • Valide y controle cualquier URL de imagen suministrada por el usuario.
  • Las cargas útiles grandes de dibujo libre y las exportaciones de alta resolución aumentan el uso de memoria; pruebe documentos realistas y valores de zoom.

Solución de problemas

SíntomaVerificación
Falta la cinta de anotacionesLa capacidad Annotation y los cuatro indicadores CSS/script de anotación
Al guardar se informa un errorExpiración del token o de la sesión, y el BasePath del widget
ctlDoc_Created (u otra función de evento) nunca se ejecutaEl nombre de la función: id del visor + _ + evento, como función global
Las anotaciones C# no aparecenLos números de página son basados en uno, y los datos se cargaron en el token activo
Una anotación de imagen se muestra en pantalla pero no en la exportaciónEl host de la imagen tiene una dirección privada o de loopback — vea arriba
Un documento reabierto no tiene anotacionesPersista XML o datos fuera de la sesión del visor, luego cárguelos en el nuevo token

¿Fue útil esta página?