PDF Library for Delphi v3.539.18 og v3.539.20 fikser to måter en PDF-lagring som ikke endrer noe, likevel kunne ødelegge dokumentmetadata: når /CreationDate og /ModDate refererte til det samme strengobjektet, skrev den automatiske ModDate-oppdateringen om begge, og når XMP-objektet ble opprettet før den opprinnelige /Metadata-strømmen var lest, erstattet en standardpakke den opprinnelige. Fiksene bytter ut ordbokreferanser i stedet for å mutere delte objekter, og fanger opp den eksisterende pakken før lat XMP-initialisering
Innstillingen er den minst interessante operasjonen et PDF-bibliotek utfører: last en fil, lagre den under et nytt navn, rør ingenting i mellom. Sidene gjengis identisk før og etter. Innholdsstrøm-hashene stemte. Filen passerte hver sjekk vi hadde, og den var likevel feil på to steder ingen renderer noen gang ville vise deg. Begge defektene satt i les-modifiser-skriv-veien som hver reell redigering går gjennom, så enhver lagring var nok til å utløse dem, og begge ble funnet først da en andre, uavhengig parser sammenlignet ikke-visuell semantikk i de to filene
Hvorfor endrer en PDF-lagring CreationDate?
Fordi dokumentinformasjonsordboken har lov til å referere til ett enkelt indirekte strengobjekt fra to nøkler, og biblioteket oppdaterte objektet i stedet for nøkkelen. ISO 32000-1 §7.3.10 lar enhver ordbokverdi være en indirekte referanse, og ingenting i §14.3.3 tabell 317 sier at verdien under /CreationDate må være et annet objekt enn verdien under /ModDate. En produsent som skrev det samme tidsstempelet to ganger ved opprettelsen, kan helt lovlig peke begge nøklene på en enkelt 2728 0 R, som er nøyaktig hva et CJK-designdokument i vårt lokale korpus gjorde
Utløseren er den automatiske modifikasjonsdatoen. Med mindre UserModDate er satt, kaller SaveToFile SetInfo('ModDate', ...) med gjeldende tid før den skriver, som når SetRawInfo. Den gamle SetRawInfo slo opp objektet under nøkkelen og kalte SetTo på det hvis den fant en TPDFString. Det er en skriving på stedet til hvilket objekt nøkkelen enn resolverer til nå, og når det objektet er delt, rapporterer /CreationDate nå lagringstiden også. Dokumentet åpner, skriver ut og gjengis fortsatt piksel for piksel som før, så en visuell regresjonspakke passerer uten å blunke
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;
Fiksen i TPDFDocument.SetRawInfo er liten, og prinsippet bak den er generelt: å oppdatere en ordbokoppføring bytter ut oppføringens referanse, aldri objektet den tilfeldigvis resolverte til. Den nye koden leser den eksisterende TPDFStringMode slik at en hex-streng forblir hex og en literal streng forblir literal, og legger deretter til en fersk streng fra FStructure.NewString(Value, StringMode) under nøkkelen. To andre detaljer betyr like mye som hovedendringen. Den gamle grenen for en strømverdi tømte strømmen med SetTo('') før den byttet den ut, noe som ville ha tømt verdien for hver annen nøkkel som fortsatt pekte på den strømmen, så den tømmingen er borte. Og det overflødiggjorte objektet slettes ikke, fordi strukturen eier det og andre referanser kan trenge det fortsatt
// Før: muter hvilket objekt nøkkelen enn resolverer til nå
if Obj is TPDFString then
TPDFString(Obj).SetTo(Value);
// Etter: behold representasjonen, bytt bare denne nøkkelens referanse
StringMode := smLiteral;
if Obj is TPDFString then
StringMode := TPDFString(Obj).StringMode;
ID.Add(Key, FStructure.NewString(Value, StringMode));
Regresjonen i Tests\SharedInfoSemantics.inc bygger aliasingen bevisst i stedet for å støtte seg på en korpusfil: én hex-streng referert fra begge datonøklene, én direkte streng delt av /Title og /Subject, én strøm delt av /Author og /Keywords. Etter å ha oppdatert én nøkkel i hvert par, må den andre fortsatt lese sin opprinnelige verdi, og den oppdaterte strengen må fortsatt være hex. Den offentlige referansen for SetInformation uttrykker nå garantien i én setning: å oppdatere et Info-felt bytter bare ut det feltet, selv når andre felt refererer til samme objekt
Hvorfor blir en eksisterende XMP-pakke erstattet av standardverdier?
På grunn av rekkefølgen på to linjer. TPDFDocument.GetMetadata har en hurtigvei: når XMP-feltet allerede er tilordnet, returnerer den XMP.SaveToString i stedet for å dekode /Metadata-strømmen fra katalogen. Flere kallesteder initialiserte lat med XMP := TPDFlibXMP.Create; XMP.LoadFromString(GetMetadata);, som leser naturlig og er feil: når GetMetadata kjører, er XMP tilordnet, så kilden som lastes, er den serialiserte standardpakken til et objekt som ble opprettet én linje tidligere. Den opprinnelige pakken, med sin dc:creator, egendefinerte navnerom og eventuell standardidentifikasjon, når aldri objektet og overskrives ved lagring. Den samme automatiske modifikasjonsdatoen er nok til å utløse det, fordi SetInfo initialiserer XMP før den rører Info-ordboken, slik at xmp:ModifyDate holder tritt med /ModDate. Legg merke til hva denne defekten gjemmer seg bak: Info-ordboksammenligningen fra den første feilen passerer, siden /Author og /Title i /Info er urørt. Bare XMP-treet endret seg, og bare en sjekk som parser og sammenligner det treet, legger merke til det
// Feil: GetMetadata serialiserer nå objektet som ble opprettet på forrige linje
XMP := TPDFlibXMP.Create;
XMP.LoadFromString(GetMetadata);
// Riktig: fang /Metadata-strømmen først, opprett og last deretter
Source := GetMetadata;
XMP := TPDFlibXMP.Create;
XMP.LoadFromString(Source);
Fiksen gjør to ting. TPDFDocument.EnsureXMP fanger nå Source := GetMetadata før TPDFlibXMP.Create, og all lat initialisering i dokumentet ble erstattet av et kall til den: SetInfo, SetXMPInformation, GetXMPInformation, modus-setterne for PDF/A, PDF/X, PDF/E, PDF/VT, PDF/VCR og PDF/UA, og metadatareparasjonsveien. Offentlige inngangspunkt som SetXMPProperty gikk allerede gjennom EnsureXMP, og GetXMPProperty leser gjennom GetDocumentMetadata, så hele flaten deler én initialiseringsrekkefølge. Én korrekt kopi av en tre-linjers sekvens er verdt mer enn ti kopier som tilfeldigvis er enige i dag
To mindre feller funnet på samme vei
XMP-serialisereren på Windows bruker plattformens XML-skriver, som sender ut en XML-deklarasjon som pakken ikke må bære. Den gamle koden strippet den ved å slette tegn helt til den nådde <?xpacket. ISO 16684-1 §7.3.2 gjør xpacket-innpakningen valgfri, og en produsent som skriver et bart <x:xmpmeta>-element, holder seg innenfor standarden, så på en slik pakke slettet løkken hele det gyldige dokumentet. Serialisereren finner nå den avsluttende ?> i deklarasjonen og fjerner bare den. Tests\XMPRetentionSemantics.inc kjører sin bevaringssjekk to ganger, én med innpakningen og én med den skåret bort, og hevder at en markør for egendefinerte navnerom og den opprinnelige forfatteren overlever SetInfo, GetMetadata, SaveToString og en ny innlasting. Den andre fellen var et preprosessor-symbol: Info-til-XMP-synkroniseringen i SetInfo var vaktet av NOVCL, som er satt for Free Pascal-bygg, men XMP-backenden er gated av operativsystemet, ikke av rammeverket, siden PDFlibXMP.pas definerer NO_XMP bare når OS_WINDOWS mangler. Et Windows Lazarus-bygg hadde derfor et fungerende XMP-objekt og en SetInfo som stille hoppet over å oppdatere det. Vakten er nå NO_XMP, så en Windows Free Pascal-applikasjon får samme synkronisering som Delphi
Hvordan beholder du opprinnelig ModDate ved en no-op-lagring
Sett KeepModDate i TPDFlibSaveOptions og lagre gjennom SaveToFileOptions. Valget setter UserModDate for varigheten av kallet, og SaveToFile hopper da over det automatiske tidsstempelet, som også er trinnet som lat-initialiserer XMP-objektet. Et dokument der du aldri rørte metadataene, og der ingen etterlevelsesmodus ble slått på, beholder både Info-ordboken og /Metadata-strømmen slik de ble lastet. Å kalle SetInformation(8, ...) har samme effekt permanent, fordi det å sette modifikasjonsdatoen selv markerer den som brukerstyrt
var
Options: TPDFlibSaveOptions;
begin
FillChar(Options, SizeOf(Options), 0);
Options.OptimizeContentStreams := True;
Options.PackObjectStreams := True;
Options.KeepModDate := True; // ingen automatisk /ModDate, ingen lat XMP-init
if Lib.SaveToFileOptions('design-resaved.pdf', Options) <> 1 then
Log(Format('save failed, LastErrorCode=%d', [Lib.LastErrorCode]));
end;
Vær ærlig om hva dette kjøper deg. KeepModDate er det riktige valget for et gjennomsendingstrinn der utdataene skal beskrive samme revisjon som inndataene, og det er feil valg for noe som faktisk redigerer innhold, fordi §14.3.3 forventer at /ModDate gjenspeiler den siste modifikasjonen. Det reparerer heller ikke i ettertid et bibliotek som muterer delte objekter; det unngår bare den ene skrivingen som avslørte defekten. Begge fiksene over er det som gjør en vanlig lagring trygg, og valget er det som gjør en bevisst no-op ærlig
Hvordan verifiserer du at en lagring bare endret ModDate?
Ikke med piksler og ikke med strømhasher, fordi begge defektene lar hver side og hver innholdsstrøm være byte-identisk. Sjekken som fanget dem, er et ikke-visuelt semantisk øyeblikksbilde tatt av en uavhengig parser, en som ikke deler kode med biblioteket under test, fra kildefilen og fra den lagrede filen, etterfulgt av en strukturell sammenligning. Øyeblikksbildet dekker Info-ordboken med /ModDate utelatt, outline-treet med hvert bokmerke resolvert til et sidetall snarere enn et objektnummer, navngitte destinasjoner og lenkemål resolvert på samme måte, skjemafeltverdier, vedleggsbyte som hasher, og XMP-pakken parset som et tre snarere enn sammenlignet som tekst. Objektnumre er bevisst ikke en del av det, siden en full omskriving nummererer alt på nytt, og en sammenligning nøklet på dem ville rapportere støy
Utelatelsene er like viktige som inkluderingene. /ModDate, xmp:ModifyDate og xmp:MetadataDate forventes å endres og droppes før sammenligningen; en fil der kilden ikke hadde XMP i det hele tatt, straffes ikke for å få en pakke. Hva sjekken ikke hevder, er like eksplisitt: å bevare en eksisterende pakke sier ingenting om hvorvidt den pakken er skjemagyldig, eller om dokumentet oppfyller PDF/UA eller noen PDF/A-del. Det er separate spørsmål med separate verktøy, og å blande sammen at metadataene overlevde med at metadataene er i samsvar, er hvordan den første feilen kunne gjemme seg så lenge som den gjorde. På biblioteksiden kjører de to regresjonene nå på hver målrettede passering på tvers av Delphi Win32 og Win64 og Free Pascal Win32 og Win64, og den semantiske sammenligningen er en passeringsbetingelse for referansekorporpus-benchmarken
Hvis du jobber på nivået under disse fiksene, er mekanikken for hvordan en lagring skriver om objekter dekket i inkrementelle oppdateringer og append-only lagring, som er den ene lagringsmodusen der et delt objekt rett og slett blir stående der det var, og i modifikasjonsnivåer og revisjonsdiffing, som er det andre stedet en utdatert eller omskrevet dato villeder en leser. Reparasjonssiden av det samme Info- og XMP-paret, der de to halvdelene bringes til å stemme snarere enn bare bevares, står i konvertering til PDF/A og reparasjon av metadata
PDF Library for Delphi er et nativt Pascal PDF-bibliotek for Delphi, C++Builder og Lazarus, og les-modifiser-skriv-veien som beskrives her, er den samme som hver redigering i din egen prosess går gjennom, så garantiene over gjelder enten du lagrer én gang eller tusen ganger om dagen — se produktsiden for PDF Library for Delphi for hvilke kompilatorer og plattformer som støttes