PDFlibPas, losLab PDF Developer Library for Delphi, zapisuje každé číslo, ktoré dostane do content streamu, s bodkou ako desatinným oddeľovačom a bez exponentu, nech hovoria regionálne nastavenia Windows čokoľvek. Od v3.539.26 formátujú AddPageMatrix, ScalePage, DeskewPage, RedactRegion, výstup text-to-path aj prefarbovanie operandy cez PLDoubleToStrConst a od v3.539.33 parsery, ktoré tie čísla čítajú späť, používajú PLTryStrToFloatInvariant namiesto systémového locale. Na nemeckom, francúzskom či brazílskom stroji vyprodukuje tentýž kód teraz tie isté bajty ako na americkom, a to je jediné správanie, ktoré súborový formát znáša
Prečo pokazí čiarkové locale PDF bez chyby?
Čiarkové locale pokazí PDF potichu, lebo čiarka nie je v PDF syntaxi znak čísla, takže škoda sa číta ako platné tokeny so zlým významom. Pred opravou bolo PLFloatToStr nič iné než holé volanie FloatToStr a FloatToStr sa riadi FormatSettings.DecimalSeparator. S čiarkovým oddeľovačom zapísalo AddPageMatrix(0.5, 0.5, 0, 0) 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 dovoluje v čísle číslice, jednu bodku a prípadné znamienko na začiatku a nič viac, takže content parser prečíta ten riadok ako číslo 0 nasledované neznámym tokenom ,5 a operátor cm skončí so zlými operandmi. Nič nevyhodí výnimku, nič nezaloguje. Strana sa jednoducho vykreslí s transformačnou maticou, ktorá sa posunula, a dohnať z premiestnenej kresby späť k nastaveniu locale je trápenie na celé popoludnie
Druhý defekt sa skrýva za prvým. FloatToStr používa formát ffGeneral, ktorý prejde na exponentový zápis, akonáhle magnitúda klesne pod 1E-4, takže malý offset vyšiel ako 1E-5. Týž §7.3.3 konštatuje, že PDF exponentový tvar nepodporuje, čo znamená, že aj stroj s americkým locale dokázal zapísať neplatný operand pri dostatočne malej hodnote. Regresné testy tohto vydania priklincujú oba tvary zlyhania: prepnú oddeľovač na čiarku, zavolajú API a preskenujú výsledný obsah po akomkoľvek tokene obsahujúcom čiarku či exponent
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 := ','; // simulácia nemeckej plochy de-DE
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 a novšie zapíše: 0.5 0 0 0.25 0.00001 12.75 cm
// staršie zostavenia písali: 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 druhy čísel, dve rodiny helperov
Oprava v PDFlibPas je prísne rozdelenie: čísla určené ľuďom môžu nasledovať locale a čísla písané pre stroj nikdy. PLFloatToStr a PLStrToFloat ostanú v PDFlibExtra.pas pre text orientovaný na používateľa a ich deklarácia teraz nesie komentár, ktorý to hovorí presne takto. Všetko, čo končí ako PDF syntax, prechádza cez PLDoubleToStrConst s pevným počtom desatinných miest zvoleným podľa úlohy: šesť pre matice, štyri pre súradnice a úpravy TJ, tri pre farby a FDF obdĺžniky. Audit pre v3.539.26 zasiahol viac miest volaní, než naznačovalo pôvodné hlásenie chyby:
AddPageMatrix,ScalePageaDeskewPage, ktoré všetky predradiacmpred existujúci obsah strany- Stavitelia elementov strany, ktoré emitujú resety
Tm, posunyTJa transformáciecm - Umiestňovacie matice glyfov a outline body v prevodníku text-to-path
- Čierne výplňové pole, ktoré predradí
RedactRegion, hodnoty/Rectpri FDF exporte a operandy, ktoré zapisuje prefarbovanie
PLDoubleToStrConst je formatter písaný na mieru, nie obal okolo FloatToStrF, a tri jeho vlastnosti sú tu podstatné. Vždy zapisuje bodku a škrtnie nuly na konci, takže 0.5 ostanú 0.5 a nie 0.500000. Pre konečný vstup nikdy nezapíše exponent. A nenulová hodnota menšia než požadovaná presnosť si ponechá svoje významné číslice namiesto kolapsu na nulu, takže PLDoubleToStrConst(1E-9, 6) vráti 0.000000001; pod približne 5E-16 sa hodnoty stanú 0. Posledné pravidlo existuje preto, lebo zaokrúhlenie malého škálovacieho faktora na nulu zmení platnú maticu na singulárnu, čo je horšia chyba než tá opravovaná
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Strojový výstup: bodková decimála, žiadny exponent, koncové nuly odškrtnuté
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // ponechá 4 významné číslice
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Strojový vstup: mäkké zlyhanie namiesto EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // content čísla čiarku nikdy nepoužívajú
end;
Prečo je parsujúca strana nebezpečnejšia než zapisujúca?
Parsujúca strana je nebezpečnejšia, lebo parser viazaný na locale nevyrobí zlé číslo, ale vyhodí výnimku. PLStrToFloat volá StrToFloat, ktoré pri texte nesúhlacom so systémovým oddeľovačom hodí EConvertError. Na čiarkovom systéme to znamenalo, že RecolorPage padla v momente, keď stretla obyčajný operátor 0.5 g, takže zlyhala každá reálna strana, nie len exotické. RenderPageRegionToFile odmietal vlastný zdokumentovaný clip formát "10.5,20.5,50.5,40.5" a atribúty dĺžok SVG, farby SVG exportu, zoznamy vrcholov anotácií a hodnoty solidity output intentu sa buď odmietali, alebo potichu nahradili predvolenými. Knižnica, ktorá na vývojárskej mašine beží dokonale a padne pri prvom zákazníkovi v Mníchove, je presne ten druh kódu, ktorý, ako prípady v článku o Delphi kóde fungujúcom náhodou, vyzerá korektne len vďaka miestu, kde sa testoval
v3.539.33 roztriedilo každé volanie StrToFloat a TryStrToFloat podľa toho, odkiaľ vstup prichádza. Operandy content streamov, atribúty SVG, farebné reťazce painterov a čiarkou oddeľované clip aj vertex zoznamy majú všetky pevnú bodkovú syntax, takže teraz prechádzajú cez PLTryStrToFloatInvariant, ktoré ostrihá text, parsuje ho s PLInvariantFormatSettings a pre prázdny, deformovaný či nekonečný vstup vráti False namiesto vyhodenia výnimky. Čiarkou oddeľovaný zoznam nedáva priestor na kompromis, lebo čiarka nemôže byť súčasne delimiterom zoznamu aj desatinným oddeľovačom. Ten istý priebeh opravil aj zápis mimo rozsahu: RenderPageRegionToFile ukladal piatu clip hodnotu za štvorprvkový buffer. Pre pipeline prefarbovania popísanú v sprievodcovi konverziou PDF do jedného farebného priestoru je praktický výsledok taký, že RecolorPage a RecolorDocument už na čiarkovom systéme nepadia. Hodnoty pravidiel, ktoré volajúci vpíše do CheckDocumentPolicy, sú jediný parsing prípad, ktorý namiesto toho používa zhovievavý helper, a to z dôvodu, ktorý vysvetľuje ďalšia sekcia
Čo sa stane, keď opravíte len jeden koniec round tripu?
Oprava iba jedného konca locale round tripu rozbije kód, ktorý fungoval, a presne preto zmena štruktúrových atribútov vo v3.539.32 posunula zapisovača a čítača spolu. Wrappery SetStructElem* prenášajú čísla ako reťazce: SetStructElemBBox naformátuje štyri hodnoty do jedného reťazca, uloží ho cez AddTagAttribute a zapisovač /A ten reťazec neskôr parsuje, aby rozhodol, či sa stane číslom, poľom alebo názvom. Oba konce používali systémové locale, takže na čiarkovom systéme bol round trip sebakonzistentný. Chyba sa ukázala až vtedy, keď sa volajúci riadil dokumentáciou a podal "0.5" do AddTagAttribute: čítač ho nedokázala rozparsovať a emitovala PDF názov /0.5. Zástupný symbol PDF/VCR mal zrkadlový problém, lebo knižnica generovala GTS_BBox s bodkou a pred uložením ho validovala cez locale
Zmena len zapisovača na bodku by bola horšia ako nerobiť nič, pretože každá hodnota SetStructElem* by potom neprešla čítačom viazaným na locale a degradovala na názov. Preto zapisovače teraz používajú PLDoubleToStrConst(v, 6) a čítač nové PLTryStrToFloatLenient, ktoré najprv skúsi bodkový tvar a až potom spadne k systémovému locale. Volajúci s čiarkovým locale, ktorý kedysi podal "1,25", stále dostane číslo 1.25. Kompromis je zámerný a zdokumentovaný: na nemeckom systéme sa "1.500" kedysi stávalo názvom, lebo StrToFloat odmieta oddeľovače tisícov, a teraz sa prečíta ako 1.5, zatiaľ čo literálne reťazce NAN a INF sa už ako čísla neprijímajú
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // volajúci s čiarkovou decimálou
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, predtým /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // aj naďalej /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Kde sa NaN a nekonečno zastavia
AddPageMatrix, ScalePage a RedactRegion teraz odmietajú NaN aj nekonečné argumenty rovno na vstupe a vracajú 0, lebo žiadne PDF číslo ich nedokáže niesť. ScalePage už odmietala faktory nula a menej, ale NaN prejde testom <= 0, takže NaN mierka sa kedysi dostala až k formatteru. Vo v3.539.26 volalo toto formátovanie ešte Round na NaN, čo na Win32 hodí EInvalidOp, keďže x87 jednotka nepotlačuje neplatné operácie; v3.539.31 prinútilo PLDoubleToStrConst zapisovať pre NaN 0 ako poslednú obrannú líniu, ale nula v matici je singulárna transformácia, takže skutočnou opravou ostáva kontrola na úrovni API. Dve hranice ostanú zámerne pri mieste. Stavové reťazce metafilu sa zapisujú a čítajú s locale vnútri jedného procesu a nikdy ho neopúšťajú, takže ich nechali na pokoji. A test, ktorý formátuje 1E-5 cez cestu elementov strany, musí obsah prečítať skôr, než sa vrstva prepíše, lebo opätovná emisia operandov v presnosti dokumentu zmení túto hodnotu legitímne na 0
Ak vaša aplikácia putuje k zákazníkom mimo sveta bodkových decimál, najbezpečnejším zvykom je ten, ktorý teraz používa test suite PDFlibPas: prebehnite cesty vyrábajúce PDF raz s FormatSettings.DecimalSeparator nastaveným na čiarku a preskenujte výstup po čiarkach a exponentoch. O druhej polovici toho istého príbehu, ako čísla načítané z existujúceho súboru zachovajú pri ukladaní svoj presný text, pojednáva článok o zachovaní presnosti naparsovaných decimál. Súbory na stiahnutie, úplná API referencia a skúšobné zostavenie sú na produktovej stránke PDFlibPas Delphi PDF library