Tekninen artikkeli

PDF-tekstin yhdistäminen sisältöstreamin tavuihin

Yksi väärä merkki laskunumerossa, ja ainoa käytettävissä oleva muokkausprimitiivi kirjoittaa koko tekstiajon uudelleen. PDF Library for Delphi sulkee tämän aukon: GetTextBlockCharContentLocation yhdistää jokaisen poimitun UTF-16-position sen tuottaneeseen sisältöstreamin käskyyn, operandiin ja koodattuun tavuväliin, ja ReplaceTextBlockCharSourceBytes korvaa vain tuon välin. Tekstin poiminta heittää tavallisesti pois kaiken mitä tähän tarvitsisit. Saat Unicode-merkit, leveydet ja geometrian, mutta alkuperä katoaa, joten lohkon 3 position 7 merkki on vain merkki. Mikä stream sen tuotti, mikä käsky, mikä operandi ja mikä tavu operandissa: kaikki poissa. Jokainen tämän päälle rakennettu pisteeditointistrategia joutuu arvaamaan, yleensä etsimällä dekoodatusta sisällöstä alimerkkijonoa ja toivomalla että se esiintyy täsmälleen kerran. Oikealla sivulla niin ei käy

Miksi koko tekstiajon uudelleenkirjoittaminen rikkoo sivun?

Koska ajo ei ole pelkkää tekstiä. ISO 32000-1 §9.4.3:n tekstin näyttävät operaattorit sisältävät TJ-operaattorin, jonka operandina on merkkijonoja ja numeerisia säätöjä vuorotteleva taulukko, ja juuri nämä numerot ovat ladontaa. Muodossa [(AB) -120 (CD)] TJ ladottu rivi sisältää merkkijonojen välissä 120 tuhannesosaa emistä olevan kernauksen. Tuota yhdistetyn tekstin sisältävän uuden Tj-operaattorin ja kernaus katoaa, rivi juoksee hieman uudelleen ja lomakkeessa arvo siirtyy ulos laatikostaan. Sama vastaväite koskee fonttia: operanditavut ovat Tf-operaattorin valitseman koodauksen koodeja, eivät Unicodea, ja yhdistelmäfontilla ne voivat olla kaksitavuisia CID-koodeja joilla ei ole yhteyttä poimimaasi merkkiin. Kun tuot ajon uudelleen, sinun täytyy tuntea oikein fontin koodaus, sen /ToUnicode-kartta ja glyfien kattavuus. Pisteeditointi kiertää kaiken tämän pysymällä tavudomainissa

Mitä GetTextBlockCharContentLocation palauttaa?

Metodi ratkaisee yhden merkin yhdeksän kentän tietueeksi, jossa jokainen kenttä on osoite eikä arvo. ContentLayer on 1-pohjainen indeksi sivun /Contents-taulukkoon tai 0, kun merkki tuli sisäkkäisestä sisällöstä. StreamObjectNumber ja StreamGeneration tunnistavat sisältävän streamin. InstructionIndex on 0-pohjainen paikka dekoodatussa sisältöohjelmassa, OperandIndex tekstimerkkijono-operandi ja ArrayElementIndex TJ-taulukon sisäinen alkio tai -1 suoran merkkijono-operandin tapauksessa. SourceByteOffset ja SourceByteLength nimeävät sen jälkeen tavuvälin dekoodatussa merkkijonossa

Var
  Lib: TPDFlib;
  ListID, Block, CharPos: Integer;
  ContentLayer, StreamObjectNumber, StreamGeneration: Integer;
  InstructionIndex, OperandIndex, ArrayElementIndex: Integer;
  SourceByteOffset, SourceByteLength, Flags: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    Lib.LoadFromFile('invoice.pdf', '');
    Lib.SelectPage(1);
    ListID:= Lib.ExtractPageTextBlocks(3);
    Try
      // Block ja CharPos tulevat omasta GetTextBlockText-skannauksestasi
      If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
        ContentLayer, StreamObjectNumber, StreamGeneration,
        InstructionIndex, OperandIndex, ArrayElementIndex,
        SourceByteOffset, SourceByteLength, Flags)= 1 Then
      Begin
        // ContentLayer = 0 tarkoittaa, että glyfi on sisäkkäisessä Form XObjectissa
        // ArrayElementIndex = -1 tarkoittaa tavallista Tj-operandia, ei TJ-taulukkoa
      End;
    Finally
      Lib.ReleaseTextBlocks(ListID);
    End;
  Finally
    Lib.Free;
  End;
End;

Haku ei maksa mitään kyselyhetkellä. Kun renderöijä dekoodaa jokaisen sisältökerroksen, se rekisteröi kulkemansa loogiset jaksot, joten positiohaku on binäärihaku järjestetystä intervallilistasta eikä jokaisen merkin kaikkien sisältöjaksojen lineaarinen läpikäynti. Mitään ei jäsennetä uudelleen kysyttäessä; kartta rakennettiin jo sen poimintavaiheen aikana jonka maksoit. Jos luettelet jo osumia PDF-tekstihakuna joka palauttaa osumien koordinaatit, sisältöposition lisääminen jokaiseen osumaan on lähes ilmaista

Tavujen muokkaaminen Unicoden sijaan

ReplaceTextBlockCharSourceBytes ottaa AnsiString-merkkijonon raakoja korvaustavuja aktiivisessa PDF-fontin koodauksessa. Koko suunnittelu on siinä, ja se on harkittu. Mitään ei transkoodata, mitään ei koodata uudelleen eikä fontista tehdä arvauksia. Kirjasto sijoittaa tavusi kohdemerkkijonon nimetyn välin päälle ja tuottaa sisältökerroksen uudelleen. Saman TJ-taulukon viereiset merkkijonot ja niiden väliset numeeriset kernaukset säilyvät tavu tavalta samoina. Ota yllä oleva asettelu: [(AB) -120 (CD)] TJ -rakenteen B-merkin sijainnista saadaan ArrayElementIndex 0, SourceByteOffset 1 ja SourceByteLength 1. Korvaa se merkillä Z, niin tuotettu sisältö sisältää (AZ)-merkkijonon jonka perässä ovat yhä -120 ja (CD), molemmat koskemattomina. Regressiotestit varmistavat juuri tämän, koska väite "säilytimme kernauksen" voi kaikessa hiljaisuudessa lakata pitämästä paikkaansa

Function EditableHere(Flags: Integer): Boolean;
Begin
  Result:= ((Flags and PDF_TEXT_CHAR_CONTENT_LOCATION_VALID)<> 0)and
    ((Flags and (PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED or
      PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT or
      PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED or
      PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER or
      PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED))= 0);
End;

// ...
If EditableHere(Flags) Then
Begin
  If Lib.ReplaceTextBlockCharSourceBytes(ListID, Block, CharPos, 'Z')= 1 Then
  Begin
    // Jokainen vanhan listan sijainti on nyt vanhentunut. Poimi uudelleen.
    Lib.ReleaseTextBlocks(ListID);
    ListID:= Lib.ExtractPageTextBlocks(3);
  End
  Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
    // Kerros muuttui poiminnan jälkeen
  Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
    // Tarkistettava lippu jäi huomaamatta tai myöhempi versio lisäsi lipun
End;

Kaksi käytännön yksityiskohtaa kannattaa painaa mieleen. Kutsu vaihtaa hetkeksi sivulle jolta tekstilista poimittiin ja palauttaa aiemmin valitun sivun sekä onnistumisen että epäonnistumisen jälkeen, joten se ei siirrä kursoria hiljaisesti. Onnistumisen jälkeen se myös tyhjentää sivuelementtien tilannekuvat, mikä mitätöi aiemmasta luettelointikierroksesta säilyttämäsi kahvat

Mitä merkkejä ei voi muokata?

Kuusi luokkaa, ja kirjasto nimeää jokaisen niistä Flags-bittimaskissa sen sijaan että epäonnistuisi epämääräisesti. Tämä on onnellista polkua tärkeämpää, koska oikeissa dokumenteissa kartoittamattomat tapaukset ovat tavallisia ja jokaisella on eri syy

  • PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: useampi poimittu UTF-16-positio laajenee yhdestä lähdeglyfistä. Yksi koodi joka /ToUnicode-merkinnässä vastaa arvoa fi antaa kaksi merkkiä jotka jakavat saman tavuvälin, joten käsittele niitä yhtenä lähdeglyfinä ja muokkaa väli kerran
  • PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: merkki syntetisoitiin ladonnan aikana. Päätellyt sanavälit ovat tavallinen tapaus, eikä niillä ole lainkaan lähdetavuja, joten SourceByteOffset palautuu arvona -1 ja SourceByteLength arvona 0
  • PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: lukemasi teksti tuli /ActualText-korvauksesta. Korvaavan merkkijonon ja lähdetavujen välillä ei ole yksikäsitteistä käänteiskuvausta, joten sijainti on vain diagnostinen
  • PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: glyfi on Form XObjectin sisällä. Tavuihin voi osoittaa, mutta useampi sivu voi piirtää Formin, joten sen muokkaaminen korkean tason API:lla olisi muutos jota et pyytänyt
  • PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: operandi oli heksamerkkijono, jossa oli UTF-16BE-tavujärjestysmerkki ja jonka nykyinen poimintapolku dekoodaa ennen fonttikartoitusta. Dekoodatun tuloksen offsetit eivät enää osoita alkuperäisiin tavuihin, joten valid-lippu nollataan
  • PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: merkkijono-operandi ja sen tekstiä näyttävä operaattori ovat kahdessa eri streamissa

Viimeinen tapaus ansaitsee oman lauseensa, koska insinöörit olettavat rutiininomaisesti ettei se voi tapahtua. ISO 32000-1 §7.8.2 sanoo, että sivun /Contents-taulukon streamit ketjutetaan ja niiden välinen jako tarvitsee osua vain leksikaaliseen rajaan. Niinpä yhdessä streamissa oleva BT /F1 16 Tf 220 340 Td (CrossLayer) ja seuraavassa oleva Tj ET muodostavat täysin laillisen sivun. Kartta säilyttää diagnostisen position mutta merkitsee sen read-onlyksi, koska operaattorin käskyindeksi kuuluu eri kerrokseen kuin operanditavut ja niiden käyttäminen toistensa osoittamiseen korruptoisi tiedoston

Miten kirjasto tietää, että kartta on edelleen kelvollinen?

Fingerprintien avulla, jotka tarkistetaan juuri ennen kirjoitusta. Jokainen poimintalista tallentaa lähdesivun sekä jokaisesta sisältökerroksesta kerroksen pituuden ja kaksi toisistaan riippumatonta rullaavaa tiivistettä: FNV-1a-tiivisteen ja DJB2-tyylisen xor-tiivisteen. Ennen kuin ReplaceTextBlockCharSourceBytes jäsentää mitään, se lukee kohdekerroksen uudelleen ja vertaa kaikkia kolmea arvoa. Mikä tahansa tavumuutos missä tahansa kerroksen kohdassa palauttaa PDFLIB_ERROR_TEXT_LOCATION_STALE-virheen eikä kirjoitusta tehdä. Tämä on tarkoituksella konservatiivista: tarkistus tehdään kerros- eikä käskytasolla, joten myös asiaan liittymätön muutos samassa sisältöstreamissa mitätöi sijaintisi. Se on oikea vaihtokauppa: offset streamissa joka on siirtynyt edes yhdellä tavulla ei ole melkein oikea vaan kyseessä on hiljainen korruptio. Sama kurinalaisuus ohjaa muuta muokkauspintaa, mukaan lukien sisältöstreamin CTM- ja clipping-tilaseuranta. Jokaisen onnistuneen korvauksen jälkeen hylkää lista ja poimi uudelleen

Read-only-kartoitus Direct Accessin kautta

DAGetTextBlockCharContentLocation antaa identtisen tietueen Direct Access -polun kautta avatulle sivulle ja käyttää samaa lippusanastoa. Se on rakenteensa vuoksi vain diagnostinen: ReplaceTextBlockCharSourceBytes toimii valitussa muokattavassa dokumentissa ja Direct Access on lukupolku. Sijaintitiedot säilyvät tekstilohkolistassa tiedostokahvan sulkemisen jälkeen, joten niitä voi käyttää offline-tarkastukseen

FileHandle:= Lib.DAOpenFileReadOnly('audit.pdf', '');
Try
  PageRef:= Lib.DAFindPage(FileHandle, 1);
  DirectList:= Lib.DAExtractPageTextBlocks(FileHandle, PageRef, 3);
  Try
    Lib.DAGetTextBlockCharContentLocation(DirectList, Block, 1,
      ContentLayer, StreamObjectNumber, StreamGeneration,
      InstructionIndex, OperandIndex, ArrayElementIndex,
      SourceByteOffset, SourceByteLength, Flags);
    // Sijainnit ovat luettavissa DACloseFile-kutsun jälkeenkin
  Finally
    Lib.DAReleaseTextBlocks(DirectList);
  End;
Finally
  Lib.DACloseFile(FileHandle);
End;

Käytä sitä kysymysten selvittämiseen, älä muuttamiseen. Millä sivuilla on tekstiä jota et voisi koskaan muokata paikan päällä? Kuinka suuri osa aineistosta saapuu /ActualText-ohituksilla? Kenen toimittajan tuloste jakaa operaattorit sisältökerrosten yli? Nämä ovat halpoja kyselyitä, kun jokaisella merkillä on osoite, ja ne kannattaa ajaa ennen kuin sitoudut korjausputkeen

Missä pisteeditointi loppuu

Pisteeditointi on skalpelli, ei tekstimoottori. Se muuttaa tavuja paikan päällä, joten alkuperäistä leveämpi tai kapeampi korvaava teksti ei juokse uudelleen, ei rivity uudelleen eikä päivitä ympäröiviä kernauksia. Yhden numeron vaihtaminen toiseen tasalevyisessä kentässä sopii hyvin. Kappaleen kirjoittaminen uudelleen ei. Eikä se ehdottomasti ole tietoturvatyökalu: glyfitavujen ylikirjoittaminen jättää alkuperäiset tavut palautettaviksi tiedoston revisiohistoriasta, joten kaikki luottamuksellisuutta vaativa kuuluu todelliseen redaktioon, joka poistaa sisällön sen peittämisen sijaan. Näiden rajoitusten vastineena saat rehellisen mallin. Jokaisella merkillä on joko käytettävä tavun osoite tai nimetty lippu joka kertoo miksi sitä ei ole, ja fingerprint-tarkistus muuttaa vanhentuneen kartan kovaksi virheeksi korruptoituneen sivun sijaan. Merkistä sisältötavuun ulottuva kartoitus ja lähdetavujen paikallinen korvaaminen kuuluvat PDF Library for Delphi -kirjaston tekstinpoiminta- ja sisällönmuokkauspintaan, joka on Delphin, C++Builderin ja Lazarusin natiivi Object Pascal -PDF-kirjasto