Preglednica vsebuje stolpec z imeni strank. Nekatera so v kitajščini, nekatera v cirilici, nekaj jih nosi nemške preglase (umlauts) ali francoski naglas. Izvozite jo v CSV in odprete rezultat, in vsak znak je nedotaknjen. Isti delovni zvezek (workbook) izvozite v RTF za predlogo spajanja dokumentov (mail-merge template), ga odprete v urejevalniku besedil in imena, ki niso izključno ASCII, so se spremenila v vrstice vprašajev. Podatki se niso nikoli spremenili. Spremenila se je pogodba o kodiranju zapisa (encoding contract), ki ste ga zapisali, in vsaka izvozna pot vsebuje drugačno
To je past, v katero se ujame knjižnica, ki se na prvi pogled zdi popolnoma seznanjena z Unicodom. Besedilo celic se interno hrani kot WideString, zato model nikoli ne izgubi znaka. Izguba se zgodi na meji, v piscu (writer), ki mora serializirati (serialise) to besedilo v obliko z lastnimi pravili o tem, kateri bajti so dovoljeni (legal) in kako mora biti kodirano vse zunaj dovoljenega razpona. Če en pisec deluje pravilno, lahko še vedno ponudite drugega, ki popači (mangles) isto besedilo. Popravek ni globalno stikalo. To je ločena in pravilna odločitev na vsaki posamezni poti
RTF je po zasnovi varen 7-bitni zapis
Format Rich Text Format (RTF) je starejši od formata Unicode in je bil določen (specified) za preživetje prenosov, ki prenašajo le natisljiv ASCII. Dokument RTF v svoji glavi deklarira kodno stran (code page), in vsak znak, ki ga pisec (writer) ne more predstaviti v tej kodni strani, mora biti izpisan (emitted) kot ubežni znak (escape) in ne kot neobdelan bajt (raw byte). Relevanten ubežni znak je \u, ki nosi predznačeno (signed) 16-bitno kodno enoto (code unit), kateri sledi nadomestni znak (fallback character) v ASCII za bralnike, ki so prestari, da bi sploh razumeli ta ubežni znak
HotXLS piše dokumente RTF na ta način. Glava dokumenta se začne z deklaracijo kodne strani, v obliki \ansi\ansicpg1252\uc1, in pisec (writer) v enoti lxRTF prečka (walks) vsak niz, pri čemer oddaja (emitting) vsak znak nad navadnim znakovnim naborom ASCII kot ubežni znak \u, tako da tok bajtov ostane čisto 7-biten ne glede na to, kaj lahko drži deklarirana kodna stran. Kodna točka (code point), kot je U+4E2D, postane dobesedno (literal) zaporedje \u20013?, ne pa neobdelan bajt (raw byte), ki bi ga pregledovalnik nato poskušal interpretirati prek tiste kodne strani, za katero bi slučajno domneval, da jo ima pred sabo. Brez te discipline nima nič zunaj deklarirane kodne strani nobene veljavne bajtne predstavitve in pisec, ki zapiše (emits) surovo vrednost, ustvari vprašaje, zaradi katerih se je ta članek začel
Podrobnost, ki jo je treba upoštevati (keep in mind), je, da sta deklarirana kodna stran in ubežni znaki (escapes) dve polovici ene pogodbe. Sama izjava o kodni strani (declaring the code page) ne pomaga besedilu, ki leži zunaj nje. Zapisovanje (emitting) ubežnih znakov brez deklarirane kodne strani pa pusti nadomestne znake (fallback characters) dvoumne. Oboje mora biti pravilno skupaj, zato pisec (writer), ki obvlada samo enega od njiju, še vedno zgreši oziroma pade (fails) že na prvem večjezičnem delovnem zvezku
Pri dodajanju ubežnih znakov v HTML (HTML escaping) gre za več kot le lomljene oklepaje
Izvoz HTML ustvari dokument z več listi, katerega navigacijski okviri nosijo imena listov kot vidno besedilo. Ta imena so nizi, ki jih nadzoruje avtor in lahko vsebujejo kateri koli znak, vključno s tistimi, ki so pomembni za označevanje (markup-significant). List z dobesednim imenom (literally named) Q1 & Q2 <draft> mora priti do strani kot znak, ki ga zamenjajo ubežne entitete (escaped entities), sicer lomljeni oklepaji odprejo fantomsko oznako (phantom tag) in ampersand (znak &) začne referenco entitete, ki ni bila nikoli predvidena. To je navadno HTML preskakovanje oziroma preprečevanje z ubežnimi znaki, preskakovanje le-tega na oznaki okvirja (frame label) pa je takšna vrsta pomanjkljivosti, ki bo na testiranjih uspela v vsakem testu, zgrajenem iz imen listov, ki vsebujejo samo znake ASCII
Vprašanje kodiranja je eno plast (layer) pod tem. Ko znaki, ki niso iz nabora ASCII, pristanejo v kontekstu, za katerega ni zagotovljeno, da bo serviran (served) kot UTF-8, je varna predstavitev numerična referenca na znak (numeric character reference), torej je znak U+00E9 zapisan kot é namesto kot neobdelan bajt, katerega pomen je odvisen od vhodnega nabora znakov odgovora (response charset). Zrcalna slika tega pravila velja v nasprotni smeri, na poti navznoter. Delovni zvezek (workbook), bran nazaj iz XLSX, prenaša deljene nize (shared strings), v katerih je znak morda že shranjen kot numerična XML entiteta (numeric XML entity), in to entiteto je treba dekodirati v en celoten znak, preden vstopi v model celice. Če ga dekodirate neprevidno, tako, da kodo točke (code point) razdelite na ločene bajte, se en znak ponovno pojavi v dveh kosih kot popačeno besedilo oziroma mojibake, ki ga noben kasnejši izvoz (export) ne more popraviti
Kontejner XLSX je oblika ZIP in ZIP ima lastno kodiranje imen
Datoteka XLSX je arhiv (archive) ZIP in ta arhiv hrani ime za vsakega člana, ki ga vsebuje. ZIP je dovolj star, da njegova prvotna specifikacija ni povedala ničesar o kodiranju teh imen, zato bralnik, ki ne najde nobenega signala, privzame arhitekturo, v kateri gre za lokalno kodno stran tistega arhiva (local code page). Ta predpostavka je napačna takoj, ko ime člana (member name) vsebuje znak zunaj nabora ASCII, kar se zgodi z lokaliziranimi imeni delov delovnega lista in z vdelanimi predstavnostmi, katerih imena datotek nosijo naglase ali nelatinično pisavo
Rešitev oziroma popravek predstavlja zgolj en sam bit (single bit). Bit 11 za splošne namene v glavi vsake lokalne datoteke izjavi in določi, da je ime tistega člana (member name) kodirano kot UTF-8. HotXLS preveri natančno ta bit ob branju tistega arhiva, s tem da testira te zastavice splošnega namena (general-purpose flags) proti the mask oziroma maski $0800, bralnik (reader) ali pisec (writer), ki ignorira to določbo oziroma oznako zastavice (flag), pa si bo potem napačno razlagal ali pa napačno prebral ime, katero pa je bilo zaradi povsem the pravilne implementacije the shranjeno in obravnavano na kot (stored as) UTF-8. Nastavitev bita je poceni in prav tako ga je the poceni obravnavati ter spoštovati; to bit the prestavlja tisto in samo the popolno in glavno razliko (whole difference) the med na the in preteklosti in the ime oziroma iz the član the tistim (member name), za katerega se ohrani prihod do oziroma the z the med okvirom or z na za tistim s on the preživetjem okrogle (survives the round trip) the in z tistim the in tistim in the za tisto or the on on z enim, the (one that arrives corrupted before the spreadsheet content is even parsed)
Združevanje velikih/malih črk in skeniranje številk skrivata enako nevarnost
Vrednotenje formul je tisto področje, kjer varnost znakov (Unicode) preneha biti vprašanje serializacije in postane stvar primerjave. Funkcija SEARCH ne razlikuje med velikimi in malimi črkami (case-insensitive), kar pomeni, da mora izenačiti velikosti črk (fold case), preden išče podniz. Napačen način je izenačevanje s pomočjo kodne strani ANSI, ker se na ta način pri pretvorbi v velike črke za znake, ki niso iz nabora ASCII, uporabi ozka kodna stran, kar pa pokvari vse izven njenih meja. Pravilen način je spreminjanje v velike črke s širokim nizom (wide-string uppercasing), ki ohrani celoten razpon UTF-16. HotXLS se izenačuje in pretvarja z uporabo WideUpperCase natanko iz tega razloga, da iskanje naglašenega ali nelatinskega besedila najde iste znake, ki so mu bili dani, in ne neki približek (approximation), popačen s kodno stranjo
Razčlenjevalnik formul (formula tokenizer) ima sorodno obveznost, ki nima nobene zveze s črkami, temveč s tem, kje se žeton (token) konča. Znanstveni zapis, na primer 1E3 ali 2.5E-3, predstavlja en sam številčni literal, pri čemer mora optični bralnik oziroma skener (scanner) prepoznati E, opcijski predznak in števke, ki sledijo, kot del števila in ne kot razdelitev vnosa na ime, ki mu sledi ločena številka. Skener, ki s tem napačno ravna, spremeni popolnoma veljavno konstanto v napako pri razčlenjevanju ali, kar je še huje, v tiho in prikrito (silently) napačen izraz. Vse skupaj sodi v isto razpravo, saj gre v obeh primerih za to, da bralnik sprejme pravilno odločitev na ravni posameznega znaka (character-level decision): ena se nanaša na vprašanje, kako prilagoditi znak za primerjavo, druga pa na to, ali znak nadaljuje trenutni žeton
Izdelava in izvoz večjezičnega delovnega zvezka
Javni API (public API) od vas ne zahteva razmišljanja o ničemer od tega. Delovni zvezek (workbook) ustvarite iz vrednosti celic tipa WideString in samo pokličete izvozno vstopno točko, ki jo želite. Odločitve glede kodiranja se dogajajo znotraj vsakega modula za zapisovanje (writer). Spodnji primer (example below) zapolni list z besedilom iz več različnih pisav, nato pa zapiše tako datoteko RTF kot tudi datoteko HTML iz istega delovnega zvezka, tako da obe poti delujeta na osnovi povsem enakega vnosa (identical input)
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;
Oba klica vrneta status Integer in oba porabita isto besedilo iz pomnilnika (in-memory text). Nič v klicni kodi ne deklarira kodne strani ali vnaša ubežnih znakov, saj je ta odgovornost na piscu (writer), ki pozna lasten zapis oziroma format. Metoda za zapis na ravni celotnega zvezka (workbook-level) z imenom SaveAsCSV sledi enaki obliki, če potrebujete izvoz z ločili (delimited export) iz povsem enakega vira (identical source)
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Varnost kodiranja Unicode je določena po poti in ne po knjižnici
Lekcija, ki si jo velja zapomniti, je, da ni nobenega enotnega (single) mesta, kjer bi lahko bili varni glede predstavitve Unicode (Unicode-safe). Zapis RTF potrebuje deklarirano kodno stran plus ubežne znake \u. Zapis HTML potrebuje izogibanje s pomočjo entitet (entity escaping) za znake, pomembne za označevanje (markup-significant characters), in pa številčne reference (numeric references), kjer nabor znakov (charset) ni zajamčen, ter na drugi strani še pravilno dekodiranje takih entitet, ki prispejo znotraj oblike deljenih nizov. Vsebnik ZIP (ZIP container) zahteva nastavljen splošni bit 11 (general-purpose bit 11), tako da je UTF-8 ime samega člana prebrano (read) kot pravi UTF-8. Ovrednotenje formul (formula evaluation) potrebuje združevanje velikih/malih črk v načinu za široke nize in pa razčlenjevalnik oziroma (tokenizer), ki drži znanstveni zapis v enem kosu. Vsaka izmed teh stvari je popolnoma drugačna in različna pogodba (contract) in tako lahko knjižnica (library) izpolni eno, medtem ko tiho in neopazno (quietly) prekrši drugo. Zato vam lahko tisto enako orodje, ki povsem pravilno obravnava CSV, v roke potisne in vrne RTF (hand you an RTF), poln vprašajev
Če se vaši izvozi (exports) opirajo predvsem na ločene formate (delimited formats), so prednosti in slabosti vsakega (trade-offs) zajete in pokrite v našem vodniku po izvozih CSV, TSV in HTML; poleg tega in ko tisti vir ni (rather than) kakšen ročno zgrajen list (hand-built sheet), temveč kar nabor rezultatov (result set), se vzorci pri izvozu baze podatkov (database export) za poročila v orodju Delphi izjemno naravno (pair naturally) povezujejo s pravili o določanju kodiranja in zapisovanja (encoding rules), opisanimi tukaj. Vse opisano je na voljo kot del komponente HotXLS Component za Delphi in C++Builder, skupaj z API-ji za branje (reading), za obravnavo formul in formata, ki so drugje že obravnavani na tem istem blogu