Technical Article

Ekspor Spreadsheet Aman-Unicode di Delphi: RTF dan HTML

Spreadsheet menampung kolom nama pelanggan. Beberapa menggunakan bahasa Mandarin, beberapa menggunakan Sirilik, beberapa memiliki umlaut Jerman atau aksen Prancis. Anda mengekspornya ke CSV dan membuka hasilnya, dan setiap karakter tetap utuh. Anda mengekspor workbook yang sama ke RTF untuk templat gabungan surat (mail-merge), membukanya di pengolah kata, dan nama non-ASCII telah runtuh menjadi baris tanda tanya. Datanya tidak pernah berubah. Yang berubah adalah kontrak pengodean (encoding contract) dari format yang Anda tulis, dan setiap jalur ekspor membawa kontrak yang berbeda.

Ini adalah jebakan yang menangkap pustaka yang terlihat sepenuhnya sadar-Unicode (Unicode-aware) di permukaan. Teks sel disimpan secara internal sebagai WideString, sehingga model tidak pernah kehilangan karakter. Kehilangan terjadi pada batasnya, dalam penulis (writer) yang harus menserialisasikan teks tersebut ke dalam format dengan aturannya sendiri tentang byte mana yang sah dan bagaimana segala sesuatu di luar rentang sah harus dikodekan. Perbaiki satu penulis dan Anda masih bisa mengirim penulis lain yang merusak teks yang sama. Perbaikannya bukan sakelar global. Ini adalah keputusan terpisah yang benar di setiap jalur.

RTF adalah format aman 7-bit secara desain

Rich Text Format mendahului Unicode dan dirancang untuk bertahan dari transportasi yang hanya melewati ASCII yang dapat dicetak. Dokumen RTF mendeklarasikan halaman kode (code page) di header-nya, dan karakter apa pun yang tidak dapat diwakili oleh penulis di halaman kode tersebut harus dikeluarkan sebagai escape alih-alih byte mentah. Escape yang relevan adalah \u, yang membawa unit kode 16-bit bertanda diikuti oleh karakter fallback ASCII untuk pembaca yang terlalu tua untuk memahami escape tersebut sama sekali.

HotXLS menulis RTF dengan cara ini. Header dokumen dibuka dengan mendeklarasikan halaman kode, dalam bentuk \ansi\ansicpg1252\uc1, dan penulis di unit lxRTF menelusuri setiap string mengeluarkan karakter apa pun di atas ASCII biasa sebagai escape \u sehingga aliran byte tetap bersih 7-bit terlepas dari apa yang dapat ditampung oleh halaman kode yang dideklarasikan. Titik kode seperti U+4E2D menjadi urutan literal  3?, not a raw byte yang kemudian coba ditafsirkan oleh pembaca melalui halaman kode apa pun yang kebetulan diasumsikannya. Tanpa disiplin tersebut, apa pun di luar halaman kode yang dideklarasikan tidak memiliki representasi byte yang sah, dan penulis yang mengeluarkan nilai mentah menghasilkan tanda tanya yang memulai artikel ini.

Detail yang harus diingat adalah bahwa halaman kode yang dideklarasikan dan escape adalah dua bagian dari satu kontrak. Mendeklarasikan halaman kode saja tidak membantu teks yang berada di luarnya. Mengeluarkan escape tanpa halaman kode yang dideklarasikan membuat karakter fallback menjadi ambigu. Keduanya harus benar bersama-sama, itulah sebabnya penulis yang hanya menangani salah satunya tetap gagal pada buku kerja (workbook) multibahasa pertama.

Pelepasan (escaping) HTML adalah tentang lebih dari sekadar tanda kurung siku

Ekspor HTML menghasilkan dokumen multi-sheet yang bingkai navigasinya membawa nama sheet sebagai teks yang terlihat. Nama-nama tersebut adalah string yang dikontrol penulis yang dapat berisi karakter apa pun, termasuk karakter yang signifikan untuk markup. Lembar kerja yang secara harfiah bernama Q1 & Q2 <draft> harus mencapai halaman sebagai entitas yang lolos (escaped entities), jika tidak, tanda kurung siku akan membuka tag siluman dan ampersand memulai referensi entitas yang tidak pernah dimaksudkan. Ini adalah pelepasan (escaping) HTML biasa, dan melewatkannya pada label bingkai adalah jenis kelalaian yang lolos dari setiap pengujian yang dibangun dari nama sheet khusus ASCII.

Pertanyaan pengodean berada satu tingkat di bawah itu. Ketika karakter non-ASCII mendarat dalam konteks yang tidak dijamin disajikan sebagai UTF-8, representasi yang aman adalah referensi karakter numerik (numeric character reference), sehingga U+00E9 ditulis sebagai é alih-alih byte mentah yang maknanya bergantung pada charset respon. Kebalikan dari aturan ini berlaku pada proses masuk. Buku kerja yang dibaca kembali dari XLSX membawa string bersama (shared strings) di mana karakter mungkin sudah disimpan sebagai entitas XML numerik, dan entitas tersebut harus didekodekan menjadi satu karakter utuh sebelum memasuki model sel. Dekodekan dengan tidak hati-hati, memisahkan titik kode menjadi byte terpisah, dan karakter tunggal muncul kembali sebagai dua bagian mojibake yang tidak dapat diperbaiki oleh ekspor mana pun nanti.

Wadah XLSX adalah ZIP, dan ZIP memiliki pengodean namanya sendiri

Berkas XLSX adalah arsip ZIP, dan arsip tersebut menyimpan nama untuk setiap anggota yang dipegangnya. ZIP cukup tua sehingga spesifikasi aslinya tidak mengatakan apa-apa tentang pengodean nama-nama tersebut, sehingga pembaca yang tidak menemukan sinyal akan mengasumsikan halaman kode lokal arsip. Asumsi tersebut salah begitu nama anggota berisi karakter non-ASCII, yang terjadi pada nama bagian lembar kerja yang dilokalkan dan dengan media tertanam yang nama berkasnya membawa aksen atau skrip non-Latin.

Perbaikannya adalah bit tunggal. Bit 11 tujuan umum (general-purpose bit 11) di setiap header berkas lokal menyatakan bahwa nama anggota dikodekan sebagai UTF-8. HotXLS memeriksa bit tersebut secara tepat ketika membaca arsip, menguji bendera tujuan umum terhadap masker $0800, dan pembaca atau penulis yang mengabaikannya akan salah membaca nama yang disimpan oleh implementasi yang benar sebagai UTF-8. Bit tersebut murah untuk disetel dan murah untuk dihormati, dan ini adalah seluruh perbedaan antara nama anggota yang bertahan dalam perjalanan pulang pergi dan nama yang tiba dalam keadaan rusak sebelum konten spreadsheet bahkan diurai.

Case folding dan pemindaian nomor menyembunyikan bahaya yang sama

Evaluasi rumus adalah tempat keamanan Unicode berhenti menjadi tentang serialisasi dan mulai menjadi tentang perbandingan. Fungsi SEARCH tidak sensitif huruf (case-insensitive), yang berarti ia harus melipat huruf (fold case) sebelum mencari substring. Cara yang salah untuk melipat adalah melalui halaman kode ANSI, karena mengubah teks non-ASCII ke huruf besar dengan cara itu merutekan karakter melalui halaman kode yang sempit dan merusak apa pun di luarnya. Cara yang benar adalah mengubah huruf besar wide-string, yang mempertahankan rentang UTF-16 penuh. HotXLS melipat dengan WideUpperCase karena alasan ini secara tepat, sehingga pencarian untuk teks beraksen atau non-Latin cocok dengan karakter yang sama dengan yang diberikan alih-alih perkiraan halaman kode yang dirusak.

Tokenizer rumus membawa kewajiban terkait yang tidak ada hubungannya dengan huruf dan memiliki segalanya untuk dilakukan dengan tempat token berakhir. Notasi ilmiah seperti 1E3 atau 2.5E-3 is literal numerik tunggal, dan pemindai (scanner) harus mengenali karakter E, tanda opsional, dan digit berikutnya sebagai bagian dari angka alih-alih memecah masukan menjadi nama diikuti oleh angka terpisah. Pemindai yang salah menangani hal ini mengubah konstanta yang sepenuhnya valid menjadi kesalahan penguraian atau, lebih buruk lagi, ekspresi yang salah secara diam-diam. Ini termasuk dalam diskusi yang sama karena kedua kasus adalah tentang pembaca yang membuat keputusan tingkat karakter yang benar: satu tentang bagaimana melipat karakter untuk perbandingan, yang lain tentang apakah karakter melanjutkan token saat ini.

Membangun dan mengekspor workbook multibahasa

API publik tidak meminta Anda untuk memikirkan semua ini. Anda membangun workbook dari nilai sel WideString dan memanggil titik masuk ekspor yang Anda inginkan. Keputusan pengodean terjadi di dalam setiap penulis. Contoh di bawah ini menyemai sheet dengan teks dalam beberapa skrip, lalu menulis berkas RTF dan berkas HTML dari workbook yang sama, sehingga kedua jalur berjalan terhadap masukan yang identik.

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';

    // Cell text is held as WideString, so every script survives the model.
    Sheet.Cells[2, 1].Value := '王伟';          // Chinese
    Sheet.Cells[2, 2].Value := '北京';
    Sheet.Cells[3, 1].Value := 'Müller';        // German umlaut
    Sheet.Cells[3, 2].Value := 'Köln';
    Sheet.Cells[4, 1].Value := 'Иванов';        // Cyrillic
    Sheet.Cells[4, 2].Value := 'Москва';
    Sheet.Cells[5, 1].Value := 'Désirée';       // French accents
    Sheet.Cells[5, 2].Value := 'Montréal';

    // RTF: the lxRTF writer declares the code page and emits every
    // non-ASCII character as a \u escape, keeping the file 7-bit clean.
    Book.SaveAsRTF('Customers.rtf');

    // HTML: sheet names are HTML-escaped and non-ASCII text is written
    // so it does not depend on a guessed response charset.
    Book.SaveAsHTML('Customers.html');
  finally
    Book := nil;
  end;
end;

Kedua panggilan mengembalikan status Integer, dan keduanya mengonsumsi teks dalam memori yang sama. Tidak ada dalam kode pemanggil yang mendeklarasikan halaman kode atau melepaskan karakter, karena tanggung jawab berada pada penulis yang mengetahui formatnya sendiri. Tingkat-workbook SaveAsCSV mengikuti bentuk yang sama jika Anda memerlukan ekspor terdelimitasi dari sumber yang identik.

// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');

Keamanan Unicode adalah per-jalur, bukan per-pustaka

Pelajaran yang layak dibawa pulang adalah bahwa tidak ada satu tempat pun untuk aman secara Unicode. RTF memerlukan halaman kode yang dideklarasikan ditambah escape \u. HTML membutuhkan entity escaping untuk karakter yang signifikan untuk markup dan referensi numerik di mana charset tidak dijamin, ditambah dekode entitas yang benar yang tiba di string bersama. Wadah ZIP membutuhkan bit 11 tujuan umum yang disetel sehingga nama anggota UTF-8 dibaca sebagai UTF-8. Evaluasi rumus membutuhkan case folding wide-string dan tokenizer yang menjaga notasi ilmiah tetap utuh. Masing-masing adalah kontrak yang berbeda, dan pustaka dapat memenuhi satu kontrak sambil melanggar kontrak lainnya secara diam-diam. Itulah alasan mengapa alat yang melakukan CSV dengan benar masih dapat menyerahkan RTF yang penuh dengan tanda tanya kepada Anda.

Jika ekspor Anda bersandar pada format terdelimitasi, kompromi di antaranya dibahas dalam panduan kami tentang ekspor CSV, TSV, dan HTML, dan ketika sumbernya adalah set hasil alih-alih lembar kerja buatan tangan, pola dalam ekspor database untuk laporan Delphi berpasangan secara alami dengan aturan pengodean yang dijelaskan di sini. Semua itu dikirim sebagai bagian dari HotXLS Component untuk Delphi dan C++Builder, bersama dengan API pembacaan, rumus, dan pemformatan yang dibahas di bagian lain di blog ini.