Un revisore apre lo stesso contratto nel tuo viewer Delphi e in Adobe Acrobat. Il pannello commenti di Acrobat elenca quattordici elementi; il tuo pannello di revisione ne mostra undici. Il ciclo non ha nulla di sbagliato. I tre mancanti sono due risposte, oggetti annotazione completi collegati ai genitori tramite riferimenti in-reply-to, e una finestra popup appartenente a una nota adesiva che avevi già contato una volta. Le annotazioni PDF non sono una lista piatta di rettangoli colorati: ISO 32000-1 §12.5 definisce una rete di dizionari con sottotipi, flag, appearance stream e relazioni padre-figlio, e un pannello di revisione che ignora quelle relazioni continuerà a non concordare con qualsiasi altro viewer posseduto dal cliente. Questa guida costruisce un workflow di revisione annotazioni su PDFium Component, il componente VCL/LCL basato su PDFium per Delphi, C++Builder e Lazarus, intorno ai punti in cui i documenti realmente revisionati oppongono resistenza.
Perché il conteggio non coincide mai con il pannello commenti di Acrobat
Acrobat presenta una vista curata: annotazioni di markup raggruppate in thread di risposte, con popup ripiegati nei rispettivi genitori. L'array grezzo delle annotazioni su ogni pagina contiene sia più sia meno di quanto quella vista suggerisca.
- Le annotazioni popup sono oggetti separati collegati a una nota padre: contarle raddoppia ogni sticky note.
- Le risposte sono annotazioni Text complete che referenziano un genitore; filtrare solo sui segni visibili elimina silenziosamente il thread di discussione.
- I flag Hidden e NoView rimuovono un'annotazione dalla visualizzazione, non dall'array, quindi i controlli sui flag appartengono alla fase di indicizzazione.
- Le annotazioni Link vivono nello stesso array, e nessun revisore considera un collegamento ipertestuale un commento.
Stabilisci la regola di conteggio prima di scrivere codice e mettila nella specifica, perché "perché il vostro pannello mostra un numero diverso da Acrobat" è il primo ticket generato da una funzione di revisione.
Indicizza tutto una volta, poi non riparsare mai una pagina
Filtrare per autore, tipo o pagina non deve avviare una nuova analisi degli oggetti pagina: su un documento di 300 pagine con markup pesante, ogni cambio di dropdown diventerebbe secondi di latenza. Il componente espone AnnotationCount e la proprietà indicizzata Annotation[] rispetto alla pagina attualmente caricata, e il record TPdfAnnotation restituito contiene tutto ciò che serve a una list view: Subtype, Flags, Color, Rectangle, ContentsText e AuthorText. Costruisci l'indice in un'unica scansione all'apertura:
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;
La coppia da sottolineare è (PageNo, i). Ogni chiamata di mutazione successiva, ricolorazione o eliminazione, è indirizzata da numero pagina più indice annotazione, e gli indici scorrono quando un'annotazione viene rimossa. Prevedi di ricostruire le voci della pagina interessata dopo qualsiasi eliminazione invece di correggere gli indici sul posto; la ricostruzione costa millisecondi, mentre un indice obsoleto elimina il commento del revisore sbagliato.
Il threading delle risposte merita un posto nel disegno dell'indice anche se la prima release si limita a contarle invece di visualizzarle. Raggruppa gli elementi per riferimento al genitore in fase di build, così il pannello potrà poi ripiegare un thread come fa Acrobat; ricostruire il raggruppamento pigramente durante lo scrolling riapre pagine che hai già pagato per analizzare. Lo stesso ragionamento a passata singola si applica alla geometria: il Rectangle in ogni record è espresso nello spazio pagina, quindi convertilo in coordinate vista in un solo helper condiviso. I pannelli di revisione accumulano bug di coordinate quando selezione, hit-testing e pittura portano ognuno la propria aritmetica di zoom e rotazione; un percorso unico di conversione mantiene un'evidenziazione, la voce in lista e il target del clic puntati allo stesso tratto di inchiostro.
Ricolorare markup e il veto dell'appearance stream
Cambiare un'evidenziazione da gialla ad ambra sembra una riga di codice, e a volte lo è. La complicazione è ISO 32000-1 §12.5.5: quando un'annotazione contiene un appearance stream /AP, i viewer conformi renderizzano quello stream prerenderizzato, e la voce colore nel dizionario dell'annotazione diventa decorativa. Poiché Acrobat scrive appearance stream praticamente per tutto ciò che crea, la maggior parte delle annotazioni ricevute dai clienti arriva esattamente in questo stato. La ricolorazione è una lettura-modifica-scrittura tramite la proprietà Annotation[], e il componente segnala onestamente il veto del motore sollevando 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;
Gestisci sempre quell'eccezione. Saltare la protezione significa che il pannello mostra il nuovo colore nella propria lista mentre la pagina continua a dipingere quello vecchio, e la discrepanza emergerà settimane dopo come segnalazione "il vostro viewer ignora le mie modifiche". Quando l'appearance è bloccata, le opzioni oneste sono ricolorare l'overlay di selezione invece dell'annotazione, oppure contrassegnare l'elemento come appearance-locked nell'interfaccia.
Eliminare annotazioni senza lasciare fantasmi
DeleteAnnotation scollega l'oggetto dalla pagina corrente, ma non ricostruisce la cache dell'aspetto pagina; se ridipingi subito dopo la chiamata, l'evidenziazione eliminata è ancora visibile. Rerenderizza il raster della pagina come parte della stessa operazione:
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
Nota l'opzione reAnnotations nella chiamata di rendering: senza di essa il raster esclude tutte le annotazioni rimanenti, cosa che per l'utente sembra una cancellazione di massa. E nota Bmp.Free: l'overload funzione di RenderPage trasferisce la proprietà del bitmap al chiamante, quindi saltare la free perde un raster a pagina intera a ogni eliminazione.
Aggiungere segni di revisione dalla tua UI
La creazione di annotazioni passa da CreateAnnotation, che prende un record TPdfAnnotation compilato, sottotipo, rettangolo, colore, contenuto e autore, e lo aggiunge alla pagina corrente. Le note adesive (anText) sono il caso semplice: posizione, contenuto, autore, fatto. Le annotazioni ink sono la trappola: il rettangolo del record delimita solo il disegno, mentre i tratti effettivi sono array di punti da collegare separatamente tramite la chiamata ink-stroke del motore (FPDFAnnot_AddInkStroke con dati FS_POINTF), catturati da mouse o penna tratto per tratto. Creare un'annotazione ink dal solo rettangolo produce uno scarabocchio vuoto che non renderizza nulla.
Decidi nello stesso momento la policy di authorship: ogni segno creato dalla tua UI deve contenere un AuthorText coerente, perché il filtraggio downstream per revisore è affidabile solo quanto i nomi che scrivi oggi.
Far uscire la revisione dal viewer
I dati di revisione valgono quando lasciano il viewer: un riepilogo che il responsabile progetto legge senza aprire il file, oppure un CSV che alimenta un foglio di tracking. Esporta dall'indice, non da una nuova analisi, e mantieni riferimenti stabili: numero pagina più rettangolo dell'annotazione sopravvive ai round-trip meglio di un indice di array che la prossima eliminazione invalida.
Una riga di export difendibile contiene pagina, sottotipo, autore, informazioni di creazione quando presenti, testo del contenuto e una tua colonna di stato. Per documenti che arrivano dall'esterno del team, conviene eseguire lo stesso passaggio di indicizzazione durante la triage di ingestione; l'articolo sul workbench di ingestione PDF mostra quel pattern, e la navigazione dei campi modulo copre il problema complementare dei documenti che raccolgono dati invece di commenti.
FAQ
Perché un'evidenziazione ricolorata mostra ancora il vecchio colore?
L'annotazione quasi certamente contiene un appearance stream /AP, che i viewer conformi dipingono preferendolo al colore del dizionario (ISO 32000-1 §12.5.5). Scrivere il record tramite Annotation[] solleva EPdfError in quel caso: tratta l'eccezione come fonte di verità, non il colore che intendevi impostare.
Perché la pagina mostra ancora un'annotazione che ho rimosso?
DeleteAnnotation aggiorna il modello del documento, non il raster in cache. Rerenderizza la pagina con RenderPage dopo una rimozione riuscita, e ricostruisci le voci dell'indice per quella pagina perché gli indici delle annotazioni scalano verso il basso dopo lo slot rimosso.
Le annotazioni flattened compaiono nell'array delle annotazioni?
No. Il flattening converte le appearance delle annotazioni in contenuto ordinario della pagina, quindi non sono più oggetti annotazione. Se un file cliente mostra markup visibile ma AnnotationCount è zero, il flattening a monte è la spiegazione più comune: non resta nulla da revisionare via codice.
La superficie API delle annotazioni usata in questo articolo, enumerazione, creazione, ricolorazione, rimozione e opzioni di rendering che mantengono onesta la visualizzazione, è inclusa in PDFium Component per Delphi, C++Builder e Lazarus/FPC.