Un rectángulo dibujado alrededor de un párrafo durante la revisión no tiene por qué convertirse en una marca dentro del PDF. El THPDFViewerModel de HotPDF expone AddHighlightRegion, un método que mantiene cada resaltado como un registro en memoria en lugar de un cambio al documento cargado, así que un revisor puede marcar docenas de páginas mientras el archivo en disco permanece byte por byte igual a como estaba. Haga zoom al 6400%, rote la página 90 grados, cambie de Ajustar al ancho a Ajustar a la página, y el mismo rectángulo sigue cayendo sobre el mismo párrafo, porque el cálculo de coordenadas pasa por la geometría de renderizado real en el momento en que se dibujó la marca
Las herramientas de revisión construidas alrededor de un visor de PDF se topan con este problema constantemente. Una pantalla de marcado, una pasada de control de calidad sobre facturas generadas, un flujo de aprobación interno: todas necesitan permitir que alguien llame la atención sobre una región de una página sin que cada marca de borrador se convierta en un cambio permanente al archivo, y sin recurrir a un subsistema de anotaciones completo solo para mostrar un recuadro de color mientras alguien todavía está decidiendo si la marca es válida. HotPDF responde a esto con una capa de resaltado dedicada que se ubica enteramente en el lado del Modelo de la división descrita en cómo construir un visor de PDF personalizado con una arquitectura MVC en Delphi, que es también la razón por la que la misma lista de resaltados puede controlarse desde una prueba unitaria sin ningún handle de ventana a la vista
¿Qué almacena realmente AddHighlightRegion de HotPDF?
AddHighlightRegion almacena exactamente tres cosas por marca: un índice de página de base cero, un THPDFRectangle en coordenadas de espacio de usuario de PDF, y un TColor, todo empaquetado como un registro THPDFViewerHighlight dentro de THPDFViewerModel. Llamar a Viewer.HighlightRegion(PageIndex, PageRect, clYellow), o su equivalente Model.AddHighlightRegion, agrega uno de estos registros a un arreglo privado y devuelve su índice, y ese índice es el único identificador que recibe quien llama: no hay un objeto separado, ninguna interfaz con conteo de referencias, nada que liberar. Cualquier otra capacidad de este artículo, dibujar la marca, remapearla tras un cambio de zoom, eliminarla, está construida sobre ese único registro pequeño
Cada rectángulo se normaliza y recorta antes de aceptarse. AddHighlightRegion intercambia los bordes izquierdo y derecho si un revisor arrastra de derecha a izquierda, intercambia arriba y abajo para un arrastre hacia arriba, y luego recorta el resultado contra el MediaBox de la página obtenido mediante GetLoadedPageBox. Un rectángulo que termina con ancho cero, alto cero, o completamente fuera de la página se rechaza directamente: el método devuelve -1 y no se agrega nada a la lista. Ese valor de retorno no es decorativo: un lote de resaltados reconstruido a partir de un archivo de revisión externo, o de coordenadas obsoletas tras reemplazar una página, puede perder entradas silenciosamente si quien llama no lo comprueba
¿Cómo permanece alineado un resaltado tras un zoom o una rotación?
Un resaltado permanece alineado porque HotPDF lo almacena en el espacio de página de PDF y lo reproyecta al espacio de pantalla en cada repintado, en lugar de almacenar un rectángulo de pantalla que quedaría obsoleto en cuanto cambie el nivel de zoom. THPDFViewerModel.PagePointToView y su inversa, ViewPointToPage, hacen esa proyección en dos etapas: primero la entrada /Rotate propia de la página, luego la ViewRotation independiente del Visor, que nunca se escribe de vuelta al PDF y solo afecta lo que muestra el Visor. Deshacer la transformación al soltar el mouse ejecuta las mismas dos etapas en orden inverso, que es lo que permite que un resaltado dibujado con zoom alto sobre una página rotada 270 grados caiga exactamente en el lugar correcto después de que el revisor restablezca la vista a Ajustar a la página
El DPI usado para esa proyección importa tanto como la rotación. El Visor de HotPDF captura el DPI exacto del mapa de bits que está actualmente en pantalla en FRenderedDPI justo después de cada renderizado, y ImageMouseUp pasa ese mismo valor a ViewPointToPage para que una coordenada de mouse siempre se convierta usando la resolución con la que realmente se dibujó, no una resolución recalculada a partir de la propiedad de zoom actual. CreatePageSnapshot y sus relacionados limitan el DPI a un rango de 12 a 2400, pero la ruta de renderizado interactiva no lleva ese tope: la escala de zoom estándar llega hasta el 6400%, lo que equivale a bastante más de 2400 DPI con la línea base predeterminada de 96 DPI, así que reutilizar un límite de tipo instantánea para el mapeo de coordenadas desplazaría cada resaltado varios píxeles en la parte alta del rango de zoom. Dos valores predeterminados más pequeños completan la interacción: un arrastre más corto que dos píxeles en cualquier eje se trata como un clic y no produce ningún resaltado, y el resaltado no puede comenzar hasta que al menos una página se haya renderizado realmente, ya que FRenderedDPI empieza en cero
Conectar el resaltado interactivo a una pantalla de revisión
Activar el resaltado interactivo es un trabajo de tres propiedades en el propio control THPDFViewer: establecer InteractionMode en vimHighlight en lugar del vimBrowse predeterminado, elegir un HighlightColor, que por defecto es clYellow, y manejar OnMarqueeSelect para saber qué acaba de dibujar el revisor. Todo lo demás, capturar el mouse, dibujar el rectángulo de selección punteado mientras el revisor arrastra, convertir el punto de liberación de vuelta al espacio de página, llamar a AddHighlightRegion, ocurre dentro del control antes de que se dispare ese evento
type
TReviewForm = class(TForm)
Viewer: THPDFViewer;
ReviewLog: TMemo;
procedure FormCreate(Sender: TObject);
private
procedure ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle;
HighlightIndex: Integer);
end;
// PdfDoc is a THotPDF already loaded elsewhere on the form
procedure TReviewForm.FormCreate(Sender: TObject);
begin
Viewer.PDFDocument := PdfDoc;
Viewer.InteractionMode := vimHighlight;
Viewer.HighlightColor := clLime;
Viewer.OnMarqueeSelect := ViewerMarqueeSelect;
end;
procedure TReviewForm.ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle; HighlightIndex: Integer);
begin
ReviewLog.Lines.Add(Format('page %d, mark #%d at (%.1f, %.1f)-(%.1f, %.1f)',
[PageIndex + 1, HighlightIndex, PageRect.Left, PageRect.Bottom,
PageRect.Right, PageRect.Top]));
end;
OnMarqueeSelect solo se dispara para un arrastre que realmente produjo un resaltado: un clic demasiado pequeño para contar como arrastre despeja de inmediato la superposición de selección, y un arrastre que cae completamente fuera de la página llega a AddHighlightRegion pero se rechaza ahí de la misma manera que lo haría una llamada programática, así que el evento permanece en silencio en cualquiera de los dos casos. Un detalle de implementación que vale la pena conocer si el resaltado alguna vez parece dejar de responder en los bordes del control: la captura del mouse pertenece al propio THPDFViewer, un descendiente de TScrollBox, no al TImage interno que muestra el mapa de bits de la página, que es lo que permite que un revisor arrastre más allá del borde de la página renderizada y aun así obtenga una liberación limpia
Agregar, eliminar y releer resaltados desde código
Los resaltados no tienen por qué provenir de un arrastre de mouse en absoluto. Viewer.HighlightRegion(PageIndex, PageRect, Color), que se canaliza hacia el mismo Model.AddHighlightRegion que llama internamente el arrastre interactivo, es público específicamente para que una pantalla de revisión pueda reconstruir resaltados a partir de datos que ya tiene: comentarios cargados desde una base de datos, resultados de una búsqueda de texto, o marcas restauradas de una sesión anterior. Como las coordenadas son simples números de espacio de usuario de PDF, nada en esta ruta depende de que una página se haya renderizado primero, a diferencia del arrastre interactivo, que necesita que FRenderedDPI ya tenga un valor real
var
I: Integer;
Item: TPriorComment; // your own record: PageIndex + PageRect
NewIndex: Integer;
begin
for I := 0 to PriorComments.Count - 1 do
begin
Item := TPriorComment(PriorComments[I]);
NewIndex := Viewer.HighlightRegion(Item.PageIndex, Item.PageRect, clAqua);
if NewIndex < 0 then
LogWarning('comment %d fell outside the page and was dropped', [I]);
end;
end;
Eliminar un solo resaltado es donde se nota el almacenamiento respaldado por un arreglo. RemoveHighlightRegion elimina un registro y desplaza cada registro posterior una posición hacia abajo para cerrar el hueco, lo que significa que cualquier índice capturado antes, de un evento OnMarqueeSelect o de una enumeración previa, deja de ser confiable en cuanto se elimina algo anterior a él en la lista. OnHighlightChange se dispara en cada adición, eliminación, y llamada a ClearHighlightRegions, pero no lleva información sobre qué cambió, así que el patrón seguro es tratarlo como una señal para reconstruir cualquier lista que un panel de revisión esté mostrando a partir de HighlightCount y TryGetHighlightRegion, en lugar de parchear un índice en caché en el lugar
procedure TReviewForm.ViewerHighlightChange(Sender: TObject);
var
I: Integer;
Mark: THPDFViewerHighlight;
begin
MarkList.Items.Clear;
for I := 0 to Viewer.Model.HighlightCount - 1 do
if Viewer.Model.TryGetHighlightRegion(I, Mark) then
MarkList.Items.AddObject(Format('page %d', [Mark.PageIndex + 1]),
TObject(I));
end;
¿Cuándo debería una marca convertirse en una anotación de resaltado real?
Una región de resaltado debería convertirse en una anotación real en el momento en que necesite sobrevivir fuera de esa única instancia de THPDFViewer. HotPDF también expone AddHighlightAnnotation para una página nueva y AddLoadedHighlightAnnotation para un documento ya cargado, y a pesar del nombre casi idéntico, este es un mecanismo completamente distinto: ambos escriben una anotación de marcado de texto real según ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, dentro del arreglo /Annots de la página, con /QuadPoints marcando el trazo exacto de los glifos, y cualquier visor de PDF conforme la renderiza una vez guardado el archivo, no solo el propio HotPDF. El mismo límite de mecanismo decide si una marca sobrevive el viaje de ida y vuelta por XFDF: una anotación creada con AddLoadedHighlightAnnotation es un objeto PDF normal que ExportLoadedAnnotationsToXFDF recoge y entrega a Acrobat u otra herramienta de revisión como marcado ISO 19444-1, cubierto en importar y exportar anotaciones de PDF como XFDF en Delphi, mientras que una región agregada mediante AddHighlightRegion es invisible para esa exportación porque nunca se escribió en el grafo de objetos: existe solo mientras exista el THPDFViewerModel que la creó. La familia completa de tipos de anotación de marcado y geométricos disponibles en una página, y cómo un rectángulo ubica cada uno, se cubre en el artículo sobre anotaciones de PDF en Delphi con HotPDF, y la regla práctica es simple: mantenga una marca desechable mientras un documento todavía se está discutiendo, y conviértala en anotación una vez que la decisión sea definitiva
Dónde se detiene la capa de resaltado
La capa de resaltado, por su parte, no intenta parecerse a un marcador translúcido: RefreshDocument dibuja cada región como un rectángulo de contorno de dos píxeles en su propio color encima del mapa de bits de página en caché, de la misma manera en que dibuja los resultados de búsqueda, en lugar de mezclar un relleno de color sobre el texto subyacente, así que un aspecto clásico de lavado amarillo debe pintarse en el código de la aplicación o dejarse para el flujo de apariencia propio de una anotación ya promovida. Una capacidad que vale la pena reutilizar una vez que existe una región es CreateCurrentPageRegionSnapshot, que toma el mismo THPDFRectangle que ya lleva un resaltado y renderiza solo esa área a un mapa de bits, útil para adjuntar una pequeña imagen de vista previa a un comentario de revisión sin exportar la página completa. Una compilación de revisión no tiene que elegir entre los dos mecanismos de antemano: por defecto, use una región desechable THPDFViewerHighlight para cada marca nueva mientras un hilo de comentarios siga abierto, y llame a AddLoadedHighlightAnnotation solo una vez que un revisor lo resuelva, lo que mantiene el PDF cargado intacto durante el ir y venir que produce la mayor parte del churn. El control de visor descrito aquí es parte del componente HotPDF estándar para Delphi y C++Builder, junto con el resto de las API de anotaciones y formularios referenciadas arriba