Technical Article

Unicode-biztos táblázat-exportálás Delphi-ben: RTF és HTML

Egy táblázat ügyfélnevek oszlopát tartalmazza. Némelyik kínaiul van, némelyik cirill betűkkel, néhány német umlautot vagy francia ékezetet hordoz. Exportálja CSV-be, megnyitja az eredményt, és minden karakter ép. Ugyanezt a munkafüzetet körlevél-sablonként (mail-merge template) RTF-be exportálja, megnyitja egy szövegszerkesztőben, és a nem-ASCII nevek kérdőjelek sorává omlottak össze. Az adatok soha nem változtak. Ami változott, az az Ön által írt formátum kódolási szerződése, és minden exportálási útvonal mást hordoz.

Ez az a csapda, amely elkapja azt a könyvtárat, amely a felszínen teljesen Unicode-tudatosnak tűnik. A cella szövege belsőleg WideString-ként van tárolva, így a modell soha nem veszít karaktert. A veszteség a határon történik, az íróban, amelynek szerializálnia kell a szöveget egy olyan formátumba, amelynek saját szabályai vannak arról, hogy mely bájtok engedélyezettek, és hogyan kell kódolni a megengedett tartományon kívüli dolgokat. Ha az egyik írót jól is készíti el, még mindig szállíthat egy másikat, amely megrongálja ugyanazt a szöveget. A javítás nem globális kapcsoló. Ez egy különálló, helyes döntés minden egyes útvonalon.

Az RTF tervezésénél fogva 7 bites biztonságú formátum

A Rich Text Format megelőzi a Unicode-ot, és úgy határozták meg, hogy túlélje azokat a transzportokat, amelyek csak nyomtatható ASCII-t továbbítanak. Egy RTF dokumentum deklarál egy kódlapot (code page) a fejlécében, és minden olyan karaktert, amelyet az író nem tud ábrázolni az adott kódlapon, escape-szekvenciaként kell kiadnia nyers bájt helyett. A vonatkozó escape a \u, amely egy előjeles 16 bites kódegységet hordoz, amelyet egy ASCII fallback karakter követ azon olvasók számára, amelyek túl régiek ahhoz, hogy egyáltalán megértsék az escape-et.

A HotXLS így írja az RTF-et. A dokumentumfejléc a kódlap deklarálásával nyit \ansi\ansicpg1252\uc1 formában, és az lxRTF egységben lévő író végigmegy minden karakterláncon, és a sima ASCII feletti összes karaktert \u escape-ként bocsátja ki, így a bájtfolyam 7 bites tiszta marad, függetlenül attól, hogy a deklarált kódlap mit képes tárolni. Egy olyan kódpont, mint az U+4E2D, a szó szerinti  3? szekvenciává válik, nem pedig nyers bájttá, amelyet a megjelenítő megpróbálna értelmezni a feltételezett kódlapon keresztül. Ezen fegyelem nélkül a deklarált kódlapon kívüli bármely dolognak nincs szabályos bájt-ábrázolása, és a nyers értéket kibocsátó író előállítja a cikk indításánál szereplő kérdőjeleket.

A szem előtt tartandó részlet az, hogy a deklarált kódlap és a menekülési szekvenciák (escapes) egyazon szerződés két felét alkotják. A kódlap önmagában való deklarálása nem segít a rajta kívül eső szövegeken. A menekülési szekvenciák kibocsátása deklarált kódlap nélkül kétértelművé teszi a fallback karaktereket. Mindkettőnek egyszerre kell helyesnek lennie, ezért bukik el az olyan író, amelyik csak az egyiket kezeli, már az első többnyelvű munkafüzetnél.

A HTML escape-elés többről szól, mint a kacsacsőrök

A HTML exportálás több lapból álló dokumentumot hoz létre, amelynek navigációs keretei (navigation frames) a lapneveket látható szövegként hordoznak. Ezek a nevek a szerző által vezérelt karakterláncok, amelyek bármilyen karaktert tartalmazhatnak, beleértve a jelölés szempontjából jelentős karaktereket is. Egy szó szerint Q1 & Q2 <draft> nevű lapnak escape-elt entitásokként kell elérnie az oldalt, különben a kacsacsőrök megnyitnak egy fantomtaget, az ampersand (&) pedig elindít egy nem tervezett entitáshivatkozást. Ez a szokásos HTML escape-elés, és ennek elhagyása egy keretcímkén olyan mulasztás, amely átmegy minden olyan teszten, amelyet csak ASCII-t tartalmazó lapnevekből építettek fel.

A kódolási kérdés egy szinttel ez alatt helyezkedik el. Ha a nem-ASCII karakterek olyan kontextusba kerülnek, ahol nem garantált az UTF-8 kiszolgálás, a biztonságos ábrázolás a numerikus karakterhivatkozás (numeric character reference), így az U+00E9 leírása é lesz, nem pedig olyan nyers bájt, amelynek jelentése a válasz karakterkészletétől (charset) függ. Ezen szabály tükörképe érvényes beolvasáskor. Az XLSX-ből visszaolvasott munkafüzet olyan megosztott karakterláncokat (shared strings) hordoz, amelyekben a karakter már numerikus XML entitásként lehet tárolva, és ezt az entitást egyetlen egész karakterré kell dekódolni, mielőtt belép a cellamodellbe. Ha figyelmetlenül dekódolja, szétosztva egy kódpontot különálló bájtokra, egyetlen karakter a mojibake (hibás kódolású szöveg) két darabjaként jelenik meg újra, amelyet semmilyen későbbi export nem tud helyreállítani.

Az XLSX konténer egy ZIP, és a ZIP-nek saját név-kódolása van

Az XLSX fájl egy ZIP archívum, and az archívum minden általa hordozott taghez tárol egy nevet. A ZIP elég régi ahhoz, hogy az eredeti specifikációja semmit se mondjon ezen nevek kódolásáról, így az olvasó, ha nem talál jelzést, az archívum helyi kódlapját feltételezi. Ez a feltételezés hibás abban a pillanatban, amint a tag neve nem-ASCII karaktert tartalmaz, ami előfordul a lokalizált munkalap-részneveknél és a beágyazott médiáknál, amelyek fájlnevei ékezeteket vagy nem-latin írásrendszert hordoznak.

A javítás egyetlen bit. Az egyes helyi fájlfejlécekben található 11-es általános célú bit (general-purpose bit 11) deklarálja, hogy a tag neve UTF-8 kódolású. A HotXLS pontosan ezt a bitet ellenőrzi az archívum olvasásakor, tesztelve az általános célú zászlókat a $0800 maszk ellenében, és a megjelenítő vagy író, amely figyelmen kívül hagyja ezt, félre fogja olvasni azt a nevet, amelyet a helyes megvalósítás UTF-8-ként tárolt. Ezt a bitet olcsó beállítani és olcsó tiszteletben tartani, és ez jelenti a teljes különbséget az olyan tagnév között, amely túléli a körutat, és az olyan között, amely már azelőtt megsérül, hogy a táblázat tartalma egyáltalán feldolgozásra kerülne.

A kis- és nagybetűk összevonása (case folding) és a számszkennelés ugyanazt a veszélyt rejti

A képlet-kiértékelés az a hely, ahol a Unicode-biztonság már nem a szerializációról, hanem az összehasonlításról szól. A SEARCH függvény nem érzékeny a kis- és nagybetűkre, ami azt jelenti, hogy össze kell vonnia (fold) a betűket, mielőtt keresné az alkarakterláncot. Az összevonás rossz módja az ANSI kódlapon keresztüli végrehajtás, mert a nem-ASCII szöveg ilyen módon történő nagybetűssé alakítása szűk kódlapon vezeti át a karaktereket, és megront mindent, ami azon kívül esik. A helyes út a wide-string nagybetűsítés, amely megőrzi a teljes UTF-16 tartományt. A HotXLS pontosan ezért az WideUpperCase segítségével vonja össze a betűket, így az ékezetes vagy nem-latin szövegek keresése ugyanazokra a karakterekre talál rá, amelyeket megadtak, nem pedig azok kódlap-csonkított közelítésére.

A képlet-tokenizáló (formula tokenizer) egy hasonló kötelezettséget hordoz, amelynek semmi köze a betűkhöz, és minden köze ahhoz van, ahol a token véget ér. Tudományos jelölés, mint a 1E3 vagy a 2.5E-3, egyetlen numerikus literál, és a szkennelőnek fel kell ismernie az E-t, az opcionális előjelet és a következő számjegyeket a szám részeként ahelyett, hogy a bemenetet egy névre és egy különálló számra bontaná. Az ezt hibásan kezelő szkennelő a tökéletesen érvényes konstanst elemzési hibává alakítja, vagy ami még rosszabb, egy csendben hibás kifejezéssé. Ez ugyanahhoz a témához tartozik, mert mindkét eset arról szól, hogy az olvasó helyes döntést hozzon karakterszinten: az egyik arról, hogyan vonja össze a karaktert az összehasonlításhoz, a másik pedig arról, hogy a karakter folytatja-e az aktuális tokent.

Többnyelvű munkafüzet felépítése és exportálása

A nyilvános API nem kéri Öntől, hogy ezeken gondolkodjon. A munkafüzetet WideString cellaértékekből építi fel, és meghívja a kívánt exportálási belépési pontot. A kódolási döntések az egyes írókon belül történnek meg. Az alábbi példa egy lapot több írásrendszerű szöveggel lát el, majd ugyanabból a munkafüzetből kiír egy RTF fájlt és egy HTML fájlt is, így a két útvonal azonos bemeneten fut le.

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;

Mindkét hívás Integer státuszt ad vissza, és mindkettő ugyanazt a memóriában lévő szöveget fogyasztja. A hívó kód semmit sem deklarál a kódlapról, és nem escape-eli a karaktereket, mert a felelősség az íróé, amely ismeri a saját formátumát. A munkafüzet szintű SaveAsCSV ugyanezt a mintát követi, ha elhatárolt (delimited) exportra van szüksége ugyanabból a forrásból.

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

A Unicode-biztonság útvonalankénti, nem könyvtárankénti jellemző

A megszívlelendő tanulság az, hogy nincs egyetlen hely, ahol Unicode-biztosak lehetünk. Az RTF-nek deklarált kódlapra és \u escape-ekre van szüksége. A HTML-nek entitás-escape-elésre van szüksége a jelölés szempontjából jelentős karaktereknél, és numerikus hivatkozásokra ott, ahol a karakterkészlet nem garantált, valamint a megosztott karakterláncokban érkező entitások helyes dekódolására. A ZIP konténernek a 11-es általános célú bit beállítására van szüksége, hogy a tag UTF-8 nevét UTF-8-ként olvassa be. A képlet-kiértékelésnek wide-string betűösszevonásra és olyan tokenizálóra van szüksége, amely egyben tartja a tudományos jelölést. Mindezek különböző szerződések, és a könyvtár teljesítheti az egyiket, miközben csendben megsérti a másikat. Ez az oka annak, hogy az a szoftver, amely helyesen írja a CSV-t, még mindig átadhat egy kérdőjelekkel teli RTF-et.

Ha az exportjai az elhatárolt formátumokra támaszkodnak, a közöttük lévő kompromisszumokat a CSV, TSV és HTML exportról szóló útmutatónk tárgyalja, és amikor a forrás egy eredményhalmaz a kézzel készített lap helyett, a Delphi jelentések adatbázis-exportálási mintái természetes módon párosulnak az itt leírt kódolási szabályokkal. Mindez a Delphi és C++Builder platformokhoz készült HotXLS Component részeként érhető el, a blogunkon máshol ismertetett olvasási, képlet- és formázási API-kkal együtt.