Techninis straipsnis

PDF šiukšlių surinkimas Delphi: žymėjimas ir šlavimas

Puslapio ištrynimas iš PDF failo neištrina jo šriftų, vaizdų ar turinio srautų. losLab PDF Library juos susigrąžina žymėjimo-šlavimo (mark-sweep) surinkėju, kuris eina per objektų grafą pirmyn nuo trailer šaknų ir pašalina kiekvieną netiesioginį objektą, kurio niekas nebepasiekia. Jis paleidžiamas pilno išsaugojimo metu, pagal numatytuosius nustatymus yra išjungtas ir grąžina pašalintų objektų skaičių

Kodėl PDF puslapių šalinimas nesumažina failo dydžio?

Todėl, kad puslapio šalinimas yra nuorodų redagavimas, o ne saugyklos operacija. DeletePages(StartPage, PageCount) atsieja puslapio objektus nuo puslapių medžio ir sutvarko į juos rodžiusius outline įrašus. Ko jis negali padaryti, tai nuspręsti, kad tų puslapių naudotas šrifto programa, turinio srautas ir vaizdo XObject dabar yra negyvi, nes ištrynimo metu niekas faile nefiksuoja, kas dar galėtų į juos rodyti. Tie objektai lieka dokumento objektų sąraše, o pilnas išsaugojimas kiekvieną iš jų vėl išrašo. Rezultatas yra skundas, nuo kurio prasideda dauguma šių palaikymo gijų: klientas ištrina devyniasdešimt procentų puslapių, išsaugo, o failas sumažėja dviem procentais. Blogiau, nutekėjimas kaupiasi. Įkelk, ištrink, išsaugok, vėl įkelk, vėl ištrink, vėl išsaugok, ir failas monotoniškai auga, nors puslapių skaičius krenta. Tai kitokia problema nei ta, kurią sprendžia šrifto poaibio kūrimas ir vaizdų mažinimas, kurie sumažina gyvus objektus. Čia objektai nėra per dideli. Jie tiesiog nebėra dokumento dalis

Šaknų rinkinys yra trailer, ne puslapių medis

PDF objektų grafas neturi atgalinio nuorodos lauko. Formatas neapibrėžia nei nuorodų skaičiaus, nei atgalinių rodyklių sąrašo, o egzistuojantys /Parent raktai priklauso konkrečioms struktūroms, pvz., puslapių medžiui, o ne visam objektų grafui. Niekas netiesioginiame objekte nepasako, kas į jį rodo, todėl klausimas "ar dar kas nors naudoja objektą 47" turi vienintelį atsakymą: eiti pirmyn nuo žinomos šaknies ir pažiūrėti, ar iki jo pasieksi. Štai kodėl surinkėjas losLab PDF Library yra žymėjimo-šlavimo surinkėjas, o ne nuorodų skaičiavimo schema

Šaknys ateina iš failo trailer (ISO 32000-1 §7.5.5). Tris raktus jas neša: /Root, dokumento katalogas iš §7.7.2, prie kurio kabo puslapių medis, vardai, outline, AcroForm ir metaduomenys; /Info, dokumento informacijos žodynas; ir /Encrypt, šifravimo žodynas. Likę du trailer raktai yra apgaulingi. /ID yra dviejų baitų eilučių masyvas, o /Prev yra sveikasis baitų poslinkis į ankstesnę kryžminės nuorodos sekciją. Nė vienas iš jų nėra netiesioginė nuoroda, todėl nė vienas neprisideda prie šaknų. losLab PDF Library į eilę įtraukia visą trailer žodyną, o ne tris pavadintus raktus, kas nieko nekainuoja ir palaiko gyvą bet kokį privatų trailer plėtinį

Pats ėjimas yra iteratyvus, o ne rekursyvus. Kai apėjimas sutinka netiesioginę nuorodą, jis įrašo tik objekto numerį ir kartą, pažymi atitinkamą langelį ir įdeda jį į FIFO eilę vietoj nedelsiamo dereferencinimo, o tai neleidžia gilioms puslapių medžių struktūroms ir ilgoms outline grandinėms apkrauti iškvietimų dėklo bei sustabdo pakartotinį to paties objekto dekodavimą. Tiesioginiai žodynai, masyvai ir srautų žodynai eina į antrą eilę, saugomą aplankytų aibės, nes realūs dokumentai turi tikrus ciklus: puslapio /Parent rodo atgal į savo puslapių medžio mazgą, o outline elementai grandinėja per /Prev ir /Next abiem kryptimis. Kartos numeriai yra atitikimo dalis, ne dekoracija. Nuoroda išsprendžiama tik tada, kai sutampa ir objekto numeris, ir karta; nuoroda į numerį, egzistuojantį kitoje kartoje, traktuojama kaip specifikacijos reikalaujamas nulinis objektas, niekada kaip gyva briauna

Kaip įjungti šiukšlių surinkimą išsaugojimo metu?

Šiukšlių surinkimas yra pasirenkamas ir priklauso išsaugojimo parinkčių įrašui. Numatytoji reikšmė yra False, nes surinkėjas yra destruktyvus perėjimas per objektų grafą, ir jokia biblioteka neturėtų tyliai ištrinti objektų, kurių iškvietėjas niekada nepaprašė ištirti

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;

Du kiti prieigos taškai pasiekia tą patį surinkėją. SetGarbageCollect(1) nustato vėliavėlę pasirinktam dokumentui, kad įprastas SaveToFile ją paisytų, o GarbageCollectObjects paleidžia perėjimą nedelsiant ir grąžina pašalintų nebenaudojamų netiesioginių objektų skaičių. Tiesioginė forma yra ta, kurią naudoti, kai norite skaičiaus žurnalui ar assert patikrai, ir verta patikrinti, nes neigiama grąžinama reikšmė nėra skaičius

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;

Šis klaidos kelias svarbesnis, nei atrodo. Objektai dekoduojami tingiai, o objektas, kuris niekada nebuvo dekoduotas, neatskleidžia jokių nuorodų. Jei surinkėjas traktuotų nedekoduojamą objektą kaip tuščią mazgą, jis nušluotų viską, kas pasiekiama tik per jį. Todėl apėjimas priverstinai dekoduoja kiekvieną objektą jį pasiekęs, o viena dekodavimo klaida nutraukia visą perėjimą su neigiamu rezultatu ir palieka dokumentą baitas į baitą identišką. Šluoti grafą, kurį suprantate tik iš dalies, yra būdas paversti pažeistą failą sunaikintu

Kas sugadina naivų PDF surinkėją?

Dvi smulkmenos, ir abi žlunga tyliai, o ne garsiai. Pirmoji yra objektų srautai. Nuo PDF 1.5 nesrautinis objektas gali gyventi suspaustas /ObjStm konteineryje (§7.5.7), o jo kryžminės nuorodos įrašas yra 2 tipo įrašas, įvardijantis konteinerį ir indeksą jame. Suspaustas objektas todėl pasiekiamas tik per savo konteinerį. Pažymėk narį, iššluok konteinerį, nes niekas jo nenurodė kaip dokumento objekto, ir gausi failą, kurio xref rodo į objektą, kuris nebeegzistuoja. Konteineris yra struktūrinė saugykla, ne dokumento duomenys, todėl jis niekada nepasirodo kaip briauna objektų grafe, per kurį einate. losLab PDF Library tai sprendžia atsiedama kiekvieną išlikusį suspaustą narį nuo jo šaltinio konteinerio dar prieš konteineriams išnykstant, po ko išsaugojimas iš naujo supakuoja išlikusius į naujus objektų srautus. Antra smulkmena yra tai, į ką iš tikrųjų nurodo srauto objektas. Baitai nėra grafo dalis. Turinio srautas, piešiantis tekstą su /F1 12 Tf, įvardija šriftą pagal resurso vardą, ir tas vardas išsprendžiamas per puslapio /Resources žodyną, todėl pasiekiamumo briauna eina puslapis → /Resources/Font → šrifto objektas, niekada per srauto duomenis. Vienintelės nuorodos, kurias srautas prideda, ateina iš jo žodyno, kur /Length, /Filter ir /DecodeParms visi gali būti netiesioginiai. Surinkėjas, nagrinėjantis srauto baitus ieškodamas nuorodų, atlieka brangų darbą veltui; surinkėjas, praleidžiantis srautų žodynus, praranda ilgio objektą ir sugadina failą

Kas nutinka objektų numeriams, kuriuos atlaisvinate

Jie tampa laisvais įrašais ir nėra pakartotinai naudojami tame pačiame išsaugojime. Šlavimas eina per objektų sąrašą mažėjančia tvarka, kad ištrynimai išliktų indekso stabilūs, sukuria paieškos indeksą iš naujo vieną kartą pabaigoje vietoj po kiekvieno pašalinimo, ir kiekvienam pašalintam objektui įrašo numerį laisvųjų sąraše su jo karta, padidinta vienetu, lygiai taip, kaip §7.5.4 numato įrašui, kuris vėliau gali būti pakartotinai naudojamas. Karta, jau pasiekusi 65535, ten ir lieka, žymėdama, kad tas numeris visam laikui pašalintas. Objektų numeriai sąmoningai nesuglaudinami. Po surinkimo failas išlaiko skyles: objektas 12 gali būti laisvas, kol 13 ir 14 naudojami, o trailer /Size vis dar praneša didžiausią numerį plius vienas, o ne išlikusių skaičių. Tai teisėta ir normalu. Pernumeravimas sutaupytų saujelę baitų kryžminės nuorodos lentelėje ir reikalautų perrašyti kiekvieną nuorodą dokumente, o tai yra būtent tokio tipo pakeitimas, kuris tyliai anuliuoja bet ką, laikantį objektų numerius iš išorės. Dydis, kurį gaunate, ateina iš objektų kūnų, ne iš xref lentelės

Kada surinkėjo negalima paleisti

Niekada laipsniško atnaujinimo metu. Surinkėjas užrakintas tik pilniems išsaugojimams, ir vėliavėlė tiesiog neskaitoma, kai dokumentas yra papildomas, o tas užraktas nėra apribojimas, kurį reikia apeiti. Laipsniškas atnaujinimas (§7.5.6) palieka pradinius baitus nepaliestus ir prideda naują kryžminės nuorodos sekciją, sujungtą su ankstesne per /Prev. Kiekviena ankstesnė versija vis dar rodo į objektus, į kuriuos visada rodė, todėl objektas, kuris nepasiekiamas dabartinėje versijoje, yra visiškai pasiekiamas senesnėje. Jo ištrynimas sugadintų kiekvieną versiją, išskyrus paskutinę, o mechanika, kodėl taip yra, aprašyta straipsnyje apie laipsniškus atnaujinimus ir append režimo išsaugojimus. Ta pati logika atmeta šiukšlių surinkimą pasirašytame dokumente, nes pilnas perrašymas, kuris padaro surinkimą įmanomą, pats yra tai, kas anuliuoja parašą

Verta aiškiai pasakyti ir tai, kuo surinkimas nėra. Tai nėra sanitarizavimo priemonė. Surinkėjas pašalina objektus, kurių niekas nenurodo; jis neturi jokios nuomonės, ar jų turinys buvo jautrus, o objektas, kuris vis dar nurodomas, lieka tuo, kuo buvo. Jei tikslas yra padaryti informaciją neatkuriamą, o ne sumažinti failą, objektų grafas yra netinkamas lygmuo, ir instrukcijų lygio redagavimas ir dokumento sanitarizavimas yra tinkama priemonė. Šie du puikiai dera būtent tokia tvarka: pirmiausia redaguok ir sanitarizuok, tada rink, kad objektai, kuriuos redagavimas atsiejo, iš tikrųjų paliktų failą. Ta pati pora egzistuoja ir resursų valymo API, kur perduodant šiukšlių surinkimo parinktį valymas po to paleidžia surinkimą ir praneša pašalintus našlaičius OrphanObjectsRemoved

Dar vienas įprotis, kurį verta įsigyti. Registruokite GarbageCollectObjects grąžinamą reikšmę bet kuriame paketiniame darbe, kuris atlieka puslapių šalinimą, ir stebėkite ją kelias savaites realių dokumentų. Nulis faile, kurį ką tik perpjovėte per pusę, reiškia, kad kažkas aukščiau vis dar laiko nuorodą, kurios nesitikėjote, dažniausiai vardų medžio įrašą, outline paskirties tašką ar AcroForm lauką, išlikusį po puslapio, prie kurio jis buvo prijungtas. Surinkėjas yra pigiausias pasiekiamumo derintuvas, kokį tik turėsite, nes jis atsako į klausimą, kurio pats PDF formatas atsisako atsakyti

Šiukšlių surinkėjas, išsaugojimo parinkčių įrašas ir resursų valymo API, aprašyti čia, yra losLab PDF Library dalis, skirta Delphi ir C++Builder, kurios produkto puslapyje pateikiama pilna išsaugojimo eigos nuoroda, įskaitant sąveiką tarp surinkimo, objektų srautų pakavimo ir linijinimo