Teknisk artikkel

PDF-tekstekstraksjon i Delphi: ordmellomrom og linjeskift

HotPDF Delphi Component bygger opp ordmellomrom og linjeskift i THotPDF.ExtractLoadedPageText fra glyfgeometri, ikke fra mellomromstegn. Et mellomrom kommer inn når gapet etter en glyfs egen bredde overstiger 0.15 av tekstens høyde, og en ny linje starter bare når tekstorigo flytter seg over skriveretningen med mer enn halve tekstens høyde. Siden v2.768.3 omfatter sideteksten også tekst malt gjennom Form XObjects og lar glyfer utenfor det synlige crop-området være. Resten av dette stykket forklarer hvorfor hver regel ser ut som den gjør, for hver eneste av dem erstattet en enklere regel som produserte plausibel men feil output på ekte dokumenter

Symptomene er kjent for alle som har ført PDF-tekst inn i et søkeindeks. En forsideside trekkes ut som PDFReferenceManualNovember4,1998, et selvangivelsesskjema splittes i 156 linjer, et diagonalvannmerke ankommer ett tegn per linje, og en beskåret korrektur starter med skriverens slug-linje som ingen visningsprogram noensinne viser. Ingen av disse filene er ødelagt. Hver eneste bruker en helt lovlig måte å plassere tekst på som en naiv ekstraktor feiltolker

Hvorfor mister uttrukket PDF-tekst ordmellomrommene sine?

Utrukket tekst mister ordmellomrom fordi en PDF aldri kreves å inneholde dem. En produsent kan skille ord ved å vise et mellomromstegn, men den kan like gjerne flytte pennen med et tall inne i en TJ-array (ISO 32000-1 §9.4.3) eller med en fersk Td (§9.4.2), og TeX-output, mange Distiller-filer og de fleste justerte oppsett gjør nøyaktig det. Før v2.766.76 så HPDFAssemblePageText bare på vertikal bevegelse, så et ordbrudd laget ved posisjonering forsvant rett og slett. Assembleren måler nå, langs den forrige glyfens skriveretning, avstanden fra slutten av glyfens egen bredde til origo til den gjeldende glyfen, og setter inn ett mellomrom når avstanden overstiger 0.15 av den gjeldende glyfboksens høyde, målt fra ascender til descender i user space. Ingen mellomrom legges til når en av sidene allerede er blank, og ingen mellom to CJK-tegn, for justering strekker ideografer fra hverandre uten at den strekningen betyr en ordgrense. Glyfrecordene eksponerer samme geometri, så du kan reprodusere avgjørelsen når en bestemt fil forvirrer deg

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
    // ascender-til-descender-høyden til glyfboksen, i user space
    Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
      Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
    // horisontal tekst: gap fra slutten av forrige glyfs egen bredde
    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;

Hvorfor måle fra glyfens egen bredde i stedet for penneposisjonen?

HotPDF måler ordgaper fra GlyphEndX / GlyphEndY fordi penneposisjonen etter en glyf allerede inneholder avstand som ikke er et gap. ISO 32000-1 §9.4.4 definerer den horisontale forskyvningen som glyfbredden ganger fontstørrelsen, pluss tegnavstand Tc, pluss ordavstand Tw, alt skalert med Tz. BaselineEndX / BaselineEndY holder den fulle forskyvningen, mens GlyphEndX / GlyphEndY bare holder font-avstanden og Tz. Forskjellen betyr noe for produsenter som strammer tracking med en negativ Tc og så gir mellomrommet tilbake gjennom en TJ-justering etter hver glyf: målt fra penneposisjonen ser tilbakegivelsen ut som et gap, og det kinesiske uttrykket “95后” ble trukket ut som “9 5 后”. Terskelen er knyttet til glyfboksens høyde i stedet for Tf-størrelsen av lignende grunn. Word-eksporter skriver ofte 1 Tf og bærer den virkelige størrelsen i en skalert Tm, så Tfs sier 1 mens teksten er 10 punkter høy, og en regel nøkklet til Tfs ville behandle de to stavemåtene av samme side forskjellig

HotPDFs ordmellomrom-regel for ExtractLoadedPageText i Delphi: et mellomrom settes bare inn når avstanden fra forrige glyfs GlyphEndX til neste glyfs BaselineStartX overstiger 0.15 av ascender-til-descender-bokshøyden, for penneposisjonen i BaselineEndX inneholder allerede Tc, Tw og Tz og gjør justerte tracking-tilbakegivelser til falske gaper som 9 5 后
Geometri, ikke mellomromstegn, avgjør hvor ord bryter — glyfrecordene eksponerer samme målinger, så du kan spille av avgjørelsen for enhver forvirrende fil

Regelen har ærlige kanter. En overskrift satt med svært løs tracking, der Tc alene åpner mer enn 0.15 av tekstens høyde mellom bokstavene, trekkes ut med et mellomrom mellom hver bokstav, noe som er slik siden ser ut, men sannsynligvis ikke hva du ville indeksere. Biter tegnet ut av rekkefølge på én grunnlinje gir negativ gap og føyer seg sammen uten mellomrom. Ingen av tilfellene er vanlig i brødtekst, og på et testkorpus hevet endringen ordmatcher mot en referanseekstraktor på 28 sider uten å senke noen

Når starter HotPDF en ny linje i uttrukket tekst?

Siden v2.766.79 starter en ny linje når flytten fra forrige glyfs origo til den gjeldende, projisert på normalen til forrige skriveretning, overstiger halvparten av den største bokshøyden av de to glyfene. Den tidligere regelen sammenlignet den rå Y-flytten med halvparten av Tfs, noe som feilet i to retninger. Med 1 Tf og en skalert Tm krympet terskelen til en halv enhet, så en opphøyd skrift løftet med en text rise på 0.4 eller vanlig grunnlinjejitter brøt linjen. Regelen ignorerte også X helt, så tekst under en rotert Tm gikk nedover siden med hver glyf og kom ut én glyf per linje. Å projisere på retningsnormalen gjør at roterte løp oppfører seg som horisontale, og å ta den største av de to høydene holder et stort eksempelord og dets lille bildetekst på én linje når de deler grunnlinje. På selvangivelsesskjemaet nevnt over falt linjeantallet fra 156 til 97. Vertikal tekst i skrivemodus 1 (§9.7.4.3) følger en egen sti: de glyfene grupperes i kolonner, leses høyre til venstre og ovenfra og ned, med linjeskift ved hvert kolonneskifte

Hvordan HotPDFs ExtractLoadedPageText avgjør linjeskift i Delphi: flytten mellom glyforigoer projiseres på skriveretningens normal og sammenlignes med halvparten av den største bokshøyden, så en opphøyd skrift løftet av en liten text rise under en 1 Tf-font og tekst som går nedover siden under en rotert Tm splittes ikke lenger til én glyf per linje
Projeksjon gjør at roterte løp oppfører seg som horisontale, og å ta den største av de to bokshøydene holder et stort eksempelord og dets lille bildetekst på én linje

Hvilken tekst inkluderer ExtractLoadedPageText, og hva lar den være?

ExtractLoadedPageText returnerer teksten en viser viser. Siden v2.766.80 arbeider den fra de synlige glyfene alene, og slipper hver glyf hvis bokssenter faller utenfor GetLoadedPageVisibleBox, som er CropBox-en beskåret til MediaBox-en (§14.11.2). Det fjerner slug-linjer og andre skrivermerker satt som tekst utenfor beskjæringsområdet. ExtractLoadedPageGlyphs beholder med vilje å returnere hver glyf i sidens content stream, så du fortsatt kan finne det materialet når du trenger det. Filteret er en boks-test, ikke en synlighetstest: tekst skjult av en klippebane, malt i hvitt eller dekket av et bilde trekkes fortsatt ut

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]));
    // hver glyf i sidens content stream, slug-linjen inkludert
    if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
      Writeln(Length(Glyphs), ' glyphs in the page content stream');
    // bare det siden viser, med Form XObject-tekst skjøtet inn
    if Pdf.ExtractLoadedPageText(0, PageText) then
      Writeln(PageText);
  finally
    Pdf.Free;
  end;
end;

Tekst malt gjennom Form XObjects er en del av sideteksten siden v2.768.3. Topptekster, stempler og vannmerker bor svært ofte i former, og noen standarddokumenter mistet 30 til 35 prosent av tegnene sine før endringen. THotPDF.InterpretContentWithForms registrerer hver Do sammen med CTM-en i kraft, tolker formen ved dens /Matrix ganger den CTM-en (§8.10.1) og skjøter formens glyfer inn på posisjonen til Do-en, med rekursjon inn i nestede former. En form uten egen /Resources låner de til streamen som maler den, slik §7.8.3 tillater. Formglyfer bærer TokenIndex = -1, og ExtractLoadedPageGlyphs returnerer fortsatt bare page stream-glyfer, for søk, erstatt og redaksjon skriver endringer tilbake gjennom TokenIndex og ville redigert feil byte hvis en formglyf snek seg inn. To forenklinger er verdt å kjenne: formtekst klippes ikke til formens /BBox, og rekursjonen stopper ved 12 nivåer i stedet for gjennom syklussdeteksjon, så en misdannet form som maler seg selv, gjentar teksten sin til den når den grensen

Hvilke glyfer HotPDF inkluderer ved uttrekk av PDF-sidetekst i Delphi: ExtractLoadedPageText beholder bare glyfer hvis bokssenter faller innenfor GetLoadedPageVisibleBox, CropBox-en beskåret til MediaBox-en, så skrivernes slug-linjer forsvinner, mens InterpretContentWithForms skjøter Form XObject-glyfer inn på hver Do-posisjon med TokenIndex satt til -1 og glyfnivå-API-et fortsatt returnerer alt
En boks-test på glyfsenteret er ikke en synlighetstest — hvit tekst, klippet tekst og dekket tekst kommer fortsatt ut, og formtekst teller siden v2.768.3

Hvorfor dekodet tekst etter en Q-operator som søppel?

Tekst etter Q kunne dekode feil før v2.766.73 fordi ekstraktoren lagret bare CTM-en på q. Teksttilstandsparameterne, nemlig font, størrelse, Tc, Tw, Tz, TL, renderingsmodus og rise, hører til grafikktilstanden (§9.3.1), så Q må gjenopprette dem sammen med alt annet på stakken (§8.4.2). En industrirapport valgte en tobyme Identity-H-font inne i q … Q og viste så enbytes WinAnsi-tekst uten egen Tf. Ekstraktoren beholdt den indre fonten, leste prikkene og ordet “Adobe” på innholdsfortegnelsen som tobyme koder og droppet 15 % av sidens tegn. Tolkens q/Q-stakk holder nå hele teksttilstanden. Uttreksreglene beskrevet her gjelder hver side, så et helt dokument kan gå til en fil i ett kall

var
  Output: TFileStream;
  Pages: Integer;
begin
  Output := TFileStream.Create('report.txt', fmCreate);
  try
    // tomt område = alle sider; form feed mellom sider; UTF-8 BOM
    Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
    Writeln(Pages, ' pages extracted');
  finally
    Output.Free;
  end;
end;

Hvilken HotPDF tekst-API bør du bruke?

ExtractLoadedPageText forblir i content stream-rekkefølge, som er riktig standard for søk og indeksering; dekodingskjeden under den er dekket i å trekke ut tekst fra lastede PDF-er med HotPDF. For taggede dokumenter der forfatterrekkefølgen betyr noe, går strukturrekkefølge-tekstekstraksjon gjennom strukturstreet i stedet for å gjette fra geometrien, og for data låst i tabeller returnerer typet tabellekstraksjon på tvers av sideskift celler i stedet for linjer. Full API-referanse og en prøvenedlasting ligger på HotPDF Delphi PDF Component produktsiden