PDFlibPas, losLab PDF Developer Library, beholder den nøyaktige desimalteksten den leste inn for hvert reelt tall i et dokument, og skriver den teksten tilbake ordrett så lenge verdien aldri ble endret. Siden v3.539.19 styrer innstillingen SetPrecision bare tall som biblioteket selv oppretter eller redigerer, så en vanlig last-og-lagre-runde ikke lenger runder et CalRGB /Gamma på 2.22221 ned til 2.2222 og endrer fargene på en side ingen har rørt. Endringen er liten i kode og stor i hva den sier om parsere: verdien du dekoder og literalen du skriver ut, er to forskjellige ting, og en rundtur gjennom en Double er ikke en identitetstransformasjon
Hvorfor endret en lagring uten endringer fargene på siden?
Fordi det var parametrene i fargerommet som ble reformatert, ikke bildet. Filen som avslørte dette, er et 35-siders kontordokument i det lokale regresjonskorpuset, med et topptekstbilde som gjenbrukes på hver side. Å laste den og lagre den rett tilbake ga bildestrømmer som var byte for byte identiske med inndataene, og en sammenligning av strømhasher rapporterte dokumentet som uendret. En gjengitt sammenligning var uenig: alle de 35 sidene viste pikselforskjeller i toppteksten, og ingen andre steder
Topptekstbildet tegnes gjennom et CalRGB-fargerom, som ISO 32000-1 §8.6.5.3 definerer med et /WhitePoint, en valgfri /Gamma-array med tre elementer og en valgfri /Matrix med ni elementer. De arrayene er vanlige numeriske objekter i fargeromsordboken. TPDFNumeric lagret hvert av dem som en Double og ingenting annet, og TPDFNumeric.Output formaterte den Double-en gjennom PDFPrecNum, som standard er fire desimaler. Så /Gamma gikk fra 2.22221 til 2.2222, en matriseverdi gikk fra 0.71519 til 0.7152, og gjengiveren produserte tro mot inndataene litt andre farger fra en litt annen kalibrering. Selve bildebytene var uskyldige; tallene rundt dem var det ikke. Det ubehagelige er hvor usynlig dette var. Å sammenligne dekodede strømbyte kan ikke se det, fordi tallene bor i en ordbok, ikke i en strøm. Å sammenligne vedleggslaster kan ikke se det. Til og med revisjonsdiffen som beskrives i artikkelen om endringsnivå, tar fingeravtrykk av en normalisert objektkropp, så begge revisjonene hasher til samme verdi og diffen rapporterer dem som identiske. Bare gjengivelsen fanget det, og det er derfor korpusbaselinen gjengir hver side i stedet for å stole på strukturelle sjekker alene
Verdien du leste inn, er ikke literalen du bør skrive
Et reelt tall i PDF er en desimalstreng, og ISO 32000-1 §7.3.3 er tydelig på at det bare er en desimalstreng: ingen radiksnotasjon, ingen eksponentform. Vedlegg C lister deretter opp presisjonen en implementasjon forventes å respektere, omtrent fem signifikante desimaler i brøkdelen. En standard utdatapresisjon på fire er allerede under det, og det blir verre nær null: PLDoubleToStr skalerer verdien, runder av til et heltall og skriver 0 når resultatet er null, så en matriseverdi på -0.000012345 mister ikke et siffer, den forsvinner helt
Å heve standarden ville bare flytte stupet. Fiksen er å slutte å late som om en Double er tallet. Når tokenizeren i TPDFStructure.Decode gjenkjenner et standard reelt tall, det vil si at tokenet inneholder et desimalpunkt og ingen eksponentmarkør, lagrer den kildeteksten i det nye FOriginalText-feltet ved siden av den konverterte verdien. Output foretrekker deretter den teksten og faller tilbake til formatering bare når det ikke er noe å foretrekke
// Lib/PDFlibStruct.pas — hele fiksen på utdatasiden
Function TPDFNumeric.Output: AnsiString;
Begin
If FOriginalText<> '' Then
Result:= FOriginalText
Else
Result:= PLDoubleToStr(FValue, Owner.PDFPrecNum);
End;
Procedure TPDFNumeric.SetTo(Const Value: Double);
Begin
FOriginalText:= ''; // et redigert tall er et nytt tall
FValue:= Value;
FChanged:= True;
End;
To grenser er bevisste. Heltall bevares ikke, fordi heltallsformatering allerede er tapsfri. Eksponentformer som 6.02E23 tolereres på inndata for ødelagte produsenters skyld, men bevares ikke på utdata, siden det å skrive dem tilbake ville videreføre en syntaks §7.3.3 forbyr; de går gjennom formatoren som ethvert tall biblioteket genererer. Tokenizeren bruker også sin vanlige minimale reparasjon før den lagrer teksten, så en literal med ledende punktum som .5 beholdes som 0.5, og en literal med avsluttende punktum som 5. som 5.0. Begge er samme tall for enhver leser og er langt mer allment akseptert
Hva garanterer SetPrecision etter v3.539.19?
TPDFlib.SetPrecision styrer nå antall desimaler for tall biblioteket selv produserer: verdier tegnet gjennom painteren, tall opprettet fra en Double, for eksempel via NewNumeric, og enhver innlest verdi som siden er redigert med SetTo. Merk at tekst som dekodes gjennom objekt-API-et, for eksempel en literal sendt til SetObjectFromString, går gjennom den samme tokenizeren og bevares på samme måte. En innlest desimal som aldri ble endret, beholder inndatapresisjonen uavhengig av innstillingen, og å endre innstillingen etter lastingen rører den ikke i ettertid. Referanseoppføringen for SetPrecision ble oppdatert i samme utgivelse for å si nøyaktig dette, fordi den gamle ordlyden antydet at innstillingen gjaldt hvert tall i filen
Tømmingen skjer i SetTo i stedet for å utledes fra Changed-flagget, og den forskjellen betyr noe. Lagringspipelinen nullstiller Changed på objekter når de er skrevet, så en sjekk av formen «skriv ut originalteksten med mindre endret» ville begynne å skrive ut foreldet tekst for en verdi som ble redigert, lagret og redigert igjen i samme sesjon. Å knytte originalteksten til selve tilordningen gjør det umulig for de to å være uenige. Regresjonstesten fester hver av disse oppførslene med verdiene fra originalfilen
uses
PDFlibStruct;
var
Structure: TPDFStructure;
Values: TPDFArray;
Number: TPDFNumeric;
begin
Structure := TPDFStructure.Create;
try
Structure.PDFPrecNum := 4;
Values := TPDFArray(Structure.Decode('[2.22221 0.71519 -0.000012345 1 0.12567]'));
// Urørt inndata overlever ordrett, inkludert verdien som
// fire-desimalers formatering ville ha kollapset til 0
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// En redigering forkaster originalteksten og følger PDFPrecNum
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// Å senke presisjonen etterpå når ikke urørt inndata
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Hvorfor normaliserer innholdsmodellen fortsatt tall?
Fordi TPDFContentProgram lover kanoniske numeriske operander, og det løftet er verdt mer enn ordrett tekst inne i en innholdsstrøm. Den redigerbare innholdsmodellen, den samme som sporingen av grafikkstatus er bygget på, finnes for at NormalizeContentStreams, optimalisereren og Emit skal produsere stabile, sammenlignbare utdata fra vilkårlige inndata. Hvis en innlest operand tok med seg originalteksten inn i modellen, ville en operatorsekvens som 0.50000 0 0 RG bli skrevet ut annerledes enn 0.5 0 0 RG, og enhver sammenligning nedstrøms ville drive med produsentens formateringsvaner
Så modellen fjerner originalteksten ved sine to inngangspunkter. NormalizeContentNumbers kjører på hver operand når parseren skyver den inn, og igjen inne i SetOperand når kallergitt kilde dekodes, og den rekurserer gjennom arrayer og ordbøker slik at stiplete mønstre, TJ-arrayer og egenskapsordbøkene til markert innhold dekkes. Å kalle SetTo(AsDouble) på hvert tall er nok, siden det nettopp er operasjonen som tømmer teksten. Rå innebygde bildedata røres ikke, som alltid
// Lib/PDFlibContentModel.pas — innholdsmodellen beholder kontrakten sin
Procedure NormalizeContentNumbers(Obj: TPDFObject);
Var
K: Integer;
Begin
If Obj is TPDFNumeric Then
TPDFNumeric(Obj).SetTo(TPDFNumeric(Obj).AsDouble)
Else If Obj is TPDFArray Then
For K:= 0 To TPDFArray(Obj).Count- 1 Do
NormalizeContentNumbers(TPDFArray(Obj).Item[K])
Else If Obj is TPDFDictionary Then
For K:= 0 To TPDFDictionary(Obj).Count- 1 Do
NormalizeContentNumbers(TPDFDictionary(Obj).Entry[K].Value);
End;
Den praktiske regelen for kallere er derfor enkel. En vanlig LoadFromFile etterfulgt av SaveToFile lar urørte innholdsstrømmer og urørte ordboktall være som de var. En side som går gjennom NormalizeContentStreams, eller enhver redigering gjort gjennom innholdsmodellen, kommer ut kanonisk med vilje, og resten av dokumentet er fortsatt bevart. Det er to forskjellige forespørsler, og de gjør nå to forskjellige ting
Hva det koster, og hvor garantien stopper
Hver TPDFNumeric bærer nå én ekstra AnsiString-referanse, og hver innlest desimal holder kildeteksten sin i live så lenge objektet lever. På et dokument med millioner av reelle tall er det virkelig minne, og det hører hjemme i enhver måling på store dokumenter i stedet for å viftes bort. Garantien er også begrenset til et talls eget dokument: å kopiere objekter mellom dokumenter eller rekonstruere verdier gjennom objekt-API-et gir nye tall, som følger utdatapresisjonen som ethvert annet nytt tall. Det er verdt å være presis på hva utgivelsen gjør og ikke gjør krav på. En last-og-lagre av et urørt dokument bevarer nå kalibreringstallene som gjengiveren faktisk bruker, og det er egenskapen korpusbaselinen sjekker. Den gjør ikke krav på byte-identiske utdata, som også avhenger av objektnummerering, strømkomprimering og trailer-identifikatoren som diskuteres i artikkelen om deterministisk PDF-ID. Og den får ikke fingeravtrykkdiffen til å se avrundingsforskjeller i filer produsert av annen programvare, siden de fortsatt hasher den normaliserte kroppen. Lærdommen generaliserer godt utover CalRGB: når en parser bare beholder den konverterte verdien, er hver lagring en redigering, og den eneste måten å oppdage det på er å se på det gjengitte resultatet. Tallhåndteringen og semantikken til SetPrecision er dokumentert på produktsiden til losLab PDF Developer Library