Tekninen artikkeli

PDF:n roskienkeruu Delphissä: mark ja sweep

PDF-sivun poistaminen ei poista sen fontteja, kuvia tai sisältövirtoja. losLab PDF Library palauttaa ne käyttöön mark-sweep-kerääjällä, joka kulkee objektigraafin läpi eteenpäin trailerin juurista alkaen ja poistaa jokaisen epäsuoran objektin, johon mikään ei enää viittaa. Se ajetaan täyden tallennuksen yhteydessä, se on oletuksena pois päältä, ja se palauttaa pudotettujen objektien lukumäärän

Miksi PDF-sivujen poistaminen ei pienennä tiedostoa?

Koska sivun poisto on viittausmuokkaus, ei tallennustoiminto. DeletePages(StartPage, PageCount) irrottaa sivuobjektit sivupuusta ja korjaa niihin osoittaneet sisällysluettelomerkinnät. Se ei kuitenkaan pysty päättämään, että sivujen käyttämä fonttiohjelma, sisältövirta ja kuva-XObject ovat nyt kuolleita, koska poistohetkellä tiedostossa ei ole mitään merkintää siitä, kuka muu saattaisi yhä viitata niihin. Nämä objektit jäävät dokumentin objektiluetteloon, ja täysi tallennus kirjoittaa jokaisen niistä takaisin ulos. Lopputulos on valitus, josta suurin osa näistä tukipyynnöistä alkaa: asiakas poistaa yhdeksänkymmentä prosenttia sivuista, tallentaa, ja tiedosto pienenee kaksi prosenttia. Pahempaa vielä, vuoto kumuloituu. Lataa, poista, tallenna, lataa uudelleen, poista uudelleen, tallenna uudelleen, ja tiedosto kasvaa monotonisesti sivumäärän laskiessa. Tämä on eri ongelma kuin se, jonka ratkaisevat fonttien osajoukotus ja kuvien alinäytteistys, jotka pienentävät elossa olevia objekteja. Tässä objektit eivät ole liian isoja. Ne eivät yksinkertaisesti enää kuulu dokumenttiin

Juurijoukko on trailer, ei sivupuu

PDF-objektigraafissa ei ole käänteisviittauskenttää. Formaatti ei määrittele viittauslaskuria eikä takaisinosoitinluetteloa, ja olemassa olevat /Parent-avaimet kuuluvat tiettyihin rakenteisiin, kuten sivupuuhun, eivät koko objektigraafiin. Mikään epäsuorassa objektissa ei kerro, kuka siihen viittaa, joten kysymykseen "käyttääkö joku yhä objektia 47" on täsmälleen yksi vastaus: kulje eteenpäin tunnetusta juuresta ja katso, päädytkö sinne. Siksi losLab PDF Libraryn kerääjä on mark-sweep-kerääjä eikä viittauslaskurijärjestelmä

Juuret tulevat tiedoston trailerista (ISO 32000-1 §7.5.5). Kolme avainta kantaa niitä: /Root, kohdan §7.7.2 dokumenttikatalogi, josta sivupuu, nimet, sisällysluettelo, AcroForm ja metatiedot kaikki riippuvat; /Info, dokumentin tietosanakirja; sekä /Encrypt, salaussanakirja. Kaksi jäljellä olevaa trailer-avainta ovat harhautuksia. /ID on kahden tavumerkkijonon taulukko, ja /Prev on kokonaislukumuotoinen tavusiirtymä edelliseen ristiviittausosioon. Kumpikaan ei ole epäsuora viittaus, joten kumpikaan ei tuota juurta. losLab PDF Library asettaa jonoon koko trailer-sanakirjan kolmen nimetyn avaimen sijaan, mikä ei maksa mitään ja pitää minkä tahansa yksityisen trailer-laajennuksen hengissä

Itse kulku on iteratiivinen, ei rekursiivinen. Kun läpikäynti kohtaa epäsuoran viittauksen, se tallentaa vain objektinumeron ja sukupolven, merkitsee vastaavan paikan ja työntää sen FIFO-jonoon sen sijaan, että se dereferoisi heti, mikä pitää syvät sivupuut ja pitkät sisällysluetteloketjut poissa kutsupinosta ja estää saman objektin dekoodaamisen kahdesti. Suorat sanakirjat, taulukot ja virtasanakirjat menevät toiseen jonoon, jota vartioi käytyjen joukko, koska todelliset dokumentit sisältävät aitoja syklejä: sivun /Parent osoittaa takaisin sen sivupuun solmuun, ja sisällysluettelokohteet ketjuuntuvat /Prev- ja /Next-avainten kautta molempiin suuntiin. Sukupolvinumerot ovat osa täsmäystä, eivät koristetta. Viittaus ratkeaa vain, kun objektinumero ja sukupolvi täsmäävät molemmat; viittausta numeroon, joka on olemassa eri sukupolvessa, käsitellään spesifikaation vaatimana null-objektina, ei koskaan elävänä kaarena

Miten roskienkeruu otetaan käyttöön tallennuksessa?

Roskienkeruu on opt-in, ja se kuuluu tallennusasetusten tietueeseen. Se on oletuksena False, koska kerääjä on tuhoisa läpikäynti objektigraafissa, eikä minkään kirjaston pitäisi hiljaa poistaa objekteja, joita kutsuja ei koskaan pyytänyt tarkastelemaan

var
  Pdf: TPDFlib;
  Opt: TPDFlibSaveOptions;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report-500pages.pdf', '') <> 1 then
      Exit;
    Pdf.DeletePages(11, 490);          // keep the first ten pages

    FillChar(Opt, SizeOf(Opt), 0);
    Opt.CompressContent := True;
    Opt.CompressFonts := True;
    Opt.OptimizeContentStreams := True;
    Opt.PackObjectStreams := True;
    Opt.GarbageCollect := True;        // drop everything the pages left behind
    Pdf.SaveToFileOptions('report-10pages.pdf', Opt);
  finally
    Pdf.Free;
  end;
end;

Kaksi muuta liittymäpistettä johtavat samaan kerääjään. SetGarbageCollect(1) asettaa lipun valitulle dokumentille, jolloin tavallinen SaveToFile noudattaa sitä, ja GarbageCollectObjects ajaa läpikäynnin heti ja palauttaa poistettujen orpojen epäsuorien objektien lukumäärän. Välitön muoto kannattaa käyttää, kun haluat luvun kirjattavaksi tai varmistettavaksi, ja sen tarkistaminen kannattaa, koska negatiivinen paluuarvo ei ole lukumäärä

var
  Removed: Integer;
begin
  Pdf.DeletePages(11, 490);
  Removed := Pdf.GarbageCollectObjects;
  if Removed < 0 then
    // The graph could not be fully decoded. Nothing was swept and the
    // document is unchanged; save it without GC or reject the input.
    LogWarning('object graph incomplete, GC skipped')
  else
    LogInfo(Format('reclaimed %d orphaned objects', [Removed]));
end;

Tuo virhepolku on merkittävämpi kuin miltä se näyttää. Objektit dekoodataan laiskasti, ja objekti, jota ei ole koskaan dekoodattu, ei paljasta yhtään viittausta. Jos kerääjä käsittelisi dekoodaamattoman objektin tyhjänä solmuna, se lakaisisi pois kaiken, joka on tavoitettavissa vain sen kautta. Siksi läpikäynti pakottaa dekoodauksen jokaista objektia koskettaessaan, ja yksittäinen dekoodausvirhe keskeyttää koko läpikäynnin negatiivisella tuloksella ja jättää dokumentin tavu tavulta muuttumattomaksi. Graafin lakaiseminen, jota vain osittain ymmärtää, on tapa muuttaa vahingoittunut tiedosto tuhotuksi

Mikä rikkoo naiivin PDF-kerääjän?

Kaksi yksityiskohtaa, ja molemmat epäonnistuvat hiljaa eivätkä äänekkäästi. Ensimmäinen on objektivirrat. PDF 1.5:stä lähtien ei-virtaobjekti voi elää pakattuna /ObjStm-säiliön sisällä (§7.5.7), ja sen ristiviittausmerkintä on tyypin 2 merkintä, joka nimeää säiliön sekä indeksin sen sisällä. Pakattu objekti on siis tavoitettavissa vain säiliönsä kautta. Merkitse jäsen, lakaise säiliö koska mikään ei viitannut siihen dokumenttiobjektina, ja olet kirjoittanut tiedoston, jonka xref osoittaa objektiin, jota ei enää ole olemassa. Säiliö on rakenteellista tallennusta, ei dokumenttidataa, joten se ei koskaan näy kaarena siinä objektigraafissa, jota kuljet. losLab PDF Library käsittelee tämän irrottamalla jokaisen säilyneen pakatun jäsenen lähdesäiliöstään ennen säiliöiden katoamista, minkä jälkeen tallennus pakkaa selviytyjät uudelleen tuoreisiin objektivirtoihin. Toinen yksityiskohta on se, mihin virtaobjekti todella viittaa. Tavut eivät ole osa graafia. Sisältövirta, joka piirtää tekstiä komennolla /F1 12 Tf, nimeää fontin resurssinimellä, ja tuo nimi ratkaistaan sivun /Resources-sanakirjan kautta, joten tavoitettavuuskaari kulkee sivu → /Resources/Font → fonttiobjekti, ei koskaan virran hyötykuorman kautta. Ainoat viittaukset, joita virta tuottaa, tulevat sen sanakirjasta, jossa /Length, /Filter ja /DecodeParms saavat kaikki olla epäsuoria. Kerääjä, joka jäsentää virran tavuja etsien viittauksia, tekee kallista työtä turhaan; kerääjä, joka ohittaa virtasanakirjat, menettää pituusobjektin ja turmelee tiedoston

Mitä vapauttamillesi objektinumeroille tapahtuu

Niistä tulee vapaita merkintöjä, eikä niitä käytetä uudelleen samassa tallennuksessa. Lakaisu kulkee objektiluettelon läpi laskevassa järjestyksessä, jotta poistot pysyvät indeksivakaina, rakentaa hakuindeksin uudelleen kerran lopussa jokaisen poiston jälkeen tekemisen sijaan, ja jokaiselle poistetulle objektille tallentaa numeron vapaaseen luetteloon sukupolvi yhdellä kasvatettuna, aivan kuten §7.5.4 määrittää merkinnälle, jota saatetaan myöhemmin käyttää uudelleen. Sukupolvi, joka on jo arvossa 65535, jää sinne, mikä merkitsee kyseisen numeron pysyvästi eläkkeelle jääneeksi. Objektinumeroita ei tarkoituksella tiivistetä. Keräyksen jälkeen tiedostoon jää aukkoja: objekti 12 voi olla vapaa, kun 13 ja 14 ovat käytössä, ja trailerin /Size ilmoittaa yhä suurimman numeron plus yksi eikä säilyneiden lukumäärää. Tämä on laillista ja normaalia. Uudelleennumerointi säästäisi kourallisen tavuja ristiviittaustaulukossa ja vaatisi jokaisen dokumentin viittauksen uudelleenkirjoittamisen, mikä on juuri se muutostyyppi, joka hiljaa mitätöi minkä tahansa, joka pitää hallussaan objektinumeroita tiedoston ulkopuolella. Takaisin saatu koko tulee objektien rungoista, ei xref-taulukosta

Milloin kerääjää ei saa ajaa

Ei koskaan asteittaisen päivityksen yhteydessä. Kerääjä on lukittu täysiin tallennuksiin, eikä lippua yksinkertaisesti lueta, kun dokumenttiin liitetään lisää sisältöä, eikä tuo lukko ole rajoitus, jota pitäisi kiertää. Asteittainen päivitys (§7.5.6) jättää alkuperäiset tavut koskemattomiksi ja liittää uuden ristiviittausosion, joka ketjuuntuu edelliseen /Prev-avaimen kautta. Jokainen aiempi versio osoittaa yhä objekteihin, joihin se on aina osoittanut, joten objekti, joka on tavoittamaton nykyisessä versiossa, on hyvinkin tavoitettavissa vanhemmassa. Sen poistaminen rikkoisi jokaisen version paitsi viimeisen, ja mekaniikka sen taustalla käsitellään artikkelissa asteittaisista päivityksistä ja liitostilan tallennuksista. Sama päättely sulkee pois roskienkeruun allekirjoitetussa dokumentissa, koska juuri se täysi uudelleenkirjoitus, joka tekee keruusta mahdollisen, mitätöi allekirjoituksen

Kannattaa myös olla selvillä siitä, mitä keruu ei ole. Se ei ole desinfiointityökalu. Kerääjä poistaa objektit, joihin mikään ei viittaa; sillä ei ole mielipidettä siitä, oliko niiden sisältö arkaluontoista, ja objekti, johon yhä viitataan, pysyy sellaisena kuin se oli. Jos tavoitteena on tehdä tiedosta palautumatonta eikä tiedostosta pienempää, objektigraafi on väärä kerros, ja käskytason peittäminen ja dokumentin desinfiointi on oikea. Nämä kaksi yhdistyvät hyvin tässä järjestyksessä: peitä ja desinfioi ensin, sitten kerää, jotta peittämisen irrottamat objektit todella poistuvat tiedostosta. Sama pari löytyy resurssien puhdistus-API:sta, jossa roskienkeruuvalinnan välittäminen saa puhdistuksen ajamaan keräyksen jälkeenpäin ja raportoimaan poistetut orvot muuttujassa OrphanObjectsRemoved

Yksi viimeinen tapa kannattaa omaksua. Kirjaa GarbageCollectObjects-kutsun paluuarvo mihin tahansa eräajoon, joka tekee sivujesi poistot, ja seuraa sitä muutaman viikon ajan todellisilla dokumenteilla. Nolla tiedostossa, jonka juuri puolitit, tarkoittaa, että jokin ylävirrassa pitää yhä hallussaan viittausta, jota et odottanut, yleensä nimipuun merkintää, sisällysluettelokohdetta tai AcroForm-kenttää, joka selvisi hengissä sivusta, johon se oli kiinnitetty. Kerääjä on halvin tavoitettavuusvianjäljitin, jonka koskaan saat, koska se vastaa kysymykseen, johon itse PDF-formaatti kieltäytyy vastaamasta

Tässä kuvattu roskienkerääjä, tallennusasetusten tietue ja resurssien puhdistus-API ovat osa losLab PDF Library -kirjastoa Delphille ja C++Builderille, jonka tuotesivu sisältää täydellisen tallennusputken viitteen, mukaan lukien keruun, objektivirtojen pakkauksen ja linearisoinnin välisen vuorovaikutuksen