PDFlibPas, losLab PDF Developer Library, păstrează textul zecimal exact pe care l-a parsat pentru fiecare număr real din document și scrie textul acela înapoi întocmai de fiecare dată când valoarea nu a fost modificată niciodată. Din v3.539.19, setarea SetPrecision guvernează doar numerele pe care librăria le creează sau le editează, așa că un simplu încarcă-și-salvează nu mai rotunjește un /Gamma CalRGB de 2.22221 în jos la 2.2222 și nu mai deplasează culorile unei pagini pe care nu a atins-o nimeni. Schimbarea este mică în cod și mare în ce spune despre parsere: valoarea pe care o decodezi și literalul pe care îl emiți sunt două lucruri diferite, iar un dus-întors prin Double nu este o transformare identitate
De ce a deplasat o salvare fără modificări culorile paginii?
Pentru că parametrii spațiului de culoare erau reformatați, nu imaginea. Fișierul care a scos asta la iveală este un document de birou de 35 de pagini din corpusul local de regresie, cu o imagine de antet refolosită pe fiecare pagină. Încărcarea lui și salvarea imediată produceau stream-uri de imagine identice octet cu octet cu intrarea, iar o comparație de hash-uri de stream raporta documentul neschimbat. O comparație de randare nu era de acord: fiecare dintre cele 35 de pagini arăta diferențe de pixeli în antet, și nicăieri altundeva
Imaginea de antet se desenează printr-un spațiu de culoare CalRGB, pe care ISO 32000-1 §8.6.5.3 îl definește printr-un /WhitePoint, un /Gamma opțional cu trei elemente și un /Matrix opțional cu nouă elemente. Tablourile acelea sunt obiecte numerice simple în dicționarul spațiului de culoare. TPDFNumeric stoca fiecare dintre ele ca un Double și nimic altceva, iar TPDFNumeric.Output formata acel Double prin PDFPrecNum, care are implicit patru zecimale. Așa că /Gamma a trecut de la 2.22221 la 2.2222, o intrare de matrice a trecut de la 0.71519 la 0.7152, iar rendererul a produs fidel culori ușor diferite pornind de la o calibrare ușor diferită. Octeții imaginii erau nevinovați; numerele din jurul lor nu erau. Partea inconfortabilă este cât de invizibil era totul. Compararea octeților de stream decodat nu poate vedea asta, pentru că numerele stau într-un dicționar, nu într-un stream. Compararea payload-urilor de atașament nu poate vedea asta. Chiar și diff-ul de revizii descris în articolul despre nivelurile de modificare ia amprenta unui corp de obiect normalizat, așa că ambele revizii dau același hash și diff-ul le raportează identice. Doar randarea a prins-o, și de aceea baseline-ul de corpus randează fiecare pagină în loc să se încreadă doar în verificările structurale
Valoarea pe care ai parsat-o nu este literalul pe care ar trebui să îl scrii
Un număr real PDF este un șir zecimal, iar ISO 32000-1 §7.3.3 este explicit că este doar un șir zecimal: fără notație de bază, fără formă cu exponent. Anexa C listează apoi precizia pe care o implementare este așteptată să o respecte, aproximativ cinci cifre zecimale semnificative în partea fracționară. O precizie implicită de ieșire de patru este deja sub asta, și devine mai rău lângă zero: PLDoubleToStr scalează valoarea, rotunjește la un întreg și emite 0 când rezultatul este zero, așa că o intrare de matrice de -0.000012345 nu pierde o cifră, ci dispare complet
Ridicarea valorii implicite ar muta doar prăpastia. Reparația este să nu ne mai prefacem că un Double este numărul. Când tokenizer-ul din TPDFStructure.Decode recunoaște un real standard, adică tokenul conține un punct zecimal și niciun marcaj de exponent, stochează textul sursă în noul câmp FOriginalText, alături de valoarea convertită. Output preferă apoi textul acela și cade înapoi pe formatare doar când nu are ce prefera
// Lib/PDFlibStruct.pas — toată reparația pe partea de ieșire
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:= ''; // un număr editat este un număr nou
FValue:= Value;
FChanged:= True;
End;
Două limite sunt deliberate. Întregii nu sunt păstrați, pentru că formatarea întregilor este deja fără pierderi. Formele cu exponent precum 6.02E23 sunt tolerate la intrare de dragul producătorilor stricați, dar nu sunt păstrate la ieșire, pentru că scrierea lor înapoi ar perpetua o sintaxă pe care §7.3.3 o interzice; trec prin formator ca orice număr generat de librărie. Tokenizer-ul aplică de asemenea reparația lui minimă obișnuită înainte de a stoca textul, așa că un literal cu punct la început precum .5 este păstrat ca 0.5, iar unul cu punct la final precum 5. ca 5.0. Ambele sunt același număr pentru orice cititor și sunt mult mai larg acceptate
Ce garantează SetPrecision după v3.539.19?
TPDFlib.SetPrecision controlează acum zecimalele numerelor pe care le produce librăria însăși: valori desenate prin painter, numere create dintr-un Double, de exemplu prin NewNumeric, și orice valoare parsată care a fost între timp editată cu SetTo. De reținut că textul decodat prin API-ul de obiecte, de exemplu un literal transmis lui SetObjectFromString, trece prin același tokenizer și este păstrat la fel. Un zecimal parsat care nu a fost modificat niciodată își păstrează precizia de intrare indiferent de setare, iar schimbarea setării după încărcare nu îl atinge retroactiv. Intrarea de referință pentru SetPrecision a fost actualizată în aceeași versiune ca să spună exact asta, pentru că formularea veche sugera că setarea se aplică fiecărui număr din fișier
Curățarea se face în SetTo, nu derivată din flag-ul Changed, iar distincția contează. Pipeline-ul de salvare resetează Changed pe obiecte după ce au fost scrise, așa că o verificare de forma „emite textul original dacă nu s-a schimbat” ar începe să emită text perimat pentru o valoare care a fost editată, salvată și editată din nou în aceeași sesiune. Legarea textului original de atribuirea însăși face imposibil ca cele două să nu fie de acord. Testul de regresie fixează fiecare dintre comportamentele acestea cu valorile din fișierul original
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]'));
// Intrarea needitată supraviețuiește întocmai, inclusiv valoarea pe care
// formatarea cu patru zecimale ar fi prăbușit-o la 0
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// O editare aruncă textul original și urmează PDFPrecNum
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// Scăderea preciziei după aceea nu ajunge la intrarea needitată
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
De ce normalizează modelul de conținut totuși numerele?
Pentru că TPDFContentProgram promite operanzi numerici canonici, iar promisiunea aceea valorează mai mult decât textul întocmai în interiorul unui stream de conținut. Modelul de conținut editabil, același pe care este construit tracker-ul de stare grafică, există ca NormalizeContentStreams, optimizatorul și Emit să producă ieșire stabilă și comparabilă din intrare arbitrară. Dacă un operand parsat și-ar duce textul original în model, o secvență de operatori precum 0.50000 0 0 RG ar fi emisă altfel decât 0.5 0 0 RG, iar fiecare comparație din aval ar deriva odată cu obiceiurile de formatare ale producătorului
Așa că modelul elimină textul original la cele două puncte de intrare ale sale. NormalizeContentNumbers rulează pe fiecare operand în timp ce parserul îl împinge și din nou în interiorul lui SetOperand când se decodează sursa furnizată de apelant, și recursează prin tablouri și dicționare, ca modelele de dash, tablourile TJ și dicționarele de proprietăți ale conținutului marcat să fie acoperite. Apelul SetTo(AsDouble) pe fiecare numeric este de ajuns, pentru că exact aceea este operația care curăță textul. Datele brute de imagine inline sunt lăsate în pace, cum au fost mereu
// Lib/PDFlibContentModel.pas — modelul de conținut își ține contractul
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;
Regula practică pentru apelanți este deci simplă. Un LoadFromFile simplu urmat de SaveToFile lasă stream-urile de conținut neatinse și numerele de dicționar neatinse așa cum erau. O pagină care trece prin NormalizeContentStreams, sau orice editare făcută prin modelul de conținut, iese canonică prin construcție, iar restul documentului este tot păstrat. Sunt două cereri diferite, și acum fac două lucruri diferite
Cât costă și unde se oprește garanția
Fiecare TPDFNumeric cară acum o referință AnsiString în plus, iar fiecare zecimal parsat își ține textul sursă viu cât trăiește obiectul. Pe un document cu milioane de numere reale asta este memorie reală, și își are locul în orice măsurătoare pe documente mari, în loc să fie trecută cu mâna. Garanția este de asemenea limitată la documentul propriu al numărului: copierea obiectelor între documente sau reconstruirea valorilor prin API-ul de obiecte produce numere noi, care urmează precizia de ieșire ca orice alt număr nou. Merită să fim preciși în privința a ce pretinde și ce nu pretinde versiunea. Un încarcă-și-salvează al unui document neatins păstrează acum numerele de calibrare pe care rendererul le consumă de fapt, care este proprietatea verificată de baseline-ul de corpus. Nu pretinde ieșire identică octet cu octet, care depinde și de numerotarea obiectelor, de compresia stream-urilor și de identificatorul de trailer discutat în articolul despre ID-ul PDF determinist. Și nu face diff-ul de amprente să vadă diferențe de rotunjire în fișiere produse de alte programe, pentru că acelea tot hash-uiesc corpul normalizat. Lecția se generalizează mult dincolo de CalRGB: când un parser păstrează doar valoarea convertită, fiecare salvare este o editare, iar singurul mod de a observa este să te uiți la rezultatul randat. Gestionarea numerelor și semantica SetPrecision sunt documentate pe pagina de produs losLab PDF Developer Library