Електронна таблица съдържа колона с имена на клиенти. Някои са на китайски, някои на кирилица, няколко носят немски умлаути или френско ударение. Експортирате я в CSV и отваряте резултата - всеки знак е непокътнат. Експортирате същата работна книга в RTF за шаблон за циркулярни писма (mail-merge), отваряте я в текстов редактор и не-ASCII имената са се свили в редове от въпросителни знаци. Данните никога не са се променяли. Това, което се е променило, е договорът за кодиране на формата, който сте записали, и всеки път за експортиране носи различен договор.
Това е капанът, който улавя библиотека, изглеждаща напълно съвместима с Unicode на повърхността. Текстът на клетката се съхранява вътрешно как WideString, так че моделът никога не губи знак. Загубата се случва на границата, в модула за запис (writer), който трябва да сериализира този текст във формат със собствени правила за това кои байтове са законни и как трябва да се кодира всичко извън законния диапазон. Направете един модул за запис правилен и пак можете да доставите друг, который разваля същия текст. Решението не е глобален превключвател. То е отделно, правилно решение за всеки път.
RTF е 7-битово безопасен формат по дизайн
Rich Text Format (RTF) предхожда Unicode и е специфициран така, че да оцелява при транспортиране, което пропуска само печатаем ASCII. RTF документ декларира кодова страница в заглавната си част и всеки знак, който модулът за запис не може да представи в тази кодова страница, трябва да бъде излъчен как екраниране (escape), а не как суров байт. Съответното екраниране е \\u, което носи 16-битова кодова единица със знак, последвана от ASCII резервен знак (fallback character) за четци, които са твърде стари, за да разберат екранирането.
HotXLS пише RTF по този начин. Заглавната част на документа се отваря с деклариране на кодовата страница под формата на \\ansi\\ansicpg1252\\uc1, а модулът за запис в модула lxRTF обхожда всеки низ, излъчвайки всеки знак над чистия ASCII как екраниране \\u, так че байтовият поток да остане 7-битово чист, независимо какво може да съдържа декларираната кодова страница. Кодова точка как U+4E2D се превръща в буквената последователност 3?, а не в суров байт, който след това четецът би се опитал да интерпретира през кодовата страница, която случайно е предположил. Без тази дисциплина всичко извън декларираната кодова страница няма законно байтово представяне, а модулът за запис, който излъчва суровата стойност, произвежда въпросителните знаци, с които започна тази статия.
Подробността, която трябва да се има предвид, е че декларираната кодова страница и екраниранията са две половини от един договор. Само декларирането на кодовата страница не помага на текст, който лежи извън нея. Излъчването на екранирания без декларирана кодова страница оставя резервните знаци двусмислени. И двете трябва да бъдат правилни заедно, поради което модулът за запис, който обработва само едно от тях, все още се проваля на първата многоезична работна книга.
Екранирането в HTML е нещо повече от ъглови скоби
Експортът към HTML произвежда документ с няколко листа, чиито навигационни рамки носят имената на листовете как видим текст. Тези имена са контролирани от автора низове, които могат да съдържат всеки знак, включително значимите за маркирането. Лист, буквално наречен Q1 & Q2 <draft>, трябва да достигне страницата как екранирани обекти (entities), иначе ъгловите скоби отварят фантомен таг, а амперсандът стартира препратка към обект, която никога не е била предвиждана. Това е обикновено екраниране на HTML и пропускането му върху етикет на рамка е вид пропускане, което преминава всеки тест, изграден с имена на листове само с ASCII знаци.
Въпросът с кодирането се намира едно ниво под това. Когато не-ASCII знаци попаднат в контекст, за който не е гарантирано, че се обслужва как UTF-8, безопасното представяне е числова референция на знак (numeric character reference), така че U+00E9 се записва как é, а не как суров байт, чието значение зависи от кодирането на отговора. Огледалният образ на това правило се прилага на входа. Работна книга, прочетена обратно от XLSX, носи споделени низове (shared strings), в които даден знак може вече да е съхранен как числов XML обект, и този обект трябва да бъде декодиран в един цял знак, преди да влезе в модела на клетката. Декодирайте го небрежно, разделяйки кодова точка на отделни байтове, и единият знак се появява отново как две парчета маймуница (mojibake), които никой по-късен експорт не може да поправи.
Контейнерът XLSX е ZIP, а ZIP има собствено кодиране на имена
XLSX файлът е ZIP архив, а архивът съхранява име за всеки член, който съдържа. ZIP е достатъчно стар, така че първоначалната му спецификация не казва нищо за кодирането на тези имена, така че четец, който не открие сигнал, приема локалната кодова страница на архива. Това предположение е погрешно в момента, в който името на член съдържа не-ASCII знак, което се случва с локализирани имена на части от работния лист и с вградени медии, чиито имена на файлове носят ударения или нелатинска писменост.
Решението е один бит. Бит 11 с общо предназначение във всяко заглавие на локален файл декларира, че името на члена е кодирано как UTF-8. HotXLS проверява точно този бит, когато чете архив, тествайки флаговете с общо предназначение срещу маската $0800, а четец или модул за запис, който го игнорира, ще прочете погрешно име, което правилна имплементация е съхранила как UTF-8. Битът е евтин за задаване и евтин за зачитане и той е цялата разлика между име на член, което оцелява при двупосочното пътуване, и такова, което пристига повредено, преди съдържанието на електронната таблица дори да бъде парсирано.
Сгъването на регистъра (case folding) и сканирането на числа крият една и съща опасност
Оценяването на формули е мястото, където безопасността на Unicode спира да се отнася до сериализацията и започва да се отнася до сравнението. Функция SEARCH не е чувствителна към регистъра (case-insensitive), което означава, че трябва да сгъне регистъра, преди да потърси подниз. Грешният начин за сгъване е през кодовата страница ANSI, защото превръщането на не-ASCII текст в главни букви по този начин пренасочва знаците през тясна кодова страница и поврежда всичко извън нея. Правилният начин е превръщането в главни букви на широк низ (wide-string uppercasing), което запазва пълния диапазон UTF-16. HotXLS сгъва с WideUpperCase точно поради тази причина, така че търсенето на текст с ударения или нелатински текст съвпада със същите знаци, които са му били дадени, а не с претърпяла срив в кодовата страница апроксимация.
Токенизаторът на формули (formula tokenizer) носи свързано задължение, което няма нищо общо с буквите и има всичко общо с това къде завършва токенът. Научна нотация как 1E3 или 2.5E-3 е един числов литерал и скенерът трябва да разпознае E, незадължителния знак и следващите цифри как част от числото, вместо да разбива входа на име, последвано от отделно число. Скенер, който се справя лошо с това, превръща напълно валидна константа в грешка при парсиране или, което е по-лошо, в тихо грешен израз. Тя принадлежи към същата дискусия, тъй като и двата случая са за четец, който взема правилно решение на ниво знак: едно за това как да сгъне знак за сравнение и друго за това дали знакът продължава текущия токен.
Изграждане и експортиране на многоезична работна книга
Публичното API не ви кара да мислите за нищо от това. Вие изграждате работната книга от WideString стойности на клетки и извиквате желаната входна точка за експорт. Решенията за кодиране се случват вътре във всеки модул за запис. Примерът по-долу зарежда лист с текст на няколко писмености, след което записва както RTF файл, така и HTML файл от същата работна книга, така че двата пътя работят срещу идентичен вход.
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;
И двете извиквания връщат статус Integer и двете консумират същия текст в паметта. Нищо в извикващия код не декларира кодова страница или не екранира знак, тъй като отговорността е на модула за запис, който познава собствения си формат. Поддържаната на ниво работна книга функция SaveAsCSV следва същата форма, ако се нуждаете от експорт с разделители от същия източник.
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Безопасността на Unicode е за всеки път поотделно, а не за цяла библиотека
Урокът, който си струва да се запомни, е че няма едно-единствено място, което да гарантира безопасност за Unicode. RTF се нуждае от декларирана кодова страница плюс екранирания \u. HTML се нуждае от екраниране на обекти за значимите за маркирането знаци и числови референции, когато кодирането не е гарантирано, плюс правилно декодиране на обекти, които пристигат в споделени низове. ZIP контейнерът се нуждае от зададен бит 11 с общо предназначение, така че име на член в UTF-8 да се чете как UTF-8. Оценяването на формули изисква сгъване на регистъра на широк низ и токенизатор, който запазва научната нотация като едно цяло. Всеки от тях е различен договор и библиотеката може да удовлетвори един, докато тихо нарушава друг. Това е причината инструмент, който обработва правилно CSV, все още да може да ви предаде RTF, пълен с въпросителни знаци.
Ако вашите експорти се опират на форматите с разделители, компромисите между тях са обхванати в нашето ръководство за експорт на CSV, TSV и HTML, а когато източникът е напредък от резултати, а не ръчно изграден лист, моделите в експорт от база данни за отчети в Delphi се съчетават естествено с описаните тук правила за кодиране. Всичко това се доставя как част от HotXLS Component за Delphi и C++Builder, заедно с API за четене, формули и форматиране, разгледани на други места в този блог.