Műszaki cikk

PDF szövegkinyerés Delphiben: szóközök és sortörések

A HotPDF Delphi Component a THotPDF.ExtractLoadedPageText-ben glyph geometriából építi újra a szóközöket és a sortöréseket, nem szóköz karakterekből. Szóköz akkor kerül be, amikor egy glyph saját szélessége utáni rés meghaladja a szövegmagasság 0,15-ödét, és új sor csak akkor kezdődik, amikor a szövegorigó az írásirány mentén a szövegmagasság felénél messzebb mozdul. A v2.768.3 óta az oldalszöveg a Form XObjecteken át rajzolt szöveget is tartalmazza, és kihagyja a látható crop területen kívül eső glyphokat. Ez a cikk a továbbiakban azt magyarázza el, miért néz ki minden szabály úgy, ahogy, mert mindegyik egy egyszerűbb szabályt váltott fel, ami valódi dokumentumokon hihető, de rossz kimenetet gyártott

A tünetek ismerősek bárkinek, aki valaha adott PDF szöveget keresési indexbe. Egy borító PDFReferenceManualNovember4,1998 alakban jön ki, egy adórlap 156 sorra esik szét, egy átlós vízjel soronként egy karakterrel érkezik, és egy levágott próbanyomat a nyomtató slug sorával vezet, amit egyetlen megjelenítő sem mutat. Egyik fájl sem hibás. Mindegyik tökéletesen legális módon helyez el szöveget, amit naiv kinyerő félreolvas

Miért veszítik el a kinyert PDF szövegek a szóközeiket?

A kinyert szöveg azért veszíti el a szóközeit, mert egy PDF-ből sosem kötelező azokat tartalmaznia. Egy producer elválthatja a szavakat szóköz karakter megjelenítésével, de ugyanúgy elmozdíthatja a tollat is egy számmal egy TJ tömbben (ISO 32000-1 §9.4.3) vagy egy friss Td-vel (§9.4.2), és a TeX kimenet, sok Distiller fájl és a legtöbb sorkizárt elrendezés pontosan ezt teszi. A v2.766.76 előtt az HPDFAssemblePageText csak a függőleges mozgást nézte, így a pozicionálással jelzett szóhatár egyszerűen eltűnt. Az összeállító mostantól az előző glyph írásirányának mentén méri a távolságot az adott glyph saját szélességének végétől az aktuális glyph origójáig, és egy szóközt szúr be, ha a távolság meghaladja az aktuális glyph dobozmagasságának 0,15-ödét, ascenttől descentig mérve user space-ben. Szóköz nem kerül be, ha bármelyik oldal már üres, és egyik sem kerül két CJK karakter közé, mert a sorkizárás széthúzza az ideogramokat anélkül, hogy az a szóhatárt jelentene. A glyph rekordok ugyanezt a geometriát kiadják, így bármikor újraszámolhatod a döntést, ha egy konkrét fájl rejtvény

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
    // a glyph doboz ascenttől descentig terjedő magassága, user space-ben
    Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
      Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
    // vízszintes szöveg: rés az előző glyph saját szélességének végétől
    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;

Miért a glyph saját szélességétől mér, és nem a tollpozíciótól?

A HotPDF a szórés távolságokat a GlyphEndX / GlyphEndY-tól méri, mert a tollpozíció egy glyph után már olyan távolságot is tartalmaz, ami nem rés. Az ISO 32000-1 §9.4.4-e a vízszintes elmozdulást a glyph szélességének és a fontméretnek a szorzataként definiálja, plusz a karaktertávolság Tc, plusz a szóköz távolság Tw, mind a Tz-vel skálázva. A BaselineEndX / BaselineEndY a teljes elmozdulást hordozza, a GlyphEndX / GlyphEndY viszont csak a font advance-et és a Tz-t. A különbség azoknál a producereknél számít, amik negatív Tc-vel szorítják a betűközt, majd minden glyph után egy TJ korrekcióval visszaadják a teret: tollpozíciótól mérve a visszaadás résnek látszik, és a kínai „95后" kifejezés „9 5 后" alakban jött ki. A küszöb hasonló okból a glyph dobozmagasságához kötődik, nem a Tf méretéhez. A Word exportok gyakran 1 Tf-t írnak, és a valódi méretet skálázott Tm-ben hordozzák, így a Tfs 1-et mond, miközben a szöveg 10 pont magas, és egy Tfs-hez kötött szabály ugyanazon oldal két helyesírását eltérően kezelné

A HotPDF szóköz szabálya az ExtractLoadedPageTexthez Delphiben: szóköz csak akkor szúródik be, ha az előző glyph GlyphEndX-étől a következő glyph BaselineStartX-éig mért távolság meghaladja az ascenttől descentig terjedő dobozmagasság 0,15-ödét, mert a tollpozíció a BaselineEndX-ben már tartalmazza a Tc-t, Tw-t és Tz-t, és a sorkizárt betűköz visszaadásokat hamis résekké fordítja, mint a 9 5 后
A geometria dönt a szóhatárokról, nem a szóköz karakterek — a glyph rekordok ugyanezeket a mértékeket adják ki, így bármelyik rejtvényfájlra újrajátszhatod a döntést

A szabálynak őszinte határai vannak. Egy nagyon laza betűközzel szedett cím, ahol a Tc önmagában több mint a szövegmagasság 0,15-ödét nyit a betűk közt, minden betű közé szóközzel jön ki, ami az, amit az oldal mutat, de valószínűleg nem az, amit indexelni akartál. Az egy bazisvonalra rendezetlen sorrendben kirajzolt darabok negatív rést produkálnak, és szóköz nélkül fűződnek össze. Egyik eset sem gyakori a szövegtörzsben, és a tesztkorpuszon a változás 28 oldalon emelte a szóegyezéseket egy referencia kinyerőhöz képest, anélkül, hogy bármelyiken csökkentette volna

Mikor kezd új sort a HotPDF a kinyert szövegben?

A v2.766.79 óta új sor akkor kezdődik, amikor az előző glyph origójából az aktuálisba menő mozdulás, az előző írásirány normálára vetítve, meghaladja a két glyph közül a nagyobb dobozmagasság felét. A korábbi szabály a nyers Y mozdulást hasonlította a Tfs feléhez, ami két irányban is elbukott. 1 Tf és skálázott Tm mellett a küszöb fél egységre zsugorodott, így egy 0,4-es text rise-zal emelt fölérendelt index vagy egy hétköznapi bazisvonal-zaj sortört. A szabály az X-et teljesen figyelmen kívül hagyta, így egy elforgatott Tm alatti szöveg minden glyphmal lépkedett lefelé az oldalon, és soronként egy glyphként jött ki. Az iránynormálra vetítés azt éri el, hogy az elforgatott futamok úgy viselkedjenek, mint a vízszintesek, a két magasság közül a nagyobbik választása pedig azt, hogy egy nagy méretű mintaszó és a kis felirata egy sorban maradjon, ha osztoznak a bazisvonalon. A fent említett adólapon a sorszám 156-ról 97-re esett. A writing mode 1-es (§9.7.4.3) függőleges szöveg külön útvonalon jár: azok a glyphok oszlopokba csoportosulnak, jobbról balra és felülről lefelé olvasódnak, oszlopváltásonként sortöréssel

Hogyan dönt a HotPDF ExtractLoadedPageText-je sortörésekről Delphiben: a glyph origók közti mozdulás az írásirány normálára vetítődik, és a nagyobb dobozmagasság feléhez hasonlítódik, így az 1 Tf font alatt kis text rise-zal emelt fölérendelt index és az elforgatott Tm alatt lépkedő szöveg sem esik többé soronként egy glyphra
A vetítés azt éri el, hogy az elforgatott futamok vízszintes módra viselkedjenek, a két dobozmagasság közül a nagyobbik pedig egy sorban tartja a nagy mintaszót és a kis feliratát

Melyik szöveget veszi fel az ExtractLoadedPageText, és melyiket hagyja el?

Az ExtractLoadedPageText azt a szöveget adja vissza, amit a megjelenítő mutat. A v2.766.80 óta csak a látható glyphokból dolgozik, és elhagy minden glyphot, aminek a dobozközepe a GetLoadedPageVisibleBox-on kívül esik, ami a MediaBoxra levágott CropBox (§14.11.2). Ez tünteti el a slug sorokat és a többi, a vágási területen kívül szedett nyomdai jelet. Az ExtractLoadedPageGlyphs szándékosan továbbra is az oldal content streamjének minden glyphját visszaadja, így arra az anyagra akkor is rátalálsz, ha kell. A szűrő dobozteszt, nem láthatóságteszt: a clipping path mögé rejtett, fehérrel festett vagy képpel takart szöveg is kijön

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]));
    // az oldal content streamjének minden glyphja, slug sorral együtt
    if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
      Writeln(Length(Glyphs), ' glyphs in the page content stream');
    // csak az, amit az oldal mutat, Form XObject szöveggel beillesztve
    if Pdf.ExtractLoadedPageText(0, PageText) then
      Writeln(PageText);
  finally
    Pdf.Free;
  end;
end;

A Form XObjecteken át festett szöveg a v2.768.3 óta része az oldalszövegnek. A fejlécek, a pecsétek és a vízjelek nagyon gyakran formákban laknak, és egyes szabványdokumentumok a változás előtt karaktereik 30-35 százalékát veszítették el. A THotPDF.InterpretContentWithForms minden Do-t feljegyez az éppen hatályos CTM-mel együtt, a formát a /Matrix-szának és annak a CTM-nek a szorzatánál értelmezi (§8.10.1), a form glyphjait pedig a Do pozíciójába illeszti be, egymásba ágyazott formákba rekurzívan lépve. A saját /Resources nélküli form a festő stream forrásaival dolgozik, ahogy a §7.8.3 megengedi. A form glyphok TokenIndex = -1 értéket hordoznak, és az ExtractLoadedPageGlyphs továbbra is csak oldalstream glyphokat ad vissza, mert a keresés, csere és redakció a TokenIndex-en át írja vissza a változásokat, és rossz bájtokat szerkesztene, ha form glyph becsúszna. Két egyszerűsítés érdemes ismerni: a form szöveget nem vágja le a form /BBox-a, és a rekurzió 12 szintnél áll meg ciklusérzékelés helyett, így egy önmagát festő, rosszul formált form a szövegét addig ismétli, amíg el nem éri azt a capet

Mely glyphokat veszi fel a HotPDF PDF oldalszöveg kinyerésekor Delphiben: az ExtractLoadedPageText csak azokat a glyphokat tartja meg, amiknek a dobozközepe a GetLoadedPageVisibleBox-ba, a MediaBoxra levágott CropBoxba esik, így a nyomdai slug sorok eltűnnek, az InterpretContentWithForms pedig minden Do pozíciónál illeszti be a Form XObject glyphokat TokenIndex = -1 értékkel, miközben a glyphszintű API továbbra is mindent visszaad
A glyph közepére végzett dobozteszt nem láthatóságteszt — a fehér, a levágott és a takart szöveg is kijön, és a form szöveg a v2.768.3 óta számít

Miért olvasódott gibberish-ként a Q operátor utáni szöveg?

A Q utáni szöveg a v2.766.73 előtt rosszul dekódolódhatott, mert a kinyerő csak a CTM-et mentette q-nál. A szövegállapot paraméterei, nevezetesen a font, a méret, a Tc, a Tw, a Tz, a TL, a renderelési mód és a rise, a grafikai állapothoz tartoznak (§9.3.1), így a Q-nak azokat is helyre kell állítania a verem minden más elemével együtt (§8.4.2). Egy ipari riport egy kétbájtos Identity-H fontot választott egy q … Q blokkon belül, majd saját Tf nélküli egybájtos WinAnsi szöveget mutatott. A kinyerő megtartotta a belső fontot, a tartalomjegyzék vezetőpontjait és az „Adobe" szót kétbájtos kódokként olvasta, és az oldal karaktereinek 15%-át elvesztette. Az interpreter q/Q verme mostantól a teljes szövegállapotot tartja. Az itt leírt kinyerési szabályok minden oldalra vonatkoznak, így egy teljes dokumentum egy hívással fájlba mehet

var
  Output: TFileStream;
  Pages: Integer;
begin
  Output := TFileStream.Create('report.txt', fmCreate);
  try
    // üres tartomány = minden oldal; lapdobás az oldalak közt; UTF-8 BOM
    Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
    Writeln(Pages, ' pages extracted');
  finally
    Output.Free;
  end;
end;

Melyik HotPDF szöveg API-t használd?

Az ExtractLoadedPageText content-stream sorrendben marad, ami keresésre és indexelésre a helyes alapértelmezés; a mögötte álló dekódolási láncot a szöveg kinyerése betöltött PDF-ekből HotPDF-fel tárgyalja. Taggelt dokumentumoknál, ahol a szerkesztési sorrend számít, a struktúrasorrendű szövegkinyerés a struktúrafát járja geometriai találgatás helyett, táblákba zárt adatoknál pedig a típusos táblakinyerés oldaltöréseken át cellákat ad vissza, nem sorokat. Teljes API referencia és trial letöltés a HotPDF Delphi PDF Component termékoldalon