Електронна таблиця містить стовпець з іменами клієнтів. Деякі з них записані китайською мовою, деякі — кирилицею, кілька містять німецькі умлаути чи французькі акценти. Ви експортуєте її в CSV, відкриваєте результат — і кожен символ залишається неушкодженим. Ви експортуєте ту саму книгу в RTF для шаблону злиття пошти, відкриваєте її в текстовому процесорі й бачите, що всі не-ASCII імена перетворилися на рядки зі знаками питання. Самі дані ніколи не змінювалися. Змінилися правила кодування формату, в який ви робили запис, і кожен шлях експорту має свої власні правила.
Це пастка, в яку потрапляють бібліотеки, які на перший погляд повністю підтримують Unicode. Текст клітинки зберігається всередині як тип WideString, тому модель даних ніколи не втрачає символи. Втрата відбувається на межі, у модулі запису, який має серіалізувати цей текст у формат із власними правилами щодо дозволених байтів та кодування всього, що виходить за межі цього діапазону. Якщо налаштувати правильно один модуль запису, інший усе одно може спотворювати той самий текст. Вирішенням проблеми є не глобальний перемикач, а окреме правильне рішення для кожного шляху експорту.
RTF за дизайном є 7-бітно безпечним форматом
Формат Rich Text Format (RTF) з'явився ще до виникнення Unicode та був розроблений для успішної передачі через канали, які підтримують лише друковані символи ASCII. Документ RTF оголошує кодову сторінку у своєму заголовку, і будь-який символ, який модуль запису не може представити в цій кодовій сторінці, має бути виведений у вигляді керівної послідовності (escape-послідовності), а не звичайного байта. Відповідною послідовністю є \u, яка містить знакове 16-бітове кодове значення, за яким слідує резервний символ ASCII для застарілих програм читання, які взагалі не розуміють ці керівні послідовності.
HotXLS записує файли RTF саме так. Заголовок документа відкривається оголошенням кодової сторінки у форматі \ansi\ansicpg1252\uc1, а модуль запису в модулі lxRTF обходить кожен рядок, виводячи будь-який символ за межами звичайного ASCII як керівну послідовність \u, тому потік байтів залишається 7-бітовим незалежно від можливостей оголошеної кодової сторінки. Наприклад, кодова точка U+4E2D стає послідовністю символів \u20013?, а не звичайним байтом, який програма перегляду намагалася б інтерпретувати через кодову сторінку за замовчуванням. Без цього правила будь-який символ за межами оголошеної кодової сторінки не матиме коректного байтового представлення, і модуль запису, виводячи вихідне значення, призведе до появи знаків питання, про які йшлося на початку статті.
Важливо пам'ятати, що оголошена кодова сторінка та керівні послідовності — це дві частини єдиного механізму. Одне лише оголошення кодової сторінки не допоможе тексту, який виходить за її межі. Виведення послідовностей без оголошення кодової сторінки робить резервні символи неоднозначними. Обидві частини мають бути реалізовані правильно, саме тому модуль запису, який підтримує лише одну з них, усе одно не впорається з першою ж багатомовною книгою таблиць.
Екранування HTML — це більше, ніж просто кутові дужки
Експорт в HTML створює документ із багатьма аркушами, навігаційні фрейми якого містять назви аркушів як видимий текст. Ці назви є рядками, які створює користувач і які можуть містити будь-які символи, включаючи значущі для розмітки. Аркуш із назвою Q1 & Q2 <draft> має потрапити на сторінку у вигляді екранованих сутностей, інакше кутові дужки відкриють вигаданий тег, а амперсанд почне посилання на сутність, якого не планувалося. Це звичайне екранування HTML, і його пропуск у назві фрейму є тією помилкою, яку неможливо виявити тестами із назвами аркушів лише на ASCII.
Питання кодування лежить на один рівень нижче. Коли символи, що не входять до ASCII, потрапляють у середовище, де не гарантується використання кодування UTF-8, безпечним представленням є числовий код символу, тому U+00E9 записується як é, а не як звичайний байт, значення якого залежить від кодової сторінки відповіді. Зворотне правило застосовується при читанні. Книга таблиць, зчитана назад із XLSX, містить спільні рядки, в яких символ вже може зберігатися як числова сутність XML, і ця сутність має бути декодована в один цілісний символ перед внесенням до моделі клітинки. Декодування її без належної уваги із розділенням кодової точки на окремі байти призведе до того, що один символ перетвориться на фрагмент зіпсованого кодування (модзібаке), який жоден подальший експорт не зможе виправити.
Контейнер XLSX — це ZIP, а ZIP має власне кодування імен
Файл XLSX є архівом ZIP, і цей архів зберігає ім'я для кожного елемента, який він містить. Формат ZIP є досить старим, тому його оригінальна специфікація нічого не говорила про кодування цих імен; відповідно, програма читання, не знаходячи сигнальних бітів, використовує локальну кодову сторінку системи. Це припущення виявляється помилковим, як тільки ім'я елемента містить не-ASCII символ, що часто трапляється з локалізованими назвами частин робочих аркушів або вбудованими медіафайлами, імена яких містять діакритичні знаки або нелатинські літери.
Вирішенням є один біт. 11-й біт загального призначення у заголовку кожного локального файлу вказує на те, що ім'я елемента закодоване як UTF-8. HotXLS перевіряє саме цей біт при читанні архіву, аналізуючи прапорці загального призначення за допомогою маски $0800; програма читання або запису, яка ігнорує його, неправильно прочитає ім'я, яке правильна реалізація зберегла як UTF-8. Встановлення та обробка цього біта є простими операціями, і це визначає, чи пройде ім'я елемента весь цей шлях без помилок, чи буде пошкоджене ще до початку парсингу вмісту електронної таблиці.
Перетворення регістру та сканування чисел приховують однакову небезпеку
Обчислення формул — це сфера, де безпека Unicode перестає стосуватися серіалізації та переходить до порівняння значень. Функція SEARCH є нечутливою до регістру, що означає необхідність приведення до одного регістру перед пошуком підрядка. Неправильний спосіб перетворення — використовувати кодову сторінку ANSI, оскільки переведення не-ASCII тексту у верхній регістр цим шляхом спрямовує символи через обмежену кодову сторінку та пошкоджує все, що лежить поза її межами. Правильним способом є переведення рядків Wide у верхній регістр із збереженням усього діапазону UTF-16. Саме тому HotXLS виконує перетворення за допомогою функції WideUpperCase, завдяки чому пошук тексту з діакритичними знаками або нелатинськими літерами знаходить саме ті символи, які були передані, а не спотворені кодовою сторінкою наближення.
Токенізатор формул має схоже завдання, яке стосується не літер, а визначення межі токена. Науковий запис чисел, наприклад 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. Обчислення формул вимагає переведення регістру Wide-рядків та токенізатора, який зберігає науковий запис чисел єдиним блоком. Кожен із цих випадків має свої правила, і бібліотека може відповідати одному з них, при цьому припускаючись помилок в іншому. Саме тому інструмент, який чудово експортує CSV, усе одно може створити файл RTF, заповнений знаками питання.
Якщо ваші експортні файли використовують формати з розділювачами, компроміси між ними описані в нашій інструкції з експорту в CSV, TSV та HTML, а коли джерелом даних є набір результатів БД, а не створена вручну таблиця, шаблони в експорті бази даних для звітів Delphi природно поєднуються з описаними тут правилами кодування. Усе це постачається у складі компонента HotXLS для Delphi та C++Builder разом з API читання, формул та форматування, що розглядаються в інших публікаціях цього блогу.