PDFium Component převádí PDF s pevným rozvržením na sémantický model, který lze přetéct, pomocí BuildReflowDocument, a tento model exportuje jako samostatné HTML přes ToHtml. Nadpisy zůstávají nadpisy, položky seznamu zůstávají položkami seznamu a tabulky rozpoznané na stránce vyjdou jako skutečný kód tabulky se zachovanými hlavičkovými buňkami a rozsahy. Nic ve výstupu neodkazuje na externí skript ani styl
Důvod, proč to chcete, je ten, že stránka PDF je sada umístěných glyfů, což je přesně to špatné pro telefonní obrazovku, čtečku obrazovky nebo vyhledávací index. Každý pokus tento problém vyřešit extrakcí prostého textu ztratí strukturu, díky které byl dokument čitelný, a každý pokus jej vyřešit převodem stránek na obrázky ztratí text úplně. Model přetoku zachovává obojí: slova i vztahy mezi nimi
Odkud pochází sémantická informace?
Vše začíná u GetStructuredText, jediného zdroje textu a sémantiky v komponentě. Když PDF nese strukturální strom, značkované PDF podle definice v ISO 32000-1 článku 14.7, model sleduje logickou hierarchii, kterou zaznamenal tvůrce. Když ne, a většina PDF v praxi ne, model se vrátí k fyzickému pořadí rozvržení, které už bylo spočítáno pro účely pořadí čtení
Tato volba udržuje pevnou hranici: kvůli zodpovězení otázek, na které umí odpovědět stávající parser, se nezavádí žádný druhý parser PDF ani druhý vykreslovací motor. Mechanika pořadí čtení pod tím vším je popsána v článku strukturované textové bloky a pořadí čtení, a model přetoku je sémantická vrstva nad ní, ne její náhrada
Každý uzel zaznamenává, odkud jeho informace pochází, takže spotřebitel dokáže odlišit nadpis, který dokument deklaroval, od nadpisu, který odvodila heuristika rozvržení. Pipeline citlivé na spolehlivost by měly toto pole číst, místo aby zacházely se všemi uzly jako se stejně autoritativními
Plochý strom, a proč to není strom objektů
Model je zploštělý strom v pre-order pořadí: pole uzlů, kde každý uzel nese ParentIndex a Depth, místo rekurzivního záznamu nebo grafu objektů s vlastnictvím. Stránky, nadpisy, odstavce, seznamy, položky seznamu, obrázky, popisky, tabulky, řádky a buňky žijí všechny v tomto jednom lineárním poli
Plynou z toho dva přínosy. Spotřebitelé mohou pole streamovat v pořadí bez rekurze, což dělá z vytváření HTML, Markdown nebo stromového pohledu jednoduchou smyčku. A rozvržení zůstává přenosné mezi Delphi, C++Builder a Free Pascal, které se liší v tom, jak zacházejí s rekurzivními spravovanými typy přes hranici ABI. Rekurzivní záznam dynamických polí je přesně ten typ konstrukce, která se přeloží všude a v každém prostředí se chová jemně jinak
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; // vložený blok stylů, žádný externí soubor
Options.MaxNodes := 200000; // rozpočet s bezpečným selháním
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;
Jak se zabraňuje tomu, aby se tabulky objevily dvakrát?
Rozpoznávání tabulek běží až poté, co byl pro stránku shromážděn strukturovaný text, což vytváří zjevné riziko: stejný obsah buňky existuje jak v textových blocích, tak v rozpoznané tabulce. Vydání obojího by vytvořilo HTML, kde po každé tabulce znovu následuje její vlastní obsah jako volné odstavce
Pravidlo, které to řeší, je geometrické. Když rozpoznaná tabulka pokrývá více než polovinu plochy textového bloku, uzel tabulky tento blok nahradí místo toho, aby se k němu připojil. Indexování buněk uvnitř řádku se sestavuje počítáním do přihrádek, takže sestavení modelu zůstává lineární v počtu buněk plus řádků, místo aby pro každý řádek znovu prohledávalo každou buňku, což má význam u finančních dokumentů, kde jedna stránka může nést stovky buněk
Rozpoznaná struktura je upřímná ohledně toho, že jde o detekci. Tabulka s linkami se rozpozná spolehlivěji než tabulka zarovnaná čistě mezerami, a spolehlivost uzlu to odráží. Pro obsah, kde je chybná tabulka lepší než žádná tabulka, nechte detekci zapnutou; pro archivní konverzi, kde je chybná tabulka horší, filtrujte podle spolehlivosti
Export HTML, které zůstává samostatné
ToHtml prochází model, který už byl sestaven, a nikdy se nevrací k PDFium, takže export dvakrát nestojí nic navíc a nemůže ze stejného modelu vytvořit odlišný výsledek. Textové a atributové hodnoty jsou escapovány jednotně, úrovně nadpisů jsou omezeny do rozsahu h1 až h6, který HTML skutečně definuje, a hlavičkové buňky, RowSpan a ColumnSpan procházejí tak, jak byly zapsány
Volitelné CSS je obyčejný vložený blok stylů. Není zde žádný skript, žádný webový font a žádný externí prostředek jakéhokoli druhu, což dělá výstup bezpečným pro vložení do e-mailu, prohlížeče nápovědy nebo izolovaného ovládacího prvku prohlížeče:
var
Html: WideString;
Stream: TFileStream;
Bytes: TBytes;
begin
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.IncludeCss := True;
Options.IncludePageSections := True; // ponechat hranice stránek viditelné
Options.PreserveLineBreaks := False; // nechat prohlížeč zalamovat odstavce
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 volba, nad kterou se nejvíc vyplatí zamyslet. Zalomení řádku v PDF je sazečské rozhodnutí učiněné pro pevnou šířku stránky, takže jeho zachování na úzké obrazovce reprodukuje přesně ten problém, kvůli kterému přetok existuje. Zachovejte zalomení pro poezii, výpisy kódu a adresy; zahoďte je pro prózu
Rozpočty, zrušení a stav stránky
Znaky, uzly, tabulky a buňky mají každý svůj strop a každý se kontroluje před alokací, ne po ní, takže poškozený nebo nepřátelský dokument selže čistě, místo aby spotřebovával paměť, dokud to neudělá něco jiného. Token zrušení se kontroluje na hranicích stránky, bloku, tabulky, řádku a buňky, což udržuje responzivní i zrušené prohledávání tisícistránkového dokumentu
Jedno chování má význam konkrétně pro GUI aplikace: celé prohledávání dokumentu běží uvnitř rozsahu, který obnovuje aktivní stránku, takže úspěch, selhání rozpočtu i zrušení ponechávají aktuální stránku volajícího nedotčenou. Prohlížeč, který umožní uživateli exportovat při pohledu na stránku 340, se poté opět ocitne na stránce 340
Na co je přetok dobrý, a na co ne
Výstup přetoku je vynikajícím vstupem pro indexování vyhledávání, přístupné pohledy pro čtení, mobilní zobrazení a migraci obsahu. Není to konvertor zachovávající věrnost: absolutní pozice, přesné fonty, vektorová grafika a přesná geometrie stránky jsou záměrně mimo jeho účel. Když úloha potřebuje, aby stránka vypadala stejně, vykreslete ji; když potřebuje, aby byla stránka čitelná jinde, přetečte ji
Konkrétně pro asistivní technologie se model přetoku pojí s funkcemi čtení popsanými v článku sestavení přístupné čtečky, a dokumenty nesoucí skutečný strukturální strom produkují znatelně lepší modely, což je dobrý argument pro ověřování značkování už na vstupu, jak popisuje článek ověřování strukturálního stromu PDF/UA
Přetok, strukturovaný text, ověřování značkování a vykreslování sdílejí jeden objekt dokumentu napříč Delphi, C++Builder a Lazarus; kompletní API popisuje stránka PDFium Component pro Delphi