PDFlibPas, losLab PDF Developer Library, gemmer den præcise decimaltekst, den parsede, for hvert reelt tal i et dokument og skriver den tekst ordret tilbage, hver gang værdien aldrig er blevet ændret. Siden v3.539.19 styrer SetPrecision-indstillingen kun tal, som biblioteket selv opretter eller redigerer, så en almindelig load-and-save ikke længere runder en CalRGB /Gamma på 2.22221 ned til 2.2222 og flytter farverne på en side, ingen rørte. Ændringen er lille i kode og stor i det, den siger om parsere: værdien, du dekoder, og literalen, du emitterer, er to forskellige ting, og en Double-roundtrip er ikke en identitetstransformation
Hvorfor flyttede en gemning uden ændringer sidens farver?
Fordi farverumsparametrene blev omformateret, ikke billedet. Filen, der udstillede det, er et kontordokument på 35 sider i det lokale regressionskorpus, med et headerbillede genbrugt på hver side. At loade den og gemme den direkte tilbage producerede image streams, der var byte for byte identiske med inputtet, og en stream-hash-sammenligning rapporterede dokumentet uændret. En rendered sammenligning var uenig: samtlige 35 sider viste pixelafvigelser i headeren og ingen andre steder
Headerbilledet tegner gennem et CalRGB-farverum, som ISO 32000-1 §8.6.5.3 definerer ved et /WhitePoint, et valgfrit /Gamma-array med tre elementer og et valgfrit /Matrix-array med ni. De arrays er almindelige numeriske objekter i farverumsdictionaryen. TPDFNumeric gemte hver af dem som en Double og intet andet, og TPDFNumeric.Output formaterede den Double gennem PDFPrecNum, som som default står til fire decimaler. Så /Gamma gik fra 2.22221 til 2.2222, et matrix-entry gik fra 0.71519 til 0.7152, og rendereren producerede trofast let forskellige farver ud fra en let forskellig kalibrering. Billedbytesene var uskyldige; tallene omkring dem var det ikke. Det ubehagelige er, hvor usynligt dette var. At sammenligne dekodede stream-bytes kan ikke se det, for tallene bor i en dictionary, ikke i en stream. At sammenligne attachment-payloads kan ikke se det. Selv revision-diffen beskrevet i artiklen om modification levels fingerprinter en normaliseret objektkrop, så begge revisioner hasher til samme værdi, og diffen rapporterer dem identiske. Kun rendering fangede det, hvilket er grunden til, at korpus-baselineen renderer hver side i stedet for at stole alene på strukturelle tjek
Værdien, du parsede, er ikke literalen, du bør skrive
Et PDF reelt tal er en decimalstreng, og ISO 32000-1 §7.3.3 er eksplicit: det er kun en decimalstreng, ingen radix-notation, ingen eksponentform. Annex C lister derefter den præcision, en implementation forventes at ære, cirka fem signifikante decimalcifre i brøkdelen. En default outputpræcision på fire er allerede under det, og det bliver værre nær nul: PLDoubleToStr skalerer værdien, runder til et heltal og emitterer 0, når resultatet er nul, så et matrix-entry på -0.000012345 mister ikke et ciffer, det forsvinder helt
At hæve defaulten ville bare flytte klippekanten. Fixet er at holde op med at lade som om, at en Double er tallet. Når tokenizeren i TPDFStructure.Decode genkender et standardreelt tal, altså tokenen indeholder et decimalpunktum og ingen eksponentmarkør, gemmer den kildeteksten i det nye FOriginalText-felt ved siden af den konverterede værdi. Output foretrækker så den tekst og falder kun tilbage til formatering, når der ikke er noget at foretrække
// Lib/PDFlibStruct.pas — hele fixet på outputsiden
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 redigeret tal er et nyt tal
FValue:= Value;
FChanged:= True;
End;
To grænser er bevidste. Heltal bevares ikke, for heltalsformatering er allerede tabsfri. Eksponentformer som 6.02E23 tolereres på input for ødelagte producers skyld, men bevares ikke på output, da at skrive dem tilbage ville videreføre en syntaks, §7.3.3 forbyder; de går gennem formatteren som ethvert biblioteksgenereret tal. Tokenizeren anvender også sin sædvanlige minimale reparation, før teksten gemmes, så en literal med indledende punktum som .5 gemmes som 0.5, og en literal med afsluttende punktum som 5. som 5.0. Begge er det samme tal for enhver læser og accepteres langt mere udbredt
Hvad garanterer SetPrecision efter v3.539.19?
TPDFlib.SetPrecision styrer nu decimalerne af tal, som biblioteket selv producerer: værdier tegnet gennem painteren, tal oprettet fra en Double, for eksempel gennem NewNumeric, og enhver parset værdi, som siden er blevet redigeret med SetTo. Bemærk, at tekst dekodet gennem objekt-API'en, for eksempel en literal givet til SetObjectFromString, går gennem den samme tokenizer og bevares på samme måde. En parset decimal, der aldrig blev ændret, beholder sin inputpræcision uanset indstillingen, og at ændre indstillingen efter load berører den ikke retrospektivt. SetPrecision-referenceentryet blev opdateret i samme release til at sige præcis dette, for den gamle formulering lod som om, at indstillingen gjaldt hvert tal i filen
Rydningen sker i SetTo i stedet for at blive udledt af Changed-flaget, og den skel betyder noget. Save-pipelinen nulstiller Changed på objekter, når de først er skrevet, så et tjek af formen "emitter original tekst medmindre ændret" ville begynde at emittere forældet tekst for en værdi, der blev redigeret, gemt og redigeret igen i samme session. At binde originalteksten til selve tildelingen gør det umuligt for de to at være uenige. Regressionstesten låser hver af disse adfærd fast med værdierne fra den oprindelige fil
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]'));
// Uredigeret input overlever ordret, inklusive den værdi, som
// fire-cifret formatering ville have kollapset til 0
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// En redigering kasserer 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');
// At sænke præcisionen bagefter når ikke uredigeret input
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Hvorfor normaliserer indholdsmodellen stadig tal?
Fordi TPDFContentProgram lover kanoniske numeriske operander, og det løfte er mere værd end ordret tekst inde i en content stream. Den redigerbare indholdsmodel, den samme som graphics-state trackeren er bygget på, findes, for at NormalizeContentStreams, optimizatoren og Emit kan producere stabilt, sammenligneligt output ud fra vilkårligt input. Hvis en parset operande bar sin originaltekst ind i modellen, ville en operatorsekvens som 0.50000 0 0 RG emitte forskelligt fra 0.5 0 0 RG, og hver downstream-sammenligning ville drive med producentens formateringsvaner
Modellen stripper derfor originalteksten ved sine to indgangspunkter. NormalizeContentNumbers kører på hver operande, efterhånden som parseren skubber den ind, og igen inde i SetOperand, når caller-leveret kilde dekodes, og den rekurrerer gennem arrays og dictionaries, så stregmønstre (dash patterns), TJ-arrays og marked contents property-dictionaries er dækket. At kalde SetTo(AsDouble) på hvert tal er nok, for det er præcis den operation, der rydder teksten. Rå inline-billeddata røres ikke, som det altid har været
// Lib/PDFlibContentModel.pas — indholdsmodellen holder sin kontrakt
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 regel for callers er derfor simpel. En almindelig LoadFromFile efterfulgt af SaveToFile efterlader urørte content streams og urørte dictionary-tal, som de var. En side, der går gennem NormalizeContentStreams, eller enhver redigering foretaget gennem indholdsmodellen, kommer kanonisk ud af designet, og resten af dokumentet er stadig bevaret. Det er to forskellige ønsker, og de gør nu to forskellige ting
Hvad det koster, og hvor garantien stopper
Hver TPDFNumeric bærer nu én ekstra AnsiString-reference, og hver parset decimal holder sin kildetekst i live i objektets levetid. På et dokument med millioner af reelle tal er det rigtig hukommelse, og det hører hjemme i enhver stor-dokument-måling i stedet for at blive viftet væk. Garantien er også afgrænset til et tals eget dokument: at kopiere objekter mellem dokumenter eller rekonstruere værdier gennem objekt-API'en producerer nye tal, som følger outputpræcisionen som ethvert andet nyt tal. Det er værd at være præcis om, hvad releasen påstår, og hvad den ikke gør. En load-and-save af et urørt dokument bevarer nu de kalibreringstal, som rendereren faktisk indtager, hvilket er den egenskab, korpus-baselineen tjekker. Den påstår ikke byte-identisk output, hvilket også afhænger af objektnummerering, stream-komprimering og trailer-id'en behandlet i artiklen om deterministisk PDF-ID. Og den får ikke fingerprint-diffen til at se rundingsforskelle i filer produceret af anden software, da den stadig hasher den normaliserede krop. Lektien generaliserer langt ud over CalRGB: når en parser kun gemmer den konverterede værdi, er hver gemning en redigering, og den eneste måde at bemærke det på er at kigge på det renderede resultat. Den numeriske håndtering og SetPrecision-semantikken er dokumenteret på losLab PDF Developer Library-produktsiden