PDF Library for Delphi v3.539.18 ir v3.539.20 pataiso dvi vietas, kuriose nieko nekeičiantis PDF išsaugojimas vis tiek galėjo sugadinti dokumento metaduomenis: kai /CreationDate ir /ModDate nurodė tą patį eilutės objektą, automatinis ModDate atnaujinimas perrašydavo abu, o kai XMP objektas būdavo sukuriamas prieš perskaitant originalų /Metadata srautą, numatytasis paketas pakeisdavo originalą. Pataisos pakeičia žodyno nuorodas, o ne mutuoja bendrus objektus, ir užfiksuoja esamą paketą prieš tingų XMP inicializavimą
Šis scenarijus yra pati neįdomiausia operacija, kokią PDF biblioteka atlieka: įkelti failą, išsaugoti jį kitu vardu, tarp jų nieko neliesti. Puslapiai atvaizduojami vienodai prieš ir po. Turinio srautų maišos sutampa. Failas praėjo kiekvieną mūsų turėtą patikrinimą, ir vis tiek buvo neteisingas dviejose vietose, kurių jokia peržiūros programa niekada jums neparodys. Abu defektai sėdėjo skaitymo-keitimo-rašymo kelyje, per kurį eina kiekvienas tikras redagavimas, tad bet koks išsaugojimas buvo pakankamas jiems suveikti, ir abu buvo rasti tik tada, kai antras, nepriklausomas analizatorius palygino dviejų failų nevaizdines semantines ypatybes
Kodėl išsaugojus PDF pasikeičia jo CreationDate?
Nes dokumento informacijos žodynui leidžiama iš dviejų raktų nurodyti vieną netiesioginį eilutės objektą, o biblioteka atnaujindavo objektą, o ne raktą. ISO 32000-1 §7.3.10 leidžia bet kuriai žodyno reikšmei būti netiesiogine nuoroda, ir nieko §14.3.3 317 lentelėje nesakoma, kad reikšmė po /CreationDate turi būti kitas objektas nei reikšmė po /ModDate. Generatorius, kūrimo metu du kartus įrašęs tą pačią laiko žymą, visiškai teisėtai gali nukreipti abu raktus į vieną 2728 0 R – būtent taip ir padarė CJK dizaino dokumentas mūsų vietiniame korpuse
Priežastis – automatinė modifikavimo data. Jei UserModDate nenustatytas, SaveToFile prieš rašydamas iškviečia SetInfo('ModDate', ...) su dabartiniu laiku, ir tai pasiekia SetRawInfo. Senasis SetRawInfo surasdavo objektą pagal raktą ir, jei rasdavo TPDFString, iškviečia jam SetTo. Tai įrašymas vietoje į bet kurį objektą, į kurį raktas tuo metu skiria, ir kai tas objektas bendras, /CreationDate dabar taip pat praneša išsaugojimo laiką. Dokumentas vis tiek atsidaro, spausdinasi ir atvaizduojamas pikselis į pikselį kaip anksčiau, tad vizualios regresijos rinkinys praeina nemirktelėjęs
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;
Pataisa TPDFDocument.SetRawInfo yra nedidelė, o principas už jos – bendras: žodyno įrašo atnaujinimas pakeičia to įrašo nuorodą, o niekada objektą, į kurį ji atsitiktinai skyrė. Naujasis kodas perskaito esamą TPDFStringMode, kad hex eilutė liktų hex, o literal eilutė – literal, tada prideda šviežią eilutę iš FStructure.NewString(Value, StringMode) po raktu. Du kiti niuansai svarbūs tiek pat, kiek ir pagrindinis pokytis. Senoji šaka srauto reikšmės įrašui išvalydavo srautą su SetTo('') prieš jį pakeisdama – tai būtų ištuštinę reikšmę kiekvienam kitam raktui, vis dar nurodančiam į tą srautą – tad tas valymas pašalintas. O pakeistas objektas nėra ištrinamas, nes struktūra jam priklauso ir kitoms nuorodoms jo gali dar prireikti
// Prieš: mutuojamas bet kuris objektas, į kurį raktas tuo metu skiria
if Obj is TPDFString then
TPDFString(Obj).SetTo(Value);
// Po: išsaugoma reprezentacija, pakeičiama tik šio rakto nuoroda
StringMode := smLiteral;
if Obj is TPDFString then
StringMode := TPDFString(Obj).StringMode;
ID.Add(Key, FStructure.NewString(Value, StringMode));
Regresija faile Tests\SharedInfoSemantics.inc tą patį objektą sukuria sąmoningai, o ne remiasi korpuso failu: viena hex eilutė, į kurią nurodo abu datų raktai, viena tiesioginė eilutė, bendra /Title ir /Subject, vienas srautas, bendras /Author ir /Keywords. Atnaujinus vieną kiekvienos poros raktą, kitas turi vis dar skaityti savo pradinę reikšmę, o atnaujinta eilutė turi likti hex. Vieša SetInformation dokumentacija dabar tą garantiją išsako vienu sakiniu: Info lauko atnaujinimas pakeičia tik tą lauką, net kai kiti laukai nurodo į tą patį objektą
Kodėl esamas XMP paketas pakeičiamas numatytuoju?
Dėl dviejų eilučių tvarkos. TPDFDocument.GetMetadata turi greitą kelią: kai XMP laukas jau priskirtas, jis grąžina XMP.SaveToString, o ne dekoduoja /Metadata srautą iš katalogo. Kelios kvietimo vietos inicializuodavo tingiai su XMP := TPDFlibXMP.Create; XMP.LoadFromString(GetMetadata);, kas skaitosi natūraliai ir yra neteisinga: tuo metu, kai GetMetadata įvykdomas, XMP jau priskirtas, tad „šaltinis“, kuris įkeliamas, yra prieš vieną eilutę sukurto objekto serializuotas numatytasis paketas. Originalus paketas su savo dc:creator, pasirinktinėmis vardų erdvėmis ir bet kokia standartų identifikacija niekada nepasiekia objekto ir išsaugant būna perrašomas. Tos pačios automatinės modifikavimo datos pakanka jam suveikti, nes SetInfo inicializuoja XMP dar prieš liesdamas Info žodyną, kad xmp:ModifyDate neatsiliktų nuo /ModDate. Atkreipkite dėmesį, už ko šis defektas pasislėpęs: Info žodyno palyginimas iš pirmojo defekto praeina, nes /Author ir /Title /Info viduje lieka nepaliesti. Pakito tik XMP medis, ir tai pastebi tik patikrinimas, kuris tą medį analizuoja ir palygina
// Neteisingai: GetMetadata dabar serializuoja prieš tai buvusioje eilutėje sukurtą objektą
XMP := TPDFlibXMP.Create;
XMP.LoadFromString(GetMetadata);
// Teisingai: pirmiausia užfiksuojamas /Metadata srautas, tada sukuriama ir įkeliama
Source := GetMetadata;
XMP := TPDFlibXMP.Create;
XMP.LoadFromString(Source);
Pataisa daro du dalykus. TPDFDocument.EnsureXMP dabar užfiksuoja Source := GetMetadata prieš TPDFlibXMP.Create, o kiekvienas tingus inicializavimas dokumente buvo pakeistas kvietimu į ją: SetInfo, SetXMPInformation, GetXMPInformation, PDF/A, PDF/X, PDF/E, PDF/VT, PDF/VCR ir PDF/UA režimų nustatymo metodai bei metaduomenų taisymo kelias. Vieši įėjimo taškai, tokie kaip SetXMPProperty, jau ėjo per EnsureXMP, o GetXMPProperty skaito per GetDocumentMetadata, tad visas paviršius dalijasi viena inicializavimo tvarka. Viena teisinga trijų eilučių sekos kopija verta daugiau nei dešimt kopijų, kurios šiandien atsitiktinai sutampa
Dar du mažesni spąstai tame pačiame kelyje
XMP serializatorius Windows aplinkoje naudoja platformos XML rašytoją, kuris išduoda XML deklaraciją, kurios paketas neturi turėti. Senas kodas ją nukirpdavo trindamas simbolius tol, kol pasiekdavo <?xpacket. ISO 16684-1 §7.3.2 xpacket apvalkalą daro neprivalomą, ir generatorius, rašantis vien <x:xmpmeta> elementą, neperžengia standarto, tad tokiame pakete ta kilpa ištrindavo visą galiojantį dokumentą. Serializatorius dabar suranda deklaracijos baigiamąjį ?> ir pašalina tik jį. Tests\XMPRetentionSemantics.inc savo išsaugojimo patikrinimą paleidžia du kartus: vieną su apvalkalu, kitą jį nukirpus, ir tvirtina, kad pasirinktinės vardų erdvės žymuo ir originalus autorius išlieka po SetInfo, GetMetadata, SaveToString ir pakartotinio įkėlimo. Antrasis spąstas buvo preprocesoriaus simbolis: Info ir XMP sinchronizavimas SetInfo viduje buvo saugomas NOVCL, kuris nustatomas Free Pascal rinkiniams, bet XMP backendą sąlygoja operacinė sistema, o ne karkasas, nes PDFlibXMP.pas apibrėžia NO_XMP tik tada, kai OS_WINDOWS nėra. Windows Lazarus rinkinys todėl turėjo veikiantį XMP objektą ir SetInfo, kuris tyliai praleisdavo jo atnaujinimą. Sargyba dabar yra NO_XMP, tad Windows Free Pascal programa gauna tokią pačią sinchronizaciją kaip Delphi
Kaip išsaugoti originalią ModDate praėjimo metu?
Nustatykite KeepModDate TPDFlibSaveOptions viduje ir išsaugokite per SaveToFileOptions. Parinktis nustato UserModDate kvietimo trukmei, o SaveToFile tada praleidžia automatinę laiko žymą – o tai irgi tas žingsnis, kuris tingiai inicializuoja XMP objektą. Dokumentas, kurio metaduomenų niekada nelietėte ir kuriam nebuvo įjungtas joks atitikties režimas, išlaiko ir savo Info žodyną, ir /Metadata srautą tokius, kokie buvo įkelti. SetInformation(8, ...) kvietimas duoda tą patį efektą visam laikui, nes patys nustatę modifikavimo datą pažymite ją kaip valdomą naudotojo
var
Options: TPDFlibSaveOptions;
begin
FillChar(Options, SizeOf(Options), 0);
Options.OptimizeContentStreams := True;
Options.PackObjectStreams := True;
Options.KeepModDate := True; // jokios automatinės /ModDate, jokio tingaus XMP
if Lib.SaveToFileOptions('design-resaved.pdf', Options) <> 1 then
Log(Format('save failed, LastErrorCode=%d', [Lib.LastErrorCode]));
end;
Būkite sąžiningi dėl to, ką tai duoda. KeepModDate yra teisingas pasirinkimas praėjimo žingsniui, kurio išvestis turi aprašyti tą pačią reviziją kaip ir įvestis, ir neteisingas pasirinkimas viskam, kas iš tikrųjų redaguoja turinį, nes §14.3.3 tikisi, kad /ModDate atspindės naujausią modifikavimą. Jis taip pat neatitaiso atgaline data bibliotekos, kuri mutuoja bendrus objektus; jis tik išvengia to vienintelio įrašymo, kuris atskleidė defektą. Būtent abi aukščiau esančios pataisos daro įprastą išsaugojimą saugų, o parinktis padaro sąmoningą no-op sąžiningą
Kaip patikrinti, kad išsaugojimas pakeitė tik ModDate?
Ne pikseliais ir ne srautų maišomis, nes abu defektai palieka kiekvieną puslapį ir kiekvieną turinio srautą identišką baitu lygmenyje. Patikrinimas, kuris juos pagavo, yra nevaizdinė semantinė momentinė kopija, kurią padaro nepriklausomas analizatorius, nesidalijantis jokiu kodu su testuojama biblioteka – iš šaltinio failo ir iš išsaugoto failo – o po to struktūrinis palyginimas. Kopija apima Info žodyną be /ModDate, kontūrų medį, kuriame kiekviena žymė išsprendžiama į puslapio numerį, o ne į objekto numerį, vardinius tikslus ir nuorodų taikinius, išsprendžiamus taip pat, formos laukų reikšmes, priedų baitus kaip maišas, ir XMP paketą, analizuojamą kaip medį, o ne lyginamą kaip tekstą. Objektų numeriai sąmoningai į tai neįeina, nes pilnas perrašymas pernumeruoja viską, ir palyginimas pagal juos praneštų triukšmą
Išimtys svarbios tiek pat, kiek įtrauktis. /ModDate, xmp:ModifyDate ir xmp:MetadataDate turi pasikeisti, tad išmetami prieš palyginimą; failas, kurio šaltinis visai neturėjo XMP, nėra baudžiamas už tai, kad gavo paketą. Ko patikrinimas neteigia, yra lygiai taip pat aišku: esamo paketo išsaugojimas nieko nesako apie tai, ar tas paketas atitinka schemą, ar dokumentas atitinka PDF/UA ar bet kurią PDF/A dalį. Tai atskiri klausimai su atskirais įrankiais, o „metaduomenys išliko“ sumaišymas su „metaduomenys atitinka reikalavimus“ – būtent taip pirmasis defektas ir slėpėsi taip ilgai. Bibliotekos pusėje abi regresijos dabar eina per kiekvieną tikslinį praėjimą Delphi Win32 ir Win64 bei Free Pascal Win32 ir Win64, o semantinis palyginimas yra praėjimo sąlyga tikrų dokumentų korpuso etaloniniam testui
Jei dirbate lygiu žemiau šių pataisų, tai, kaip išsaugojimas perrašo objektus, aprašyta straipsnyje inkrementiniai atnaujinimai ir tik priedais išsaugojimas – tai vienintelis išsaugojimo režimas, kuriame bendras objektas tiesiog paliekamas ten, kur buvo – ir straipsnyje modifikavimo lygiai ir revizijų palyginimas, kuris yra kita vieta, kur pasenusi ar perrašyta data klaidina skaitytoją. To paties Info ir XMP poros vaizdas iš taisymo pusės, kai abi pusės suderinamos, o ne vien išsaugomos, yra straipsnyje konvertavimas į PDF/A ir metaduomenų taisymas
PDF Library for Delphi yra savoji Pascal PDF biblioteka, skirta Delphi, C++Builder ir Lazarus, o čia aprašytas skaitymo-keitimo-rašymo kelias yra tas pats, per kurį eina kiekvienas redagavimas jūsų pačių procese, tad aukščiau išvardytos garantijos galioja, ar išsaugote kartą, ar tūkstantį kartų per dieną – palaikomus kompiliatorius ir platformas rasite PDF Library for Delphi produkto puslapyje