Il componente PDFium crea annotazioni di markup testo, ovvero evidenziazioni, sottolineature, sbarrature e testi ondulati, tramite TPdf.CreateAnnotation: si imposta HasAttachmentPoints := True nel record TPdfAnnotation e si compila il quadrilatero AttachmentPoints, e il componente scrive la voce QuadPoints definita nello standard ISO 32000-1 §12.5.6.10. Questa rappresenta l'intera interfaccia delle API. Il motivo per cui esiste questo articolo risiede in ciò che accade sotto la superficie, poiché la catena di chiamate dirette di PDFium presenta una modalità di errore che produce il sintomo meno chiaro del toolkit: FPDFAnnot_SetAttachmentPoints restituisce false su un'annotazione appena creata, ogni volta, senza codice di errore o indicazioni. Questa è la guida lato creazione complementare al nostro articolo sulla lettura e revisione di annotazioni esistenti, che analizza le stesse strutture nella direzione opposta
La fase di debug segue sempre lo stesso schema. Si crea un'annotazione di evidenziazione, si chiama il setter dei punti di aggancio con indice 0, la funzione restituisce false e si inizia a dubitare delle coordinate. Si traspongono i punti, si inverte l'asse Y, si scambia lo spazio della pagina con lo spazio del dispositivo. Nulla di tutto ciò si rivela utile, perché il problema non sono mai state le coordinate. Il problema risiede nella semantica dell'indice delle API C, e una volta compresa, la soluzione si riduce a due righe di codice
Cosa significano i QuadPoints nello standard ISO 32000-1
QuadPoints è un array di 8x n numeri che descrive n quadrilateri, e lo standard ISO 32000-1 §12.5.6.10 lo richiede per ogni annotazione di markup testo: ciascun quadrilatero delimita una parola o un gruppo di parole contigue a cui si applica l'evidenziazione, la sottolineatura o la sbarratura. La voce Rect dell'annotazione è comunque presente, ma per i sottotipi di markup delimita solo la regione complessiva; i quadrilateri rappresentano ciò che il motore di rendering disegna effettivamente. Si utilizza un quadrilatero anziché un rettangolo poiché il testo può essere ruotato o inclinato, per cui i quattro angoli vengono memorizzati como quattro punti indipendenti: x1 y1 x2 y2 x3 y3 x4 y4
L'ordine di questi quattro punti è l'aspetto in cui le specifiche e le implementazioni reali si dividono. Il testo delle specifiche descrive i punti come un tracciamento del quadrilatero in senso antiorario, ma il visualizzatore di Adobe li ha sempre interpretati secondo uno schema a Z: prima il bordo superiore da sinistra a destra, poi il bordo inferiore da sinistra a destra. Poiché ogni autore ha eseguito test rispetto ad Acrobat, quasi tutti i motori di rendering, PDFium incluso, seguono lo schema a Z, e i file che seguono la lettera delle specifiche si visualizzano come evidenziazioni compresse o distorte in alcuni lettori. La struttura FS_QUADPOINTSF di PDFium codifica proprio questa convenzione: (x1,y1) rappresenta l'angolo in alto a sinistra, (x2,y2) in alto a destra, (x3,y3) in basso a sinistra e (x4,y4) in basso a destra, nelle coordinate di pagina in cui la Y cresce verso l'alto. Segui questo ordine e non avrai problemi; i lettori sono tolleranti su molti aspetti, ma un quadrilatero disordinato non è tra questi
Perché FPDFAnnot_SetAttachmentPoints restituisce false?
La funzione FPDFAnnot_SetAttachmentPoints fallisce su una nuova annotazione perché il suo comportamento prevede di sostituire il quadrilatero a un dato indice, e un'annotazione appena creata ha zero quadrilateri da sostituire. La firma accetta un handle di annotazione, un quad_index e i punti; l'indice 0 non significa "il primo slot, creandolo se necessario", ma indica "il quadrilatero esistente numero 0", e quando FPDFAnnot_CountAttachmentPoints restituisce 0, tale quadrilatero non esiste e la chiamata restituisce false. La funzione per creare uno slot è FPDFAnnot_AppendAttachmentPoints. Ogni annotazione creata tramite FPDFPage_CreateAnnot inizia con un conteggio pari a zero, per cui il percorso di creazione deve chiamare prima Append, e solo i successivi aggiornamenti possono chiamare Set
Questo aspetto ha interessato il componente PDFium stesso. Fino alla versione v1.79.0 la routine interna condivisa da CreateAnnotation and SetAnnotation presentava la chiamata fissa FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), corretta per aggiornare un'annotazione di markup esistente ma destinata a fallire per una nuova, manifestandosi con un'eccezione EPdfException e il messaggio 'Cannot set attachment points'. La correzione, introdotta nella versione v1.79.1, prevede un controllo basato sul conteggio
// 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');
Lo stesso pattern si applica se si chiamano direttamente le funzioni C esportate, operazione consentita dal componente in quanto tutti i punti di ingresso FPDFAnnot_* sono esposti in PDFium.pas. Ogni volta che si possiede un handle FPDF_ANNOTATION e si desidera scrivere quadrilateri, interroga prima FPDFAnnot_CountAttachmentPoints e procedi di conseguenza. Se stai cercando spiegazioni sul perché "FPDFAnnot_SetAttachmentPoints restituisce false", questo controllo seguito da append è quasi certamente la risposta
Creare un'evidenziazione con TPdf.CreateAnnotation
Con il componente che gestisce la logica tra Append e Set al posto tuo, creare un'evidenziazione si riduce alla compilazione di un record. L'esempio seguente crea una pagina A4 e inserisce un'evidenziazione gialla semitrasparente su una regione di 200x20 punti; si noti che il quadrilatero segue lo schema a Z descritto sopra, e che Rectangle viene impostato per racchiudere il quadrilatero, garantendo che i visualizzatori che eseguono l'hit-test su Rect si comportino in modo corretto
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;
Cambiare sottotipo richiede una sola riga di codice. anUnderline, anStrikeout e anSquiggly adottano la stessa struttura di record, inclusi i quadrilateri, poiché lo standard ISO 32000-1 considera tutti e quattro gli elementi come parte della medesima famiglia di annotazioni, che differiscono solo per le modalità di decorazione dell'area del quadrilatero. I sottotipi che non appartengono al markup del testo, come anSquare, anCircle e anText, si posizionano unicamente in base a Rectangle; per questi ultimi, mantieni HasAttachmentPoints su False per evitare l'esecuzione della logica dei quadrilateri
Perché AttachmentPoints[0] viene compilato in Delphi ma fallisce in FPC?
TQuadrilateralPoint è dichiarato come array [1..4] of TPdfPoint, un array a base 1, e questo può trarre in inganno chiunque sia abituato all'indicizzazione a base zero. Scrivendo A.AttachmentPoints[0], il compilatore dcc32 di Delphi completerà l'operazione senza segnalare errori, poiché il controllo dell'intervallo (range checking) è disattivato per impostazione predefinita; in fase di esecuzione l'espressione leggerà o scriverà silenziosamente la memoria situata appena prima dell'array, che nel record TPdfAnnotation corrisponde a un campo adiacente. L'evidenziazione riceverà un angolo con valori errati, o verrà corrotto un campo vicino, senza che venga segnalata alcuna anomalia. Free Pascal ha intercettato questo specifico bug nei sorgenti della nostra demo durante il porting per Lazarus: fpc esegue un controllo dell'intervallo in fase di compilazione sugli indici costanti e ha rifiutato direttamente la dicitura AttachmentPoints[0..3], consentendo di scoprire sia l'indice errato sia il bug della libreria relativo a Set e Append
Ne derivano due buone abitudini. Indicizza il quadrilatero da 1 a 4, rispettando l'ordine degli angoli indicato nel codice sopra, ed esegui la compilazione del codice delle annotazioni almeno una volta con il controllo dell'intervallo attivato, tramite {$R+} in Delphi o in qualsiasi build fpc, prima di considerarlo corretto. Il fatto che una compilazione standard dcc32 vada a buon fine non dimostra che gli indici siano esatti, ma indica solo che non si è verificato un crash sulla memoria causale occupata
Ottenere le coordinate dei quadrilateri da testo reale
I rettangoli definiti in modo fisso sono adatti per una demo, ma le evidenziazioni in ambiente di produzione devono seguire i caratteri effettivi, e le coordinate dovrebbero provenire dalla geometria del testo della pagina di PDFium anziché da stime approssimative. Le routine descritte nella nostra guida all'estrazione del testo con il componente PDFium restituiscono i rettangoli di delimitazione per singolo carattere nello stesso spazio di coordinate di pagina usato dai quadrilateri, per cui un risultato di ricerca si converte direttamente in punti d'angolo: sinistra del primo carattere, destra dell'ultimo, parte superiore e inferiore dalle estensioni della riga. Se stai generando tu stesso il testo e hai bisogno di sapere dove si posizioneranno le righe prima che esistano, l'articolo sulla misurazione del testo e a capo automatico descrive come calcolare tali estensioni in anticipo
Un limite reale: il record TPdfAnnotation contiene un singolo TQuadrilateralPoint, per cui una chiamata a CreateAnnotation scrive un solo quadrilatero. Una selezione distribuita su tre righe richiede tre quadrilateri, uno per riga, in base al paragrafo §12.5.6.10, e vi sono due modi per gestire la situazione. Il modo più semplice prevede di creare un'annotazione per riga, che si visualizza correttamente ovunque e mantiene le API a livello di componente. Il modo compatto, con un'unica annotazione contenente tre quadrilateri, prevede di creare l'annotazione tramite il componente e poi chiamare direttamente la funzione esportata FPDFAnnot_AppendAttachmentPoints per il secondo e il terzo quadrilatero, approccio che funziona proprio perché Append crea slot anziché sostituirli. Non tentare di gestire più quadrilateri tramite chiamate ripetute a SetAttachmentPoints: ogni indice superiore al conteggio corrente restituirà semplicemente false, per lo stesso motivo riscontrato con l'indice 0 sulla nuova annotazione
Dopo la scrittura, verifica il risultato in un visualizzatore reale anziché affidarti solo ai codici di ritorno: apri il file in Acrobat o in qualsiasi lettore basato su PDFium e conferma che il markup si posizioni sopra il testo, presenti l'opacità desiderata e superi un ciclo di salvataggio e riapertura. I tipi di annotazione, la gestione dei quadrilateri e la scrittura basata sul conteggio illustrati in questa pagina fanno parte del componente PDFium standard per Delphi, C++Builder e Lazarus; la pagina del prodotto contiene la guida di riferimento completa per le API delle annotazioni e il resto della libreria