PDFlibPas, losLab PDF Developer Library za Delphi, zapiše vsako število, ki ga spravi v tok vsebine, z decimalnim ločilom pika in brez eksponenta, kar koli že pravijo regionalne nastavitve Windows. Od v3.539.26 AddPageMatrix, ScalePage, DeskewPage, RedactRegion, izpis text-to-path in prebarvanje oblikujejo operande skozi PLDoubleToStrConst, od v3.539.33 pa razčlenjevalniki, ki ta števila berejo nazaj, uporabljajo PLTryStrToFloatInvariant namesto sistemskega locale. Na nemškem, francoskem ali brazilskem računalniku ista koda zdaj da iste bajte kot na ameriškem, to pa je edino obnašanje, ki ga format datoteke lahko sprejme
Zakaj locale z decimalno vejico pokvari PDF brez napake?
Locale z decimalno vejico pokvari PDF tiho, ker vejica v skladnji PDF ni znak števila, zato se škoda bere kot veljavni žetoni z napačnim pomenom. Pred popravkom je bil PLFloatToStr nič drugega kot gol klic FloatToStr, FloatToStr pa sledi FormatSettings.DecimalSeparator. Z ločilom vejica je AddPageMatrix(0.5, 0.5, 0, 0) zapisal 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 v številu dovoli števke, eno piko in vodilni predznak ter nič drugega, zato razčlenjevalnik vsebine prebere to vrstico kot število 0, ki mu sledi neznan žeton ,5, operator cm pa konča z napačnimi operandi. Nič ne sproži, nič ne beleži. Stran se preprosto izriše s transformacijsko matriko, ki je zdrsnila, povratno delo od napačno postavljene risbe do lokalne nastavitve pa je beden popoldan
Druga napaka se skriva za prvo. FloatToStr uporablja format ffGeneral, ki se, ko velikost pade pod 1E-4, preklopi na eksponentni zapis, zato je droben odmik izšel kot 1E-5. Isti §7.3.3 navaja, da PDF ne podpira eksponentne oblike, kar pomeni, da je lahko tudi računalnik z ameriškim locale zapisal neveljaven operand, če je bila vrednost dovolj majhna. Regresijski preizkusi te izdaje pribijeto obe obliki odpovedi: ločilo preklopijo na vejico, pokličejo API in preiščejo nastalo vsebino za vsakim žetonom, ki vsebuje vejico ali 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 namizja de-DE
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 in kasneje zapišejo: 0.5 0 0 0.25 0.00001 12.75 cm
// starejše verzije so zapisale: 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 vrsti števil, dve družini pomožnikov
Popravek v PDFlibPas je stroga ločitev: števila, prikazana ljudem, smejo slediti locale, števila, zapisana za stroj, pa tega nikoli. PLFloatToStr in PLStrToFloat ostajata v PDFlibExtra.pas za besedilo, namenjeno uporabniku, njuni deklaraciji pa zdaj nosita komentar, ki pravi točno to. Vse, kar konča kot skladnja PDF, gre skozi PLDoubleToStrConst s fiksnim številom decimalnih mest, izbranim za posel: šest za matrike, štiri za koordinate in prilagoditve TJ, tri za barve in pravokotnike FDF. Revizija za v3.539.26 se je dotaknila več klicnih mest, kot je predlagal prvotno poročilo o hrošču:
AddPageMatrix,ScalePageinDeskewPage, ki vsi pripnejocmpred obstoječo vsebino strani- Graditelji elementov strani, ki izdajo ponastavitve
Tm, pomikeTJin transformacijecm - Matrike postavitve glifov in točke orisa v pretvorniku text-to-path
- Črno polnilno okence, ki ga pripne
RedactRegion, vrednosti/Rectv izvozu FDF in operandi, ki jih zapiše prebarvanje
PLDoubleToStrConst je ročno napisana funkcija oblikovanja, ne ovijalka okoli FloatToStrF, tri od njenih lastnosti pa so tukaj pomembne. Vedno zapiše piko in odstrani ničle na koncu, tako da 0.5 ostane 0.5 in ne 0.500000. Za končni vhod nikoli ne zapiše eksponenta. Neničelna vrednost, manjša od zahtevane natančnosti, pa obdrži svoje signifikantne števke, namesto da bi se sesedla v nič, zato PLDoubleToStrConst(1E-9, 6) vrne 0.000000001; šele vrednosti pod približno 5E-16 postanejo 0. Zadnje pravilo obstaja, ker zaokrožitev drobnega merilnega faktorja na nič spremeni veljavno matriko v singularno, kar je slabši hrošč kot tisti, ki se ga popravlja
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Izhod za stroj: decimalna pika, brez eksponenta, končne ničle odstranjene
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // obdrži 4 signifikantne števke
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Vhod za stroj: mehka odpoved namesto EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // števila vsebine nikoli ne uporabljajo vejice
end;
Zakaj je stran razčlenjevanja bolj nevarna kot stran pisanja?
Stran razčlenjevanja je bolj nevarna, ker razčlenjevalnik, vezan na locale, ne da napačnega števila, temveč vrže. PLStrToFloat pokliče StrToFloat, ki sproži EConvertError, ko se besedilo ne ujema s sistemskim ločilom. Na sistemu z decimalno vejico je to pomenilo, da je RecolorPage odpovedal v trenutku, ko je srečal povsem običajnega operatorja 0.5 g, zato je vsaka resnična stran odpovedala, ne le eksotične. RenderPageRegionToFile je odklonil lastni dokumentirani format izreza "10.5,20.5,50.5,40.5", atributi dolžin SVG, barve izvoza SVG, seznami točk opomb in vrednosti trdnosti output intenta pa so bili bodisi zavrnjeni bodisi tiho zamenjani s privzetki. Knjižnica, ki deluje popolnoma na razvijalčevem računalniku in odpove pri prvi stranki v Münchnu, je točno tista vrsta kode, ki, tako kot primeri v članku o kodi Delphi, ki deluje po naključju, izgleda pravilna samo zaradi kraja, kjer je bila testirana
v3.539.33 je razvrstil vsak klic StrToFloat in TryStrToFloat po tem, od kod prihaja njegov vhod. Operandi toka vsebine, atributi SVG, barvni nizi risalnika ter seznami izrezov in točk, ločeni z vejico, vsi imajo fiksno skladnjo s piko, zato zdaj gredo skozi PLTryStrToFloatInvariant, ki obreže besedilo, ga razčleni s PLInvariantFormatSettings in za prazen, napačno oblikovan ali nekončen vhod vrne False namesto da bi vržel. Seznam, ločen z vejico, ne pušča prostora za kompromis, ker vejica ne more biti hkrati ločilo seznama in decimalno znamenje. Isti prehod je popravil tudi pisanje čez mejo: RenderPageRegionToFile je nekoč shranil peto vrednost izreza za svojim štiriehtotničnim medpomnilnikom. Za cevovod prebarvanja, opisan v vodniku za pretvorbo PDF v en barvni prostor, je praktični rezultat, da RecolorPage in RecolorDocument na sistemu z decimalno vejico več ne odpovedata. Vrednosti pravil, ki jih klicatelj vtipka v CheckDocumentPolicy, pa so edini primer razčlenjevanja, ki namesto tega uporablja popustljivega pomožnika, iz razloga, ki ga razloži naslednji razdelek
Kaj se zgodi, če popravite samo en konec povratne poti?
Popravec samo enega konca locale povratne poti pokvari kodo, ki je delovala, zato je sprememba atributov strukture v v3.539.32 premaknila pisalca in bralca skupaj. Ovijalke SetStructElem* posredujejo števila kot nize: SetStructElemBBox oblikuje štiri vrednosti v en niz, shrani ga skozi AddTagAttribute, pisalec /A pa ta niz kasneje razčleni, da se odloči, ali postane število, polje ali ime. Oba konca sta uporabljala sistemski locale, zato je bila povratna pot na sistemu z decimalno vejico sama sebi skladna. Hrošč se je pokazal šele, ko je klicatelj sledil dokumentaciji in podal "0.5" k AddTagAttribute: bralec ga ni znal razčleniti in je izdal PDF ime /0.5. Odrigiralnik PDF/VCR je imel zrcalno obrnjen problem, ker je knjižnica generirala GTS_BBox s piko in ga nato pred shranjevanjem validirala z locale
Sprememba samo pisalca na piko bi bila slabša od ničesar, ker bi potem vsaka vrednost SetStructElem* odpovedala locale-vezanemu bralcu in degradirala v ime. Zato pisci zdaj uporabljajo PLDoubleToStrConst(v, 6), bralec pa novega PLTryStrToFloatLenient, ki najprej poskusi obliko s piko in pade nazaj na sistemski locale. Klicatelj z vejico kot locale, ki je v preteklosti podal "1,25", še vedno dobi število 1.25. Kompromis je nameren in dokumentiran: na nemškem sistemu bi "1.500" postalo ime, ker StrToFloat odkloni ločila tisočic, zdaj pa se prebere kot 1.5, dobesedni nizi NAN in INF pa kot števila niso več sprejeti
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // klicatelj z decimalno vejico
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, prej /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // še vedno /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Kjer NaN in neskončnost ustavita
AddPageMatrix, ScalePage in RedactRegion zdaj že na vratih odklonijo argumente NaN in neskončne ter vrnejo 0, ker nobeno število PDF jih ne more predstaviti. ScalePage je faktorje nič ali manj že odklanjal, a NaN prestane preizkus <= 0, zato je NaN merilo nekoč potovalo vse do oblikovalne funkcije. V v3.539.26 je ta še vedno poklicala Round na NaN, kar sproži EInvalidOp na Win32, kjer enota x87 ne maskira neveljavnih operacij; v3.539.31 je določil, da PLDoubleToStrConst za NaN zapiše 0 kot zadnjo linijo obrambe, a nič v matriki je singularna transformacija, zato je preizkus na ravni API še vedno pravi popravek. Dve meji ostajata na mestu namerno. Nizi stanja metafile se zapišejo in preberejo z locale znotraj enega procesa in nikoli ne zapustijo njega, zato so bili puščeni pri miru. Preizkus, ki oblikuje 1E-5 skozi pot elementov strani, pa mora vsebino prebrati, preden je plast prepisana, ker ponovna izdaja operandov na natančnosti dokumenta zakonito spremeni to vrednost v 0
Če vaša aplikacija potuje k strankam izven sveta decimalne pike, je najvarnejša navada tista, ki jo zdaj uporablja testna zbirka PDFlibPas: poti, ki ustvarjajo PDF, poženite enkrat z FormatSettings.DecimalSeparator nastavljenim na vejico in preiščite izhod za vejicami in eksponenti. Članek o ohranjanju razčlenjene decimalne natančnosti pokrije drugo polovico iste zgodbe, kako števila, prebrana iz obstoječe datoteke, ob shranjevanju obdržijo svoje točno besedilo. Prenosi, popolna referenca API in preizkusna verzija so na strani izdelka PDFlibPas Delphi PDF library