Teknik Makale

Delphi'de Unicode Güvenli Elektronik Tablo Dışa Aktarımı: RTF ve HTML

Bir elektronik tablo bir müşteri adları sütunu tutar. Bazıları Çince, bazıları Kiril, birkaçı Almanca umlaut veya Fransız aksanı taşır. CSV olarak dışa aktarır ve sonucu açarsınız, her karakter bozulmamış haldedir. Aynı çalışma kitabını bir adres-mektup birleştirme şablonu için RTF olarak dışa aktarır, bir kelime işlemcide açarsınız ve ASCII olmayan adlar soru işareti sıralarına çöker. Veri asla değişmedi. Değişen şey yazdığınız formatın kodlama sözleşmesidir ve her dışa aktarım yolu farklı bir sözleşme taşır

Yüzeyde tamamen Unicode uyumlu görünen bir kütüphaneyi yakalayan tuzak budur. Hücre metni dahili olarak WideString olarak tutulur, böylece model asla bir karakter kaybetmez. Kayıp, bu metni, hangi baytların yasal olduğu ve yasal aralığın dışındaki herhangi bir şeyin nasıl kodlanması gerektiği ile ilgili kendi kuralları olan bir biçime serileştirmek zorunda olan yazıcıda sınırda gerçekleşir. Bir yazıcıyı doğru bir şekilde elde ederseniz, aynı metni bozan başka bir tane daha gönderebilirsiniz. Düzeltme genel bir anahtar (switch) değildir. Her yolda ayrı, doğru bir karardır

RTF, tasarımı gereği 7-bit güvenli bir formattır

Zengin Metin Biçimi (Rich Text Format) Unicode'dan önce gelir ve yalnızca yazdırılabilir ASCII geçen aktarımlarda hayatta kalmak üzere belirlenmiştir. Bir RTF belgesi başlığında bir kod sayfası bildirir ve yazıcının bu kod sayfasında temsil edemeyeceği herhangi bir karakterin ham bayt (raw byte) yerine bir kaçış (escape) olarak yayımlanması gerekir. İlgili kaçış, imzalı (signed) 16 bitlik bir kod birimini ve ardından kaçışı hiç anlamayacak kadar eski okuyucular için bir ASCII geri dönüş (fallback) karakterini taşıyan \u'dur

HotXLS RTF'yi bu şekilde yazar. Belge başlığı, \ansi\ansicpg1252\uc1 biçiminde kod sayfasını bildirerek açılır ve lxRTF birimindeki yazar, bildirilen kod sayfasının neleri tutabileceğinden bağımsız olarak bayt akışının 7-bit temiz kalması için düz ASCII'nin üzerindeki herhangi bir karakteri \u kaçışı olarak yayımlayarak her dizede yürür. U+4E2D gibi bir kod noktası, bir görüntüleyicinin daha sonra varsaydığı kod sayfası üzerinden yorumlamaya çalışacağı ham bir bayt yerine kelimenin tam anlamıyla \u20013? dizisine dönüşür. Bu disiplin olmadan, bildirilen kod sayfasının dışındaki hiçbir şeyin yasal bir bayt temsili yoktur ve ham değeri yayan bir yazıcı, bu makaleyi başlatan soru işaretlerini üretir

Akılda tutulması gereken ayrıntı, bildirilen kod sayfası ve kaçışların bir sözleşmenin iki yarısı olduğudur. Kod sayfasını tek başına bildirmek, onun dışında kalan metne yardımcı olmaz. Bildirilmiş bir kod sayfası olmadan kaçışlar yayımlamak, geri dönüş karakterlerini belirsiz bırakır. Her ikisinin de birlikte doğru olması gerekir, bu nedenle bunlardan yalnızca birini işleyen bir yazıcının ilk çok dilli çalışma kitabında hala başarısız olmasının nedeni budur

HTML kaçışı, açılı ayraçlardan daha fazlası ile ilgilidir

HTML dışa aktarımı, gezinme çerçeveleri sayfa adlarını görünür metin olarak taşıyan çok sayfalı bir belge üretir. Bu adlar, biçimlendirme için anlamlı olanlar da dahil olmak üzere herhangi bir karakteri içerebilen, yazar kontrollü dizelerdir. Tam anlamıyla Q1 & Q2 <draft> olarak adlandırılan bir sayfanın kaçmış varlıklar (escaped entities) olarak sayfaya ulaşması gerekir, aksi takdirde açılı ayraçlar hayalet bir etiket açar ve ve işareti (ampersand) asla amaçlanmayan bir varlık referansını başlatır. Bu sıradan HTML kaçışıdır ve onu bir çerçeve etiketinde atlamak, yalnızca ASCII olan sayfa adlarından oluşturulmuş her testi geçen türden bir ihmaldir

Kodlama (encoding) sorusu bunun bir katman altında yer alır. ASCII olmayan karakterler, UTF-8 olarak sunulması garanti edilmeyen bir bağlama düştüğünde, güvenli temsil sayısal bir karakter referansıdır, dolayısıyla U+00E9, anlamı yanıt karakter kümesine bağlı olan ham bir bayt yerine &#233; olarak yazılır. Bu kuralın ayna görüntüsü girerken de geçerlidir. XLSX'ten geri okunan bir çalışma kitabı, bir karakterin zaten sayısal bir XML varlığı olarak depolanabileceği paylaşılan dizeler taşır ve bu varlığın hücre modeline girmeden önce tam bir karakter olarak çözülmesi (decode) gerekir. Onu dikkatsizce, bir kod noktasını ayrı baytlara bölerek çözerseniz, tek bir karakter, hiçbir sonraki dışa aktarımın onaramayacağı iki parça bozuk metin (mojibake) olarak yeniden ortaya çıkar

XLSX kapsayıcısı bir ZIP'tir ve ZIP'in kendi isim kodlaması vardır

XLSX dosyası bir ZIP arşividir ve arşiv tuttuğu her üye için bir ad saklar. ZIP o kadar eskidir ki, orijinal özelliği bu adların kodlanması (encoding) hakkında hiçbir şey söylemez, bu nedenle hiçbir sinyal bulamayan bir okuyucu, arşivin yerel kod sayfasını varsayar. Bir üye adı ASCII olmayan bir karakter içerdiği anda bu varsayım yanlıştır, ki bu yerelleştirilmiş (localized) çalışma sayfası parça adlarında ve dosya adları aksan veya Latin dışı alfabe taşıyan gömülü medyada meydana gelir

Düzeltme tek bir bittir. Her yerel dosya başlığındaki (local file header) genel amaçlı bit 11, üye adının UTF-8 olarak kodlandığını beyan eder. HotXLS, bir arşivi okurken tam olarak bu biti denetler, genel amaçlı bayrakları $0800 maskesine karşı test eder ve onu yok sayan bir okuyucu veya yazıcı, doğru bir uygulamanın UTF-8 olarak sakladığı bir adı yanlış okur. Biti ayarlamak ve ona uymak ucuzdur ve gidiş dönüşte (round trip) hayatta kalan bir üye adıyla elektronik tablo içeriği ayrıştırılmadan önce bozuk gelen ad arasındaki bütün farktır

Büyük/küçük harf katlama ve sayı tarama da aynı tehlikeyi gizler

Formül değerlendirmesi, Unicode güvenliğinin serileştirme olmaktan çıkıp karşılaştırma (comparison) ile ilgili olmaya başladığı yerdir. SEARCH işlevi büyük/küçük harfe duyarlı değildir, bu da bir alt dize (substring) aramadan önce büyük/küçük harf katlaması (case folding) yapması gerektiği anlamına gelir. Katlamanın yanlış yolu ANSI kod sayfasından geçmektir, çünkü ASCII olmayan metni bu şekilde büyük harf yapmak, karakterleri dar bir kod sayfasından geçirir ve dışındaki her şeyi bozar. Doğru yol, tam UTF-16 aralığını koruyan geniş-dize (wide-string) büyük harf yapmadır. HotXLS tam olarak bu nedenle WideUpperCase ile katlanır, böylece aksanlı veya Latin olmayan metin için bir arama, bu karakterlerin kod-sayfasıyla-bozulmuş bir yaklaşımı yerine verilen aynı karakterlerle eşleşir

Formül simgeleştiricisi (tokenizer), harflerle hiçbir ilgisi olmayan ve bir simgenin (token) nerede bittiğiyle her türlü ilgisi olan benzer bir yükümlülük taşır. 1E3 veya 2.5E-3 gibi bilimsel gösterimler tek bir sayısal harf dizisidir (literal) ve tarayıcının E'yi, isteğe bağlı bir işareti ve ardından gelen basamakları, girdiyi bir isim ve ardından ayrı bir sayı olarak bölmek yerine sayının bir parçası olarak tanıması gerekir. Bunu yanlış işleyen bir tarayıcı, tamamen geçerli bir sabiti bir ayrıştırma (parse) hatasına veya daha kötüsü sessizce yanlış bir ifadeye dönüştürür. Aynı tartışmaya aittir çünkü her iki durum da okuyucunun doğru bir karakter düzeyinde karar vermesiyle ilgilidir: biri karşılaştırma için bir karakterin nasıl katlanacağı, diğeri ise bir karakterin geçerli simgeye (token) devam edip etmediği hakkında

Çok dilli bir çalışma kitabı oluşturma ve dışa aktarma

Açık (public) API bunların hiçbiri hakkında düşünmenizi istemez. Çalışma kitabını WideString hücre değerlerinden oluşturur ve istediğiniz dışa aktarma giriş noktasını (entry point) çağırırsınız. Kodlama kararları her yazarın (writer) içinde gerçekleşir. Aşağıdaki örnek, bir sayfayı çeşitli alfabelerde metinle doldurur, ardından aynı çalışma kitabından hem bir RTF dosyası hem de bir HTML dosyası yazar, böylece iki yol aynı girdiye karşı çalışır

uses
  lxHandle;

procedure ExportMultilingualWorkbook;
var
  Book: IXLSWorkbook;
  Sheet: IXLSWorksheet;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Customers');

    Sheet.Cells[1, 1].Value := 'Name';
    Sheet.Cells[1, 2].Value := 'City';

    // Hücre metni WideString olarak tutulur, bu nedenle her alfabe modelden sağ çıkar.
    Sheet.Cells[2, 1].Value := '王伟';          // Çince
    Sheet.Cells[2, 2].Value := '北京';
    Sheet.Cells[3, 1].Value := 'Müller';        // Almanca umlaut
    Sheet.Cells[3, 2].Value := 'Köln';
    Sheet.Cells[4, 1].Value := 'Иванов';        // Kiril
    Sheet.Cells[4, 2].Value := 'Москва';
    Sheet.Cells[5, 1].Value := 'Désirée';       // Fransız aksanları
    Sheet.Cells[5, 2].Value := 'Montréal';

    // RTF: lxRTF yazarı kod sayfasını beyan eder ve ASCII olmayan her karakteri
    // dosyayı 7-bit temiz tutarak \u kaçışı olarak yayımlar.
    Book.SaveAsRTF('Customers.rtf');

    // HTML: sayfa adları HTML-kaçışlıdır ve ASCII olmayan metin
    // tahmin edilen bir yanıt karakter kümesine bağlı kalmayacak şekilde yazılır.
    Book.SaveAsHTML('Customers.html');
  finally
    Book := nil;
  end;
end;

Her iki çağrı da bir Integer (Tamsayı) durum döndürür ve her ikisi de bellekteki aynı metni tüketir. Çağıran kodda (calling code) hiçbir şey bir kod sayfası bildirmez veya bir karakteri kaçışa (escape) zorlamaz, çünkü sorumluluk kendi biçimini bilen yazara (writer) aittir. Aynı kaynaktan (source) sınırlandırılmış (delimited) bir dışa aktarmaya ihtiyacınız varsa, çalışma kitabı düzeyindeki SaveAsCSV aynı şekli izler

// Aynı çalışma kitabı, kendi kodlama kurallarına sahip üçüncü bir dışa aktarma yolu.
Book.SaveAsCSV('Customers.csv');

Unicode güvenliği kütüphane başına değil, yol başınadır

Buradan çıkarılması gereken ders, Unicode güvenliğinin olacağı tek bir yerin olmadığıdır. RTF, bildirilmiş bir kod sayfası artı \u kaçışları (escapes) gerektirir. HTML, işaretleme (markup) açısından önemli karakterler için varlık kaçışı (entity escaping) ve karakter kümesinin (charset) garanti edilmediği durumlarda sayısal referanslar ve paylaşılan dizelerde gelen varlıkların doğru şekilde çözülmesini (decoding) gerektirir. ZIP kapsayıcısı (container), genel amaçlı bit 11'in ayarlanmasına ihtiyaç duyar, böylece UTF-8 üye adı UTF-8 olarak okunur. Formül değerlendirmesi, geniş-dize (wide-string) büyük/küçük harf katlama (case folding) ve bilimsel gösterimi tek parça tutan bir simgeleştirici (tokenizer) gerektirir. Bunların her biri farklı bir sözleşmedir ve bir kütüphane birini sessizce ihlal ederken diğerini yerine getirebilir. CSV'yi doğru yapan bir aracın size yine de soru işaretleriyle dolu bir RTF sunabilmesinin nedeni budur

Dışa aktarımlarınız sınırlandırılmış (delimited) formatlara yaslanıyorsa, bunlar arasındaki ödünleşimler (trade-offs) CSV, TSV ve HTML dışa aktarımı kılavuzumuzda kapsanmaktadır ve kaynak elle oluşturulmuş bir sayfa yerine bir sonuç kümesi olduğunda, Delphi raporları için veritabanı dışa aktarımı makalesindeki desenler burada açıklanan kodlama kurallarıyla doğal olarak eşleşir. Tüm bunlar, bu blogun başka bir yerinde ele alınan okuma, formül ve biçimlendirme API'lerinin yanı sıra Delphi ve C++Builder için HotXLS Bileşeninin bir parçası olarak gönderilir