Tabuľka obsahuje stĺpec mien zákazníkov. Niektoré sú v čínštine, niektoré v azbuke, niekoľko ich obsahuje nemecké prehlásky alebo francúzsky dĺžeň. Exportujete ju do CSV, otvoríte výsledok a každý znak je neporušený. Exportujete rovnaký zošit do RTF pre šablónu hromadnej korešpondencie, otvoríte ho v textovom procesore a mená mimo znakov ASCII sa zrútili do radov otáznikov. Dáta sa nikdy nezmenili. To, čo sa zmenilo, je kódovacia zmluva formátu, ktorý ste zapísali, a každá exportná cesta nesie inú zmluvu.
Toto je pasca, ktorá zachytí knižnicu, ktorá navonok vyzerá plne prispôsobená pre Unicode. Text bunky je interne uchovávaný ako WideString, sože model nikdy nestratí znak. K strate dochádza na hranici, v zapisovači, ktorý musí tento text serializovať do formátu s jeho vlastnými pravidlami o tom, ktoré bajty sú legálne a ako sa musí zakódovať čokoľvek mimo legálneho rozsahu. Získajte jeden správny zapisovač a stále môžete dodať iný, ktorý rovnaký text znehodnotí. Riešením nie je globálny prepínač. Je to samostatné, správne rozhodnutie na každej ceste.
RTF je od návrhu 7-bitovo bezpečný formát
Formát Rich Text Format predchádza Unicode a bol špecifikovaný tak, aby prežil prenosy, ktoré prepúšťajú len tlačiteľné ASCII. Dokument RTF deklaruje kódovú stránku vo svojej hlavičke a akýkoľvek znak, ktorý zapisovač nedokáže v tejto kódovej stránke reprezentovať, musí byť emitovaný ako escape sekvencia, a nie ako surový bajt. Príslušným escape znakom je \u, ktorý nesie 16-bitovú kódovú jednotku so znamienkom, nasledovanú záložným znakom ASCII (ASCII fallback) pre čítačky príliš staré na to, aby escape sekvencii vôbec rozumeli.
HotXLS zapisuje RTF týmto spôsobom. Hlavička dokumentu začína deklarovaním kódovej stránky v tvare \ansi\ansicpg1252\uc1, a zapisovač v jednotke lxRTF prechádza každý reťazec, pričom akýkoľvek znak nad rámec obyčajného ASCII emituje ako escape sekvenciu \u, takže tok bajtov zostáva 7-bitovo čistý bez ohľadu na to, čo deklarovaná kódová stránka dokáže udržať. Kódový bod ako U+4E2D sa stáva doslovnou sekvencou 3?, nie surovým bajtom, ktorý by sa prehliadač potom pokúsil interpretovať prostredníctvom akejkoľvek kódovej stránky, ktorú náhodne predpokladal. Bez tejto disciplíny nemá čokoľvek mimo deklarovanej kódovej stránky žiadnu legálnu bajtovú reprezentáciu a zapisovač, ktorý vygeneruje surovú hodnotu, vytvorí otázniky, ktorými tento článok začal.
Detail, ktorý treba mať na pamäti, je, že deklarovaná kódová stránka a escape sekvencie sú dve polovice jednej zmluvy. Samotné deklarovanie kódovej stránky nepomôže textu, ktorý leží mimo nej. Generovanie escape sekvencií bez deklarovanej kódovej stránky ponecháva záložné znaky nejednoznačné. Obe musia byť správne spoločne, a preto zapisovač, ktorý rieši len jednu z nich, stále zlyhá na prvom viacjazyčnom zošite.
HTML escapovanie je o viac ako len lomených zátvorkách
Export do HTML vytvára viaclistový dokument, ktorého navigačné rámce nesú názvy hárkov ako viditeľný text. Tieto názvy sú reťazce riadené autorom, ktoré môžu obsahovať akýkoľvek znak vrátane tých, ktoré sú významné pre značkovanie. Hárok doslova pomenovaný Q1 & Q2 <draft> musí prísť na stránku ako escapované entity, inak lomené zátvorky otvoria fiktívnu značku a ampersand začne odkaz na entitu, ktorý nebol nikdy zamýšľaný. Toto je bežné escapovanie HTML, a jeho vynechanie na štítku rámca je presne tým typom opomenutia, ktoré prejde každým testom postaveným na názvoch hárkov obsahujúcich iba ASCII.
Otázka kódovania sedí o vrstvu nižšie. Keď znaky mimo ASCII skončia v kontexte, pri ktorom nie je zaručené, že bude poskytovaný ako UTF-8, bezpečnou reprezentáciou je číselná znaková referencia, takže U+00E9 sa zapíše ako é namiesto surového bajtu, ktorého význam závisí od znakovej sady odpovede. Zrkadlový obraz tohto pravidla platí pri vstupe. Zošit načítaný späť z XLSX nesie zdieľané reťazce, v ktorých môže byť znak už uložený ako číselná XML entita, a táto entita sa musí pred vstupom do modelu bunky dekódovať do jedného celého znaku. Dekódujte ju neopatrne, rozdelením kódového bodu na samostatné bajty, a jeden znak sa znova objaví ako dva kusy rozsypaného čaju (mojibake), ktoré už žiadny neskorší export nedokáže opraviť.
Kontajner XLSX je ZIP a ZIP má vlastné kódovanie názvov
Súbor XLSX je archív ZIP a archív ukladá názov pre každého člena, ktorého obsahuje. ZIP je dostatočne starý na to, aby jeho pôvodná špecifikácia nehovorila nič o kódovaní týchto názvov, takže čítačka, ktorá nenájde žiadny signál, predpokladá lokálnu kódovú stránku archívu. Tento predpoklad je nesprávny v momente, keď názov člena obsahuje znak mimo ASCII, čo sa stáva pri lokalizovaných názvoch častí hárkov a pri vložených médiách, ktorých názvy súborov nesú diakritiku alebo nelatinské písmo.
Opravou je jediný bit. Bit 11 na všeobecné účely (general-purpose bit 11) v každej lokálnej hlavičke súboru deklaruje, že názov člena je kódovaný ako UTF-8. HotXLS kontroluje presne tento bit pri čítaní archívu, pričom testuje príznaky na všeobecné účely voči maske $0800, a čítačka alebo zapisovač, ktorý ho ignoruje, nesprávne prečíta názov, ktorý správna implementácia uložila ako UTF-8. Nastavenie tohto bitu je lacné a jeho rešpektovanie tiež, a je to celý rozdiel medzi názvom člena, ktorý prežije spiatočnú cestu, a názvom, ktorý príde poškodený ešte pred samotnou analýzou obsahu tabuľky.
Prevod veľkosti písmen (case folding) a skenovanie čísel skrývajú rovnaké nebezpečenstvo
Vyhodnocovanie vzorcov je momentom, kedy bezpečnosť pre Unicode prestáva byť o serializácii a stáva sa o porovnávaní. Funkcia SEARCH nerozlišuje veľkosť písmen, čo znamená, že pred hľadaním podreťazca musí previesť veľkosť písmen. Nesprávnym spôsobom je prevod cez kódovú stránku ANSI, pretože prevod textu mimo ASCII na veľké písmená týmto spôsobom smeruje znaky cez úzku kódovú stránku a poškodzuje čokoľvek mimo nej. Správnou cestou je prevod na veľké písmená pomocou wide-string, ktorý zachováva celý rozsah UTF-16. HotXLS robí prevod pomocou WideUpperCase presne z tohto dôvodu, takže vyhľadávanie textu s diakritikou alebo nelatinského textu sa zhoduje s rovnakými znakmi, aké dostal, a nie s ich aproximáciou poškodenou kódovou stránkou.
Tokenizátor vzorcov nesie súvisiacu povinnosť, ktorá nemá nič spoločné s písmenami a všetko s tým, kde token končí. Vedecký zápis ako 1E3 alebo 2.5E-3 is jedným číselným literálom a skener musí rozpoznať E, voliteľné znamienko a nasledujúce číslice ako súčasť čísla, namiesto toho, aby vstup rozdelil na názov nasledovaný samostatným číslom. Skener, ktorý s týmto zaobchádza nesprávne, zmení úplne platnú konštantu na chybu analýzy alebo, čo je horšie, na ticho nesprávny výraz. Patrí to do rovnakej diskusie, pretože v oboch prípadoch ide o to, že čítačka robí správne rozhodnutie na úrovni znakov: jedno o tom, ako previesť znak na porovnanie, druhé o tom, či znak pokračuje v aktuálnom tokene.
Vytvorenie a export viacjazyčného zošita
Verejné API od vás nežiada, aby ste o čomkoľvek z tohto premýšľali. Zošit zostavíte z hodnôt buniek typu WideString a zavoláte požadovaný vstupný bod exportu. Rozhodnutia o kódovaní sa dejú vo vnútri každého zapisovača. Príklad nižšie napĺňa hárok textom v niekoľkých písmach a potom zapíše súbor RTF aj súbor HTML z rovnakého zošita, takže obe cesty bežia voči identickému vstupu.
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;
Obe volania vracajú status typu Integer a obe spotrebúvajú rovnaký text v pamäti. Nič vo volajúcom kóde nedeklaruje kódovú stránku ani neescapuje znak, pretože zodpovednosť leží na zapisovači, ktorý pozná svoj vlastný formát. Metóda SaveAsCSV na úrovni zošita má rovnaký tvar, ak potrebujete export s oddelenými hodnotami z identického zdroja.
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Bezpečnosť pre Unicode je na úroveň cesty, nie na úroveň knižnice
Ponaučením, ktoré si treba odniesť, je, že neexistuje jediné miesto, kde by sa dalo byť bezpečný pre Unicode. RTF potrebuje deklarovanú kódovú stránku plus escape sekvencie \u. HTML needs entity escaping for markup-significant characters and numeric references where the charset is not guaranteed, plus correct decoding of entities that arrive in shared strings. Kontajner ZIP potrebuje nastavený bit 11 na všeobecné účely, aby sa názov člena v UTF-8 prečítal ako UTF-8. Vyhodnocovanie vzorcov potrebuje wide-string prevod veľkosti písmen a tokenizátor, ktorý udrží vedecký zápis v jednom kuse. Každá z týchto vecí je inou zmluvou a knižnica môže splniť jednu, zatiaľ čo potichu porušuje druhú. To je dôvod, prečo nástroj, ktorý správne zvláda CSV, vám stále môže odovzdať RTF plné otáznikov.
Ak sa vaše exporty opierajú o formáty s oddeľovačmi, kompromisy medzi nimi sú opísané v našom sprievodcovi exportom do CSV, TSV a HTML, a keď je zdrojom množina výsledkov namiesto ručne vytvoreného hárku, vzory v exporte databáz pre reporty v Delphi sa prirodzene dopĺňajú s kódovacími pravidlami popísanými tu. To všetko sa dodáva ako súčasť HotXLS Component pre Delphi and C++Builder spolu s rozhraniami API pre čítanie, vzorce a formátovanie, ktoré sú popísané inde na tomto blogu.