Tehnički članak

PDF tekst u Delphiju: razmaci riječi i prijelomi redaka

HotPDF Delphi Component obnavlja razmake riječi i prijelome redaka u THotPDF.ExtractLoadedPageText iz geometrije glifova, ne iz znakova razmaka. Razmak ulazi kad procijep iza vlastite širine glifa prijeđe 0.15 visine teksta, a novi redak kreće tek kad se ishodište teksta pomakne preko smjera pisanja za više od pola visine teksta. Od v2.768.3 tekst stranice uključuje i tekst iscrtan kroz Form XObjecte i ostavlja van glifove izvan vidljivog crop područja. Ostatak ovog teksta objašnjava zašto svako pravilo izgleda onako kako izgleda, jer je svako od njih zamijenilo jednostavnije pravilo koje je na stvarnim dokumentima davalo vjerodostojan ali pogrešan izlaz

Simptomi su poznati svakomu tko je PDF tekst ubacio u tražilicu. Naslovnica se izvuče kao PDFReferenceManualNovember4,1998, poreski obrazac se raspadne u 156 redaka, dijagonalni vodeni žig stigne jedan znak po retku, a obrezani probni otisak kreće sa slug linijom tiskara koju nijedan preglednik nikad ne prikaže. Nijedna od tih datoteka nije pokvarena. Svaka koristi savršeno legalan način postavljanja teksta koji naivni extractor pogrešno čita

Zašto izvučeni PDF tekst gubi razmake između riječi?

Izvučeni tekst gubi razmake riječi jer PDF nikad nije dužan sadržavati ih. Proizvođač može razdvojiti riječi prikazujući znak razmaka, ali perom može pomaknuti jednako dobro brojem unutar polja TJ (ISO 32000-1 §9.4.3) ili svježim Tdom (§9.4.2), i to točno čine TeX izlaz, mnoge Distiller datoteke i većina razvučenih postava. Prije v2.766.76 HPDFAssemblePageText gledao je samo okomito kretanje, pa je prijelom riječi učinjen pozicioniranjem jednostavno nestao. Assembler sada mjeri, duž smjera pisanja prethodnog glifa, udaljenost od kraja vlastite širine tog glifa do ishodišta trenutnog glifa, i ubaci jedan razmak kad udaljenost prijeđe 0.15 visine kutije trenutnog glifa, mjereno od ascenta do descenta u user spaceu. Razmak se ne dodaje kad je bila koja strana već prazna, ni između dva CJK znaka, jer razvlačenje razvlazi ideograme bez da to razvlačenje znači granicu riječi. Zapisi glifova izlažu istu geometriju, pa odluku možete reproducirati kad vas neka datoteka muči

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
    // visina kutije glifa od ascenta do descenta, u user spaceu
    Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
      Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
    // horizontalni tekst: procijep od kraja vlastite širine prethodnog glifa
    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;

Zašto mjeriti od vlastite širine glifa umjesto od pozicije pera?

HotPDF mjeri procijepe riječi od GlyphEndX / GlyphEndY jer pozicija pera iza glifa već sadrži razmake koji nisu procijep. ISO 32000-1 §9.4.4 definira vodoravni pomak kao širinu glifa puta veličinu fonta, plus razmak znakova Tc, plus razmak riječi Tw, sve skalirano s Tz. BaselineEndX / BaselineEndY nose taj puni pomak, dok GlyphEndX / GlyphEndY nose samo fontovski advance i Tz. Razlika je bitna za proizvođače koji stegnu tracking negativnim Tcom, a onda prostor vrate TJ prilagodbom iza svakog glifa: mjereno od pozicije pera, vraćeni prostor izgleda kao procijep, i kineski je izraz “95后” izvučen kao “9 5 后”. Prag je vezan uz visinu kutije glifa, a ne uz veličinu Tfa iz sličnog razloga. Word izvozi često zapišu 1 Tf i pravu veličinu nose u skaliranoj Tm, pa Tfs kaže 1 dok je tekst visok 10 točaka, i pravilo vezano uz Tfs tretiralo bi dva zapisa iste stranice različito

Pravilo razmaka riječi HotPDF-a za ExtractLoadedPageText u Delphiju: razmak se ubacuje samo kad udaljenost od GlyphEndX prethodnog glifa do BaselineStartX sljedećeg prijeđe 0.15 visine kutije od ascenta do descenta, jer pozicija pera u BaselineEndX već sadrži Tc, Tw i Tz i pretvara vraćene prostore razvučenog trackinga u lažne procjepe poput 9 5 后
Geometrija, ne znakovi razmaka, odlučuje gdje se riječi lome — zapisi glifova izlažu ista mjerenja, pa odluku možete ponoviti za bilo koju čudnu datoteku

Pravilo ima poštene rubove. Naslov posložen vrlo labavim trackingom, gdje sam Tc otvori više od 0.15 visine teksta između slova, izvlači se s razmakom između svakog slova, što je ono kako stranica izgleda ali vjerojatno nije ono što ste željeli indeksirati. Komadi nacrtani izvan reda na jednoj baselini daju negativan procijep i spoje se bez razmaka. Nijedan slučaj nije čest u tjelesnom tekstu, a na testnom korpusu je izmjena podigla poklapanja riječi prema referentnom extractoru na 28 stranica, bez da ijedno spustila

Kad HotPDF počinje novi redak u izvučenom tekstu?

Od v2.766.79 novi redak kreće kad pomak od ishodišta prethodnog glifa do trenutnog, projiciran na normalu prethodnog smjera pisanja, prijeđe polovinu veće visine kutije dvaju glifova. Ranije je pravilo uspoređivalo sirovi Y pomak s polovicom Tfsa, što je padalo u dva smjera. S 1 Tfom i skaliranom Tm prag se stegnuo na pola jedinice, pa je superskript podignut text riseom od 0.4 ili obično treperenje baseline slomilo redak. Pravilo je X u cijelosti ignoriralo, pa je tekst pod rotiranom Tm silazio stranicom sa svakim glifom i izlazio jedan glif po retku. Projekcija na normalu smjera čini da se rotirani nizovi ponašaju poput vodoravnih, a uzimanje veće od dviju visina drži veliku uzorčanu riječ i njezin mali natpis u jednom retku kad dijele baseline. Na spomenutom poreskom obrascu broj redaka pao je sa 156 na 97. Okomiti tekst u writing mode 1 (§9.7.4.3) ide zasebnim putem: ti se glifovi grupiraju u stupce, čitaju s desna na lijevo i od gore prema dolje, s prijelomom retka pri svakoj promjeni stupca

Kako ExtractLoadedPageText HotPDF-a odlučuje prijelome redaka u Delphiju: pomak između ishodišta glifova projicira se na normalu smjera pisanja i uspoređuje s polovinom veće visine kutije, pa superskript podignut malim text riseom pod fontom 1 Tf i tekst koji silazi stranicom pod rotiranom Tm više se ne raspadaju u jedan glif po retku
Projekcija čini da se rotirani nizovi ponašaju poput vodoravnih, a uzimanje veće od dviju visina kutija drži veliku uzorčanu riječ i njezin mali natpis u jednom retku

Koji tekst ExtractLoadedPageText uključuje, a koji ostavlja van?

ExtractLoadedPageText vraća tekst koji preglednik prikaže. Od v2.766.80 radi samo s vidljivim glifovima, bacajući svaki glif čije središte kutije padne izvan GetLoadedPageVisibleBoxa, koji je CropBox izrezan na MediaBox (§14.11.2). To uklanja slug linije i ostale tiskarske oznake posložene kao tekst izvan trim područja. ExtractLoadedPageGlyphs namjerno i dalje vraća svaki glif content streama stranice, pa taj materijal možete naći i kad vam zatreba. Filter je test kutije, ne test vidljivosti: tekst skriven clipping pathom, iscrtan u bijelo ili prekriven slikom i dalje se izvlači

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]));
    // svaki glif content streama stranice, uključivo slug liniju
    if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
      Writeln(Length(Glyphs), ' glyphs in the page content stream');
    // samo ono što stranica prikaže, s utkanim tekstom Form XObjecta
    if Pdf.ExtractLoadedPageText(0, PageText) then
      Writeln(PageText);
  finally
    Pdf.Free;
  end;
end;

Tekst iscrtan kroz Form XObjecte dio je teksta stranice od v2.768.3. Zaglavlja, pečati i vodeni žigovi vrlo često žive u formama, i neki su standardski dokumenti prije izmjene izgubili 30 do 35 posto svojih znakova. THotPDF.InterpretContentWithForms zabilježi svaki Do zajedno s CTM-om na snazi, interpretira formu na njezinoj /Matrix pomnoženoj s tim CTM-om (§8.10.1), i utka glifove forme na poziciji Doa, rekurzivno u ugniježđene forme. Forma bez vlastitih /Resources posuđuje one streama koji je crta, kako dopušta §7.8.3. Form glifovi nose TokenIndex = -1, i ExtractLoadedPageGlyphs i dalje vraća samo glifove page streama, jer search, replace i redakcija upisuju promjene natrag kroz TokenIndex i uređivali bi krive bajtove da se form glif ušunjao. Dvije su pojednostavnjenja vrijedna poznavanja: form tekst ne reže se na formin /BBox, a rekurzija staje na 12 razina umjesto detekcijom ciklusa, pa deformirana forma koja crta samu sebe ponavlja svoj tekst dok ne dosegne taj strop

Koji glifovi HotPDF ulaze pri izvlačenju teksta PDF stranice u Delphiju: ExtractLoadedPageText čuva samo glifove čije središte kutije padne unutar GetLoadedPageVisibleBoxa, CropBoxa izrezanog na MediaBox, pa tiskarske slug linije nestaju, dok InterpretContentWithForms utka glifove Form XObjecta na svakoj poziciji Doa s TokenIndexom postavljenim na -1, a API na razini glifova i dalje vraća sve
Test kutije na središtu glifa nije test vidljivosti — bijeli, izrezani i prekriveni tekst i dalje izlaze, a form tekst se računa od v2.768.3

Zašto se tekst iza operatora Q dekodirao u besmislice?

Tekst iza Q mogao se pogrešno dekodirati prije v2.766.73 jer je extractor na q snimao samo CTM. Parametri tekstovnog stanja, dakle font, veličina, Tc, Tw, Tz, TL, način renderiranja i rise, pripadaju grafičkom stanju (§9.3.1), pa Q mora vratiti njih zajedno sa svime ostalim na stacku (§8.4.2). Jedno je industrijsko izvješće unutar q … Q odabralo dvobajtni Identity-H font, pa zatim prikazalo jednobajtni WinAnsi tekst bez vlastitog Tfa. Extractor je zadržao unutarnji font, pročitao vodilje i riječ “Adobe” u sadržaju kao dvobajtne kodove, i bacio 15% znakova stranice. Stack q/Q interpretera sada drži puno tekstovno stanje. Pravila izvlačenja opisana ovdje vrijede za svaku stranicu, pa cijeli dokument može u datoteku jednim pozivom

var
  Output: TFileStream;
  Pages: Integer;
begin
  Output := TFileStream.Create('report.txt', fmCreate);
  try
    // prazan raspon = sve stranice; form feed između stranica; UTF-8 BOM
    Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
    Writeln(Pages, ' pages extracted');
  finally
    Output.Free;
  end;
end;

Koji HotPDF tekstovni API biste trebali koristiti?

ExtractLoadedPageText ostaje u redoslijedu content streama, što je pravi zadani izbor za search i indeksiranje; dekodirani lanac ispod njega pokriva izvlačenje teksta iz učitanih PDF-ova s HotPDF-om. Za tagirane dokumente kojima je redoslijed autorstva bitan, izvlačenje teksta u redoslijedu strukture hoda strukturnim stablom umjesto da pogađa iz geometrije, a za podatke zarobljene u tablicama tipizirano izvlačenje tablica preko prijeloma stranica vraća ćelije umjesto redaka. Potpuna API referenca i trial preuzimanje su na stranici proizvoda HotPDF Delphi PDF Component