Tekninen artikkeli

Tekstin etsiminen ja korvaaminen olemassa olevassa PDF-tiedostossa Delphillä

HotPDF-komponentti voi etsiä ja korvata tekstiä olemassa olevan PDF-tiedoston sisältä Delphillä ja C++Builderilla. SearchLoadedPageText ja SearchLoadedDocumentText paikallistavat merkkijonon jokaisen esiintymän glyyfitason tarkkuudella, ja ReplaceLoadedPageText sekä ReplaceLoadedDocumentText kirjoittavat täsmänneet tavut uudelleen paikoilleen — edellyttäen, että jokainen korvaava merkki voidaan koodata uudelleen alkuperäisen fontin kautta. Tätä fyysistä rajoitusta käsitellään tässä artikkelissa rehellisesti alaviitteeseen piilottamisen sijaan

Tämän ominaisuuden taustalla oleva tarve on yleensä arkinen. Yritys vaihtaa nimensä, ja kolmetuhatta arkistoitua laskua sisältää edelleen vanhan nimen. Sopimusmalli lähetettiin viime vuoden vanhentumispäivällä. Tuotekoodeja poistettiin käytöstä, ja jokainen sitä mainitseva tuotetietolehti tarvitsee sen sijaan uuden koodin. Tekstinkäsittelyohjelmassa kukin näistä on puolen minuutin työ. PDF-tiedostossa se on todella vaikea ongelma, ja sen ymmärtäminen auttaa käyttämään sovellusliittymää (API) oikein sen sijaan, että tekee virheilmoituksen, joka todellisuudessa johtuu vain määrittelyiden asettamista rajoituksista

Miksi tekstin korvaaminen PDF-tiedostossa on niin vaikeaa?

Tekstin korvaaminen PDF-tiedostossa on vaikeaa, koska PDF-sivu ei sisällä muokattavaa tekstiä — se sisältää paikoilleen aseteltuja glyyfejä. Standardin ISO 32000-1 §9.4 mukaisen tekstinnäyttömallin alaisuudessa sisältövirta ajaa operaattoreita, kuten Tj ja TJ, jotka piirtävät merkkikoodijonoja tekstin matriisin määrittämiin koordinaatteihin. Nämä koodit eivät ole Unicodea; ne ovat indeksejä mihin tahansa koodaukseen, jonka sivun fontti ilmoittaa. Kartoitus takaisin luettaviksi merkeiksi voi sijaita /ToUnicode CMapissa, koodauksen erotaulukossa (encoding difference array) tai CID-kartoitusketjussa. Tiedostossa ei ole kappaleobjektia, tekstivirtaa eikä mitään takuuta siitä, että yksi visuaalinen sana olisi tallennettu yhtenä merkkijonona

Korvaaminen tuo toisen vaikeustason dekoodauksen päälle: sinun on tiedettävä tarkalleen, mitkä alkuperäisen virran tavut tuottivat kunkin glyyfin, jotta voit liittää uudet tavut täsmälleen tuolle alueelle etkä mihinkään muualle. Tekstinpurkaja voi heittää tavujen sijaintitiedot pois saatuaan Unicoden ulos. Korvaaja ei voi tehdä niin. Siksi HotPDF jakoi työn kahden julkaisun välille — versiossa v2.251.0 rakennettiin kohdistuksen seuranta- ja etsintäkerros, ja versiossa v2.252.0 sen päälle rakennettiin uudelleenkirjoituskerros

Tekstin löytäminen: glyyfitason etsintä tavukohdistuksen seurannalla

HotPDF:n SearchLoadedDocumentText löytää etsittävän merkkijonon jokaisen esiintymän täsmäämällä sen kunkin sivun dekoodattuun Unicode-glyyfi-sekvenssiin, ei raakoihin virtatavuihin. Osuma on siis osuma riippumatta siitä, miten fontti sen koodasi. Sen alla oleva infrastruktuuri esiteltiin versiossa v2.251.0: sisältövirran jäsennin tallentaa StartOfs/EndOfs-tavuvälin jokaiselle merkkijono-operandille — mukaan lukien sen ( ) tai < > erottimet — ja jokainen dekoodattu glyyfi kantaa mukanaan TokenIndex/ItemIndex/ByteOffset-tunnisteiden kolmikkoa, joka osoittaa takaisin tarkkaan operandiin, TJ-taulukon kohteeseen ja koodiyksikköön, joka sen tuotti. Sama glyyfitulkki ohjaa purku-API:a, jota käsitellään artikkelissa tekstin purkamisesta ladatusta PDF-tiedostosta Delphissä. Etsintä yksinkertaisesti säilyttää sen alkuperätiedon, jonka purkaminen hävittää

Jokainen osuma palautuu THPDFTextMatch-tietueena, joka sisältää sivunindeksin, sisältyvän glyyfi-alueen, osuman käyttäjäavaruuden X/Y-origon ja leveyden, lähdetokenin ja kohteen indeksin sekä itse täsmänneen tekstin. Tämä riittää korostuksen, tarkistuskäyttöliittymän tai korvausvaiheen ohjaamiseen. Etsintä, joka ei löydä mitään, palauttaa tyhjän taulukon epäonnistumisen sijaan, joten kutsumalli pysyy yksinkertaisena

Eräs tarkoituksellinen suunnitteluvalinta ansaitsee huomautuksen. Kun CaseSensitive on False, vertailu muuttaa kirjainkokoa vain ASCII-merkkien osalta. Tämä on tehty suunnitellusti: täydellinen Unicode-kirjainkoon muunnos käyttäytyy eri tavalla HotPDF:n tukemissa Delphi 5:stä XE-versioihin ulottuvissa työkaluketjuissa. Etsintä-API, joka löytää eri osumia riippuen siitä, mikä kääntäjä rakensi sovelluksesi, on huonompi kuin sellainen, jolla on dokumentoitu ja ennustettava rajoitus. Latinalaiselle yritystekstille — nimille, koodeille, päivämäärille — ASCII-muunnos kattaa käytännön tarpeet

Tekstin korvaaminen: käänteinen koodaus ja kirurginen liittäminen

HotPDF v2.252.0 -versiossa lisätty ReplaceLoadedDocumentText kirjoittaa etsityn merkkijonon jokaisen esiintymän uudelleen ajamalla dekoodauskoneiston takaperin. Funktio HPDFEncodeUnicode on merkkikoodidekooderin käänteistoiminto: se käy läpi saman strategiaketjun käänteisesti — /ToUnicode-taulukon bfchar- ja bfrange-haut, koodausvirran CID-kartoituksen, Type0:n identiteettikartoitukset sekä ennalta määritellyt WinAnsi- ja MacRoman-taulukot — muuttaakseen jokaisen korvaavan merkin takaisin niiksi merkkikooditavuiksi, joita alkuperäinen fontti odottaa. Uudelleenkoodatut tavut sarjoitetaan sen jälkeen oikein muotoilluksi merkkijonoliteraaliksi tai heksamerkkijonoksi, mikä jäljittelee jäsentimen omia koodinvaihtosääntöjä (escaping rules), jotta jäsennys-uudelleensarjoituskierros on vakaa

Itse liitos tehdään kirurgisesti eikä tukkuna. Vain osuman kattama kooditavualue korvataan merkkijono-operandin sisällä; saman operandin muut tavut, tokenien väliset tyhjät välit ja kaikki ympäröivät operaattorit säilytetään sanatarkasti tavu tavulta. Merkkijonon bca korvaaminen tekstissä abcabc tuottaa tulokseksi a + korvaava teksti + bc, eikä koko operandi tuhoudu. Korvaava teksti voi olla lyhyempi tai pidempi kuin etsitty merkkijono — literaali sarjoitetaan uudelleen ja virran /Length päivitetään — ja monivirtasivun jokainen /Contents-virta käsitellään erillään, jotta sivu pysyy oikein muodostettuna

var
  Pdf: THotPDF;
  ReplaceCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
    begin
      if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
        True, ReplaceCount) then
        WriteLn(Format('%d operand rewrites performed', [ReplaceCount]));
      Pdf.SaveLoadedDocument('contract-final.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Note what the API does not do: it does not re-typeset the page. PDF has no reflow, so a replacement that is visually wider than the original will simply occupy more horizontal space and may crowd whatever was painted to its right. Same-length or near-length substitutions — dates, version strings, part numbers, name corrections — are the sweet spot. Wholesale rewording belongs in the source document, not the PDF

Miksi tekstiä ei voi korvata merkeillä, joita fontin osajoukko ei koskaan sisältänyt?

Tekstiä ei voi korvata merkillä, jota upotettu fontin osajoukko (subset) ei koskaan sisältänyt, koska merkin valitsevaa tavusekvenssiä ei yksinkertaisesti ole olemassa fontin kartoitustaulukoissa. Kun PDF-luontiohjelma upottaa osafontin, sen /ToUnicode CMap ja koodausrakenteet kattavat vain ne glyyfit, joita alkuperäinen dokumentti todella käytti. HPDFEncodeUnicode voi peruuttaa vain olemassa olevan kartoituksen: jos dokumentti ei koskaan sisältänyt kirjainta E kyseisellä fontilla, ei ole olemassa merkkikoodia, johon E voitaisiin palauttaa. Tämä on tiedoston fyysinen ominaisuus, ei minkään tietyn kirjaston rajoitus — mikään työkalu ei voi luoda glyyfikartoitusta, jota ei ole koskaan upotettu

HotPDF käsittelee epäonnistumiset varovasti. Jos jotakin korvaavan tekstin yksittäistä merkkiä ei voida koodata uudelleen, koko etsityn tekstin esiintymä ohitetaan. Ei poikkeusta, ei osittaista sotkuista tekstiä, eikä esiintymää yksinkertaisesti lasketa mukaan ReplaceCount-arvoon. Käytännön seurauksena: vertaa ReplaceCount-määrää aiemman etsinnän osumamäärään ja käsittele vajaus signaalina. Yllä olevassa päivämääräesimerkissä numeron 6 on esiinnyttävä jossakin dokumentin tekstissä kyseisellä fontilla, jotta uudelleenkirjoitus onnistuu — mikä on todennäköistä laskussa, mutta ei koskaan taattua yleisesti. Jos tarvitsemasi merkit eivät yksinkertaisesti ole saatavilla ja tavoitteena on poistaa arkaluonteinen teksti sen uudelleenmuotoilun sijaan, todellinen sisällön poistaminen on joka tapauksessa parempi työkalu. Katso tälle polulle artikkeli ladattujen PDF-tiedostojen sensuroinnista ja uudelleenrakentamisesta Delphissä

var
  Matches: THPDFTextMatchArray;
  Expected, Replaced: Integer;
begin
  Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
  Expected := Length(Matches);
  Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
  if Replaced < Expected then
    WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
      'from the font subset, or match spans multiple operands',
      [Expected - Replaced]));
end;

The second skip condition in that message is the other documented boundary: a needle that spans multiple string operands — Hello split across [(He)(llo)] TJ items, for instance — is found by search, because search matches the decoded glyph sequence, but is skipped by replace, because rewriting across operand boundaries would require merging adjacent byte spans. Search-then-verify makes both limits visible instead of silent

Mitkä asiat tiedostossa muuttuvat, kun tallennat?

Korvattu /Contents-virta tallennetaan pakkaamattomana. FlateDecode-pakatut tietovirrat puretaan muokkausta varten, ja kun HotPDF kirjoittaa uudelleenrakennetut tavut, se pudottaa virran /Filter-merkinnän ja päivittää /Length-pituuden uudelleenpakkaamisen sijaan. Tuloksena saatu PDF on täysin validi ja hahmontuu normaalisti yleisimmissä katseluohjelmissa; kompromissina on suurempi tiedostokoko jokaiselle muokatulle virralle. Tuhansia dokumentteja käsittelevässä eräajoputkessa tulee varautua tähän kasvuun tai suorittaa erillinen pakkausvaihe jälkikäteen. Se, miten uudelleenkirjoitetut objektit ovat vuorovaikutuksessa dokumentin ristiinviittausrakenteen kanssa tallennettaessa, on oma aiheensa, jota käsitellään artikkelissa objektivirroista ja inkrementaalisista päivityksistä HotPDF:ssä

Kaikki muu tiedostossa jätetään ennalleen. Koskemattomat tietovirrat säilyttävät pakkauksensa, fontteja ja kuvia ei kirjoiteta uudelleen, ja operanditason liitos tarkoittaa, että jopa muokatut virrat eroavat alkuperäisestä vain sieltä, minne osuma kohdistui. Tämä konservatiivisuus on tarkoituksellista: mitä enemmän kirjasto kirjoittaa ladattua dokumenttiaan uudelleen, sitä enemmän sillä on mahdollisuuksia rikkoa jokin luontiohjelman erikoisuus, jota se ei osannut ennakoida

Tekstin etsintä ja korvaaminen täydentää tekstin purun, sensuroinnin ja sivujen hahmontamisen HotPDF:n ladattujen dokumenttien työkalusarjassa. Kaikkia ohjaa sama sisältövirran tulkki ja ne ovat käytettävissä Delphi 5:stä aina nykyisiin RAD Studio -julkaisuihin saakka ilman ulkoisia riippuvuuksia. Täydellinen API-viite ja kokeiluversio ovat ladattavissa HotPDF Component -tuotesivulta