PDF Library for Delphi v3.539.18 in v3.539.20 popravljata dva načina, kako lahko shranjevanje PDF, ki ne spremeni nič, vseeno pokvari metapodatke dokumenta: ko sta /CreationDate in /ModDate kazala na isti objekt niza, je samodejna posodobitev ModDate prepisala oba, in ko je bil objekt XMP ustvarjen, preden je bil prebran izvirni tok /Metadata, je privzeti paket zamenjal izvirnika. Popravka zamenjata sklice v slovarju, namesto da bi spreminjala deljene objekte, in zajameta obstoječi paket pred leno inicializacijo XMP
Ta nastavitev je najbolj dolgočasna operacija, ki jo knjižnica PDF opravi: naloži datoteko, shrani jo pod novim imenom, vmes se ne dotakni ničesar. Strani so se pred in po njej upodobile enako. Zgoščene vrednosti tokov vsebine so se ujemale. Datoteka je prestala vsako preverjanje, ki smo ga imeli, in je bila vseeno napačna na dveh mestih, ki vam jih noben upodabljalnik ne bi nikoli pokazal. Obe napaki sta sedeli na poti branje-sprememba-pisanje, skozi katero gre vsako resnično urejanje, zato je za sprožitev zadoščalo že kakršno koli shranjevanje, obe pa sta bili odkriti šele, ko je drug, neodvisen razčlenjevalnik primerjal nevizualno semantiko obeh datotek
Zakaj shranjevanje PDF spremeni njegov CreationDate?
Ker sme slovar informacij o dokumentu iz dveh ključev kazati na isti posredni objekt niza, knjižnica pa je posodabljala objekt in ne ključa. ISO 32000-1 §7.3.10 dovoljuje, da je vsaka vrednost v slovarju posredni sklic, in nič v §14.3.3, tabela 317, ne pravi, da mora biti vrednost pod /CreationDate drug objekt od vrednosti pod /ModDate. Producent, ki je ob ustvarjanju dvakrat zapisal isti časovni žig, lahko povsem zakonito usmeri oba ključa v en sam 2728 0 R, in prav to je storil dokument oblikovanja CJK v našem lokalnem korpusu
Sprožilec je samodejni datum spremembe. Če UserModDate ni nastavljen, SaveToFile pred pisanjem pokliče SetInfo('ModDate', ...) s trenutnim časom, kar doseže SetRawInfo. Stari SetRawInfo je poiskal objekt pod ključem in če je našel TPDFString, je na njem poklical SetTo. To je pisanje na mestu v tisti objekt, na katerega se ključ trenutno razreši, in kadar je ta objekt deljen, /CreationDate zdaj poroča tudi čas shranjevanja. Dokument se še vedno odpre, natisne in upodobi piko za piko kot prej, zato vizualni regresijski niz prestane brez trenutka oklevanja
var
Lib: TPDFlib;
Before, After: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('design.pdf', '');
Before := Lib.GetInformation(7); // 7 = CreationDate, 8 = ModDate
Lib.SaveToFile('design-resaved.pdf');
Lib.LoadFromFile('design-resaved.pdf', '');
After := Lib.GetInformation(7);
if Before <> After then
Log('a save that changed nothing rewrote CreationDate');
finally
Lib.Free;
end;
end;
Popravek v TPDFDocument.SetRawInfo je majhen, načelo za njim pa splošno: posodobitev vnosa v slovarju zamenja sklic tega vnosa, nikoli objekta, na katerega se je slučajno razrešil. Nova koda prebere obstoječi TPDFStringMode, da šestnajstiški niz ostane šestnajstiški in dobesedni niz dobesedni, nato pa pod ključ doda svež niz iz FStructure.NewString(Value, StringMode). Še dve podrobnosti štejeta prav toliko kot glavna sprememba. Stara veja za vnos s tokom je tok pred zamenjavo počistila s SetTo(''), kar bi izpraznilo vrednost za vsak drug ključ, ki je še kazal na ta tok, zato je tega čiščenja konec. Nadomeščeni objekt pa se ne izbriše, ker je v lasti strukture in druge reference bi ga lahko še potrebovale
// Prej: spremeni tisti objekt, na katerega se ključ trenutno razreši
if Obj is TPDFString then
TPDFString(Obj).SetTo(Value);
// Potem: ohrani predstavitev, zamenjaj le sklic tega ključa
StringMode := smLiteral;
if Obj is TPDFString then
StringMode := TPDFString(Obj).StringMode;
ID.Add(Key, FStructure.NewString(Value, StringMode));
Regresijski test v Tests\SharedInfoSemantics.inc to povezovanje zgradi namenoma, ne pa da bi se zanašal na datoteko iz korpusa: en šestnajstiški niz, na katerega kažeta oba datumska ključa, en neposredni niz, ki si ga delita /Title in /Subject, en tok, ki si ga delita /Author in /Keywords. Po posodobitvi enega ključa iz vsakega para mora drugi še vedno brati svojo izvirno vrednost, posodobljeni niz pa mora ostati šestnajstiški. Javna referenca za SetInformation zdaj jamstvo izrazi v enem stavku: posodobitev polja Info zamenja samo to polje, tudi kadar druga polja kažejo na isti objekt
Zakaj obstoječi paket XMP nadomestijo privzete vrednosti?
Zaradi vrstnega reda dveh vrstic. TPDFDocument.GetMetadata ima hitro pot: kadar je polje XMP že dodeljeno, vrne XMP.SaveToString, namesto da bi dekodiral tok /Metadata iz kataloga. Več klicev je inicializiralo leno z XMP := TPDFlibXMP.Create; XMP.LoadFromString(GetMetadata);, kar se bere naravno in je napačno: ko se GetMetadata izvede, je XMP že dodeljen, zato je »vir«, ki se nalaga, serializirani privzeti paket objekta, ustvarjenega eno vrstico prej. Izvirni paket s svojim dc:creator, imenskimi prostori po meri in morebitno identifikacijo standarda nikoli ne doseže objekta in se ob shranjevanju prepiše. Za sprožitev zadošča isti samodejni datum spremembe, ker SetInfo inicializira XMP, preden se dotakne slovarja Info, da xmp:ModifyDate ostane v koraku z /ModDate. Pomislite, za čim se ta napaka skriva: primerjava slovarja Info iz prve napake prestane, saj sta /Author in /Title v /Info nedotaknjena. Spremenilo se je le drevo XMP in to opazi le preverjanje, ki to drevo razčleni in primerja
// Napačno: GetMetadata zdaj serializira objekt iz prejšnje vrstice
XMP := TPDFlibXMP.Create;
XMP.LoadFromString(GetMetadata);
// Pravilno: najprej zajemi tok /Metadata, nato ustvari in naloži
Source := GetMetadata;
XMP := TPDFlibXMP.Create;
XMP.LoadFromString(Source);
Popravek naredi dvoje. TPDFDocument.EnsureXMP zdaj zajame Source := GetMetadata pred TPDFlibXMP.Create, vsako leno inicializacijo v dokumentu pa je nadomestil klic nanj: SetInfo, SetXMPInformation, GetXMPInformation, nastavljalnike načinov PDF/A, PDF/X, PDF/E, PDF/VT, PDF/VCR in PDF/UA ter pot popravljanja metapodatkov. Javni vstopi, kot je SetXMPProperty, so šli skozi EnsureXMP že prej, GetXMPProperty pa bere skozi GetDocumentMetadata, zato si cela površina deli en vrstni red inicializacije. Ena sama pravilna kopija trivrstičnega zaporedja je vredna več kot deset kopij, ki se danes slučajno ujemajo
Dve manjši pasti na isti poti
Serializator XMP v sistemu Windows uporablja platformski pisalnik XML, ki odda deklaracijo XML, ki je paket ne sme nositi. Stara koda jo je odstranila tako, da je brisala znake, dokler ni prišla do <?xpacket. ISO 16684-1 §7.3.2 naredi ovoj xpacket neobvezen, producent, ki zapiše goli element <x:xmpmeta>, pa je v okviru standarda, zato je zanka na takem paketu izbrisala celoten, veljaven dokument. Serializator zdaj poišče zaključni ?> deklaracije in odstrani le tega. Tests\XMPRetentionSemantics.inc svoje preverjanje ohranitve požene dvakrat, enkrat z ovojem in enkrat z odrezanim ovojem, ter zahteva, da oznaka imenskega prostora po meri in izvirni avtor preživita SetInfo, GetMetadata, SaveToString in ponovno nalaganje. Druga past je bil simbol predprocesorja: sinhronizacija Info v XMP v SetInfo je bila zaščitena z NOVCL, ki je nastavljen v gradnjah Free Pascala, zaledje XMP pa je pogojeno z operacijskim sistemom in ne z ogrodjem, saj PDFlibXMP.pas definira NO_XMP le, kadar OS_WINDOWS ni prisoten. Gradnja Lazarus v sistemu Windows je zato imela delujoč objekt XMP in SetInfo, ki je njegovo posodabljanje tiho izpustil. Zaščita je zdaj NO_XMP, tako da aplikacija Free Pascal v sistemu Windows dobi isto sinhronizacijo kot Delphi
Kako ob shranjevanju brez posegov ohranite izvirni ModDate?
Nastavite KeepModDate v TPDFlibSaveOptions in shranite skozi SaveToFileOptions. Možnost za čas klica nastavi UserModDate, SaveToFile pa nato izpusti samodejni časovni žig, ki je hkrati korak, ki leno inicializira objekt XMP. Dokument, katerega metapodatkov se niste nikoli dotaknili in za katerega ni bil vklopljen noben način skladnosti, obdrži tako svoj slovar Info kot svoj tok /Metadata takšna, kot sta bila naložena. Klic SetInformation(8, ...) ima enak učinek trajno, ker datum spremembe, ki ga nastavite sami, označi kot uporabniško nadzorovan
var
Options: TPDFlibSaveOptions;
begin
FillChar(Options, SizeOf(Options), 0);
Options.OptimizeContentStreams := True;
Options.PackObjectStreams := True;
Options.KeepModDate := True; // brez samodejnega /ModDate in lene inicializacije XMP
if Lib.SaveToFileOptions('design-resaved.pdf', Options) <> 1 then
Log(Format('save failed, LastErrorCode=%d', [Lib.LastErrorCode]));
end;
Bodite pošteni glede tega, kaj vam to prinese. KeepModDate je prava izbira za korak brez posegov, katerega izhod naj bi opisoval isto revizijo kot vhod, in napačna izbira za vse, kar vsebino resnično ureja, ker §14.3.3 pričakuje, da /ModDate odraža zadnjo spremembo. Prav tako ne popravi za nazaj knjižnice, ki spreminja deljene objekte; izogne se le tistemu enemu zapisu, ki je napako razkril. Oba zgornja popravka sta tisto, kar naredi običajno shranjevanje varno, možnost sama pa je tisto, kar naredi namerno prazno operacijo pošteno
Kako preverite, da shranjevanje ni spremenilo ničesar razen ModDate?
Ne s pikami in ne z zgoščenimi vrednostmi tokov, ker obe napaki pustita vsako stran in vsak tok vsebine bajt za bajt nespremenjena. Preverjanje, ki ju je ujelo, je nevizualni semantični posnetek, ki ga naredi neodvisen razčlenjevalnik, tak, ki si s testirano knjižnico ne deli nobene kode, iz izvorne in iz shranjene datoteke, temu pa sledi strukturna primerjava. Posnetek zajema slovar Info brez /ModDate, drevo obrisa, kjer je vsak zaznamek razrešen v številko strani in ne v številko objekta, imenovane cilje in cilje povezav, razrešene na enak način, vrednosti polj obrazca, bajte prilog kot zgoščene vrednosti in paket XMP, razčlenjen v drevo in ne primerjan kot besedilo. Številke objektov namenoma niso del tega, saj popoln prepis vse preštevilči in bi primerjava, oprta na njih, poročala samo šum
Izključitve so prav tako pomembne kot vključitve. /ModDate, xmp:ModifyDate in xmp:MetadataDate se pričakovano spremenijo in se pred primerjavo izločijo; datoteka, katere izvirnik ni nosil nobenega XMP, ni kaznovana zato, ker je dobila paket. Kar preverjanje ne trdi, je enako izrecno: ohranitev obstoječega paketa ne pove nič o tem, ali je ta paket shematsko veljaven ali ali dokument ustreza PDF/UA oziroma kateremu koli delu PDF/A. To so ločena vprašanja z ločenimi orodji in enačenje »metapodatki so preživeli« z »metapodatki so skladni« je tisto, zaradi česar se je prva napaka skrivala tako dolgo. Na strani knjižnice oba regresijska testa zdaj tečeta ob vsakem ciljnem prehodu na Delphi Win32 in Win64 ter Free Pascal Win32 in Win64, semantična primerjava pa je pogoj za uspeh pri merjenju korpusa resničnih dokumentov
Če delate na ravni pod temi popravki, sta mehanika tega, kako shranjevanje prepiše objekte, opisani v inkrementalnih posodobitvah in dodajanju na konec, kar je edini način shranjevanja, pri katerem deljeni objekt preprosto ostane tam, kjer je bil, in v ravneh sprememb in primerjavi revizij, kar je drugo mesto, kjer zastarel ali prepisan datum zavede bralca. Pogled s strani popravljanja na isti par Info in XMP, kjer se obe polovici uskladita in ne le ohranita, je v pretvorbi v PDF/A in popravljanju metapodatkov
PDF Library for Delphi je izvorna pascalovska knjižnica PDF za Delphi, C++Builder in Lazarus, opisana pot branje-sprememba-pisanje pa je ista, skozi katero gre vsako urejanje v vašem procesu, zato zgornja jamstva veljajo, ne glede na to, ali shranite enkrat ali tisočkrat na dan — podprte prevajalnike in platforme najdete na strani izdelka PDF Library for Delphi