PDFlibPas, razvijalska knjižnica PDF družbe losLab, obdrži natančno decimalno besedilo, ki ga je razčlenila za vsako realno število v dokumentu, in to besedilo zapiše nazaj dobesedno, kadar vrednost ni bila nikoli spremenjena. Od različice v3.539.19 nastavitev SetPrecision velja le za števila, ki jih knjižnica ustvari ali uredi, zato običajno nalaganje in shranjevanje ne zaokroži več /Gamma barvnega prostora CalRGB z 2.22221 navzdol na 2.2222 in ne premakne barv strani, ki se je nihče ni dotaknil. Sprememba je v kodi majhna, v tem, kar pove o razčlenjevalnikih, pa velika: vrednost, ki jo dekodirate, in literal, ki ga izdate, sta dve različni stvari in pot skozi Double in nazaj ni identitetna preslikava
Zakaj je shranjevanje, ki ni spremenilo nič, premaknilo barve strani?
Ker so se preoblikovali parametri barvnega prostora, ne slika. Datoteka, ki je to razkrila, je 35-stranski pisarniški dokument v lokalnem regresijskem korpusu, s sliko glave, ki se ponovi na vsaki strani. Nalaganje in takojšnje shranjevanje nazaj je dalo tokove slik, ki so bili bajt za bajt enaki vhodu, primerjava zgoščenih vrednosti tokov pa je dokument prijavila kot nespremenjen. Primerjava upodobljenih strani se s tem ni strinjala: vseh 35 strani je pokazalo razlike v pikah v glavi in nikjer drugje
Slika glave se riše skozi barvni prostor CalRGB, ki ga ISO 32000-1 §8.6.5.3 določa z /WhitePoint, neobveznim trielementnim nizom /Gamma in neobveznim deveelementnim /Matrix. Ti nizi so običajni številski objekti v slovarju barvnega prostora. TPDFNumeric je vsakega shranil kot Double in nič drugega, TPDFNumeric.Output pa je ta Double oblikoval skozi PDFPrecNum, ki privzeto pomeni štiri decimalna mesta. Tako je /Gamma šel z 2.22221 na 2.2222, vnos matrike z 0.71519 na 0.7152, upodabljalnik pa je zvesto izdelal nekoliko drugačne barve iz nekoliko drugačne kalibracije. Bajti slike so bili nedolžni; številke okoli njih ne. Neprijetno pri tem je, kako nevidno je bilo. Primerjava dekodiranih bajtov toka tega ne more videti, ker številke živijo v slovarju in ne v toku. Primerjava koristnih obremenitev prilog tega ne more videti. Celo primerjava revizij, opisana v članku o ravneh sprememb, odtisne normalizirano telo objekta, zato obe reviziji dasta isto zgoščeno vrednost in primerjava poroča, da sta enaki. Ujelo ju je le upodabljanje, zato korpusno izhodišče upodobi vsako stran, namesto da bi zaupalo samo strukturnim preverjanjem
Vrednost, ki ste jo razčlenili, ni literal, ki bi ga morali zapisati
Realno število PDF je decimalni niz in ISO 32000-1 §7.3.3 je izrecen, da je samo decimalni niz: brez zapisa z osnovo in brez eksponentne oblike. Dodatek C nato našteje natančnost, ki naj bi jo izvedba spoštovala, približno pet pomembnih decimalnih števk v decimalnem delu. Privzeta izhodna natančnost štirih je že pod tem in blizu nič je še slabše: PLDoubleToStr vrednost skalira, zaokroži na celo število in izda 0, kadar je rezultat nič, zato vnos matrike -0.000012345 ne izgubi števke, ampak povsem izgine
Zvišanje privzete vrednosti bi le premaknilo rob. Popravek je v tem, da prenehamo delati, kot da je Double tisto število. Ko tokenizator v TPDFStructure.Decode prepozna standardno realno število, torej žeton, ki vsebuje decimalno piko in nobene oznake eksponenta, izvorno besedilo shrani v novo polje FOriginalText poleg pretvorjene vrednosti. Output nato raje uporabi to besedilo in se na oblikovanje zateče le, kadar ni česa, kar bi preferiral
// Lib/PDFlibStruct.pas — celoten popravek na izhodni strani
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:= ''; // urejeno število je novo število
FValue:= Value;
FChanged:= True;
End;
Dve meji sta namerni. Celih števil se ne ohranja, ker je oblikovanje celih števil že brez izgube. Eksponentne oblike, kot je 6.02E23, se na vhodu tolerirajo zaradi pokvarjenih producentov, na izhodu pa se ne ohranjajo, saj bi njihovo prepisovanje ohranjalo skladnjo, ki jo §7.3.3 prepoveduje; gredo skozi oblikovalnik kot vsako število, ki ga ustvari knjižnica. Tokenizator pred shranjevanjem besedila uporabi tudi svoje običajno minimalno popravljanje, zato se literal z vodilno piko, kot je .5, obdrži kot 0.5, literal s končno piko, kot je 5., pa kot 5.0. Za vsakega bralca je to isto število in je veliko bolj razširjeno sprejeto
Kaj SetPrecision zagotavlja po različici v3.539.19?
TPDFlib.SetPrecision zdaj nadzoruje decimalna mesta števil, ki jih ustvari knjižnica sama: vrednosti, narisane skozi risalnik, števila, ustvarjena iz Double, na primer skozi NewNumeric, in vsako razčlenjeno vrednost, ki je bila pozneje urejena s SetTo. Upoštevajte, da gre besedilo, dekodirano skozi objektni API, na primer literal, predan v SetObjectFromString, skozi isti tokenizator in se ohrani na enak način. Razčlenjena decimalka, ki ni bila nikoli spremenjena, obdrži natančnost vhoda ne glede na nastavitev, sprememba nastavitve po nalaganju pa se je ne dotakne za nazaj. Vnos v referenci za SetPrecision je bil v isti izdaji posodobljen, da pravi natanko to, ker je staro besedilo namigovalo, da nastavitev velja za vsako število v datoteki
Čiščenje se zgodi v SetTo in ni izpeljano iz zastavice Changed, ta razlika pa je pomembna. Veriga shranjevanja zastavico Changed na objektih ponastavi, ko so ti zapisani, zato bi preverjanje v obliki »izdaj izvirno besedilo, razen če je spremenjeno« začelo izdajati zastarelo besedilo za vrednost, ki je bila v isti seji urejena, shranjena in znova urejena. Če je izvirno besedilo vezano na samo dodelitev, se ta dva ne moreta razhajati. Regresijski test vsako od teh vedenj pripne z vrednostmi iz izvirne datoteke
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]'));
// Neurejeni vhod preživi dobesedno, tudi vrednost, ki bi jo
// oblikovanje na štiri mesta strlo na 0
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// Ureditev zavrže izvirno besedilo in sledi PDFPrecNum
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// Poznejše znižanje natančnosti ne doseže neurejenega vhoda
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Zakaj model vsebine števila še vedno normalizira?
Ker TPDFContentProgram obljublja kanonične številske operande in ta obljuba je vredna več kot dobesedno besedilo znotraj toka vsebine. Urejalni model vsebine, isti, na katerem je zgrajen sledilnik grafičnega stanja, obstaja zato, da NormalizeContentStreams, optimizator in Emit iz poljubnega vhoda dajo stabilen, primerljiv izhod. Če bi razčlenjeni operand v model prinesel svoje izvirno besedilo, bi se zaporedje operatorjev, kot je 0.50000 0 0 RG, izdalo drugače kot 0.5 0 0 RG, vsaka dolvodna primerjava pa bi se premikala skupaj z oblikovalskimi navadami producenta
Zato model izvirno besedilo odstrani na obeh vstopnih točkah. NormalizeContentNumbers se izvede na vsakem operandu, ko ga razčlenjevalnik potisne, in znova znotraj SetOperand, ko se dekodira izvorna koda, ki jo poda klicatelj, rekurzivno pa gre skozi nize in slovarje, tako da so pokriti vzorci črtkanja, nizi TJ in slovarji lastnosti označene vsebine. Dovolj je, da se na vsakem številu pokliče SetTo(AsDouble), saj je prav to operacija, ki besedilo počisti. Surovi podatki vgrajenih slik ostanejo nedotaknjeni, kot so vedno bili
// Lib/PDFlibContentModel.pas — model vsebine drži svojo pogodbo
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;
Praktično pravilo za klicatelje je torej preprosto. Navaden LoadFromFile, ki mu sledi SaveToFile, pusti nedotaknjene tokove vsebine in nedotaknjena števila v slovarjih takšna, kot so bila. Stran, ki gre skozi NormalizeContentStreams, ali kakršno koli urejanje skozi model vsebine, izide kanonična po zasnovi, preostali del dokumenta pa je še vedno ohranjen. To sta dve različni zahtevi in zdaj delata dve različni stvari
Koliko to stane in kje se jamstvo ustavi
Vsak TPDFNumeric zdaj nosi en sklic na AnsiString več in vsaka razčlenjena decimalka ohranja svoje izvorno besedilo pri življenju toliko časa, kolikor živi objekt. Pri dokumentu z milijoni realnih števil je to resničen pomnilnik in sodi v vsako meritev velikih dokumentov, ne pa da ga odmahnemo. Jamstvo je tudi omejeno na dokument, ki mu število pripada: kopiranje objektov med dokumenti ali rekonstruiranje vrednosti skozi objektni API ustvari nova števila, ta pa sledijo izhodni natančnosti kot vsako drugo novo število. Velja biti natančen glede tega, kaj izdaja trdi in česa ne. Nalaganje in shranjevanje nedotaknjenega dokumenta zdaj ohrani kalibracijske številke, ki jih upodabljalnik resnično porabi, in prav to lastnost preverja korpusno izhodišče. Ne trdi bajtno enakega izhoda, ki je odvisen tudi od številčenja objektov, stiskanja tokov in identifikatorja v repu, obravnavanega v članku o determinističnem ID PDF. Prav tako ne naredi tega, da bi primerjava odtisov videla razlike v zaokroževanju v datotekah druge programske opreme, saj te še vedno zgoščujejo normalizirano telo. Nauk se posploši daleč onkraj CalRGB: kadar razčlenjevalnik obdrži samo pretvorjeno vrednost, je vsako shranjevanje urejanje in edini način, da to opazite, je pogled na upodobljeni rezultat. Ravnanje s števili in semantika SetPrecision sta dokumentirana na strani izdelka losLab PDF Developer Library