PDFlibPas, teda losLab PDF Developer Library, si pre každé reálne číslo v dokumente drží presný desatinný text, ktorý naparsoval, a ten text zapisuje späť doslovne vždy, keď hodnota nebola nikdy upravená. Od verzie v3.539.19 nastavenie SetPrecision ovláda len čísla, ktoré knižnica vytvorí alebo upraví, takže bežné načítanie a uloženie už nezaokrúhli CalRGB /Gamma s hodnotou 2.22221 na 2.2222 a neposunie farby stránky, ktorej sa nikto nedotkol. Zmena je malá v kóde a veľká v tom, čo hovorí o parseroch: hodnota, ktorú dekódujete, a literál, ktorý zapisujete, sú dve rôzne veci a round trip cez Double nie je identická transformácia
Prečo uloženie, ktoré nič nezmenilo, posunulo farby stránky?
Pretože sa preformátovali parametre farebného priestoru, nie obrázok. Súbor, ktorý to odhalil, je 35-stranový kancelársky dokument v lokálnom regresnom korpuse s hlavičkovým obrázkom opakovaným na každej strane. Jeho načítanie a okamžité uloženie vyprodukovalo image streamy, ktoré boli bajt po bajte identické so vstupom, a porovnanie hashov streamov hlásilo dokument ako nezmenený. Porovnanie vykreslenia nesúhlasilo: všetkých 35 stránok vykazovalo pixelové rozdiely v hlavičke a nikde inde
Hlavičkový obrázok sa kreslí cez farebný priestor CalRGB, ktorý ISO 32000-1 §8.6.5.3 definuje cez /WhitePoint, voliteľné trojprvkové pole /Gamma a voliteľnú deväťprvkovú /Matrix. Tie polia sú obyčajné číselné objekty v slovníku farebného priestoru. TPDFNumeric každý z nich ukladal ako Double a nič iné, a TPDFNumeric.Output ten Double formátoval cez PDFPrecNum, ktorý má predvolene štyri desatinné miesta. Takže /Gamma prešlo z 2.22221 na 2.2222, položka matice prešla z 0.71519 na 0.7152 a renderer verne vyprodukoval mierne iné farby z mierne inej kalibrácie. Bajty obrázka boli nevinné; čísla okolo nich nie. Nepríjemné je, aké neviditeľné to bolo. Porovnanie dekódovaných bajtov streamu to nevidí, pretože tie čísla žijú v slovníku, nie v streame. Porovnanie payloadov príloh to nevidí. Dokonca ani revízny diff opísaný v článku o modification levels robí fingerprint normalizovaného tela objektu, takže obe revízie hashujú na tú istú hodnotu a diff ich hlási ako identické. Chytilo to len vykreslenie, a preto korpusový baseline vykresľuje každú stránku namiesto toho, aby slepo veril štrukturálnym kontrolám
Hodnota, ktorú ste naparsovali, nie je literál, ktorý máte zapísať
Reálne číslo v PDF je desatinný reťazec a ISO 32000-1 §7.3.3 je explicitný v tom, že je to len desatinný reťazec: žiadny radix zápis, žiadny exponentový tvar. Annex C potom vypisuje presnosť, ktorú má implementácia dodržať, približne päť významových desatinných miest v zlomkovej časti. Predvolená výstupná presnosť štyri je už pod tým a pri nule sa to zhoršuje: PLDoubleToStr hodnotu škáluje, zaokrúhli na celé číslo a keď je výsledok nula, zapíše 0, takže položka matice -0.000012345 nestratí jednu číslicu, ale zmizne úplne
Zvýšenie predvolenej hodnoty by len posunulo tú priepasť. Opravou je prestať predstierať, že to číslo je Double. Keď tokenizer v TPDFStructure.Decode rozpozná štandardné reálne číslo, teda token obsahuje desatinnú bodku a žiadny exponent marker, uloží zdrojový text do nového poľa FOriginalText popri skonvertovanej hodnote. Output potom preferuje ten text a na formátovanie sa vráti len vtedy, keď nie je čo preferovať
// Lib/PDFlibStruct.pas — celá oprava na výstupnej strane
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:= ''; // upravené číslo je nové číslo
FValue:= Value;
FChanged:= True;
End;
Dve hranice sú zámerné. Celé čísla sa nezachovávajú, pretože formátovanie celých čísel je už bezztrátové. Exponentové tvary ako 6.02E23 sa na vstupe tolerujú kvôli pokazeným producerom, ale na výstupe sa nezachovávajú, keďže ich zápis späť by perpetuoval syntax, ktorú §7.3.3 zakazuje; idú cez formátovač ako každé iné číslo vygenerované knižnicou. Tokenizer pred uložením textu nasadí aj svoju obvyklú minimálnu opravu, takže literál s vedúcou bodkou ako .5 zostane ako 0.5 a literál s koncovou bodkou ako 5. ako 5.0. Pre každého čitateľa je to to isté číslo a je oveľa širšie akceptované
Čo SetPrecision garantuje po v3.539.19?
TPDFlib.SetPrecision teraz ovláda desatinné miesta čísel, ktoré produkuje samotná knižnica: hodnoty kreslené cez painter, čísla vytvorené z Double, napríklad cez NewNumeric, a každú naparsovanú hodnotu, ktorá bola odvtedy upravená cez SetTo. Všimnite si, že text dekódovaný cez objektové API, napríklad literál odovzdaný do SetObjectFromString, prechádza tým istým tokenizerom a zachováva sa rovnako. Naparsované desatinné číslo, ktoré nebolo nikdy upravené, si drží vstupnú presnosť bez ohľadu na nastavenie a zmena nastavenia po načítaní sa ho spätne nedotkne. Referenčný záznam SetPrecision bol v tom istom release upravený tak, aby hovoril presne toto, pretože stará formulácia naznačovala, že nastavenie platí na každé číslo v súbore
Čistenie sa deje v SetTo, nie odvodením od príznaku Changed, a ten rozdiel je dôležitý. Ukladacia pipeline resetuje Changed na objektoch, len čo boli zapísané, takže kontrola v štýle "zapíš pôvodný text, ak sa nezmenil" by začala zapisovať zastaraný text pre hodnotu, ktorá bola v tej istej session upravená, uložená a znova upravená. Zviazanie pôvodného textu so samotným priradením robí nemožným, aby si tieto dve veci odporovali. Regresný test pripína každé z týchto správaní hodnotami z pôvodného súboru
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]'));
// Neupravený vstup prežije doslovne, vrátane hodnoty, ktorú by
// formátovanie na štyri miesta zrútilo na 0
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// Úprava zahodí pôvodný text a riadi sa PDFPrecNum
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// Neskoršie zníženie presnosti sa nedostane na neupravený vstup
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Prečo content model čísla aj tak normalizuje?
Pretože TPDFContentProgram sľubuje kanonické číselné operandy a ten sľub má väčšiu cenu než doslovný text vo vnútri content streamu. Upravovateľný content model, ten istý, na ktorom stojí graphics-state tracker, existuje preto, aby NormalizeContentStreams, optimalizér a Emit produkovali stabilný, porovnateľný výstup z ľubovoľného vstupu. Keby naparsovaný operand niesol svoj pôvodný text do modelu, operátorová sekvencia ako 0.50000 0 0 RG by sa zapisovala inak než 0.5 0 0 RG a každé porovnanie po prúde by sa posúvalo podľa formátovacích zvykov producenta
Model teda strháva pôvodný text na svojich dvoch vstupných bodoch. NormalizeContentNumbers beží na každom operande, keď ho parser zatlačí, a znova vo vnútri SetOperand, keď sa dekóduje zdroj od volajúceho, a rekurzívne prechádza polia aj slovníky, takže pokrýva dash patterns, polia TJ aj slovníky vlastností marked content. Zavolať SetTo(AsDouble) na každom čísle stačí, keďže práve to je tá operácia, ktorá ten text vyčistí. Surové dáta inline obrázkov zostávajú nedotknuté, ako vždy
// Lib/PDFlibContentModel.pas — content model si drží svoj 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;
Praktické pravidlo pre volajúcich je preto jednoduché. Obyčajné LoadFromFile nasledované SaveToFile necháva nedotknuté content streamy aj nedotknuté čísla v slovníkoch tak, ako boli. Stránka, ktorá prejde cez NormalizeContentStreams, alebo akákoľvek úprava urobená cez content model, vyjde zámerne kanonická a zvyšok dokumentu sa stále zachová. To sú dve rôzne požiadavky a teraz robia dve rôzne veci
Čo to stojí a kde záruka končí
Každý TPDFNumeric teraz nesie o jednu referenciu AnsiString navyše a každé naparsované desatinné číslo si drží svoj zdrojový text živý po celý život objektu. V dokumente s miliónmi reálnych čísel je to skutočná pamäť a patrí to do každého merania veľkých dokumentov, namiesto zametania pod koberec. Záruka je navyše ohraničená na vlastný dokument toho čísla: kopírovanie objektov medzi dokumentmi alebo rekonštrukcia hodnôt cez objektové API produkuje nové čísla, ktoré sledujú výstupnú presnosť ako každé iné nové číslo. Stojí za to byť presný v tom, čo release tvrdí a čo netvrdí. Načítanie a uloženie nedotknutého dokumentu teraz zachová kalibračné čísla, ktoré renderer naozaj konzumuje, a to je tá vlastnosť, ktorú korpusový baseline kontroluje. Netvrdí bajtovo identický výstup, ktorý závisí aj od číslovania objektov, kompresie streamov a identifikátora v traileri rozoberaného v článku o deterministickom PDF ID. A nespôsobí, že fingerprint diff uvidí rozdiely v zaokrúhlení v súboroch vyprodukovaných iným softvérom, keďže tie stále hashujú normalizované telo. Lekcia sa zovšeobecňuje ďaleko za CalRGB: keď parser drží len skonvertovanú hodnotu, každé uloženie je úprava a jediný spôsob, ako si to všimnúť, je pozrieť sa na vykreslený výsledok. Spracovanie čísel a sémantika SetPrecision sú zdokumentované na produktovej stránke losLab PDF Developer Library