El componente PDFium crea anotaciones de marcado de texto — es decir, de resaltado, subrayado, tachado y ondulado — a través de TPdf.CreateAnnotation: usted establece HasAttachmentPoints := True en el registro TPdfAnnotation y llena su cuadrilátero AttachmentPoints, y el componente escribe la entrada QuadPoints definida en la norma ISO 32000-1 §12.5.6.10. Esa es toda la superficie de la API. El motivo por el cual existe este artículo es lo que sucede debajo de ella, ya que la cadena de llamadas sin procesar de PDFium tiene un modo de falla que produce el síntoma menos útil del kit de herramientas: FPDFAnnot_SetAttachmentPoints devuelve falso en una anotación recién creada, siempre, sin ningún código de error ni pista. Este es el complemento en el lado de la creación para nuestro artículo sobre cómo leer y revisar anotaciones existentes, que recorre la dirección opuesta a través de las mismas estructuras
La escena de depuración siempre es la misma. Crea una anotación de resaltado, llama al establecedor de puntos de acoplamiento (AttachmentPoints) con el índice 0, la función devuelve falso y comienza a dudar de sus coordenadas. Transpone los puntos, invierte el eje Y, intercambia el espacio de página por el espacio de dispositivo. Nada de eso ayuda, porque las coordenadas nunca fueron el problema. El problema es la semántica del índice de la API de C y, una vez que la comprende, la solución requiere solo dos líneas
Qué significan los QuadPoints en ISO 32000-1
QuadPoints es una matriz de 8×n números que describen n cuadriláteros, y la norma ISO 32000-1 §12.5.6.10 lo requiere en cada anotación de marcado de texto: cada cuadrilátero marca una palabra o grupo de palabras contiguas a las que se aplica el resaltado, subrayado o tachado. La entrada Rect de la anotación todavía existe, pero para subtipos de marcado solo limita la región; los cuadriláteros (quads) son lo que el renderizador realmente pinta. Se utiliza un cuadrilátero en lugar de un rectángulo porque el texto se puede girar o distorsionar, 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 divergen. 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 creador probaba sus archivos contra Acrobat, casi 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 procesan como resaltados colapsados o torcidos en algunos visores. La estructura FS_QUADPOINTSF de PDFium codifica exactamente esta convención: (x1,y1) is la esquina superior izquierda, (x2,y2) la superior derecha, (x3,y3) la inferior izquierda y (x4,y4) la inferior derecha, en coordenadas de página donde el eje 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 anotación nueva 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 número 0 existente", 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 recuento 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 componente PDFium. Hasta la versión v1.79.0, la rutina interna compartida por CreateAnnotation y SetAnnotation incluía la llamada rígida FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), la cual era correcta para actualizar una anotación de marcado existente y estaba garantizada a fallar 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, realiza una bifurcación basada en el recuento
// 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 directamente a las funciones exportadas de C, lo cual el componente le permite hacer ya que todos los puntos de entrada FPDFAnnot_* están expuestos en PDFium.pas. Cada vez que tenga un controlador FPDF_ANNOTATION y desee escribir cuadriláteros, consulte primero a FPDFAnnot_CountAttachmentPoints y dirija la llamada en consecuencia. Si está buscando por qué "FPDFAnnot_SetAttachmentPoints devuelve falso", esta bifurcación de contar y luego anexar (count-then-append) es casi seguro su respuesta
Crear un resaltado con TPdf.CreateAnnotation
Dado que el componente se encarga de dirigir la llamada entre Append y Set por usted, la creación de un resaltado se reduce a llenar un registro. El siguiente ejemplo crea una página A4 y coloca un resaltado amarillo semitransparente sobre una región de 200×20 puntos; note que el cuadrilátero sigue el orden en Z descrito anteriormente y que Rectangle se establece para encerrar el cuadrilátero, lo que mantiene el comportamiento esperado en los visores que realizan pruebas de colisión contra Rect
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 de subtipo cuesta una sola línea. anUnderline, anStrikeout y anSquiggly adoptan la misma forma de registro, incluidos los cuadriláteros, porque la norma ISO 32000-1 trata a los cuatro como la misma familia de anotaciones, diferenciéndose solo 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 False para ellos y la maquinaria de cuadriláteros nunca se ejecutará
¿Por qué AttachmentPoints[0] compila en Delphi pero falla en FPC?
TQuadrilateralPoint se declara como array [1..4] of TPdfPoint, una matriz indexada 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 quejas, porque la verificación de rango está desactivada de forma predeterminada; en tiempo de ejecución, la expresión lee o escribe silenciosamente la memoria justo antes de la matriz, que en un registro TPdfAnnotation es un campo adyacente. Su resaltado obtiene una esquina corrupta, o un campo vecino se daña, y no se produce ninguna excepción. Free Pascal detectó exactamente este error en nuestras propias fuentes de demostración durante la adaptación a Lazarus: fpc realiza una comprobación de rango en tiempo de compilación para índices constantes y rechazó AttachmentPoints[0..3] por complet, que es como se descubrieron juntos el error del índice desviado en uno y el error de Set frente a Append de la biblioteca
Dos hábitos se derivan de esto: indexe el cuadrilátero de 1 a 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 verificación de rango activada — ya sea con {$R+} en Delphi o en cualquier compilación de fpc — antes de confiar en él. Que una compilación predeterminada de dcc32 se complete con éxito no es evidencia de que los índices sean correctos; solo es evidencia de que nada falló en la memoria que se encontraba allí por casualidad
Obtener coordenadas de cuadriláteros a partir de texto real
Los rectángulos codificados de forma rígida están bien para una demostración, pero los resaltados de producción siguen 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 descritas en nuestra guía sobre extracción de texto con el componente PDFium le brindan cuadros delimitadores por carácter en el mismo espacio de coordenadas de página que usan los cuadriláteros, por lo que una coincidencia de búsqueda se convierte directamente en puntos de esquina: el lado izquierdo del primer carácter, el derecho del último y la parte superior e inferior a partir de las dimensiones 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 cómo calcular esas dimensiones de antemano
Un límite honesto: el registro TPdfAnnotation contiene un único 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 simple es una anotación por línea, que se representa correctamente en todas partes y conserva la API a nivel de componente. La forma compacta, una sola anotación que lleva tres cuadriláteros, significa 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 cual funciona precisamente porque Append crea ranuras en lugar de reemplazarlas. No intente lograr múltiples cuadriláteros a través de llamadas repetidas a SetAttachmentPoints; cada índice que supere el recuento 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 caiga sobre el texto, se lea con la opacidad prevista y sobreviva a un ciclo completo de guardado y recarga. Los tipos de anotaciones, el manejo de cuadriláteros y el escritor basado en recuentos que se muestran aquí son parte del componente estándar PDFium Component 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