Arkusz kalkulacyjny zawiera kolumnę z nazwiskami klientów. Niektóre są w języku chińskim, inne w cyrylicy, kilka ma niemieckie przegłosy (umlauty) lub francuski akcent. Eksportujesz to do CSV, otwierasz wynik i każdy znak jest nienaruszony. Eksportujesz ten sam skoroszyt do RTF jako szablon korespondencji seryjnej, otwierasz go w edytorze tekstu, a nazwy spoza ASCII zredukowały się do rzędów znaków zapytania. Dane nigdy się nie zmieniły. Zmieniła się umowa dotycząca kodowania zapisywanego formatu, a każda ścieżka eksportu ma inną
To pułapka, w którą wpada biblioteka wydająca się na pierwszy rzut oka w pełni obsługiwać Unicode. Tekst w komórce jest wewnętrznie przechowywany jako WideString, więc model nigdy nie traci żadnego znaku. Utrata następuje na granicy, w procedurze zapisującej (writerze), która musi zserializować ten tekst do formatu mającego własne reguły określające, które bajty są dozwolone i jak wszystko poza dozwolonym zakresem musi zostać zakodowane. Możesz poprawnie zaimplementować jednego writera, a i tak dostarczyć innego, który zniekształci ten sam tekst. Rozwiązaniem nie jest globalny przełącznik. To oddzielna, poprawna decyzja na każdej ze ścieżek
RTF jest z założenia formatem bezpiecznym w standardzie 7-bitowym
Format Rich Text Format powstał przed Unicode i został określony tak, aby przetrwać transmisje, które przepuszczają tylko drukowalne ASCII. Dokument RTF deklaruje stronę kodową w swoim nagłówku, a każdy znak, którego writer nie może w niej reprezentować, musi zostać wyemitowany jako sekwencja ucieczki (escape), a nie jako surowy bajt. Odpowiednią sekwencją ucieczki jest \u, która niesie ze sobą ze znakiem 16-bitową jednostkę kodu, po której następuje znak rezerwowy ASCII dla czytników zbyt starych, by w ogóle zrozumieć tę sekwencję
HotXLS zapisuje pliki RTF w ten sposób. Nagłówek dokumentu otwiera się zadeklarowaniem strony kodowej, w postaci \ansi\ansicpg1252\uc1, a writer w module lxRTF przechodzi przez każdy ciąg znaków, emitując każdy znak powyżej zwykłego ASCII jako sekwencję ucieczki \u, dzięki czemu strumień bajtów pozostaje 7-bitowo czysty, niezależnie od tego, co może pomieścić zadeklarowana strona kodowa. Punkt kodowy taki jak U+4E2D staje się dosłowną sekwencją \u20013?, a nie surowym bajtem, który przeglądarka próbowałaby następnie zinterpretować poprzez jakąkolwiek stronę kodową, jaką by założyła. Bez tej dyscypliny cokolwiek poza zadeklarowaną stroną kodową nie ma legalnej reprezentacji bajtowej, a writer, który emituje surową wartość, tworzy znaki zapytania, od których zaczął się ten artykuł
Szczegółem, o którym należy pamiętać, jest to, że zadeklarowana strona kodowa i sekwencje ucieczki to dwie połowy jednej umowy. Deklaracja samej strony kodowej nie pomaga tekstowi, który leży poza nią. Emitowanie sekwencji ucieczki bez zadeklarowanej strony kodowej pozostawia znaki rezerwowe w stanie dwuznacznym. Oba te elementy muszą być poprawne jednocześnie, dlatego writer, który obsługuje tylko jeden z nich, nadal zawodzi przy pierwszym wielojęzycznym skoroszycie
Ucieczka w HTML to coś więcej niż nawiasy ostre
Eksport do HTML tworzy dokument wieloarkuszowy, którego ramki nawigacyjne przenoszą nazwy arkuszy jako widoczny tekst. Nazwy te są ciągami znaków kontrolowanymi przez autora, które mogą zawierać dowolny znak, w tym te znaczące dla znaczników. Arkusz nazwany dosłownie Q1 & Q2 <draft> musi dotrzeć na stronę jako jednostki (encje) chronione znakami ucieczki, w przeciwnym razie nawiasy ostre otwierają fałszywy tag, a znak ampersand rozpoczyna odniesienie do encji, które nigdy nie było zamierzone. Jest to zwykłe kodowanie znaków ucieczki w HTML, a pominięcie go na etykiecie ramki jest rodzajem przeoczenia, które przechodzi przez każdy test zbudowany na nazwach arkuszy zawierających tylko ASCII
Problem kodowania znajduje się jedną warstwę poniżej. Kiedy znaki spoza zakresu ASCII trafią do kontekstu, który nie ma gwarancji obsługi jako UTF-8, bezpieczną reprezentacją jest odwołanie do znaku numerycznego, więc U+00E9 jest zapisywane jako é zamiast surowego bajtu, którego znaczenie zależy od zestawu znaków odpowiedzi. Lustrzane odbicie tej reguły ma zastosowanie w drugą stronę. Skoroszyt wczytany z powrotem z XLSX zawiera udostępnione (współdzielone) ciągi znaków, w których znak może być już przechowywany jako encja numeryczna XML, a encja ta musi zostać zdekodowana do jednego całego znaku, zanim wejdzie do modelu komórki. Zdekoduj to niedbale, dzieląc punkt kodowy na oddzielne bajty, a pojedynczy znak pojawi się ponownie jako dwa kawałki „krzaków” (mojibake), których żaden późniejszy eksport nie będzie w stanie naprawić
Kontener XLSX to ZIP, a ZIP ma własne kodowanie nazw
Plik XLSX jest archiwum ZIP, a archiwum przechowuje nazwę dla każdego składnika, który zawiera. Format ZIP jest na tyle stary, że jego oryginalna specyfikacja nie mówiła nic o kodowaniu tych nazw, więc czytnik, który nie znajduje żadnego sygnału, przyjmuje lokalną stronę kodową archiwum. Takie założenie jest błędne w momencie, gdy nazwa członka zawiera znak spoza tabeli ASCII, co ma miejsce w przypadku zlokalizowanych nazw części arkuszy roboczych i osadzonych multimediów, których nazwy plików zawierają akcenty lub pismo niełacińskie
Rozwiązaniem jest pojedynczy bit. Bit 11 ogólnego przeznaczenia w każdym lokalnym nagłówku pliku deklaruje, że nazwa składnika jest zakodowana w UTF-8. HotXLS sprawdza dokładnie ten bit podczas odczytu archiwum, testując flagi ogólnego przeznaczenia za pomocą maski $0800, a czytnik lub writer, który to zignoruje, błędnie odczyta nazwę, którą poprawna implementacja zapisała jako UTF-8. Bit jest łatwy do ustawienia i łatwy do zinterpretowania, a stanowi całkowitą różnicę między nazwą członka, która przetrwa tę podróż w obie strony, a taką, która dotrze uszkodzona, zanim zawartość arkusza kalkulacyjnego zostanie w ogóle przeanalizowana
Składanie wielkości liter (case folding) i skanowanie liczb kryją to samo zagrożenie
Wykonywanie formuł to miejsce, w którym bezpieczeństwo Unicode przestaje dotyczyć serializacji, a zaczyna dotyczyć porównań. Funkcja SEARCH (SZUKAJ.TEKST) nie rozróżnia wielkości liter, co oznacza, że musi ujednolicić wielkość liter przed wyszukaniem podciągu (case folding). Złym sposobem ujednolicania jest robienie tego przez stronę kodową ANSI, ponieważ zmiana liter na wielkie tekstu spoza ASCII w ten sposób kieruje znaki przez wąską stronę kodową i uszkadza wszystko poza nią. Właściwym sposobem jest konwersja na wielkie litery za pomocą tzw. wide-string (szerokiego ciągu znaków), co pozwala zachować pełen zakres UTF-16. HotXLS ujednolica z użyciem WideUpperCase właśnie z tego powodu, dzięki czemu wyszukiwanie tekstu z akcentami lub znakami niełacińskimi pasuje do tych samych znaków, które podano, a nie do ich zniekształconego przez stronę kodową przybliżenia
Tokenizer formuł niesie ze sobą podobne zobowiązanie, które nie ma nic wspólnego z literami, ale ściśle wiąże się z miejscem zakończenia tokenu. Notacja naukowa, taka jak 1E3 lub 2.5E-3 to pojedynczy literał liczbowy, a skaner musi rozpoznać E, opcjonalny znak oraz kolejne cyfry jako część liczby, zamiast rozbijać wejście na nazwę, po której następuje oddzielna liczba. Skaner, który błędnie to obsłuży, zamienia całkowicie poprawną stałą w błąd przetwarzania lub, co gorsza, w cicho działające, ale błędne wyrażenie. Należy to do tej samej dyskusji, ponieważ oba przypadki dotyczą podejmowania przez czytnik poprawnej decyzji na poziomie znaku: po pierwsze, jak zamienić wielkość znaków do porównania, a po drugie, czy znak kontynuuje bieżący token
Budowanie i eksport wielojęzycznego skoroszytu
Publiczne API nie wymaga od ciebie myślenia o niczym z tych rzeczy. Budujesz skoroszyt z wartości komórek WideString i wywołujesz żądany punkt wejścia dla eksportu. Decyzje dotyczące kodowania zapadają wewnątrz każdego writera. Poniższy przykład wypełnia arkusz tekstem w kilku różnych systemach pisma, a następnie zapisuje zarówno plik RTF, jak i HTML z tego samego skoroszytu, dzięki czemu obie ścieżki działają na identycznych danych wejściowych
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 wywołania zwracają status typu Integer i oba korzystają z tego samego tekstu w pamięci. Nic w kodzie wywołującym nie deklaruje strony kodowej ani nie koduje znaku w ramach ucieczki, ponieważ ta odpowiedzialność spoczywa na writerze, który zna swój własny format. Eksport SaveAsCSV na poziomie skoroszytu odbywa się w identyczny sposób, jeśli potrzebujesz eksportu rozdzielanego znakami z identycznego źródła
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Bezpieczeństwo Unicode dotyczy konkretnej ścieżki eksportu, a nie całej biblioteki
Wnioskiem, który warto wyciągnąć, jest to, że nie ma jednego miejsca, w którym zapewnia się bezpieczeństwo Unicode. RTF potrzebuje zadeklarowanej strony kodowej i sekwencji ucieczek \u. HTML potrzebuje encji do ucieczki znaków znaczących dla znaczników oraz numerycznych odniesień tam, gdzie zestaw znaków nie jest gwarantowany, a także poprawnego dekodowania encji, które przychodzą w udostępnionych ciągach znaków. Kontener ZIP potrzebuje ustawienia bitu 11 ogólnego przeznaczenia, by nazwa członka UTF-8 była odczytywana jako UTF-8. Wykonywanie formuł wymaga konwersji znaków do ujednolicenia wielkości liter (case folding) w trybie wide-string i tokenizera, który traktuje notację naukową jako całość. Każda z tych rzeczy jest inną umową, a biblioteka może spełnić jedną, jednocześnie po cichu łamiąc drugą. Z tego właśnie powodu narzędzie, które prawidłowo eksportuje pliki CSV, nadal może zaserwować ci RTF pełen znaków zapytania
Jeśli Twoje eksporty opierają się na formatach rozdzielanych, kompromisy między nimi zostały omówione w naszym przewodniku po eksporcie CSV, TSV i HTML, a kiedy źródłem jest zestaw wyników, a nie ręcznie stworzony arkusz, wzorce w sekcji eksport bazy danych dla raportów Delphi naturalnie łączą się z zasadami kodowania opisanymi tutaj. Wszystko to jest dostarczane jako część komponentu HotXLS Component dla Delphi i C++Builder, obok interfejsów API do czytania, formuł i formatowania opisanych w innych miejscach na tym blogu