Електронна таблица съдържа колона с имена на клиенти. Някои са на китайски, някои на кирилица, няколко носят немски умлаути или френски акцент. Вие я експортирате в CSV и отваряте резултата, и всеки знак е непокътнат. Експортирате същата работна книга в RTF за шаблон за циркулярни писма (mail-merge template), отваряте я в текстообработваща програма, и имената извън ASCII (non-ASCII names) са се сринали в редове от въпросителни знаци. Данните никога не са се променяли. Това, което се е променило, е договорът за кодиране (encoding contract) на формата, който сте написали, и всеки път за експорт (export path) носи различен такъв
Това е капанът, който улавя библиотека, която изглежда напълно Unicode-съвместима (Unicode-aware) на повърхността. Текстът на клетката се държи вътрешно като WideString, така че моделът никога не губи знак. Загубата се случва на границата, в писателя (writer), който трябва да сериализира този текст във формат със свои собствени правила относно това кои байтове са легални и как всичко извън легалния диапазон трябва да бъде кодирано. Направете един писател правилен и все още можете да доставите друг, който осакатява (mangles) същия текст. Поправката не е глобален превключвател (global switch). Тя е отделно, правилно решение за всеки път
RTF е 7-битово безопасен формат по дизайн
Rich Text Format предхожда Unicode и беше специфициран да преживява транспорти, които пропускат само печатаем ASCII (printable ASCII). Един RTF документ декларира кодова страница (code page) в своя хедър и всеки знак, който писателят не може да представи в тази кодова страница, трябва да бъде излъчен като ескейп (escape), а не като суров байт. Подходящият ескейп е \u, който носи кодова единица (code unit) със знак 16 бита, последвана от ASCII резервен знак (fallback character) за четци, които са твърде стари, за да разберат ескейпа изобщо
HotXLS пише RTF по този начин. Хедърът на документа започва, като декларира кодовата страница във формата \ansi\ansicpg1252\uc1, и писателят в модула lxRTF обхожда всеки низ, излъчвайки всеки знак над обикновения ASCII като ескейп \u, така че байтовият поток остава 7-битово чист (7-bit clean), независимо какво може да побере декларираната кодова страница. Кодова точка като U+4E2D става буквалната последователност \u20013?, а не суров байт, който програма за преглед след това би се опитала да интерпретира чрез каквато кодова страница случайно е предположила. Без тази дисциплина всичко извън декларираната кодова страница няма легално байтово представяне, и писател, който излъчва суровата стойност, произвежда въпросителните знаци, които започнаха тази статия
Детайлът, който трябва да имате предвид, е че декларираната кодова страница и ескейпите са две половини на един договор (contract). Декларирането само на кодовата страница не помага на текст, който лежи извън нея. Излъчването на ескейпи без декларирана кодова страница оставя резервните знаци двусмислени. И двете трябва да са правилни заедно, поради което писател, който се справя само с едното от тях, все пак се проваля при първата многоезична работна книга
HTML ескейпването (escaping) е нещо повече от ъглови скоби
HTML експортът произвежда многолистов документ, чиито навигационни рамки (navigation frames) носят имената на листовете като видим текст. Тези имена са контролирани от автора низове, които могат да съдържат всеки знак, включително тези със значение за маркировката (markup-significant). Лист, буквално наречен Q1 & Q2 <draft>, трябва да достигне страницата като ескейпнати обекти (escaped entities), или ъгловите скоби отварят фантомен таг, а амперсандът стартира препратка към обект (entity reference), която никога не е била предвидена. Това е обикновено HTML ескейпване, и пропускането му върху етикет на рамка е видът пропуск, който преминава всеки тест, изграден от имена на листове само с ASCII
Въпросът за кодирането седи един слой под това. Когато знаци извън ASCII попаднат в контекст, за който не е гарантирано да бъде сервиран като UTF-8, безопасното представяне е числова препратка към знак (numeric character reference), така че U+00E9 се записва като é, а не като суров байт, чието значение зависи от charset-а на отговора. Огледалният образ на това правило важи по пътя навътре. Работна книга, прочетена обратно от XLSX, носи споделени низове (shared strings), в които даден знак може вече да се съхранява като числов XML обект, и този обект трябва да бъде декодиран в един цял знак, преди да влезе в модела на клетката. Декодирайте го небрежно, разделяйки кодова точка на отделни байтове, и един-единствен знак се появява отново като две парчета mojibake, които никой последващ експорт не може да поправи
Контейнерът XLSX е ZIP и ZIP има свое собствено кодиране на имената
XLSX файлът е ZIP архив и архивът съхранява име за всеки член (member), който държи. ZIP е достатъчно стар, че оригиналната му спецификация не казва нищо за кодирането на тези имена, така че четец, който не намира сигнал, приема локалната кодова страница на архива. Това предположение е погрешно в мига, в който име на член съдържа знак извън ASCII, което се случва с локализирани имена на части на работен лист и с вградена медия, чиито имена на файлове носят акценти или нелатински скрипт
Поправката е един-единствен бит (single bit). Бит с общо предназначение 11 (General-purpose bit 11) във всеки локален хедър на файл декларира, че името на члена е кодирано като UTF-8. HotXLS проверява точно този бит, когато чете архив, тествайки флаговете с общо предназначение срещу маската $0800, и четец или писател, който го игнорира, ще прочете грешно име, което правилна имплементация е съхранила като UTF-8. Битът е евтин за задаване и евтин за спазване, и той е цялата разлика между име на член, което преживява пътуването напред-назад (round trip), и такова, което пристига повредено, преди съдържанието на електронната таблица изобщо да бъде парснато
Case folding и сканирането на числа крият същата опасност
Оценяването на формули е мястото, където Unicode безопасността спира да се отнася до сериализация и започва да се отнася до сравнение. Функцията SEARCH не прави разлика между главни и малки букви (case-insensitive), което означава, че трябва да сгъне регистъра (fold case), преди да търси подниз. Грешният начин за сгъване е чрез ANSI кодовата страница, защото правенето на главни букви на текст извън ASCII по този начин маршрутизира знаците през тясна кодова страница и поврежда всичко извън нея. Правилният начин е uppercasing на широки низове (wide-string uppercasing), което запазва пълния диапазон UTF-16. HotXLS сгъва (folds) с WideUpperCase точно поради тази причина, така че търсене на акцентиран или нелатински текст съвпада със същите знаци, които са му дадени, а не с някакво осакатено от кодова страница приближение до тях
Токенизаторът на формули (formula tokenizer) носи свързано задължение, което няма нищо общо с буквите и всичко общо с това къде свършва даден токен. Научна нотация като 1E3 или 2.5E-3 е един-единствен цифров литерал, и скенерът трябва да разпознае E, незадължителен знак и следващите цифри като част от числото, вместо да разделя входа на име, последвано от отделно число. Скенер, който се справя лошо с това, превръща перфектно валидна константа в грешка при парсване или, още по-лошо, в тихо грешен израз. Това принадлежи към същата дискусия, защото и двата случая са свързани с това четецът да вземе правилно решение на ниво знак: едно относно това как да се сгъне знак за сравнение, другото относно това дали даден знак продължава текущия токен
Изграждане и експортиране на многоезична работна книга
Публичното API не иска от вас да мислите за нищо от това. Вие изграждате работната книга от стойности в клетките WideString и извиквате входната точка за експорт (export entry point), която искате. Решенията за кодиране се случват вътре във всеки писател. Примерът по-долу зарежда (seeds) лист с текст в няколко скрипта, след което записва както 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 се нуждае от ескейпване на обекти (entity escaping) за знаци със значение за маркировката и числови препратки, където charset не е гарантиран, плюс правилно декодиране на обекти, които пристигат в споделени низове. Контейнерът ZIP се нуждае от зададен бит 11 с общо предназначение, така че име на член в UTF-8 да се чете като UTF-8. Оценяването на формули се нуждае от case folding за широки низове и токенизатор, който запазва научната нотация цяла. Всяко от тези е различен договор и една библиотека може да удовлетвори един, докато тихо нарушава друг. Това е причината инструмент, който прави CSV правилно, все пак да може да ви връчи RTF пълен с въпросителни знаци
Ако вашите експорти разчитат на формати с разделители, компромисите (trade-offs) между тях са обхванати в нашето ръководство за експорт в CSV, TSV и HTML, а когато източникът е набор от резултати, а не ръчно изграден лист, моделите в експорт на база данни за Delphi отчети се съчетават естествено с правилата за кодиране, описани тук. Всичко това се доставя като част от компонента HotXLS за Delphi и C++Builder, заедно с API-тата за четене, формули и форматиране, обхванати на други места в този блог