PDFlibPas, losLab PDF Developer Library, čuva tačan decimalni tekst koji je parsirao za svaki realan broj u dokumentu i upisuje taj tekst verbatim kada god vrednost nikada nije menjana. Od v3.539.19 podešavanje SetPrecision upravlja samo brojevima koje biblioteka kreira ili menja, pa obično učitavanje-i-čuvanje više ne zaokružuje CalRGB /Gamma od 2.22221 na 2.2222 i ne pomera boje stranice koju niko nije dirao. Promena je mala u kodu a velika u onome što govori o parserima: vrednost koju dekodirate i literal koji emitujete su dve različite stvari, a Double round trip nije identitetska transformacija
Zašto je čuvanje bez izmena pomerilo boje stranice?
Zato što su se preformatirali parametri color space-a, a ne slika. Fajl koji je to razotkrio je kancelarijski dokument od 35 stranica u lokalnom regresionom korpusu, sa header slikom koja se ponavlja na svakoj stranici. Njegovo učitavanje i čuvanje odmah nazad dalo je image stream-ove bajt za bajt identične ulazu, a poređenje hash-eva stream-ova prijavilo je dokument kao nepromenjen. Poređenje renderovanog izlaza se nije složilo: svaka od 35 stranica pokazivala je razlike u pikselima u header-u, i nigde drugde
Header slika se crta kroz CalRGB color space, koji ISO 32000-1 §8.6.5.3 definiše kroz /WhitePoint, opcioni /Gamma niz od tri elementa i opcioni /Matrix od devet elemenata. Ti nizovi su obični numerički objekti u rečniku color space-a. TPDFNumeric je svaki čuvao kao Double i ništa drugo, a TPDFNumeric.Output je formatirao taj Double kroz PDFPrecNum, koji podrazumevano ima četiri decimalna mesta. Tako je /Gamma otišao sa 2.22221 na 2.2222, unos matrice sa 0.71519 na 0.7152, a renderer je verno proizveo malo drugačije boje iz malo drugačije kalibracije. Bajtovi slike bili su nevini; brojevi oko njih nisu. Neprijatni deo je koliko je to bilo nevidljivo. Poređenje dekodiranih bajtova stream-a to ne može da vidi, jer brojevi žive u rečniku, a ne u stream-u. Poređenje payload-a priloga to ne može da vidi. Čak i diff revizija opisan u članku o nivoima izmena fingerprint-uje normalizovano telo objekta, pa obe revizije daju isti hash i diff ih prijavljuje kao identične. Samo je renderovanje to uhvatilo, i zato baseline korpusa renderuje svaku stranicu umesto da veruje samo strukturnim proverama
Vrednost koju ste parsirali nije literal koji treba da upišete
PDF realan broj je decimalni string, i ISO 32000-1 §7.3.3 je eksplicitan da je on samo decimalni string: bez radix notacije, bez eksponentnog oblika. Aneks C zatim navodi preciznost koju implementacija treba da poštuje, otprilike pet značajnih decimalnih cifara u frakcionom delu. Podrazumevana izlazna preciznost od četiri je već ispod toga, a blizu nule postaje gore: PLDoubleToStr skalira vrednost, zaokružuje na ceo broj i emituje 0 kada je rezultat nula, pa unos matrice -0.000012345 ne gubi cifru, nego nestaje u potpunosti
Podizanje podrazumevane vrednosti samo bi pomerilo liticu. Popravka je da se prestane sa pretvaranjem da je Double taj broj. Kada tokenizer u TPDFStructure.Decode prepozna standardni real, što znači da token sadrži decimalnu tačku i nema marker eksponenta, on smešta izvorni tekst u novo polje FOriginalText uporedo sa konvertovanom vrednošću. Output zatim preferira taj tekst i vraća se na formatiranje samo kada nema šta da preferira
// Lib/PDFlibStruct.pas — cela popravka na izlaznoj 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:= ''; // izmenjen broj je novi broj
FValue:= Value;
FChanged:= True;
End;
Dve granice su namerne. Celi brojevi se ne čuvaju, jer je formatiranje celih brojeva već bez gubitka. Eksponentni oblici kao 6.02E23 tolerišu se na ulazu zbog pokvarenih proizvođača, ali se ne čuvaju na izlazu, jer bi njihovo ponovno upisivanje održavalo sintaksu koju §7.3.3 zabranjuje; oni idu kroz formater kao svaki broj koji je biblioteka generisala. Tokenizer takođe primenjuje svoju uobičajenu minimalnu popravku pre nego što sačuva tekst, pa se literal koji počinje tačkom kao .5 čuva kao 0.5, a literal koji se tačkom završava kao 5. kao 5.0. Oba su isti broj za svakog čitača i daleko su šire prihvaćena
Šta SetPrecision garantuje posle v3.539.19?
TPDFlib.SetPrecision sada kontroliše decimalna mesta brojeva koje biblioteka sama proizvodi: vrednosti iscrtane kroz painter, brojeve kreirane iz Double kao kroz NewNumeric, i svaku parsiranu vrednost koja je od tada izmenjena sa SetTo. Primetite da tekst dekodiran kroz object API, na primer literal prosleđen u SetObjectFromString, prolazi kroz isti tokenizer i čuva se na isti način. Parsirana decimala koja nikada nije menjana zadržava svoju ulaznu preciznost bez obzira na podešavanje, a promena podešavanja posle učitavanja ne dopire do nje retroaktivno. Unos u referenci za SetPrecision ažuriran je u istom izdanju da kaže upravo to, jer je stara formulacija implicirala da se podešavanje primenjuje na svaki broj u fajlu
Brisanje se dešava u SetTo, a ne izvodi se iz flag-a Changed, i ta razlika je važna. Save pipeline resetuje Changed na objektima kada su jednom upisani, pa bi provera oblika „emituj izvorni tekst osim ako je izmenjen“ počela da emituje zastareo tekst za vrednost koja je izmenjena, sačuvana i ponovo izmenjena u istoj sesiji. Vezivanje izvornog teksta za samu dodelu čini nemogućim da se njih dvoje raziđu. Regresioni test prikiva svako od ovih ponašanja vrednostima iz originalnog fajla
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]'));
// Neizmenjen ulaz preživljava verbatim, uključujući vrednost koju bi
// formatiranje na četiri mesta srušilo na 0
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// Izmena odbacuje izvorni tekst i prati PDFPrecNum
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// Naknadno snižavanje preciznosti ne dopire do neizmenjenog ulaza
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Zašto content model i dalje normalizuje brojeve?
Zato što TPDFContentProgram obećava kanonske numeričke operande, i to obećanje vredi više od verbatim teksta unutar content stream-a. Model sadržaja koji se može uređivati, isti onaj na kojem je izgrađen tracker graphics-state-a, postoji da bi NormalizeContentStreams, optimizator i Emit proizvodili stabilan, uporediv izlaz iz proizvoljnog ulaza. Kada bi parsirani operand nosio svoj izvorni tekst u model, niz operatora kao 0.50000 0 0 RG emitovao bi se drugačije od 0.5 0 0 RG, i svako nizvodno poređenje odstupalo bi sa formaterskim navikama proizvođača
Zato model skida izvorni tekst na svoje dve ulazne tačke. NormalizeContentNumbers se izvršava nad svakim operandom kada ga parser potisne i ponovo unutar SetOperand kada se dekodira izvor koji je zadao pozivalac, i rekurzivno prolazi kroz nizove i rečnike tako da su dash pattern-i, TJ nizovi i property rečnici označenog sadržaja pokriveni. Poziv SetTo(AsDouble) na svakom numeriku je dovoljan, jer je to upravo operacija koja briše tekst. Sirovi inline image podaci ostaju netaknuti, kao i uvek
// Lib/PDFlibContentModel.pas — content model zadržava svoj ugovor
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 pozivaoce je zato jednostavno. Prost LoadFromFile praćen SaveToFile ostavlja netaknute content stream-ove i netaknute brojeve u rečnicima onakvima kakvi su bili. Stranica koja prođe kroz NormalizeContentStreams, ili bilo koja izmena napravljena kroz content model, izlazi kanonska po dizajnu, a ostatak dokumenta je i dalje sačuvan. To su dva različita zahteva, i sada rade dve različite stvari
Šta to košta, i gde se garancija zaustavlja
Svaki TPDFNumeric sada nosi jednu dodatnu AnsiString referencu, i svaka parsirana decimala drži svoj izvorni tekst živim za celi životni vek objekta. Na dokumentu sa milionima realnih brojeva to je stvarna memorija, i to pripada svakom merenju velikih dokumenata umesto da se odmahne rukom. Garancija je takođe ograničena na dokument samog broja: kopiranje objekata između dokumenata ili rekonstrukcija vrednosti kroz object API proizvodi nove brojeve, koji prate izlaznu preciznost kao svaki drugi nov broj. Vredi biti precizan o tome šta izdanje tvrdi a šta ne. Učitavanje-i-čuvanje netaknutog dokumenta sada čuva kalibracione brojeve koje renderer stvarno troši, što je svojstvo koje baseline korpusa proverava. Ne tvrdi bajt identičan izlaz, koji takođe zavisi od numerisanja objekata, kompresije stream-ova i identifikatora u trailer-u o kojem se govori u članku o determinističkom PDF ID-u. I ne čini da fingerprint diff vidi razlike u zaokruživanju u fajlovima koje je proizveo drugi softver, jer se i oni hash-uju po normalizovanom telu. Pouka se generalizuje mnogo dalje od CalRGB-a: kada parser čuva samo konvertovanu vrednost, svako čuvanje je izmena, a jedini način da se to primeti je da se pogleda renderovani rezultat. Rukovanje brojevima i semantika SetPrecision dokumentovani su na stranici proizvoda losLab PDF Developer Library