HotPDF-komponentti purkaa Unicode-tekstin mistä tahansa ladatusta PDF-tiedostosta Delphissä kahdella kutsulla: ExtractLoadedPageText palauttaa sivun lukusuunnan mukaisen tekstin, ja ExtractLoadedPageTextLayout (lisätty versiossa v2.263.0) rekonstruoi sivun visuaalisen asettelun pelkkänä tekstinä. Tämän ansiosta sarakkeet, sisennykset ja taulukkoasettelut säilyvät tulosteessa. Molemmat toimivat dokumenteissa, joita HotPDF ei ole itse luonut, mikä onkin käytännössä se merkittävin tapaus: asiakkaan sähköpostitse lähettämä lasku, skannauspalvelun toimittama raportti tai sopimus, jonka on luonut ohjelmisto, jonka nimeä kukaan ei enää muista
Tähän pääseminen vaati enemmän koneistoa kuin kaksi funktiosignatuuria antavat ymmärtää, sillä PDF-tiedosto ei tallenna tekstiä samalla tavalla kuin tekstitiedosto. Tässä artikkelissa käydään läpi molemmat purkutilat ja kurkistetaan niiden alla vaikuttavan kolmen osan toimintaan — CMap-lukijaan, sisältövirran tulkkiin ja fontin purun varajärjestelmäketjuun. Kartoituksen ymmärtäminen auttaa selvittämään syyn siihen, miksi tuloste saattaa joskus näyttää pelkältä sotkulta
Miksi tekstin purkaminen on vaikeampaa kuin merkkijonojen lukeminen tiedostosta?
PDF:n sisältövirta tallentaa merkkikoodeja, ei merkkejä. Operaattorit Tj ja TJ (ISO 32000-1 §9.4.3) kuljettavat tavujonoja, joiden merkitys riippuu täysin edeltävällä Tf-operaattorilla valitusta fontista: tavu 0x41 voi olla kirjain A WinAnsi-koodauksessa, mielivaltainen glyyfi osafontissa tai puolet kaksitavuisesta CID-tunnisteesta yhdistetyssä CJK-fontissa. Standardi ISO 32000-1 §9.10 määrittelee tekstin purkamisen juuri tällaisena dekoodausongelmana — eli kunkin koodin yhdistämisenä takaisin Unicodeen käyttämällä sitä tietoa, jonka fonttihakemisto tarjoaa — ja standardi toteaa selkeästi, että vaatimustenmukaisen tiedoston ei tarvitse sisältää riittävästi tietoa tämän tekemiseksi
Tämä viimeinen lause selittää jokaisen näkemäsi virheraportin siitä, miksi kopiointi ja liittäminen PDF-tiedostosta tuottaa siansaksaa. Luontiohjelma, joka upottaa osafontin ilman /ToUnicode-taulukkoa, on kirjoittanut tiedoston, joka hahmontuu täydellisesti mutta jonka teksti purkautuu merkityksettömänä. Syynä on, että koodi-glyyfi-kartoitus on olemassa, mutta koodi-Unicode-kartoitusta ei koskaan toimitettu tiedoston mukana. Jokainen rehellinen purku-API on siksi parhaaseen suoritukseen pyrkivä varaketju, ja hyödyllinen kysymys onkin, kuinka syvälle tämä ketju ulottuu
Lukusuunnan mukainen tekstin purkaminen ExtractLoadedPageText-toiminnolla
Hakuhakemistojen luontia, avainsanojen täsmäystä tai analyysiputkea varten ExtractLoadedPageText on juuri sopiva kutsu. Funktiosignatuuri on function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — sivunindeksit ovat nollapohjaisia, tulos saapuu natiivina Delphin UnicodeString-tyyppinä ja funktio palauttaa arvon False poikkeuksen nostamisen sijaan silloin, kun sivulla ei ole luettavaa sisältövirtaa
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText, AllText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('invoice.pdf');
AllText := '';
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
AllText := AllText + PageText + #13#10;
// AllText now holds the reading-flow text of the document
finally
Pdf.Free;
end;
end;
Rivinaihdot tulosteessa saadaan aikaan yksinkertaisella heuristiikalla: kun glyyfin pystysuora origo siirtyy yli puolet nykyisestä fonttikoosta — mikä on merkki Td- tai T*-vaiheesta sisältövirrassa — lisätään rivinvaihto. Merkit, joita dekooderi ei pysty selvittämään, muuttuvat välilyönneiksi katoamisen sijaan, joten sanavälit säilyvät, vaikka yksittäiset glyyfit eivät säilyisikään. Tämä tila ei yritä suorittaa lukujärjestyksen ryhmittelyä tai monisarakehavainnointia: kaksisarakkeinen sivu tulostuu limitettynä sisältövirran järjestyksessä, mikä vastaa yleensä, mutta ei aina, visuaalista järjestystä
Milloin tulisi sen sijaan käyttää asettelun säilyttävää purkamista?
ExtractLoadedPageTextLayout on oikea kutsu silloin, kun sijainnilla on merkitystä: taulukot, lomakkeet, koodilistaukset tai mikä tahansa, mitä aiot verrata (diff), etsiä (grep) tai jäsentää sarakkeittain. Sen sijaan, että se litistäisi glyyfit yhdeksi virraksi, se ryhmittelee ne perusviivoihin (baselines), lajittelee kunkin perusviivan X-koordinaatin mukaan ja tuottaa horisontaalisen sekä vertikaalisen tyhjän tilan tasalevyiselle merkkiruudukolle, jonka koko määräytyy glyyfin keskimääräisen etenemän ja fonttikoon mukaan. Saman perusviivan tekstiosien väliset suuret raot muuttuvat välilyönneiksi ja suuret raot perusviivojen välillä tyhjiksi riveiksi. Tulos näyttää samalta kuin sivu näyttää
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Columns, indentation and table alignment survive as
// spaces and blank lines on a character grid
end;
Molemmat tilat jakavat dekoodauskoneiston jokaisen tavun ja eroavat toisistaan vain siinä, miten ne asettelevat puretut glyyfit. Valinta ei siis vaikuta tarkkuuteen. Valitse ExtractLoadedPageText silloin, kun vain sanoilla on merkitystä, ja ExtractLoadedPageTextLayout silloin, kun asettelu on tärkeä. Monisarakkeinen lukujärjestyksen havaitseminen jää molempien ulkopuolelle — kaksisarakkeisen sivun ruudukkoesitys näyttää molemmat sarakkeet rinnakkain luotettavasti. Tämä on täysin oikein vertailua varten, mutta ei tekstin juoksevaa hahmontamista varten
Miten HotPDF dekoodaa merkkikoodit Unicodeksi?
HotPDF-komponentti selvittää kunkin merkkikoodin priorisoidun varajärjestelmäketjun kautta: ensin fontin upotettu /ToUnicode CMap, sitten /Encoding-merkintä (tietovirta tai nimetty CMap), sitten — yhdistetyille fonteille — Adoben standardit CMap-tiedostot merkkikokoelmille kuten Adobe-GB1, Adobe-CNS1, Adobe-Japan1 ja Adobe-KR, ja lopuksi sisäänrakennetut WinAnsi- ja MacRoman-taulukot yksinkertaisille fonteille. Aistrategia, joka ei pysty tarjoamaan vastausta, siirtyy hiljaisesti seuraavaan poikkeuksen nostamisen sijaan, ja koodi, joka käy koko ketjun läpi tuloksetta, palauttaa arvon 0, jotta kutsuja voi laskea epäonnistumiset arvailun sijaan
/ToUnicode CMap (ISO 32000-1 §9.10.3) on ensimmäisenä, koska se on kartoitus, jonka luontiohjelma on kirjoittanut nimenomaan tekstin purkamista varten. Adoben standardi CMap-polku on tärkeä CJK-dokumenteille, jotka käyttävät valmiiksi määriteltyjä CMapeja kuten UniGB-UTF16-H upottamisen sijaan: HotPDF toimittaa kokoelmatiedostot resources\CMap-hakemistossa, etsii ne ajon aikana suhteessa suoritettavaan tiedostoon ja tallentaa kunkin jäsennetyn kartan välimuistiin prosessikohtaisesti. Tämä on hyvä tietää, sillä suurin niistä, Adobe-GB1-kartta, on noin 2 megatavua lähdetekstiä, jota ei haluta jäsentää uudelleen jokaisella sivulla. Jos hakemisto puuttuu, dekooderi ohittaa levyltä luettavat CMapit ja toimii upotettujen taulukoiden ja sisäänrakennettujen koodausten avulla. Tämä on lukupuolen vastine tekstin muotoiluongelmalle, jota käsitellään artikkelissa monimutkaisten kirjoitusjärjestelmien tekstin muotoilusta HotPDF:llä, jossa sama koodi-glyyfi-ero kohdataan kirjoitusvaiheessa
Kaksi hyödyllistä CMap-syntaksiansaa
CMap-tiedostot näyttävät helposti jäsenneltäviltä, mutta ne eivät ole sitä. Kaksi yksityiskohtaa aiheuttaa useimmat ensimmäisen yrityksen jäsennysvirheet. Ensimmäinen on se, että tietueiden määrä tulee ennen osion avainsanaa: osiossa lukee 2 beginbfchar, not beginbfchar 2. Jäsennin, joka odottaa määrää avainsanan jälkeen, kuluttaa numeron harhautuneena tokenina ja löytää sen jälkeen nolla merkintää jokaisesta osiosta. Vankka lähestymistapa — johon HotPDF:n lukija päätyi — on jättää määrä kokonaan huomiotta ja toistaa silmukkaa, kunnes vastaan tulee vastaava endbfchar / endbfrange -avainsana. Tämän etuna on se, että se sietää tiedostoja, joiden ilmoitetut määrät ovat virheellisiä
Toinen ansa on, että kohteet bfchar ja bfrange ovat UTF-16BE-merkkijonoja, eivät kokonaislukuja. Kohde <D83DDE00> tarkoittaa arvoa U+1F600 — eli korvikeparia (surrogate pair), joka on yhdistettävä yhdeksi koodipisteeksi. Näiden neljän tavun lukeminen big-endian-kokonaislukuna tuottaa merkityksettömän arvon jokaiselle koodipisteelle perusmerkkisivun (Basic Multilingual Plane) ulkopuolella. Emojit PDF-tiedostoissa eivät ole enää harvinaisia, joten dekooderi, joka ohittaa korvikeparien yhdistämisen, epäonnistuu tiedostoissa, joita käyttäjilläsi todella on. HotPDF jäsentää heksaliteraalin ensin raakatavuiksi ja yhdistää sen jälkeen UTF-16BE-koodiyksiköt, mikä kattaa myös ligatuurikartoitusten tuottamat monimerkkiset kohteet
Siirtymä glyyfitasolle ExtractLoadedPageGlyphs-toiminnolla
Molemmat tekstikutsut perustuvat funktioon ExtractLoadedPageGlyphs, ja sen taustalla oleva THPDFGlyphArray on myös koodisi käytettävissä. Jokainen THPDFGlyphRecord sisältää selvitetyn Unicode-koodipisteen sekä raa'an merkkikoodin, koodin tavuleveyden (1, 2 tai 4 CMapin codespacerange-määrityksen mukaan), aktiivisen fonttiresurssin avaimen ja koon, käyttäjäavaruuden X- ja Y-origon sekä vaakasuuntaisen etenemän. Tämä on riittävästi sanarajojen havaitsemisen, kohdistetun korostuksen tai mukautetun asettelualgoritmin rakentamiseen koskematta itse sisältövirtaan
var
Glyphs: THPDFGlyphArray;
I, Unresolved: Integer;
begin
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
begin
Unresolved := 0;
for I := 0 to High(Glyphs) do
if Glyphs[I].Unicode = 0 then
Inc(Unresolved);
if Unresolved > 0 then
ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
[Unresolved, Length(Glyphs)]);
end;
end;
Tietueiden Unicode = 0 laskeminen, kuten edellä esitettiin, on rehellinen tapa mitata tekstin purkulaatua dokumentissa ennen kuin luotat tekstiin jatkokäsittelyssä. Glyyfitietueet ankkuroivat myös jokaisen merkin sisältövirran lähdeoperandiin, mikä mahdollistaa HotPDF:n ladatun dokumentin tekstin etsinnän ja korvaamisen saman perustan päällä
Mitkä PDF-tiedostot eivät luovuta tekstiään?
Jotkin tiedostot estävät tekstin purkamisen kokonaan, ja on parempi havaita ne kuin toimittaa tyhjää tulostetta. Skannatut asiakirjat ovat selkein tapaus: sivu, joka on yksi suuri kuva, ei sisällä lainkaan tekstitunnisteita, joten purku palauttaa oikeutetusti tyhjän merkkijonon — korjaus tähän on tekstintunnistus (OCR), ja sivun kuvien purkaminen ladatusta PDF-tiedostosta on tuon prosessiputken ensimmäinen vaihe. Vaikeampi tapaus ovat osafontit ilman /ToUnicode-taulukkoa: jos myös /Encoding-polku ja standardit CMapit ovat tyhjiä, nämä glyyfit selvittyvät nollaksi ja näkyvät välilyönneinä tekstikutsuissa. Salattujen dokumenttien teksti purkautuu normaalisti, kunhan ne ladataan salasanalla varustetulla LoadFromFile-ylikuormituksella, jolloin tietovirrat puretaan ennen kuin tulkki näkee niitä
Yksi rajoitus on syytä todeta selkeästi: dekoodausketju lukee CMap- ja sisältövirtoja HotPDF:n Flate-polun kautta, joten fontti, jonka ToUnicode-virta käyttää epätavallista suodatinta, siirtyy seuraavaan strategiaan sivun epäonnistumisen sijaan. Käytännössä FlateDecode kattaa lähes kaiken kahden viime vuosikymmenen aikana tuotetun, ja tämä siirtymä on suunniteltu hiljaiseksi — saat parhaan mahdollisen tekstin, jonka tiedosto sallii, poikkeuksen sijaan. Sama lukupuolen objektikoneisto, joka selvittää fonttihakemistot tässä, ohjaa myös ladattujen dokumenttien metatietojen muokkausta, joten dokumenttien vastaanottoputki voi purkaa, tarkastaa ja lisätä huomautuksia yhdellä ajolla
Tekstin purku, asettelun säilyttävä hahmontaminen, glyyfitason käyttö sekä niiden päälle rakennetut etsi- ja korvaa-ominaisuudet ovat kaikki osa standardia HotPDF Component -kirjastoa Delphille ja C++Builderille — ei ulkoisia DLL-tiedostoja, ei käyttöjärjestelmän tekstipalveluita, vain Object Pascalia, jota voit jäljittää askel kerrallaan, kun outo tiedosto saapuu jonoon