Tekninen artikkeli

Tekstin, kuvien ja fonttien poiminta PDF:stä PDFlibPasilla

Tekstin, kuvien ja fonttien poimiminen olemassa olevasta PDF:stä kuulostaa ratkaistulta ongelmalta, kunnes ajat sen läpi todellisen dokumenttijoukon. Suunnat hakuinteksin Neljällekymmentätuhannelle asiakastiedostolle, ja vioittuminen lajitteleutuu muutamaan tunnistettavaan kekoon. Sanat tarttuvat yhteen, koska kukaan ei kertonut poimijalle, kuinka leveä rako lasketaan välilyönniksi. Muut sivut palaavat roskaa, koska osittainen fontti ei sisällä kuvaausta merkistökoodien ja todellisten merkkien välillä. Ja "yrityksen logo" paljastuu yhdeksäksi erilliseksi kuvaobjektiksi, jotka on pinottu pehmeän maskin taakse. Mikään näistä ei ole bugi kirjastossa. Kyse on erotuksesta poimintafunktion kutsumisen ja sen ymmärtämisen välillä, mitä funktio kykenee ja mitä ei kykene palauttamaan levyn tavuista

losLab PDF Library, Pascal-versio, antaa Delphi- ja C++Builder-koodille useamman kuin yhden tavan lukea kunkin näistä kolmesta virrasta, ja tasot eroavat siinä, mitä ne takaavat. Temppu on sovittaa taso työhön: hakuindeksi, redigoinnin tarkastaja ja PDF/A-preflight-läpipääsy kaikki haluavat eri asioita samalta sivulta, ja väärän kutsun tavoittelu tuhlaa vaivaa tai tuottaa tulosta, johon et voi luottaa

Tekstinpoiminnan tasot ja mitä kukin lupaa

GetPageText ottaa options-arvon nollasta kahdeksaan, ja tuo numero valitsee moottorin formaatin sijaan. Arvot 0–2 ajavat kevyen läpikäynnin, joka kelpaa pikaisessa esikatselussa. Arvot 3–8 reitittävät asettelutietoisen moottorin kautta, joka rakentaa rivit ja välit uudelleen siitä, missä glyfit todella sijaitsevat sivulla. Tuon välin sisällä vaihtelu merkitsee: 4 ja 6 jakavat tulosteet sanoiksi, 5 ja 6 emittoivat glyfikohtaiset leveydet, ja 7 palauttaa pelkkää tekstiä, josta fontti, väri ja lohkon metadata on tahallaan pudotettu. Vaihtoehto 7 on se, joka syötetään hakuindeksille, koska indeksi haluaa sanoja eikä mitään muuta

Mikään option-asetus ei voi pelastaa dokumenttia, joka ei koskaan sisällyttänyt tietoa alun perinkään. PDF kuvaa merkkikoodit glyfin muodoiksi, ja ainoa asia, joka kääntää nuo koodit takaisin luettavaksi tekstiksi, on fontin ToUnicode CMap (ISO 32000-1 §9.10). Kun osittainen fontti toimitetaan ilman sellaista, jokainen poimija on jumissa. Tämä kirjasto, kopiointi ja liittäminen katseluohjelmassa, kilpaileva työkalupakki: kaikki ne reducedoituvat arvailemaan glyfinimistä tai palauttamaan ei mitään. Käytännön vastaus on tunnistaminen, ei sankarillisuutta. Pisteytä sivu heikkoluottamukselliseksi ja lähetä se OCR:lle, koska roskan hiljainen indeksointi on pahempaa kuin sen myöntäminen, ettet osaa lukea sitä

Kaavio Delphi-PDF-tekstinpoiston tasoista: GetPageText-asetukset 0–8 ohjautuvat kevyelle kierrokselle tai asettelutietoiseen moottoriin, ja sivut, joiden ositettujen fonttien ToUnicode CMap puuttuu, turvautuvat OCR:ään
GetPageText:n asetusarvot 0–8 valitsevat kevyen esikatselukierroksen ja asettelutietoisen moottorin väliltä, vaihtoehto 7 on varattu hakemistoinnille ja puuttuvat ToUnicode-CMapit ohjataan OCR:lle

Tapauksiin, joita tasovalinnat eivät kata — oma tokenisointi, sisältövirran forensiikka, omaa sääntösiä noudattava tekstsuppilo — dekooderi on saatavilla yhden tason alempana. TPDFExtractor rakennetaan sivun resurssisanakirjan ja fonttikokoelman päälle. Sen ExtractTextW-metodi ajaa raakojen sisältövirran tekstioperaatioiden läpi saman fonttikoneiston, jotta Unicode palautuu, ja sen OnFindObject-tapahtuma antaa kunkin objektin sinulle sitä mukaa kun ne virtaavat ohi. Useimmat koodit eivät koskaan tarvitse kaivata näin syvälle. Ne sovellukset, jotka tarvitsevat, ovat niitä, jotka ovat iloisia siitä, että taso on julkinen eikä haudattu

Sijoitetut lohkot: hakutreffien ja redigointitarkastuksen yksikkö

Pelkkä teksti kertoo, mitä sivu sanoo. Sooner or later tuote tarvitsee myös tietää, missä se sanoo sen, jotta se voi korostaa hakutreffin, piirtää laatikon redigointiehdokkaan ympärille tai ankkuroi merkinnän oikeaan paikkaan. ExtractPageTextBlocks palauttaa kahvan tekstiajojen luetteloon, ja kukin ajo kantaa tekstinsä, rajaavansa laatikon sekä fontin nimen ja koon, johon se asetettiin:

var
  Pdf: TPDFlib;
  Blocks, I: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Pdf.SelectPage(1);
    Blocks := Pdf.ExtractPageTextBlocks(0);
    for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
      Writeln(Format('%s  [%s %.1f pt at %.0f,%.0f]',
        [Pdf.GetTextBlockText(Blocks, I),
         Pdf.GetTextBlockFontName(Blocks, I),
         Pdf.GetTextBlockFontSize(Blocks, I),
         Pdf.GetTextBlockBound(Blocks, I, 0),
         Pdf.GetTextBlockBound(Blocks, I, 1)]));
    Pdf.ReleaseTextBlocks(Blocks);
  finally
    Pdf.Free;
  end;
end;

Yksi yksityiskohta tällä alueella kompastelee integraatioita enemmän kuin mikään muu. SetTextExtractionArea, SetTextExtractionWordGap ja SetTextExtractionOptions ovat dokumenttitason tilaa, joka säilyy, eivätkä argumentteja, joita annat per kutsu. Määritä aluerajoitus yhdelle ominaisuudelle — esimerkiksi vain otsikkonauhan lukeminen dokumentin luokittelemiseksi — ja se hiljaisesti typistää joka ikisen poiminnan, joka seuraa samalla kahvalla, mukaan lukien asettelutietoiset GetPageText-tasot, joihin tartut myöhemmin. Joko nollaa poimintatila loogisten tehtävien välillä tai anna kullekin tehtävälle oma dokumenttikahvansa

Sanavälin kynnys on vipu sille ensimmäiselle vikakasalle, sanoille, jotka tarttuvat yhteen. SetTextExtractionWordGap kertoo asettelumoottorille, kuinka paljon horizontaalista tilaa — mitattuna sivun omaa glyfiväliä vasten — erottaa sanan seuraavasta. Tiheä taulukko haluaa pienemmän raon kuin löyhästi aseteltu markkinointisivu, joten dokumenttiluokkaa kohti viritetty kynnys voittaa yhden globaalin vakion. Se säilyy dokumentilla kuten muukin poimintatila, joten aseta se tarkoituksellisesti ennemmin kuin kerran ja unohda

Kaavio, joka osoittaa asiakirjatason PDF-poistotilan Delphissä säilyvän kutsujen läpi yhdellä kahvalla nollaukseen asti, mikä estää myöhempien poistojen hiljaisen katkeamisen
Poistoalue, sanaväli ja asetusvalinnat pysyvät dokumenttin handlessa, joten yhtä ominaisuutta varten rajattu alue hiljaisesti lyhentää kaikki myöhemmät poistot, kunnes tila nollataan tai handle vaihdetaan

Kuvat: alkuperäiset virrat, ei ruutukaappauksia

Väärä tapa saada kuvat ulos PDF:stä on renderöidä sivu ja rajata se. Tämä ottaa uudelleennäytteen pikseleistä, paistaa sisään kaikki kierto ja heittää menetään sen, mikä alkuperäinen oli. GetPageImageList sen sijaan luettelee todelliset kuvaresurssit, joihin sivu viittaa, ja kukin kohde palauttaa ominaisuutensa ja alkuperäisen, häiriintymättömän datansa:

var
  ImgList, I: Integer;
begin
  Pdf.SelectPage(1);
  ImgList := Pdf.GetPageImageList(0);
  for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
  begin
    Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
    Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
      Format('page1-img%.2d.bin', [I]));
  end;
  Pdf.ReleaseImageList(ImgList);
end;

Tarkista GetImageListItemFormatDesc ennen kuin oletat mitään kohteesta, koska se, mihin sivu viittaa, on harvoin yksi siisti kuva per näkyvä kuva. Pehmeä maski näkyy omana erillisenä kohteenaan. Sama XObject toistuu usein monella sivulla, joten deduplikoi sisältötiivisteen mukaan ennen kuin arkistoit "kaikki kuvat" -viennin, tai kirjoitat saman logon sata kertaa. CMYK-JPEG:t tarvitsevat värinhallinnan käyttöä alavirrassa, tai ne renderöityvät käänteisinä katseluohjelmissa, jotka ottavat kanavat nimellisarvonaan. Kun haluat dokumenttlaajuisen inventaation sivun sijaan kerrallaan, FindImages yhdessä SetFindImagesMode kanssa skannaa koko tiedoston yhdellä läpikäynnillä

On yksi raja, joka kannattaa nostaa esiin sidosryhmien kanssa ennen kuin kukaan kirjoittaa hyväksymiskriteerejä: kuvanpoiminta palauttaa vain rasteriresursseja. Logo tai kaavio, joka on piirretty vektoripolkuna, ei ole resurssimerkityksessä kuva, eikä se koskaan näy missään kuvaluettelossa, olkoon kuinka selvästi se näyttää kuvalta ruudulla. Kun vaatimus todella on toimittaa tuo kaavio tiedostona, rehellinen lähestymistapa on renderöidä sivun alue bitmapiksi, mikä on eri operaatio eri tarkkuudella. Kaksi tyyppiä tulosta eivät kuulu samaan vientikansioon ilman nimilappua, joka sanoo, kumpi on kumpi

Vertailu Delphi-PDF-sivun renderoinnista kuvien sieppaamiseksi ja alkuperäisten kuvaratojen poistamisesta GetPageImageListillä, mukaan lukien pehmeämaski-, kaksois-XObject- ja CMYK-varaukset
Renderöinti ja rajaus näytteistävät pikselit uudelleen ja heittävät pois alkuperäisen kuvadatan, kun taas GetPageImageList luettelee tallennetut kuvarahstot ominaisuuksineen ja häiriintymättömine streameineen

Fontit: auditointipinta, ei vientiominaisuus

Fontti-API vastaa kysymyksiin fonteista. Se ei anna sinulle itse fonttitiedostoja, ja tuo ero muokkaa kaikkea, mitä voit rakentaa sen varaan. Kun FindFonts on skannannut dokumentin, luettelointi käy fontit läpi ID:n mukaan, ja ominaisuuskutsut raportoivat siitä fontista, joka on parhaillaan valittuna:

var
  I: Integer;
begin
  Pdf.FindFonts;
  for I := 1 to Pdf.FontCount do        // fonttien indeksointi alkaa luvusta 1, ei 0
    if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
      Writeln(Format('%s  type=%d  embedded=%d  subset=%d',
        [Pdf.FontName, Pdf.FontType,
         Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;

Valvo silmukkarenaita. Fontti-indeksit kulkevat 1:stä FontCount:iin, kun taas tekstilohkon ja kuvaluettelon indeksit muutamaa kappaletta ylempänä ovat nollapohjaisia. Vie yhden käytännön toiseen ja saat off-by-one-virheen, joka joko ohittaa ensimmäisen fontin tai juoksee luettelon lopun yli, ja se menee läpi kevyestä testauksesta, koska useimmissa dokumenteissa on useita fontteja ja väärä näyttää edelleen uskottavalta. Ole myös selkeä laajuudesta. Tällä API:lla ei ole tavutasoista vientiä. Mikään kutsu ei palauta upotettua fonttiohjelmaa TTF- tai OTF-tiedostona, ja luettelointi sekä metadatan tarkastus ovat koko malli. Tuo malli silti kattaa sen, mitä tuotantotyö oikeasti fonteilta kysyy: osittaisen tunnistuksen nimikuvion mukaan, upotusauditoinnit ennen arkistointimuunnosta (upottamaton fontti on kova PDF/A-este, kuten PDF/A- ja PDF/UA-preflight Delphissä käy läpi), ja koodaustdiagnostiikka silloin kun poimintaluottamus laskee. Myös lisenssisyytä on, miksi raja on täällä. Osittainen fonttiohjelma on lisensoitua materiaalia ja, puuttuvat useimmat glyfinsä, hyödytön asennettavana fonttina joka tapauksessa. Sen käsittely auditointimetadatana ennemmin kuin poimittavana omaisuutena on asema, jota voit puolustaa

Tuo viimeinen kanto vetää painonsa triagessa. Aja GetFontEncoding jokaiselle fontille, lue se osittelulipun rinnalla, ja voit ennustaa poimintalaatua ennen yksittäisen merkin vetämistä. Sivu, jonka fontit ovat kaikki ositeltuja epästandardeilla koodauksilla, on pelkällä tarkastelulla OCR-ehdokas, mikä antaa eräputken reitittää sen oikein ilman, että se ensin tuhlaa epäonnistunutta poimintakierrosta siihen

Poiminta mittakaavassa ilman dokumenttien lataamista

Eräputkessa koko dokumentin lataaminen vain yhden sivun lukemiseksi on hukattua I/O:ta, ja se kertyy nopeasti koko joukon yli. Yksikutsuiset variantit, ExtractFilePageText ja ExtractFilePageTextBlocks, ottavat tiedostonimen, salasanan ja sivunumeron suoraan ja ohittavat täyden latauksen. Gigatavuun kokoisille tiedostoille on olemassa vielä alempi vaihde. Suoran pääsyn polku avaa tiedoston suoratoistoxref-lukuina, joten DAOpenFileReadOnly:n jälkeinen DAExtractPageText koskettaa vain niitä objekteja, joita tuo yksi sivu oikeasti tarvitsee. Se tulee mukana konventiosihiftillä, joka kannattaa painaa mieleensä: DA-funktiot osoittavat sivut PageRefillä, objektiviitekahvalla, jonka saat DAFindPage:ltä, ei koskaan raalla sivunumerolla. Anna numero sinne, minne kahva kuuluu, ja kutsu operoi väärää objektia nostamatta virhettä, mikä on pahin debugattava virhelaji. Loput suoran pääsyn työkalupakista on esitelty suurten PDF-tiedostojen yhdistämisessä, jakamisessa ja suorassa pääsyssä

Jos on olemassa yksittäinen tapa, joka erottaa poimintakoodin, joka kestää todellisen joukon, koodista, joka limpautuu, se on sivun käsittely epäluotettuna syötteenä ennemmin kuin puhtaana tietolähteenä. Teksti, joka on eri mieltä siitä, mitä katseluohjelma renderöi, on lähes aina koodausongelma, ligatuuri, joka reducedoituu yhdeksi glyfiksi, tai osittainen fontti, jolta puuttuu ToUnicode-merkinnät, ja korjaus on mitata luottamusta ja ohjata huonot sivut OCR:lle, ei taistella tavuja vastaan. Fontti-API ei koskaan tuota TTF:ää tai OTF:ää, suunnitelman mukaisesti, joten rakenna fonttityönkulut auditointikysymysten ympärille. Ja pysyvä poimintatila, alue-suorakulmio ennen kaikkea, on asetus, jonka omistat dokumenttikahvan eliniän ajan, ei parametri, jonka unohdat yhden kutsun jälkeen. Kun saat nuo kolme refleksia oikein, loput API:sta käyttäytyy

Evaluaatioversiot, demoprojektit ja täydellinen poiminta-API-viite ovat losLab PDF Library for Delphi -tuotesivulla