PDFium Component premieňa PDF s pevným rozložením na sémantický model, ktorý sa dá preplynuliť, pomocou BuildReflowDocument, a tento model exportuje ako samostatné HTML cez ToHtml. Nadpisy zostávajú nadpismi, položky zoznamu zostávajú položkami zoznamu, a tabuľky rozpoznané na stránke vychádzajú ako skutočný tabuľkový markup so zachovanými hlavičkovými bunkami a rozpätiami. Nič vo výstupe neodkazuje na externý skript ani štýlopis
Dôvod, prečo toto chcieť, je, že stránka PDF je množina umiestnených glyfov, čo je presne to nesprávne pre obrazovku telefónu, čítačku obrazovky alebo vyhľadávací index. Každý pokus vyriešiť to extrakciou obyčajného textu stráca štruktúru, vďaka ktorej bol dokument čitateľný, a každý pokus vyriešiť to prevodom stránok na obrázky stráca text úplne. Model preplynulenia si zachováva oboje: slová aj vzťahy medzi nimi
Odkiaľ pochádza sémantická informácia?
Všetko začína pri GetStructuredText, jedinom zdroji textu a sémantiky v komponente. Keď PDF nesie štruktúrny strom, teda značkované PDF podľa definície v ISO 32000-1, časť 14.7, model sleduje logickú hierarchiu, ktorú zaznamenal producent. Keď ho nenesie, a väčšina reálnych PDF ho nenesie, model sa vráti k fyzickému poradiu rozloženia, ktoré je už vypočítané na účely poradia čítania
Táto voľba udržuje pevnú hranicu: na zodpovedanie otázok, ktoré dokáže zodpovedať existujúci parser, sa nezavádza druhý parser PDF ani druhý vykresľovací engine. Mechanika poradia čítania pod povrchom je opísaná v časti štruktúrované textové bloky a poradie čítania, a model preplynulenia je sémantická vrstva nad ňou, nie jej náhrada
Každý uzol zaznamenáva, odkiaľ pochádza jeho informácia, takže konzument dokáže rozlíšiť nadpis, ktorý dokument deklaroval, od nadpisu, ktorý odvodila heuristika rozloženia. Pipeline citlivé na spoľahlivosť by mali čítať toto pole namiesto toho, aby považovali všetky uzly za rovnako autoritatívne
Plochý strom, a prečo to nie je strom objektov
Model je splošteným stromom v poradí preorder: pole uzlov, kde každý uzol nesie ParentIndex a Depth, namiesto rekurzívneho záznamu alebo grafu objektov s vlastníctvom. Stránky, nadpisy, odseky, zoznamy, položky zoznamu, obrázky, popisky, tabuľky, riadky aj bunky žijú v tomto jedinom lineárnom poli
Z toho plynú dva prínosy. Konzumenti môžu pole streamovať v poradí bez rekurzie, čím sa generovanie HTML, Markdownu alebo zobrazenia stromu stáva jednoduchou slučkou. A rozloženie zostáva prenositeľné naprieč Delphi, C++Builder a Free Pascal, ktoré sa líšia v tom, ako spracúvajú rekurzívne spravované typy naprieč hranicou ABI. Rekurzívny záznam dynamických polí je presne ten typ konštrukcie, ktorá sa skompiluje všade a v každom prípade sa správa jemne odlišne
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; // inline blok štýlov, žiadny externý súbor
Options.MaxNodes := 200000; // rozpočet zlyhávajúci do bezpečného stavu
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;
Ako sa zabráni tomu, aby sa tabuľky objavili dvakrát?
Detekcia tabuliek beží po tom, čo sa pre stránku zozbieral štruktúrovaný text, čo vytvára zjavné riziko: rovnaký obsah bunky existuje v textových blokoch aj v rozpoznanej tabuľke. Generovanie oboch produkuje HTML, kde po každej tabuľke nasleduje jej vlastný obsah ešte raz ako voľné odseky
Pravidlo, ktoré to rieši, je geometrické. Keď rozpoznaná tabuľka pokrýva viac než polovicu plochy textového bloku, uzol tabuľky tento blok nahradí namiesto toho, aby sa k nemu pripojil. Indexovanie buniek vo vnútri riadku sa buduje počítaním do priehradiek, takže zostavovanie modelu zostáva lineárne v počte buniek plus riadkov namiesto opätovného prehľadávania každej bunky pre každý riadok, čo je dôležité pri finančných dokumentoch, kde jedna stránka môže niesť stovky buniek
Rozpoznaná štruktúra je úprimná v tom, že ide o detekciu. Tabuľka s deliacimi čiarami sa rozpozná spoľahlivejšie než tá, ktorá je zarovnaná čisto medzerami, a spoľahlivosť uzla to odráža. Pri obsahu, kde je nesprávna tabuľka lepšia než žiadna tabuľka, nechajte detekciu zapnutú; pri archívnej konverzii, kde je nesprávna tabuľka horšia, filtrujte podľa spoľahlivosti
Export HTML, ktoré zostáva samostatné
ToHtml prechádza už zostavený model a nikdy sa nevracia k PDFium, takže dvojnásobný export nestojí nič navyše a nemôže z toho istého modelu produkovať odlišný výsledok. Hodnoty textu a atribútov sa jednotne escapujú, úrovne nadpisov sa orezávajú na rozsah h1 až h6, ktorý HTML skutočne definuje, a hlavičkové bunky, RowSpan a ColumnSpan prechádzajú tak, ako boli zapísané
Voliteľné CSS je obyčajný inline blok štýlov. Neexistuje žiadny skript, žiadny webový font ani žiadny externý zdroj akéhokoľvek druhu, a práve to robí výstup bezpečným na vloženie do e-mailu, prehliadača pomocníka alebo izolovaného ovládacieho prvku prehliadača:
var
Html: WideString;
Stream: TFileStream;
Bytes: TBytes;
begin
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.IncludeCss := True;
Options.IncludePageSections := True; // zachovať viditeľné hranice stránok
Options.PreserveLineBreaks := False; // nechať prehliadač zalamovať odseky
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 je možnosť, nad ktorou sa najviac oplatí zamyslieť. Zalomenie riadku v PDF je sadzobné rozhodnutie urobené pre pevnú šírku stránky, takže jeho zachovanie na úzkej obrazovke reprodukuje presne ten problém, kvôli ktorému preplynulenie existuje. Zachovajte zalomenia pre poéziu, výpisy kódu a adresy; pri súvislom texte ich zahoďte
Rozpočty, zrušenie a stav stránky
Znaky, uzly, tabuľky aj bunky majú každý svoj strop, a každý sa kontroluje pred alokáciou, nie po nej, takže poškodený alebo nepriateľský dokument čisto zlyhá namiesto toho, aby spotrebúval pamäť, kým to neurobí niečo iné. Token na zrušenie sa kontroluje na hraniciach stránky, bloku, tabuľky, riadku aj bunky, čo udržuje zrušené skenovanie tisícstranového dokumentu responzívne
Jedno správanie je dôležité konkrétne pre GUI aplikácie: celé skenovanie dokumentu beží vo vnútri rozsahu, ktorý obnovuje aktívnu stránku, takže úspech, zlyhanie rozpočtu aj zrušenie ponechávajú aktuálnu stránku volajúceho nedotknutú. Prehliadač, ktorý umožní používateľovi exportovať počas prezerania stránky 340, sa aj potom nájde stále na stránke 340
Na čo je preplynulenie dobré a na čo nie
Výstup preplynulenia je vynikajúcim vstupom pre vyhľadávacie indexovanie, prístupné zobrazenia na čítanie, mobilné zobrazenie a migráciu obsahu. Nie je to konvertor zachovávajúci vernosť: absolútne pozície, presné písma, vektorová grafika a presná geometria stránky sú zámerne mimo jeho účelu. Keď úloha potrebuje, aby stránka vyzerala rovnako, vykreslite ju; keď potrebuje, aby bola stránka čitateľná niekde inde, preplynulite ju
Konkrétne pre asistenčné technológie sa model preplynulenia dopĺňa s funkciami čítania opísanými v časti zostavenie prístupnej čítačky, a dokumenty, ktoré nesú skutočný štruktúrny strom, produkujú citeľne lepšie modely, čo je dobrý argument pre validáciu značkovania vyššie v reťazci, ako opisuje časť validácia štruktúrneho stromu PDF/UA
Preplynulenie, štruktúrovaný text, validácia značkovania a vykresľovanie zdieľajú jeden objekt dokumentu naprieč Delphi, C++Builder a Lazarus; kompletné API opisuje stránka PDFium Component pre Delphi