Tehnički članak

Izvoz tabela na bezbedan način po Unicode u Delphi-ju: RTF i HTML

Tabela sadrži kolonu sa imenima kupaca. Neka su na kineskom, neka na ćirilici, nekoliko nosi nemačke umlaute ili francuski akcenat. Izvezete je u CSV i otvorite rezultat, i svaki znak je netaknut. Izvezete istu radnu svesku u RTF za šablon za spajanje pošte (mail-merge), otvorite je u programu za obradu teksta, i imena van ASCII skupa su se srušila u redove punih znakova pitanja. Podaci se nikada nisu promenili. Ono što se promenilo je ugovor o kodiranju formata u koji ste zapisivali, a svaka izvozna putanja nosi drugačiji

Ovo je zamka koja uhvati biblioteku koja na površini izgleda potpuno svesna Unicode-a. Tekst ćelije se interno čuva kao WideString, tako da model nikada ne gubi znak. Gubitak se dešava na granici, u pisaču koji mora da serijalizuje taj tekst u format sa sopstvenim pravilima o tome koji bajtovi su legalni i kako se mora kodirati sve što je van legalnog opsega. Uradite jednog pisača ispravno i i dalje možete isporučiti drugog koji unakazi isti tekst. Popravka nije globalni prekidač. To je odvojena, ispravna odluka na svakoj putanji

HotXLS Delphi piše jedan WideString ćelijski model kroz tri pisca: RTF backslash-u izbegavanja, HTML numeričke entitete, i naivnog pisca koji emituje znakove pitanja
Isti tekst u memoriji doseže disk kao 7-bitno čiste RTF escape sekvence ili HTML entitete zavisno od pisca. Pisac koji emituje sirove bajtove kroz pretpostavljenu kodnu stranicu je ono što proizvodi upitnike

RTF je po dizajnu bezbedan 7-bitni format

Rich Text Format prethodi Unicode-u i specifikovan je tako da preživi prenose koji propuštaju samo štampani ASCII. RTF dokument deklariše kodnu stranu u svom zaglavlju, a svaki znak koji pisac ne može da predstavi na toj kodnoj strani mora biti emitovan kao eskejp umesto sirovog bajta. Relevantni eskejp je \u, koji nosi potpisanu 16-bitnu kodnu jedinicu praćenu ASCII rezervnim znakom za čitače previše stare da bi uopšte razumeli eskejp

HotXLS zapisuje RTF na ovaj način. Zaglavlje dokumenta se otvara deklarisanjem kodne strane, u obliku \ansi\ansicpg1252\uc1, a pisac u jedinici lxRTF prolazi kroz svaki string emitujući svaki znak iznad običnog ASCII-ja kao eskejp \u, tako da tok bajtova ostaje 7-bitno čist bez obzira na to šta deklarisana kodna strana može da sadrži. Kodna tačka poput U+4E2D postaje doslovna sekvenca \u20013?, a ne sirovi bajt koji bi pregledač zatim pokušao da protumači kroz bilo koju kodnu stranu za koju je pretpostavio da se koristi. Bez te discipline, sve van deklarisane kodne strane nema legalnu bajtovsku reprezentaciju, a pisac koji emituje sirovu vrednost proizvodi znakove pitanja kojima je ovaj članak počeo

Detalj koji treba imati na umu je da deklarisana kodna strana i eskejpovi predstavljaju dve polovine jednog ugovora. Samo deklarisanje kodne strane ne pomaže tekstu koji leži van nje. Emitovanje eskejpova bez deklarisane kodne strane ostavlja rezervne znakove nejasnim. Oboje mora biti ispravno istovremeno, zbog čega pisac koji rukuje samo jednim od njih i dalje otkazuje na prvoj višejezičnoj radnoj svesci

HTML eskejpovanje je više od uglastih zagrada

HTML izvoz proizvodi dokument sa više listova čiji navigacioni okviri nose imena listova kao vidljiv tekst. Ta imena su stringovi koje kontroliše autor i koji mogu sadržati bilo koji znak, uključujući one značajne za markup. List doslovno nazvan Q1 & Q2 <draft> mora do stranice stići kao eskejpovani entiteti, inače će uglaste zagrade otvoriti fantomsku oznaku, a ampersand će početi referencu entiteta koja nikada nije bila nameravana. Ovo je obično HTML eskejpovanje, i preskakanje na oznaci okvira je vrsta propusta koja prolazi svaki test izgrađen od imena listova samo sa ASCII znakovima

Pitanje kodiranja se nalazi jedan sloj ispod toga. Kada znakovi van ASCII-ja upadnu u kontekst koji nije garantovano poslužen kao UTF-8, bezbedna reprezentacija je numerička referenca znaka, pa se U+00E9 piše kao é umesto kao sirovi bajt čije značenje zavisi od charseta odgovora. Ogledalski obrnuto ovo pravilo važi na ulazu. Radna sveska pročitana iz XLSX-a nosi deljene stringove u kojima znak može već biti sačuvan kao numerički XML entitet, a taj entitet mora biti dekodiran u jedan ceo znak pre nego što uđe u model ćelije. Dekodujte ga nepažljivo, razbijajući kodnu tačku na odvojene bajtove, i jedan znak se ponovo pojavi kao dva komada nerazumljivog teksta koje kasniji izvoz ne može da popravi

XLSX kontejner je ZIP, a ZIP ima sopstveno kodiranje imena

XLSX fajl je ZIP arhiva, a arhiva čuva ime za svakog člana koga sadrži. ZIP je dovoljno star da njegova originalna specifikacija nije rekla ništa o kodiranju tih imena, pa čitač koji ne pronađe signal pretpostavlja lokalnu kodnu stranu arhive. Ta pretpostavka je pogrešna u trenutku kada ime člana sadrži znak van ASCII-ja, što se dešava sa lokalizovanim imenima delova radnog lista i sa ugrađenim medijima čija imena fajlova nose akcente ili nelatinično pismo

Popravka je jedan jedini bit. Bit opšte namene 11 u svakom lokalnom zaglavlju fajla deklariše da je ime člana kodirano kao UTF-8. HotXLS proverava tačno taj bit kada čita arhivu, testirajući zastavice opšte namene naspram maske $0800, a čitač ili pisac koji ga ignoriše pogrešno će pročitati ime koje je ispravna implementacija sačuvala kao UTF-8. Bit je jeftin za postavljanje i jeftin za poštovanje, i predstavlja celu razliku između imena člana koje preživi put tamo i nazad i onog koje stigne oštećeno pre nego što je sadržaj radne sveske uopšte analiziran

Pretvaranje veličine slova i skeniranje brojeva kriju istu opasnost

Izračunavanje formula je mesto gde Unicode bezbednost prestaje da bude o serijalizaciji i postaje o poređenju. Funkcija SEARCH ne razlikuje velika i mala slova, što znači da mora da pretvori veličinu slova pre nego što potraži podniz. Pogrešan način pretvaranja je kroz ANSI kodnu stranu, jer pretvaranje teksta van ASCII-ja u velika slova na taj način usmerava znakove kroz usku kodnu stranu i kvari sve van nje. Ispravan način je pretvaranje širokih stringova, koje čuva ceo UTF-16 opseg. HotXLS pretvara pomoću WideUpperCase upravo iz tog razloga, tako da pretraga akcentovanog ili nelatiničnog teksta pronalazi iste znakove koji su joj dati, a ne aproksimaciju oštećenu kodnom stranom

Tokenizator formula nosi srodnu obavezu koja nema nikakve veze sa slovima, a svu vezu sa tim gde se token završava. Naučna notacija poput 1E3 ili 2.5E-3 je jedan numerički literal, a skener mora da prepozna E, opcioni znak, i cifre koje slede kao deo broja, umesto da razbije unos na ime praćeno odvojenim brojem. Skener koji ovo loše obrađuje pretvara potpuno validnu konstantu u grešku parsiranja ili, gore, u tiho pogrešan izraz. Ovo spada u istu raspravu jer se oba slučaja tiču čitača koji donosi ispravnu odluku na nivou znaka: jedna o tome kako pretvoriti znak radi poređenja, druga o tome da li znak nastavlja tekući token

Izgradnja i izvoz višejezične radne sveske

Javni API ne traži od vas da razmišljate ni o čemu od ovoga. Izgradite radnu svesku od vrednosti ćelija tipa WideString i pozovete ulaznu tačku izvoza koju želite. Odluke o kodiranju se dešavaju unutar svakog pisca. Primer ispod puni list tekstom na nekoliko pisama, a zatim iz iste radne sveske zapisuje i RTF fajl i HTML fajl, tako da obe putanje rade nad identičnim ulazom

HotXLS Delphi proverava ZIP opšte-namenski bit 11 naspram maske 0800 tako UTF-8 imena članova prežive dok čist bit pokvari ime kroz lokalnu kodnu stranicu
Opšte-namenski bit 11 je jedini signal da je ime člana sačuvano kao UTF-8. Čitači koji ignorišu masku vraćaju se na lokalnu kodnu stranicu i kvare ime pre nego što se bilo koji sadržaj lista raščlani
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';

    // Tekst ćelije se čuva kao WideString, tako da svako pismo preživi model.
    Sheet.Cells[2, 1].Value := '王伟';          // kineski
    Sheet.Cells[2, 2].Value := '北京';
    Sheet.Cells[3, 1].Value := 'Müller';        // nemački umlaut
    Sheet.Cells[3, 2].Value := 'Köln';
    Sheet.Cells[4, 1].Value := 'Иванов';        // ćirilica
    Sheet.Cells[4, 2].Value := 'Москва';
    Sheet.Cells[5, 1].Value := 'Désirée';       // francuski akcenti
    Sheet.Cells[5, 2].Value := 'Montréal';

    // RTF: pisac lxRTF deklariše kodnu stranu i emituje svaki
    // znak van ASCII-ja kao eskejp \u, čime fajl ostaje 7-bitno čist.
    Book.SaveAsRTF('Customers.rtf');

    // HTML: imena listova su HTML-eskejpovana, a tekst van ASCII-ja se zapisuje
    // tako da ne zavisi od pretpostavljenog charseta odgovora.
    Book.SaveAsHTML('Customers.html');
  finally
    Book := nil;
  end;
end;

Oba poziva vraćaju status tipa Integer, i oba obrađuju isti tekst u memoriji. Ništa u kodu koji poziva ne deklariše kodnu stranu niti eskejpuje znak, jer odgovornost leži na piscu koji poznaje sopstveni format. SaveAsCSV na nivou radne sveske prati isti oblik ako vam treba razgraničeni izvoz iz istog izvora

// Ista radna sveska, treća izvozna putanja sa sopstvenim pravilima kodiranja.
Book.SaveAsCSV('Customers.csv');

Unicode bezbednost je po putanji, ne po biblioteci

Pouka koju vredi poneti je da ne postoji jedno jedino mesto na kome treba biti bezbedan za Unicode. RTF-u treba deklarisana kodna strana plus eskejpovi \u. HTML-u treba eskejpovanje entiteta za znakove značajne za markup i numeričke reference tamo gde charset nije garantovan, plus ispravno dekodiranje entiteta koji stižu u deljenim stringovima. ZIP kontejneru treba postavljen bit opšte namene 11 kako bi se ime člana u UTF-8 čitalo kao UTF-8. Izračunavanju formula treba pretvaranje veličine slova širokih stringova i tokenizator koji drži naučnu notaciju u jednom komadu. Svako od ovoga je drugačiji ugovor, a biblioteka može zadovoljiti jedan dok tiho krši drugi. To je razlog zbog kog alat koji ispravno uradi CSV vam ipak može uručiti RTF pun znakova pitanja

HotXLS Delphi Unicode spisak: RTF izbegavanja, HTML entiteti, ZIP bit 11 i savijanje formula širokih stringova tvore četiri odvojena ugovora kodiranja
Svaka izvozna putanja nosi sopstveni ugovor kodiranja preko iste WideString radne sveske. Pogađanje jednog ne govori ništa o sledećem, zbog čega CSV uspeh može koegzistirati s RTF-om punim upitnika

Ako se vaši izvozi oslanjaju na razgraničene formate, kompromisi među njima su pokriveni u našem vodiču kroz izvoz CSV, TSV i HTML, a kada je izvor skup rezultata umesto ručno građenog lista, obrasci u izvozu baze podataka za Delphi izveštaje prirodno se uklapaju sa pravilima kodiranja opisanim ovde. Sve ovo je deo HotXLS Delphi Component-a za Delphi i C++Builder, uz API-je za čitanje, formule, i formatiranje pokrivene na drugim mestima na ovom blogu