Proračunska tablica sadrži stupac s imenima kupaca. Neka su na kineskom, neka na ćirilici, nekoliko ih nosi njemačke prijeglase (umlaut) ili francuski naglasak. Izvezete je u CSV i otvorite rezultat, i svaki je znak netaknut. Izvezete istu radnu knjigu u RTF za predložak spajanja pošte (mail-merge), otvorite je u programu za obradu teksta, a nazivi koji nisu ASCII pretvorili su se u redove upitnika. Podaci se nikada nisu promijenili. Ono što se promijenilo je ugovor o kodiranju formata koji ste napisali, a svaka staza izvoza nosi drugačiji ugovor.
To je zamka koja hvata knjižnicu koja na površini izgleda potpuno svjesna Unicodea. Tekst ćelije se interno drži kao WideString, tako da model nikada ne gubi znak. Gubitak se događa na granici, u piscu (writer) koji mora serijalizirati taj tekst u format s vlastitim pravilima o tome koji su bajtovi dopušteni i kako se mora kodirati sve što je izvan tog dopuštenog raspona. Ako ispravite jednog pisca, još uvijek možete isporučiti drugog koji kvari isti tekst. Rješenje nije globalni prekidač. To je zasebna, ispravna odluka na svakoj stazi.
RTF je po dizajnu format siguran za 7-bitni prijenos
Rich Text Format prethodi Unicodeu i specificiran je kako bi preživio prijenose koji propuštaju samo ispisni ASCII. RTF dokument deklarira kodnu stranicu u svom zaglavlju, a svaki znak koji pisac ne može prikazati u toj kodnoj stranici mora biti emitiran kao escape sekvenca, a ne kao sirovi bajt. Odgovarajuća escape sekvenca je \u, koja nosi predznačenu 16-bitnu kodnu jedinicu praćenu zamjenskim ASCII znakom (fallback character) za čitatelje koji su prestari da bi uopće razumjeli escape sekvencu.
HotXLS piše RTF na ovaj način. Zaglavlje dokumenta počinje deklariranjem kodne stranice, u obliku \ansi\ansicpg1252\uc1, a pisac u jedinici lxRTF prolazi kroz svaki niz znakova emitirajući svaki znak iznad običnog ASCII-ja kao \u escape sekvencu, tako da tok bajtova ostaje 7-bitno čist bez obzira na to što deklarirana kodna stranica može sadržavati. Kodna točka kao što je U+4E2D postaje doslovni niz 3?, a ne sirovi bajt koji bi preglednik zatim pokušao interpretirati kroz bilo koju kodnu stranicu koju pretpostavi. Bez te discipline, sve izvan deklarirane kodne stranice nema legalnu reprezentaciju bajtova, a pisac koji emitira sirovu vrijednost proizvodi upitnike s kojima je ovaj članak i započeo.
Detalj koji treba imati na umu jest da su deklarirana kodna stranica i escape sekvence dvije polovice jednog ugovora. Deklariranje same kodne stranice ne pomaže tekstu koji leži izvan nje. Emitiranje escape sekvenci bez deklarirane kodne stranice ostavlja zamjenske znakove dvosmislenima. Obje polovice moraju biti točne zajedno, zbog čega pisac koji obrađuje samo jednu od njih i dalje zakazuje na prvoj višejezičnoj radnoj knjizi.
HTML izbjegavanje (escaping) je više od šiljastih zagrada
Izvoz u HTML proizvodi dokument s više listova čiji navigacijski okviri nose nazive listova kao vidljivi tekst. Ti nazivi su nizovi znakova pod kontrolom autora koji mogu sadržavati bilo koji znak, uključujući one koji su značajni za oznake. List doslovno nazvan Q1 & Q2 <draft> mora stići na stranicu kao izbjegnuti entiteti (escaped entities), inače šiljaste zagrade otvaraju fantomsku oznaku, a ampersand započinje referencu entiteta koja nikada nije bila namjeravana. Ovo je obično HTML izbjegavanje, a preskakanje istog na oznaci okvira je vrsta propusta koja prolazi svaki test izgrađen od naziva listova koji sadrže samo ASCII.
Pitanje kodiranja sjedi jedan sloj ispod toga. Kada se znakovi koji nisu ASCII nađu u kontekstu za koji nije zajamčeno da će biti poslužen kao UTF-8, sigurna reprezentacija je numerička referenca znaka, pa se U+00E9 piše kao é, a ne kao sirovi bajt čije značenje ovisi o skupu znakova odgovora. Zrcalna slika ovog pravila primjenjuje se na ulazu. Radna knjiga pročitana natrag iz XLSX-a nosi zajedničke nizove znakova (shared strings) u kojima znak već može biti pohranjen kao numerički XML entitet, a taj se entitet mora dekodirati u jedan cijeli znak prije nego što uđe u model ćelije. Dekodirajte ga nepažljivo, dijeleći kodnu točku na zasebne bajtove, i jedan se znak ponovno pojavljuje kao dva komada mojibakea (iskrivljenog teksta) koje nikakav kasniji izvoz ne može popraviti.
XLSX spremnik je ZIP, a ZIP ima vlastito kodiranje naziva
XLSX datoteka je ZIP arhiva, a arhiva pohranjuje naziv za svakog člana kojeg sadrži. ZIP je dovoljno star da njegova izvorna specifikacija nije govorila ništa o kodiranju tih naziva, pa čitač koji ne pronađe signal pretpostavlja lokalnu kodnu stranicu arhive. Ta je pretpostavka pogrešna u trenutku kada naziv člana sadrži znak koji nije ASCII, što se događa s lokaliziranim nazivima dijelova radnog lista i s ugrađenim medijima čiji nazivi datoteka nose naglaske ili nelatinično pismo.
Rješenje je jedan bit. Bit 11 opće namjene (general-purpose bit 11) u svakom lokalnom zaglavlju datoteke izjavljuje da je naziv člana kodiran kao UTF-8. HotXLS provjerava točno taj bit kada čita arhivu, testirajući zastavice opće namjene protiv maske $0800, a čitač ili pisac koji ga zanemari pogrešno će pročitati naziv koji je ispravna implementacija pohranila kao UTF-8. Taj bit je jeftino postaviti i jeftino poštovati, i to je cijela razlika između naziva člana koji preživlava povratno putovanje i onog koji dolazi oštećen prije nego što se sadržaj proračunske tablice uopće analizira.
Pretvaranje veličine slova i skeniranje brojeva skrivaju istu opasnost
Evaluacija formula je mjesto gdje sigurnost Unicodea prestaje biti stvar serijalizacije i postaje stvar usporedbe. Funkcija SEARCH je neosjetljiva na velika i mala slova, što znači da mora izjednačiti veličinu slova (case folding) prije nego što traži podniz znakova. Pogrešan način je to učiniti kroz ANSI kodnu stranicu, jer pretvaranje teksta koji nije ASCII u velika slova na taj način usmjerava znakove kroz usku kodnu stranicu i kvari sve izvan nje. Ispravan način je pretvaranje u velika slova na razini širokog niza znakova (wide-string), što čuva puni UTF-16 raspon. HotXLS vrši izjednačavanje pomoću WideUpperCase upravo iz tog razloga, tako da pretraživanje naglašenog ili nelatiničnog teksta odgovara istim znakovima koji su mu zadani, a ne aproksimaciji iskrivljenoj kodnom stranicom.
Tokenizator formula nosi srodnu obvezu koja nema nikakve veze s slovima, već sa svime što ima veze s mjestom gdje token završava. Znanstveni zapis kao što je 1E3 ili 2.5E-3 je jedan numerički literal, a skener mora prepoznati E, neobavezni predznak i znamenke koje slijede kao dio broja, umjesto da razbije ulaz na naziv praćen zasebnim brojem. Skener koji time pogrešno rukuje pretvara savršeno valjanu konstantu u pogrešku parsiranja ili, još gore, u tiho pogrešan izraz. To pripada istoj raspravi jer se u oba slučaja radi o tome da čitač donosi ispravnu odluku na razini znaka: jednu o tome kako presaviti znak radi usporedbe, a drugu o tome nastavlja li znak trenutni token.
Izgradnja i izvoz višejezične radne knjige
Javni API ne traži od vas da razmišljate o bilo čemu od ovoga. Gradite radnu knjigu iz WideString vrijednosti ćelija i pozivate izvoznu točku koju želite. Odluke o kodiranju donose se unutar svakog pisca. Primjer u nastavku puni list tekstom na nekoliko pisama, a zatim piše i RTF datoteku i HTML datoteku iz iste radne knjige, tako da se obje staze izvode na identičnom ulazu.
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;
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Unicode sigurnost je po stazi, a ne po knjižnici
Pouka koju vrijedi ponijeti sa sobom jest da ne postoji jedno mjesto na kojem se postiže Unicode sigurnost. RTF treba deklariranu kodnu stranicu plus \u escape sekvence. HTML treba izbjegavanje entiteta (entity escaping) za znakove značajne za oznake i numeričke reference gdje skup znakova nije zajamčen, plus ispravno dekodiranje entiteta koji dolaze u zajedničkim nizovima znakova. ZIP spremnik treba postavljen bit 11 opće namjene kako bi se UTF-8 naziv člana čitao kao UTF-8. Evaluacija formula treba pretvaranje veličine slova širokog niza znakova i tokenizator koji drži znanstveni zapis u jednom komadu. Svaki od njih je drugačiji ugovor, a knjižnica može zadovoljiti jedan dok tiho krši drugi. To je razlog zašto vam alat koji ispravno rješava CSV i dalje može predati RTF pun upitnika.
Ako se vaši izvozi oslanjaju na razgraničene formate, kompromisi među njima pokriveni su u našem vodiču kroz CSV, TSV i HTML izvoz, a kada je izvor skup rezultata, a ne ručno izrađena tablica, uzorci u izvozu baze podataka za Delphi izvješća prirodno se uparuju s ovdje opisanim pravilima kodiranja. Sve se to isporučuje kao dio komponente HotXLS za Delphi i C++Builder, uz API-je za čitanje, formule i oblikovanje koji su pokriveni drugdje na ovom blogu.