A PDFlibPas, a losLab PDF Developer Library minden valós számhoz megőrzi azt a pontos decimális szöveget, amit parsolt, és szó szerint azt írja vissza, valahányszor az értéket soha nem módosították. A v3.539.19 óta a SetPrecision beállítás csak azokra a számokra vonatkozik, amiket a könyvtár hoz létre vagy szerkeszt, így egy hétköznapi betöltés-mentés nem kerekíti le többé a CalRGB /Gamma 2.22221 értékét 2.2222-re, és nem tolja el egy érintetlen oldal színeit. A változás kicsi a kódban, és nagy abban, amit a parserekről mond: a dekódolt érték és a kiírt literál két különböző dolog, és egy Double oda-vissza útja nem identitás-transzformáció
Miért tolta el egy változatlan mentés az oldal színeit?
Mert a színterek paraméterei formázódtak újra, nem a kép. A fájl, ami ezt felszínre hozta, egy 35 oldalas irodai dokumentum a helyi regressziós korpuszban, minden oldalon újrahasznált fejlécképpel. Betöltve és egyenesen visszamentve a kimenet képeinek streamjei bájt pontosan megegyeztek a bemenettel, és a stream-hash összehasonlítás változatlannak jelentette a dokumentumot. A renderelt összehasonlítás nem értett egyet: mind a 35 oldal pixeleltérést mutatott a fejlécben, és sehol máshol
A fejléckép CalRGB színtéren keresztül rajzolódik, amit az ISO 32000-1 §8.6.5.3 egy /WhitePoint-tal, egy opcionális háromelemű /Gamma tömbbel és egy opcionális kilencelemű /Matrix-szal definiál. Ezek a tömbök sima numerikus objektumok a színtér-szótárban. A TPDFNumeric mindegyiket Double-ként tárolta, semmi másként, a TPDFNumeric.Output pedig azt a Double-t a PDFPrecNum-on keresztül formázta, ami alapértelmezésben négy tizedesjegy. Így a /Gamma 2.22221-ből 2.2222 lett, egy mátrixelem 0.71519-ből 0.7152, a renderelő pedig hűségesen némileg más színeket állított elő némileg más kalibrációból. A kép bájtjai ártatlanok voltak; a körülöttük lévő számok nem. A kellemetlen rész az, hogy ez mennyire láthatatlan volt. A dekódolt stream-bájtok összehasonlítása nem látja, mert a számok szótárban élnek, nem streamben. A melléklet-hasznos terhek összehasonlítása nem látja. Még a módosítási szintekről szóló cikkben leírt revízió-diff is egy normalizált objektumtestet vesz ujjlenyomatul, így mindkét revízió ugyanarra az értékre hashel, és a diff azonosnak jelenti őket. Csak a renderelés kapta el, ezért rendereli a korpusz alapszintje minden oldalt, ahelyett hogy csak a strukturális ellenőrzésekben bízna
A parsolt érték nem az a literál, amit ki kell írnod
A PDF valós száma egy decimális string, és az ISO 32000-1 §7.3.3 kimondja, hogy az csak decimális string: se radix jelölés, se kitevős forma. A C melléklet aztán felsorolja azt a pontosságot, amit egy implementációnak be kell tartania, körülbelül öt jelentős decimális jegyet a tört részben. A négy tizedesjegy alapértelmezett kimeneti pontosság már ez alatt van, és a nulla közelében még rosszabb: a PLDoubleToStr skálázza az értéket, egészre kerekít, és 0-t ad ki, amikor az eredmény nulla, így egy -0.000012345 mátrixelem nem veszít egy jegyet, hanem teljesen eltűnik
Az alapértelmezés megemelése csak odébb tolná a szakadékot. A javítás az, hogy felhagyunk azzal, hogy a Double-t a számnak higgyük. Amikor a TPDFStructure.Decode tokenizere felismer egy szabványos valós számot, azaz a token tartalmaz tizedespontot és nem tartalmaz kitevőjelölőt, a forrásszöveget az új FOriginalText mezőbe teszi a konvertált érték mellé. Az Output ezután azt a szöveget részesíti előnyben, és csak akkor esik vissza formázásra, amikor nincs mit előnyben részesíteni
// Lib/PDFlibStruct.pas — a teljes javítás a kimeneti oldalon
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:= ''; // egy szerkesztett szám egy új szám
FValue:= Value;
FChanged:= True;
End;
Két határvonal szándékos. Az egészek nem őrződnek meg, mert az egészformázás már veszteségmentes. A kitevős formák, mint a 6.02E23, bemeneten toleráltak a hibás producerek kedvéért, kimeneten viszont nem őrződnek meg, mivel a visszaírásuk egy olyan szintaxist tartana fenn, amit a §7.3.3 tilt; ezek ugyanúgy átmennek a formázón, mint bármely könyvtár által generált szám. A tokenizer a szokásos minimális javítását is alkalmazza a szöveg tárolása előtt, így egy vezető ponttal írt literál, mint a .5, 0.5-ként marad meg, a záró ponttal írt literál, mint az 5., pedig 5.0-ként. Mindkettő ugyanaz a szám minden olvasónak, és sokkal szélesebb körben elfogadott
Mit garantál a SetPrecision a v3.539.19 után?
A TPDFlib.SetPrecision mostantól a könyvtár által előállított számok tizedesjegyeit szabályozza: a painteren keresztül rajzolt értékekét, a Double-ből létrehozott számokét, például a NewNumeric-on keresztül, és minden parsolt értékét, amit azóta SetTo-val szerkesztettek. Vedd észre, hogy az objektum-API-n keresztül dekódolt szöveg, például egy literál, amit a SetObjectFromString-nek adsz át, ugyanazon a tokenizeren megy át, és ugyanígy megőrződik. Egy parsolt decimális, amit soha nem módosítottak, a bemeneti pontosságát tartja meg a beállítástól függetlenül, és a beállítás betöltés utáni megváltoztatása visszamenőleg nem nyúl hozzá. A SetPrecision referencia-bejegyzése ugyanebben a kiadásban frissült, hogy pontosan ezt mondja, mert a régi szövegezés azt sugallta, hogy a beállítás a fájl minden számára vonatkozik
A törlés a SetTo-ban történik, nem a Changed flagből származik, és ez a különbség számít. A mentési pipeline nullázza a Changed flaget az objektumokon, miután kiírták őket, így egy „írd ki az eredeti szöveget, hacsak nem változott” típusú ellenőrzés elavult szöveget kezdene kiírni egy olyan értékhez, amit ugyanabban a munkamenetben szerkesztettek, mentettek, majd újra szerkesztettek. Az eredeti szöveget magához az értékadáshoz kötni lehetetlenné teszi, hogy a kettő ellentmondjon egymásnak. A regressziós teszt mindegyik viselkedést az eredeti fájl értékeivel rögzíti
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]'));
// A szerkesztetlen bemenet szó szerint túlél, beleértve azt az értéket is,
// amit a négy tizedesjegyre formázás 0-ra omlasztott volna
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// Egy szerkesztés eldobja az eredeti szöveget és a PDFPrecNum-ot követi
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// A pontosság utólagos csökkentése nem éri el a szerkesztetlen bemenetet
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Miért normalizálja a tartalommodell mégis a számokat?
Mert a TPDFContentProgram kanonikus numerikus operandusokat ígér, és az az ígéret többet ér, mint a szó szerinti szöveg egy tartalom-streamben. A szerkeszthető tartalommodell, ugyanaz, amire a graphics-state tracker épül, azért létezik, hogy a NormalizeContentStreams, az optimalizáló és az Emit stabil, összehasonlítható kimenetet állítson elő tetszőleges bemenetből. Ha egy parsolt operandus átvinné az eredeti szövegét a modellbe, egy olyan operátorsorozat, mint a 0.50000 0 0 RG, másképp íródna ki, mint a 0.5 0 0 RG, és minden későbbi összehasonlítás a producer formázási szokásaival együtt sodródna
A modell tehát a két belépési pontján lecsupaszítja az eredeti szöveget. A NormalizeContentNumbers minden operanduson lefut, amikor a parser betolja, és újra a SetOperand-on belül, amikor a hívó által adott forrás dekódolódik, és rekurzívan bejárja a tömböket és szótárakat, hogy a dash-minták, a TJ tömbök és a megjelölt tartalom tulajdonságszótárai is le legyenek fedve. Ha minden numerikuson meghívod a SetTo(AsDouble)-t, az elég, mivel pontosan az a művelet törli a szöveget. A nyers inline képadat érintetlen marad, ahogy mindig is
// Lib/PDFlibContentModel.pas — a tartalommodell tartja a szerződését
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;
A hívók gyakorlati szabálya ezért egyszerű. Egy sima LoadFromFile, amit SaveToFile követ, úgy hagyja az érintetlen tartalom-streameket és az érintetlen szótárszámokat, ahogy voltak. Egy oldal, ami átmegy a NormalizeContentStreams-on, vagy bármilyen szerkesztés a tartalommodellen keresztül, tervezés szerint kanonikusan jön ki, a dokumentum többi része pedig továbbra is megőrződik. Ez két különböző kérés, és mostantól két különböző dolgot tesznek
Mibe kerül, és hol áll meg a garancia
Minden TPDFNumeric mostantól egy plusz AnsiString referenciát hordoz, és minden parsolt decimális életben tartja a forrásszövegét az objektum élettartamáig. Egy több millió valós számot tartalmazó dokumentumnál ez valódi memória, és ez beletartozik minden nagy dokumentumos mérésbe, nem lehet elintézni egy legyintéssel. A garancia ráadásul egy szám saját dokumentumára van korlátozva: az objektumok dokumentumok közötti másolása vagy az értékek újjáépítése az objektum-API-n keresztül új számokat állít elő, amik a többi új számhoz hasonlóan követik a kimeneti pontosságot. Érdemes pontosan megfogalmazni, mit állít és mit nem állít ez a kiadás. Egy érintetlen dokumentum betöltése és mentése mostantól megőrzi azokat a kalibrációs számokat, amiket a renderelő valóban elfogyaszt, és ez az a tulajdonság, amit a korpusz alapszintje ellenőriz. Nem állítja viszont a bájt pontosan azonos kimenetet, ami többek között az objektumszámozástól, a stream-tömörítéstől és a determinisztikus PDF ID-ről szóló cikkben tárgyalt trailer-azonosítótól is függ. És nem éri el azt sem, hogy a fingerprint-diff kerekítési eltéréseket lásson más szoftverek által előállított fájlokban, mivel azok továbbra is a normalizált testet hashelik. A tanulság jól általánosítható a CalRGB-n túlra: amikor egy parser csak a konvertált értéket tartja meg, minden mentés egy szerkesztés, és az egyetlen módja annak, hogy ezt észrevegyük, a renderelt eredmény megnézése. A számkezelést és a SetPrecision szemantikáját a losLab PDF Developer Library termékoldala dokumentálja