PDFlibPas, losLab PDF Developer Library za Delphi, zapisuje svaki broj koji stavi u content stream s točkom kao decimalnim separatorom i bez eksponenta, ma što govorile regionalne postavke Windowsa. 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 natrag koriste PLTryStrToFloatInvariant umjesto lokala sustava. Na njemačkom, francuskom ili brazilskom stroju isti kod sada proizvodi iste bajtove kao na američkom, a to je jedino ponašanje koje format datoteke može podnijeti
Zašto lokal sa zarezom kao decimalom pokvari PDF bez pogreške?
Lokal sa zarezom kao decimalom tiho pokvari PDF jer zarez nije znak broja u PDF sintaksi, pa se šteta čita kao valjani tokeni s krivim značenjem. Prije ispravka PLFloatToStr nije bio ništa više od golog poziva FloatToStr, a FloatToStr slijedi FormatSettings.DecimalSeparator. Sa zarezom kao separatorom, AddPageMatrix(0.5, 0.5, 0, 0) zapisao je 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 dopušta znamenke, jednu točku i vodeći predznak u broju i ništa više, pa content parser tu liniju čita kao broj 0 iza kojeg slijedi nepoznat token ,5, i operator cm završi s krivim operandima. Ništa ne baci, ništa ne zapiše. Stranica se jednostavno renderira s transformacijskom matricom koja je odlutala, a rad unatrag od pomaknutog crteža do postavke lokala jest jadno poslijepodne
Drugi se defekt krije iza prvoga. FloatToStr koristi format ffGeneral, koji pređe na eksponencijalni zapis čim veličina padne ispod 1E-4, pa je sitni offset izašao kao 1E-5. Isti §7.3.3 kaže da PDF ne podržava eksponencijalni oblik, što znači da bi i stroj s američkim lokalom mogao zapisati nevaljan operand uz dovoljno malu vrijednost. Regresijski testovi ovog izdanja pribijaju oba oblika kvara: prebace 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 := ','; // simuliraj de-DE radnu površinu
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 i noviji zapisuju: 0.5 0 0 0.25 0.00001 12.75 cm
// stariji buildovi: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
Dva tipa brojeva, dvije obitelji helpera
Ispravak u PDFlibPasu stroga je podjela: brojevi pokazani ljudima mogu slijediti lokal, a brojevi zapisani za stroj nikad. PLFloatToStr i PLStrToFloat ostaju u PDFlibExtra.pas za tekst okrenut korisniku, i njihova deklaracija sada nosi komentar koji upravo to kaže. Sve što završi kao PDF sintaksa prolazi kroz PLDoubleToStrConst s fiksnim brojem decimalnih mjesta odabranim za posao: šest za matrice, četiri za koordinate i TJ prilagodbe, tri za boje i FDF pravokutnike. Revizija za v3.539.26 dirala je više call siteova nego što je izvorno izvješće o bugu nagovještavalo:
AddPageMatrix,ScalePageiDeskewPage, koji svi dodajucmispred postojećeg sadržaja stranice- Graditelji elemenata stranice koji ispuštaju
Tmresetove,TJpomake icmtransformacije - Matrice pozicioniranja glifova i outline točke u text-to-path konverteru
- Crna kutija za popunjavanje koju
RedactRegiondoda ispred, vrijednosti/Rectu FDF izvozu i operandi koje prebojavanje zapiše
PLDoubleToStrConst ručno je rađeni formater, a ne omot oko FloatToStrF, i tri mu svojstva ovdje znače. Uvijek zapisuje točku i reže zaostale nule, pa 0.5 ostane 0.5 umjesto 0.500000. Nikad ne zapiše eksponent za konačan ulaz. I nenulta vrijednost manja od tražene preciznosti zadrži svoje značajne znamenke umjesto da se sruši u nulu, pa PLDoubleToStrConst(1E-9, 6) vrati 0.000000001; samo vrijednosti ispod otprilike 5E-16 postanu 0. To posljednje pravilo postoji jer zaokruživanje sitnog faktora skaliranja u nulu pretvori valjanu matricu u singularnu, što je gori bug od onoga koji se popravlja
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Strojni izlaz: točkasta decimala, bez eksponenta, zaostale nule izrezane
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // zadržava 4 značajne znamenke
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Strojni ulaz: blag pad umjesto EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // brojevi u sadržaju nikad ne koriste zarez
end;
Zašto je strana parsiranja opasnija od strane pisanja?
Strana parsiranja opasnija je jer parser vezan uz lokal ne proizvede krivi broj, nego baci. PLStrToFloat zove StrToFloat, koji podigne EConvertError kad se tekst ne poklopi sa separatorom sustava. Na sustavu sa zarezom kao decimalom to je značilo da RecolorPage padne u trenutku kad sretne običan operator 0.5 g, pa su propadale sve stvarne stranice, ne samo egzotične. RenderPageRegionToFile odbijao je vlastiti dokumentirani clip format "10.5,20.5,50.5,40.5", a SVG atributi duljina, boje SVG izvoza, liste vrhova anotacija i vrijednosti output intent čvrstoće ili su odbijani ili tiho zamijenjeni zadanim. Biblioteka koja na razvojnom stroju radi savršeno, a padne na prvom kupcu u Münchenu, točno je ona vrsta koda koja, poput slučajeva u članku o Delphi kodu koji radi slučajno, djeluje ispravno samo zbog toga gdje je testirana
v3.539.33 razvrstao je svaki poziv StrToFloat i TryStrToFloat prema tome odakle mu ulaz dolazi. Operandi content streama, SVG atributi, stringovi boja slikara i liste clipova i vrhova odvojene zarezom sve imaju fiksnu točkastu sintaksu, pa sada prolaze kroz PLTryStrToFloatInvariant, koja obreže tekst, parsira ga s PLInvariantFormatSettings i vrati False za prazan, deformiran ili ne-konačan ulaz umjesto da baci. Lista odvojena zarezom ne ostavlja prostora za kompromis, jer zarez ne može biti i ograničitelj liste i decimalni znak. Isti prolaz popravio je i ispis izvan granica: RenderPageRegionToFile čuvao je petu clip vrijednost iza svog buffera od četiri elementa. Za recoloring pipeline opisan u vodiču kroz pretvorbu PDF-a u jedan color space, praktičan je rezultat da se RecolorPage i RecolorDocument više ne prekidaju na sustavu sa zarezom kao decimalom. Vrijednosti pravila koje pozivatelj utipka u CheckDocumentPolicy jedini su parsing slučaj koji umjesto toga koristi blagi helper, iz razloga koji objašnjava idući odjeljak
Što se dogodi ako popravite samo jedan kraj round tripa?
Popravak samo jednog kraja lokalskog round tripa slomi kod koji je radio, zato je izmjena atributa strukture u v3.539.32 pomakla writer i reader zajedno. Omoti SetStructElem* prenašaju brojeve kao stringove: SetStructElemBBox formatira četiri vrijednosti u jedan string, pohrani ga kroz AddTagAttribute, a writer /A kasnije parsira taj string da odluči postaje li broj, polje ili ime. Oba su kraja koristila lokal sustava, pa je na sustavu sa zarezom kao decimalom round trip bio samodosljedan. Bug se pokazao tek kad je pozivatelj slijedio dokumentaciju i predao "0.5" u AddTagAttribute: reader ga nije mogao parsirati i ispuštao je PDF ime /0.5. PDF/VCR placeholder imao je ogledalski problem, jer je biblioteka generirala GTS_BBox s točkom, a onda ga validirala lokalom prije spremanja
Mijenjati samo writera u točku bilo bi gore od nerada, jer bi tada svaka vrijednost SetStructElem* padala na readeru vezanom uz lokal i degradirala u ime. Zato writeri sada koriste PLDoubleToStrConst(v, 6), a reader novi PLTryStrToFloatLenient, koji prvo proba točkasti oblik, pa se vrati na lokal sustava. Pozivatelj sa zarez-lokalom koji je u prošlosti predao "1,25" i dalje dobije broj 1.25. Kompromis je namjeran i dokumentiran: na njemačkom sustavu "1.500" postajalo je ime jer StrToFloat odbija separatore tisuća, a sada se čita kao 1.5, dok se literalni stringovi NAN i INF više ne prihvaćaju kao brojevi
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // pozivatelj sa zarezom kao 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, nekad /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;
Gdje se NaN i beskonačnost zaustave
AddPageMatrix, ScalePage i RedactRegion sada unaprijed odbijaju NaN i beskonačne argumente i vraćaju 0, jer nijedan PDF broj ne može predstaviti te vrijednosti. ScalePage već je odbijao faktore nula ili manje, ali NaN prolazi test <= 0, pa je NaN skala nekada putovala sve do formatera. U v3.539.26 taj je formater još zvao Round nad NaN-om, što podigne EInvalidOp na Win32 gdje x87 jedinica ne maskira nevaljane operacije; v3.539.31 natjerao je PLDoubleToStrConst da za NaN zapiše 0 kao posljednju liniju obrane, ali je nula u matrici singularna transformacija, pa je provjera na razini API-ja i dalje pravi ispravak. Dvije granice ostaju na mjestu namjerno. Metafile state stringovi zapisuju se i čitaju s lokalom unutar jednog procesa i nikad ga ne napuštaju, pa su ostavljeni na miru. I test koji formatira 1E-5 kroz put elemenata stranice mora pročitati sadržaj prije nego se sloj prepravi, jer ponovno ispuštanje operanada na preciznosti dokumenta valjano tu vrijednost pretvori u 0
Ako vaša aplikacija ide kupcima izvan svijeta točkastih decimala, najsigurnija je navika ona koju test suite PDFlibPas sada koristi: vrtite PDF-proizvodne puteve jednom s FormatSettings.DecimalSeparator postavljenim na zarez i pretražite izlaz za zareze i eksponente. Članak o očuvanju parsirane decimalne preciznosti pokriva drugu polovicu iste priče, kako brojevi pročitani iz postojeće datoteke čuvaju svoj točan tekst pri spremanju. Preuzimanja, potpuna API referenca i trial build su na stranici proizvoda PDFlibPas Delphi PDF library