losLab PDF Library gali sukurti baitas į baitą identišką PDF išvestį identiškai įvesčiai, kai iškviečiama SetDeterministicDocumentID(1). Numatytuoju atveju antraštės /ID masyvas yra sisteminio laikrodžio MD5 santrauka, todėl du to paties generatoriaus paleidimai skiriasi bent tais baitais. Deterministinis režimas /ID išveda iš stabilios sėklos, ir tai atkuria versijų atkuriamumą
Simptomas dažniausiai pasirodo CI aplinkoje, dar prieš kam nors pradedant jo ieškoti. Šablonas nepakito, įvesties įrašas nepakito, šriftai nepakito, o sugeneruotas PDF vis tiek kiekvieną kartą sumaišosi kitaip. Kūrimo talpyklos niekada nepataiko. Adresuojama pagal turinį saugykla per naktinę versiją sukaupia po naują dvejetainį objektą. Baitų lygio regresijos skirtumai užsidega failuose, kurių niekas nelietė. Susekus skirtumą iki pačių baitų, beveik visada tai — tas pats saujelė šešioliktainių skaitmenų, esančių failo trailerėje
Kam skirtas antraštės ID masyvas
Antraštės /ID yra failo tapatybės žymė, o ne turinio kontrolinė suma. ISO 32000-1 §14.4 apibrėžia jį kaip dviejų baitų eilučių masyvą: pirmasis elementas — nuolatinis identifikatorius, priskiriamas dokumento sukūrimo metu ir turintis išlikti po kiekvieno vėlesnio redagavimo, o antrasis elementas — kintantis identifikatorius, kurį rašytojas atnaujina kiekvieną kartą modifikavus failą. Kartu jie leidžia sistemai nuspręsti, ar du failai yra vieno dokumento redakcijos, ar du nesusiję dokumentai. §7.5.5 praktiškai paverčia šį įrašą privalomu, nes antraštėje turi būti /ID visada, kai joje yra ir /Encrypt
Specifikacijoje nieko nesakoma apie tai, kaip skaičiuoti šią reikšmę. Rekomenduojama santrauka iš tokių dalykų kaip dabartinis laikas, failo kelias, failo dydis ir dokumento informacijos žodynas, o sisteminis laikrodis — tai ingredientas, dėl kurio rezultatas tampa unikalus. Tai lygiai ta savybė, kurios reikia tapatybei, ir lygiai ta savybė, kuri sunaikina atkuriamumą, todėl tai turi būti aiškus jungiklis, o ne tylus elgesio pokytis
Kodėl ta pati versija kiekvieną kartą sukuria kitokį PDF?
Todėl, kad numatytasis identifikatorius kildinamas iš generavimo momento. Istoriškai losLab PDF Library kūrė /ID eilutes iš dabartinio laiko žymos MD5, todėl dokumentas, sukurtas du kartus per sekundės skirtumą, neša du skirtingus nuolatinius identifikatorius, net kai visi kiti failo baitai identiški. Tolesnės grandinės kaina reali: kūrimo sistema, indeksuojanti artefaktus pagal santrauką, niekada negali pakartotinai panaudoti PDF žingsnio, dublikatus šalinanti objektų saugykla laiko po vieną kopiją kiekvienai versijai vietoj vienos kopijos kiekvienam dokumentui, o žiūrovas, tikrinantis dvejetainį skirtumą, turi įrodyti, kad vienintelis pokytis — triukšmas, prieš pasitikėdamas likusia skirtumo dalimi. Deterministinis /ID generavimas egzistuoja tam, kad pašalintų šį triukšmą, ta pačia dvasia kaip išdėstymo stabilumo darbas, aprašytas užrašuose apie objektų srautus ir kryžminės nuorodos srautus
Perjungimas į atkuriamą identifikatorių
Deterministinis režimas — pasirenkamas, taikomas kiekvienam dokumentui atskirai ir pagal nutylėjimą išjungtas, todėl esama išvestis lieka nepakitusi, kol jo nepaprašysite. SetDeterministicDocumentID priima 0 arba 1 ir grąžina 1, kai reikšmė priimta, 0 — bet kam už šio diapazono ribų; GetDeterministicDocumentID praneša dabartinę būseną. SetDocumentIDSeed pateikia aiškią sėklos eilutę, kuri viršija viską kita, o tuščios sėklos perdavimas grąžina kildintą sėklą. GetDocumentFileID nuskaito /ID[0] po išsaugojimo, kad galėtumėte tai užregistruoti ar patikrinti
var
Lib: TPDFlib;
FileID: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed('invoice-4471-rev3');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Invoice 4471');
Lib.SaveToFile('invoice.pdf');
FileID := Lib.GetDocumentFileID; // identical on every run
finally
Lib.Free;
end;
end;
Atnaujinimas įvyksta išsaugojimo metu, o ne tada, kai perjungiate vėliavėlę, todėl deterministinio režimo įjungimas dokumento kūrimo pabaigoje vis tiek įsigalioja. Tai taip pat reiškia, kad pakeista sėkla į failą patenka su kitu pilnu išsaugojimu: nustatykite sėklą A, išsaugokite, nustatykite sėklą B, išsaugokite, ir du failai neš skirtingus identifikatorius, o sėklos A atkūrimas atkurs pradinę reikšmę. Aiški sėkla — tinkamas pasirinkimas visada, kai jūsų dokumentas turi natūralų stabilų raktą, tokį kaip sąskaitos numeris, įrašo redakcija ar git commit identifikatorius, nes ji atsieja identifikatorių nuo atsitiktinių metaduomenų
Iš kur ateina sėkla, kai jos nenurodote patys?
Be aiškios sėklos, losLab PDF Library ją kildina iš dokumento būsenos, kuri turėtų būti nekintanti tarp identiškų pakartotinio generavimo atvejų: PDF versijos antraštės, puslapių skaičiaus ir kiekvieno įrašo dokumento informacijos žodyne. Eilutės ir pavadinimų reikšmės imamos tiesiogiai, kiti objektų tipai prisideda savo serializuota forma, o visa tai sumaišoma į /ID eilutes. Svarbi pasekmė — CreationDate ir ModDate yra informacijos žodyno dalis, taigi ir sėklos dalis pagal projektą. Du paleidimai gauna tą patį identifikatorių tik tada, kai jie iš tikrųjų sukuria tuos pačius dokumento metaduomenis
Lib.SetDeterministicDocumentID(1);
// No SetDocumentIDSeed: the seed is derived from document state,
// so the timestamps in the Info dictionary have to be pinned.
Lib.SetInformation(2, 'Quarterly Report'); // Title
Lib.SetInformation(5, 'reporting-service 4.2'); // Creator
Lib.SetInformation(7, 'D:20260101000000Z'); // CreationDate
Lib.SetInformation(8, 'D:20260101000000Z'); // ModDate
Lib.SaveToFile('report.pdf');
ModDate užfiksavimas raktu 8 atlieka dvigubą darbą, ir čia žmonės dažniausiai apsigauna. Vien deterministinis /ID nepadaro failo baitas į baitą identiško, nes išsaugojimo kelias antspauduoja ModDate dabartiniu laiku, nebent iškviesantysis jį nustatė aiškiai. Rakto 8 nustatymas pažymi reikšmę kaip pateiktą iškviesiančiojo ir slopina tą antspaudavimą. Jei norite ne tik atkuriamo identifikatoriaus, bet ir atkuriamo failo, laikykite metaduomenų laiko žymas kūrimo įvestimi: kildinkite jas iš šaltinio įrašo arba iš fiksuotos epochos, niekada — iš Now
Kodėl ID perrašymas sugadina šifruotą PDF?
Todėl, kad /ID[0] šifruotame dokumente nėra vien metaduomenys — tai raktinė medžiaga. ISO 32000-1 §7.6.3.3 2 algoritmas įtraukia pirmąjį failo identifikatoriaus elementą į šifravimo rakto skaičiavimą standartiniam saugumo tvarkytojui 2–4 redakcijose, kartu su papildytu slaptažodžiu, /O reikšme ir leidimų bitais. Gautas raktas tada sukuria /U patikrinimo eilutę, kurią skaitytuvas tikrina atveriant, o failo raktas kildinamas ir talpinamas iškviečiant Encrypt arba kraunant šifruotą dokumentą — abu šie veiksmai vyksta prieš išsaugojimą. Identifikatoriaus perrašymas išsaugojimo metu todėl sukurtų struktūriškai galiojantį failą, kurio /U patikrinimas nepavyktų pakartotinai atveriant: ne subtilų sugadinimą, o dokumentą, kurio niekas negalės atverti, įskaitant jus pačius. Štai kodėl deterministinis atnaujinimas taikomas tik dokumentams, neturintiems šifravimo būsenos, ir kodėl šifruotas dokumentas išlaiko tą /ID, kurį jau turėjo, nesvarbu, ar deterministinis režimas įjungtas, ir šis nustatymas tiesiog neturi jokio poveikio tam keliui. Susijęs redakcijos apdorojimas ir leidimų semantika aprašyti straipsnyje apie PDF šifravimo ir leidimų auditą. Taip pat verta pastebėti, kad šifravimo atkūrimo kelias atnaujina tik /ID[1], keičiantį identifikatorių, lygiai taip, kaip numato §14.4
Kodėl priaugantys išsaugojimai išlaiko pradinį identifikatorių
Antroji riba — pridėjimo režimas. Priaugantis atnaujinimas palieka kiekvieną ankstesnį failo baitą nepaliestą ir po jo įrašo naują redakciją, o /ID[0] nuolatinumas visame §14.4 kontekste kaip tik ir praneša vartotojui, kad nauja redakcija priklauso tam pačiam dokumentui kaip ir senoji. Jo perrašymas nutrauktų tą ryšį, prieštarautų jau failo viduje esančioms redakcijoms ir trukdytų parašo semantikai, nes parašas apima konkretaus dokumento konkrečios redakcijos baitų intervalą. Todėl losLab PDF Library atnaujina deterministinį identifikatorių tik pilnų išsaugojimų metu ir niekada pridėjimo režimu, taip išsaugant garantiją, aprašytą straipsnyje apie PDF priaugančius atnaujinimus ir pridėjimą prie srauto
Vienas siaurumo taškas identifikatoriaus generavimui
Visas /ID generavimas losLab PDF Library dabar sueina per vieną vidinę procedūrą, NewFileIDString, ir būtent tai daro deterministinį jungiklį patikimą, o ne pataisą vienam kodo keliui. Tuščio dokumento kūrimas, tingus trūkstamo /ID masyvo kūrimas pagal poreikį ir šifravimo pirštų atspaudo atkūrimo kelias — visi ją kviečia, todėl yra lygiai viena vieta, kur sisteminis laikrodis galėtų sugrįžti. Tai taip pat reiškia, kad būsimi variantai, tokie kaip turiniu grįstas identifikatorius, yra vienos funkcijos pakeitimas, o ne viso serializatoriaus auditas
function BuildQuote(const Seed: WideString): AnsiString;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed(Seed);
Lib.SetInformation(7, 'D:20260101000000Z');
Lib.SetInformation(8, 'D:20260101000000Z');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Quote 8812');
Result := Lib.SaveToString;
finally
Lib.Free;
end;
end;
// Regression guard: two independent builds, one byte sequence.
if BuildQuote('quote-8812') = BuildQuote('quote-8812') then
WriteLn('reproducible')
else
WriteLn('nondeterminism leaked into the output');
Įtraukite šį palyginimą į savo testų rinkinį, prieš pasikliaudami atkuriama išvestimi bet kur kitur, nes jis garsiai sugriūva, vos tik kokia nors nauja funkcija vėl įneša laiko žymą. Kitaip atkuriamumas yra savybė, kuri tyliai nyksta, o vienas patvirtinimas dviem atminties viduje esantiems išsaugojimams kainuoja beveik nieko, vykdant jį kiekvienoje versijoje
Čia parodyta deterministinio identifikatoriaus API dalyvauja losLab PDF Library Delphi ir C++Builder platformoms, kartu su pilna dokumento informacijos, šifravimo ir priaugančio išsaugojimo dokumentacija