Techninis straipsnis

PDF teksto ištraukimas Delphi: žodžių tarpai ir eilutės

HotPDF Delphi Component žodžių tarpus ir eilučių lūžius THotPDF.ExtractLoadedPageText atkuria iš glifų geometrijos, o ne iš tarpo simbolių. Tarpas įsiterpia, kai tarpas po glifo pačio pločio viršija 0.15 teksto aukščio, o nauja eilutė prasideda tik tada, kai teksto pradžios taškas, pasislinkęs statmenai rašymo krypčiai, nueina daugiau nei per pusę teksto aukščio. Nuo v2.768.3 puslapio tekstas apima ir tekstą, nupieštą pro Form XObjects, ir neima glifų, esančių už matomos apkarpytos srities. Šio rašinio likusi dalis paaiškina, kodėl kiekviena taisyklė atrodo taip, kaip atrodo, nes kiekviena iš jų pakeitė paprastesnę taisyklę, kuri tikruose dokumentuose duodavo įtikinamos, bet klaidingos išvesties

Simptomai pažįstami kiekvienam, kas kada nors davė PDF tekstą paieškos indeksui. Viršelis ištraukiamas kaip PDFReferenceManualNovember4,1998, mokesčių forma skyla į 156 eilutes, įstrižas vandens ženklas atkeliauja po vieną simbolį eilutei, o apkarpytas spaudos egzempliorius prasideda spausdinimo valdymo eilute, kurios jokia peržiūryklė nerodo. Nė vienas iš tų failų nesugadintas. Kiekvienas naudoja visiškai teisėtą teksto išdėstymo būdą, kurį naivi ištraukėja perskaito neteisingai

Kodėl ištrauktas PDF tekstas praranda žodžių tarpus?

Ištrauktas tekstas praranda žodžių tarpus, nes PDF jų turėti niekada neprivalo. Gamintojas gali žodžius skirti rodamas tarpo simbolį, bet gali lygiai taip pat perkelti plunksną skaičiumi TJ masyve (ISO 32000-1 §9.4.3) arba šviežiu Td (§9.4.2), ir TeX išvestis, nemažai Distiller failų bei dauguma lygiuotų maketų daro būtent tai. Iki v2.766.76 HPDFAssemblePageText žiūrėjo tik į vertikalų judesį, tad pozicionavimu padarytas žodžių lūžis tiesiog dingo. Surinkėjas dabar išmatuoja, ankstesnio glifo rašymo kryptimi, atstumą nuo to glifo pačio pločio pabaigos iki dabartinio glifo pradžios ir įterpia vieną tarpą, kai atstumas viršija 0.15 dabartinio glifo dėžės aukščio, išmatuoto nuo viršutinio iki apatinio taško vartotojo erdvėje. Tarpas neįdedamas, kai bet kuri pusė jau tuščia, ir tarp dviejų CJK simbolių, nes lygiuotė išskiria ideogramas be to, kad tas išskirtumas reikštų žodžio ribą. Glifo įrašai atskleidžia tą pačią geometriją, tad kai kuris failas apstulbina, sprendimą galite atkurti patys

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
    // glifo dėžės aukštis nuo viršutinio iki apatinio taško, vartotojo erdvėje
    Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
      Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
    // horizontalus tekstas: tarpas nuo ankstesnio glifo pačio pločio pabaigos
    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;

Kodėl matuoti nuo glifo pačio pločio, o ne nuo plunksnos pozicijos?

HotPDF žodžių tarpus matuoja nuo GlyphEndX / GlyphEndY, nes plunksnos pozicija po glifo jau turi tarpus, kurie nėra tarpas. ISO 32000-1 §9.4.4 horizontalųjį poslinkį apibrėžia kaip glifo plotį, daugintą šrifto dydžiu, plius simbolių tarpą Tc, plius žodžių tarpą Tw, viską skaluojant Tz. BaselineEndX / BaselineEndY laiko tą pilną poslinkį, o GlyphEndX / GlyphEndY – tik šrifto žingsnį ir Tz. Skirtumas svarbus gamintojams, kurie suspaudžia tarpraidžius neigiamu Tc, o tada po kiekvieno glifo atiduoda tarpą TJ koregavimu: matuojant nuo plunksnos pozicijos tas atidavimas atrodo kaip tarpas, ir kinų terminas “95后” ištraukiamas kaip “9 5 后”. Slenkstis susietas su glifo dėžės aukščiu, o ne su Tf dydžiu, iš panašios priežasties. Word eksportai dažnai rašo 1 Tf ir tikrąjį dydį neša išmastytame Tm, tad Tfs sako 1, o tekstas yra 10 taškų aukščio, ir taisyklė, susieta su Tfs, tuos du to paties puslapio rašybas elgtųsi skirtingai

HotPDF žodžių tarpo taisyklė ExtractLoadedPageText Delphi: tarpas įterpiamas tik tada, kai atstumas nuo ankstesnio glifo GlyphEndX iki kito glifo BaselineStartX viršija 0.15 dėžės aukščio nuo viršutinio iki apatinio taško, nes plunksnos pozicija BaselineEndX jau turi Tc, Tw ir Tz ir lygiuotės tarpraidžių atidavimus paverčia netikrais tarpais, tokiais kaip 9 5 后
Kur lūžta žodžiai, sprendžia geometrija, o ne tarpo simboliai – glifo įrašai atskleidžia tuos pačius matavimus, tad sprendimą bet kuriam apstulbinančiam failui galite pakartoti

Taisyklė turi sąžiningus kraštus. Antraštė, sustatyta su labai laisvais tarpraidžiais, kur vien Tc tarp raidžių atveria daugiau nei 0.15 teksto aukščio, ištraukiama su tarpu tarp kiekvienų raidžių – taip atrodo puslapis, bet greičiausiai ne tai, ką norėjote indeksuoti. Vienoje pagrindo linijoje netvarkinga tvarka nupiešti gabalai duoda neigiamą tarpą ir sulimpa be tarpo. Nė vienas iš tų atvejų nebūdingas pagrindiniam tekstui, o testų korpuse pakeitimas pakėlė žodžių atitikimų skaičių prieš atskaitinę ištraukėją 28 puslapiuose, nenumažindamas nė vieno

Kada HotPDF ištrauktame tekste pradeda naują eilutę?

Nuo v2.766.79 nauja eilutė prasideda, kai judesys nuo ankstesnio glifo pradžios taško iki dabartinio, projektuotas ant ankstesnės rašymo krypties normalės, viršija pusę didesnės iš dviejų glifų dėžių aukščio. Ankstesnioji taisyklė žalią Y judesį lygino su puse Tfs, kas klysdavo dviem kryptimis. Su 1 Tf ir išmastytu Tm slenkstis sumažėdavo iki pusės vieneto, tad viršutinį indeksą, pakeltą 0.4 teksto pakilimu, arba paprastas pagrindo drebėjimas lauždavo eilutę. Taisyklė taip pat visiškai ignoruodavo X, tad tekstas po pasukta Tm lipdavo žemyn puslapiu su kiekvienu glifu ir išeidavo po vieną glifą eilutei. Projektavimas ant krypties normalės pasuktas atkarpas elgia kaip horizontalias, o didesnio iš dviejų aukščių paėmimas laiko didelį pavyzdinį žodį ir jo mažą paaiškinimą vienoje eilutėje, kai jie dalijasi pagrindo linija. Minėtoje mokesčių formoje eilučių skaičius nukrito nuo 156 iki 97. Vertikalus tekstas rašymo veiksena 1 (§9.7.4.3) eina atskiru keliu: tie glifai grupuojami į stulpelius, skaitomi iš dešinės į kairę ir iš viršaus į apačią, su eilutės lūžiu prie kiekvieno stulpelio pasikeitimo

Kaip HotPDF ExtractLoadedPageText Delphi nusprendžia eilučių lūžius: judesys tarp glifo pradžios taškų projektuojamas ant rašymo krypties normalės ir lyginamas su puse didesnės dėžės aukščio, tad viršutinis indeksas, pakeltas nedideliu teksto pakilimu po 1 Tf šriftu, ir tekstas, lipantis žemyn puslapiu po pasuktos Tm, daugiau nesiskaido į po vieną glifą eilutei
Projektavimas pasuktas atkarpas elgia kaip horizontalias, o didesnio iš dviejų dėžės aukščių paėmimas laiko didelį pavyzdinį žodį ir jo mažą paaiškinimą vienoje eilutėje

Kurį tekstą ExtractLoadedPageText ima, o kurį palieka?

ExtractLoadedPageText grąžina tekstą, kurį rodo peržiūryklė. Nuo v2.766.80 ji dirba tik su matomais glifais, metdama kiekvieną glifą, kurio dėžės centras krenta už GetLoadedPageVisibleBox – CropBox, apkarpyto MediaBox (§14.11.2). Tai pašalina spausdinimo valdymo eilutes ir kitus spausdinimo ženklus, išdėstytus tekstu už apkirpimo srities. ExtractLoadedPageGlyphs sąmoningai ir toliau grąžina kiekvieną puslapio turinio srauto glifą, tad tą medžiagą vis dar rasite, kai jos prireiks. Filtras yra dėžės testas, o ne matomumo testas: tekstas, paslėptas apkarpymo keliu, nupieštas balta arba padengtas vaizdu, vis tiek ištraukiamas

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]));
    // kiekvienas puslapio turinio srauto glifas, su spausdinimo valdymo eilute
    if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
      Writeln(Length(Glyphs), ' glyphs in the page content stream');
    // tik tai, ką rodo puslapis, su įpjautu Form XObject tekstu
    if Pdf.ExtractLoadedPageText(0, PageText) then
      Writeln(PageText);
  finally
    Pdf.Free;
  end;
end;

Tekstas, nupieštas pro Form XObjects, yra puslapio teksto dalis nuo v2.768.3. Antraštės, antspaudai ir vandens ženklai labai dažnai gyvena formose, ir kai kurie standartų dokumentai iki pakeitimo prarasdavo 30–35 procentus simbolių. THotPDF.InterpretContentWithForms įsimena kiekvieną Do kartu su galiojančiu CTM, interpretuoja formą jos /Matrix, sudaugintos su ta CTM, (§8.10.1) ir formos glifus įpjauta Do vietoje, rekursyviai eidama į įdėtines formas. Forma be savo /Resources pasiskolina to srauto, kuris ją piešia, – kaip leidžia §7.8.3. Formos glifai neša TokenIndex = -1, o ExtractLoadedPageGlyphs vis tiek grąžina tik puslapio srauto glifus, nes paieška, keitimas ir juodinimas pakeitimus rašo atgal per TokenIndex ir redaguotų netinkamus baitus, jei formos glifas įslietų. Dviejų supaprastinimų verta žinoti: formos tekstas nėra apkarpomas iki formos /BBox, o rekursija sustoja ties 12 lygiais vietoj ciklų aptikimo, tad netinkama forma, piešianti save pati, kartos savo tekstą, kol pasieks tą ribą

Kuriuos glifus HotPDF ima ištraukdamas PDF puslapio tekstą Delphi: ExtractLoadedPageText palieka tik glifus, kurių dėžės centras krenta į GetLoadedPageVisibleBox – CropBox, apkarpytą MediaBox – tad spausdinimo valdymo eilutės dingsta, o InterpretContentWithForms Form XObject glifus įpjauta prie kiekvieno Do su nustatytu TokenIndex = -1, kol glifo lygio API vis tiek grąžina viską
Dėžės testas glifo centrui nėra matomumo testas – baltas tekstas, apkarpomas tekstas ir padengtas tekstas vis tiek išeina, o formų tekstas skaičiuojasi nuo v2.768.3

Kodėl tekstas po Q operatoriaus iškoduodavosi kaip šiukšlės?

Tekstas po Q iki v2.766.73 galėjo iškoduotis neteisingai, nes ištraukėja q metu įsimindavo vien CTM. Teksto būsenos parametrai – šriftas, dydis, Tc, Tw, Tz, TL, atvaizdavimo veiksena ir pakilimas – priklauso grafinės būsenos daliai (§9.3.1), tad Q privalo juos atstatyti kartu su viskuo kitu krūvoje (§8.4.2). Vienoje pramonės ataskaitoje dviejų baitų Identity-H šriftas buvo parinktas q … Q viduje, o tada rodomas vieno baito WinAnsi tekstas be savo Tf. Ištraukėja liko prie vidinio šrifto, perskaitė taškus ir žodį “Adobe” turinio lentelėje kaip dviejų baitų kodus ir prarado 15% puslapio simbolių. Interpretatoriaus q/Q krūva dabar laiko pilną teksto būseną. Čia aprašytos ištraukimo taisyklės galioja kiekvienam puslapiui, tad visas dokumentas gali nukeliauti į failą vienu iškvietimu

var
  Output: TFileStream;
  Pages: Integer;
begin
  Output := TFileStream.Create('report.txt', fmCreate);
  try
    // tuščias diapazonas = visi puslapiai; form feed tarp puslapių; UTF-8 BOM
    Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
    Writeln(Pages, ' pages extracted');
  finally
    Output.Free;
  end;
end;

Kurį HotPDF teksto API rinktis?

ExtractLoadedPageText lieka turinio srauto tvarkoje, kuri yra teisingasis numatytasis paieškai ir indeksavimui; po ja esanti dekodavimo grandinė aprašyta teksto ištraukime iš pakrautų PDF su HotPDF. Žymėtiems dokumentams, kuriems svarbi autorystės tvarka, teksto ištraukimas struktūros tvarka eina struktūros medžiu vietoj spėliojimo iš geometrijos, o duomenims, įkalintiems lentelėse, tipizuotų lentelių ištraukimas per puslapių lūžius grąžina langelius, o ne eilutes. Pilna API nuoroda ir bandomasis atsisiuntimas laukia HotPDF Delphi PDF Component produkto puslapyje