Artículo técnico

Anotaciones de marcado de texto con QuadPoints de PDFium en Delphi

El PDFium Component crea anotaciones de marcado de texto (es decir, resaltado, subrayado, tachado y ondulado) a través de TPdf.CreateAnnotation: se establece HasAttachmentPoints := True en el registro TPdfAnnotation y se completa su cuadrilátero AttachmentPoints, y el componente escribe la entrada QuadPoints definida en ISO 32000-1 §12.5.6.10. Esa es toda la superficie de la API. La razón por la que existe este artículo es lo que sucede debajo de ella, porque la cadena de llamadas nativa de PDFium tiene un modo de fallo que produce el síntoma menos útil del conjunto de herramientas: FPDFAnnot_SetAttachmentPoints devuelve falso en una anotación recién creada, siempre, sin código de error y sin ninguna pista. Este es el complemento en el lado de la creación para nuestro artículo sobre lectura y revisión de anotaciones existentes, que recorre la otra dirección a través de las mismas estructuras

La escena de depuración es siempre la misma. Crea una anotación de resaltado, llama al establecedor de puntos de adjunto (attachment-points) con el índice 0, la función devuelve falso y comienza a dudar de sus coordenadas. Transpone los puntos, invierte el eje Y, cambia el espacio de página por el espacio del dispositivo. Nada de eso ayuda, porque las coordenadas nunca fueron el problema. El problema son las semánticas de índice de la API de C, y una vez que las comprende, la solución son dos líneas

Qué significan los QuadPoints en la norma ISO 32000-1

QuadPoints es una matriz de 8×n números que describen n cuadrilaterals, y la norma ISO 32000-1 §12.5.6.10 lo exige en cada anotación de marcado de texto: cada cuadrilátero marca una palabra o un grupo de palabras contiguas a las que se aplica el resaltado, subrayado o tachado. La entrada Rect de la anotación sigue existiendo, pero para los subtipos de marcado solo limita la región; los cuadriláteros (quads) son lo que el renderizador pinta realmente. Se utiliza un cuadrilátero en lugar de un rectángulo porque el texto se puede rotar o sesgar, por lo que las cuatro esquinas se almacenan como cuatro puntos independientes: x1 y1 x2 y2 x3 y3 x4 y4

El orden de esos cuatro puntos es donde la especificación y la base instalada difieren. El texto de la especificación describe los puntos trazando el cuadrilátero en sentido contrario a las agujas del reloj, pero el propio renderizador de Adobe siempre los ha interpretado en un patrón en Z: primero el borde superior de izquierda a derecha, luego el borde inferior de izquierda a derecha. Debido a que cada autor realizó sus pruebas con respecto a Acrobat, prácticamente todos los renderizadores (incluido PDFium) siguen el patrón en Z, y los archivos que siguen la redacción literal de la especificación se renderizan como resaltados colapsados o torcidos en algunos visores. La estructura FS_QUADPOINTSF de PDFium codifica exactamente esta convención: (x1,y1) es la esquina superior izquierda, (x2,y2) la superior derecha, (x3,y3) la inferior izquierda, (x4,y4) la inferior derecha, en coordenadas de página donde Y crece hacia arriba. Siga ese orden y listo; los renderizadores son indulgentes con muchas cosas, pero un cuadrilátero desordenado no es una de ellas

¿Por qué FPDFAnnot_SetAttachmentPoints devuelve falso?

FPDFAnnot_SetAttachmentPoints falla en una nueva anotación porque su contrato es reemplazar el cuadrilátero en un índice dado, y una anotación recién creada tiene cero cuadriláteros para reemplazar. La firma recibe un controlador de anotación, un quad_index y los puntos; el índice 0 no significa "la primera ranura, creándola si es necesario", significa "el cuadrilátero existente número 0", y cuando FPDFAnnot_CountAttachmentPoints informa 0, no existe tal cuadrilátero y la llamada devuelve falso. La función que crea una ranura es FPDFAnnot_AppendAttachmentPoints. Cada anotación creada a través de FPDFPage_CreateAnnot comienza con un conteo de cero, por lo que la ruta de creación debe llamar a Append primero, y solo las actualizaciones posteriores pueden llamar a Set

Esto afectó al propio PDFium Component. Hasta la versión v1.79.0, la rutina interna compartida por CreateAnnotation y SetAnnotation tenía codificado de forma fija FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), lo cual era correcto para actualizar una anotación de marcado existente y fallaba con seguridad para una nueva, presentándose como una excepción EPdfException con el mensaje 'Cannot set attachment points'. La solución, distribuida en la versión v1.79.1, se ramifica en función del conteo

// Inside the component's annotation writer (v1.79.1+):
// a new annotation has no quad slots yet, so Append creates
// the first one; Set only replaces a slot that already exists
if FPDFAnnot_CountAttachmentPoints(Annotation) = 0 then
  Check(FPDFAnnot_AppendAttachmentPoints(Annotation, QuadPoints) <> 0,
    'Cannot set attachment points')
else
  Check(FPDFAnnot_SetAttachmentPoints(Annotation, 0, QuadPoints) <> 0,
    'Cannot set attachment points');

El mismo patrón se aplica si llama a las funciones de C exportadas directamente, lo que el componente le permite hacer ya que todos los puntos de entrada FPDFAnnot_* se exponen en PDFium.pas. Siempre que tenga un controlador de FPDF_ANNOTATION y desee escribir cuadriláteros, pregunte a FPDFAnnot_CountAttachmentPoints primero y enrute en consecuencia. Si está buscando "FPDFAnnot_SetAttachmentPoints devuelve falso", esta ramificación de conteo y luego anexo es casi seguro la respuesta

Crear un resaltado con TPdf.CreateAnnotation

Al hacer el componente el enrutamiento Append frente a Set por usted, la creación de un resaltado se reduce a completar un registro. El siguiente ejemplo crea una página A4 y coloca un resaltado amarillo semitransparente sobre una región de 200×20 puntos; tenga en cuenta que el cuadrilátero sigue el orden en Z descrito anteriormente, y que Rectangle está configurado para encerrar el cuadrilátero, lo que hace que los visores que realizan pruebas de colisión frente a Rect se comporten de manera sensata

var
  Pdf: TPdf;
  A: TPdfAnnotation;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    Pdf.AddPage(0, 595, 842);

    FillChar(A, SizeOf(A), 0);
    A.Subtype := anHighlight;
    A.HasColor := True;
    A.Color := clYellow;
    A.ColorAlpha := $80;                     // 50% opacity
    A.HasAttachmentPoints := True;
    A.AttachmentPoints[1].X := 50;  A.AttachmentPoints[1].Y := 700; // top-left
    A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // top-right
    A.AttachmentPoints[3].X := 50;  A.AttachmentPoints[3].Y := 680; // bottom-left
    A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // bottom-right
    A.Rectangle.Left := 50;  A.Rectangle.Top := 700;
    A.Rectangle.Right := 250; A.Rectangle.Bottom := 680;
    A.ContentsText := 'Highlighted region';
    Pdf.CreateAnnotation(A);

    Pdf.SaveAs('highlighted.pdf');
  finally
    Pdf.Free;
  end;
end;

Cambiar los subtipos cuesta una línea. anUnderline, anStrikeout y anSquiggly toman la misma forma de registro, cuadriláteros y todo, porque la norma ISO 32000-1 trata a las cuatro como la misma familia de anotaciones, diferenciadas únicamente por cómo se decora la región del cuadrilátero. Los subtipos que no son marcas de texto, como anSquare, anCircle y anText, se posicionan únicamente a partir de Rectangle; deje HasAttachmentPoints en falso para aquellos y la maquinaria del cuadrilátero nunca se ejecutará

¿Por qué AttachmentPoints[0] se compila en Delphi pero falla en FPC?

TQuadrilateralPoint se declara como array [1..4] of TPdfPoint, una matriz basada en 1, y eso confunde a cualquiera cuyos dedos tiendan por defecto a la indexación basada en cero. Si escribe A.AttachmentPoints[0], el compilador dcc32 de Delphi lo compilará sin quejarse, porque la verificación de rango está desactivada de forma predeterminada; en tiempo de ejecución, la expresión lee o escribe silenciosamente en la memoria justo antes de la matriz, que en un registro TPdfAnnotation es un campo adyacente. Su resaltado obtiene una esquina basura, o un campo vecino se corrompe, y no se genera ningún error. Free Pascal detectó exactamente este error en nuestras propias fuentes de demostración durante la migración a Lazarus: fpc realiza una verificación de rango en tiempo de compilación sobre índices constantes y rechazó AttachmentPoints[0..3] directamente, lo que permitió descubrir juntos el error de desfase en uno (off-by-one) y el error de la biblioteca de Set frente a Append

De esto se derivan dos hábitos. Indexe el cuadrilátero del 1 al 4, coincidiendo con el orden de las esquinas en el código anterior, y compile su código de anotaciones al menos una vez con la comprobación de rango activada (ya sea {$R+} en Delphi o en cualquier compilación de fpc) antes de confiar en él. Que una compilación predeterminada de dcc32 tenga éxito no es prueba de que los índices sean correctos; solo es evidencia de que nada falló en la memoria que resultó estar allí

Obtener coordenadas de cuadrilátero a partir de texto real

Los rectángulos codificados de forma fija están bien para una demostración, pero los resaltados de producción trazan glifos reales, y las coordenadas deben provenir de la geometría de la página de texto de PDFium en lugar de conjeturas. Las rutinas cubiertas en nuestra guía de extracción de texto con el PDFium Component le brindan cuadros delimitadores por carácter en el mismo espacio de coordenadas de página que usan los cuadriláteros, por lo que un resultado de búsqueda se convierte directamente en puntos de esquina: el lado izquierdo del primer carácter, el derecho del último, y el superior e inferior a partir de las extensiones de la línea. Si está generando el texto usted mismo y necesita saber dónde caerán las líneas antes de que existan, el artículo sobre medición de texto y ajuste de línea cubre el cálculo de esas extensiones de antemano

Un detalle que vale la pena exponer con honestidad: el registro TPdfAnnotation lleva una sola TQuadrilateralPoint, por lo que una llamada a CreateAnnotation escribe un solo cuadrilátero. Una selección que abarque tres líneas necesita tres cuadriláteros (uno por línea, según §12.5.6.10), y tiene dos formas de lograrlo. La forma sencilla es una anotación por línea, que se renderiza correctamente en todas partes y mantiene la API a nivel de componente. La forma compacta (una anotación que lleva tres cuadriláteros) implica crear la anotación a través del componente y luego llamar usted mismo a la función exportada FPDFAnnot_AppendAttachmentPoints para el segundo y tercer cuadrilátero, lo que funciona precisamente porque Append crea ranuras en lugar de reemplazarlas. No intente lograr múltiples cuadriláteros mediante llamadas repetidas a SetAttachmentPoints; cada índice que supere el conteo actual simplemente devolverá falso, por la misma razón que lo hizo el índice 0 en la anotación nueva

Después de escribir, verifique en un visor real en lugar de confiar en los códigos de retorno: abra el archivo en Acrobat o en cualquier visor basado en PDFium y confirme que el marcado aterrice sobre el texto, se lea con la opacidad prevista y sobreviva a un viaje de ida y vuelta de guardado y recarga. Los tipos de anotaciones, el manejo de cuadriláteros y el escritor sensible al conteo que se muestran aquí son parte del PDFium Component estándar para Delphi, C++Builder y Lazarus; la página del producto contiene la referencia completa de la API de anotaciones junto con el resto de la biblioteca