Un revisor abre el mismo contrato en su visor Delphi y en Adobe Acrobat. El panel de comentarios de Acrobat lista catorce elementos; el panel de revisión de ustedes muestra once. No hay nada malo en su bucle. Los tres faltantes son dos respuestas — objetos de anotación completos enlazados a sus padres mediante referencias in-reply-to — y una ventana emergente que pertenece a una nota adhesiva que ustedes ya contaron una vez. Las anotaciones PDF no son una lista plana de rectángulos de colores: ISO 32000-1 §12.5 define una red de diccionarios con subtipos, banderas, appearance streams y relaciones padre-hijo, y un panel de revisión que ignore esas relaciones seguirá discrepando con todos los demás visores que tenga el cliente. Este recorrido construye un flujo de revisión de anotaciones sobre PDFium Component, el componente VCL/LCL basado en PDFium para Delphi, C++Builder y Lazarus, alrededor de los puntos donde los documentos revisados reales se resisten.
Por qué su conteo nunca coincide con el panel de comentarios de Acrobat
Acrobat presenta una vista curada: anotaciones de marcado agrupadas en hilos de respuestas, con ventanas emergentes plegadas dentro de sus padres. El arreglo bruto de anotaciones de cada página contiene a la vez más y menos de lo que sugiere esa vista.
- Las anotaciones popup son objetos separados unidos a una nota padre: contarlas duplica cada nota adhesiva.
- Las respuestas son anotaciones Text completas que referencian a un padre; filtrar solo marcas visibles elimina en silencio el hilo de discusión.
- Las banderas Hidden y NoView quitan una anotación de la visualización, no del arreglo, por lo que las revisiones de banderas pertenecen al pase de indexación.
- Las anotaciones Link viven en el mismo arreglo, y ningún revisor considera que un hipervínculo sea un comentario.
Fijen la regla de conteo antes de escribir código y pónganla en la especificación, porque "por qué su panel muestra un número distinto al de Acrobat" es el primer ticket de soporte que genera una función de revisión.
Indexen todo una vez y luego no vuelvan a analizar una página
Filtrar por autor, tipo o página no debe disparar un nuevo análisis de objetos de página: en un documento de 300 páginas con muchas marcas, eso convierte cada cambio de desplegable en segundos de latencia. El componente expone AnnotationCount y la propiedad indexada Annotation[] contra la página cargada actualmente, y el registro TPdfAnnotation devuelto trae todo lo que necesita una vista de lista: Subtype, Flags, Color, Rectangle, ContentsText y AuthorText. Construyan el índice en una sola pasada al abrir:
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 conviene subrayar es (PageNo, i). Cada llamada de mutación posterior — recolorear, eliminar — se direcciona por número de página más índice de anotación, y los índices se desplazan cuando se elimina una anotación. Planeen reconstruir las entradas de la página afectada después de cualquier eliminación en lugar de parchar índices en sitio; la reconstrucción cuesta milisegundos, mientras que un índice obsoleto borra el comentario del revisor equivocado.
El hilado de respuestas merece un espacio en el diseño del índice aunque la primera versión solo cuente respuestas en lugar de mostrarlas. Agrupen los elementos por su referencia padre durante la construcción para que el panel pueda más adelante plegar un hilo como lo hace Acrobat; reconstruir la agrupación de manera perezosa durante el desplazamiento vuelve a abrir páginas que ya pagaron por analizar. La misma idea de una sola pasada aplica a la geometría: el Rectangle de cada registro está expresado en espacio de página, así que conviértanlo a coordenadas de vista en exactamente un helper compartido. Los paneles de revisión acumulan errores de coordenadas cuando selección, hit-testing y pintado cargan cada uno su propia aritmética de zoom y rotación; una ruta única de conversión mantiene el resaltado, su entrada de lista y su objetivo de clic apuntando a la misma tinta.
Recolorear marcas y el veto del appearance stream
Cambiar un resaltado de amarillo a ámbar suena como una línea, y a veces lo es. La complicación está en ISO 32000-1 §12.5.5: cuando una anotación lleva un appearance stream /AP, los visores conformes renderizan ese flujo preconstruido, y la entrada de color del diccionario de la anotación se vuelve decorativa. Como Acrobat escribe appearance streams para prácticamente todo lo que crea, la mayoría de las anotaciones que llegan de clientes están exactamente en ese estado. Recolorear es una lectura-modificación-escritura mediante la propiedad Annotation[], y el componente informa un veto del motor de forma honesta lanzando 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;
Manejen esa excepción siempre. Saltarse la guarda significa que su panel muestra el color nuevo en su propia lista mientras la página sigue pintando el anterior, y la discrepancia aparece semanas después como un reporte de "su visor ignora mis ediciones". Cuando la apariencia está bloqueada, las opciones honestas son recolorear su superposición de selección en lugar de la anotación, o marcar el elemento como bloqueado por apariencia en la UI.
Eliminar anotaciones sin dejar restos
DeleteAnnotation separa el objeto de la página actual, pero no reconstruye la apariencia de página en caché: si pintan inmediatamente después de la llamada, el resaltado eliminado todavía se ve. Vuelvan a renderizar el raster de la página como parte de la misma operación:
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
Noten la opción reAnnotations en la llamada de renderizado: sin ella, el raster excluye todas las anotaciones restantes, lo que para el usuario parece una eliminación masiva. Y noten Bmp.Free: la sobrecarga estilo función de RenderPage transfiere la propiedad del bitmap al llamador, así que omitir el free filtra un raster de página completo en cada eliminación.
Agregar marcas de revisión desde su propia UI
Crear anotaciones pasa por CreateAnnotation, que recibe un registro TPdfAnnotation lleno: subtipo, rectángulo, color, contenido, autor, y lo agrega a la página actual. Las notas adhesivas (anText) son el caso simple: posición, contenido, autor, listo. Las anotaciones de tinta son la trampa: el rectángulo del registro solo delimita el dibujo, y los trazos reales son arreglos de puntos que deben adjuntarse por separado mediante la llamada de tinta del motor (FPDFAnnot_AddInkStroke con datos FS_POINTF), capturados desde mouse o lápiz trazo por trazo. Crear una anotación de tinta solo a partir de un rectángulo produce un garabato vacío que no renderiza nada.
Decidan la política de autoría al mismo tiempo: cada marca que cree su UI debe llevar un AuthorText consistente, porque el filtrado posterior por revisor es tan confiable como los nombres que escriban hoy.
Sacar la revisión del visor
Los datos de revisión ganan valor cuando salen del visor: un resumen que el responsable del proyecto lee sin abrir el archivo, o un CSV que alimenta una hoja de seguimiento. Exporten desde el índice, no desde un nuevo análisis, y mantengan referencias estables: número de página más rectángulo de anotación sobrevive mejor a los viajes de ida y vuelta que un índice de arreglo que invalida la siguiente eliminación.
Una fila de exportación defendible lleva página, subtipo, autor, información de creación cuando exista, texto de contenido y su propia columna de estado. Para documentos que llegan desde fuera del equipo, conviene ejecutar el mismo pase de indexación durante la clasificación de entrada; el artículo sobre workbench de intake PDF muestra ese patrón, y la navegación de campos de formulario cubre el problema compañero de revisar documentos que capturan datos en lugar de comentarios.
Preguntas frecuentes
¿Por qué un resaltado que recoloreé todavía muestra su color anterior?
Casi con seguridad la anotación lleva un appearance stream /AP, que los visores conformes pintan con preferencia sobre el color del diccionario (ISO 32000-1 §12.5.5). Escribir el registro de vuelta mediante Annotation[] lanza EPdfError en ese caso: traten la excepción como la fuente de verdad, no el color que querían establecer.
¿Por qué la página todavía muestra una anotación que eliminé?
DeleteAnnotation actualiza el modelo del documento, no el raster en caché. Vuelvan a renderizar la página con RenderPage después de una eliminación exitosa, y reconstruyan las entradas de índice de esa página porque los índices de anotación se desplazan hacia abajo después del espacio eliminado.
¿Las anotaciones aplanadas aparecen en el arreglo de anotaciones?
No. El aplanado convierte las apariencias de anotación en contenido ordinario de página, por lo que dejan de ser objetos de anotación. Si un archivo de cliente muestra marcas visibles pero AnnotationCount es cero, la explicación habitual es un aplanado previo: ya no queda nada que revisar mediante programación.
La superficie de API de anotaciones usada en este artículo — enumeración, creación, recoloreado, eliminación y opciones de renderizado que mantienen honesta la visualización — se incluye con PDFium Component para Delphi, C++Builder y Lazarus/FPC.