HotPDF Delphi Component obnovuje mezery mezi slovy a konce řádků v THotPDF.ExtractLoadedPageText z geometrie glyfů, ne ze znaků mezery. Mezera se vloží, když mezera za vlastní šířkou glyfu přesáhne 0.15 výšky textu, a nový řádek začne jen tehdy, když se počátek textu přesune napříč směrem psaní o víc než polovinu výšky textu. Od v2.768.3 text stránky zahrnuje i text malovaný přes Form XObjects a nechává stranou glyfy mimo viditelnou crop oblast. Zbytek článku vysvětluje, proč každé pravidlo vypadá tak, jak vypadá, protože každé z nich nahradilo jednodušší pravidlo, které na reálných dokumentech vydávalo věrohodný, ale špatný výstup
Příznaky zná každý, kdo někdy poslal PDF text do vyhledávacího indexu. Obálka se extrahuje jako PDFReferenceManualNovember4,1998, daňový formulář se rozpadne na 156 řádků, šikmý vodoznak dorazí po jednom znaku na řádek a oříznutý korekční tisk vede řádkem tiskaře, který žádný prohlížeč neukazuje. Ani jeden z těch souborů není rozbitý. Každý používá dokonale legální způsob umísťování textu, který naivní extraktor přečte špatně
Proč extrahovaný text PDF ztrácí mezery mezi slovy?
Extrahovaný text ztrácí mezery proto, že PDF je nikdy obsahovat nemusí. Producer může oddělovat slova zobrazením znaku mezery, ale stejně tak je může posunout perem číslem uvnitř pole TJ (ISO 32000-1 §9.4.3) nebo čerstvým Td (§9.4.2) a výstup z TeXu, řada souborů z Distilleru i většina zarovnaných sazeb dělá přesně to. Před v2.766.76 se HPDFAssemblePageText díval jen na svislý pohyb, takže zlom slova provedený pozicováním prostě zmizel. Assembler teď měří, podél směru psaní předchozího glyfu, vzdálenost od konce jeho vlastní šířky k počátku aktuálního glyfu a vloží jednu mezeru, když vzdálenost přesáhne 0.15 výšky krabice aktuálního glyfu, měřené od ascent po descent v user space. Mezera se nepřidává, když je některá strana už prázdná, a ani mezi dvěma CJK znaky, protože zarovnávání roztahuje ideografy bez toho, aby tahle rozteč znamenala hranici slova. Záznamy glyfů vystavují tutéž geometrii, takže rozhodnutí dokážete zreprodukovat, když vás nějaký soubor zaskočí
uses
SysUtils, HPDFDoc, HPDFContentStream;
procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
Glyphs: THPDFGlyphArray;
I: Integer;
Height, Gap: Double;
begin
if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
Exit;
for I := 1 to High(Glyphs) do
begin
// výška krabice glyfu od ascent po descent, v user space
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// vodorovný text: mezera od konce vlastní šířky předchozího glyfu
Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
if (Height > 0) and (Gap > 0.15 * Height) then
Writeln(Format('U+%.4x gap %.2f height %.2f: space',
[Glyphs[I].Unicode, Gap, Height]));
end;
end;
Proč měřit od vlastní šířky glyfu a ne od pozice pera?
HotPDF měří mezery slov od GlyphEndX / GlyphEndY, protože pozice pera za glyfem už obsahuje rozestupy, které mezera nejsou. ISO 32000-1 §9.4.4 definuje vodorovný posun jako šířku glyfu krát velikost fontu, plus character spacing Tc, plus word spacing Tw, všechno škálované přes Tz. BaselineEndX / BaselineEndY drží celý tenhle posun, zatímco GlyphEndX / GlyphEndY jen fontový advance a Tz. Rozdíl hraje roli u producerů, kteří dotáhnou tracking záporným Tc a pak prostor vrátí korekcí TJ po každém glyfu: měřeno od pozice pera vypadá ta návratka jako mezera a čínský termín „95后“ se extrahoval jako „9 5 后“. Práh se váže na výšku krabice glyfu, ne na velikost Tf, ze stejného důvodu. Word exporty často píšou 1 Tf a reálnou velikost nesou ve škálovaném Tm, takže Tfs hlásí 1, zatímco text je vysoký 10 bodů, a pravidlo klíčované na Tfs by se ke dvěma hláskováním téže stránky chovalo rozdílně
Pravidlo má poctivé okraje. Nadpis sazený velmi volným trackingem, kde samotné Tc otevře mezi písmeny víc než 0.15 výšky textu, se extrahuje s mezerou za každým písmenem — přesně tak, jak stránka vypadá, ale nejspíš ne to, co chcete indexovat. Kusy nakreslené v nesprávném pořadí na jedné baseline dají zápornou mezeru a spojí se bez mezery. Ani jeden případ není v těle textu běžný a na testovacím korpusu změna navýšila shody slov proti referenčnímu extraktoru na 28 stránkách, aniž by některou snížila
Kdy HotPDF zahájí v extrahovaném textu nový řádek?
Od v2.766.79 začíná nový řádek, když pohyb z počátku předchozího glyfu k aktuálnímu, promítnutý na normálu předchozího směru psaní, přesáhne polovinu větší ze dvou výšek krabic obou glyfů. Dřívější pravidlo srovnávalo surový Y pohyb s polovinou Tfs, což selhávalo dvěma směry. S 1 Tf a škálovaným Tm se práh smrskl na půl jednotky, takže horní index zdvižený text rise 0.4 nebo obyčejné chvění baseline zlomil řádek. Pravidlo navíc ignorovalo X úplně, takže text pod otočeným Tm ustupoval stránkou s každým glyfem a vyšel po jednom glyfu na řádek. Promítnutí na normálu směru činí otočené běhy chovanými jako vodorovné a brání-li se větší ze dvou výšek, zůstane velké vzorové slovo a jeho malý popisek na jednom řádku, když sdílejí baseline. U daňového formuláře zmíněného výše klesl počet řádků ze 156 na 97. Svislý text v režimu psaní 1 (§9.7.4.3) jde samostatnou cestou: tyhle glyfy se seskupují do sloupců, čtou zprava doleva a shora dolů, se zlomem řádku při každé změně sloupce
Který text ExtractLoadedPageText zahrne a který nechá stranou?
ExtractLoadedPageText vrací text, který prohlížeč ukazuje. Od v2.766.80 pracuje jen z viditelných glyfů a zahazuje každý glyf, jehož střed krabice spadne mimo GetLoadedPageVisibleBox, tedy CropBox oříznutý na MediaBox (§14.11.2). Tím zmizí slug řádky a jiné tiskařské značky sazené jako text mimo ořezovou oblast. ExtractLoadedPageGlyphs se záměrně pořád vrací s každým glyfem content streamu stránky, takže tenhle materiál najdete, když ho potřebujete. Filtr je test krabice, ne test viditelnosti: text schovaný ořezovou cestou, malovaný bíle nebo překrytý obrázkem se pořád extrahuje
var
Pdf: THotPDF;
Glyphs: THPDFGlyphArray;
PageText: UnicodeString;
L, B, R, T: Single;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('trimmed-proof.pdf');
if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
// každý glyf content streamu stránky, slug řádek nevyjímaje
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// jen to, co stránka ukazuje, se vpleteným textem Form XObjectů
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
Text malovaný přes Form XObjects je součástí textu stránky od v2.768.3. Hlavičky, razítka a vodoznaky bydlí velmi často ve formách a některé standaryzované dokumenty před změnou ztrácely 30 až 35 procent znaků. THotPDF.InterpretContentWithForms si zapisuje každé Do spolu s platnou CTM, interpretuje formu na její /Matrix krát ta CTM (§8.10.1) a vplete glyfy formy na pozici Do, s rekurzí do vnořených forem. Forma bez vlastních /Resources si půjčí ty od streamu, který ji maluje, jak dovoluje §7.8.3. Formové glyfy nesou TokenIndex = -1 a ExtractLoadedPageGlyphs pořád vrací jen glyfy page streamu, protože hledání, nahrazování a redakce zapisují změny zpět přes TokenIndex a upravily by špatné bajty, kdyby se mezi ně vloudil formový glyf. Dvě zjednodušení stojí za znát: text formy se neořezává na její /BBox a rekurze končí na 12 úrovních místo detekce cyklů, takže poškozená forma, která maluje sama sebe, opakuje svůj text, dokud nenarazí na tenhle strop
Proč se text za operátorem Q dekódoval jako nesmysl?
Text za Q se mohl před v2.766.73 dekódovat špatně, protože extraktor si na q ukládal jen CTM. Parametry textového stavu — font, velikost, Tc, Tw, Tz, TL, režim vykreslování a zdvih — patří do grafického stavu (§9.3.1), takže Q je musí obnovit spolu se vším ostatním na zásobníku (§8.4.2). Jeden oborový report vybral uvnitř q … Q dvoubajtový font Identity-H a pak ukazoval jednobajtový text WinAnsi bez vlastního Tf. Extraktor si nechal vnitřní font, četl úvodní tečky a slovo „Adobe“ v obsahu jako dvoubajtové kódy a ztratil 15 % znaků stránky. Zásobník q/Q interpretu teď drží celý textový stav. Extrahovací pravidla popsaná tady platí pro každou stránku, takže celý dokument jde do souboru jedním voláním
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// prázdný rozsah = všechny stránky; form feed mezi stránkami; UTF-8 BOM
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
Které textové API HotPDF máte použít?
ExtractLoadedPageText zůstává v pořadí content streamu, což je správný default pro hledání a indexaci; dekódovací řetěz pod ním popisuje extrakce textu z načtených PDF s HotPDF. Pro tagované dokumenty, kde záleží na pořadí autorství, jde extrakce textu v pořadí struktury po strukturovém stromě místo hádání z geometrie a pro data zavřená v tabulkách vrátí typovaná extrakce tabulek přes zlomy stránek buňky místo řádků. Kompletní API reference a trial ke stažení jsou na stránce produktu HotPDF Delphi PDF Component