Odborný článok

Extrakcia PDF textu v Delphi: medzery a konce riadkov

HotPDF Delphi Component skladá medzery medzi slovami a konce riadkov v THotPDF.ExtractLoadedPageText z glyfovej geometrie, nie zo znakov medzery. Medzera sa vloží, keď medzera za vlastnou šírkou glyfu presiahne 0,15 výšky textu, a nový riadok sa začne len vtedy, keď sa pôvod textu posunie naprieč smerom písania o viac než polovicu výšky textu. Od v2.768.3 text strany zahrňuje aj text namaľovaný cez Form XObjects a vynechá glyfy mimo viditeľnej crop oblasti. Zvyšok článku vysvetľuje, prečo každé pravidlo vyzerá tak, ako vyzerá, lebo každé z nich nahradilo jednoduchšie pravidlo, ktoré na reálnych dokumentoch vyrábalo vierohodný, ale zlý výstup

Príznaky pozná každý, kto kedy pustil PDF text do vyhľadávacieho indexu. Obálka sa vyextrahuje ako PDFReferenceManualNovember4,1998, daňový formulár sa rozpadne na 156 riadkov, diagonálny vodoznak dorazí znak na riadok a orezaná tlačová predloha vedie riadkom pre tlačiareň, ktorý žiadny prehliadač nikdy neukáže. Žiadny z týchto súborov nie je pokazený. Každý používa úplne legálny spôsob umiestňovania textu, ktorý naivný extraktor prečíta zle

Prečo extrahovaný PDF text príde o medzery medzi slovami?

Extrahovaný text príde o medzery medzi slovami, lebo PDF ich nikdy obsahovať nemusí. Producent môže slová oddeliť zobrazením znaku medzery, ale rovnako dobre môže pohnúť perom číslom vnútri poľa TJ (ISO 32000-1 §9.4.3) alebo čerstvým Td (§9.4.2) a výstup TeXu, mnohé Distiller súbory aj väčšina zarovnaných rozložení robia presne to. Pred v2.766.76 sa HPDFAssemblePageText pozeral len na vertikálny pohyb, takže zlom slova urobený pozicovaním jednoducho zmizol. Assembler teraz meria v smere písania predchádzajúceho glyfu vzdialenosť od konca jeho vlastnej šírky po pôvod aktuálneho glyfu a vloží jednu medzeru, keď vzdialenosť presiahne 0,15 výšky boxu aktuálneho glyfu, meranej od ascent po descent v user space. Medzera sa nepridá, keď je niektorá strana už prázdna, a žiadna medzi dvoma CJK znakmi, lebo zarovnávanie rozťahuje ideografy bez toho, aby ten rozťah znamenal hranicu slova. Glyfové záznamy vystavujú tú istú geometriu, takže rozhodnutie dokážete reprodukovať, keď vás konkrétny súbor zarazí

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 glyfového boxu 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]));
    // horizontálny text: medzera od konca vlastnej šírky predchádzajúceho 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;

Prečo merať od vlastnej šírky glyfu namiesto pozície pera?

HotPDF meria medzery medzi slovami od GlyphEndX / GlyphEndY, lebo pozícia pera po glyfe už obsahuje rozostupy, ktoré nie sú medzerou. ISO 32000-1 §9.4.4 definuje horizontálne posunutie ako šírku glyfu krát veľkosť fontu, plus character spacing Tc, plus word spacing Tw, všetko škálované Tz. BaselineEndX / BaselineEndY držia to celé posunutie, zatiaľ čo GlyphEndX / GlyphEndY držia len font advance a Tz. Rozdiel je podstatný pre producentov, ktorí stiahnu tracking záporným Tc a potom priestor vrátia úpravou TJ po každom glyfe: merané z pozície pera vyzerá to vrátenie ako medzera a čínsky termín “95后” sa vyextrahoval ako “9 5 后”. Prah je viazaný na výšku glyfového boxu namiesto veľkosti Tf z podobného dôvodu. Word exporty často zapisujú 1 Tf a skutočnú veľkosť nesú v škálovanom Tm, takže Tfs hovorí 1, kým text je vysoký 10 bodov, a pravidlo zavesené na Tfs by zaobchádzalo s oboma hláskovaniami tej istej strany odlišne

Pravidlo medzier medzi slovami v ExtractLoadedPageText v HotPDF v Delphi: medzera sa vloží len vtedy, keď vzdialenosť od GlyphEndX predchádzajúceho glyfu po BaselineStartX nasledujúceho presiahne 0,15 výšky boxu od ascent po descent, lebo pozícia pera v BaselineEndX už obsahuje Tc, Tw a Tz a mení vrátenia zarovnaného trackingu na falošné medzery ako 9 5 后
O tom, kde sa lámia slová, rozhoduje geometria, nie znaky medzery — glyfové záznamy vystavujú tie isté merania, takže rozhodnutie zopakujete pre akýkoľvek záhadný súbor

Pravidlo má poctivé okraje. Nadpis zložený s veľmi voľným trackingom, kde Tc samo otvorí medzi písmenami viac než 0,15 výšky textu, sa vyextrahuje s medzerou medzi každým písmenom, čo je to, ako strana vyzerá, ale pravdepodobne nie to, čo ste chceli indexovať. Časti nakreslené neusporiadane na jednom baseline vytvoria zápornú medzeru a spoja sa bez medzery. Ani jeden prípad nie je v tele textu bežný a na testovacom korpuse zmena zvýšila zhody slov oproti referenčnému extraktoru na 28 stranách, ani jednu neznížila

Kedy HotPDF začína v extrahovanom texte nový riadok?

Od v2.766.79 sa nový riadok začína, keď posun od pôvodu predchádzajúceho glyfu k aktuálnemu, premietnutý na normálu predchádzajúceho smeru písania, presiahne polovicu väčšej výšky boxu z oboch glyfov. Skoršie pravidlo porovnávalo surový Y posun s polovicou Tfs, čo zlyhalo v dvoch smeroch. S 1 Tf a škálovaným Tm sa prah scvrkol na polovicu jednotky, takže horný index zdvihnutý text rise 0,4 alebo obyčajný baseline jitter zlámal riadok. Pravidlo tiež úplne ignorovalo X, takže text pod otočeným Tm schádzal po strane s každým glyfom a vyšiel glyf na riadok. Premietnutie na normálu smeru spraví z otočených behov správanie ako z horizontálnych a väčšia z oboch výšok udrží veľké vzorové slovo aj jeho malý popis na jednom riadku, keď zdieľajú baseline. Na spomínanom daňovom formulári klesol počet riadkov zo 156 na 97. Vertikálny text v writing mode 1 (§9.7.4.3) ide po samostatnej ceste: tie glyfy sa zoskupujú do stĺpcov, čítajú sprava doľava a zhora nadol, so zlomom riadka pri každej zmene stĺpca

Ako rozhoduje ExtractLoadedPageText v HotPDF o zlomoch riadkov v Delphi: posun medzi pôvodmi glyfov sa premietne na normálu smeru písania a porovná s polovicou väčšej výšky boxu, takže horný index zdvihnutý malým text rise pod fontom 1 Tf ani text schádzajúci po strane pod otočeným Tm sa už nerozpadne na glyf na riadok
Premietnutie spraví z otočených behov správanie ako z horizontálnych a väčšia z oboch výšok boxov udrží veľké vzorové slovo aj jeho malý popis na jednom riadku

Ktorý text ExtractLoadedPageText zahrnie a ktorý vynechá?

ExtractLoadedPageText vráti text, ktorý ukazuje prehliadač. Od v2.766.80 pracuje len z viditeľných glyfov a zahodí každý glyf, ktorého stred boxu padá mimo GetLoadedPageVisibleBox, čo je CropBox orezaný na MediaBox (§14.11.2). Tým zmiznú riadky pre tlačiareň a ďalšie tlačové značky uložené ako text mimo orezovej oblasti. ExtractLoadedPageGlyphs zámierne naďalej vracia každý glyf content streamu strany, takže ten materiál nájdete aj vtedy, keď ho potrebujete. Filter je testom boxu, nie testom viditeľnosti: text skrytý za clipping path, namaľovaný bielou alebo prekrytý obrázkom sa vyextrahuje tiež

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 strany, riadok pre tlačiareň vrátane
    if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
      Writeln(Length(Glyphs), ' glyphs in the page content stream');
    // len to, čo strana ukazuje, s vpleteným textom Form XObjects
    if Pdf.ExtractLoadedPageText(0, PageText) then
      Writeln(PageText);
  finally
    Pdf.Free;
  end;
end;

Text namaľovaný cez Form XObjects je súčasťou textu strany od v2.768.3. Hlavičky, pečiatky a vodoznaky bývajú veľmi často vo formách a niektoré dokumenty noriem stratili pred zmenou 30 až 35 percent svojich znakov. THotPDF.InterpretContentWithForms si zaznamená každý Do spolu s platným CTM, interpretuje formu pri jej /Matrix krát to CTM (§8.10.1) a vpletie glyfy formy na pozíciu Do a rekurzívne zostupí do vnorených foriem. Forma bez vlastných /Resources si požičia tie streamu, ktorý ju maľuje, ako dovoľuje §7.8.3. Glyfy foriem nesú TokenIndex = -1 a ExtractLoadedPageGlyphs stále vracia len glyfy page streamu, lebo hľadanie, nahradzovanie a redakcia zapisujú zmeny späť cez TokenIndex a upravili by zlé bajty, keby sa medzi ne vpletol glyf formy. Dve zjednodušenia stoja za poznanie: text formy sa neorezáva na jej /BBox a rekurzia sa zastaví na 12 úrovniach namiesto detekcie cyklov, takže deformovaná forma, ktorá maľuje seba, opakuje svoj text, kým dospeje k tomu stropu

Ktoré glyfy zahrnie HotPDF pri extrakcii textu PDF strany v Delphi: ExtractLoadedPageText drží len glyfy, ktorých stred boxu padá vnútri GetLoadedPageVisibleBox, tedy CropBox orezaný na MediaBox, takže riadky pre tlačiareň zmiznú, zatiaľ čo InterpretContentWithForms vpletie glyfy Form XObject na pozíciu každého Do s TokenIndex nastaveným na -1 a glyfové API stále vracia všetko
Test boxu na strede glyfu nie je testom viditeľnosti — biely text, orezaný text aj prekrytý text stále vychádza a text foriem sa počíta od v2.768.3

Prečo sa text po operátore Q dekódoval ako smetie?

Text po Q mohol pred v2.766.73 dekódovať zle, lebo si extraktor pri q uložil len CTM. Parametre textového stavu, teda font, veľkosť, Tc, Tw, Tz, TL, renderovací režim aj rise, patria do grafického stavu (§9.3.1), takže Q ich musí obnoviť spolu so všetkým ostatným na zásobníku (§8.4.2). Jeden priemyselný report vybral dvojbajtový font Identity-H vnútri q … Q a potom ukazoval jednobajtový text WinAnsi bez vlastného Tf. Extraktor si nechal vnútorný font, prečítal bodky riadiace obsah a slovo “Adobe” v obsahu ako dvojbajtové kódy a prišiel o 15 % znakov strany. Zásobník q/Q v interpretri teraz drží celý textový stav. Pravidlá extrakcie popísané tu platia pre každú stranu, takže celý dokument môže ísť do súboru na jedno volanie

var
  Output: TFileStream;
  Pages: Integer;
begin
  Output := TFileStream.Create('report.txt', fmCreate);
  try
    // prázdny rozsah = všetky strany; form feed medzi stranami; UTF-8 BOM
    Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
    Writeln(Pages, ' pages extracted');
  finally
    Output.Free;
  end;
end;

Ktoré textové API HotPDF máte použiť?

ExtractLoadedPageText ostáva v poradí content streamu, čo je správna predvoľba pre hľadanie a indexovanie; dekódovací reťaz pod ňou popisuje článok o extrakcii textu z načítaných PDF s HotPDF. Pre tagované dokumenty, kde záleží na poradí autorovania, prejde extrakcia textu v poradí štruktúry štruktúrovým stromom namiesto hádania z geometrie a pre dáta zamknuté v tabuľkách vráti typovaná extrakcia tabuliek cez zlomy strán bunky namiesto riadkov. Úplná API referencia aj skúšobná verzia sú na produktovej stránke HotPDF Delphi PDF Component