Technický článek

Oprava duplicitního textu z fake bold při extrakci PDF

HotPDF Delphi Component odstraňuje od verze v2.768.15 duplicitní znaky, které do extrahovaného textu PDF zanechává fake bold a potištěná loga. Když producent namaluje tentýž glyph dvakrát zlomek bodu od sebe, ExtractLoadedPageText, ExtractLoadedPageTextLayout a ExtractLoadedPageTextColumns ten znak teď přečtou jednou, takže „Bold words“ už nevyleze jako „Bold wordsBold words“

Tohle je jedna z těch chyb, které vypadají jako selhání dekodéru a nejsou. Každý glyph se dekóduje správně; text tam doopravdy je dvakrát. Oprava musí rozhodnout, který ze dvou naprosto platných glyphů je tou kopií, a udělat to, aniž by spolkla legitimně opakovaná písmena jako dvojité l v „fill“ nebo identické buňky tabulky. Zbytek článku vysvětluje pravidlo, které HotPDF používá, prahy za ním, kde se záměrně liší od jiných extrakčních nástrojů, a druhou, nesouvisející extrakční chybu ze stejné vydavatské řady, která taky usekla text na koncích řádků

Proč fake bold při extrakci PDF text zdvojuje?

Fake bold zdvojuje extrahovaný text, protože producent string nakreslí dvakrát a obě kopie jsou skutečné operace zobrazení textu. Když rodina fontů nemá bold řez, nebo se autoring nástroj prostě nesnaží, mnoho generátorů emuluje bold tak, že tentýž string zobrazí přes Tj nebo TJ, posune textovou pozici doprava o pár desetin bodu a zobrazí ho znovu. Na obrazovce se dvě kopie slijí do tlustšího tahu. V content streamu jsou dvě kompletní běhy znakových kódů a ISO 32000-1 §9.4 nedává extraktoru žádný příznak typu „tahle je jen dekorace“

Potištěná loga trefí týž problém z jiné strany. Logotyp se může nakreslit jednou jako fill a podruhé jako stroke nebo mírně posunutý stín, se stejným fontem a stejnými znaky. Naivní extraktor, který jde content streamem v malovacím pořadí, vypustí celý první běh, pak celý druhý, proto výsledek čte „Bold wordsBold words“ a ne „BBoolldd“. Před v2.768.15 držel text assembly v HotPDF obě kopie a duplicita rozbila to, na čem většině volajících doopravdy záleží: hledání „Bold words“ v textu stránky mohlo selhat, protože hranice slova mezi oběma běhy zmizela

Skutečný text rendering mode 2 (fill pak stroke, ISO 32000-1 §9.3.6) tenhle problém nemá, protože je to jedna operace zobrazení textu, která vyprodukuje jednu sadu glyphů. Duplicity se objeví jen tehdy, když producent text vystaví dvakrát

Jak HotPDF rozhoduje, který glyph je tou duplicitou?

HotPDF bere dva glyphy jako jeden namalovaný znak, když nesou stejnou Unicode hodnotu, mají stejný směr psaní, ve velikosti se liší do 5 % a sedí téměř přes sebe: podél baseline nejvýš 0,1 velikosti glyphu a napříč nejvýš 0,2 velikosti glyphu. Z každé takové dvojice se zachová dřív namalovaný glyph a pozdější se zahodí. Tohle jsou hodnoty implementace v2.768.15 a každá tam je z nějakého důvodu

  • Stejná Unicode, stejný směr. Duplicity mohou být jen identické znaky a vodorovný glyph se nikdy nesrovnává se svislým (WMode 1)
  • Velikost do 5 %. Velké písmeno nadpisu a malé písmeno popisku, které náhodou překryjí, nejsou týž text, takže nesouhlas velikosti dvojici vyřadí
  • 0,1 velikosti podél baseline. Odsady fake bold jsou pár desetin bodu, pár procent běžné velikosti textu. Dvě skutečné sousední písmena, i pevně kernované „ll“, dělí zhruba celá advance width — řádově víc, než činí tahle tolerance
  • 0,2 velikosti napříč baseline. Stíny a outline průchody se často posunou i dolů, ne jen doprava, takže svislá tolerance je volnější, ale pořád daleko pod vzdáleností k dalšímu řádku
  • Vyhrává dřív namalovaný. Zachování dřívějšího glyphu zachová pořadí čtení prvního běhu, takže přeživší text teče tak, jak ho producent napsal

Vzdálenosti se měří ve vlastním baseline rámu glyphu, ne v syrových X a Y stránky, takže otočený text a text na šikmé baseline používají stejné prahy jako obyčejné vodorovné řádky. Velikostí se rozumí velikost glyphu, jak je namalovaná na stránce, odvozená z jeho quadu, ne operand velikosti zadaný do Tf; na tom záleží u dokumentů, které nastaví Tf na 1 a všechno škálují přes textovou matici, kde syrová velikost fontu by učinila každý práh k ničemu

Mezery nikdy nejsou kandidáti. Znakové body do U+0020 včetně, nezalomitelná mezera U+00A0 a ideografická mezera U+3000 se přeskočí, protože prázdné glyphy nemají žádnou viditelnou barvu, kterou by šlo zdvojit, a zahodit jeden by zlepil slova k sobě

Udržení rychlé kontroly na hustých stránkách

Kontrola duplicit v HotPDF běží zhruba v čase n log n, ne n na druhou, protože nejdřív seřadí a porovnává jen glyphy v malém okně. Srovnávat každý glyph s každým by bylo správné a nepoužitelné: hustá stránka drobného tisku klidně nese desetitisíce glyphů a kontrola běží u každého volání extrakce textu

Interní rutina seřadí glyphy bez mezer podle Unicode hodnoty, pak podle počátku X, pak podle malovacího pořadí, pomocí stabilního merge sortu. Po seřazení sedí každý potenciální duplicita glyphu hned za ním v pořadí, takže sken jde jen dopředu, dokud se Unicode hodnota shoduje a rozdíl X zůstává do 0,25 velikosti glyphu. Okno 0,25 je záměrně širší než tolerance 0,1 podél baseline, takže dvojice na mírně šikmé baseline spadne dovnitř, ještě než se spustí přesný test v baseline rámu. Jeden jemný případ: pokud sken od glyphu A potká shodný glyph namalovaný dřív než A, kopiem je A, takže se A zahodí a sken se zastaví a dřívější glyph pokračuje v srovnávání s čímkoli dalším. Tohle pravidlo je to, co drží „vyhrává dřív namalovaný“ u trojitých úderů stejně jako u dvojic

Srovnává se Unicode hodnota, ne font nebo znakový kód. Dva glyphy mapované na tutéž Unicode přes různé fonty, třeba fill průchod v jednom vloženém subsetu a stroke průchod v jiném, se pořád poznají jako jeden znak. Opačná strana mince je, že glyph, jehož kód se rozloží na Unicode 0, nemá k porovnání nic spolehlivého, což je další důvod, proč hlídat počet nerozřešených, jemuž se věnuje extrakce textu z načtených PDF pomocí HotPDF

Která extrakční volání HotPDF odstraňují duplicitní glyphy?

Všechna volání produkující text v HotPDF duplicity odstraňují a surové volání glyphů ne. Deduplikace bydlí v THotPDF.ExtractVisibleLoadedPageGlyphs, kroku, který taky zahazuje glyphy mimo viditelný box stránky, a extrakce prostého textu, sloupců, layoutu a sémantického textu čtou své glyphy všechny z ní. THotPDF.ExtractLoadedPageGlyphs pořád vrací každý glyph, který content stream namaluje, včetně overprintů, protože volající, kteří staví zvýraznění, redakci nebo forenzní nářadí, potřebují kompletní záznam o tom, co bylo nakresleno

Pro obyčejnou práci s textem se ve volajícím kódu nic nemění. Táž smyčka, která před v2.768.15 extrahovala zdvojený text, teď extrahuje každé slovo jednou

var
  Pdf: THotPDF;
  PageCount, I: Integer;
  PageText: UnicodeString;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('brochure.pdf');
    for I := 0 to PageCount - 1 do
      if Pdf.ExtractLoadedPageText(I, PageText) then
        if Pos('Bold words', PageText) > 0 then
          Writeln('Found on page ', I + 1);
  finally
    Pdf.Free;
  end;
end;

Výstup layoutu a sloupců těží stejně, protože zdvojený běh strkal zbytek řádku do boku na znakové mřížce. ExtractLoadedPageTextColumns si drží volitelný multiplátor mezery; filtr duplicit běží před dělením na sloupce, takže s tím nastavením neinteraguje

var
  Grid, Columns: UnicodeString;
begin
  if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
    TFile.WriteAllText('page1-layout.txt', Grid, TEncoding.UTF8);
  // Sloupce se dělí tam, kde mezera přesahuje 3x medián advance glyphu
  if Pdf.ExtractLoadedPageTextColumns(0, Columns, 3.0) then
    TFile.WriteAllText('page1-columns.txt', Columns, TEncoding.UTF8);
end;

Pokud chcete vědět, jestli stránka vůbec obsahuje potištěný text, srovnejte obě volání glyphů. Počítejte s tím, že rozdíl počítá i glyphy, které spadnou mimo crop box, takže to berte jako horní mez pro duplicity, ne přesné číslo

var
  AllGlyphs, Visible: THPDFGlyphArray;
begin
  if Pdf.ExtractLoadedPageGlyphs(0, AllGlyphs) and
     Pdf.ExtractVisibleLoadedPageGlyphs(0, Visible) then
    Writeln(Format('%d painted, %d read as text (%d overprinted or off-page)',
      [Length(AllGlyphs), Length(Visible),
       Length(AllGlyphs) - Length(Visible)]));
end;

Pravidla mezer mezi slovy a lámání řádků, která z těchto glyphů dělají text, filtr nemění; podrobně je rozebírá jak HotPDF umisťuje mezery mezi slovy a konce řádků v textu stránky

Kde se pravidlo záměrně rozchází s jinými nástroji

HotPDF zahazuje úplné duplicity, které některé jiné extrakční nástroje drží, a ten rozdíl je záměrný. Když se změna v2.768.15 kontrolovala proti referenčnímu korpusu, jedna patička stránky namalovaná dvakrát na úplně stejné pozici se extrahovala jednou v HotPDF a dvakrát v referenčním extraktoru. Glyph namalovaný na identický glyph je jako samostatný znak neviditelný, takže přečíst ho jednou je to, co stránka doopravdy ukazuje

Poctivá mez běží opačným směrem. Fake bold, který posune druhý průchod o víc než 0,1 velikosti glyphu podél baseline, se nechytí a ani stínový průchod nakreslený v nápadně jiné velikosti. Taková uspořádání jsou v praxi vzácná, protože vizuální efekt přestává vypadat jako bold, jakmile je odsada tak velká, ale pokud vidíte zdvojený text z dekorativního nadpisu, srovnejte hodnoty X glyphů z ExtractLoadedPageGlyphs, než usoudíte, že je filtr rozbitý. Filtr taky nedělá nic pro duplicity, které nejsou geometricky na stejném místě, jako běžící hlavička zopakovaná ve skryté vrstvě jinde na stránce; to je otázka obsahu, ne overprintu

Proč se v textu CID fontů usekávaly konce řádků?

Samostatná chyba opravená v HotPDF v2.768.12 zahazovala konce řádků v textu sázeném CID fonty (Type 0), jejichž pole šířek /W nebylo seřazené podle CID. ISO 32000-1 §9.7.4.3 definuje formát pole /W, ale nevyžaduje, aby jeho položky byly ve vzestupném pořadí CID, a některé generátory je zapisují v pořadí, v jakém se glyphy v dokumentu poprvé použily

Dekodér extrakce textu hledal šířky binárním vyhledáváním, které potichu předpokládá seřazený vstup. Na neseřazeném poli většina hledání minula a spadla zpět na výchozí šířku /DW, která je 1000 jednotek, celé em. Latinský text v takovém fontu se proto sázel, jako by každé písmeno bylo plné šířky, řádky běžely daleko za pravý okraj stránky a glyphy, které spadly mimo viditelný box, se zahodily, takže konce řádků zmizely z extrahovaného textu i z hledání. Nic nespadlo a nic se nenahlásilo, a právě proto se to hůř odhalovalo

HotPDF teď seřadí položky /W stabilním merge sortem hned po přečtení, takže tam, kde se dvě položky překrývají, stále vyhrává dřív zapsaná, což odpovídá tomu, jak pole čte v pořadí souboru. Renderér stránek nebyl nikdy postižený, protože si drží vlastní kopii dat o šířkách a skenuje ji lineárně a svislé pole šířek /W2 se taky skenuje lineárně. Na tohle rozdělení stojí za to vzpomínat, když stránka vypadá dokonale a extrahuje se špatně: vykreslování a extrakce sdílí v PDF knihovně často mnohem méně kódu, než byste čekali

FAQ

Odstraňuje HotPDF opakovaná písmena jako dvojité l v „fill“?

Ne. Dvě skutečné sousední písmena dělí zhruba jedna advance width, zatímco HotPDF spojuje jen identické znaky, které jsou do 0,1 velikosti glyphu podél baseline a 0,2 napříč ní, takže legitimní opakování zůstávají

Jak dostanu každý glyph, včetně potištěných duplicit?

Zavolejte THotPDF.ExtractLoadedPageGlyphs, které vrací každý glyph, který stránka maluje, v malovacím pořadí. THotPDF.ExtractVisibleLoadedPageGlyphs a všechna volání extrakce textu vracejí deduplikovanou, viditelnou sadu

Která verze HotPDF opravuje zdvojený text z fake bold?

HotPDF v2.768.15 přidal odstraňování duplicitních glyphů do extrakce textu, layoutu, sloupců a sémantického textu a v2.768.12 opravila chybějící konce řádků u CID fontů s neseřazeným polem /W

Obě opravy dodává aktuální HotPDF Delphi PDF component, která extrahuje text z načtených PDF v Delphi a C++Builder vedle svých funkcí pro tvorbu, úpravu a vykreslování PDF