Műszaki cikk

PDF-decimális pontosság megőrzése mentéskor Delphiben

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

Hol veszítette el a PDFlibPas a CalRGB pontosságot egy no-op mentésnél Delphiben: a parsolt /Gamma 2.22221 és egy 0.71519 mátrixelem Double-ként él a TPDFNumericban, az Output a PLDoubleToStr-en keresztül formázza őket a PDFPrecNum négy tizedesjegyével, minden strukturális ellenőrzés változatlannak jelenti a dokumentumot, és csak a renderelt összehasonlítás mutatja mind a 35 fejlécképet eltolva
A kép bájtjai ártatlanok voltak: a TPDFNumeric a körülöttük lévő kalibrációs számokat formázta újra a PDFPrecNum-on keresztül, így a stream-hashek és a fingerprint-diff is azonosnak jelentette a revíziókat, míg a renderelő minden oldalon némileg más színeket állított elő

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

Hogyan őrzi meg a PDFlibPas a parsolt decimális szöveget Delphiben: a TPDFStructure.Decode tokenizere a forrásliterált az FOriginalText mezőben tartja minden olyan tokennél, amiben van tizedespont és nincs kitevő, az Output szó szerint azt írja ki a PLDoubleToStr hívása helyett, a SetTo pedig törli, mert egy szerkesztett szám egy új szám
A dekódolt érték és a kiírt literál két különböző dolog: a parsolt szöveg előnyben részesítése pontosan tartja a 2.22221-et, míg a könyvtár által létrehozott és szerkesztett számok továbbra is a PDFPrecNum-ot követik, és a beállítás soha nem ér el az érintetlen bemenetig
// 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

Miért normalizálja a PDFlibPas tartalommodellje mégis a számokat: a NormalizeContentNumbers ott fut, ahol a parser betolja az operandust, és újra a SetOperand-on belül, rekurzívan bejárja a tömböket és szótárakat, így a dash-minták, a TJ tömbök és a megjelölt tartalom tulajdonságszótárai is le vannak fedve, a SetTo AsDouble pedig törli az eredeti szöveget, hogy a 0.50000 és a 0.5 azonosan íródjon ki
A kanonikus numerikus operandusok a tartalommodell ígérete: a nyers inline képadat érintetlen marad, a tartalom-streameken kívüli érintetlen szótárszámok pedig megőrzik a szó szerinti garanciát, így egy sima LoadFromFile és SaveToFile páros még megőrzi őket
// 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