Artículo técnico

Revisión de Anotaciones PDF en Delphi con PDFium Component

Una anotación PDF es un diccionario adjunto a una página, no una marca dibujada en ella. El estándar ISO 32000-1 §12.5 define aproximadamente dos docenas de subtipos, y cada uno tiene un /Subtype, un rectángulo en coordenadas de página, un conjunto de indicadores y, por lo general, un flujo de apariencia que decide qué dibuja realmente un visor. Los subtipos no significan todos lo mismo para una persona que revisa un documento. Un resaltado y un trazo de tinta son comentarios; un enlace es navegación; un mensaje emergente (Popup) es la pequeña ventana que se abre al hacer clic en una nota adhesiva, almacenada como su propio objeto y apuntada por un padre. Las respuestas son anotaciones de Texto completas que hacen referencia al comentario que responden a través de una entrada in-reply-to. Por lo tanto, el arreglo de anotaciones a nivel de página no es la lista de comentarios del revisor. Es una bolsa plana que contiene comentarios, la infraestructura que los conecta y varias cosas que ningún revisor llamaría un comentario en absoluto. Un panel que trata el arreglo como la lista de comentarios estará en desacuerdo con cualquier otro visor que el cliente ejecute

Construir un flujo de trabajo de revisión de anotaciones sobre PDFium Component, el componente VCL/LCL basado en PDFium para Delphi, C++Builder y Lazarus, significa concentrarse en los puntos donde esa brecha entre el arreglo sin procesar y la vista humana causa problemas: contar, indexar, cambiar el color de marcas que el motor ya ha congelado, eliminar sin dejar rastros y agregar sus propias marcas

Por qué su recuento nunca coincide con el panel de comentarios de Acrobat

Abra un contrato marcado en su visor y en Acrobat de lado a lado y los totales rara vez concuerdan. Acrobat muestra una vista curada: el marcado agrupado en hilos de respuesta, los mensajes emergentes (Popups) plegados en las notas a las que pertenecen, los enlaces y los widgets de formulario omitidos. El arreglo sin procesar contiene todo indiferenciado, por lo que un recuento ingenuo es alto en algunos aspectos y bajo en otros al mismo tiempo

Los mensajes emergentes (Popups) inflan el total, porque cada nota adhesiva viene con un objeto Popup separado y contar ambos duplica la nota. Las respuestas lo desinflan si filtra por marcas visibles, ya que una respuesta es una anotación de Texto sin nada pintado hasta que alguien expande el hilo, y omitirla pierde la discusión. Los indicadores Hidden y NoView sacan una anotación de la pantalla sin sacarla del arreglo, por lo que un recuento ciego a los indicadores incluye marcas que el usuario no puede ver. Las anotaciones de Enlace se encuentran en el mismo arreglo que los comentarios y no pertenecen ni al recuento ni a la lista. Decida la regla de conteo antes de escribir el bucle, y anote la decisión, porque "por qué su panel muestra un número diferente que Acrobat" es el primer ticket que gana una función de revisión

Indexe todo una vez, luego nunca vuelva a analizar una página

Una regla de diseño impulsa todo lo que sigue: filtrar por autor, tipo o página nunca debe volver a analizar los objetos de la página. En un documento de 300 páginas con un marcado denso, volver a analizar en cada cambio de menú desplegable convierte el panel en algo que tartamudea durante segundos a la vez. El componente expone AnnotationCount y la propiedad indexada Annotation[], ambos en el ámbito de la página actualmente cargada, y el registro TPdfAnnotation que devuelven contiene lo que necesita una vista de lista: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. El movimiento correcto es barrer cada página una vez al momento de la apertura y mantener su propio índice plano:

procedure TReviewPanel.BuildIndex;
var
  PageNo, i: Integer;
  A: TPdfAnnotation;
begin
  FItems.Clear;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    for i := 0 to Pdf.AnnotationCount - 1 do
    begin
      A := Pdf.Annotation[i];
      // Keep reviewer-relevant subtypes only; record the page and
      // index pair because all later edits are addressed by it
      if A.Subtype in [anText, anHighlight, anInk] then
        FItems.Add(TReviewItem.Create(PageNo, i,
          A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
    end;
  end;
end;

El par que vale la pena subrayar es (PageNo, i). Toda mutación posterior, ya sea un cambio de color o una eliminación, se direcciona por número de página más índice de anotación, y el índice es frágil: eliminar una anotación renumera todo lo que viene después de ella en esa página. Así que planee reconstruir las entradas de la página afectada después de cualquier eliminación en lugar de parchar los números de índice en su lugar. La reconstrucción cuesta un milisegundo. Un índice obsoleto, por el contrario, elimina el comentario del revisor equivocado, que es el tipo de error que erosiona la confianza en toda la función

El enhebrado merece un lugar en el índice, incluso si su primera versión solo cuenta las respuestas en lugar de mostrarlas. Agrupe los elementos por su referencia principal mientras tiene la página abierta, para que el panel pueda plegar un hilo de la manera en que lo hace Acrobat. Reconstruir esa agrupación de forma diferida durante el desplazamiento frustra todo el punto de indexar una vez, porque vuelve a abrir páginas que ya pagó para analizar. La geometría quiere la misma disciplina. El Rectangle en cada registro está en el espacio de la página, y convertirlo a coordenadas de visualización pertenece a un único asistente compartido, no disperso por el código. Los paneles desarrollan errores de coordenadas cuando la selección, las pruebas de acierto y la pintura inventan cada uno su propia matemática de zoom y rotación; enrute los tres a través de una sola conversión y un resaltado, su fila en la lista y su objetivo de clic permanecerán fijados a la misma tinta

Cambiar el color del marcado y el veto del flujo de apariencia

Cambiar un resaltado de amarillo a ámbar suena como una sola línea, y a veces lo es. El truco es la norma ISO 32000-1 §12.5.5. Cuando una anotación tiene un flujo de apariencia /AP, un visor conforme pinta ese flujo preconstruido y trata la entrada de color en el diccionario como metadatos muertos. Acrobat escribe flujos de apariencia para esencialmente todo lo que crea, por lo que la mayoría de las anotaciones que llegan de los clientes ya están en este estado, y el color que tan confiadamente estableció nunca llega a la pantalla. Cambiar el color es una operación de lectura-modificación-escritura a través de la propiedad Annotation[], y el componente es honesto sobre el conflicto: cuando el motor se niega a permitir que el color de un diccionario anule una apariencia integrada, la escritura genera EPdfError

A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF;       // amber
A.ColorAlpha := 160;
try
  Pdf.Annotation[Item.Index] := A;
except
  on EPdfError do
  begin
    // The annotation owns a pre-rendered /AP stream; the dictionary
    // color alone cannot change what viewers paint
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

Atrape esa excepción cada vez y trátela como información en lugar de fracaso. Omita la protección y su panel mostrará alegremente ámbar en su propia lista mientras la página sigue pintando amarillo; el usuario lo reporta semanas después como "su visor ignora mis ediciones", y usted pasa una tarde sin poder reproducirlo en un archivo que casualmente no tiene flujo de apariencia. Una vez que sepa que la apariencia está bloqueada, tiene dos respuestas honestas: vuelva a colorear su propia superposición de selección en lugar de la anotación, para que el revisor al menos vea el resaltado que eligió, o marque la fila como de apariencia bloqueada para que nadie espere que el cambio se mantenga

Eliminación de anotaciones sin dejar rastros

DeleteAnnotation elimina el objeto del árbol de anotaciones de la página actual, pero deja en paz el ráster de la página en caché. Pinte inmediatamente después de la llamada y el resaltado eliminado seguirá en la pantalla, asentado en un mapa de bits que ya no coincide con el modelo de documento que tiene detrás. La solución es tratar el nuevo renderizado como parte de la eliminación, no como un paso que el autor de la llamada podría olvidar:

Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // raises EPdfError on failure
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // RenderPage hands bitmap ownership to the caller
end;
RebuildPageEntries(Item.PageNo);  // indices after Item.Index shifted

Dos detalles en ese bloque son fáciles de hacer mal. La opción reAnnotations tiene que estar presente, o el nuevo ráster descarta todas las anotaciones restantes y la página se ve como si hubiera borrado todo el conjunto de comentarios en lugar de una marca. Y el Bmp.Free no es opcional: la sobrecarga de estilo de función de RenderPage entrega la propiedad del mapa de bits al autor de la llamada, por lo que un liberado (free) omitido filtra un ráster de página completa en cada eliminación, lo que un revisor que trabaje a través de un documento largo convertirá en presión de memoria real en minutos

Agregar marcas del revisor desde su propia interfaz de usuario

La creación de anotaciones se realiza a través de CreateAnnotation, que toma un registro TPdfAnnotation rellenado (subtipo, rectángulo, color, contenido, autor) y lo adjunta a la página actual. Una nota adhesiva, del subtipo anText, es el caso fácil: establezca la posición, el contenido y el autor y habrá terminado. Las anotaciones de tinta son donde las personas quedan atrapadas. El rectángulo del registro solo limita el dibujo; los trazos en sí son arreglos de puntos que deben adjuntarse por separado a través de la llamada de trazo de tinta del motor, FPDFAnnot_AddInkStroke alimentada con datos FS_POINTF, capturados de la entrada del mouse o del lápiz un trazo a la vez. Cree una anotación de tinta a partir de un rectángulo y nada más y obtendrá un garabato vacío que se representa como un espacio en blanco, lo que parece un error en el motor y es en realidad una anotación a medio terminar

Resuelva la política de autoría en el mismo momento. Cada marca que cree su interfaz de usuario debe tener un AuthorText constante, porque el filtro de revisor que construya el próximo mes solo será tan bueno como los nombres que estampe en los comentarios hoy. Las cadenas de autor en blanco o inconsistentes no se pueden reparar retroactivamente sin volver a abrir todos los archivos

Sacar la revisión del visor

Los datos de revisión ganan su sustento una vez que pueden salir del visor, como un resumen que el líder del proyecto lee sin abrir el archivo o un CSV que alimenta una hoja de seguimiento. Exporte desde el índice que ya construyó, nunca desde un análisis nuevo, y elija una forma estable de referirse a cada marca. Un número de página emparejado con el rectángulo de la anotación sobrevive a viajes de ida y vuelta que un índice de arreglo no, porque la siguiente eliminación renumera silenciosamente los índices y su CSV comienza a apuntar a los comentarios equivocados

Una fila que vale la pena conservar lleva la página, el subtipo, el autor, la marca de tiempo de creación cuando el archivo registra una, el texto de contenido y una columna de estado que usted posee en lugar de una que proporciona el PDF. La misma pasada de indexación es útil antes, durante la recepción, cuando un documento llega de fuera del equipo y desea saber qué hay en él antes de que alguien lo revise. El artículo sobre el banco de trabajo de recepción de PDF analiza ese triaje, y navegación de campos de formularios cubre el problema inverso: revisar documentos creados para recopilar datos en lugar de comentarios

Un caso que el arreglo no le mostrará

Un modo de falla merece un indicador porque parece un defecto en su código y no lo es. Un cliente reporta resaltados visibles en toda una página, pero su panel no enumera nada, y AnnotationCount devuelve cero. La explicación habitual es que las marcas se acoplaron (flattened) en algún lugar del proceso anterior. El acoplamiento integra la apariencia de las anotaciones en el contenido normal de la página, por lo que los resaltados se convierten en parte de los gráficos de la página y dejan de existir como objetos de anotación por completo. No queda nada para que una API de anotación enumere, vuelva a colorear o elimine. Cuando vea un marcado pintado con un recuento de cero, deje de buscar el error en su bucle de enumeración y pregunte cómo se produjo el archivo

La superficie de anotación utilizada aquí, desde la enumeración y creación hasta el cambio de color, eliminación y las opciones de renderizado que mantienen la pantalla honesta, se envía con PDFium Component para Delphi, C++Builder y Lazarus/FPC