PDFium Component transformă un PDF cu layout fix într-un model semantic ce poate fi reordonat (reflow), folosind BuildReflowDocument, și exportă acel model ca HTML autonom prin ToHtml. Titlurile rămân titluri, elementele de listă rămân elemente de listă, iar tabelele detectate pe pagină ies ca marcaj de tabel real, cu celule de antet și spanuri păstrate. Nimic din rezultat nu referă un script sau o foaie de stil externă
Motivul pentru care merită asta e că o pagină PDF este un set de glife poziționate, ceea ce este exact greșit pentru un ecran de telefon, un cititor de ecran sau un index de căutare. Fiecare încercare de a rezolva asta prin extragerea de text simplu pierde structura care făcea documentul lizibil, iar fiecare încercare de a rezolva asta prin convertirea paginilor în imagini pierde textul în întregime. Un model de reflow le păstrează pe amândouă: cuvintele și relațiile dintre ele
De unde provine informația semantică?
Totul pornește de la GetStructuredText, sursa unică de text și semantică din componentă. Când PDF-ul poartă un arbore de structură, PDF marcat (tagged) așa cum este definit în ISO 32000-1 clauza 14.7, modelul urmează ierarhia logică pe care producătorul a înregistrat-o. Când nu o face, iar majoritatea PDF-urilor din lumea reală nu o fac, modelul revine la ordinea de layout fizic deja calculată pentru scopuri de ordine de citire
Această alegere păstrează o limită fermă: niciun al doilea parser PDF și niciun al doilea motor de randare nu sunt introduse pentru a răspunde la întrebări la care cel existent poate deja răspunde. Mecanismul de ordine de citire de dedesubt este descris în blocurile de text structurat și ordinea de citire, iar modelul de reflow este un strat semantic deasupra lui, nu o înlocuire
Fiecare nod înregistrează de unde a provenit informația lui, astfel încât un consumator poate distinge un titlu pe care documentul l-a declarat de un titlu pe care euristicile de layout l-au dedus. Pipeline-urile sensibile la încredere ar trebui să citească acel câmp, în loc să trateze toate nodurile ca fiind la fel de autoritare
Un arbore aplatizat, și de ce nu e un arbore de obiecte
Modelul este un arbore aplatizat în ordine pre-order: un array de noduri unde fiecare nod poartă un ParentIndex și o Depth, nu o înregistrare recursivă sau un graf de obiecte cu proprietate. Pagini, titluri, paragrafe, liste, elemente de listă, figuri, legende, tabele, rânduri și celule locuiesc toate în acel unic array liniar
Rezultă două beneficii. Consumatorii pot parcurge array-ul în ordine, în flux, fără recursivitate, ceea ce transformă emiterea de HTML, Markdown sau o vizualizare de arbore într-o buclă simplă. Iar layout-ul rămâne portabil între Delphi, C++Builder și Free Pascal, care diferă în modul în care gestionează tipuri gestionate recursive peste o graniță ABI. O înregistrare recursivă de array-uri dinamice este exact genul de construcție care compilează peste tot și se comportă subtil diferit în fiecare
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; // bloc de stil inline, fără fișier extern
Options.MaxNodes := 200000; // buget de eșuare controlată (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;
Cum sunt împiedicate tabelele să apară de două ori?
Detectarea tabelelor rulează după ce textul structurat a fost colectat pentru o pagină, ceea ce creează un pericol evident: același conținut de celulă există atât în blocurile de text, cât și în tabelul detectat. Emiterea ambelor produce HTML în care fiecare tabel este urmat de propriul conținut, din nou, ca paragrafe libere
Regula care rezolvă asta este geometrică. Când un tabel detectat acoperă mai mult de jumătate din aria unui bloc de text, nodul de tabel înlocuiește acel bloc, în loc să i se alăture. Indexarea celulelor în interiorul unui rând este construită prin numărare în găleți (buckets), astfel încât construirea modelului rămâne liniară în celule plus rânduri, în loc să rescaneze fiecare celulă pentru fiecare rând, ceea ce contează pe documente financiare unde o singură pagină poate purta sute de celule
Structura detectată este onestă cu privire la faptul că este detecție. Un tabel cu linii de trasare este recunoscut mai fiabil decât unul aliniat pur prin spații albe, iar încrederea nodului reflectă asta. Pentru conținut unde un tabel greșit e mai bun decât niciun tabel, lasă detectarea activă; pentru conversie de arhivare unde un tabel greșit e mai rău, filtrează după încredere
Exportarea de HTML care rămâne autonom
ToHtml parcurge modelul deja construit și nu revizitează niciodată PDFium, așa că exportul de două ori nu costă nimic în plus și nu poate produce un rezultat diferit din același model. Textul și valorile de atribute sunt scăpate uniform, nivelurile de titlu sunt limitate la intervalul h1 până la h6 pe care HTML îl definește efectiv, iar celulele de antet, RowSpan și ColumnSpan trec neschimbate
CSS-ul opțional este un simplu bloc de stil inline. Nu există niciun script, niciun font web și nicio resursă externă de niciun fel, ceea ce face rezultatul sigur de încorporat într-un email, un vizualizator de ajutor sau un control de browser sandboxat:
var
Html: WideString;
Stream: TFileStream;
Bytes: TBytes;
begin
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.IncludeCss := True;
Options.IncludePageSections := True; // păstrează limitele de pagină vizibile
Options.PreserveLineBreaks := False; // lasă browserul să încadreze paragrafele
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 este opțiunea care merită gândită cel mai mult. O întrerupere de rând PDF este o decizie de tehnoredactare luată pentru o lățime de pagină fixă, așa că păstrarea ei pe un ecran îngust reproduce exact problema pe care reflow-ul există să o rezolve. Păstrează întreruperile pentru poezie, listinguri de cod și adrese; elimină-le pentru proză
Bugete, anulare și starea paginii
Caracterele, nodurile, tabelele și celulele au fiecare un plafon, iar fiecare este verificat înainte de alocare, nu după, astfel încât un document malformat sau ostil eșuează curat, în loc să consume memorie până când altceva o face. Token-ul de anulare este verificat la limitele de pagină, bloc, tabel, rând și celulă, ceea ce menține responsiv un scan anulat al unui document de o mie de pagini
Un comportament contează în mod specific pentru aplicații GUI: întregul scan al documentului rulează în interiorul unui domeniu (scope) care restaurează pagina activă, astfel încât succesul, eșecul de buget și anularea lasă toate neatinsă pagina curentă a apelantului. Un vizualizator care lasă utilizatorul să exporte în timp ce se uită la pagina 340 se regăsește tot pe pagina 340 după aceea
Pentru ce e bun reflow-ul, și pentru ce nu
Rezultatul de reflow este o intrare excelentă pentru indexare de căutare, vizualizări accesibile de citire, afișare mobilă și migrare de conținut. Nu este un convertor care păstrează fidelitatea: pozițiile absolute, fonturile exacte, grafica vectorială și geometria precisă a paginii sunt în afara scopului său, prin design. Când un job are nevoie ca pagina să arate la fel, randeaz-o; când are nevoie ca pagina să fie lizibilă în altă parte, aplică-i reflow
Pentru tehnologia asistivă în mod specific, modelul de reflow se combină cu funcțiile de citire descrise în construirea unui cititor accesibil, iar documentele care poartă un arbore de structură autentic produc modele vizibil mai bune, ceea ce este un argument bun pentru validarea marcării în amonte, așa cum este descris în validarea arborelui de structură PDF/UA
Reflow, text structurat, validarea marcării și randarea împart un singur obiect document pe Delphi, C++Builder și Lazarus; API-ul complet este descris pe pagina componentei PDFium pentru Delphi