Tehnični članak

Lokalno neodvisna števila PDF za Delphi z decimalno vejico

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;
PDFlibPas AddPageMatrix na namizju z decimalno vejico je zapisal 0,5 0 0 0,25 1E-5 12,75 cm pred popravkom, kar razčlenjevalnik PDF bere kot število 0 plus neznane žetone, cm ostane z napačnimi operandi in stran se tiho transformira, PLDoubleToStrConst pa zapiše veljavne decimalne vrednosti s piko
Vejica v skladnji PDF ni znak števila, zato se škoda bere kot veljavni žetoni z napačnim pomenom — eksponenti ffGeneral, kot je 1E-5, pa so bili neveljavni na vsakem locale, ne le na tistih z vejico

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, ScalePage in DeskewPage, ki vsi pripnejo cm pred obstoječo vsebino strani
  • Graditelji elementov strani, ki izdajo ponastavitve Tm, pomike TJ in transformacije cm
  • Matrike postavitve glifov in točke orisa v pretvorniku text-to-path
  • Črno polnilno okence, ki ga pripne RedactRegion, vrednosti /Rect v 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

PDFlibPas razdeli oblikovanje števil na dvoje: PLFloatToStr in PLStrToFloat ostajata vezana na locale za besedilo za uporabnika, PLDoubleToStrConst in PLTryStrToFloatInvariant pa oblikujeta vse, kar postane skladnja PDF, s piko, brez eksponenta in s fiksno natančnostjo na posel, šestimi, štirimi ali tremi decimalkami
Invariantna funkcija oblikovanja je namerno napisana ročno: odstrani ničle na koncu, nikoli ne zapiše eksponenta in obdrži signifikantne števke drobnih vrednosti, ker bi zaokrožitev merilnega faktorja na nič spremenila veljavno matriko v singularno
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

PDFlibPas SetStructElemBBox in njegovi sorodniki posredujejo števila kot nize skozi AddTagAttribute, pisalec /A pa te nize razčleni nazaj, zato je v3.539.32 premaknil oba konca skupaj: PLDoubleToStrConst zapiše s piko in PLTryStrToFloatLenient bere najprej piko z locale rezervo, tako da zapuščinska 1,25 z vejico še vedno prebere kot 1.25
Popravec samo pisalca bi vsak atribut strukturnega elementa degradiral v PDF ime, zato povratna pot premakne oba konca skupaj ali pa sploh ne
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