Книга Excel может нести картинки EMF и WMF, и обычный способ нарисовать их — отдать поток байтов системному плееру метафайлов. Это решение, на которое стоит посмотреть прямо: метафайл — это сериализованный поток команд для графического API, и воспроизведение его означает позволить файлу, пришедшему по почте, управлять графическим драйвером. HotXLS идёт другим путём. XLSDecodeVectorScene сама разбирает метафайл, проверяет заголовок, каждый размер записи, объявленное общее число записей и точное положение записи конца файла, безоговорочно отвергает escape-записи и возвращает TXLSVectorScene из примитивных команд рисования, которые бэкенды Canvas и SVG воспроизводят собственным кодом. Никакое воспроизведение драйвером не участвует ни в какой момент
Обмен — покрытие за сдерживание. Белый список команд, ориентированный на прямоугольники, не воспроизведёт каждый метафайл, какой может создать дизайнер, поэтому сцена сообщает, сколько записей рисования она не смогла представить, а вызывающий решает, что с этим делать. Для серверного процесса, отрисовывающего документы, которые он не создавал, этот обмен повёрнут правильной стороной
Почему воспроизведение метафайла плохо подходит недоверенному вводу?
Потому что формат — не картинка, а программа. Поток записей EMF манипулирует стеком состояния контекста устройства, выделяет и выбирает объекты из таблицы дескрипторов и может нести escape-записи, чья полезная нагрузка передаётся драйверу устройства. Воспроизведение его упражняет пути в графическом стеке платформы, написанные в допущении, что метафайл пришёл от сотрудничающего приложения на той же машине. Когда ввод — вложение электронной таблицы, это допущение исчезло, и никакая аккуратность внутри библиотеки таблиц не поможет, ведь библиотека — не тот компонент, который разбирает
Это то же рассуждение, что управляет слоем контейнера. Книга — это ZIP-архив, и HotXLS проверяет его центральный каталог, а не доверяет объявленным смещениям, как описано в статье о проверке ZIP end-of-central-directory. Полезные нагрузки метафайлов — следующий слой той же проблемы
Что декодер проверяет, прежде чем что-либо нарисовать
Проверка структурна и происходит заранее, ведь разборщик, начинающий рисовать и проверяющий по ходу, уже подействовал на данные, которые не верифицировал. Заголовок должен совпадать строго, а не правдоподобно. Каждая запись должна объявлять размер, помещающийся в оставшийся буфер и достаточно большой для собственных фиксированных полей. Число записей, объявленное заголовком, должно совпадать с фактически присутствующими записями. Запись конца файла должна лежать точно там, где кончается поток, а не просто где-то рядом, что закрывает трюк с завершающим мусором, прячущим вторую полезную нагрузку за корректной картинкой
Помимо структуры декодер отказывающе закрыт по семантике. Escape-записи отвергаются, а не пропускаются. Меняющая состояние запись, которую декодер не моделирует, приводит к отказу декодирования, а не игнорируется: игнорирование смены состояния означает, что каждая последующая команда рисования исполняется в состоянии, которого файл не запрашивал, и результат — картинка, неверная так, что никто не предскажет. Записи рисования вне поддерживаемого набора команд — другое дело: они считаются и пропускаются, ведь отсутствующая фигура — видимая, сообщаемая провалка, а не молчаливая порча
Бюджеты — часть контракта формата
У векторных форматов есть собственная версия бомбы декомпрессии. Пара килобайтов записей может объявить полилинии с сотнями миллионов точек или изображение, чьи объявленные размеры перемножаются в терабайты. Поэтому границы должны быть явными константами, а не тем, что машина случайно переживёт
// Из lxVectorScene: бюджет декодирования, заявленный, а не подразумеваемый
XL_VECTOR_MAX_RECORDS = 1000000;
XL_VECTOR_MAX_HANDLES = 4096;
XL_VECTOR_MAX_DC_DEPTH = 32;
XL_VECTOR_MAX_COMMANDS = 100000;
XL_VECTOR_MAX_POINTS_PER_RECORD = 100000;
XL_VECTOR_MAX_TOTAL_POINTS = 2000000;
XL_VECTOR_MAX_TEXT_CHARS = 4096;
XL_VECTOR_MAX_TOTAL_TEXT_CHARS = 1000000;
XL_VECTOR_MAX_IMAGE_SIDE = 8192;
XL_VECTOR_MAX_IMAGE_PIXELS = 32 * 1024 * 1024;
XL_VECTOR_MAX_IMAGE_BYTES = 64 * 1024 * 1024;
XL_VECTOR_MAX_COORD = 1000000000;
Две из них заслуживают примечания. Предел глубины контекста устройства 32 существует, потому что записи SaveDC и RestoreDC вкладываются, а несбалансированный поток может толкать бесконечно; 32 щедро для настоящих метафайлов и дёшево в исполнении. Предел координат существует, потому что координаты питают трансформацию, и значение у границ целочисленного диапазона даёт преобразованный результат, бесконечный или переполняющийся, после чего всякая вычисленная ниже ограничивающая рамка — бессмыслица. Ограничивать координаты при разборе куда легче для рассуждений, чем защищать каждого потребителя геометрии
Использование сцены
Декодер отдаёт объект, которым вы владеете, число команд, номинальный размер и число записей рисования, которые он предпочёл не представлять
uses
lxVectorScene;
var
Scene: TXLSVectorScene;
Error: WideString;
I: Integer;
begin
// Data хранит сырую полезную нагрузку картинки, взятую из книги
if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
begin
// Отказ: заголовок, границы, итоги, положение EOF или бюджет
LogReject('metafile rejected: ' + Error);
Exit;
end;
try
if Scene.SkippedDrawRecords > 0 then
LogWarning(Format('%d drawing records outside the safe subset',
[Scene.SkippedDrawRecords]));
for I := 0 to Scene.Count - 1 do
case Scene.Commands[I].Kind of
xlsvcRectangle: DrawRect(Scene.Commands[I]);
xlsvcEllipse: DrawEllipse(Scene.Commands[I]);
xlsvcPolyline,
xlsvcPolygon,
xlsvcBezier: DrawPath(Scene.Commands[I]);
xlsvcText: DrawText(Scene.Commands[I]);
xlsvcImage: DrawImage(Scene.Commands[I]);
end;
finally
Scene.Free;
end;
end;
Запись команды несёт всё, что нужно бэкенду, и ничего, требующего устройства: наличие пера, цвет, ширину и стиль; наличие кисти и цвет; геометрию; а для текста — строку, имя шрифта, размер, стили и выравнивание. Именно поэтому одна и та же сцена пригодна и экрному рендереру канвы, и писателю SVG, и потому векторный путь не расходится между предпросмотром и экспортом. Отрисовка содержимого листа на экране в целом рассмотрена в статье об отрисовке пользовательской сетки VCL
Отвержение картинки не повреждает книгу
Важное свойство этого дизайна: отвергнутое декодирование влияет только на отрисовку. Исходная полезная нагрузка остаётся в модели, поэтому книга, открытая и сохранённая снова, выносит свои картинки-метафайлы байт в байт, смог ли безопасный декодер их нарисовать или нет. Существующий ограниченный растровый путь тоже остаётся доступен как резерв. Иначе говоря, строгий разборщик ставит заслон тому, что исполняется, а не тому, что сохраняется, — различие, позволяющее изменению с мотивом безопасности выйти в свет, не превратившись в изменение с потерей данных
Обработка объектов рисования в целом, включая части объектной модели, проходящие циклы туда-обратно нетронутыми, рассмотрена в статье о диаграммах, изображениях и рисунках
Где это оставляет серверное развёртывание
Если вы отрисовываете загруженные пользователями книги в службе, практическая позиция теперь защитима: картинки-метафайлы разбираются кодом, который вы можете проаудировать, ограничены константами, которые вы можете прочесть, и никогда не передаются графическому драйверу. Честная оговорка — покрытие. Сложные метафайлы, порождённые инструментами рисования, будут попадать в счётчик пропущенных записей, и ответ здесь — вынести счётчик на поверхность, а не тихо расширять белый список. Картинка, отрисованная частично и сказавшая об этом, — разговор с поддержкой; картинка, отрисованная неверно и промолчавшая, — отчёт об ошибке от клиента
HotXLS обрабатывает XLS, XLSX, ODS и CSV нативно в Delphi и C++Builder без установленного Excel, и та же философия ограниченного разбора пронизывает его слои контейнера, формул и рисования. Детали формата и безопасности перечислены на странице продукта HotXLS Delphi spreadsheet component