Műszaki cikk

Locale-független PDF számok Delphihez, vesszős tizedessel

A PDFlibPas, a losLab Delphihez készült PDF Developer Library-ja minden számot, amit content streambe ír, pont tizedeselválasztóval és exponenciális nélkül ír ki, akármit mondanak is a Windows területi beállításai. A v3.539.26 óta az AddPageMatrix, a ScalePage, a DeskewPage, a RedactRegion, a szöveg-path kimenet és az átszínezés a PLDoubleToStrConst-on át formázza az operandusokat, a v3.539.33 óta pedig azok a parserek, amik ezeket a számokat visszaolvassák, a rendszer locale-ja helyett a PLTryStrToFloatInvariant-t használják. Egy német, francia vagy brazil gépen ugyanez a kód most ugyanazokat a bájtokat gyártja, mint egy US gépen, ami az egyetlen viselkedés, amit egy fájlformátum elvisel

Hogyan rongál meg egy vesszős tizedesű locale egy PDF-et hiba nélkül?

Egy vesszős tizedesű locale azért rongál meg csendben egy PDF-et, mert a vessző nem szám karakter a PDF szintaxisban, így a kár érvényes tokenekként olvasható, rossz jelentéssel. A javítás előtt a PLFloatToStr nem volt más, mint egy csupasz FloatToStr hívás, a FloatToStr pedig a FormatSettings.DecimalSeparator-t követi. Vessző elválasztóval az AddPageMatrix(0.5, 0.5, 0, 0) azt írta, hogy 0,5 0 0 0,5 0 0 cm. Az ISO 32000-1 §7.3.3-a számjegyeket, egy pontot és egy kezdő előjelet enged egy számban, és semmi mást, így egy content parser azt a sort 0 számként olvassa, amit egy ismeretlen ,5 token követ, és a cm operátor rossz operandusokkal fejeződik be. Semmi nem dob exceptiont, semmi nem logol. Az oldal egyszerűen elmozdult transzformációs mátrixszal renderel, és egy rossz helyre került rajzolásból visszafejteni egy locale beállítást egy nyomorúságos délután

A második hiba az első mögé rejtőzik. A FloatToStr az ffGeneral formátumot használja, ami akkor vált exponenciális jelölésre, ha a nagyságrend 1E-4 alá esik, így egy pici offset 1E-5 alakban jött ki. Ugyanez a §7.3.3 kimondja, hogy a PDF nem támogatja az exponenciális alakot, ami azt jelenti, hogy még egy US locale-s gép is írhatott érvénytelen operandust elég kicsi értéknél. Ennek a kiadásnak a regressziós tesztjei mindkét hibaalakot rögzítik: vesszőre kapcsolják az elválasztót, meghívják az API-t, és átvizsgálják a keletkező tartalmat minden vesszőt vagy exponentet tartalmazó token után

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 := ',';   // egy de-DE asztal szimulálása
      Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
      // a v3.539.26 és újabb így ír: 0.5 0 0 0.25 0.00001 12.75 cm
      // régebbi buildek így írtak:    0,5 0 0 0,25 1E-5 12,75 cm
      Writeln(Lib.GetPageContentToString);
    finally
      FormatSettings.DecimalSeparator := OldSeparator;
    end;
  finally
    Lib.Free;
  end;
end;
A PDFlibPas AddPageMatrix vesszős tizedesű asztalon a javítás előtt 0,5 0 0 0,25 1E-5 12,75 cm alakot írt, amit egy PDF parser 0 szám plusz ismeretlen tokenekként olvas, a cm rossz operandusokkal marad, és az oldal csendben eltorzul; a PLDoubleToStrConst ezzel szemben érvényes pont tizedeseket ír
A vessző nem szám karakter a PDF szintaxisban, így a kár érvényes tokenekként olvasható, rossz jelentéssel — az ffGeneral-féle 1E-5 exponentek pedig minden locale-n érvénytelenek voltak, nem csak a vesszősökön

Kétféle szám, két helpercsalád

A PDFlibPas-beli javítás szigorú szétválasztás: az embereknek mutatott számok követhetik a locale-t, a gépnek írt számok sosem. A PLFloatToStr és a PLStrToFloat a PDFlibExtra.pas-ban marad a felhasználónak szánt szövegekhez, és a deklarációjuk most olyan kommentet hordoz, ami pontosan ezt mondja. Minden, ami PDF szintaxissá válik, a PLDoubleToStrConst-on megy át, a feladathoz választott fix tizedeshellyel: hat mátrixokhoz, négy koordinátákhoz és TJ korrekciókhoz, három színekhez és FDF téglalapokhoz. A v3.539.26 auditja több hívási helyet érintett, mint az eredeti hibajelentés sejtette:

  • AddPageMatrix, ScalePage és DeskewPage, amik mindegyike cm-t fűz a meglévő oldaltartalom elé
  • Az oldalelem-építők, amik Tm visszaállításokat, TJ léptetéseket és cm transzformációkat bocsátanak ki
  • Glyph elhelyezési mátrixok és körvonalpontok a szöveg-path konverterben
  • A fekete kitöltődoboz, amit a RedactRegion elé fűz, a FDF export /Rect értékei és az átszínezés által írt operandusok

A PLDoubleToStrConst kézzel írt formázó, nem FloatToStrF köré tekert wrapper, és három tulajdonsága számít itt. Mindig pontot ír, és levágja a záró nullákat, így a 0.5 0.5 marad, 0.500000 helyett. Véges bemenetre sosem ír exponentet. És egy nullától különböző, de a kért pontosságnál kisebb érték megtartja a jelentős számjegyeit nullává összecsukás helyett, így a PLDoubleToStrConst(1E-9, 6) azt adja vissza, hogy 0.000000001; csak a nagyjából 5E-16 alatti értékek válnak 0-vá. Ez az utolsó szabály azért létezik, mert egy pici skálafaktor nullára kerekítése érvényes mátrixból szingulárist csinál, ami rosszabb hiba, mint az épp javított

A PDFlibPas kettéválasztja a számformázást: a PLFloatToStr és a PLStrToFloat locale-hoz kötve marad a felhasználónak szánt szövegeknél, a PLDoubleToStrConst és a PLTryStrToFloatInvariant viszont mindent, ami PDF szintaxissá válik, ponttal, exponent nélkül és feladatonként fixen hat, négy vagy három tizedessel formáz
Az invariant formázó szándékosan kézzel írt: levágja a záró nullákat, sosem ír exponentet, és megtartja a pici értékek jelentős számjegyeit, mert egy skálafaktor nullára kerekítése érvényes mátrixot tene szingulárissá
uses
  PDFlibExtra;

procedure CheckNumberHelpers;
var
  V: Double;
begin
  // Gépi kimenet: pont tizedes, exponenciális nélkül, záró nullák levágva
  Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
  Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
  Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235');  // megtart 4 jelentős számjegy
  Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');

  // Gépi bemenet: finom bukás EConvertError helyett
  Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
  Assert(not PLTryStrToFloatInvariant('0,5', V));   // content számok sosem használnak vesszőt
end;

Miért veszélyesebb a parseoló oldal, mint az író oldal?

A parseoló oldal azért veszélyesebb, mert egy locale-hoz kötött parser nem rossz számot gyárt, hanem dob. A PLStrToFloat a StrToFloat-ot hívja, ami EConvertError-t dob, ha a szöveg nem egyezik a rendszer elválasztójával. Vesszős tizedesű rendszeren ez azt jelentette, hogy a RecolorPage abban a pillanatban megszakadt, amikor egy közönséges 0.5 g operátorba botlott, így minden valós oldal megbukott, nem csak az egzotikusak. A RenderPageRegionToFile elutasította a saját, dokumentált clip formátumát, a "10.5,20.5,50.5,40.5"-et, az SVG hossz attribútumok, az SVG export színek, az annotáció csúcspontlisták és az output intent tömörségértékek pedig vagy elutasítódtak, vagy csendben alapértékre cserélődtek. Egy library, ami a fejlesztő gépén tökéletesen működik, és az első müncheni vásárlónál bukik el, pontosan az a kód, ami, mint a véletlenül működő Delphi kódról szóló cikk esetei, csak azért tűnik helyesnek, mert ott tesztelték, ahol tesztelték

A v3.539.33 minden StrToFloat és TryStrToFloat hívást aszerint sorolt be, honnan jön a bemenete. A content stream operandusok, az SVG attribútumok, a painter színstringek és a vesszővel elválasztott clip és csúcspontlisták mind fix pont szintaxisúak, így mostantól a PLTryStrToFloatInvariant-on mennek át, ami levágja a szóközöket, a PLInvariantFormatSettings-szel parseolja, és üres, rosszul formált vagy nem véges bemenetre False-t ad vissza exception dobása helyett. A vesszővel elválasztott lista nem hagy teret kompromisszumra, mert a vessző nem lehet egyszerre listaelválasztó és tizedesjel. Ugyanez a menet egy tömbhatáron túli írást is javított: a RenderPageRegionToFile egy ötödik clip értéket tárolt a négyelemes bufferén túlra. A PDF egyetlen color space-re konvertálásáról szóló útmutatóban tárgyalt átszínezési pipeline szempontjából a gyakorlati eredmény az, hogy a RecolorPage és a RecolorDocument többé nem szakad meg vesszős tizedesű rendszeren. Azok a szabályértékek, amiket egy hívó a CheckDocumentPolicy-be begépel, az egyetlen parseolási eset, ami az elnéző helpert használja, az okért, amit a következő szakasz magyaráz el

Mi történik, ha egy oda-vissza útnak csak az egyik végét javítod meg?

Ha egy locale oda-vissza útnak csak az egyik végét javítod meg, működő kódot törsz el, ezért a v3.539.32-es struktúraattribútum-változás az írót és az olvasót együtt mozgatta. A SetStructElem* wrapperek számokat hordoznak stringként: a SetStructElemBBox négy értéket formáz egyetlen stringgé, a AddTagAttribute-on át tárolja, és a /A író később parseolja azt a stringet, hogy eldöntse, számmá, tömbbé vagy névvé válik-e. Mindkét vég a rendszer locale-ját használta, így vesszős tizedesű rendszeren az oda-vissza út önmagával konzisztens volt. A hiba csak akkor bukkant fel, ha egy hívó a dokumentáció szerint járt el, és "0.5"-öt adott a AddTagAttribute-nak: az olvasó nem tudta parseolni, és a /0.5 PDF nevet bocsátotta ki. A PDF/VCR placeholdernek fordított problémája volt, mert a library a GTS_BBox-ot ponttal generálta, majd mentés előtt a locale-ossal validálta

Csak az írót pont formára állítani rosszabb lett volna, mint nem csinálni semmit, mert minden SetStructElem* érték megbukott volna a locale-hoz kötött olvasón, és névvé silányult volna. Ezért az írók mostantól a PLDoubleToStrConst(v, 6)-ot használják, az olvasó pedig az új PLTryStrToFloatLenient-et, ami előbb a pont formát próbálja, majd a rendszer locale-jára esik vissza. Az a vesszős locale-s hívó, aki korábban "1,25"-öt adott át, továbbra is az 1.25 számot kapja. A kompromisszum szándékos és dokumentált: német rendszeren a "1.500" korábban névvé vált, mert a StrToFloat elutasítja az ezres elválasztókat, mostantól pedig 1.5-ként olvasódik, miközben a literális NAN és INF stringeket többé nem fogadja el számként

A PDFlibPas SetStructElemBBox és testvérei a számokat stringként hordozzák át a AddTagAttribute-on, és a /A író ezeket a stringeket parseolja vissza, ezért a v3.539.32 mindkét véget együtt mozgatta: a PLDoubleToStrConst ponttal ír, a PLTryStrToFloatLenient pedig előbb ponttal olvas, locale eséssel, így egy vesszős locale-s 1,25 továbbra is 1.25-ként olvasódik
Csak az író javítása minden struktúraelem-attribútumot PDF névvé silányított volna, ezért egy oda-vissza út vagy mindkét végét együtt mozgatja, vagy egyiket sem
uses
  System.SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
begin
  FormatSettings.DecimalSeparator := ',';   // vesszős tizedesű hívó
  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, korábban /0.5
    Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // továbbra is /StartIndent 1.25
    Lib.DrawText(20, 20, 'figure');
    Lib.EndTag;
    Lib.SaveToFile('tagged.pdf');
  finally
    Lib.Free;
  end;
end;

Hol állítódik meg a NaN és a végtelen

Az AddPageMatrix, a ScalePage és a RedactRegion mostantól előrebukik a NaN és a végtelen argumentumokon, és 0-t ad vissza, mert őket egyetlen PDF szám sem tudja ábrázolni. A ScalePage már korábban is elutasította a nullás vagy annál kisebb faktorokat, de a NaN átment egy <= 0 teszten, így egy NaN skála egészen a formázóig eljutott. A v3.539.26-ban az a formázó még a Round-ot hívta NaN-on, ami Win32-n EInvalidOp-ot dob, ahol az x87 unit nem maszkolja az érvénytelen műveleteket; a v3.539.31 a PLDoubleToStrConst-ot NaN esetén 0 írására állította utolsó védvonalként, de a nulla egy mátrixban szinguláris transzformáció, így az API-szintű ellenőrzés marad a valódi javítás. Két határ szándékosan a helyén marad. A metafile állapotstringek egyetlen folyamaton belül a locale-ossal íródnak és olvasódnak, és sosem hagyják el azt, így békén hagyták őket. És egy teszt, ami 1E-5-öt formáz az oldalelem útvonalon, a réteg átírása előtt kell olvassa a tartalmat, mert az operandusok dokumentum-pontossággal való újrakibocsátása jogszerűen 0-vá teszi azt az értéket

Ha az alkalmazásod a pont tizedesű világon kívüli vásárlókhoz is eljut, a legbiztonságosabb szokás az, amit mostantól a PDFlibPas tesztcsomagja használ: futtasd le a PDF-et gyártó útvonalakat egyszer FormatSettings.DecimalSeparator vesszőre állítva, és szeld át a kimenetet vesszők és exponentek után. A parseolt tizedes pontosság megőrzéséről szóló cikk ugyanennek a történetnek a másik felét tárgyalja, hogyan maradnak meg a meglévő fájlból olvasott számok pontos szövegükkel mentéskor. A letöltések, a teljes API referencia és a trial build a PDFlibPas Delphi PDF library termékoldalon érhetők el