Una annotazione PDF è un dizionario collegato a una pagina, non un segno disegnato su di essa. ISO 32000-1 §12.5 definisce circa due dozzine di sottotipi, e ciascuno porta un /Subtype, un rettangolo in coordinate di pagina, un insieme di flag e di solito uno stream di aspetto che decide cosa un viewer dipinge davvero. I sottotipi non significano tutti la stessa cosa per una persona che sta revisionando un documento. Un Highlight e un tratto Ink sono commenti; un Link è navigazione; un Popup è la finestrella che si apre quando fate clic su una nota adesiva, memorizzata come oggetto a sé e puntata da un genitore. Le risposte sono annotazioni Text a tutti gli effetti che fanno riferimento al commento a cui rispondono tramite una voce di risposta. Quindi l'array di annotazioni a livello di pagina non è l'elenco dei commenti del revisore. È un sacco piatto che contiene commenti, l'impianto idraulico che li collega e diverse cose che nessun revisore chiamerebbe mai commenti. Un pannello che tratta l'array come elenco dei commenti sarà in disaccordo con ogni altro viewer che il cliente usa
Costruire un flusso di revisione delle annotazioni su PDFium Component, il componente VCL/LCL basato su PDFium per Delphi, C++Builder e Lazarus, significa concentrarsi sui punti in cui quello scarto fra array grezzo e vista umana crea guai: contare, indicizzare, ricolorare segni che il motore ha già congelato, cancellare senza lasciare fantasmi e aggiungere segni vostri
Perché il vostro conteggio non coincide mai con il pannello commenti di Acrobat
Aprite un contratto pieno di segni nel vostro viewer e in Acrobat affiancati e i totali raramente concordano. Acrobat mostra una vista curata: i marcatori raggruppati in thread di risposta, i popup ripiegati nelle note a cui appartengono, link e widget di modulo esclusi. L'array grezzo li contiene tutti indifferenziati, quindi un conteggio ingenuo risulta alto per certi versi e basso per altri nello stesso momento
I popup gonfiano il totale, perché ogni nota adesiva viaggia con un oggetto Popup separato e contarli entrambi raddoppia la nota. Le risposte lo sgonfiano se filtrate sui segni visibili, dato che una risposta è una annotazione Text che non dipinge nulla finché qualcuno non espande il thread, e scartarla fa perdere la discussione. I flag Hidden e NoView tolgono una annotazione dallo schermo senza toglierla dall'array, quindi un conteggio cieco ai flag include segni che l'utente non può vedere. Le annotazioni Link stanno nello stesso array dei commenti e non appartengono né al conteggio né all'elenco. Decidete la regola di conteggio prima di scrivere il ciclo, e mettete per iscritto la decisione, perché "perché il vostro pannello mostra un numero diverso da Acrobat" è il primo ticket che una funzione di revisione si guadagna
Indicizzate tutto una volta, poi non ri-analizzate mai una pagina
Una regola di progetto guida tutto ciò che segue: filtrare per autore, tipo o pagina non deve mai ri-analizzare gli oggetti di pagina. Su un documento di 300 pagine fitto di marcature, ri-analizzare a ogni cambio di menu a tendina trasforma il pannello in qualcosa che singhiozza per secondi interi. Il componente espone AnnotationCount e la proprietà indicizzata Annotation[], entrambe limitate alla pagina attualmente caricata, e il record TPdfAnnotation che restituiscono porta ciò che serve a una lista: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. La mossa giusta è passare in rassegna ogni pagina una volta sola all'apertura e tenere un vostro indice piatto:
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];
// Tenete solo i sottotipi rilevanti per il revisore; registrate la
// coppia pagina e indice perché ogni modifica successiva la usa
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 che vale la pena sottolineare è (PageNo, i). Ogni mutazione successiva, che sia una ricolorazione o una cancellazione, viene indirizzata dal numero di pagina più l'indice di annotazione, e l'indice è fragile: rimuovere una annotazione rinumera tutto ciò che la segue su quella pagina. Quindi prevedete di ricostruire le voci della pagina interessata dopo ogni cancellazione, invece di rattoppare i numeri di indice sul posto. La ricostruzione costa un millisecondo. Un indice obsoleto, al contrario, cancella il commento del revisore sbagliato, che è il tipo di bug che erode la fiducia nell'intera funzione
Il threading merita uno spazio nell'indice anche se la vostra prima release si limita a contare le risposte anziché mostrarle. Raggruppate le voci per riferimento al genitore mentre avete la pagina aperta, così che il pannello possa più tardi ripiegare un thread come fa Acrobat. Ricostruire quel raggruppamento pigramente durante lo scorrimento vanifica tutto il senso di indicizzare una volta sola, perché riapre pagine che avete già pagato per analizzare. La geometria vuole la stessa disciplina. Il Rectangle in ogni record è nello spazio della pagina, e convertirlo in coordinate di vista spetta a un unico helper condiviso, non sparso per il codice. I pannelli sviluppano bug di coordinate quando selezione, hit test e disegno inventano ciascuno la propria matematica di zoom e rotazione; incanalate tutti e tre in una sola conversione e una evidenziazione, la sua riga nell'elenco e il suo bersaglio di clic restano agganciati allo stesso inchiostro
Ricolorare i marcatori e il veto dello stream di aspetto
Cambiare una evidenziazione da giallo ad ambra sembra una riga di codice, e a volte lo è. Il punto è ISO 32000-1 §12.5.5. Quando una annotazione porta uno stream di aspetto /AP, un viewer conforme dipinge quello stream già pronto e tratta la voce di colore nel dizionario come metadato morto. Acrobat scrive stream di aspetto praticamente per tutto ciò che crea, quindi la maggior parte delle annotazioni che arrivano dai clienti è già in questo stato, e il colore che avete impostato con tanta sicurezza non raggiunge mai lo schermo. La ricolorazione è un ciclo leggi-modifica-scrivi attraverso la proprietà Annotation[], e il componente è onesto sul conflitto: quando il motore rifiuta di lasciare che un colore di dizionario prevalga su un aspetto già cotto, la scrittura solleva EPdfError
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF; // ambra
A.ColorAlpha := 160;
try
Pdf.Annotation[Item.Index] := A;
except
on EPdfError do
begin
// L'annotazione possiede uno stream /AP già renderizzato; il colore
// del dizionario da solo non cambia ciò che i viewer dipingono
Item.AppearanceLocked := True;
StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
end;
end;
Intercettate quella eccezione ogni volta, e trattatela come informazione anziché come fallimento. Saltate la guardia e il vostro pannello mostrerà allegramente l'ambra nel proprio elenco mentre la pagina continua a dipingere il giallo; l'utente lo segnalerà settimane dopo come "il vostro viewer ignora le mie modifiche", e voi passerete un pomeriggio senza riuscire a riprodurlo su un file che per caso non ha stream di aspetto. Una volta che sapete che l'aspetto è bloccato, avete due risposte oneste: ricolorare il vostro overlay di selezione invece dell'annotazione, così che il revisore veda almeno l'evidenziazione che ha scelto, oppure segnare la riga come bloccata dall'aspetto, così che nessuno si aspetti che la modifica regga
Cancellare annotazioni senza lasciare fantasmi
DeleteAnnotation rimuove l'oggetto dall'albero delle annotazioni della pagina corrente, ma lascia intatta la raster di pagina in cache. Disegnate subito dopo la chiamata e l'evidenziazione cancellata è ancora sullo schermo, ferma in una bitmap che non corrisponde più al modello di documento che le sta dietro. La correzione è trattare il nuovo rendering come parte della cancellazione, non come un passo che il chiamante potrebbe dimenticare:
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index); // solleva EPdfError in caso di errore
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
PaintPageBitmap(Bmp);
finally
Bmp.Free; // RenderPage cede la proprietà della bitmap al chiamante
end;
RebuildPageEntries(Item.PageNo); // gli indici dopo Item.Index sono slittati
Due dettagli in quel blocco si sbagliano facilmente. L'opzione reAnnotations deve essere presente, altrimenti la nuova raster perde ogni annotazione rimasta e la pagina sembra come se aveste spazzato via l'intero insieme di commenti anziché un solo segno. E il Bmp.Free non è facoltativo: l'overload di RenderPage in forma di funzione cede la proprietà della bitmap al chiamante, quindi una liberazione mancante perde una raster a pagina intera a ogni singola cancellazione, cosa che un revisore alle prese con un documento lungo trasformerà in vera pressione sulla memoria nel giro di minuti
Aggiungere segni di revisione dalla vostra UI
La creazione di annotazioni passa da CreateAnnotation, che prende un record TPdfAnnotation compilato (sottotipo, rettangolo, colore, contenuto, autore) e lo collega alla pagina corrente. Una nota adesiva, sottotipo anText, è il caso facile: impostate posizione, contenuto e autore e avete finito. È sulle annotazioni Ink che le persone si incastrano. Il rettangolo del record delimita soltanto il disegno; i tratti veri e propri sono array di punti che vanno collegati separatamente tramite la chiamata di tratto del motore, FPDFAnnot_AddInkStroke alimentata con dati FS_POINTF, catturati dal mouse o dalla penna un tratto alla volta. Costruite una annotazione Ink da un rettangolo e nient'altro e ottenete uno scarabocchio vuoto che si renderizza come spazio bianco, il che sembra un bug del motore ed è in realtà una annotazione lasciata a metà
Sistemate la politica di paternità nello stesso respiro. Ogni segno creato dalla vostra UI dovrebbe portare un AuthorText coerente, perché il filtro per revisore che costruirete il mese prossimo vale solo quanto i nomi che stampate sui commenti oggi. Stringhe di autore vuote o incoerenti non si possono riparare retroattivamente senza riaprire ogni file
Portare la revisione fuori dal viewer
I dati di revisione si ripagano nel momento in cui possono lasciare il viewer, come riepilogo che il capoprogetto legge senza aprire il file o come CSV che alimenta un foglio di tracciamento. Esportate dall'indice che avete già costruito, mai da una nuova analisi, e scegliete un modo stabile per fare riferimento a ciascun segno. Un numero di pagina abbinato al rettangolo dell'annotazione sopravvive ad andate e ritorni a cui un indice di array non sopravvive, perché la cancellazione successiva rinumera silenziosamente gli indici e il vostro CSV inizia a puntare ai commenti sbagliati
Una riga che vale la pena conservare porta la pagina, il sottotipo, l'autore, la marca temporale di creazione quando il file ne registra una, il testo del contenuto e una colonna di stato che possedete voi anziché una fornita dal PDF. Lo stesso passaggio di indicizzazione è utile prima, in fase di accettazione, quando un documento arriva da fuori del team e volete sapere cosa contiene prima che qualcuno lo revisioni. L'articolo sul banco di lavoro per accettazione PDF illustra quel triage, e la navigazione fra campi modulo copre il problema speculare: revisionare documenti costruiti per raccogliere dati anziché commenti
Un caso che l'array non vi mostrerà
Una modalità di guasto merita una segnalazione perché sembra un difetto del vostro codice e non lo è. Un cliente riferisce evidenziazioni visibili su tutta una pagina, ma il vostro pannello non elenca nulla e AnnotationCount torna a zero. La spiegazione abituale è che i segni sono stati appiattiti da qualche parte a monte. L'appiattimento cuoce gli aspetti delle annotazioni dentro il normale contenuto di pagina, quindi le evidenziazioni diventano parte della grafica di pagina e smettono del tutto di esistere come oggetti annotazione. Non resta nulla che una API di annotazione possa enumerare, ricolorare o cancellare. Quando vedete marcature dipinte con un conteggio a zero, smettete di cercare il bug nel vostro ciclo di enumerazione e chiedete come è stato prodotto il file
La superficie di annotazione usata qui, dall'enumerazione e creazione fino a ricolorazione, cancellazione e opzioni di rendering che tengono onesto il display, è distribuita con PDFium Component per Delphi, C++Builder e Lazarus/FPC