PDFlibPas, losLab PDF Developer Library for Delphi, kiekvieną skaičių, patenkantį į turinio srautą, rašo su tašku kaip dešimtainiu skirtuku ir be laipsnio, ką benagtų Windows regioniniai nustatymai. Nuo v3.539.26 AddPageMatrix, ScalePage, DeskewPage, RedactRegion, tekstas-į-kelią išvestis ir perdažymas operandus formatuoja per PLDoubleToStrConst, o nuo v3.539.33 analizatoriai, skaitantys tuos skaičius atgal, naudoja PLTryStrToFloatInvariant vietoj sistemos lokalės. Vokietijos, Prancūzijos ar Brazilijos mašinoje tas pats kodas dabar duoda tuos pačius baitus kaip ir JAV mašinoje – vienintelis elgesys, kurį failo formatas gali pakęsti
Kodėl kablelio lokalė sugadina PDF be jokios klaidos?
Kablelio lokalė PDF sugadina tyliai, nes kablelis PDF sintaksėje nėra skaičiaus simbolis, tad žala atrodo kaip teisėti žetonai su netinkama reikšme. Iki pataisos PLFloatToStr buvo niekuo daugiau nei plikas FloatToStr iškvietimas, o FloatToStr seka FormatSettings.DecimalSeparator. Su kableliu kaip skirtuku AddPageMatrix(0.5, 0.5, 0, 0) rašė 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 leidžia skaičiuje skaitmenis, vieną tašką ir priekinį ženklą ir nieko kito, tad turinio analizatorius tą eilutę skaito kaip skaičių 0, po kurio seka neatpažintas žetonas ,5, ir cm operatorius lieka su netinkamais operandais. Niekas nesukelia klaidos, niekas neįrašoma į žurnalą. Puslapis tiesiog atvaizduojamas su nuslinkusia transformavimo matrica, o atsekimas nuo pamesto piešinio iki lokalės nustatymo yra pražuvusi popietė
Antras defektas slepiasi už pirmojo. FloatToStr naudoja ffGeneral formatą, kuris pereina prie laipsnio užrašo, kai reikšmės dydis nukrenta žemiau 1E-4, tad mažas poslinkis išeidavo kaip 1E-5. Tas pats §7.3.3 teigia, kad PDF laipsnio formos nepalaiko, vadinasi net JAV lokalės mašina galėjo parašyti netinkamą operandą, gavusi pakankamai mažą reikšmę. Šios laidos regresijos testai užkalė abu gedimo pavidalus: jie skirtuką pasuka į kablelį, kviečia API ir nuskaitytame turinyje ieško bet kokio žetono su kableliu arba laipsniu
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 := ','; // imituojame de-DE darbalaukį
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 ir naujesnės rašo: 0.5 0 0 0.25 0.00001 12.75 cm
// senesnės laidos rašė: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
Dvi rūšys skaičių, dvi pagalbinių funkcijų šeimos
Pataisa PDFlibPas yra griežtas suskirstymas: skaičiai, rodomi žmonėms, gali sekti lokalę, o skaičiai, rašomi mašinai, niekada to nedaro. PLFloatToStr ir PLStrToFloat lieka faile PDFlibExtra.pas vartotojui skiriamam tekstui, ir jų deklaracija dabar neša komentarą, tai pasakantį aiškiai. Viskas, kas virsta PDF sintakse, eina pro PLDoubleToStrConst su fiksuotu dešimtainių vietų skaičiumi, parinktu pagal darbą: šešios matricoms, keturios koordinatėms ir TJ poslinkiams, trys spalvoms ir FDF stačiakampiams. v3.539.26 auditas palietė daugiau kvietimo vietų, nei siūlė originali klaidos ataskaita:
AddPageMatrix,ScalePageirDeskewPage, kurie visi prieš esamą puslapio turinį prikišacm- Puslapio elementų kūrėjai, išduodantys
Tmatstatymus,TJposlinkius ircmtransformacijas - Glifų išdėstymo matricos ir kontūro taškai tekstas-į-kelią keitiklyje
- Juodas užpildymo stačiakampis, kurį prikiša
RedactRegion,/Rectreikšmės FDF eksporte ir operandai, kuriuos rašo perdažymas
PLDoubleToStrConst yra rankomis rašytas formatuotojas, o ne apvalkalas aplink FloatToStrF, ir trys jo savybės čia svarbios. Jis visuomet rašo tašką ir nukerpa galinius nulius, tad 0.5 lieka 0.5, o ne 0.500000. Jis niekada nerašo laipsnio baigtiniam įvedimui. Ir nenulinė reikšmė, mažesnė už prašomąjį tikslumą, išlaiko savo reikšmingus skaitmenis vietoj susmaigstymo iki nulio, tad PLDoubleToStrConst(1E-9, 6) grąžina 0.000000001; tik reikšmės žemiau maždaug 5E-16 tampa 0. Ta paskutinė taisyklė gyvuoja todėl, kad mažo mastelio koeficiento suapvalinimas iki nulio teisėtą matricą paverčia vienarange – tai blogesnė klaida nei ta, kuri taisoma
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Mašininė išvestis: taško dešimtainis, be laipsnio, galiniai nuliai nukirpti
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // išlaiko 4 reikšmingus skaitmenis
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Mašininis įvedimas: švelni nesėkmė vietoj EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // turinio skaičiai kablelio niekada nevartoja
end;
Kodėl analizavimo pusė pavojingesnė už rašymo pusę?
Analizavimo pusė pavojingesnė, nes lokalę sejantis analizatorius negamina netinkamo skaičiaus – jis meta išimtį. PLStrToFloat kviečia StrToFloat, kuri kelia EConvertError, kai tekstas nesutampa su sistemos skirtuku. Kablelio lokalės sistemoje tai reiškė, kad RecolorPage nutrūkdavo akimirką, kai sutikdavo paprastą 0.5 g operatorių, tad krisdavo kiekvienas realus puslapis, ne tik egzotiški. RenderPageRegionToFile atmesdavo savo paties dokumentuotą clip formatą "10.5,20.5,50.5,40.5", o SVG ilgio atributai, SVG eksporto spalvos, anotacijų viršūnių sąrašai ir output intent dangos reikšmės būdavo arba atmetamos, arba tyliai keičiamos numatytosiomis. Biblioteka, kuri puikiai dirba kūrėjo mašinoje ir krenta ant pirmojo kliento Miunchene, yra būtent tos rūšies kodas, kuris, kaip straipsnio apie atsitiktinai veikiantį Delphi kodą atvejai, atrodo teisingas tik dėl to, kur jis buvo testuotas
v3.539.33 kiekvieną StrToFloat ir TryStrToFloat iškvietimą klasifikavo pagal tai, iš kur atkeliauja jo įvedimas. Turinio srautų operandai, SVG atributai, tapytojo spalvų eilutės ir kableliu skiriami clip bei viršūnių sąrašai visi turi fiksuotą taško sintaksę, tad dabar eina pro PLTryStrToFloatInvariant, kuri apkerpa tekstą, analizuoja jį su PLInvariantFormatSettings ir grąžina False tuščiam, blogai suformuotam ar nebegtinam įvedimui vietoj klaidos kelio. Kableliu skiriamas sąrašas nepalieka vietos kompromisui, nes kablelis negali būti ir sąrašo skirtukas, ir dešimtainis ženklas. Tas pats praėjimas sutvarkė ir rašymą už ribų: RenderPageRegionToFile anksčiau saugodavo penktą clip reikšmę už savo keturių elementų buferio ribų. Perdažymo pipeline, aprašytam gide apie PDF konvertavimą į vieną spalvų erdvę, praktinis rezultatas toks: RecolorPage ir RecolorDocument kablelio lokalės sistemoje daugiau nenutrūksta. Taisyklių reikšmės, kurias kvietėjas pats surašo į CheckDocumentPolicy, yra vienintelis analizavimo atvejis, naudojantis atlaisvintąją pagalbinę funkciją – dėl priežasties, kurią paaiškins kita sekcija
Kas nutinka, jei sutvarkai tik vieną round-trip galą?
Sutvarkius tik vieną lokalės round-trip galą sulaužai anksčiau veikusį kodą, todėl struktūros atributų pakeitimas v3.539.32 perkėlė rašytoją ir skaitytuvą kartu. SetStructElem* apvalkalai skaičius perduoda kaip eilutes: SetStructElemBBox suformatuoja keturias reikšmes į vieną eilutę, saugoja ją per AddTagAttribute, o /A rašytojas vėliau tą eilutę išanalysuoja, spręsdamas, ar ji tampa skaičiumi, masyvu ar vardu. Abu galai naudojo sistemos lokalę, tad kablelio lokalės sistemoje round-trip buvo pats sau nuoseklus. Klaida pasirodė tik tada, kai kvietėjas pasekė dokumentaciją ir perdavė "0.5" į AddTagAttribute: skaitytuvas jos nesugebėjo išanalysuoti ir išdavė PDF vardą /0.5. PDF/VCR vietos laikiklis turėjo veidrodinę problemą, nes biblioteka GTS_BBox generavo su tašku, o prieš išsaugodama ją patikrindavo su lokale
Pakeitus vien rašytoją į tašką būtų buvę blogiau nei nedaryti nieko, nes kiekviena SetStructElem* reikšmė tada pralaimėtų lokalę sejančiam skaitytuvui ir sunyktų į vardą. Todėl rašytojai dabar naudoja PLDoubleToStrConst(v, 6), o skaitytuvas – naująją PLTryStrToFloatLenient, kuri pirmiausiai bando taško formą, o nesėkmės atveju grįžta prie sistemos lokalės. Kablelio lokalės kvietėjas, anksčiau perdavęs "1,25", vis tiek gauna skaičių 1.25. Kompromisas sąmoningas ir dokumentuotas: vokiškoje sistemoje "1.500" anksčiau virsdavo vardu, nes StrToFloat atmeta tūkstančių skirtukus, o dabar jis skaitomas kaip 1.5, o literalūs NAN ir INF tekstai kaip skaičiai nebepriimami
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // kablelio dešimtainis kvietėjas
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, anksčiau buvo /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // vis tiek /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Kur NaN ir begalybės sutinkamos prie vartų
AddPageMatrix, ScalePage ir RedactRegion dabar NaN ir begalybinius argumentus atmeta prie pat durų ir grąžina 0, nes jokio PDF skaičiaus jų išreikšti negalima. ScalePage jau atmesdavo nulinius ar neigiamus koeficientus, bet NaN pralenda pro <= 0 testą, tad NaN mastelis anksčiau nukeliaudavo iki pat formatuotojo. v3.539.26 tas formatuotojas NaN atveju vis dar kviestų Round, kuri Win32 aplinkoje, kur x87 blokas nenumeta netinkamų operacijų, kelia EInvalidOp; v3.539.31 PLDoubleToStrConst NaN atveju pradėjo rašyti 0 kaip paskutinę gynybos liniją, bet nulis matricoje yra vienarangė transformacija, todėl API lygio patikra lieka tikrąja pataisa. Dvi ribos paliktos vietoje sąmoningai. Metafailo būsenos eilutės rašomos ir skaitomos su lokale vieno proceso viduje ir niekada jo nepalieka, todėl jos paliktos ramybėje. Ir testas, kuris formatuoja 1E-5 pro puslapio elementų kelią, turi perskaityti turinį prieš sluoksnį parašant iš naujo, nes operandų pakartotinis išdavimas dokumento tikslumu teisėtai tą reikšmę paverčia 0
Jei jūsų taikomoji programa keliauja pas klientus už taško dešimtainio pasaulio ribų, saugiausias įprotis tas, kurį dabar naudoja PDFlibPas testų rinkinys: PDF gaminančius kelius vieną kartą paleisti su FormatSettings.DecimalSeparator, nustatytu į kablelį, ir išvestyje ieškoti kablelių bei laipsnių. Straipsnis apie išanalysuoto dešimtainio tikslumo išsaugojimą dengia kitą tos pačios istorijos pusę – kaip skaičiai, perskaityti iš egzistuojančio failo, išsaugojime išlaiko savo tikrąjį tekstą. Parsisiuntimai, pilna API nuoroda ir bandomasis variantas laukia PDFlibPas Delphi PDF bibliotekos produkto puslapyje