PDFium Component trasforma un PDF a layout fisso in un modello semantico che può essere sottoposto a reflow, usando BuildReflowDocument, ed esporta quel modello come HTML autonomo tramite ToHtml. I titoli restano titoli, gli elementi di elenco restano elementi di elenco, e le tabelle rilevate sulla pagina emergono come vero markup di tabella con celle di intestazione e span preservati. Nulla nell'output fa riferimento a uno script o a un foglio di stile esterno
Il motivo per cui questo è desiderabile è che una pagina PDF è un insieme di glifi posizionati, il che è esattamente sbagliato per uno schermo di telefono, uno screen reader, o un indice di ricerca. Ogni tentativo di risolvere il problema estraendo testo semplice perde la struttura che rendeva leggibile il documento, e ogni tentativo di risolverlo convertendo le pagine in immagini perde del tutto il testo. Un modello di reflow mantiene entrambi: le parole e le relazioni tra loro
Da dove proviene l'informazione semantica?
Tutto parte da GetStructuredText, l'unica fonte di testo e semantica nel componente. Quando il PDF porta un albero di struttura, PDF taggato come definito nella clausola 14.7 di ISO 32000-1, il modello segue la gerarchia logica registrata dal produttore. Quando non ce l'ha, e la maggior parte dei PDF in circolazione non ce l'ha, il modello ricade sull'ordine di layout fisico già calcolato per scopi di ordine di lettura
Questa scelta mantiene un confine rigido: non viene introdotto un secondo parser PDF né un secondo motore di rendering per rispondere a domande a cui quello esistente può già rispondere. Il meccanismo dell'ordine di lettura sottostante è descritto in blocchi di testo strutturato e ordine di lettura, e il modello di reflow è un livello semantico sopra di esso, non una sua sostituzione
Ogni nodo registra da dove proviene la propria informazione, così un consumatore può distinguere un titolo dichiarato dal documento da un titolo inferito dalle euristiche di layout. Le pipeline sensibili alla confidenza dovrebbero leggere quel campo anziché trattare tutti i nodi come ugualmente autorevoli
Un albero appiattito, e perché non è un albero di oggetti
Il modello è un albero appiattito in pre-ordine: un array di nodi dove ogni nodo porta un ParentIndex e una Depth, anziché un record ricorsivo o un grafo di oggetti con proprietà. Pagine, titoli, paragrafi, elenchi, elementi di elenco, figure, didascalie, tabelle, righe e celle risiedono tutti in quell'unico array lineare
Ne derivano due vantaggi. I consumatori possono scorrere l'array in ordine senza ricorsione, il che rende l'emissione di HTML, Markdown o una vista ad albero un semplice ciclo. E il layout resta portabile tra Delphi, C++Builder e Free Pascal, che differiscono nel modo in cui gestiscono i tipi gestiti ricorsivi attraverso un confine ABI. Un record ricorsivo di array dinamici è esattamente il tipo di costrutto che compila ovunque e si comporta in modo sottilmente diverso in ciascuno
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfReflowOptions;
Doc: TPdfReflowDocument;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.LoadDocument;
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.DetectTables := True;
Options.IncludeCss := True; // blocco di stile inline, nessun file esterno
Options.MaxNodes := 200000; // budget fail-closed
Options.MaxCharacters := 4000000;
Doc := Pdf.BuildReflowDocument(Options);
for I := 0 to High(Doc.Nodes) do
case Doc.Nodes[I].Kind of
prnkHeading:
Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
prnkParagraph:
Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Copy(Doc.Nodes[I].Text, 1, 60)]));
prnkTable:
Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
end;
Writeln(Format('%d node(s), %d table(s), %d character(s)',
[Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
finally
Pdf.Free;
end;
end;
Come si evita che le tabelle compaiano due volte?
Il rilevamento delle tabelle viene eseguito dopo che il testo strutturato è stato raccolto per una pagina, il che crea un rischio evidente: lo stesso contenuto di cella esiste sia nei blocchi di testo sia nella tabella rilevata. Emettere entrambi produce HTML dove ogni tabella è seguita di nuovo dal proprio contenuto come paragrafi sciolti
La regola che risolve il problema è geometrica. Quando una tabella rilevata copre più della metà dell'area di un blocco di testo, il nodo tabella sostituisce quel blocco anziché unirsi ad esso. L'indicizzazione delle celle dentro una riga viene costruita contando in bucket, così la costruzione del modello resta lineare in celle più righe invece di riscansionare ogni cella per ogni riga, il che conta su documenti finanziari dove una singola pagina può portare centinaia di celle
La struttura rilevata è onesta riguardo al proprio essere rilevata. Una tabella con linee di delimitazione viene riconosciuta in modo più affidabile di una allineata solo con spazi bianchi, e la confidenza del nodo lo riflette. Per contenuti dove una tabella sbagliata è meglio di nessuna tabella, tieni attivo il rilevamento; per la conversione archivistica dove una tabella sbagliata è peggio, filtra in base alla confidenza
Esportare HTML che resta autonomo
ToHtml percorre il modello già costruito e non torna mai a interpellare PDFium, quindi esportare due volte non costa nulla in più e non può produrre un risultato diverso dallo stesso modello. Il testo e i valori degli attributi vengono sfuggiti in modo uniforme, i livelli dei titoli vengono limitati all'intervallo h1-h6 effettivamente definito da HTML, e le celle di intestazione, RowSpan e ColumnSpan passano invariati
Il CSS opzionale è un semplice blocco di stile inline. Non c'è alcuno script, alcun web font e alcuna risorsa esterna di alcun tipo, il che è ciò che rende l'output sicuro da incorporare in un'email, un visualizzatore di guida o un controllo browser sandboxato:
var
Html: WideString;
Stream: TFileStream;
Bytes: TBytes;
begin
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.IncludeCss := True;
Options.IncludePageSections := True; // mantieni visibili i confini di pagina
Options.PreserveLineBreaks := False; // lascia che il browser vada a capo nei paragrafi
Html := Pdf.BuildReflowDocument(Options).ToHtml;
Bytes := TEncoding.UTF8.GetBytes(string(Html));
Stream := TFileStream.Create('report.html', fmCreate);
try
if Length(Bytes) > 0 then
Stream.WriteBuffer(Bytes[0], Length(Bytes));
finally
Stream.Free;
end;
end;
PreserveLineBreaks è l'opzione su cui vale più la pena riflettere. Un'interruzione di riga nel PDF è una decisione di impaginazione presa per una larghezza di pagina fissa, quindi preservarla su uno schermo stretto riproduce esattamente il problema che il reflow esiste per risolvere. Preserva le interruzioni per poesia, elenchi di codice e indirizzi; scartale per la prosa
Budget, cancellazione e stato della pagina
Caratteri, nodi, tabelle e celle hanno ciascuno un limite massimo, e ognuno viene controllato prima dell'allocazione anziché dopo, così un documento malformato o malevolo fallisce in modo pulito invece di consumare memoria finché non lo fa qualcos'altro. Il token di cancellazione viene controllato ai confini di pagina, blocco, tabella, riga e cella, il che mantiene reattiva una scansione annullata di un documento di mille pagine
Un comportamento conta specificamente per le applicazioni GUI: l'intera scansione del documento viene eseguita dentro uno scope che ripristina la pagina attiva, così successo, fallimento del budget e cancellazione lasciano tutti intatta la pagina corrente del chiamante. Un visualizzatore che permette all'utente di esportare mentre guarda la pagina 340 si ritrova ancora sulla pagina 340 in seguito
A cosa serve il reflow, e a cosa non serve
L'output del reflow è un ottimo input per l'indicizzazione di ricerca, le viste di lettura accessibili, la visualizzazione mobile e la migrazione dei contenuti. Non è un convertitore che preserva la fedeltà: posizioni assolute, font esatti, grafica vettoriale e geometria di pagina precisa sono fuori dal suo scopo per progetto. Quando un lavoro richiede che la pagina appaia identica, renderizzala; quando richiede che la pagina sia leggibile altrove, sottoponila a reflow
Specificamente per le tecnologie assistive, il modello di reflow si abbina alle funzionalità di lettura descritte in costruire un lettore accessibile, e i documenti che portano un vero albero di struttura producono modelli notevolmente migliori, il che è un buon argomento per validare il tagging a monte come descritto in validazione dell'albero di struttura PDF/UA
Reflow, testo strutturato, validazione del tagging e rendering condividono un unico oggetto documento tra Delphi, C++Builder e Lazarus; l'intera API è descritta nella pagina di PDFium Component per Delphi