Teknik Makale

Virgüllü ondalık Delphi için locale bağımsız PDF sayıları

PDFlibPas, losLab PDF Developer Library for Delphi, content streamine koyduğu her sayıyı, Windows bölgesel ayarları ne derse desin, nokta ondalık ayracıyla ve üs olmadan yazar. v3.539.26dan beri AddPageMatrix, ScalePage, DeskewPage, RedactRegion, text-to-path çıktısı ve yeniden renklendirme operandları PLDoubleToStrConst üzerinden biçimlenir; v3.539.33den beri bu sayıları geri okuyan ayrıştırıcılar sistem localei yerine PLTryStrToFloatInvariant kullanır. Alman, Fransız ya da Brezilyalı bir makinede aynı kod artık ABD makinesiyle aynı baytları üretir; bir dosya formatının tolere edebileceği tek davranış da budur

Virgüllü ondalık bir locale bir PDFi hatasız mı bozar?

Virgüllü ondalık bir locale bir PDFi sessizce bozar, çünkü virgül PDF sözdiziminde sayı karakteri değildir; hasar, anlamı kaymış geçerli tokenler olarak okunur. Düzeltmeden önce PLFloatToStr çıplak bir FloatToStr çağrısından ibaretti ve FloatToStr FormatSettings.DecimalSeparatorı izler. Virgül ayracıyla AddPageMatrix(0.5, 0.5, 0, 0), 0,5 0 0 0,5 0 0 cm yazıyordu. ISO 32000-1 §7.3.3 bir sayıda rakamlara, tek bir noktaya ve baştaki işarete izin verir, başkasına değil; dolayısıyla content parser o satırı 0 sayısı ve ardından gelen bilinmeyen bir ,5 tokeni olarak okur ve cm operatörü yanlış operandlarla kalır. Hiçbir şey fırlatmaz, hiçbir şey loglamaz. Sayfa, kaymış bir transformation matrix ile render edilir ve kaymış bir çizimden geriye doğru bir locale ayarına inmek, üzerinden çıkılması zor bir öğleden sonradır

İkinci kusur birincinin arkasına saklanır. FloatToStr, büyüklük 1E-4ün altına düştüğünde üs gösterimine geçen ffGeneral formatını kullanır; yani minicik bir offset 1E-5 olarak çıkıyordu. Aynı §7.3.3, PDFin üs formunu desteklemediğini söyler; bu da yeterince küçük bir değer verildiğinde ABD localeli bir makinenin bile geçersiz operand yazabileceği demektir. Bu sürümün regresyon testleri iki başarısızlık biçimini de çiviler: ayracı virgüle çevirir, APIyi çağırır ve oluşan contentte virgül ya da üs taşıyan herhangi bir tokeni tarar

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 := ',';   // Alman (de-DE) masaüstünü taklit et
      Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
      // v3.539.26 ve sonrası yazar: 0.5 0 0 0.25 0.00001 12.75 cm
      // eski derlemeler yazıyordu:  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, düzeltmeden önce virgüllü ondalık bir masaüstünde 0,5 0 0 0,25 1E-5 12,75 cm yazıyordu; PDF parserı bunu 0 sayısı artı bilinmeyen tokenler olarak okur, cm yanlış operandlarla kalır ve sayfa sessizce dönüşür; PLDoubleToStrConst ise geçerli noktalı ondalıklar yazar
Virgül PDF sözdiziminde sayı karakteri değildir; hasar, anlamı kaymış geçerli tokenler olarak okunur — 1E-5 gibi ffGeneral üsleri her localede geçersizdi, yalnızca virgüllülerde değil

İki tür sayı, iki yardımcı ailesi

PDFlibPasteki düzeltme katı bir ayrımdır: insanlara gösterilen sayılar localei izleyebilir, makine için yazılan sayılar asla izlemez. PLFloatToStr ile PLStrToFloat, kullanıcıya dönük metin için PDFlibExtra.pasta kalır ve bildirimleri artık tam olarak bunu söyleyen bir yorum taşır. PDF sözdizimine dönüşen her şey PLDoubleToStrConsttan geçer; işe göre seçilmiş sabit ondalık basamakla: matrixler için altı, koordinatlar ve TJ ayarları için dört, renkler ve FDF dikdörtgenleri için üç. v3.539.26 denetimi, özgün hata raporunun ima ettiğinden daha çok çağrı noktasına dokundu:

  • AddPageMatrix, ScalePage ve DeskewPage; üçü de mevcut sayfa içeriğinin başına bir cm ekler
  • Tm sıfırlamaları, TJ ilerlemeleri ve cm dönüşümleri yayan sayfa element kurucuları
  • Text-to-path dönüştürücüsünde glyph yerleşim matrixleri ve outline noktaları
  • RedactRegionun başına eklediği siyah doldurma kutusu, FDF exportundaki /Rect değerleri ve yeniden renklendirmenin yazdığı operandlar

PLDoubleToStrConst FloatToStrF sarmalayıcısı değil, elle yazılmış bir biçimlendiricidir ve üç özelliği burada önemlidir. Her zaman nokta yazar ve sondaki sıfırları kırpır; 0.5, 0.500000 yerine 0.5 olarak kalır. Sonlu girdi için asla üs yazmaz. Ve istenen hassasiyetten küçük sıfır olmayan bir değer, sıfıra çökmek yerine anlamlı basamaklarını korur; PLDoubleToStrConst(1E-9, 6) çağrısı 0.000000001 döndürür, kabaca 5E-16 altındaki değerler ancak 0 olur. O son kural, minicik bir ölçek faktörünü sıfıra yuvarlamanın geçerli bir matrixi tekil hâle getirmesindendir; bu, düzeltilen hatadan daha kötü bir kusur olur

PDFlibPas sayı biçimlendirmeyi ikiye ayırır: PLFloatToStr ile PLStrToFloat kullanıcıya dönük metin için localee bağlı kalır; PLDoubleToStrConst ile PLTryStrToFloatInvariant ise PDF sözdizimine dönüşen her şeyi nokta ile, üssüz ve işe göre sabit altı, dört ya da üç ondalıkla biçimler
Invariant biçimlendirici bilerek elle yazılmıştır: sondaki sıfırları kırpar, asla üs yazmaz ve minik değerlerin anlamlı basamaklarını korur, çünkü bir ölçek faktörünü sıfıra yuvarlamak geçerli bir matrixi tekil hâle getirir
uses
  PDFlibExtra;

procedure CheckNumberHelpers;
var
  V: Double;
begin
  // Makine çıktısı: nokta ondalık, üs yok, sondaki sıfırlar kırpılmış
  Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
  Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
  Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235');  // 4 anlamlı basamağı korur
  Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');

  // Makine girdisi: EConvertError yerine yumuşak başarısızlık
  Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
  Assert(not PLTryStrToFloatInvariant('0,5', V));   // content sayıları asla virgül kullanmaz
end;

Ayrıştırma tarafı yazma tarafından neden daha tehlikeli?

Ayrıştırma tarafı daha tehlikelidir, çünkü localee bağlı bir ayrıştırıcı yanlış sayı üretmez, istisna fırlatır. PLStrToFloat StrToFloat çağırır; metin sistem ayracıyla uyuşmazsa EConvertError fırlatır. Virgüllü ondalık sistemde bu, RecolorPagein sıradan bir 0.5 g operatörüyle karşılaştığı anda durması demekti; yani gerçek dünyadaki her sayfa başarısız oluyordu, egzotik olanlar değil. RenderPageRegionToFile kendi belgelenmiş clip formatını, "10.5,20.5,50.5,40.5"i reddediyordu; SVG uzunluk attributeleri, SVG export renkleri, annotation vertex listeleri ve output intent solidity değerleri ya reddediliyor ya da sessizce varsayılanlarla değiştiriliyordu. Geliştirici makinesinde kusursuz çalışan, Münihteki ilk müşteride çöken kütüphane, kazara çalışan Delphi kodu yazısındaki durumlarda olduğu gibi, yalnızca nerede test edildiyse doğru görünen koddur

v3.539.33 her StrToFloat ve TryStrToFloat çağrısını, girdisinin nereden geldiğine göre sınıflandırdı. Content stream operandları, SVG attributeleri, painter renk dizeleri ve virgülle ayrılmış clip ile vertex listelerinin hepsinin sabit nokta sözdizimi vardır; hepsi artık metni kırpıp PLInvariantFormatSettings ile ayrıştıran ve boş, bozuk ya da sonlu olmayan girdide istisna fırlatmak yerine False döndüren PLTryStrToFloatInvarianttan geçer. Virgülle ayrılmış bir listede orta yol yoktur, çünkü virgül hem liste ayracı hem ondalık işareti olamaz. Aynı geçiş sınır dışı bir yazmayı da düzeltti: RenderPageRegionToFile, dört elementlik tamponunun ötesine beşinci clip değerini yazıyordu. PDFi tek renk uzayına çevirme rehberinde anlatılan recoloring hattı için pratik sonuç şu: RecolorPage ve RecolorDocument artık virgüllü ondalık sistemde durmaz. Çağıranın CheckDocumentPolicye yazdığı kural değerleri ise, bir sonraki bölümün açıkladığı sebeple, hoşgörülü yardımcıyı kullanan tek ayrıştırma durumudur

Round-tripin yalnızca bir ucunu düzeltirseniz ne olur?

Bir locale round-tripinin yalnızca bir ucunu düzeltmek, eskiden çalışan kodu bozar; v3.539.32deki structure attribute değişimi bu yüzden yazanı ve okuyanı birlikte taşıdı. SetStructElem* sarmalayıcıları sayıları dize olarak aktarır: SetStructElemBBox dört değeri tek dizeye biçimler, AddTagAttribute üzerinden saklar ve /A yazıcısı daha sonra o diziyi ayrıştırıp sayı mı, dizi mi, name mi olacağına karar verir. İki uç da sistem localeini kullanıyordu; virgüllü ondalık sistemde round-trip kendi içinde tutarlıydı. Hata, bir çağıran belgeleri izleyip AddTagAttributee "0.5" geçirdiğinde ortaya çıktı: okuyucu onu ayrıştıramadı ve PDF namei /0.5 yaydı. PDF/VCR placeholderının ayna görüntüsü bir sorunu vardı; kütüphane GTS_BBoxu noktayla üretiyor, kaydetmeden önce localele doğruluyordu

Yalnızca yazanı noktaya çevirmek hiçbir şey yapmamaktan daha kötü olurdu; her SetStructElem* değeri localee bağlı okuyucuda başarısız olup namee düşerdi. Bu yüzden yazanlar artık PLDoubleToStrConst(v, 6) kullanır, okuyan ise yeni PLTryStrToFloatLenienti kullanır: önce nokta biçimini dener, sonra sistem localeine döner. Eskiden "1,25" geçiren virgüllü localeli bir çağıran yine de 1.25 sayısını alır. Takas bilinçli ve belgelenmiştir: Alman sistemde "1.500" eskiden name oluyordu, çünkü StrToFloat binlik ayracı reddeder; artık 1.5 olarak okunur, buna karşılık sabit NAN ve INF dizeleri artık sayı olarak kabul edilmez

PDFlibPas SetStructElemBBox ve kardeşleri sayıları AddTagAttribute üzerinden dize olarak aktarır ve /A yazıcısı o dizeleri geri ayrıştırır; v3.539.32 iki ucu birlikte taşıdı: PLDoubleToStrConst noktayla yazar, PLTryStrToFloatLenient önce noktayı okuyup localee döner, böylece virgüllü localeli 1,25 hâlâ 1.25 olarak okunur
Yalnızca yazanı düzeltmek her yapılandırılmış element attributeunu bir PDF nameine düşürürdü; bir round-trip iki ucu ya birlikte ya hiç taşır
uses
  System.SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
begin
  FormatSettings.DecimalSeparator := ',';   // virgüllü ondalık çağıran
  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, eskiden /0.5
    Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // hâlâ /StartIndent 1.25
    Lib.DrawText(20, 20, 'figure');
    Lib.EndTag;
    Lib.SaveToFile('tagged.pdf');
  finally
    Lib.Free;
  end;
end;

NaN ve sonsuzluk nerede durdurulur?

AddPageMatrix, ScalePage ve RedactRegion artık NaN ve sonsuz argümanları baştan reddedip 0 döndürür, çünkü hiçbir PDF sayısı onları temsil edemez. ScalePage sıfır ve altı faktörleri çoktan reddediyordu ama NaN bir <= 0 testinden geçer; yani NaN bir ölçek, biçimlendiriciye kadar yol alıyordu. v3.539.26da o biçimlendirici NaN üzerinde hâlâ Round çağırıyordu; x87 birimi geçersiz işlemleri maskelemediği için Win32de bu EInvalidOp fırlatır. v3.539.31, PLDoubleToStrConstu NaN için son savunma hattı olarak 0 yazacak hâle getirdi; ama matrixteki bir sıfır tekil bir dönüşümdür, dolayısıyla gerçek düzeltme hâlâ API düzeyindeki kontrol. İki sınır bilinçli olarak yerinde durur. Metafile durum dizeleri tek süreç içinde localele yazılıp okunur ve süreçten hiç çıkmaz; olduğu gibi bırakıldılar. Ve sayfa element yolundan 1E-5 biçimleyen bir test, katman yeniden yazılmadan önce içeriği okumalıdır, çünkü operandların belge hassasiyetiyle yeniden yayılması o değeri meşru olarak 0a çevirir

Uygulamanız nokta ondalık dünyasının dışındaki müşterilere gidiyorsa en güvenli alışkanlık, PDFlibPas test takımının şu an kullandığı olandır: PDF üreten yolları bir kez FormatSettings.DecimalSeparator virgüle set edilmiş hâlde çalıştırın ve çıktıyı virgül ile üs için tarayın. Ayrıştırılan ondalık hassasiyetini koruma yazısı aynı hikâyenin öteki yarısını kapsar: mevcut bir dosyadan okunan sayılar kaydederken kendi tam metinlerini nasıl korur. İndirmeler, tam API referansı ve deneme derlemesi PDFlibPas Delphi PDF library ürün sayfasındadır