Techninis straipsnis

Lokalės neutralūs PDF skaičiai kablelinėms Delphi programoms

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;
PDFlibPas AddPageMatrix kablelio lokalės darbalaukyje iki pataisos rašė 0,5 0 0 0,25 1E-5 12,75 cm, ką PDF analizatorius skaito kaip skaičių 0 su neatpažintais žetonais, palikdamas cm su netinkamais operandais ir tyliai transformuotą puslapį, o PLDoubleToStrConst rašo teisėtus taško dešimtainius
Kablelis PDF sintaksėje nėra skaičiaus simbolis, tad žala atrodo kaip teisėti žetonai su netinkama reikšme – o ffGeneral laipsniai, tokie kaip 1E-5, buvo netinkami bet kurioje lokalėje, ne tik kablelio

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, ScalePage ir DeskewPage, kurie visi prieš esamą puslapio turinį prikiša cm
  • Puslapio elementų kūrėjai, išduodantys Tm atstatymus, TJ poslinkius ir cm transformacijas
  • Glifų išdėstymo matricos ir kontūro taškai tekstas-į-kelią keitiklyje
  • Juodas užpildymo stačiakampis, kurį prikiša RedactRegion, /Rect reikš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

PDFlibPas skaičių formatavimą padalija perpus: PLFloatToStr ir PLStrToFloat lieka susieti su lokale vartotojui skiriamam tekstui, o PLDoubleToStrConst ir PLTryStrToFloatInvariant formatuoja viską, kas virsta PDF sintakse, su tašku, be laipsnio ir fiksuotu darbui skirtu tikslumu – šešiais, keturiais ar trimis dešimtainiais
Invariantinis formatuotojas rankomis rašytas sąmoningai: jis nukerpa galinius nulius, niekada nerašo laipsnio ir išlaiko mažų reikšmių reikšmingus skaitmenis, nes mastelio koeficiento suapvalinimas iki nulio teisėtą matricą paverstų vienarange
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

PDFlibPas SetStructElemBBox ir jo bendrai skaičius perduoda kaip eilutes per AddTagAttribute, o /A rašytojas tas eilutes skaito atgal, tad v3.539.32 abu galus perkėlė kartu: PLDoubleToStrConst rašo su tašku, o PLTryStrToFloatLenient skaito pirmiausiai tašką su lokalės atsargine forma, tad kablelio lokalės 1,25 vis tiek skaitosi kaip 1.25
Sutvarkius vien rašytoją kiekviena struktūrinio elemento atributo reikšmė sunyktų į PDF vardą, todėl round-trip perkelia abu galus kartu arba visai neperkelia
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