Tekninen artikkeli

PDF-tietojen peittäminen (redaction) ja N-up-sivujen yhdistäminen Delphissä HotPDF-kirjastolla

Työpöydällesi laskeutuu pyyntö: ota erä valmiiksi renderöityjä tiliotteita, peitä tilinumerot ja tulosta kaksi sivua arkille paperin säästämiseksi. Molemmat tehtävän osat ovat sisältövirran kirurgiaa PDF-tiedostossa, jota et itse luonut, joten käytettävissä ei ole tuttua sivun piirtoalustaa (canvas) eikä fontinhallintaa, johon nojata. Muokkaat suoraan ladatun asiakirjan objektikaaviota ja lisäät raakoja piirto-operaattoreita sivulle, jonka jokin toinen työkalu on asetellut. HotPDF tarjoaa tähän tarkalleen kaksi sisääntulopistettä, ja niistä vaarallisempi on se, joka vaikuttaa vaarattomalta

HotPDF on alkuperäinen VCL PDF -komponentti Delphille ja C++Builderille. Sen round-nine-julkaisun ladattujen asiakirjojen API toi ensimmäiset metodit, jotka _luovat_ täysin uutta sisältöä levyltä avaamallesi sivulle sen sijaan, että sivu olisi rakennettu tyhjästä. Käsittelemme tässä kahta niistä: RedactLoadedRect, joka maalaa peittävän suorakulmion alueen päälle, ja StitchLoadedPage, joka skaalaa sivun ja piirtää sen toiselle sivulle. Molemmat toimivat kirjoittamalla ISO 32000-1 §8.5 -standardin mukaisia sisältövirran operaattoreita sivun /Contents-virtaan. Sen ymmärtäminen, mitä nämä operaattorit tekevät, ja yhtä tärkeää, mitä ne eivät tee, on ero toimivan työkalun ja tietoturvaloukkauksen välillä

Operaattorien lisääminen ladattuun sivuun

Kun rakennat sivun normaalilla HotPDF-API:lla, komponentti omistaa sisältövirran ja sarjallistaa TextOut- ja verktorikutsusi puolestasi. Ladattu sivu on toisenlainen: sen /Contents on olemassa oleva virta-objekti, mahdollisesti jaettu tai osa sisältötaulukkoa, ja sinun on liityttävä siihen rikkomatta jo olemassa olevaa. Round-nine toi kolme pientä apuohjelmaa, jotka tekevät tästä turvallista. NewIndirectStream varaa uuden epäsuoran THPDFStreamObject-olion tyhjällä puskurilla ja /Length 0 -merkinnällä; ResolveLoadedStream seuraa epäsuoraa viitettä taustalla olevaan virtaan; ja AppendLoadedStream kirjoittaa raa'at tavut virran loppuun ja kirjoittaa /Length-arvon uudelleen, jotta tallennettu objekti pysyy oikeassa muodossa

Molemmat julkiset metodit noudattavat samaa kaavaa. Etsitään sivun /Contents, selvitetään se virraksi, ja jos käyttökelpoista virtaa ei ole, luodaan sellainen ja liitetään se. Tämän jälkeen lisätään operaattorit. Koska uudet tavut menevät virran _loppuun_, maalausmalli (painter's model) takaa, että ne renderöidään kaiken sen päälle, mitä alkuperäinen asettelu piirsi. Tämä järjestys on koko peittävän suorakulmion mekanismin taustalla, ja se on myös syy siihen, miksi suorakulmio ei ole sitä, mitä useimmat ihmiset olettavat

RedactLoadedRect: peittävä suorakulmio, ei poistaminen

RedactLoadedRect ottaa nollapohjaisen sivun indeksin, neljä käyttäjäavaruuden koordinaattia ja kolme värikomponenttia välillä 0–1:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statement.pdf') > 0 then
    begin
      // Cover the account-number band on page 1 with solid black.
      // Coordinates are PDF user space: origin bottom-left, points.
      Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
      Pdf.SaveLoadedDocument('statement-covered.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Konepellin alla metodi tuottaa sisältövirtaan kolme operaattoria: täyttövärin asetus DeviceRGB-avaruudessa (r g b rg), suorakulmiopolku (x y w h re) ja täyttö (f). Leveys ja korkeus johdetaan kaavoilla X2 - X1 ja Y2 - Y1, joten välität kaksi vastakkaista kulmaa ja annat metodin laskea laajuuden. Välitä väriksi 0, 0, 0 saadaksesi mustan palkin, tai 1, 1, 1 valkoiselle palkille, joka sulautuu valkoiseen sivuun. Koordinaatit ovat ladatun sivun omassa käyttäjäavaruudessa, mikä tarkoittaa, että origo on vasemmassa alakulmassa ja yksikkönä on piste. Se tarkoittaa myös sitä, että tarvitset sivun /MediaBox-laatikon sijoittaaksesi mitään tarkasti; GetLoadedPageBox arvolla pbMediaBox tarjoaa tämän

Lue tämä kahdesti: täytetty suorakulmio peittää sisällön vain visuaalisesti, se ei poista sitä. Teksti, kuva tai vektorigrafiikka suorakulmion alla on edelleen mukana PDF-tiedostossa, edelleen objektikaaviossa ja kenen tahansa kopioitavissa, joka kopioi sivun, ajaa tekstinpurkajan tai yksinkertaisesti poistaa suorakulmiosi sisältövirrasta. Tämä on visuaalista peittämistä (masking), ei tietojen poistamista (redaction) juridisessa tai tietoturvamerkityksessä. Jos olet piilottamassa todella arkaluonteisia tietoja – kuten tilanumeroita, potilastietoja tai henkilöllisyyksiä – niiden peittäminen mustalla laatikolla ja tiedoston lähettäminen on tietovuoto, joka vain odottaa paljastumistaan. Todellinen tietojen poistaminen (redaction) vaatii taustalla olevien sisältöobjektien poistamista, ei niiden päälle maalaamista

Metodin nimi sanoo "Redact", mikä toimii hyödyllisenä varoituksena siitä, miten lopputulosta luetaan väärin, ei lupauksena siitä, mitä se poistaa. Toteutus on tästä rehellinen omassa kommentissaan: se kutsuu itseään "visuaalisen poiston primitiiviksi" (visual redaction primitive) ja huomauttaa, että sisällön poistava suodatus vaatisi sisältövirran tulkin, joka käy läpi ja kirjoittaa uusiksi olemassa olevat operaattorit. HotPDF:n ladatun asiakirjan polku ei tee sitä tässä. Joten turvallinen sääntö on kapea: käytä RedactLoadedRect-metodia ei-arkaluonteiseen visuaaliseen peittämiseen – kuten luonnos-vesileiman piilottamiseen, alueen tyhjentämiseen ennen kuvakaappausta tai vanhentuneen logon peittämiseen sisäisessä vedoksessa. Heti kun laatikon alla olevalla asialla olisi merkitystä sen vuotaessa, tämä metodi on väärä työkalu, ja oikea ratkaisu on luoda asiakirja uudelleen ilman kyseisiä tietoja tai käyttää todellista sisällön poistoputkea

StitchLoadedPage: skaalaa, siirrä, piirrä

Monisivuinen asemointi (N-up imposition) on helpompi ongelma, koska mitään ei piiloteta, vaan asiat vain järjestetään uudelleen. StitchLoadedPage ottaa kohdesivun indeksin, lähdesivun indeksin, X/Y-siirtymän (offset) ja skaalaustekijän, ja se piirtää lähdesivun kohdesivulle kyseiseen paikkaan ja kokoon:

// Overlay page 2 (index 1) onto page 1 (index 0),
// scaled to 70% and nudged up-right.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);

// Convenience 2-up: source page on the right half of the target.
Pdf.StitchLoadedPageSideBySide(0, 1);

Lisättävä operaattorimerkkijono on standardi muunnos-ja-piirto-sekvenssi: q grafiikkatilan tallentamiseksi, cm-matriisi, joka kuljettaa skaalauksen diagonaalilla ja siirtymän käännöspaikoissa, /StitchSrc Do ulkoisen objektin kutsumiseksi ja Q tilan palauttamiseksi. q/Q-pari on tärkeä: se eristää muunnoksen, jotta yhdistetty sivu ei vuoda koordinaattijärjestelmäänsä mihinkään sen jälkeen lisättävään. Metodi suojautuu myös ilmeisiltä virheiltä – kuten indeksit alueen ulkopuolella, kohde sama kuin lähde, ei-positiivinen skaalaus (jonka se pakottaa arvoon 1.0) – ja poistuu hiljaisesti heittämättä poikkeusta, joten tarkista syötteesi, sillä hiljainen tekemättä jättäminen näyttää identtiseltä onnistumisen kanssa

StitchLoadedPageSideBySide on kevyt mukavuusmetodi yleisen metodin päällä. Se lukee kohteen media-boxin leveyden, puolittaa sen ja kutsuu StitchLoadedPage-metodia tällä puolella leveydellä X-siirtymänä ja kiinteällä skaalauksella 0.5, asettaen lähteen oikealle puoliskolle. Tämä kovakoodattu 0.5 olettaa, että lähteellä ja kohteella on sama leveys; jos näin ei ole, lähde ei täytä puoliskoaan siististi, ja haluat käyttää yleistä StitchLoadedPage-metodia skaalauksella, jonka lasket itse molemmista media-boxeista

Yksinkertaistettu XObject-strategia ja sen ISO-kompromissi

Tässä kohdassa toteutus tekee tietoisen oikopolun, joka sinun on tiedettävä ennen kuin luotat tulosteeseen eri katseluohjelmissa. Oikeaoppinen N-up-asemointi käärii lähdesivun sisällön Form XObjectiin – itsenäiseen piirrettävään objektiin, jonka ISO 32000-1 §8.10.1 -standardin mukaan on sisällettävä määritykset /Type /XObject, /Subtype /Form ja oma rajauslaatikko /BBox. HotPDF:n round-nine-julkaisun yhdistäminen (stitch) ei rakenna tätä käärettä. Sen sijaan se rekisteröi lähteen _sivusanakirjan itsessään_ suoraan kohteen /Resources /XObject -osion alle nimellä StitchSrc, ja piirtää sen sitten Do-operaattorilla. Sivusanakirja ja Form XObject jakavat tarpeeksi sisältömallistaan – molemmat viittaavat sisältövirtaan ja resurssisanakirjaan – joten monet lukijat renderöivät tuloksen

Se ei kuitenkaan ole standardin mukainen Form XObject. Siitä puuttuu /Subtype /Form -tunniste ja oma /BBox, mikä tarkoittaa, että tiukka vastaanottaja on oikeutettu ohittamaan Do-kutsun tai rajaamaan sen toisin kuin odotat. Tämän kierroksen tekniset huomautukset (TechnicalNotes) sanovat sen suoraan: lähestymistapa "renderöityy useimmissa lukijoissa", mutta "ei ole tiukasti ISO-yhteensopiva Form XObject", ja täysi yhteensopivuus vaatisi todellisen Form XObject -virran syntetisointia erillisenä vaiheena. Käsittele siis yhdistämistulosta kuten mitä tahansa ei-standardinmukaista rakennetta: varmista se katseluohjelmissa, joita asiakkaasi todellisuudessa käyttävät, äläkä luota tähän polkuun, jos tarvitset arkistokelpoisia tai tiukan validaattorin läpäiseviä PDF-tiedostoja. Sama kurinalaisuus koskee kaikkea, mitä rakennat ladatun objektikaavion päälle, minkä vuoksi PDF-esitarkastus (preflight) Delphissä ansaitsee paikkansa julkaisuputkessa aina, kun muokkaat asiakirjoja ohjelmallisesti

Mihin nämä sopivat ja mihin eivät

Molemmat metodit ovat sisältövirtatyökaluja, joten ajatusmalli on sama kuin suorassa piirtämisessä. Jos olet rakentanut sivuja tyhjästä komponentilla, näiden kutsujen taustalla olevat vektori- ja värioperaattorit näyttävät tutuilta artikkelista HotPDF-piirtäminen Delphissä; ero on vain siinä, että tässä lisäät tietoa jonkun muun luomaan virtaan sen sijaan, että omistaisit sen itse. Pidä mielessä kolme rajaa:

  • Tietojen peittäminen on kosmeettista. RedactLoadedRect maalaa sisällön päälle eikä koskaan poista sitä. Kaikelle arkaluonteiselle tiedolle luo lähdeasiakirja uudelleen tai käytä todellista sisällönpoistoa – musta laatikko ei ole tietoturvaa
  • Yhdistäminen (stitch) ei ole standardinmukaista suunnittelun vuoksi. Lähdesivuun viitataan näennäisenä XObjectina ilman §8.10.1 mukaista /Subtype /Form -merkintää ja /BBox-laatikkoa, joten varmista renderöinti katseluohjelmissasi ja vältä sitä, jos vaaditaan tiukkaa validointia
  • Koordinaatit ovat sivun käyttäjäavaruutta. Origo vasemmassa alakulmassa, pisteet, sivun oman media-boxin mukaisesti. Lue laatikko GetLoadedPageBox-metodilla ennen minkään sijoittamista, sillä lataamasi sivu ei välttämättä ole sen kokoinen kuin oletit

Näissä rajoissa käytettynä tämä kaksikko kattaa todellisen työnkulun: järjestä sivuja uudelleen tulostusta varten, peitä ei-luottamuksellisia alueita ja kirjoita tulos takaisin SaveLoadedDocument-metodilla – kaikki ilman täyttä uudelleenrenderöintiä. Ladatun asiakirjan API, joka sisältää nämä yhdistämis- ja peittämisprimitiivit, toimitetaan HotPDF-komponentin mukana Delphille ja C++Builderille yhdessä saman kierroksen lomakekenttä-, huomautus- ja FDF-metodien kanssa