Tekninen artikkeli

Miksi muuttumaton PDF-tallennus turmelee Info- ja XMP-tiedot

PDF Library for Delphi v3.539.18 ja v3.539.20 korjaavat kaksi tapaa, joilla mitään muuttamaton PDF-tallennus saattoi silti turmella asiakirjan metatiedot: kun /CreationDate ja /ModDate viittasivat samaan merkkijono-objektiin, automaattinen ModDate-päivitys kirjoitti molemmat uudelleen, ja kun XMP-objekti luotiin ennen alkuperäisen /Metadata-streamin lukemista, oletuspaketti korvasi alkuperäisen. Korjaukset vaihtavat sanakirjaviittauksia sen sijaan että mutatoisivat jaettuja objekteja, ja kaappaavat olemassa olevan paketin ennen XMP:n laiskaa alustusta

Asetelma on vähiten kiinnostava toimenpide, jonka PDF-kirjasto tekee: lataa tiedosto, tallenna se uudella nimellä, älä koske välissä mihinkään. Sivut renderöityivät identtisesti ennen ja jälkeen. Sisältöstreamien tiivisteet täsmäsivät. Tiedosto läpäisi jokaisen tarkistuksen, joka meillä oli, ja se oli silti väärin kahdesta kohdasta, joita yksikään renderöijä ei koskaan näyttäisi sinulle. Molemmat viat istuivat lue-muokkaa-kirjoita-polussa, jonka jokainen oikea muokkaus kulkee läpi, joten mikä tahansa tallennus riitti laukaisemaan ne, ja molemmat löytyivät vasta kun toinen, riippumaton jäsennin vertasi kahden tiedoston ei-visuaalista semantiikkaa

Miksi PDF:n tallennus muuttaa sen CreationDate-arvoa?

Koska asiakirjan tietosanakirja saa viitata kahdesta avaimesta yhteen epäsuoraan merkkijono-objektiin, ja kirjasto päivitti objektia avaimen sijaan. ISO 32000-1 §7.3.10 sallii minkä tahansa sanakirja-arvon olevan epäsuora viittaus, eikä mikään §14.3.3 taulukossa 317 sano, että /CreationDate-avaimen arvon on oltava eri objekti kuin /ModDate-avaimen arvon. Tuottaja, joka kirjoitti saman aikaleiman kahdesti luontihetkellä, voi täysin laillisesti osoittaa molemmat avaimet yhteen ainoaan 2728 0 R -objektiin, ja juuri niin teki paikallisessa korpuksessamme ollut CJK-suunnitteluasiakirja

Laukaisija on automaattinen muokkauspäivämäärä. Ellei UserModDate ole asetettu, SaveToFile kutsuu SetInfo('ModDate', ...) -metodia kuluva aika -argumentilla ennen kirjoittamista, mikä päätyy SetRawInfo-metodiin. Vanha SetRawInfo etsi objektin avaimen alta ja kutsui SetTo-metodia, jos löysi TPDFString-olion. Se on paikan päällä tehtävä kirjoitus siihen objektiin, johon avain kullakin hetkellä ratkeaa, ja kun tuo objekti on jaettu, /CreationDate ilmoittaa nyt myös tallennusajan. Asiakirja avautuu, tulostuu ja renderöityy edelleen pikseli pikseliltä kuten ennenkin, joten visuaalinen regressiosarja menee läpi silmääkään räpäyttämättä

Jaetun Info-merkkijonon mutaatio PDFlibPasissa: /CreationDate ja /ModDate viittaavat laillisesti yhteen merkkijono-objektiin 2728 0 R, vanha SetRawInfo kutsui SetTo-metodia siihen objektiin johon avain ratkesi ja kirjoitti molemmat päivämäärät tallennusajalla, ja uusi SetRawInfo lisää tuoreen merkkijonon avaimen alle säilyttäen hex-merkkijonotilan
Sanakirjamerkinnän päivitys vaihtaa nyt tuon merkinnän viittauksen sen sijaan että mutatoisi jaettua objektia, joten yksi automaattinen ModDate-kirjoitus ei voi enää muuttaa CreationDate-arvoa, ja syrjäytetty objekti säilytetään muita viittauksia varten
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;

Korjaus TPDFDocument.SetRawInfo-metodissa on pieni, ja sen taustalla oleva periaate on yleinen: sanakirjamerkinnän päivitys vaihtaa tuon merkinnän viittauksen, ei koskaan objektia, johon se sattui ratkeamaan. Uusi koodi lukee olemassa olevan TPDFStringMode-arvon, jotta hex-merkkijono pysyy hexinä ja literaalimerkkijono literaalina, ja lisää sitten tuoreen merkkijonon FStructure.NewString(Value, StringMode) -kutsusta avaimen alle. Kaksi muuta yksityiskohtaa ovat yhtä tärkeitä kuin itse päämuutos. Vanha haara stream-arvolliselle merkinnälle tyhjensi streamin SetTo('')-kutsulla ennen sen korvaamista, mikä olisi tyhjentänyt arvon jokaiselta muulta avaimelta, joka osoitti tuohon streamiin, joten tuo tyhjennys on poistettu. Eikä syrjäytettyä objektia poisteta, koska rakenne omistaa sen ja muut viittaukset voivat tarvita sitä edelleen

// Ennen: mutatoi objektia, johon avain kullakin hetkellä ratkeaa
if Obj is TPDFString then
  TPDFString(Obj).SetTo(Value);

// Jälkeen: säilytä esitysmuoto, vaihda vain tämän avaimen viittaus
StringMode := smLiteral;
if Obj is TPDFString then
  StringMode := TPDFString(Obj).StringMode;
ID.Add(Key, FStructure.NewString(Value, StringMode));

Tests\SharedInfoSemantics.inc-tiedoston regressiotesti rakentaa aliaksen tarkoituksella sen sijaan että nojautuisi korpus-tiedostoon: yksi hex-merkkijono, johon molemmat päivämääräavaimet viittaavat, yksi suora merkkijono, jonka /Title ja /Subject jakavat, ja yksi stream, jonka /Author ja /Keywords jakavat. Kun kummastakin parista päivitetään toinen avain, toisen on edelleen luettava alkuperäinen arvonsa ja päivitetyn merkkijonon on edelleen oltava hexiä. SetInformation-metodin julkinen viite kertoo takeen nyt yhdellä lauseella: Info-kentän päivitys korvaa vain tuon kentän, vaikka muut kentät viittaisivat samaan objektiin

Miksi olemassa oleva XMP-paketti korvautuu oletuksilla?

Kahden rivin järjestyksen takia. TPDFDocument.GetMetadata-metodilla on nopea polku: kun XMP-kenttä on jo asetettu, se palauttaa XMP.SaveToString-kutsun tuloksen sen sijaan että purkaisi /Metadata-streamin luettelosta. Useampi kutsukohta alusti laiskasti muodossa XMP := TPDFlibXMP.Create; XMP.LoadFromString(GetMetadata);, mikä lukee luontevasti ja on väärin: siihen mennessä kun GetMetadata ajetaan, XMP on asetettu, joten ladattava ”lähde” on yhden rivin aiemmin luodun objektin serialisoitu oletuspaketti. Alkuperäinen paketti dc:creator-kenttineen, mukautettuine nimiavaruuksineen ja mahdollisine standarditunnisteineen ei koskaan päädy objektiin ja kirjoitetaan yli tallennuksessa. Sama automaattinen muokkauspäivämäärä riittää laukaisemaan sen, koska SetInfo alustaa XMP:n ennen kuin koskee Info-sanakirjaan, jotta xmp:ModifyDate pysyy /ModDate-arvon tahdissa. Huomaa, minkä taakse tämä vika piiloutuu: ensimmäisen bugin Info-sanakirjavertailu menee läpi, koska /Info-sanakirjan /Author ja /Title ovat koskemattomia. Vain XMP-puu muuttui, ja vain sitä puuta jäsentävä ja vertaileva tarkistus huomaa sen

XMP:n laiskan alustuksen järjestys PDFlibPasissa: XMP-objektin luominen ennen GetMetadata-kutsua saa nopean polun serialisoimaan oletuspaketin ja pudottamaan dc:creator-kentän, mukautetut nimiavaruudet ja standarditunnisteen, kun taas Source-arvon kaappaaminen ennen TPDFlibXMP.Create-kutsua lataa alkuperäisen /Metadata-streamin luettelosta
Mikä tahansa tallennus laukaisi vaihdon, koska SetInfo alustaa XMP:n pitääkseen xmp:ModifyDate-arvon /ModDate-arvon tahdissa, joten asiakirjan jokainen laiska alustus kulkee nyt yhden EnsureXMP:n kautta, joka kaappaa olemassa olevan paketin ennen objektin luomista
// Väärin: GetMetadata serialisoi nyt edellisellä rivillä luodun objektin
XMP := TPDFlibXMP.Create;
XMP.LoadFromString(GetMetadata);

// Oikein: kaappaa /Metadata-stream ensin, sitten luo ja lataa
Source := GetMetadata;
XMP := TPDFlibXMP.Create;
XMP.LoadFromString(Source);

Korjaus tekee kaksi asiaa. TPDFDocument.EnsureXMP kaappaa nyt Source := GetMetadata -sijoituksen ennen TPDFlibXMP.Create-kutsua, ja jokainen asiakirjan laiska alustus korvattiin kutsulla siihen: SetInfo, SetXMPInformation, GetXMPInformation, PDF/A-, PDF/X-, PDF/E-, PDF/VT-, PDF/VCR- ja PDF/UA-tilojen asettajat sekä metatietojen korjauspolku. Julkiset sisääntulopisteet kuten SetXMPProperty kulkivat jo EnsureXMP-kutsun kautta, ja GetXMPProperty lukee GetDocumentMetadata-kutsun kautta, joten koko pinta jakaa yhden alustusjärjestyksen. Yksi oikea kopio kolmen rivin sekvenssistä on arvokkaampi kuin kymmenen kopiota, jotka sattuvat olemaan samaa mieltä tänään

Kaksi pienempää ansaa samalla polulla

Windowsin XMP-serialisoija käyttää alustan XML-kirjoittajaa, joka tuottaa XML-deklaraation, jota paketissa ei saa olla. Vanha koodi riisui sen poistamalla merkkejä kunnes päästiin kohtaan <?xpacket. ISO 16684-1 §7.3.2 tekee xpacket-kääreestä valinnaisen, ja tuottaja, joka kirjoittaa paljaan <x:xmpmeta>-elementin, pysyy standardin sisällä, joten sellaisessa paketissa luuppi poisti koko kelvollisen asiakirjan. Serialisoija paikantaa nyt deklaraation päättävän ?>-merkkijonon ja poistaa vain sen. Tests\XMPRetentionSemantics.inc ajaa säilyvyystarkistuksensa kahdesti, kerran kääreen kanssa ja kerran se leikattuna pois, ja varmistaa, että mukautetun nimiavaruuden merkki ja alkuperäinen tekijä säilyvät SetInfo-, GetMetadata- ja SaveToString-kutsujen sekä uudelleenlatauksen yli. Toinen ansa oli esiprosessoritunnus: Info–XMP-synkronointia SetInfo-metodissa vartioi NOVCL, joka on asetettu Free Pascal -käännöksille, mutta XMP-taustan portinvartijana on käyttöjärjestelmä eikä kehys, sillä PDFlibXMP.pas määrittelee NO_XMP-tunnuksen vain kun OS_WINDOWS puuttuu. Windows-Lazarus-käännöksessä oli siis toimiva XMP-objekti ja SetInfo, joka hiljaa jätti sen päivittämättä. Vartija on nyt NO_XMP, joten Windowsin Free Pascal -sovellus saa saman synkronoinnin kuin Delphi

Miten alkuperäinen ModDate säilytetään läpikuljettavassa tallennuksessa?

Aseta KeepModDate TPDFlibSaveOptions-tietueessa ja tallenna SaveToFileOptions-kutsun kautta. Valinta asettaa UserModDate-arvon kutsun ajaksi, ja SaveToFile ohittaa silloin automaattisen aikaleiman, joka on samalla se vaihe, joka alustaa XMP-objektin laiskasti. Asiakirja, jonka metatietoihin et koskaan koskenut ja jolle ei otettu käyttöön mitään vaatimustenmukaisuustilaa, säilyttää sekä Info-sanakirjansa että /Metadata-streaminsa ladattuina. SetInformation(8, ...)-kutsu vaikuttaa samalla tavalla pysyvästi, koska muokkauspäivämäärän asettaminen itse merkitsee sen käyttäjän hallitsemaksi

var
  Options: TPDFlibSaveOptions;
begin
  FillChar(Options, SizeOf(Options), 0);
  Options.OptimizeContentStreams := True;
  Options.PackObjectStreams := True;
  Options.KeepModDate := True;      // ei automaattista /ModDate-arvoa, ei laiskaa XMP-alustusta
  if Lib.SaveToFileOptions('design-resaved.pdf', Options) <> 1 then
    Log(Format('save failed, LastErrorCode=%d', [Lib.LastErrorCode]));
end;

Ole rehellinen siitä, mitä tämä sinulle antaa. KeepModDate on oikea valinta läpikulkuvaiheelle, jonka tuotoksen pitäisi kuvata samaa revisiota kuin sen syötteen, ja väärä valinta kaikelle, mikä oikeasti muokkaa sisältöä, koska §14.3.3 odottaa /ModDate-arvon heijastavan viimeisintä muokkausta. Sekään ei korjaa jälkikäteen kirjastoa, joka mutatoi jaettuja objekteja; se vain välttää sen yhden kirjoituksen, joka paljasti vian. Molemmat yllä olevat korjaukset ovat se, mikä tekee tavallisesta tallennuksesta turvallisen, ja tämä valinta on se, mikä tekee tarkoituksellisesta no-opista rehellisen

Miten varmistetaan, ettei tallennus muuttanut muuta kuin ModDate-arvon?

Ei pikseleillä eikä streamien tiivisteillä, koska molemmat viat jättävät jokaisen sivun ja jokaisen sisältöstreamin tavu tavulta identtisiksi. Ne napanneen tarkistuksen teki riippumaton jäsennin, joka ei jaa koodia testattavan kirjaston kanssa ja ottaa ei-visuaalisen semanttisen tilannekuvan lähdetiedostosta ja tallennetusta tiedostosta, minkä jälkeen seuraa rakenteellinen vertailu. Tilannekuva kattaa Info-sanakirjan ilman /ModDate-arvoa, kirjanmerkkien puun, jossa jokainen kirjanmerkki on ratkaistu sivunumeroksi objektinumeron sijaan, nimetut kohteet ja linkkien maalit samalla tavalla ratkaistuina, lomakekenttien arvot, liitetiedostojen tavut tiivisteinä sekä XMP-paketin puuna jäsennettynä tekstinä vertaamisen sijaan. Objektinumerot eivät ole siinä tarkoituksella mukana, koska täysi uudelleenkirjoitus numeroi kaiken uudelleen ja niihin perustuva vertailu raportoisi kohinaa

Ei-visuaalinen semanttinen varmennus PDFlibPasin tallennuksille: riippumaton jäsennin, jolla ei ole jaettua koodia, ottaa tilannekuvan Info-sanakirjasta ilman /ModDate-arvoa, jäsennyspuusta ja kohteiden sivuista, lomakearvoista, liitteiden tiivisteistä ja XMP-puusta ja vertaa sitten lähdettä tallennettuun tiedostoon jättäen pois /ModDate-, xmp:ModifyDate- ja xmp:MetadataDate-arvot odotettuina muutoksina
Pikselit ja streamien tiivisteet pysyvät tavu tavulta identtisinä molempien vikojen läpi, joten vertailu toimii ratkaistulla semantiikalla objektinumeroiden sijaan, ja säilynyt metadata raportoidaan rehellisesti säilyneenä eikä skeeman kelvollisena tai PDF/UA- ja PDF/A-yhteensopivana

Poissulkemiset ovat yhtä tärkeitä kuin mukaan otettavat. /ModDate-, xmp:ModifyDate- ja xmp:MetadataDate-arvojen odotetaan muuttuvan ja ne pudotetaan ennen vertailua; tiedostoa, jonka lähde ei kuljettanut lainkaan XMP:tä, ei moitita paketin saamisesta. Se, mitä tarkistus ei väitä, on yhtä selkeää: olemassa olevan paketin säilyttäminen ei kerro mitään siitä, onko paketti skeeman mukainen tai täyttääkö asiakirja PDF/UA-vaatimukset tai minkään PDF/A-osan. Ne ovat erillisiä kysymyksiä erillisine työkaluineen, ja se, että ”metadata säilyi” sekoitetaan siihen että ”metadata on vaatimustenmukaista”, on juuri se, miten ensimmäinen bugi piiloutui niin pitkäksi aikaa kuin piiloutui. Kirjaston puolella molemmat regressiot ajetaan nyt jokaisella kohdennetulla ajokerralla Delphin Win32- ja Win64-käännöksillä sekä Free Pascalin Win32- ja Win64-käännöksillä, ja semanttinen vertailu on läpäisyehto oikeiden asiakirjojen korpusbenchmarkille

Jos työskentelet näiden korjausten alapuolella olevalla tasolla, tallennuksen objektien uudelleenkirjoitusmekaniikkaa käsitellään artikkeleissa inkrementaaliset päivitykset ja vain lisäävä tallennus, joka on se ainoa tallennustila, jossa jaettu objekti yksinkertaisesti jätetään paikalleen, ja muokkaustasot ja revisioiden diffaus, joka on toinen paikka, jossa vanhentunut tai uudelleenkirjoitettu päivämäärä johtaa lukijaa harhaan. Saman Info- ja XMP-parin korjauspuolen näkymä, jossa kaksi puoliskoa saadaan sopimaan yhteen pelkän säilyttämisen sijaan, on artikkelissa muuntaminen PDF/A:ksi ja metatietojen korjaus

PDF Library for Delphi on natiivi Pascal-PDF-kirjasto Delphille, C++Builderille ja Lazarusille, ja tässä kuvattu lue-muokkaa-kirjoita-polku on sama, jonka jokainen oma prosessisi muokkaus kulkee läpi, joten yllä olevat takeet pätevät riippumatta siitä, tallennatko kerran vai tuhat kertaa päivässä — tuetut kääntäjät ja alustat löydät PDF Library for Delphi -tuotesivulta