Tekninen artikkeli

PDF-inkrementaalipäivitykset Delphissä: AppendToStream-opas

PDF-tiedostojen inkrementaalipäivitykset mahdollistavat dokumentin muokkaamisen Delphi-sovelluksessa lisäämällä vain muuttuneet objektit tiedoston perään ja jättämällä alkuperäiset tavut täysin koskemattomiksi. losLab PDF Library toteuttaa tämän AppendToStream-metodilla, joka kirjoittaa vain standardin ISO 32000-1 §7.5.6 mukaisen inkrementaalisen osan. Näin ollen yhden kirjanmerkin muokkaus 2 gigatavun tiedostoon vie tulosteesta vain muutaman kilotavun täydellisen uudelleenkirjoituksen sijaan. Tämän saman mekanismin ansiosta allekirjoitettuja dokumentteja voidaan päivittää ilman, että niiden allekirjoitukset mitätöityvät

Ongelma, jonka tämä ratkaisee, on konkreettinen. Täydellinen tallennus kirjoittaa koko tiedoston uudelleen: jokainen objekti sarjoitetaan uudelleen, jokainen ristiinviittauskohdistus (cross-reference offset) lasketaan uudelleen, eikä tulosteella ole mitään tavutason yhteyttä syötteeseen. Tämä on aivan sopiva ratkaisu 40 kilotavun laskulle. Sen sijaan 2 gigatavun skannatulle arkistolle, jossa vain korjattiin kirjoitusvirhe dokumentin otsikossa, kahden gigatavun kirjoittaminen uudelleen kahdenkymmenen tavun muuttamiseksi on absurdia — ja jos tiedostossa oli digitaalinen allekirjoitus, uudelleenkirjoitus tuhosi sen

Miksi PDF-tallennus rikkoo sen digitaalisen allekirjoituksen?

PDF-tiedoston digitaalinen allekirjoitus ei allekirjoita dokumentin loogista sisältöä, vaan se allekirjoittaa fyysisen tiedoston tavualueet. Allekirjoitushakemiston /ByteRange-merkintä tallentaa tarkalleen, mitkä tiedoston osat kryptografinen tiiviste kattaa. Mikä tahansa tallennustoiminto, joka sarjoittaa nämä tavut uudelleen — jopa sellainen, joka tuottaa semanttisesti samanlaisen dokumentin — muuttaa tiivistettä, ja jokainen validointiohjelma raportoi allekirjoituksen olevan rikki. Tämä on tarkoituksellista: allekirjoitus todistaa ne tavut, jotka allekirjoittaja näki, eikä jotakin abstraktia dokumenttimallia

Inkrementaalipäivitykset ovat PDF-määrityksen tarjoama pakotie. Koska inkrementaalinen tallennus lisää uutta tietoa alkuperäisen %%EOF-merkinnän jälkeen eikä koskaan koske allekirjoitettuihin tavualueisiin, olemassa oleva allekirjoitus pysyy validina sen kattamilla tavuilla. Validointiohjelmat luokittelevat sen jälkeen lisätyt muutokset erikseen — toiseksi allekirjoitukseksi, lomakkeen täytöksi, huomautukseksi — ja päättävät, ovatko ne sallittuja muutoksia. Jokainen moniallekirjoituksen työnkulku riippuu tästä: jokainen allekirjoittaja lisää inkrementaalisen osan edellisen päälle. Jos rakennat allekirjoitusputkia, kumppaniartikkeli PAdES-allekirjoituksesta ja validoinnista Delphissä käsittelee yksityiskohtaisesti allekirjoituksen tavualueiden ja inkrementaalisten osioiden vuorovaikutusta

Miten inkrementaalipäivitykset toimivat standardin ISO 32000-1 §7.5.6 mukaan

ISO 32000-1 §7.5.6 määrittelegee mallin kolmella säännöllä. Ensinnäkin alkuperäinen tiedoston sisältö jätetään täysin koskemattomaksi — yhtäkään tavua ei siirretä. Toiseksi muuttuneet ja uudet luodut objektit lisätään viimeisen %%EOF-merkinnän jälkeen, kukin samalla objektinumerolla kuin aiemmin (muuttuneet objektit saavat vain uudemman määritelmän, joka varjostaa vanhan). Kolmanneksi lisätään uusi ristiinviittausosio (cross-reference section) ja traileri; trailerin /Prev-merkintä osoittaa takaisin edellisen ristiinviittausosion tavukohdistukseen muodostaen ketjun, jonka lukija käy läpi uusimmasta vanhimpaan selvittääkseen kunkin objektin uusimman määritelmän

Tästä rakenteesta seuraa kaksi hyödyllistä ominaisuutta. Päivitykset ovat edullisia suhteessa siihen, mikä muuttui, eivät dokumentin kokoon — lisäyksen kustannus on muokattujen objektien koko plus pieni xref/trailerin yleiskustannus. Lisäksi tiedostosta tulee oma versiohistoriansa: jokainen aiempi versio on edelleen fyysisesti mukana, joten tarkastaja voi katkaista tiedoston mistä tahansa aiemmasta %%EOF-merkinnästä ja palauttaa tarkalleen sillä hetkellä olemassa olleen dokumentin. Vaatimustenmukaisuuden työnkuluille, joiden on todistettava miltä dokumentti näytti ennen kutakin muutosta, tämä sisäänrakennettu tarkastusketju on usein ratkaiseva peruste inkrementaalisille tallennuksille

Inkrementaalisen päivityksen kirjoittaminen AppendToStream-toiminnolla

losLab PDF Library tarjoaa inkrementaalisen tulosteen funktion AppendToStream(AppendMode: Integer; OutStream: TStream): Integer kautta, joka palauttaa arvon 1 onnistuessaan ja 0 epäonnistuessaan. AppendMode-parametri määrittää, mitä kohdevirtaan kirjoitetaan. Tila 0 kirjoittaa täydellisen tiedoston: alkuperäiset lähdetavut kopioidaan ensin virtaan, minkä jälkeen lisätään inkrementaalinen osio. Tila 1 kirjoittaa vain itse inkrementaalisen osion — deltan — ja ohittaa lähdetavut kokonaan. Tila 2 kirjoittaa ensin kutsujan toimittaman etuliitteen (prefix), joka on rekisteröity SetAppendInputFromString-metodilla, ja lisää sen jälkeen päivitysosion sen päälle

Tila 1 on mielenkiintoinen järjestelmäsuunnittelun kannalta. Koska delta on itsenäinen, voit siirtää sitä riippumattomasti alkuperäisestä tiedostosta: voit tallentaa versiot erillisinä objekteina (blobs) objektitallennustilaan, replikoida vain deltat etäsivustolle tai muodostaa minkä tahansa version uudelleen yhdistämällä perustiedoston sen inkrementtiketjuun. Uudelleenmuodostussääntö on pelkkää tavujen yhdistämistä — ensin alkuperäinen tiedosto ja sitten jokainen delta järjestyksessä — koska se on juuri se rakenne, jonka §7.5.6 määrää inkrementaalisesti päivitetylle tiedostolle

Miten kirjasto laskee xref-kohdistukset kopioimatta alkuperäistä tiedostoa?

Inkrementaalisen osion sisällä olevien ristiinviittausten on sisällettävä absoluuttiset tavukohdistukset — eli sijainnit mitattuna valmiin tiedoston alusta, ei deltan alusta. Tämä luo haasteen tilassa 1: kirjoittaja ei koskaan kirjoita alkuperäisiä tavuja, mutta jokaisen sen tallentaman kohdistuksen on teeskenneltävä niiden olevan olemassa. losLab PDF Library ratkaisee tämän sisäisellä virta-adapterilla TPDFAppendSectionStream, joka tarjoaa virtuaalisen koordinaattiavaruuden sarjoittajalle. Adapteri luodaan käyttämällä alkuperäisen tiedoston tavupituutta sen peruskohdistuksena (base offset), se ilmoittaa sijaintinsa ja kokonsa kyseisenä peruskohdistuksena lisättynä tähän mennessä liitetyllä sisällöllä ja välittää vain vastakirjoitetut tavut kutsujan kohdevirtaan

Seurauksena on, että tila 1 ei koskaan luo fyysistä kopiota lähdedokumentista — ei levylle eikä muistiin. Yksinkertainen toteutus (täyden tiedoston kirjoittaminen tilapäiseen puskuriin ja sen jälkeen hännän leikkaaminen) vaatisi väliaikaisen kopion koko alkuperäisestä PDF-tiedostosta, mikä gigatavuluokan syötteillä on juuri se kustannus, jonka välttämiseksi inkrementaalipäivitykset ovat olemassa. Tämä kohdistuksen virtuaalisointitekniikka on läheistä sukua kirjastossa muualla käytetylle tavuviitteiden siirrolle; artikkeli nopeasta PDF-yhdistämisestä tavuviitteiden siirron avulla näyttää saman idean sovellettuna dokumenttien yhdistämiseen, ja opas suurten PDF-tiedostojen yhdistämiseen ja jakamiseen suoralla tiedosto-oikeudella kattaa ympäröivän I/O-arkkitehtuurin tiedostoille, jotka eivät mahdu mukavasti RAM-muistiin

Täysien tallennusten suoratoisto SaveToStream-toiminnolla

Inkrementaalinen tulostus on puolet suoratoistotarinasta; toinen puoli on se, mitä tapahtuu täydellisessä tallennuksessa. losLab PDF Libraryn SaveToStream ohjaa dokumenttisarjoittajaa suoraan kohdevirtaan sen sijaan, että hahmontaisi ensin koko dokumentin väliaikaiseksi AnsiString-muuttujaksi ja kirjoittaisi puskurin sitten yhdellä kutsulla. Vanhempi lähestymistapa toimi, mutta se tarkoitti, että jokainen täysi tallennus piti väliaikaisesti toista kokonaista tulosteen kopiota muistissa — harmitonta 10 megatavulla, kivuliasta 500 megatavulla ja ehdoton este monen gigatavun tulosteille 32-bittisissä prosesseissa. Suora sarjoitus saa muistinhuipun seuraamaan dokumentin objektirakenteita sen sarjoitetun pituuden sijaan

Jako-oikeusoppitunti: kun AppendToFile palautti arvon 0

Eräs tähän liittyvä regressio on syytä kertoa uudelleen, koska sen virhemalli yleistyy. AppendToFile(FileName) lisää inkrementaalisen päivityksen suoraan olemassa olevaan PDF-tiedostoon levyllä — mikä on luonnollinen kutsu paikalliselle audit-trail-työnkululle: lataa tiedosto, tee muutos, liitä samaan polkuun. Versiossa v3.71.2 tämä kyseinen järjestys alkoi palauttaa arvoa 0. Juurisyy oli lataajassa, ei kirjoittajassa: suurten dokumenttien tarpeenmukaisen lukemisen tukemiseksi LoadFromFile pitää lähdetiedoston kahvan auki dokumentti-objektin elinkaaren ajan, ja kyseinen kahva avattiin tilassa fmShareDenyWrite. Kun AppendToFile yritti tämän jälkeen avata saman tiedoston uudelleen kirjoittamista varten, lataajan oma jakotila esti sen, ja sovellusliittymä epäonnistui ennen kuin kirjoitti tavuakaan

Korjaus lievensi lataajan jakotilan muotoon fmShareDenyNone, mikä on turvallista juuri siksi, mitä inkrementaalinen lisäys on: se lisää tavuja tiukasti tiedoston lopun jälkeen eikä koskaan kirjoita uudelleen aluetta, jota lukijan pitkäikäinen kahva palvelee. Yleinen opetus jokaiselle, joka käärii tätä kirjastoa — tai rakentaa vastaavia suoratoistavia lataajia — on, että laiskat, kahvaa pitävät lukijat ja saman tiedoston kirjoittajat ovat ristiriidassa, ja avaamishetkellä valitsemasi jakotila on API-sopimus, ei toteutusyksityiskohta. Jos AppendToFile koskaan palauttaa arvon 0 koodissasi, tarkista ensin, pitääkö jokin muu prosessissasi oleva kohdetiedostoa hallussaan rajoittavalla jakotilalla

Todelliset kustannukset: milloin inkrementaalipäivitykset ovat väärä työkalu

Inkrementaalipäivitykset vaihtavat tiedoston koon kirjoitustehokkuuteen, eikä tämä vaihtokauppa ole aina edullinen. Jokainen versio lisää muuttuneet objektinsa, kun taas korvatut määritelmät jäävät tiedostoon, joten satoja kertoja muokattu dokumentti kerää kuolleita objekteja ja pitkän /Prev-ketjun, joka jokaisen lukijan on käytävä läpi. Mikä pahempaa, "poistettu" sisältö ei ole kadonnut: versiossa viisi poistettu teksti on edelleen fyysisesti läsnä version neljä tavuissa, ja sen voi palauttaa kuka tahansa, joka katkaisee tiedoston. Sensurointi (redaction), puhdistaminen (sanitization) tai mikä tahansa arkaluonteisen sisällön poistaminen vaatii siksi täydellisen uudelleenkirjoituksen — sensuroinnin inkrementaalinen tallennus on tietovuoto lisävaiheilla

Täysi tallennus on myös oikea valinta silloin, kun tavoitteena on tiivistäminen (kertyneiden lisäysten ja käyttämättömien objektien poistaminen), kun muutetaan koko dokumentin laajuisia ominaisuuksia kuten salausta — uudelleensalaus koskee jokaista merkkijonoa ja virtaa, joten muutoksessa ei ole mitään "inkrementaalista" jäljellä — tai kun tuotetaan puhdas toimitettava tiedosto, jonka mukana muokkaushistoria ei saisi kulkea tiedoston mukana. Kohtuullinen sääntö: käytä AppendToStream- tai AppendToFile-toimintoa, kun dokumentti on aktiivinen ja muuttuu, erityisesti kun siinä on allekirjoituksia; käytä täydellistä SaveToStream-uudelleenkirjoitusta elinkaaren rajoilla, kun dokumentti lähtee järjestelmästäsi tai sen historia on tasoitettava

Inkrementaalipäivitykset, virtuaalisen kohdistuksen deltatulosteet ja suora sarjoitus virtaan ovat kaikki osa standardia losLab PDF Library -kirjastoa Delphille, C#:lle ja VB.NET:lle; tuotesivu luettelee koko tallennus- ja lisäys-API-pinnan yhdessä edellä mainittujen allekirjoitus- ja suurtiedosto-ominaisuuksien kanssa