Внутри диаграммы Ганта: холст 480 на 360, палитра на 30 цветов и число без последнего бита

—

от автора

Пустая диаграмма — шесть дней работы

Я пишу “Развертку” — утилиту, которая разбирает файл конфигурации 1С:Предприятия в XML и собирает его обратно. Главное требование: результат должен совпадать с файлом, который создает сама платформа, с точностью до бита. “Почти такой же” не подходит.

В табличных документах типовых конфигураций встречаются диаграммы Ганта. Их немного, и все они пустые: точки, серии и интервалы появляются только во время работы программы.

Казалось бы, что сложного в пустой диаграмме? Но на ее разбор ушло шесть дней.

Рассказываю, что оказалось внутри.

Диаграмма внутри диаграммы

Диаграмма Ганта устроена как луковица: снимаешь один слой, а под ним следующий, и на каждом есть повод прослезиться.

Снаружи это рисунок табличного документа особого вида. Внутри — обычная диаграмма с легендой, палитрой, шкалами и рамками. Поверх нее добавлены элементы Ганта: шкала времени, интервалы, связи между ними и фоновые интервалы.

Слой

Что содержит

Обертка Ганта

Шкалу времени, интервалы, связи и фоновые интервалы

Обычная диаграмма

Серии, точки, легенду, заголовок, палитру и рамки

Холст 480 x 360

Координаты областей диаграммы

Формат растет вместе с платформой. В версии записи 17 у обертки Ганта 27 полей, в версии 18 — 31, в версии 19 — 33. Новые поля добавляются в конец: например, нужно ли показывать текст точек и где его размещать. Начало записи остается прежним.

Даже пустые точки и серии содержат данные: ключи, служебное число baseData, шрифт, картинку, цвет штриховки. В XML это выглядит как десяток нулей, а во внутренней записи превращается во вложенные списки глубиной в три уровня.

Поэтому разбирать Ганта пришлось изнутри: сначала обычную диаграмму, затем ее холст и только потом обертку.

Холст 480 x 360

Первая загадка — координаты.

В XML положение области построения, легенды и заголовка задается долями от размера диаграммы. Например, левый край области построения — 0.1675, правый — 0.8325.

Во внутренней записи тоже хранятся доли, но другие: ровно 1/6 и 5/6.

Ответ нашелся в коде. Платформа размещает элементы на воображаемом холсте размером 480 x 360 точек, независимо от реального размера рисунка. Сначала она переводит долю в координату на этом холсте и округляет ее до целой точки. Затем переводит обратно в долю:

0.1675 x 480 = 80.4После округления: 8080 / 480 = 1/6

Каждая координата рамки сохраняется дважды: как двоичное число с плавающей точкой и как десятичное число с 27 знаками после запятой. Для одной шестой эти значения выглядят так:

1.666666666666667e-10.166666666666666666666666667

До этого открытия диаграмма Ганта из отчета об анализе журнала регистрации не собиралась правильно: я рассчитывал рамки по реальному размеру рисунка. Когда перешел на холст 480 x 360, результат совпал с точностью до бита.

Почему не совпадает последний бит

Следующая загадка нашлась в одной из форм ERP. Координата рамки соответствовала 338 точкам из 360. Обычное деление в Python дает:

>>> 338 / 3600.9388888888888889

Платформа записывала 9.388888888888888e-1 — чуть меньше, с отличием в последнем разряде.

С дробью 19/360 происходило то же самое:

Расчет

Результат

Python

0.05277777777777778

Платформа

5.277777777777777e-2

Для изображения разница незаметна. Для совпадения с точностью до бита — критична.

Причина оказалась в преобразовании чисел. Платформа хранит десятичное число в виде блоков по девять цифр — то есть использует длинную арифметику с основанием миллиард.

Чтобы получить из него двоичное число с плавающей точкой, платформа:

  1. Отбрасывает нулевые блоки по краям.

  2. Собирает три старших блока: умножает накопленное значение на миллиард и прибавляет следующий блок. На каждом шаге происходит округление.

  3. Делит результат на нужную степень миллиарда.

Из-за промежуточных округлений результат может отличаться от обычного деления исходных чисел. Так и “уезжал” последний бит.

Я повторил порядок операций платформы — и все рамки совпали.

Здесь важна последовательность: двоичное значение получается из уже сохраненного десятичного числа, а не напрямую из исходной дроби. Для точного совпадения пришлось воспроизвести весь путь преобразования.

Палитра на 30 цветов и серия с номером -1

Цвет серии в XML часто задан как auto. Но во внутренней записи вместо него стоит конкретный цвет. Откуда он берется?

В библиотеке диаграмм есть палитра из 30 цветов. Индекс цвета — остаток от деления номера серии на 30. Маркер выбирается похожим способом: остаток от деления на 3 определяет ромб, квадрат или круг.

Самое интересное происходит со сводной серией. Ее номер — -1.

При выборе цвета платформа рассматривает этот номер как беззнаковое 64-битное число. Значение -1 превращается в 2^64 - 1:

>>> (2**64 - 1) % 3015

В результате сводная серия получает цвет с индексом 15.

А при выборе маркера номер остается знаковым. Операция вычисления остатка в коде платформы дает для -1 значение -1. Такой вариант не предусмотрен, поэтому выбирается маркер по умолчанию — квадрат.

У диаграммы в реквизите формы есть своя таблица цветов. Кроме того, выбор зависит от флага типа диаграммы: график, ступенчатая, биржевая и другие “линейные” виды используют цвета иначе, чем круговая.

Значение этого флага я проверял уже на работающем процессе с помощью Frida: для круговой диаграммы получил 0, для графика — 1.

Как платформа раскладывает легенду

Самая сложная часть нашлась в формах.

Если у диаграммы реквизита формы есть серии, а легенда расположена у одного из краев, платформа при загрузке сама рассчитывает ее положение и размеры.

Правила оказались такими:

  • Каждой серии выделяется ячейка: у графика — 32 x 19 точек, у гистограммы — 19 x 19.

  • Сводная серия в расчете не участвует.

  • Текст серий и размеры поля на эту раскладку не влияют.

  • Легенда размещается по центру выбранного края; нечетная точка уходит вниз.

  • Область построения отступает от края с легендой на размер одной ячейки.

Эти правила я получил экспериментально. Загружал одну и ту же форму с одной, двумя, тремя и четырьмя сериями, каждый раз размещая легенду у разных краев. Сравнение результатов позволило восстановить алгоритм.

Обнаружилась и странность: заголовок с текстом 12313, расположенный у верхнего края, получил пустую рамку. Похоже, при загрузке платформа рассчитывает его положение так, будто текста нет. Но это пока только наблюдение по сохраненной записи — подтверждения в коде я не нашел.

Как я искал правила

Сначала я сравнивал образцы: вот XML, вот внутренняя запись — осталось найти закономерность.

На диаграммах этот подход быстро перестал работать. Образцов мало, полей десятки, а на результат влияют скрытые преобразования.

В итоге сложился такой порядок работы:

Шаг

Инструмент

Что удалось узнать

Найти код сохранения диаграммы

Дизассемблер

Порядок полей и различия между версиями записи

Найти таблицы и константы

Ссылки на данные в коде

Палитры, перечисления, варианты размещения легенды и заголовка

Проверить значения во время работы

Frida

Значения флагов, которые трудно определить по одному коду

Восстановить оставшиеся расчеты

Серия загрузок в платформе

Правила автоматической раскладки

Основные правила я извлекал из кода. Там, где оставались пробелы, ставил отдельные эксперименты. А эталонные файлы использовал для итоговой проверки: совпало ли все до последнего бита.

Что осталось за рамками

Саму отрисовку Ганта — размещение интервалов, связей и текста — я не разбирал. В исследованных конфигурациях диаграммы пустые, поэтому для их точного восстановления эта часть не понадобилась.

Отрисовка заслуживает отдельного разбора. Если интересно — напишите.

А пока два вопроса:

  • Встречали форматы, где одно число хранится сразу в двух представлениях? Как проверяли точность?

  • Что было бы интересно разобрать следующим?

ссылка на оригинал статьи https://habr.com/ru/articles/1101884/