Tehnični članak

Zakaj shranjevanje PDF brez sprememb pokvari Info in XMP

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

Spreminjanje deljenega niza Info v PDFlibPas: /CreationDate in /ModDate zakonito kažeta na en objekt niza 2728 0 R, stari SetRawInfo je poklical SetTo na tistem, na kar se je ključ razrešil, in prepisal oba datuma s časom shranjevanja, novi SetRawInfo pa pod ključ doda svež niz in ohrani način šestnajstiškega niza
Posodobitev vnosa v slovarju zdaj zamenja sklic tega vnosa in ne spremeni deljenega objekta, zato en sam samodejni zapis ModDate ne more več spremeniti CreationDate, nadomeščeni objekt pa se obdrži za druge sklice
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

Vrstni red lene inicializacije XMP v PDFlibPas: ustvarjanje objekta XMP pred klicem GetMetadata povzroči, da hitra pot serializira privzeti paket in izpusti dc:creator, imenske prostore po meri in identifikacijo standarda, medtem ko zajem Source pred TPDFlibXMP.Create naloži izvirni tok /Metadata iz kataloga
Vsako shranjevanje je sprožilo zamenjavo, ker SetInfo inicializira XMP, da xmp:ModifyDate ostane v koraku z /ModDate, zato vsaka lena inicializacija v dokumentu zdaj teče skozi en sam EnsureXMP, ki obstoječi paket zajame pred ustvarjanjem objekta
// 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

Nevizualno semantično preverjanje shranjevanj PDFlibPas: neodvisen razčlenjevalnik brez deljene kode posname slovar Info brez /ModDate, obris in strani ciljev, vrednosti obrazcev, zgoščene vrednosti prilog in drevo XMP, nato pa izvorno datoteko primerja s shranjeno, pri čemer /ModDate, xmp:ModifyDate in xmp:MetadataDate izpusti kot pričakovane spremembe
Pike in zgoščene vrednosti tokov ob obeh napakah ostanejo bajt za bajt enake, zato primerjava dela na razrešeni semantiki in ne na številkah objektov, preživeli metapodatki pa se pošteno poročajo kot ohranjeni in ne kot shematsko veljavni ali skladni s PDF/UA in PDF/A

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