Teknik Makale

Delphi ve HotPDF ile Sabit PDF Sayı Biçimlendirme

Bir PDF dosyasına ulaşan her gerçek sayı, işletim sistemi ondalık ayırıcının ne olduğunu düşünürse düşünsün, bir noktayla yazılmalıdır. HotPDF Delphi Component hepsini HPDFTypes içindeki tek bir değişmez dönüştürme katmanından geçirir; böylece bir Alman, Fransız veya Brezilyalı iş istasyonu, uyumlu hiçbir okuyucunun ayrıştırmayacağı virgülle ayrılmış varyant yerine 0.5 0 0 0.5 5.5 7.25 cm yayar

Bu, iki satırlık bir düzeltme gibi duyuluyor. Öyle değildi. Kusur yıllarca bir yan etki tarafından kamufle edilmişti, kimsenin sayı biçimlendirme kodu olarak düşünmediği üç alt sistemde yeniden belirdi, bir derleyici altında kayboldu ve bir başkasının altında yaşadı ve son katmanının ondalık ayırıcıyla hiçbir ilgisi yoktu

Ondalık virgüllü bir yerel ayar bir PDF içerik akışını neden bozar?

Çünkü ISO 32000-1 §7.3.3, sayısal bir nesneyi isteğe bağlı işaret, ondalık rakamlar ve en fazla bir nokta olarak tanımlar. Virgül o üretimin parçası değildir ve §7.2.2 altında bir ayırıcı da değildir; bu yüzden okuyucu, tek bir işlenen kastettiğiniz yerde iki işlenen görmez. Sözcüksel olarak başarısız olan bir belirteç görür ve onu içeren operatör atılır. Bir grafik durumu matrisindeki tek bir 0,5, çevresindeki tüm q ... Q bloğunu götürür. Başarısızlık ayrıca sahadaki ayırt edici bir şekle sahiptir: asla geliştirici makinesinde tekrarlanmaz, tek bir ülkeden bir destek bileti olarak gelir ve çoğu görüntüleyici bozuk operatörü sessizce atıp sayfanın geri kalanını render ettiği için bir sözdizimi hatası değil render hatası gibi okunur

HotPDF tarafından üretilen bir PDF içerik akışındaki yerel ayar bozulması diyagramı: ondalık virgüllü bir konak süreç bir rg ve cm operatörünün içine 0,5 yayar, ISO 32000-1 sayı dilbilgisi yalnızca nokta kabul ettiği için belirteç sözcüksel olarak başarısız olur ve görüntüleyici çevresindeki q Q bloğuyla birlikte operatörleri sessizce atar
Virgül sayısal üretimin parçası değildir ve ayırıcı da değildir; bu yüzden tek bir bozuk işlenen, hiçbir hata olmadan redaksiyon dikdörtgenini ve birleştirilmiş sayfayı kaldırır
// DecimalSeparator = ',' ile çalışan konak süreç
Doc.LoadFromFile('invoice.pdf');
Doc.RedactLoadedRect(0, 10.25, 20.5, 40.75, 60.5, 0.5, 0.25, 0.125);
Doc.StitchLoadedPage(0, 1, 5.5, 7.25, 0.5);

// Eklenen /Contents'in taşıdığı şey:
//   0,5 0,25 0,125 rg 10,25 20,5 30,5 40 re f
//   q 0,5 0 0 0,5 5,5 7,25 cm /StitchSrc Do Q
//
// ISO 32000-1 7.3.3'te "0,5" için bir üretim yoktur, bu yüzden redaksiyon
// dikdörtgeni ve birleştirilmiş sayfa, hata olmadan ikisi de kaybolur

Kusuru yıllarca gizleyen yan etki

HotPDF, oluşturma yolu sayılarının çoğunu _CutFloat üzerinden yazar ve bu fonksiyonun ilk revizyonları sessizce düşmanca bir şey yaptı: her çağrıda konak sürecin global FormatSettings.DecimalSeparator'una '.' atadı. Bu, bileşeni barındıran uygulamanın yerel ayar durumunu bozar ve iş parçacığı güvenli değildir, ama ilginç sonuç üçüncüsüdür. Herhangi bir içerik üretildikten sonra süreç ayırıcısı bir noktaydı; bu yüzden kütüphanenin başka yerindeki her çıplak FloatToStr ve StrToFloat kazara doğru hâle geldi. Hemen hemen her örneğin kullandığı oluştur-sonra-düzenle sırası bu yüzden geçti ve maskelediği şey, ilk _CutFloat çağrısından önce çalışan her yoldu: yüklenmiş belge düzenleyicileri StitchLoadedPage ve RedactLoadedRect, metin ve görüntü damgaları, bileşik filigranlar, Format('%.2f') üzerinden barkod çizimi, /DA varsayılan görünüm ayrıştırıcısı ve Type 1 yazı tipi alt sisteminin tamamı

HotPDF'te _CutFloat yan etkisinin zaman çizelgesi: ilk çağrı global FormatSettings.DecimalSeparator'a bir nokta atar, bu da sonraki her çıplak FloatToStr'u kazara doğru yapar; yüklenmiş düzenleyiciler StitchLoadedPage ve RedactLoadedRect, barkod yolu, /DA ayrıştırıcısı ve Type 1 yazı tipi alt sistemi ise bozuk kalır
Her örneğin kullandığı oluştur-sonra-düzenle sırası kusuru görünmez tuttu, çünkü ilk dönüştürmeden sonraki her şey çevrilmiş global ayırıcıyı miras aldı

Type 1 ayrıştırma, grubun en çirkin yoluyla başarısız oldu. FontMatrix, BlueValues, sap gövde ipuçları ve Type 2 charstring'lerin içindeki gerçek sayılar, Adobe Type 1 Font Format belirtimine göre noktalarla yazılmış PostScript gerçek sayılarıdır. Virgüllü bir yerel ayar altında 0.001 ayrıştırmak EConvertError yükseltir, saran bir try/except onu yuttu ve font matrisi sıfırda kaldı. O fonttan her glif sonra çöktü; bu, bir yerel ayar sorunundan çok bir font sorunu gibi görünür ve sizi bir öğleden sonra gömülü glif render etmeyi hata ayıklamaya gönderir

Dağınık yamalar yerine tek bir dönüştürme katmanı

HotPDF'in şimdi izlediği kural nettir: PDF, PostScript, JSON veya JavaScript sözdizimine yazılan ya da oradan okunan her sayı, açık bir değişmez dönüştürmeden geçer ve hiçbir kod, global bir FormatSettings yan etkisine bağlı değildir. HPDFTypes dört dönüştürmenin, paylaştıkları ayarlar kaydının ve barkod yolu için bir sabit duyarlık yardımcısının sahibidir

// Lib/HPDFTypes.pas - tek değişmez dönüştürme yüzeyi
function HPDFInvariantFormatSettings: TFormatSettings;

function HPDFFloatToInvariantStr(Value: Extended): string;
function HPDFTryInvariantStrToFloat(const S: string;
  out Value: Extended): Boolean;
function HPDFInvariantStrToFloat(const S: string): Extended;
function HPDFInvariantStrToFloatDef(const S: string;
  const Default: Extended): Extended;

function HPDFFormatInvariant2(Value: Extended): string;

İki birim zaten kendi özel InvariantFormatSettings fonksiyonunu taşıyordu, HPDFContentStream ve HPDFTextExtraction; ikisi de paralel uygulamalar olarak bırakılmak yerine tek satırlık bir ileri bildirime indirildi. Type 1 alt sistemi bilinçli istisnadır: VCL bağımlılığı taşımaz ve HPDFTypes'e başvurmaz; bu yüzden aynı semantiklerle HPDFType1Core.Type1StrToFloat adında yerel bir aynayı korur; gerekçe, kopyalanmış on satırlık bir fonksiyonun grafik katmanını bir font ayrıştırıcısına sürüklemekten daha küçük bir yükümlülük olduğudur. İki başka yer bilerek yalnız bırakıldı. HPDFHTMLImport CSS uzunluklarını, noktayı bilinçli olarak yerel ayar ayırıcısıyla takas edip sonra yerel ayarla ayrıştırarak çözer; bu o girdi için doğrudur. HPDFType1CB.GlyphCommands ve FontBBox dizeleri ise, çağıranların kendi StrToFloat'uyla geri okuduğu genel API'dir; aynı yerel ayarda üretip tüketmek öz tutarlıdır ve değişmez çıktı, virgül bölgesindeki her çağıranı kırardı

HPDF_HAS_FORMAT_SETTINGS derleyiciler arasında neyi garanti eder?

Değişmez yolun desteklenen her araç zincirinde gerçekten derlendiğini garanti eder; bariz sürüm kapısı bunu yapmıyordu. Delphi XE2, TFormatSettings alan TFormatSettings.Create ve FloatToStr / TryStrToFloat aşırı yüklemelerini getirdi; bu yüzden ilk içgüdü, her çağrı noktasına {$IFDEF XE2+}, InvariantFormatSettings{$ENDIF} yazmaktır. Bu içgüdünün bir kör noktası vardır: HotPDF.inc, Free Pascal altında D2009+'ı tanımlar ama XE2+'ı asla tanımlamaz, çünkü XE2+ Delphi sürüm numaraları hakkında bir soruyu yanıtlarken Free Pascal 3.2.2 o aşırı yüklemeleri koşulsuz getirir. Bu yüzden o kapıların her biri, özgün raporların geldiği platformlarda tam da, FPC derlemelerinde sessizce yerel ayara bağlı dala dönüştü. HPDF_HAS_FORMAT_SETTINGS, HotPDF.inc'in hem FPC bloğunda hem XE2+ bloğunda tanımlanır ve sayı yardımcılarının test ettiği tek semboldür

{$IFDEF HPDF_HAS_FORMAT_SETTINGS}
function HPDFInvariantFormatSettings: TFormatSettings;
begin
{$IFDEF FPC}
  Result := DefaultFormatSettings;   // FPC'de TFormatSettings.Create yok
{$ELSE}
  Result := TFormatSettings.Create;
{$ENDIF}
  Result.DecimalSeparator := '.';
  Result.ThousandSeparator := ',';
end;
{$ENDIF}

Fonksiyonun içinde bir derleyici dalı yaşar, çünkü Free Pascal'da TFormatSettings.Create yoktur ve DefaultFormatSettings'tan başlamak zorundasınız. Dönüştürmeleri yalnızca iki sayı ayırıcısı besler; tarih ve para birimi alanları, geçerli yerel ayarın sağladığı her neyse onu korur. XE2 öncesi Delphi'de aşırı yükleme hiç yoktur ve orada yardımcılar, süreç yerel ayarıyla dönüştürüp ardından sonuç dizede ayırıcı karakterini yeniden yazmaya geri döner; bu çirkin ama global duruma asla dokunmaz

Üçüncü katman: SVG'in izin verdiği ama PDF'in izin vermediği üsler

Ondalık ayırıcıyı düzeltmek gerekli ve yeterli değildir. W3C SVG 1.1 §4.2, sayı üretiminde bir üs kabul eder ve XPS işaretlemesi de aynısını yapar; bu yüzden 1E-6 girişte tamamen geçerli bir koordinattır; ISO 32000-1 §7.3.3 ise hiçbir üs kabul etmez. Değişmez biçimlendirmeden sağ çıkan bir değer, yine de içerik akışına 1E-6 olarak inebilir ve reddedilebilir; ayrıca sabit biçimdeki FloatToStrF, değer RTL rakam aralığının dışına çıktığında kendisi üs biçimine geri düşebilir. HPDFSafeSVG.SVGFloat bu yüzden değişmez dönüştürmeden sonra kendi işini yapar: 1.0E-7'nin altındaki her şeyi sıfıra boşaltır, sonra bir E hayatta kalırsa üssü söker ve ondalık noktayı elle kaydırır, kaydırmanın gerektirdiği tarafa sıfırlarla doldurur. SVGFinite girişi, biçimlendiriciye ulaşmadan önce NaN'ı ve ±1.0E30 dışındaki her şeyi reddederek korur ve SVGReadNumber ile SVGSkipSeparators, gelen yönü RTL'ye devretmek yerine karakter karakter halleder

HotPDF'in PDF işlenenlerini yazmadan önce üsleri nasıl söktüğünü gösteren boru hattı: SVGFinite NaN'ı ve 1.0E30 ötesindeki değerleri reddeder, değişmez dönüştürme bir nokta bırakır, çok küçük büyüklükler sıfıra boşalır ve hayatta kalan E taşıyan HPDFFloatToInvariantStr çıktısında üs kaldırılıp ondalık nokta elle kaydırılır
SVG 1.1 bir üs kabul eder, ISO 32000-1 kabul etmez; bu yüzden dönüştürme sınırı, 1E-6'yı uyumlu bir okuyucunun ayrıştırabileceği düz ondalık gösterime yeniden yazar
// Lib/HPDFSafeSVG.pas - PDF işlenenleri için ondalık gösterim
if Abs(Value) < 0.0000001 then
  Value := 0;
S := HPDFFloatToInvariantStr(Value);
ExponentAt := Pos('E', UpperCase(S));
if ExponentAt > 0 then
begin
  ExponentValue := StrToInt(Copy(S, ExponentAt + 1, MaxInt));
  SetLength(S, ExponentAt - 1);
  // ... ondalık noktayı ExponentValue kadar kaydır, '0' ile
  // sola veya sağa doldur, ardından işareti yeniden ekle
end;

Bu, SVG sayfa dışa aktarımını ve grafik durumu işlemesini besleyen aynı TSVGBuffer'dir ve XPS ve OXPS belgelerini PDF'e dönüştürürken HPDFCreateXPSDocumentHandler ve THPDFHandledDocumentFormat'ın hdfXPS üyesi üzerinden aynı sayısal disiplin içe aktarma tarafında da geçerlidir. İşaretleme biçimleri sayı sözdizimi konusunda cömerttir. PDF içerik akışı öyle değildir ve bu farkın çözülmesi gereken yer dönüştürme sınırıdır

Yerel ayarı varsaymak yerine yeniden üreten regresyon testleri

Yalnızca başkasının makinesinde beliren bir yerel ayar hatası, yerel ayarı kendileri set eden testlerle sabitlenmeye değerdir ve HotPDF iki tane taşır. LoadedEditsEmitInvariantNumbersUnderCommaLocale, ilk düzenlemeden önce FormatSettings.DecimalSeparator'a bir virgül atar, RedactLoadedRect, StampLoadedPageText, StitchLoadedPage ve bir bileşik damga çalıştırır, sonra sayfa içerik baytlarını geri okur ve hem tam operatör dizelerinin mevcut olduğunu hem de arabelleğin hiçbir yerinde virgül olmadığını iddia eder. DocumentCreationEmitsInvariantNumbersAndKeepsHostLocale diğer yarıyı ve özgün günahı kapsar: aynı virgül yerel ayarı altında, sabit iki ondalık biçimlendirmeli DirectDrawBarcode dahil oluşturma yolunu sürer, sonra içerik akışının virgül taşımadığını ve EndDoc döndüğünde FormatSettings.DecimalSeparator'un hâlâ bir virgül olduğunu iddia eder

İkinci iddia, özgün kusuru yazıldığı gün yakalayacak olandır, çünkü kütüphanenin konağına ne yaptığını değil, yalnızca diske ne yazdığını değil, ikisini birden test eder. Ders PDF'in ötesine genelleşir: sabit bir sözcüksel dilbilgisi olan her biçim — PostScript, JSON ve JavaScript dahil — sınırda açık bir değişmez dönüştürme gerektirir. Doğru cevabı almak için bir global ayara uzanmak, birisi kodunuzu farklı bir sırada, farklı bir iş parçacığında veya farklı bir derleyiciden çağırana kadar çalışır ve o zamana kadar başarısızlık, biçimlendirme sorunu dışında her şeye benzer

Burada anlatılan sayı biçimlendirme katmanı, ona bağlı yüklenmiş belge düzenleme, barkod, SVG dışa aktarım ve XPS dönüştürme yollarıyla birlikte Delphi ve C++Builder için HotPDF Delphi Component'in bir parçası olarak gelir