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 arvoafiantaa kaksi merkkiä jotka jakavat saman tavuvälin, joten käsittele niitä yhtenä lähdeglyfinä ja muokkaa väli kerranPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: merkki syntetisoitiin ladonnan aikana. Päätellyt sanavälit ovat tavallinen tapaus, eikä niillä ole lainkaan lähdetavuja, jotenSourceByteOffsetpalautuu arvona -1 jaSourceByteLengtharvona 0PDF_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 diagnostinenPDF_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änytPDF_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 nollataanPDF_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