PDFlibPas, losLab PDF Developer Library za Delphi, svaki broj koji upisuje u content stream piše sa tačkom kao decimalnim separatorom i bez eksponenta, ma šta Windows regionalna podešavanja tvrdila. Od v3.539.26 AddPageMatrix, ScalePage, DeskewPage, RedactRegion, text-to-path izlaz i prebojavanje formatiraju operande kroz PLDoubleToStrConst, a od v3.539.33 parseri koji te brojeve čitaju nazad koriste PLTryStrToFloatInvariant umesto lokala sistema. Na nemačkoj, francuskoj ili brazilskoj mašini isti kod sada daje iste bajtove kao na američkoj, a to je jedino ponašanje koje jedan fajl format može da trpi
Zašto lokal sa zapetom decimalom pokvari PDF bez ijedne greške?
Lokal sa zapetom decimalom tiho kvari PDF jer zarez nije znak broja u PDF sintaksi, pa se šteta čita kao validni tokeni sa pogrešnim značenjem. Pre popravke, PLFloatToStr nije bio ništa drugo nego gol FloatToStr poziv, a FloatToStr prati FormatSettings.DecimalSeparator. Sa zarezom kao separatorom, AddPageMatrix(0.5, 0.5, 0, 0) upisao je 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 dopušta cifre, jednu tačku i vodeći znak u broju i ništa više, pa content parser tu liniju čita kao broj 0 praćen nepoznatim tokenom ,5, i operator cm završi sa pogrešnim operandima. Ništa ne podigne grešku, ništa ne upiše u log. Stranica se jednostavno iscrta sa transformacionom matricom koja je otisla, a trag od pomerjenog crteža nazad do lokala je jadno popodne
Drugi defekat krije se iza prvog. FloatToStr koristi ffGeneral format, koji na eksponencijalni zapis pređe čim veličina padne ispod 1E-4, pa je sićušni offset izašao kao 1E-5. Isti §7.3.3 navodi da PDF ne podržava eksponencijalni oblik, što znači da bi i mašina sa US lokalom mogla da upiše neispravan operand uz dovoljno malu vrednost. Regresioni testovi ovog izdanja prikivaju oba oblika kvara: prevrnu separator na zarez, pozovu API i pretraže nastali sadržaj za svaki token koji sadrži zarez ili eksponent
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
OldSeparator: Char;
begin
Lib := TPDFlib.Create;
try
Lib.SetPageDimensions(300, 200);
Lib.DrawBox(10, 10, 20, 20, 1);
OldSeparator := FormatSettings.DecimalSeparator;
try
FormatSettings.DecimalSeparator := ','; // simulacija nemačke radne površine
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 i noviji upisuju: 0.5 0 0 0.25 0.00001 12.75 cm
// stariji buildovi su pisali: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
Dve vrste brojeva, dve porodice pomoćnih funkcija
Popravka u PDFlibPas-u je strog raspored: brojevi namenjeni ljudima smeju pratiti lokal, a brojevi upisivani za mašinu nikada. PLFloatToStr i PLStrToFloat ostaju u PDFlibExtra.pas za tekst okrenut korisniku, i njihova deklaracija sada nosi komentar koji to tačno kaže. Sve što postaje PDF sintaksa prolazi kroz PLDoubleToStrConst sa fiksnim brojem decimala izabranim po poslu: šest za matrice, četiri za koordinate i TJ pomeraje, tri za boje i FDF pravougaonike. Revizija za v3.539.26 dotakla je više poziva nego što je prvobitni bug izveštaj naveo:
AddPageMatrix,ScalePageiDeskewPage, koji svi ispred postojećeg sadržaja stranice dodajucm- Graditelji elemenata stranice koji izbacuju
Tmresete,TJpomeraje icmtransformacije - Matrice pozicija glifova i tačke konture u text-to-path konverteru
- Crni popunjen kvadrat koji
RedactRegiondodaje ispred,/Rectvrednosti u FDF izvozu i operandi koje prebojavanje upisuje
PLDoubleToStrConst je ručno pisani formater, a ne omotač oko FloatToStrF-a, i tri njegove osobine su ovde bitne. Uvek upisuje tačku i skida prateće nule, pa 0.5 ostaje 0.5 a ne 0.500000. Nikada ne upisuje eksponent za konačan ulaz. I ne-nulta vrednost manja od tražene preciznosti zadržava svoje značajne cifre umesto da se sruši na nulu, pa PLDoubleToStrConst(1E-9, 6) vraća 0.000000001; tek vrednosti ispod otprilike 5E-16 postaju 0. Poslednje pravilo postoji jer pretvaranje sićušnog faktora skaliranja u nulu od validne matrice pravi singularnu, a to je gori bug od onog koji se ispravljao
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Mašinski izlaz: tačka kao decimala, bez eksponenta, bez pratećih nula
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // čuva 4 značajne cifre
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Mašinski ulaz: blag kvar umesto EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // content brojevi nikada ne koriste zarez
end;
Zašto je parsirajuća strana opasnija od upisne?
Parsirajuća strana je opasnija jer parser vezan za lokal ne proizvede pogrešan broj nego baci izuzetak. PLStrToFloat zove StrToFloat, koji podiže EConvertError kad tekst ne odgovara separatoru sistema. Na sistemu sa zapetom decimalom to je značilo da RecolorPage odustaje u trenutku kad sretne sasvim običan operator 0.5 g, pa su padale sve stranice iz stvarnog sveta, ne samo egzotične. RenderPageRegionToFile odbijao je sopstveni dokumentovani clip format "10.5,20.5,50.5,40.5", a SVG atributi dužina, SVG boje izvoza, liste temena anotacija i vrednosti solidnosti output intenta ili su odbijani ili tiho zamenjivani podrazumevanim. Biblioteka koja radi savršeno na mašini programera a pada na prvom mušteriji u Minhenu baš je ta vrsta koda koja, poput slučajeva iz članka o Delphi kodu koji radi slučajno, deluje ispravno samo zbog toga gde je testirana
v3.539.33 je svaki StrToFloat i TryStrToFloat poziv svrstao prema tome odakle mu ulaz dolazi. Operandi content streamova, SVG atributi, painterski stringovi boja i liste clipova i temena razdvojeni zarezom imaju fiksnu tačkastu sintaksu, pa sada prolaze kroz PLTryStrToFloatInvariant, koji odseče beline, parsira uz PLInvariantFormatSettings i vraća False za prazan, neispravan ili ne-konačan ulaz umesto da baci izuzetak. Lista razdvojena zarezom ne ostavlja prostor za kompromis, jer zarez ne može biti istovremeno delimiter liste i decimalni znak. Isti prolaz ispravio je i upis van granica: RenderPageRegionToFile je u bafer od četiri elementa upisivao i petu clip vrednost. Za cevovod prebojavanja opisan u vodiču za pretvaranje PDF-a u jedan color space, praktičan rezultat je da se RecolorPage i RecolorDocument više ne prekidaju na sistemu sa zapetom decimalom. Vrednosti pravila koje pozivalac ukuca u CheckDocumentPolicy jedini su parsirajući slučaj koji umesto toga koristi popustljiviji helper, iz razloga koji objašnjava sledeći odeljak
Šta se dešava ako popravite samo jedan kraj round tripa?
Popravka samo jednog kraja round tripa kvari kod koji je ranije radio, i zato je izmena strukturnih atributa u v3.539.32 pomerila pisca i čitača zajedno. SetStructElem* omotači prenose brojeve kao stringove: SetStructElemBBox formatira četiri vrednosti u jedan string, čuva ga kroz AddTagAttribute, a pisac /A-e kasnije parsira taj string da odluči da li postaje broj, niz ili ime. Oba kraja koristila su lokal sistema, pa je round trip na sistemu sa zapetom decimalom bio samosaglasan. Bug se pokazao tek kad je pozivalac pratio dokumentaciju i prosledio "0.5" u AddTagAttribute: čitač ga nije umeo da parsira i izbacio je PDF ime /0.5. PDF/VCR placeholder imao je ogledalo-problem, jer je biblioteka generisala GTS_BBox sa tačkom, a zatim ga pre čuvanja validirala lokalom
Promena samo pisca na tačku bila bi gora od nerada, jer bi svaka SetStructElem* vrednost tada padala na čitaču vezanom za lokal i propadala u ime. Zato pisci sada koriste PLDoubleToStrConst(v, 6), a čitač novi PLTryStrToFloatLenient, koji prvo proba tačkasti oblik pa pada na lokal sistema. Pozivalac sa zapetim lokalom koji je ranije prosledio "1,25" i dalje dobija broj 1.25. Kompromis je nameran i dokumentovan: na nemačkom sistemu "1.500" je ranije postajalo ime jer StrToFloat odbija separatore hiljada, a sada se čita kao 1.5, dok literalni NAN i INF stringovi više nisu prihvatani kao brojevi
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // pozivalac sa zapetom decimalom
Lib := TPDFlib.Create;
try
Lib.BeginTag('Figure', 'Sales chart', '');
Lib.SetStructElemBBox(10.5, 20.25, 200.5, 100.75); // /BBox [ 10.5 20.25 200.5 100.75 ]
Lib.AddTagAttribute('Layout', 'SpaceAfter', '0.5'); // /SpaceAfter 0.5, ranije /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // i dalje /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Gde se NaN i beskonačnost zaustavljaju
AddPageMatrix, ScalePage i RedactRegion sada unapred odbijaju NaN i beskonačne argumente i vraćaju 0, jer nijedan PDF broj ne može da ih predstavi. ScalePage je već odbijao faktore nula ili manje, ali NaN prolazi test <= 0, pa je NaN skala stizala sve do formatera. U v3.539.26 taj formater je nad NaN-om još zvao Round, koji podiže EInvalidOp na Win32 gde x87 jedinica ne maskira neispravne operacije; v3.539.31 je učinio da PLDoubleToStrConst za NaN upiše 0 kao poslednju liniju odbrane, ali je nula u matrici singularna transformacija, pa je provera na nivou API-ja i dalje prava popravka. Dve granice ostaju na mestu namerno. Metafile state stringovi upisuju se i čitaju lokalom unutar jednog procesa i nikada ga ne napuštaju, pa su ostavljeni na miru. I test koji formatira 1E-5 kroz putanju elemenata stranice mora sadržaj da pročita pre nego što se sloj prepiše, jer ponovno izbacivanje operanada na preciznosti dokumenta tu vrednost zakonito pretvara u 0
Ako vaša aplikacija ide mušterijama van sveta tačkastih decimala, najbezbednija navika je ona koju test skup PDFlibPas-a sada koristi: jednom provucite PDF-proizvodne putanje uz FormatSettings.DecimalSeparator postavljen na zarez i pretražite izlaz za zarezima i eksponentima. Članak o čuvanju preciznosti parsiranih decimala pokriva drugu polovinu iste priče, kako brojevi pročitani iz postojećeg fajla pri čuvanju zadržavaju svoj tačan tekst. Preuzimanja, kompletna API referenca i probni build nalaze se na stranici proizvoda PDFlibPas Delphi PDF library