PDFlibPas, losLab PDF Developer Library for Delphi, zapisuje každé číslo, které dává do content streamu, s tečkou jako desetinným oddělovačem a bez exponentu, ať říkají Windows regional settings cokoliv. Od v3.539.26 formátují operandy přes PLDoubleToStrConst AddPageMatrix, ScalePage, DeskewPage, RedactRegion, výstup text-to-path i přebarvování, a od v3.539.33 parsery, která ta čísla čtou zpět, používají PLTryStrToFloatInvariant místo systémového locale. Na německém, francouzském či brazilském stroji vyleze ze stejného kódu tentýž bajt za bajtem jako na americkém, a jinak to formát souborů nesnese
Proč kazí čárkové locale PDF bez jediné chyby?
Čárkové locale kazí PDF potichu, protože čárka není v syntaxi PDF číslicový znak, takže škoda vypadá jako validní tokeny se špatným významem. Před opravou nebylo PLFloatToStr nic víc než nahé volání FloatToStr a FloatToStr se řídí FormatSettings.DecimalSeparator. S čárkou jako oddělovačem zapsalo AddPageMatrix(0.5, 0.5, 0, 0) 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 v čísle dovoluje číslice, jednu tečku a úvodní znaménko a nic víc, takže content parser přečte tenhle řádek jako číslo 0 následované neznámým tokenem ,5 a operátor cm skončí se špatnými operandy. Nic nezvedne výjimku, nic nezaloguje. Stránka se prostě vykreslí s transformační maticí, která někde ujela, a dohledávat z přeházené kresby locale nastavení je nechtěné odpoledne
Druhá vada se schovává za tou první. FloatToStr používá formát ffGeneral, který přepne na exponentový zápis, jakmile velikost spadne pod 1E-4, takže drobný offset vyšel jako 1E-5. Týž §7.3.3 říká, že PDF exponentový tvar nepodporuje, což znamená, že i stroj s US locale mohl zapsat nevalidní operand při dost malé hodnotě. Regresní testy tohohle vydání přibíjí oba tvary selhání: přepnou oddělovač na čárku, zavolají API a proskenují výsledný obsah na jakýkoli token s čárkou nebo exponentem
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 := ','; // simulace plochy de-DE
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 a novější zapisují: 0.5 0 0 0.25 0.00001 12.75 cm
// starší buildy psaly: 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, dvě rodiny helperů
Oprava v PDFlibPas je přísný rozkol: čísla určená lidem můžou za locale jít, čísla zapsaná stroji nikdy. PLFloatToStr a PLStrToFloat zůstávají v PDFlibExtra.pas pro texty mířené na uživatele a jejich deklarace teď nese komentář, který to říká napřímo. Všechno, co skončí jako PDF syntaxe, jde přes PLDoubleToStrConst s pevným počtem desetinných míst zvoleným pro daný úkol: šest pro matice, čtyři pro souřadnice a korekce TJ, tři pro barvy a FDF obdélníky. Audit pro v3.539.26 sáhl do víc míst, než naznačoval původní bug report:
AddPageMatrix,ScalePageaDeskewPage, které všechny předřadícmpřed stávající obsah stránky- Stavitelé prvků stránky, kteří vysílají resety
Tm, posunyTJa transformacecm - Matice umístění glyfů a obrysové body v konvertoru text-to-path
- Černá výplňová krabice, kterou přidává
RedactRegion, hodnoty/Rectv FDF exportu a operandy, které zapisuje přebarvování
PLDoubleToStrConst je ručně psaný formátovač, ne obal kolem FloatToStrF, a tři jeho vlastnosti jsou tu podstatné. Pořád zapisuje tečku a odstraňuje koncové nuly, takže 0.5 zůstane 0.5 a ne 0.500000. Pro konečný vstup nikdy nezapisuje exponent. A nenulová hodnota menší než požadovaná přesnost si podrží své významné číslice místo zhroucení na nulu, takže PLDoubleToStrConst(1E-9, 6) vrátí 0.000000001; na 0 se smrsknou jen hodnoty pod zhruba 5E-16. Tohle poslední pravidlo existuje proto, že zaokrouhlení drobného škálovacího faktoru na nulu promění validní matici v singulární, což je horší bug než ten, který se právě opravuje
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Strojový výstup: desetinná tečka, žádný exponent, koncové nuly odstraněny
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // drží 4 významné číslice
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Strojový vstup: měkké selhání místo EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // čísla v obsahu čárku nikdy nepoužívají
end;
Proč je parse strana nebezpečnější než zapisovací?
Parse strana je nebezpečnější, protože parser vázaný na locale nevyprodukuje špatné číslo — rovnou vyhodí výjimku. PLStrToFloat volá StrToFloat, které na neshodě se systémovým oddělovačem zvedne EConvertError. Na systému s čárkou to znamenalo, že RecolorPage skončil v okamžiku, kdy potkal obyčejný operátor 0.5 g, takže selhávala každá reálná stránka, ne jen exotické. RenderPageRegionToFile odmítal vlastní zdokumentovaný clip formát "10.5,20.5,50.5,40.5" a délkové atributy SVG, barvy SVG exportu, seznamy vrcholů anotací a hodnoty solidity output intentu byly buď odmítnuté, nebo potichu nahrazené defaulty. Knihovna, která funguje dokonalé na vývojářském stroji a padá na prvním zákazníkovi v Mnichově, je přesně ten druh kódu, který — jako případy z článku o Delphi kódu fungujícím náhodou — vypadá správně jen proto, kde se testoval
v3.539.33 roztřídila každé volání StrToFloat a TryStrToFloat podle toho, odkud vstup pochází. Operandy content streamů, atributy SVG, barvové řetězce painteru a čárkou oddělené clip a vertex seznamy mají všichni pevnou tečkovou syntaxi, takže nyní jdou přes PLTryStrToFloatInvariant, které text ořezá, parsuje ho přes PLInvariantFormatSettings a u prázdného, deformovaného nebo nekonečného vstupu vrátí False místo výjimky. Čárkou oddělený seznam nedává kompromisu žádný prostor, protože čárka nemůže být zároveň delimiterem seznamu i desetinným oddělovačem. Týž průchod opravil i zápis mimo hranice bufferu: RenderPageRegionToFile ukládal pátou clip hodnotu za svůj čtyřprvkový buffer. Pro přebarvovací pipeline popsanou v průvodci konverzí PDF do jedného barevného prostoru je praktickým výsledkem, že RecolorPage a RecolorDocument už na čárkovém systému nepřerušují. Hodnoty pravidel, které volající vypíše do CheckDocumentPolicy, jsou jediným parse případem, který místo toho používá shovívavý helper, a důvod vysvětluje další sekce
Co se stane, když opravíte jen jeden konec round tripu?
Oprava jen jednoho konce locale round tripu rozbije kód, který fungoval, a proto změna strukturálních atributů ve v3.539.32 posunula writer a reader dohromady. Wrappery SetStructElem* přenášejí čísla jako řetězce: SetStructElemBBox naformátuje čtyři hodnoty do jednoho řetězce, uloží ho přes AddTagAttribute a writer /A tenhle řetězec později parsuje, aby rozhodl, zda se stane číslem, polem nebo názvem. Oba konce používaly systémové locale, takže round trip byl na čárkovém systému vnitřně konzistentní. Bug se ukázal, jen když volající podle dokumentace předal do AddTagAttribute "0.5": reader ho neparsnul a vydal PDF název /0.5. Placeholder PDF/VCR měl zrcadlově obrácený problém, protože knihovna vygenerovala GTS_BBox s tečkou a pak ho před uložením validovala přes locale
Přepnout jen writer na tečku by bylo horší než nedělat nic, protože každá hodnota SetStructElem* by pak propadla readerem vázaným na locale a degradovala na název. Takže writery teď používají PLDoubleToStrConst(v, 6) a reader nové PLTryStrToFloatLenient, které zkusí nejdřív tečkový tvar a sáhne po systémovém locale jako záloze. Volající s čárkovým locale, který dřív poslal "1,25", dostane i nadále číslo 1.25. Kompromis je záměrný a zdokumentovaný: na německém systému se "1.500" dřív stávalo názvem, protože StrToFloat odmítá oddělovače tisíců, a teď se přečte jako 1.5, zatímco doslovné řetězce NAN a INF už se jako čísla nepřijímají
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // volající s čárkou jako desetinným oddělovačem
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, dřív /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // pořád /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Kde se NaN a nekonečno zastaví
AddPageMatrix, ScalePage a RedactRegion teď NaN a nekonečné argumenty odmítnou hned na vstupu a vrátí 0, protože žádné PDF číslo je neumí reprezentovat. ScalePage už dřív odmítala faktory nula a méně, ale NaN testem <= 0 projde, takže NaN měřítko kdysi procestovalo až do formátovače. Ve v3.539.26 volal tenhle formátovač na NaN stále Round, které zvedá EInvalidOp na Win32, kde x87 jednotka neinvalidní operace nemaskuje; v3.539.31 učinil zápis 0 pro NaN v PLDoubleToStrConst poslední záložní linií, ale nula v matici je singulární transformace, takže kontrola na úrovni API je pořád ta skutečná oprava. Dvě hranice zůstávají záměrně na místě. Stavové řetězce metafile se zapisují i čtou s locale uvnitř jednoho procesu a nikdy z něj nevyjdou, takže byly ponechány. A test, který formátuje 1E-5 přes cestu prvků stránky, musí obsah přečíst dřív, než se vrstva přepíše, protože znovuvysílání operandů v přesnosti dokumentu legitimně promění tuhle hodnotu v 0
Pokud vaše aplikace putuje k zákazníkům mimo tečkový svět, nejbezpečnější návyk je ten, který teď používá test suite PDFlibPas: projděte cesty tvořící PDF jednou s FormatSettings.DecimalSeparator nastaveným na čárku a proskenujte výstup na čárky a exponenty. Článek o zachování parsované desetinné přesnosti pokrývá druhou polovinu téhož příběhu — jak čísla přečtená z existujícího souboru drží svůj přesný text při uložení. Ke stažení, kompletní API reference i trial build najdete na stránce produktu PDFlibPas Delphi PDF library