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. ISO 32000-1 §12.5 define aproximadamente dos docenas de subtipos, y cada uno lleva un /Subtype, un rectángulo en coordenadas de página, un conjunto de banderas, y normalmente un flujo de apariencia que decide lo que un visor realmente pinta. Los subtipos no significan todos lo mismo para una persona que revisa un documento. Un Highlight y un trazo de Ink son comentarios; un Link es navegación; un Popup es la pequeña ventana que se abre cuando hace clic en una nota adhesiva, almacenada como su propio objeto y señalada por un padre. Las respuestas son anotaciones Text completas que referencian el comentario al que responden a través de una entrada en-respuesta-a. Así que el arreglo de anotaciones a nivel de página no es la lista de comentarios del revisor. Es un saco plano que contiene comentarios, la fontanería que los conecta, y varias cosas que ningún revisor llamaría comentario. Un panel que trate el arreglo como la lista de comentarios no estará de acuerdo 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 ese hueco entre el arreglo crudo y la vista humana causa problemas: contar, indexar, recolorear marcas que el motor ya ha congelado, borrar sin dejar fantasmas, y añadir marcas propias

Diagrama que muestra cómo un panel de revisión Delphi de PDFium filtra el array en bruto de anotaciones de página de comentarios, popups, respuestas y enlaces hacia la lista curada de comentarios que ve el revisor
El array de anotaciones de página mezcla comentarios con popups, respuestas, enlaces y marcas ocultas, de modo que un panel de revisión necesita una regla de recuento antes de mostrar un total

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

Abra un contrato marcado en su visor y en Acrobat lado a lado y los totales rara vez coinciden. Acrobat muestra una vista curada: el marcado agrupado en hilos de respuesta, los popups plegados en las notas a las que pertenecen, los enlaces y los widgets de formulario dejados fuera. El arreglo crudo contiene todo ello indiferenciado, así que un recuento naïve corre alto en algunos sentidos y bajo en otros al mismo tiempo

Los Popups inflan el total, porque cada nota adhesiva se envía 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 Text sin nada pintado hasta que alguien expande el hilo, y descartarla pierde la discusión. Las banderas Hidden y NoView sacan una anotación de la pantalla sin sacarla del arreglo, así que un recuento ciego a banderas incluye marcas que el usuario no puede ver. Las anotaciones Link se sientan 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 escriba la decisión, porque "por qué su panel muestra un número distinto al de Acrobat" es el primer ticket que una característica de revisión se gana

Indexe todo una vez, luego nunca re-analice una página

Una regla de diseño dirige todo lo que sigue: filtrar por autor, tipo o página nunca debe re-analizar objetos de página. En un documento de 300 páginas con marcado denso, re-analizar en cada cambio de menú desplegable convierte el panel en algo que tartamudea durante segundos. El componente expone AnnotationCount y la propiedad indexada Annotation[], ambas con alcance a la página cargada actualmente, y el registro TPdfAnnotation que devuelven lleva lo que una vista de lista necesita: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. El movimiento correcto es barrer cada página una vez al abrir 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];
      // Quédate solo con los subtipos que interesan al revisor; guarda el par página e
      // índice, porque todas las ediciones posteriores se direccionan con él
      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 recoloreo o un borrado, 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 le sigue en esa página. Así que planee reconstruir las entradas de la página afectada tras cualquier borrado en lugar de parchear números de índice en el sitio. La reconstrucción cuesta un milisegundo. Un índice obsoleto, por el contrario, borra el comentario del revisor equivocado, que es el tipo de error que erosiona la confianza en toda la característica

El hilado merece un sitio en el índice incluso si su primer lanzamiento solo cuenta respuestas en lugar de mostrarlas. Agrupe los elementos por su referencia padre mientras tiene la página abierta, de modo que el panel pueda luego plegar un hilo como hace Acrobat. Reconstruir esa agrupación perezosamente durante el desplazamiento derrota todo el punto de indexar una vez, porque reabre páginas que ya pagó por analizar. La geometría quiere la misma disciplina. El Rectangle en cada registro es espacio de página, y convertirlo a coordenadas de vista pertenece a un único helper compartido, no esparcido por el código. Los paneles desarrollan errores de coordenadas cuando la selección, la comprobación de impacto y el pintado inventan cada uno su propia matemática de zoom y rotación; enrute los tres a través de una única conversión y un resaltado, su fila en la lista, y su objetivo de clic permanecen fijados a la misma tinta

Recolorear marcado y el veto del flujo de apariencia

Cambiar un resaltado de amarillo a ámbar suena a una sola línea, y a veces lo es. La trampa es ISO 32000-1 §12.5.5. Cuando una anotación lleva 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, así que la mayoría de las anotaciones que llegan de los clientes ya están en este estado, y el color que fijó con tanta confianza nunca llega a la pantalla. Recolorear es una 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 dejar que un color del diccionario reemplace una apariencia preconstruida, la escritura levanta EPdfError

Diagrama de la ruta de recoloreado lectura-modificación-escritura en un componente Delphi de PDFium donde un stream de apariencia ya horneado veta el color del diccionario y lanza EPdfError
Cuando una anotación lleva un flujo /AP preconstruido, el motor rechaza el color del diccionario y lanza EPdfError, de modo que el panel recolorea su propia superposición o marca la fila como bloqueada en apariencia
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
    // La anotación tiene su propio stream /AP prerrenderizado; el color del
    // diccionario por sí solo no cambia lo que pintan los visores
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

Capture esa excepción cada vez, y trátela como información en lugar de fallo. Sáltese la guarda y su panel muestra 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 resulta no tener flujo de apariencia. Una vez que sabe que la apariencia está bloqueada, tiene dos respuestas honestas: recolorear su propio overlay de selección en lugar de la anotación, de modo que el revisor al menos vea el resaltado que eligió, o marcar la fila como bloqueada-por-apariencia para que nadie espere que el cambio persista

Borrar anotaciones sin dejar fantasmas

DeleteAnnotation elimina el objeto del árbol de anotaciones de la página actual, pero deja solo el ráster de página cacheado. Pinte inmediatamente después de la llamada y el resaltado borrado sigue en pantalla, sentado en un mapa de bits que ya no coincide con el modelo de documento detrás de él. La solución es tratar el re-render como parte del borrado, no un paso que el llamador podría olvidar:

Diagrama del ciclo de borrado de tres pasos en Delphi PDFium que elimina la anotación, re-renderiza la página con reAnnotations y reconstruye el índice de página
Eliminar solo toca el árbol de anotaciones, de modo que el panel debe re-renderizar con reAnnotations y reconstruir las entradas de página antes de que la pantalla y el índice vuelvan a decir la verdad
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // lanza EPdfError si falla
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // RenderPage cede la propiedad del bitmap al llamante
end;
RebuildPageEntries(Item.PageNo);  // los índices posteriores a Item.Index se han desplazado

Dos detalles en ese bloque son fáciles de equivocar. La opción reAnnotations tiene que estar presente, o el nuevo ráster suelta cada anotación restante y la página parece que borró todo el conjunto de comentarios en lugar de una marca. Y el Bmp.Free no es opcional: la sobrecarga funcional de RenderPage entrega la propiedad del mapa de bits al llamador, así que un free ausente fuguea un ráster de página completa en cada borrado, lo que un revisor trabajando en un documento largo convertirá en presión de memoria real en minutos

Añadir marcas de revisor desde su propia interfaz

Crear anotaciones pasa por CreateAnnotation, que toma un registro TPdfAnnotation rellenado (subtipo, rectángulo, color, contenidos, autor) y lo adjunta a la página actual. Una nota adhesiva, subtipo anText, es el caso fácil: fije la posición, los contenidos y el autor y ha terminado. Las anotaciones Ink son donde la gente se queda atrapada. El rectángulo del registro solo limita el dibujo; los trazos mismos son arreglos de puntos que hay que adjuntar separadamente a través de la llamada de trazo-ink del motor, FPDFAnnot_AddInkStroke alimentada con datos FS_POINTF, capturados desde entrada de ratón o pluma un trazo a la vez. Construya una anotación ink a partir de un rectángulo y nada más y obtiene un garabato vacío que se renderiza como espacio en blanco, lo que parece un error en el motor y es realmente una anotación a medio terminar

Decida la política de autoría en el mismo aliento. Cada marca que su interfaz cree debería llevar un AuthorText consistente, porque el filtro de revisor que construya el mes que viene solo es tan bueno como los nombres que estampe en los comentarios hoy. Las cadenas de autor vacías o inconsistentes no se pueden reparar retroactivamente sin reabrir cada archivo

Sacar la revisión del visor

Los datos de revisión se ganan su sitio una vez 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 fresco, y elija una forma estable de referenciar 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 el próximo borrado renumera silenciosamente los índices y su CSV empieza 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 contenidos, y una columna de estado que usted posee en lugar de una que el PDF suministra. El mismo pase de indexación es útil antes, durante el intake, cuando un documento llega de fuera del equipo y quiere saber qué hay en él antes de que nadie lo revise. El artículo del workbench de intake PDF recorre ese triage, y navegación de campos de formulario cubre el problema imagen-espejo: revisar documentos construidos para recolectar datos en lugar de comentarios

Un caso que el arreglo no le mostrará

Un modo de fallo merece una bandera porque parece un defecto en su código y no lo es. Un cliente reporta resaltados visibles por toda una página, pero su panel no lista nada, y AnnotationCount vuelve cero. La explicación habitual es que las marcas se aplanaron en algún punto aguas arriba. Aplanar cuece las apariencias de anotación en contenido de página ordinario, así que los resaltados se vuelven parte de los gráficos de página y dejan de existir como objetos de anotación por completo. No queda nada para que una API de anotaciones lo enumere, recoloree o borre. Cuando vea marcado pintado con un recuento cero, deje de buscar el error en su bucle de enumeración y pregunte cómo se produjo el archivo

La superficie de anotaciones usada aquí, desde enumeración y creación hasta recoloreo, borrado y las opciones de render que mantienen honesta la pantalla, se envía con PDFium Component para Delphi, C++Builder y Lazarus/FPC