04 — Форматы файлов: совместимость, Meridian Open Package, чужие форматы, кодеки
Версия 0.1 · 2026-10-03 · основан на
00-foundation.md(разделы 3 «Имена, форматы, расширения», 4 «Стек», 7 «Фазы»). Всё, что здесь противоречит библии, — ошибка этого документа. Отступления и дополнения — в разделе 9 «Вопросы к библии». Пометка «(уточнить)» означает факт, который не удалось проверить по первоисточнику на момент написания.
Связанные файлы: spec/formats/manifest.example.xml,
spec/formats/write.example.xml,
spec/formats/sheets.example.xml,
spec/formats/slides.example.xml,
spec/formats/mop-package-layout.md,
spec/formats/write.rnc,
spec/formats/units-and-ids.md.
Опорные спецификации: ECMA-376 5-е изд. (2021) / ISO/IEC 29500-1:2016 (OOXML), ODF 1.3 (OASIS, 2021; ISO/IEC 26300 для 1.3 — в стадии принятия, для 1.2 — ISO/IEC 26300:2015), ODF 1.4 (OASIS, 2024), MS-DOC / MS-XLS / MS-PPT / MS-XLSB / MS-CFB / MS-OFFCRYPTO / MS-ONE / MS-ONESTORE / MS-PST / MS-OXMSG (открытые спецификации Microsoft), RTF 1.9.1 (2008), PDF 2.0 = ISO 32000-2:2020, PDF/A (ISO 19005), PDF/UA (ISO 14289), Adobe PSD File Format Specification, OpenRaster 0.0.6, iWork IWA (обратный инжиниринг obriensp/iWorkFileFormat), EPUB 3.3 (W3C, 2023), FictionBook 2.1, RFC 9562 (UUID v7), RFC 9106 (Argon2), ETSI EN 319 132-1 v1.3.1 (XAdES), RFC 5322/2045 (EML/MIME), RFC 5545 (iCalendar), RFC 6350 (vCard 4).
1. Стратегия совместимости
1.1 Принцип «читать всё, писать главное»
| Направление | Правило | Следствие для кода |
|---|---|---|
| Чтение | Любой формат из матрицы 3.3 библии открывается; всё, что не понято, не теряется молча — попадает в «мешок неизвестного» (§1.5) и в паспорт фидельности (§5.6) | Импортёры никогда не паникуют и не отказываются от файла из-за одного неизвестного элемента; обязателен режим восстановления (§6.10) |
| Запись в свой формат | MOP — единственный формат без потерь: модель ядра ↔︎ MOP взаимно однозначны | Любой объект ядра обязан иметь сериализацию в MOP; тесты «модель → MOP → модель» равны по структуре |
| Запись в чужие форматы | Пишем «главное»: подмножество, которое гарантированно открывается приложением-источником без диалога «восстановить файл» и выглядит так же | Экспортёр заявляет Capabilities (§5.2); всё за их
пределами — предупреждение в паспорте до сохранения |
| Round-trip чужого файла | Файл, открытый из .docx и сохранённый обратно в
.docx, не теряет то, что мы не редактировали |
Неизменённые чужие части пакета возвращаются байт-в-байт из
foreign/ (§1.5) |
Приоритет при конфликте: точность отображения > сохранность данных > скорость > размер файла.
1.2 Уровни точности
| Уровень | Определение | Как измеряется | Где обязателен |
|---|---|---|---|
| V — визуальный | Отрендеренная страница совпадает с эталоном приложения-источника | пиксельное/перцептивное сравнение (§1.4) | импорт всех форматов полной поддержки (R/W) |
| S — структурный | Дерево документа (секции, абзацы, прогоны, таблицы, ячейки, фигуры, стили) эквивалентно исходному | нормализованное сравнение деревьев | импорт/экспорт R/W-форматов, тесты кодеков |
| R — round-trip | чужой → MOP → чужой даёт файл, структурно эквивалентный
исходному, включая не поддерживаемые нами элементы |
сравнение исходного и выходного пакета с нормализацией | R/W-форматы семейства OOXML и ODF |
| T — текстовый | Извлекаемый текст и порядок чтения совпадают | сравнение строк (Левенштейн) | форматы «только чтение» (R*), PDF, устаревшие |
Для каждого формата в §4 указано, какой уровень является целевым и к какой фазе.
1.3 Корпус тестов (10 000+ файлов)
Корпус живёт вне основного репозитория: corpus/ —
отдельный Git-репозиторий с Git LFS + зеркало в объектном хранилище CI.
Каждому файлу соответствует запись в
corpus/manifest.toml:
[[file]]
path = "docx/govdocs1/123456.docx"
sha256 = "…"
source = "govdocs1" # источник (ключ из таблицы ниже)
license = "public-domain" # SPDX или "proprietary-internal"
origin_app = "Word 2010" # приложение-источник, если известно
features = ["numbering", "tables", "footnotes", "vml"] # теги для выборок
reference = "ref/docx/govdocs1/123456.pdf" # эталонный рендер
tier = "gate" # gate (блокирует релиз) | nightly | weekly| Источник | Форматы | Лицензия | Объём (цель) |
|---|---|---|---|
| GovDocs1 (NIST/Digital Corpora) | doc, xls, ppt, pdf, rtf, html, csv | публичное достояние (документы правительства США) | 3 000 |
Тестовые файлы LibreOffice (sw/qa, sc/qa,
sd/qa) |
docx, doc, odt, xlsx, xls, ods, pptx, ppt, odp, rtf, vsd | MPL-2.0 | 2 500 |
Apache POI test-data |
doc, docx, xls, xlsx, ppt, pptx, msg, vsd, xlsb | Apache-2.0 | 800 |
| python-docx / openpyxl / python-pptx / docx4j тесты | docx, xlsx, pptx | MIT / Apache-2.0 | 300 |
| pdf.js, PDFBox, veraPDF, pdfium test corpora | Apache-2.0 / смешанные (проверять пофайлово) | 1 000 | |
| Project Gutenberg, Standard Ebooks | epub, txt, html | публичное достояние / CC0 | 300 |
| Wikimedia Commons | svg, png, jpg, tiff, webp, pdf, djvu | CC BY / CC BY-SA / PD (фиксируем автора) | 400 |
| EDRM Enron Email Data Set | pst, eml, mbox | открытая исследовательская лицензия EDRM | 100 |
| Собственные синтетические (генератор, ниже) | все R/W-форматы | внутренние, свободно | 2 500+ |
| Файлы пользователей, присланные через «Сообщить о проблеме» | любые | только закрытый бакет, анонимизация | по мере поступления |
Правила: файлы с неясной лицензией — только в закрытом бакете и никогда в LFS-репозитории; текст пользовательских файлов анонимизируется («лорем-ипсумизация»: замена букв в пределах класса символов с сохранением длины, регистра, письменности и структуры).
Генерация синтетических файлов. Генератор
tools/corpus-gen/ строит файлы по сетке свойств: для
каждого свойства модели (выравнивание, интервал, нумерация, граница,
объединение ячеек, формат числа, тип диаграммы, анимация …) × набор
значений × комбинации по 2–3 свойства. Три пути генерации:
- Библиотеки (
python-docx,openpyxl,python-pptx,odfpy) — быстро, без лицензий, но файлы «идеальные». - COM-автоматизация Microsoft Office на Windows-агенте CI (лицензия
Office на агенте) — файлы «как из Word», эталонный рендер тем же Office
(
ExportAsFixedFormat→ PDF → PNG через pdfium). Это главный источник эталонов для уровня V. soffice --headless --convert-to— эталоны для ODF и перекрёстная проверка.
Каждый синтетический файл несёт в docProps/custom.xml
(или meta.xml) описание того, что в нём проверяется — тесты
читают его и проверяют именно это свойство на уровне S, а не только
картинку.
1.4 Метрика точности
Одна цифра на файл — fidelity score ∈ [0, 1], и три компоненты:
fidelity = 0.5 · visual + 0.3 · layout + 0.2 · text
| Компонента | Алгоритм |
|---|---|
visual |
Рендер каждой страницы в PNG 96 dpi (и 300 dpi для «пиксельного» уровня в nightly). Сравнение с эталоном: сначала выравнивание (сдвиг ±2 px), затем SSIM по блокам 8×8 и доля пикселей с |
layout |
Из эталонного PDF извлекаются прямоугольники слов (pdfium
FPDFText_GetCharBox), из нашей раскладки — те же
прямоугольники. Сопоставление слов по тексту, метрика — средний IoU;
штраф за несовпадение числа страниц (−0.1 за каждую), за перенос строки
в другом месте (−0.01 за строку) |
text |
Извлечённый текст в порядке чтения, нормализация пробелов и
переносов, 1 − levenshtein/len |
Для уровня S отдельно считается structural diff:
деревья документов нормализуются (единицы → EMU, цвета →
#RRGGBB, стили разрешаются в прямое форматирование) и
сравниваются как JSON (jd-подобный дифф); результат —
список расхождений по путям.
CI: на каждый PR — tier = gate (≈1 500 файлов, < 15
мин на 32 ядрах), nightly — весь корпус, отчёт
fidelity-report.json
- HTML с картинками «было/стало/дифф»; регрессия медианы > 0.002 или любого файла > 0.02 блокирует слияние.
1.5 «Мешок неизвестного» (unknown bag)
Назначение: сохранять то, что импортёр не понял, и возвращать это при экспорте в тот же формат. Три уровня хранения:
| Уровень | Что | Где в MOP |
|---|---|---|
| Часть целиком | чужие части пакета, которые мы не интерпретируем
(customXml/item1.xml,
word/glossary/document.xml, xl/metadata.xml,
ppt/commentAuthors.xml, docProps/custom.xml,
VBA vbaProject.bin, Thumbnails/ ODF) |
foreign/<source>/<исходный путь> +
foreign/<source>/index.xml (типы содержимого, связи,
хэши) |
| Фрагмент внутри части | неизвестные элементы/атрибуты внутри понятой части
(w:sdtPr редкого типа, mc:AlternateContent,
расширения a:extLst, x14:) |
элемент <m:x> рядом с объектом-якорем, содержит
исходный XML как есть (иностранные пространства имён сохраняются) |
| Свойства | известный элемент с неизвестным атрибутом или значением | атрибут m:x-attrs="w:cnfStyle=000000100000" на объекте
(список qname=value, значения экранированы) |
<!-- Абзац из docx с неизвестным расширением: оригинал сохранён для round-trip -->
<t:p id="0192b4c8-8f10-7a51-8e34-6b1a2c3d4e5f" style="Normal">
<t:r>Текст абзаца</t:r>
<m:x source="ooxml" part="word/document.xml" anchor="w:p" hash="sha256:3f1c…" mode="sibling-after">
<w14:paraId xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml">5A1B2C3D</w14:paraId>
</m:x>
</t:p>Правила возврата (mode): child — вставить
внутрь соответствующего элемента, sibling-after — после
него, attr — как атрибут, part — часть
целиком. Атрибут hash — хэш исходного объекта-якоря: если
при экспорте объект изменился (хэш его нормализованной сериализации
другой), фрагмент помечается stale и не возвращается (но
попадает в паспорт как «потеряно расширение»). Пользователь может
очистить мешок командой «Файл → Сведения → Очистить данные других
форматов». Размер мешка ограничен настройкой
(compat.foreign-max-size, по умолчанию 64 МБ); при
превышении сохраняются только части, помеченные essential
(VBA, customXml, подписи).
Элемент m:x допустим внутри любого элемента MOP; схемы
(RNC/XSD) объявляют его через общий паттерн
m-extension.
2. Meridian Open Package (MOP) — спецификация контейнера
2.1 ZIP-контейнер
| Требование | Значение |
|---|---|
| Формат | ZIP (PKWARE APPNOTE 6.3.x), ZIP64 обязателен для чтения, при записи — автоматически при > 4 ГиБ или > 65 535 записей |
| Первая запись | mimetype, метод stored, без extra-полей,
без дескриптора данных: байты 30–37 файла содержат
mimetype, байты с 38 — тип (как ODF/EPUB). Детект по первым
64 байтам файла |
| Порядок записей | mimetype, manifest.xml,
meta.xml, settings.xml,
styles.xml, содержимое (content.xml или части
приложения), comments.xml, media/*,
objects/*, foreign/*, history/*,
scripts/*, thumbnail.png,
signatures/*. Порядок важен для потокового чтения: манифест
и стили известны до содержимого |
| Сжатие | deflate уровень 6 для XML; stored для уже
сжатых медиа (png, jpg, webp, zstd-сегментов истории, EXR с внутренним
сжатием); deflate для svg/bmp/tiff без сжатия |
| Имена | UTF-8 (флаг 0x800), прямой слэш, без .., без ведущего
/, регистрозависимые; максимум 255 байт на компонент |
| Время записей | UTC-время сохранения документа, одинаковое для всех записей (детерминированность: два сохранения одного состояния дают байт-идентичный ZIP, кроме времени) |
| Шифрование ZIP | не используется (ни ZipCrypto, ни AES-ZIP); шифрование — только на уровне пакета (§2.16) |
| Запрещено | вложенные ZIP как части без расширения .mop-семейства;
симлинки; записи-каталоги (пустые папки не хранятся) |
Детерминированная запись: атрибуты сортируются (обязательные схемой
первыми, затем по алфавиту), отступ — 2 пробела, перевод строки
LF, кодировка UTF-8 без BOM,
xml:space="preserve" на корневом элементе содержимого.
2.2 manifest.xml
Корень m:manifest
(xmlns:m="urn:meridian:office:core:1"). Полный пример —
spec/formats/manifest.example.xml.
| Элемент / атрибут | Назначение |
|---|---|
@version |
версия формата major.minor (сейчас 1.0);
правила — §2.11 |
@app |
`write |
@generator |
строка приложения и сборки
(Meridian Write 1.0.0 (build 1234; macOS 15.1)) |
@min-reader |
минимальная версия формата, которую должен понимать читатель, чтобы открыть без потерь |
m:document |
@id (UUID v7 документа, постоянен между сохранениями),
@mime, @created |
m:part |
одна запись на каждую часть пакета, кроме mimetype и
самого манифеста: @path, @type (MIME),
@role, @size, @sha256,
@compression (`deflate |
m:features |
список m:feature/@name — что использует документ
(tracked-changes, comments,
scripts, history, signatures,
foreign, embedded-fonts, math,
pivots, external-links,
macros-converted). Позволяет показать значки и
предупреждения без разбора содержимого |
m:history |
@state-hash — SHA-256 нормализованного содержимого,
соответствующего последней записи журнала; @records,
@head-seq; если хэш не совпадает с фактическим содержимым,
история считается рассогласованной (§2.8) |
m:dependencies |
внешние ссылки, которые нужны для полной работы: `m:dependency/@kind="font |
Роли (@role): content, styles,
meta, settings, comments,
strings, sheet, slide,
master, layout, notes,
media, object, chart,
thumbnail, history, snapshot,
script, foreign, signature,
font, pivot, page,
layer.
Манифест — единственная часть, которую читатель обязан разобрать до всего остального; части, отсутствующие в манифесте, игнорируются (с предупреждением в режиме восстановления — используются).
2.3 meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<m:meta xmlns:m="urn:meridian:office:core:1" xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:title>Отчёт о продажах за III квартал</dc:title>
<dc:creator>Иван Петров</dc:creator>
<dc:subject>продажи</dc:subject>
<dc:description/>
<dc:language>ru-RU</dc:language>
<dc:identifier>urn:uuid:0192b4c8-5e7a-7c3e-9d2f-0a1b2c3d4e5f</dc:identifier>
<m:keywords><m:keyword>Q3</m:keyword><m:keyword>2026</m:keyword></m:keywords>
<m:created at="2026-09-28T07:12:40Z" by="a1"/>
<m:modified at="2026-10-03T09:12:00Z" by="a2"/>
<m:printed at="2026-10-01T12:00:00Z"/>
<m:editing cycles="14" duration="PT6H21M"/>
<m:template ref="urn:meridian:template:report-ru" name="Отчёт (RU)"/>
<m:statistics pages="12" words="4310" characters="27802" characters-spaces="32011" paragraphs="188" tables="6" images="4"/>
<m:authors>
<m:author id="a1" name="Иван Петров" initials="ИП" color="#3B5BDB" uid="mid:7f3a…"/>
<m:author id="a2" name="Анна Смирнова" initials="АС" color="#1E8E5A"/>
</m:authors>
<m:provenance source-format="docx" source-app="Microsoft Word 2021" imported-at="2026-09-28T07:12:40Z"
codec="meridian-codecs/ooxml 1.0.0" fidelity-loss="2" fidelity-warn="5"/>
<m:custom>
<m:property name="Отдел" type="string">Продажи</m:property>
<m:property name="Утверждено" type="bool">1</m:property>
<m:property name="Срок" type="date">2026-10-15</m:property>
</m:custom>
</m:meta>Типы m:property/@type:
string|int|float|bool|date|datetime|uuid|uri. Авторы
вынесены в meta.xml, чтобы комментарии, правки и история
ссылались на короткий @id, а персональные данные удалялись
одним действием («Удалить сведения об авторах» заменяет имена на «Автор
1», сохраняя связи).
2.4 settings.xml
Плоский список пар «имя — значение», сгруппированный по областям; неизвестные настройки сохраняются как есть.
<m:settings xmlns:m="urn:meridian:office:core:1">
<m:group name="view">
<m:setting name="zoom" type="int">110</m:setting>
<m:setting name="mode" type="string">print-layout</m:setting>
<m:setting name="show-marks" type="bool">0</m:setting>
</m:group>
<m:group name="compat">
<m:setting name="layout-profile" type="string">ooxml-2013</m:setting> <!-- native | ooxml-2007 | ooxml-2010 | ooxml-2013 | odf -->
<m:setting name="foreign-max-size" type="int">67108864</m:setting>
</m:group>
<m:group name="history">
<m:setting name="enabled" type="bool">1</m:setting>
<m:setting name="keep-days" type="int">365</m:setting>
<m:setting name="keep-records" type="int">200000</m:setting>
<m:setting name="max-size" type="int">268435456</m:setting>
</m:group>
<m:group name="lang">
<m:setting name="default" type="string">ru-RU</m:setting>
<m:setting name="spell-ignore" type="list">Meridian;MOP</m:setting>
</m:group>
</m:settings>Профиль совместимости compat.layout-profile переключает
набор поведений раскладки (как w:compat/w:compatSetting в
OOXML): способ расчёта межстрочного интервала у разных шрифтов,
обработка висячих строк, автоширина таблиц, кернинг по умолчанию.
Документы из Word 2013+ открываются с ooxml-2013, созданные
нами — native.
2.5 styles.xml
Один файл стилей на пакет; стили всех видов в одном пространстве
@id, различаются @kind.
<m:styles xmlns:m="urn:meridian:office:core:1" xmlns:t="urn:meridian:office:text:1"
xmlns:tb="urn:meridian:office:table:1" xmlns:g="urn:meridian:office:graphic:1">
<m:defaults>
<t:rpr font="theme:minor" size="139700" lang="ru-RU" lang-ea="zh-CN" lang-cs="ar-SA"/> <!-- 11 pt -->
<t:ppr space-after="101600" line="x1.15" widow-control="1"/> <!-- 8 pt -->
</m:defaults>
<m:theme name="Meridian">
<m:colors dk1="#1A1D21" lt1="#FFFFFF" dk2="#0B3C5D" lt2="#F5F6F8"
accent1="#3B5BDB" accent2="#1E8E5A" accent3="#E25C3D" accent4="#0E8A8A" accent5="#C2309B" accent6="#F2A541"
hlink="#1C7ED6" folHlink="#7048E8"/>
<m:fonts major="PT Serif" minor="Carlito" major-ea="Noto Serif CJK SC" minor-ea="Noto Sans CJK SC"
major-cs="Noto Naskh Arabic" minor-cs="Noto Sans Arabic"/>
<m:effects/> <!-- резерв: стили линий/заливок/эффектов темы -->
</m:theme>
<m:style id="Normal" kind="paragraph" name="Обычный" builtin="normal" default="1"/>
<m:style id="Heading1" kind="paragraph" name="Заголовок 1" parent="Normal" next="Normal" builtin="heading-1"
priority="10" quick="1" outline="1">
<t:ppr keep-next="1" keep-lines="1" space-before="304800" space-after="76200" outline-level="1"/>
<t:rpr font="theme:major" size="203200" bold="1" color="theme(accent1)"/>
</m:style>
<m:style id="Emphasis" kind="character" name="Выделение" builtin="emphasis"><t:rpr italic="1"/></m:style>
<m:style id="TableGrid" kind="table" name="Сетка таблицы" builtin="table-grid">
<tb:tpr cell-margin-left="68580" cell-margin-right="68580">
<tb:borders><tb:border side="all" style="single" width="6350" color="#000000"/></tb:borders>
</tb:tpr>
<tb:part type="first-row"><t:rpr bold="1"/><tb:cpr fill="theme(accent1, tint 20%)"/></tb:part>
<tb:part type="band-1-row"><tb:cpr fill="#F5F6F8"/></tb:part>
</m:style>
<t:list-style id="ls-bullets" name="Маркированный">
<t:level n="0" format="bullet" text="•" font="Symbol" indent-left="457200" hanging="228600" suffix="tab"/>
<t:level n="1" format="bullet" text="◦" indent-left="914400" hanging="228600" suffix="tab"/>
</t:list-style>
<t:list-style id="ls-decimal" name="Нумерованный">
<t:level n="0" format="decimal" text="%1." start="1" indent-left="457200" hanging="228600" suffix="tab" align="left"/>
<t:level n="1" format="lower-letter" text="%2)" start="1" indent-left="914400" hanging="228600" suffix="tab"/>
<t:level n="2" format="lower-roman" text="%3." indent-left="1371600" hanging="228600" suffix="tab"/>
</t:list-style>
<m:style id="Shape-Default" kind="shape" builtin="shape-default"><g:spr fill="theme(accent1)" line="theme(accent1, shade 50%)" line-width="12700"/></m:style>
</m:styles>@kind |
Содержимое | Используют |
|---|---|---|
paragraph |
t:ppr + t:rpr |
Write, Slides, Draw, Notes, текстовые блоки Sheets |
character |
t:rpr |
то же |
linked |
t:ppr + t:rpr, применим и к абзацу, и к
прогону (как связанные стили Word) |
Write |
table |
tb:tpr + условные части tb:part/@type
(whole, first-row, last-row, first-col, last-col, band-1-row, band-2-row, band-1-col, band-2-col, ne-cell, nw-cell, se-cell, sw-cell) |
Write, Slides, Draw |
cell |
s:cpr (шрифт, заливка, границы, формат числа,
выравнивание, защита) с наследованием parent |
Sheets |
cell-diff |
s:cpr частичный — для условного форматирования и таблиц
(аналог dxf) |
Sheets |
shape, line, connector,
text-box |
g:spr (заливка, линия, тень, свечение, геометрия по
умолчанию) |
Draw, Slides, Write |
chart |
ch:cpr (палитра, стиль серий, фон) |
Chart |
page |
w:page (размер, поля, ориентация) — именованные
страницы |
Write, Draw, Publisher |
note |
n:npr — стиль страницы заметок |
Notes |
Атрибуты стиля: id (ASCII, уникален в пакете),
name (отображаемое, локализуемое), parent,
next, link (пара paragraph↔︎character),
builtin (ключ встроенного стиля для сопоставления с чужими
форматами, см. §6.1), priority, quick
(показывать в галерее), hidden, locked,
semi-hidden, custom (создан пользователем),
outline (участвует в оглавлении).
Автоматические стили (ODF automatic-styles) не нужны:
прямое форматирование хранится атрибутами на объекте; при записи
писатель может вынести повторяющиеся наборы прямого форматирования в
стили id="a<n>" с auto="1" (экономия
места в Sheets, где одинаковые ячейки повторяются миллионы раз; в Write
— не выносится).
2.6
Содержимое: content.xml или части по приложению
| Приложение | Главная часть | Дополнительные части |
|---|---|---|
| Write | content.xml (w:document) |
comments.xml, objects/,
media/ |
| Sheets | workbook.xml (s:workbook) |
sheets/sheet-<n>.xml, strings.xml,
pivots/, objects/chart-<n>.xml,
comments.xml, external/ |
| Slides | presentation.xml |
masters/master-<n>.xml,
layouts/layout-<n>.xml,
slides/slide-<n>.xml,
notes/notes-<n>.xml, comments.xml |
| Draw | content.xml (d:drawing) |
stencils/<name>.xml, objects/,
media/ |
| Paint | image.xml (pt:image) |
layers/, masks/, merged.png,
selection.png |
| Notes | notebook.xml |
pages/<uuid>.xml, media/,
ink/ |
| Forms | form.xml |
media/, themes/ |
| Math | content.xml (mq:formula) |
— |
index.sqlite, folders.xml |
mail/, calendar/, contacts/,
tasks/ |
|
| Base | не ZIP: SQLite-файл с таблицами meridian_* (§3.7) |
— |
Разделение на части выбрано там, где это даёт параллельную загрузку
(листы, слайды) или потоковую запись (большие книги). Для Write
намеренно один content.xml: колонтитулы, сноски и списки
живут в нём — меньше ссылок и rels-подобной косвенности, чем в
OOXML.
2.7 media/,
objects/, thumbnail.png
media/<kind>-<uuid7>.<ext>— изображения, видео, аудио, шрифты (media/font-<uuid7>.woff2). Имя включает UUID, чтобы слияние документов и CRDT не порождали конфликтов имён. Одинаковые файлы (по SHA-256) хранятся один раз; ссылка —@src="media/…". Допустимые типы изображений: PNG, JPEG, WebP, GIF, SVG (санитизированный: без скриптов и внешних ссылок), TIFF, BMP, EMF/WMF (только какforeign-оригинал рядом с растеризованной/векторизованной копией), AVIF/HEIC (только чтение → перекодировка при сохранении).objects/<kind>-<uuid7>.<ext>— встроенные объекты: пакеты MOP других приложений (objects/sheet-….msx— таблица Sheets в Write,objects/chart-….xml— диаграммаch:chart,objects/formula-….meq), чужие OLE-объекты как оригинал (objects/ole-….bin, CFB) с обязательнымpreviewвmedia/. Встроенные пакеты не распаковываются внутрь (ZIP в ZIP допускается только для этого случая и хранитсяstored).thumbnail.png— 256×256 (вписано), sRGB, без альфа; для шифрованных пакетов отсутствует.
2.8 history/ —
журнал операций CRDT
История опциональна (settings/history.enabled).
Содержимое документа (content.xml и части) —
материализованная проекция последнего состояния;
история — источник для совместного редактирования (Meridian Sync, Yrs),
«Версий документа» и восстановления.
history/
index.xml ← список сегментов, снапшотов и вех
log-000001.mhl ← сегмент журнала: кадры операций, zstd
log-000002.mhl
snapshot-001532.mhs ← полный снимок состояния Yrs (update v2) на seq=1532 + state vector
versions/001200.content.xml.zst ← материализованный content.xml для вехи (просмотр без CRDT)
Сегмент .mhl (Meridian History Log):
заголовок MHL1 (4 байта) + doc-id (16 байт) +
first-seq (u64 LE) + zstd-кадр, внутри — последовательность
записей [len: uvarint][CBOR map]:
| Ключ CBOR | Тип | Назначение |
|---|---|---|
s |
u64 | порядковый номер записи (seq), монотонный в пакете |
t |
u64 | время, мс Unix (UTC) |
a |
u8/u16 | индекс автора в meta.xml/m:authors |
k |
u8 | вид: 0 update (Yrs v2), 1 milestone
(веха), 2 compaction marker, 3 comment/meta
op, 4 undo-barrier |
u |
bytes | полезная нагрузка: для k=0 — yrs::Update в
кодировке v2 |
l |
text | метка вехи (для k=1) |
d |
map | произвольные метаданные (устройство, сессия,
origin: "import:docx") |
Один кадр соответствует одной транзакции Yrs (обычно — одной
пользовательской операции или 2-секундному интервалу автосохранения).
Сегмент закрывается при достижении 4 МиБ сжатых данных или 10 000
записей. В ZIP сегменты хранятся stored (сжаты zstd
изнутри, уровень 3 для текущего сегмента и 19 при уплотнении).
Снапшот .mhs: MHS1 +
doc-id + seq + state-vector
(длина-префикс) + zstd(encode_state_as_update_v2). Загрузка
истории = последний снапшот + сегменты после него.
index.xml:
<m:history-index xmlns:m="urn:meridian:office:core:1" head-seq="1532" state-hash="sha256:9b7e…">
<m:snapshot seq="1200" path="history/snapshot-001200.mhs" size="412000"/>
<m:segment from="1201" to="1532" path="history/log-000002.mhl" size="88120"/>
<m:milestone seq="1200" label="Версия для согласования" author="a1" at="2026-10-01T15:00:00Z"
materialized="history/versions/001200.content.xml.zst"/>
<m:truncated before="1200" reason="policy:keep-days" at="2026-10-03T09:00:00Z"/>
</m:history-index>Усечение (compaction): по политике
(keep-days, keep-records,
max-size) или вручную. Алгоритм: выбрать точку
seq=S ≥ всех записей, подлежащих удалению; записать снапшот
на S; сегменты, целиком лежащие до S, удалить;
вехи до S оставить в index.xml только как
метаданные + их материализованные версии (если
history.keep-versions=1). После усечения отменить изменения
раньше S нельзя, но посмотреть вехи — можно.
Согласованность:
manifest/m:history/@state-hash обязан равняться хэшу
нормализованного содержимого. Если файл изменило приложение, не ведущее
историю (или вручную), хэш не сойдётся: читатель показывает «История
изменений рассинхронизирована» и предлагает либо начать историю заново
от текущего содержимого (по умолчанию), либо восстановить содержимое из
истории.
Соответствие модели ↔︎ Yrs: документ = YXmlFragment,
структура повторяет content.xml (элементы =
YXmlElement, текст прогонов = YXmlText с
атрибутами форматирования), таблицы Sheets — YMap по адресу
ячейки внутри YMap листа, бинарные ресурсы — вне CRDT (по
ссылке на media/). Детали — docs/apps/sync.md
(фаза 6).
2.9 scripts/
scripts/
manifest.json ← {"entries":[{"id":"main","path":"main.ts","lang":"ts","events":["open","beforeSave"],"api":"1"}],"permissions":["document","ui"]}
main.ts
lib/helpers.ts
Правила из библии §3.1: расширение документа не меняется; в
manifest.xml часть имеет role="script" и
trust="unsigned|signed|publisher"; без явного разрешения
скрипты не исполняются; при наличии подписи signatures/ с
охватом scripts/ и доверенным издателем — по политике
организации. Макросы VBA из чужих файлов хранятся как
foreign/ooxml/word/vbaProject.bin (не исполняются), а
результат транслятора VBA→MeridianScript (если удалось) — в
scripts/converted/, с пометкой
origin="vba".
2.10 signatures/
signatures/sig-<n>.xml — XMLDSig/XAdES (§2.17).
Подпись покрывает все части, кроме signatures/*,
history/* и thumbnail.png. Подписанный
документ открывается в режиме «только чтение»; редактирование снимает
подписи с предупреждением.
2.11 Версионирование и совместимость
| Правило | Описание |
|---|---|
| Версия формата | manifest/@version = major.minor. major
меняется при несовместимых изменениях схемы (ожидается не чаще раза в 5+
лет), minor — при добавлении элементов/атрибутов |
Чтение старшего minor |
читатель версии 1.2 открывает файл 1.5: неизвестные элементы → мешок неизвестного, с предупреждением «создан в более новой версии» |
Чтение старшего major |
отказ с сообщением и предложением обновиться; исключение —
@min-reader ≤ версии читателя (писатель заявил, что
документ использует только совместимое подмножество) |
| Запись | писатель всегда пишет свою версию; «Сохранить для версии 1.0» — экспорт с паспортом потерь |
| Пространства имён | urn:meridian:office:<module>:1 — число в URN =
major; в minor URN не меняется |
| Схемы | spec/schemas/<module>-<major>.<minor>.rnc
+ сгенерированные XSD; валидность по схеме обязательна для писателя
(тесты) и не обязательна для читателя (терпимый разбор) |
| Устаревание | элемент помечается deprecated в схеме минимум на одну
minor-версию до удаления в следующем
major |
Общие модули ядра имеют собственные пространства имён, дополняющие таблицу 3.2 библии (см. §9):
| Префикс | URN | Содержимое |
|---|---|---|
m |
urn:meridian:office:core:1 |
манифест, мета, настройки, стили-контейнер, тема, общие типы,
m:x |
t |
urn:meridian:office:text:1 |
абзацы, прогоны, поля, заметки, комментарии, правки, списки |
tb |
urn:meridian:office:table:1 |
таблицы (Write/Slides/Draw/Notes) |
g |
urn:meridian:office:graphic:1 |
фигуры, изображения, группы, коннекторы, привязка, обтекание |
ch |
urn:meridian:office:chart:1 |
диаграммы |
w, s, p, d,
pt, n, f, mq,
mm |
urn:meridian:office:{write,sheets,slides,draw,paint,notes,forms,math,mail}:1 |
приложения |
2.12 Единицы измерения — решение
Все длины в MOP хранятся как целые EMU (English Metric Unit,
1 дюйм = 914 400 EMU), тип i64. Угол — 1/60 000
градуса (i32), проценты — 1/1000 % (i32,
50000 = 50 %), множители (межстрочный интервал) — с
префиксом x (x1.15).
Обоснование выбора против альтернатив:
| Вариант | За | Против | Итог |
|---|---|---|---|
| EMU (OOXML DrawingML) | целое; без потерь делится на pt (12 700), twip (635), 1/100 мм (360), мм (36 000), px@96 (9 525), 1/8 pt (1 587,5 — единственная дробь, округляется); импорт OOXML (≈80 % входящих файлов) без потерь; целочисленная арифметика без накопления ошибок | нечитаемо для человека; большие числа | выбран |
| 1/100 мм (внутренняя единица LibreOffice Draw) | целое, читаемо в метрической системе | не делит pt (1 pt = 35,28), twip, px → потери при импорте OOXML и округления при каждом сохранении | отклонён |
| pt с дробями (float/decimal) | привычно типографам | нецелое; f64 даёт ошибки сравнения и
недетерминированность; decimal — медленнее и всё равно не
делит 1/100 мм |
отклонён |
Строки с единицами как в ODF (2.54cm) |
читаемо, самодокументируемо | парсинг и форматирование на каждый атрибут; неоднозначное округление; нельзя сравнивать как числа | только как входной синтаксис |
Правила: писатель пишет только целые EMU без суффикса; читатель
принимает также <число><единица>
(pt, mm, cm, in,
px, pc, twip, emu) —
для рукописных и плоских файлов; плоский формат (§2.18) в режиме
pretty-units пишет суффиксные значения там, где они точно
представимы (12pt, 2.5cm), иначе EMU.
Внутренняя модель ядра: Length(i64) EMU; раскладка считает
в f64 pt, конвертируя один раз на входе. Полная таблица
коэффициентов — spec/formats/units-and-ids.md.
2.13 Цвета
Грамматика атрибутов цвета (color, fill,
line, highlight, shading …):
color := "#" HEX6 | "#" HEX8 | "theme(" slot ("," mod)* ")" | "system(" name ")" | "auto" | "none"
slot := dk1 | lt1 | dk2 | lt2 | accent1..accent6 | hlink | folHlink
mod := "tint" pct | "shade" pct | "lum" signed-pct | "sat" signed-pct | "alpha" pct | "hue" angle
#RRGGBB/#RRGGBBAA— sRGB, альфа 00–FF (по умолчанию FF). Прозрачность отдельного объекта — вalpha, а не в цвете, чтобы цвет из темы можно было комбинировать с прозрачностью.theme(accent1, tint 40%)— ссылка на слот темы с модификаторами; порядок применения — слева направо;tint/shade— в линейном RGB по формулам DrawingML (a:tint,a:shade),lum/sat/hue— в HSL. Результат разрешается при рендере; при экспорте в формат без тем — вычисленное значение.system(window-text)— системные цвета (для совместимости сw:color="auto"и VML); при печати →#000000.- Цветовые пространства: документные форматы — только sRGB; Paint —
профиль ICC в
image.xml/@profile(встроен вmedia/), слои EXR — линейный scRGB; при вставке растров в Write/Slides — конвертация в sRGB. - Схемы OOXML
bg1/tx1/bg2/tx2→lt1/dk1/lt2/dk2черезclrSchemeMappingпри импорте (§6.8).
2.14 Идентификаторы (UUID v7)
- Формат — UUID версии 7 (RFC 9562): 48 бит времени в мс + 4 бита
версии + 12 бит
rand_a+ 2 бита варианта + 62 битаrand_b. Запись — каноническая строкаxxxxxxxx-xxxx-7xxx-[89ab]xxx-xxxxxxxxxxxx, строчные hex. - Почему v7, а не v4: монотонность во времени даёт локальность в индексах SQLite (Base, Notes, Hub), лучшее сжатие (общие префиксы в одном сеансе), естественную сортировку объектов по времени создания при слиянии; при этом без счётчика и MAC-адреса (v1).
- Монотонность внутри процесса: генератор держит последний
unix_ts_msи 12-битный счётчик вrand_a(метод 1 RFC 9562, §6.2); при переполнении счётчика ждёт следующую миллисекунду. - Что получает
@idобязательно: документ, секции, таблицы, строки Sheets? — нет (адресуются координатами), листы, слайды, мастера, макеты, фигуры, изображения, объекты, комментарии, правки, закладки, заметки/сноски, именованные диапазоны, сводные, диаграммы, страницы Notes, слои Paint, записи Mail. Абзацы и прогоны —@idопционально; присваивается при первом участии в совместном редактировании, при появлении ссылки (закладка, комментарий, перекрёстная ссылка) или по настройкеcollab.ids=all. - Ссылки между объектами — всегда по
@id/@ref, никогда по позиции. Внутрипакетные короткие идентификаторы (style/@id,author/@id,note/@id,list/@id) — ASCII[A-Za-z0-9_-]{1,64}, уникальны в своём пространстве. - Идентификатор документа (
m:document/@id) не меняется при «Сохранить»; «Сохранить как» и шаблон → новый документ получает новый@id, старый пишется вmeta.xml/m:derived-from.
2.15 Шрифтовая информация
<m:fonts xmlns:m="urn:meridian:office:core:1"> <!-- внутри styles.xml, после m:theme -->
<m:font name="Carlito" family="sans" pitch="variable" panose="020F0502020204030204" charset-hint="cyr,lat"
metric-equivalent="Calibri" embed="media/font-0192…-subset.woff2" embed-mode="subset" license="OFL-1.1"/>
<m:font name="Calibri" family="sans" panose="020F0502020204030204" metric-equivalent="Carlito">
<m:metrics units-per-em="2048" ascent="1536" descent="512" line-gap="452" x-height="938" cap-height="1294" avg-width="1038"/>
</m:font>
</m:fonts>- Для каждого шрифта, использованного в документе, пишется
m:fontсpanose, семейством, подсказкой письменности и метриками (m:metrics, в единицах em из таблицhhea/OS/2), чтобы на машине без шрифта раскладка подменного шрифта могла быть подогнана (§6.2): высота строки и ширины для «метрически близких» подмен. - Встраивание:
embed-mode="subset|full|none", формат — WOFF2 (субсет по использованным глифам + базовая латиница/кириллица для редактирования), для подписанных документов —full. Лицензия встраивания берётся изOS/2.fsType: приrestrictedвстраивание запрещено, пишутся только метрики. Шрифты Office с лицензией «только предпросмотр» встраиваются по правилам fsType (preview & print). - При импорте OOXML с
w:embedRegular(obfuscated TTF) — деобфускация по алгоритму OOXML (XOR первых 32 байт с GUID) и сохранение вmedia/.
2.16 Шифрование
Схема повторяет уровни OOXML Agile Encryption (MS-OFFCRYPTO: пароль → ключ-шифровальщик → ключ данных; у Office — AES-256-CBC, SHA-512, 100 000 итераций), но с современными примитивами: AES-256-GCM для данных и Argon2id (RFC 9106) для пароля.
Зашифрованный документ — тоже ZIP:
mimetype = application/vnd.meridian.encrypted (stored, первый)
encryption.xml = параметры и обёрнутые ключи
package.bin = шифротекст исходного пакета (целиком, чанками)
<m:encryption xmlns:m="urn:meridian:office:core:1" version="1" inner-mime="application/vnd.meridian.write">
<m:data cipher="AES-256-GCM" chunk-size="4194304" chunks="7" plain-size="27311002"
nonce-prefix="b64:3q2+7w==" aad="meridian-mop-v1"/>
<m:key-encryptors>
<m:password kdf="argon2id" memory="65536" time="3" parallelism="4" salt="b64:…(16 байт)"
wrapped-key="b64:…(32+16 байт)" wrap-nonce="b64:…(12 байт)"/>
<m:certificate alg="RSA-OAEP-SHA256" x509="b64:…DER…" wrapped-key="b64:…"/>
<m:recovery alg="X25519-HPKE" org="mid:org:…" public-key="b64:…" wrapped-key="b64:…"/>
</m:key-encryptors>
</m:encryption>Алгоритм записи:
- Сформировать обычный пакет MOP в памяти/временном файле
(
P). - DEK ← 32 случайных байта (CSPRNG ОС).
nonce-prefix← 8 случайных байт. - Для чанка
i(4 МиБ, последний короче):nonce = nonce-prefix || u32_be(i);AAD = "meridian-mop-v1" || u32_be(i) || u32_be(chunks) || u64_be(plain-size);C_i = AES-256-GCM(DEK, nonce, AAD, P_i); вpackage.binпишетсяC_i || tag_iподряд. AAD с номером и общим числом чанков защищает от перестановки и усечения. - Для пароля:
pw ← NFC(пароль) как UTF-8;KEK = Argon2id(pw, salt, m=64 MiB, t=3, p=4, tag=32)(вторая рекомендация RFC 9106 — открывается на слабых машинах; профиль организации может задатьmemory="2097152" time="1"— первая рекомендация);wrapped-key = AES-256-GCM(KEK, wrap-nonce, AAD="meridian-kek", DEK). Тег GCM заменяетencryptedVerifierиз OOXML. - Для сертификата:
wrapped-key = RSA-OAEP(pub, DEK)или HPKE (RFC 9180) с X25519 — корпоративное восстановление по политике. thumbnail.pngиmeta.xmlв открытом виде не сохраняются (всё внутриP).
Чтение — симметрично; неверный пароль обнаруживается на шаге
разворачивания DEK (неверный тег) без расшифровки данных. Параметры
Argon2 ограничены при чтении (memory ≤ 4 GiB,
time ≤ 16), чтобы файл не стал DoS-инструментом.
Шифрованный документ открывается только после ввода пароля; история и
совместная работа с шифрованными документами — через Meridian Cloud с
серверным ключом (фаза 6, вне этого документа).
2.17 Подписи
- Формат — XMLDSig + XAdES baseline (ETSI EN 319 132-1): уровни B-B
(по умолчанию), B-T (метка времени RFC 3161), B-LT (сертификаты и
OCSP/CRL внутри), B-LTA (архивные метки). Файл
signatures/sig-<n>.xml, кореньds:Signature, одна подпись на файл. - Подписываемые данные:
ds:Referenceна каждую часть пакета (URI = путь внутри ZIP, как в ODFMETA-INF/documentsignatures.xml), кромеsignatures/*,history/*,thumbnail.png; XML-части канонизируются C14N 1.1, бинарные — хэш байтов. Дополнительно —ds:Referenceнаmanifest.xml(который сам содержит хэши), что делает проверку целостности возможной до разбора частей. - Алгоритмы по умолчанию: ECDSA P-256 + SHA-256; поддерживаются RSA-PSS (2048+), ECDSA P-384, Ed25519; хэши SHA-256/384/512.
- ГОСТ через плагин: провайдер подписи — трейт
SignatureProvider(ядро) с реализациямиbuiltin-ringиplugin-cryptopro(КриптоПро CSP через их CAPI/OpenSSL-engine), алгоритмы ГОСТ Р 34.10-2012 / 34.11-2012 с URI по RFC 9215 и «urn:ietf:params:xml:ns:cpxmlsec:…» (принимаются оба, записывается RFC-вариант) (уточнить предпочтение валидаторов госсектора). Ключи — только в хранилище провайдера, ядро их не видит. - Видимая подпись (Write): элемент
t:signature-lineс@sig-ref, отрисовка статуса (действительна / не проверена / нарушена). - Проверка при открытии: цепочка доверия (системные хранилища + корпоративные корни из политики), отзыв (OCSP, с офлайн-допуском), метка времени; результат — в панели «Сведения» и значке статуса; при нарушенной подписи документ открывается, но помечен.
2.18 Плоский вариант
*xf
Один XML-файл (.mwxf, .msxf,
.mpxf, .mvxf), MIME
application/vnd.meridian.<app>+xml. Назначение —
системы контроля версий, диффы, генерация из скриптов, ручное
редактирование.
<?xml version="1.0" encoding="UTF-8"?>
<m:flat xmlns:m="urn:meridian:office:core:1" xmlns:w="urn:meridian:office:write:1" xmlns:t="urn:meridian:office:text:1"
version="1.0" app="write" units="pretty" xml:space="preserve">
<m:manifest …/> <!-- как manifest.xml, без m:part для частей, вложенных ниже -->
<m:meta …/>
<m:settings …/>
<m:styles …/>
<w:document …/> <!-- содержимое; для Sheets: s:workbook + s:sheet*, для Slides: p:presentation + части -->
<m:comments …/>
<m:media>
<m:file path="media/image-0192….png" type="image/png" encoding="base64">iVBORw0KGgo…</m:file>
<m:file path="media/image-0193….jpg" type="image/jpeg" href="report.mwxf.media/image-0193….jpg"/> <!-- внешняя папка -->
</m:media>
</m:flat>Правила: история, подписи и foreign/ в плоский файл не
попадают (при сохранении — предупреждение; подписи недействительны по
определению); медиа — base64 внутри (по умолчанию до 2 МБ на файл) или
внешняя папка <имя>.media/;
units="pretty" разрешает суффиксные длины; сериализация
детерминирована (§2.1), чтобы git diff показывал только
смысловые изменения; идентификаторы стабильны между сохранениями.
3. Форматы приложений
3.1 Write — .mwx /
.mwt / .mwxf
Полный пример — spec/formats/write.example.xml, схема —
spec/formats/write.rnc.
Структура content.xml
w:document (xml:space="preserve")
├─ w:body
│ └─ w:section* ← секция: параметры страницы + поток блоков
│ ├─ w:page ← размер, поля, ориентация (или @page-style)
│ ├─ w:columns? ← колонки
│ ├─ w:header* / w:footer* ← kind="default|first|even"
│ ├─ w:page-numbering? ← start, format, chapter
│ ├─ w:footnotes? / w:endnotes? ← настройки нумерации заметок секции
│ └─ (t:p | tb:table | t:field[block] | w:sdt-block)* ← блоки
├─ w:notes ← w:note* (сноски и концевые сноски)
├─ w:lists ← t:list* (экземпляры списков с переопределениями)
└─ w:bookmarks? ← индекс закладок (опционально, для быстрого поиска)
Секции идут последовательно; разрыв секции — просто начало следующего
w:section с
@break="next-page|continuous|even-page|odd-page". В OOXML
w:sectPr стоит в конце секции (в последнем абзаце) — у нас
параметры в начале, что упрощает потоковый разбор и раскладку.
Абзац t:p. Скалярные свойства —
атрибуты; составные — дочерние элементы перед прогонами.
| Атрибут | Значение | Атрибут | Значение |
|---|---|---|---|
id |
UUID v7 (опц.) | style |
id стиля абзаца |
align |
`left | center | right |
indent-left, indent-right |
EMU | indent-first |
EMU (отрицательный = выступ) |
space-before, space-after |
EMU | line |
x1.15 (множитель) или EMU; `line-rule="exact |
keep-next, keep-lines,
page-break-before, widow-control |
`0 | 1` | outline-level |
list |
id экземпляра t:list |
level |
0–8 |
suppress-hyphenation,
suppress-line-numbers |
`0 | 1` | frame |
Дочерние: t:borders
(t:border side="top|bottom|left|right|between|bar" style="single|double|dotted|dashed|…" width="EMU" color space="EMU"),
t:shading (fill, pattern,
pattern-color), t:tabs
(t:tab pos align="left|center|right|decimal|bar" leader="none|dot|dash|underscore"),
t:mark (свойства знака абзаца и правки:
ins/del), t:change (отслеженное
изменение свойств абзаца).
Текст прямо внутри t:p без обёртки t:r
допустим и означает прогон без прямого форматирования.
Прогон t:r. Все свойства — атрибуты
(style, font, font-ea,
font-cs, size, size-cs,
bold, italic, underline,
underline-color, strike, color,
highlight, shading, caps,
vert, position, spacing,
scale, kern, lang,
lang-ea, lang-cs, hidden,
no-proof, rtl, effect,
ligatures, number-form,
stylistic-set). Содержимое — текст и пустые элементы:
t:tab, t:br kind="line|page|column|text-wrap",
t:sym font char, t:soft-hyphen,
t:nbhyphen, t:note-ref,
t:field-ref. Пробелы значимы всегда
(xml:space="preserve" на корне), поэтому нет аналога
w:t/@xml:space.
Встроенные конструкции в абзаце (на уровне
t:p, между прогонами):
t:link href tooltip target (обёртка прогонов),
t:field, t:bookmark-start id name /
t:bookmark-end ref, t:comment-start ref /
t:comment-end ref, t:ins, t:del,
t:move-from, t:move-to, t:control
(контент-контроль), t:math (MathML), t:ruby,
g:inline, g:anchor,
t:signature-line.
Поля
<t:p>Стр. <t:field type="PAGE" instr="PAGE" format="decimal"><t:result>3</t:result></t:field> из
<t:field type="NUMPAGES"><t:result>12</t:result></t:field></t:p>
<t:p>См. <t:field type="REF" instr="REF bm-intro \h"><t:result>раздел 1</t:result></t:field></t:p>
<!-- Блочное поле (оглавление) прямо в w:section -->
<t:field type="TOC" instr="TOC \o "1-3" \h \z \u" dirty="0">
<t:result>
<t:p style="TOC1"><t:link href="#bm-h1">1. Введение<t:tab/>3</t:link></t:p>
</t:result>
</t:field>@type — канонический тип
(PAGE NUMPAGES SECTIONPAGES DATE TIME SAVEDATE CREATEDATE AUTHOR TITLE SUBJECT KEYWORDS FILENAME DOCPROPERTY REF PAGEREF NOTEREF STYLEREF SEQ TOC INDEX XE TA TOA MERGEFIELD IF FORMULA SYMBOL HYPERLINK ASK FILLIN QUOTE INCLUDETEXT INCLUDEPICTURE BARCODE CUSTOM),
@instr — строка инструкции в синтаксисе кодов полей Word
(хранится для round-trip и для редких ключей), @locked,
@dirty (требует пересчёта), t:result —
кэшированный результат (прогоны или блоки).
Закладки, сноски, комментарии, правки
<t:p id="0192b4c8-9a00-7b12-8c3d-4e5f60718293" style="Normal">
<t:bookmark-start id="bm-intro" name="intro"/>
<t:r>Введение</t:r>
<t:bookmark-end ref="bm-intro"/>
<t:r> содержит ссылку</t:r>
<t:note-ref kind="footnote" ref="n1"/>
<t:comment-start ref="c1"/>
<t:ins id="r1" author="a2" date="2026-10-02T14:03:00Z"><t:r> и вставленный текст</t:r></t:ins>
<t:del id="r2" author="a2" date="2026-10-02T14:03:30Z"><t:r>, а это удалено</t:r></t:del>
<t:comment-end ref="c1"/>
<t:r bold="1"><t:change id="r3" author="a2" date="2026-10-02T14:04:00Z" kind="rpr"><t:old bold="0" italic="1"/></t:change>жирный</t:r>
</t:p>
…
<w:notes>
<w:note id="n1" kind="footnote"><t:p style="FootnoteText"><t:note-mark/> Текст сноски.</t:p></w:note>
<w:note id="n2" kind="endnote" custom-mark="*"><t:p style="EndnoteText"><t:note-mark/> Концевая.</t:p></w:note>
</w:notes>- Правки:
t:ins/t:delоборачивают прогоны и встроенные объекты; удалённый знак абзаца —<t:mark del="r5"/>; изменение свойств —t:change kind="ppr|rpr|tblpr|rowpr|cellpr|sectpr|numbering"сt:old(старые значения в тех же атрибутах); перемещение —t:move-from/t:move-toс общим@move. Атрибуты правки:id,author(ссылка наmeta.xml),date. - Комментарии — в
comments.xml:m:comment id author date resolved parentс абзацамиt:pи ответами (вложенныеm:commentсparent); в тексте — только маркеры диапазона. Комментарии к объектам (картинка, ячейка, слайд) —m:comment/@target="<uuid>"без маркеров. - Сноски: тело — в
w:notes; знак сноски внутри тела —t:note-mark; настройки нумерации — на секции (<w:footnotes start="1" format="decimal" restart="continuous|section|page" position="page-bottom|below-text"/>).
Нумерация. Два уровня, как
abstractNum/num в OOXML, но с понятными
именами:
<!-- styles.xml -->
<t:list-style id="ls-decimal" name="Нумерованный">
<t:level n="0" format="decimal" text="%1." start="1" align="left" indent-left="457200" hanging="228600" suffix="tab"
restart-after="-1" legal="0"><t:rpr bold="1"/></t:level>
</t:list-style>
<!-- content.xml / w:lists -->
<w:lists>
<t:list id="L1" style="ls-decimal"/>
<t:list id="L2" style="ls-decimal"><t:override level="0" start="5"/></t:list> <!-- продолжение с 5 -->
<t:list id="L3" style="ls-decimal" continue="L1"/> <!-- продолжить нумерацию L1 -->
</w:lists>
<!-- абзац -->
<t:p list="L1" level="0">Первый пункт</t:p>@format:
decimal, decimal-zero, upper-roman, lower-roman, upper-letter, lower-letter, ordinal, cardinal-text, ordinal-text, bullet, none, chicago, russian-upper, russian-lower, ...
(полный список ISO 29500 ST_NumberFormat поддерживается для
round-trip). Номера не хранятся — считаются при раскладке. Для docx
w:numStyleLink/w:styleLink —
t:list-style/@linked-style.
Таблицы
<tb:table id="0192b4c8-a100-7c00-9e11-223344556677" style="TableGrid" width="x100%" align="center" layout="auto"
look="first-row,band-rows" indent="0">
<tb:cols><tb:col width="2880000"/><tb:col width="2880000"/><tb:col width="1800000"/></tb:cols>
<tb:row header="1" height="360000" height-rule="at-least" cant-split="1">
<tb:cell><t:p style="Normal" align="center"><t:r bold="1">Показатель</t:r></t:p></tb:cell>
<tb:cell colspan="2"><t:p>Значение</t:p></tb:cell>
</tb:row>
<tb:row>
<tb:cell rowspan="2" valign="center" fill="#F5F6F8"><t:p>Выручка</t:p></tb:cell>
<tb:cell><t:p align="right">1 240</t:p></tb:cell>
<tb:cell><t:p align="right">+12 %</t:p></tb:cell>
</tb:row>
<tb:row><tb:cell><t:p>980</t:p></tb:cell><tb:cell><t:p>+4 %</t:p></tb:cell></tb:row>
</tb:table>Объединения — явные colspan/rowspan
(ячейки-«продолжения» не записываются, как в HTML;
w:vMerge="continue" и gridSpan преобразуются
при импорте, §6.5). @width: EMU, x<pct>%
от доступной ширины или auto. Ширины столбцов —
предпочтительные (tb:col/@width), конечные считаются при
раскладке (layout="auto") или фиксированы
(fixed). Вложенные таблицы — tb:table внутри
tb:cell. Свойства ячейки: width,
valign, fill, dir,
margins (tb:margins), tb:borders,
no-wrap, fit-text, hide-mark.
Плавающие таблицы — tb:table/@frame с теми же атрибутами,
что у g:anchor.
Плавающие и встроенные объекты
<g:anchor id="0192b4c8-b200-7d00-8f22-334455667788" x="914400" y="457200" rel-x="column" rel-y="paragraph"
wrap="square" wrap-side="both" z="3" behind="0" lock="0" dist-top="0" dist-bottom="0" dist-left="114300" dist-right="114300">
<g:image src="media/image-0192b4c8-b201-7e00-9a33-445566778899.png" width="2743200" height="1828800" alt="График продаж"
crop="0 0 0 0" rotation="0"/>
</g:anchor>
<t:p><t:r>Текст с </t:r><g:inline width="457200" height="457200"><g:image src="media/icon-….svg"/></g:inline><t:r> значком.</t:r></t:p>Внутри g:anchor/g:inline:
g:image, g:shape (с g:text для
текста фигуры), g:text-box, g:group,
g:connector, ch:chart
(@src="objects/chart-….xml"), g:object
(встроенный пакет/OLE: @src, @type,
@preview, @activation="embed|link"),
t:math. Относительные положения:
rel-x="page|margin|column|character|left-margin|right-margin|inside|outside",
rel-y="page|margin|paragraph|line|top-margin|bottom-margin|inside|outside";
выравнивание вместо координаты — x="align:center".
Контент-контроли и формы в документе:
t:control type="text|rich|date|dropdown|combo|checkbox|picture|group|repeating" tag title locked placeholder
с t:options/t:option для списков; блочный
вариант — w:sdt-block. Используется для шаблонов и импорта
w:sdt.
3.2 Sheets — .msx /
.mst / .msxf
Полный пример — spec/formats/sheets.example.xml.
workbook.xml
<s:workbook xmlns:s="urn:meridian:office:sheets:1" xmlns:m="urn:meridian:office:core:1" id="0192b4c8-c300-7e00-8a44-556677889900">
<s:props date-base="1899-12-30" calc="auto" iterate="0" iterate-max="100" iterate-delta="0.001"
precision-as-displayed="0" r1c1="0" full-precision="1"/>
<s:sheets>
<s:sheet id="0192b4c8-c301-7e01-8b55-66778899aa11" name="Данные" src="sheets/sheet-1.xml" state="visible" color="theme(accent2)"/>
<s:sheet id="0192b4c8-c302-7e02-8c66-778899aabb22" name="Отчёт" src="sheets/sheet-2.xml"/>
</s:sheets>
<s:names>
<s:name name="Продажи" scope="workbook">Данные!$A$2:$C$100</s:name>
<s:name name="_PrintArea" sheet="0192b4c8-c301-7e01-8b55-66778899aa11" builtin="print-area">Данные!$A$1:$H$50</s:name>
<s:name name="Ставка" scope="workbook" comment="НДС">0.2</s:name> <!-- константа -->
</s:names>
<s:external-links>
<s:link id="x1" href="file:///…/budget.msx" cache="external/x1.xml" refresh="prompt"/>
</s:external-links>
<s:protection structure="0" windows="0"/>
</s:workbook>Лист sheets/sheet-<n>.xml — компактное
хранение
<s:sheet xmlns:s="urn:meridian:office:sheets:1" xmlns:t="urn:meridian:office:text:1" xmlns:g="urn:meridian:office:graphic:1"
id="0192b4c8-c301-7e01-8b55-66778899aa11" name="Данные" xml:space="preserve">
<s:props grid="1" headings="1" zoom="100" rtl="0" default-row-height="190500" default-col-width="800100" tab-color="theme(accent2)"/>
<s:view active-cell="B2" selection="B2:B10" top-left="A1" frozen-rows="1" frozen-cols="0" mode="normal"/>
<s:cols>
<s:col from="1" to="1" width="1600200"/>
<s:col from="2" to="3" width="1143000" style="Num2"/>
<s:col from="4" to="4" hidden="1"/>
<s:col from="5" to="8" outline-level="1" collapsed="0"/>
</s:cols>
<s:data>
<s:row r="1" style="Header" height="228600">
<s:c c="1" t="s" v="0"/> <!-- общая строка № 0 -->
<s:c c="2" t="str">Цена</s:c> <!-- строка inline -->
<s:c c="3" t="s" v="1"/>
</s:row>
<s:row r="2">
<s:c c="1" t="str">Ноутбук</s:c>
<s:c c="2" s="Num2" v="84990"/> <!-- число: @t по умолчанию "n" -->
<s:c c="3" v="101988"><s:f>B2*(1+Ставка)</s:f></s:c>
<s:c c="4" t="b" v="1"/>
<s:c c="5" t="e" v="#DIV/0!"><s:f>1/0</s:f></s:c>
<s:c c="6" s="Date" v="46296"/> <!-- 2026-10-01 как серийное число -->
<s:c c="7" v="169980"><s:f shared="sf1" ref="G2:G100">B2*2</s:f></s:c>
<s:c c="8" t="str" v="Ноутбук, 84990"><s:f>A2&", "&B2</s:f></s:c>
</s:row>
<s:row r="3">
<s:c c="7" v="…"><s:f shared="sf1"/></s:c> <!-- участник общей формулы -->
<s:c c="9" v="12"><s:f array="I3:I7" dynamic="1">SORT(B2:B6)</s:f></s:c> <!-- динамический массив, якорь -->
<s:c c="12" t="rich"><s:rt bold="1">Жирно</s:rt><s:rt color="#C92A2A"> и красно</s:rt></s:c>
<s:cs c="13" n="200" s="Fill"/> <!-- 200 пустых ячеек с одним стилем -->
</s:row>
<s:rows from="101" to="5000" style="Body"/> <!-- диапазон одинаковых пустых строк -->
</s:data>
<s:merges><s:merge ref="A1:C1"/></s:merges>
<s:cf ref="C2:C100">
<s:rule type="cell" op="gt" priority="1" dxf="dxf-hot" stop="0"><s:f>100000</s:f></s:rule>
<s:rule type="color-scale" priority="2"><s:point kind="min" color="#F8696B"/><s:point kind="percentile" value="50" color="#FFEB84"/><s:point kind="max" color="#63BE7B"/></s:rule>
<s:rule type="data-bar" priority="3" color="theme(accent1)" min="auto" max="auto" gradient="1"/>
<s:rule type="icon-set" priority="4" set="3-arrows" reverse="0"/>
<s:rule type="formula" priority="5" dxf="dxf-weekend"><s:f>WEEKDAY($F2,2)>5</s:f></s:rule>
<s:rule type="top" priority="6" rank="10" percent="0" bottom="0" dxf="dxf-top"/>
<s:rule type="duplicate" priority="7" dxf="dxf-dup"/>
</s:cf>
<s:validations>
<s:validation ref="D2:D100" type="list" allow-blank="1" show-dropdown="1" error-style="stop"><s:f>"да,нет"</s:f>
<s:prompt title="Выбор" text="Выберите значение"/><s:error title="Ошибка" text="Только да/нет"/></s:validation>
<s:validation ref="B2:B100" type="decimal" op="between"><s:f>0</s:f><s:f>1000000</s:f></s:validation>
</s:validations>
<s:tables>
<s:table id="0192b4c8-c310-7e10-8d77-8899aabbcc33" name="Товары" ref="A1:C100" header-row="1" totals-row="0"
style="TableMedium2" banded-rows="1" first-col="0">
<s:col id="1" name="Товар"/><s:col id="2" name="Цена" totals="sum"/><s:col id="3" name="С НДС" formula="[@Цена]*(1+Ставка)"/>
<s:autofilter/>
</s:table>
</s:tables>
<s:hyperlinks><s:link ref="A5" href="https://example.org/" tooltip="Сайт"/><s:link ref="A6" location="Отчёт!B2"/></s:hyperlinks>
<s:notes><s:note ref="B3" author="a1" visible="0" width="1600200" height="762000"><t:p>Проверить цену</t:p></s:note></s:notes>
<s:print><s:page size="A4" orientation="landscape" scale="100" fit-width="1" fit-height="0" margins="720000 720000 720000 720000 360000 360000"/>
<s:titles rows="1:1"/><s:breaks><s:break row="50"/></s:breaks><s:options grid="0" headings="0" center-h="1"/></s:print>
<s:drawings>
<g:anchor from="E2" from-dx="0" from-dy="0" to="K20" to-dx="0" to-dy="0" move="cell" size="cell">
<ch:chart xmlns:ch="urn:meridian:office:chart:1" src="objects/chart-0192b4c8-c320-7e20-8e88-99aabbccdd44.xml"/>
</g:anchor>
</s:drawings>
<s:autofilter ref="A1:C100"><s:filter col="2"><s:custom op="gt"><s:f>1000</s:f></s:custom></s:filter></s:autofilter>
<s:outline summary-below="1" summary-right="1"/>
<s:protection sheet="0" select-locked="1" select-unlocked="1" format-cells="0"/>
</s:sheet>| Элемент | Правило компактности |
|---|---|
s:row |
только непустые строки; @r — номер; свойства строки —
атрибуты (height, hidden, style,
outline-level, collapsed) |
s:c |
@c — номер столбца (1-based, без буквенного адреса —
короче и без парсинга); @t опускается для чисел; значение —
@v (числа, индексы строк, bool, ошибки) или содержимое
(строки str, rich) |
s:cs |
серия из @n ячеек с одним стилем без значений |
s:rows |
диапазон строк с одинаковыми свойствами без ячеек |
| общие строки | strings.xml:
<s:strings><s:si>…</s:si></s:strings>;
писатель включает строку в общие, если она встречается ≥ 2 раз или
длиннее 32 байт; остальные — inline |
| формулы | s:f — текст формулы в канонической форме (§6.6);
@shared+@ref — общая формула (участники —
пустой s:f shared); @array — диапазон массива,
@dynamic="1" — динамический (переливающийся); кэш
результата — @v/@t ячейки |
Типы @t: n (число, по умолчанию),
s (общая строка, @v = индекс),
str (строка inline), rich (форматированная,
s:rt), b (0|1), e
(ошибка:
#NULL! #DIV/0! #VALUE! #REF! #NAME? #NUM! #N/A #SPILL! #CALC! #GETTING_DATA),
d (дата-время ISO 8601 — используется только при импорте из
форматов с «настоящими» датами и в плоском файле; в пакете — серийное
число + формат).
Даты: серийное число дней от date-base (по умолчанию
1899-12-30, как ODF/LibreOffice; значение совпадает с Excel
1900 для дат с 1900-03-01); время — дробная часть;
date-base="1904-01-01" сохраняется при импорте книг 1904
(§6.7).
Форматы чисел: синтаксис кодов Excel
(#,##0.00;[Red]-#,##0.00, [$-419]d mmmm yyyy,
0.00E+00, @) — промышленный стандарт,
поддержанный всеми целевыми форматами; ODF number:*-style
преобразуется в код при импорте и обратно при экспорте. Встроенные
форматы — по
builtin="general|number|currency|accounting|date|time|percent|fraction|scientific|text"
плюс явный код.
Стили ячеек (styles.xml)
<m:style id="Num2" kind="cell" parent="Default"><s:cpr numfmt="#,##0.00" halign="right"/></m:style>
<m:style id="Header" kind="cell" parent="Default">
<s:cpr bold="1" color="#FFFFFF" fill="theme(accent2)" halign="center" valign="center" wrap="1">
<s:borders><s:border side="bottom" style="medium" color="#000000"/></s:borders>
</s:cpr>
</m:style>
<m:style id="dxf-hot" kind="cell-diff"><s:cpr fill="#FFC7CE" color="#9C0006"/></m:style>
<m:style id="a17" kind="cell" parent="Num2" auto="1"><s:cpr fill="#FFF2CC"/></m:style> <!-- автоматический -->В отличие от styles.xml OOXML (массивы
fonts/fills/borders/numFmts/cellXfs/cellStyleXfs и ссылки
по индексам) каждый стиль самодостаточен и наследует
parent; kind="cell-diff" — частичный стиль для
условного форматирования и стилей таблиц.
Сводные таблицы
pivots/pivot-<n>.xml
<s:pivot xmlns:s="urn:meridian:office:sheets:1" id="0192b4c8-c330-7e30-8f99-aabbccddee55" name="Сводная1"
source="Данные!A1:F1000" target="Отчёт!A3" cache="pivots/cache-1.bin" refresh-on-open="0" style="PivotLight16">
<s:fields>
<s:field name="Регион" axis="row" sort="asc" subtotal="sum" show-all="0"/>
<s:field name="Квартал" axis="col"/>
<s:field name="Год" axis="page"><s:selected>2026</s:selected></s:field>
<s:field name="Продажи" axis="data" agg="sum" numfmt="#,##0" caption="Сумма продаж"/>
<s:field name="Продажи" axis="data" agg="count" caption="Сделок"/>
<s:field name="Дата" group="months,years"/>
</s:fields>
<s:layout form="compact" grand-row="1" grand-col="1" empty-as="" repeat-labels="0"/>
<s:cache-def hash="sha256:…" records="999"><s:cache-field name="Регион" type="string" unique="12"/>…</s:cache-def>
</s:pivot>Кэш (cache-1.bin: столбцово-ориентированный, zstd)
опционален — без него сводная пересчитывается из источника при открытии;
при импорте xlsx кэш переносится, чтобы сводная по удалённому источнику
не опустела.
Диаграммы
objects/chart-<uuid>.xml (общий модуль
Meridian Chart, тот же элемент в Write/Slides):
<ch:chart xmlns:ch="urn:meridian:office:chart:1" xmlns:t="urn:meridian:office:text:1" id="…" type="bar" variant="clustered"
direction="column" style="ch-style-2" rounded="0">
<ch:title><t:p align="center"><t:r bold="1">Продажи по кварталам</t:r></t:p></ch:title>
<ch:plot gap-width="150" overlap="0">
<ch:series id="s1" name="Данные!$B$1" cat="Данные!$A$2:$A$5" val="Данные!$B$2:$B$5" fill="theme(accent1)">
<ch:cache><ch:pt i="0" v="120"/><ch:pt i="1" v="135"/><ch:pt i="2" v="98"/><ch:pt i="3" v="160"/></ch:cache>
<ch:labels show="value" position="outside-end"/>
<ch:trendline type="linear" forecast-forward="1"/>
</ch:series>
</ch:plot>
<ch:axes>
<ch:axis id="cat" kind="category" position="bottom" title="Квартал"/>
<ch:axis id="val" kind="value" position="left" min="0" max="auto" major-unit="50" numfmt="#,##0" gridlines="major"/>
</ch:axes>
<ch:legend position="bottom"/>
</ch:chart>Типы:
bar, line, area, pie, doughnut, scatter, bubble, radar, stock, surface, combo, treemap, sunburst, histogram, waterfall, funnel, box
(новые типы Office 2016+ — на уровне V для импорта, запись в pptx/xlsx —
через cx: ChartEx или fallback-картинку).
Именованные диапазоны, таблицы, внешние ссылки — см.
workbook.xml и s:tables выше.
Структурированные ссылки (Товары[[#Headers],[Цена]],
[@Цена]) — часть канонического синтаксиса формул.
3.3 Slides — .mpx /
.mpt / .mpxf
Пример — spec/formats/slides.example.xml.
presentation.xml p:presentation: размер, порядок слайдов, мастера, секции, параметры показа
masters/master-<n>.xml p:master: фон, фигуры, текстовые стили уровней (title/body/other), список макетов
layouts/layout-<n>.xml p:layout: тип (title, title-content, two-content, comparison, title-only, blank, section, picture …), плейсхолдеры
slides/slide-<n>.xml p:slide: фигуры, переход, анимации, заметки докладчика
comments.xml m:comment/@target = id слайда или фигуры
<p:slide xmlns:p="urn:meridian:office:slides:1" xmlns:g="urn:meridian:office:graphic:1" xmlns:t="urn:meridian:office:text:1"
id="0192b4c8-d400-7f00-8a11-bbccddeeff66" layout="0192b4c8-d001-7f01-8b22-ccddeeff0077" show="1" xml:space="preserve">
<p:background fill="theme(lt1)"/>
<p:shapes>
<g:shape id="…1" placeholder="title" x="838200" y="365125" width="10515600" height="1325563">
<g:text autofit="shrink"><t:p align="left"><t:r>Итоги квартала</t:r></t:p></g:text>
</g:shape>
<g:shape id="…2" placeholder="body" idx="1" x="838200" y="1825625" width="10515600" height="4351338">
<g:text><t:p level="0" list="L1">Выручка +12 %</t:p><t:p level="1" list="L1">Онлайн-канал +30 %</t:p></g:text>
</g:shape>
<g:image id="…3" src="media/image-….png" x="7000000" y="2000000" width="4000000" height="3000000"/>
</p:shapes>
<p:transition type="fade" duration="500" advance="click" advance-after="0"/>
<p:animations>
<p:seq>
<p:anim target="…2" effect="appear" trigger="click" build="by-paragraph" duration="0"/>
<p:anim target="…3" effect="fly-in" dir="from-left" trigger="after-previous" delay="200" duration="700" ease="ease-out"/>
<p:anim target="…3" effect="emphasis:pulse" trigger="with-previous" duration="400" repeat="2"/>
</p:seq>
</p:animations>
<p:notes><t:p>Напомнить про прогноз на IV квартал.</t:p></p:notes>
</p:slide>Модель анимаций намеренно плоская: список p:anim с
эффектом (entrance:*, emphasis:*,
exit:*, motion:path), целью, триггером
(click|with-previous|after-previous|hover:<id>|click:<id>),
задержкой, длительностью, построением текста (build),
повтором, параметрами эффекта (атрибуты). Дерево
p:timing/p:tnLst OOXML с условиями и под-узлами сводится к
этому списку при импорте (§4.3); неразобранные узлы — в
m:x. Плейсхолдеры наследуют позицию и форматирование от
макета → мастера (при отсутствии x/y/width/height).
3.4 Draw — .mvx /
.mvt / .mvxf
<d:drawing xmlns:d="urn:meridian:office:draw:1" xmlns:g="urn:meridian:office:graphic:1" xmlns:t="urn:meridian:office:text:1" id="…">
<d:page id="…" name="Схема сети" width="7560000" height="10692000" scale="1:50" unit="mm" grid="180000" snap="grid,guides,shapes">
<d:layer id="…" name="Фон" visible="1" locked="1" print="1"/>
<d:layer id="…" name="Устройства" visible="1"/>
<g:shape id="n1" layer="…2" preset="rounded-rect" x="914400" y="914400" width="1828800" height="914400" style="Shape-Default"
master="stencils/network.xml#router">
<g:text><t:p align="center">Маршрутизатор</t:p></g:text>
<g:ports><g:port id="e" x="x100%" y="x50%"/><g:port id="w" x="0" y="x50%"/></g:ports>
<g:data><g:prop name="ip" type="string">10.0.0.1</g:prop></g:data>
</g:shape>
<g:shape id="n2" preset="custom" x="4572000" y="914400" width="1828800" height="914400">
<g:geom w="100" h="100"><g:path>M 0 50 L 50 0 L 100 50 L 50 100 Z</g:path><g:guide name="r" formula="w/4"/></g:geom>
</g:shape>
<g:connector id="c1" from="n1" from-port="e" to="n2" to-port="w" route="orthogonal" begin-arrow="none" end-arrow="triangle"
line="theme(dk1)" line-width="12700"><g:points><g:pt x="2743200" y="1371600"/></g:points></g:connector>
</d:page>
<d:guides><d:guide axis="x" pos="3780000"/></d:guides>
</d:drawing>Геометрия g:geom — путь в системе координат
w×h (единицы формулы, не EMU) с командами
M L C Q A Z и направляющими-формулами
(g:guide, арифметика +
min/max/sqrt/sin/cos/atan2), что покрывает DrawingML
custGeom и формулы Visio ShapeSheet
(подмножество). Стенсилы — stencils/<имя>.xml с
мастер-фигурами, экземпляр ссылается @master. Коннекторы
хранят маршрут (g:points) как кэш и перестраиваются при
перемещении узлов.
3.5 Paint — .mpi
Пакет без styles.xml/settings.xml;
структура близка к OpenRaster (экспорт в .ora без потерь
базовой структуры):
mimetype application/vnd.meridian.paint
manifest.xml
meta.xml
image.xml pt:image
layers/<uuid>.png слои 8/16 бит, sRGB/профиль; layers/<uuid>.exr — 32-бит float (линейный)
masks/<uuid>.png маски (8/16-бит серый)
merged.png сведённое изображение (обязательно; для просмотрщиков и Hub)
thumbnail.png
selection.png текущее выделение (опционально)
media/profile-….icc цветовой профиль
<pt:image xmlns:pt="urn:meridian:office:paint:1" xmlns:t="urn:meridian:office:text:1" id="…" width="4000" height="3000" dpi="300"
mode="rgb" depth="16" profile="media/profile-0192….icc" background="transparent">
<pt:stack>
<pt:group id="…" name="Ретушь" opacity="100000" blend="normal" visible="1" expanded="1">
<pt:layer id="…" name="Кожа" src="layers/0192….png" x="0" y="0" opacity="80000" blend="soft-light" visible="1" locked="0"
mask="masks/0192….png" mask-linked="1" clip="below"/>
<pt:adjust id="…" name="Кривые" type="curves" opacity="100000"><pt:param name="rgb" value="0,0 64,58 192,200 255,255"/></pt:adjust>
</pt:group>
<pt:text id="…" name="Подпись" x="200" y="2600" width="1800" opacity="100000" src="layers/0192…-cache.png">
<t:p><t:r font="Inter" size="609600" color="#FFFFFF">Meridian</t:r></t:p>
</pt:text>
<pt:layer id="…" name="Фон" src="layers/0192…-bg.png" x="0" y="0" opacity="100000" blend="normal"/>
</pt:stack>
<pt:guides/><pt:paths/>
</pt:image>Порядок в pt:stack — сверху вниз (как ORA). Режимы
наложения — имена W3C Compositing
(normal multiply screen overlay darken lighten color-dodge color-burn hard-light soft-light difference exclusion hue saturation color luminosity)
+ linear-dodge, linear-burn,
dissolve (PSD). Корректирующие слои
(pt:adjust) и текстовые (pt:text) хранят
параметры + кэш растра @src для просмотрщиков. Слои больше
16 384 px по стороне режутся на тайлы
(src="layers/<uuid>/" — папка
tile-<x>-<y>.png,
tile="1024").
3.6 Notes — .mnx
Одна записная книжка = один пакет; разделы и страницы — отдельные части для быстрого открытия и синхронизации по страницам.
notebook.xml n:notebook → n:section* → n:page-ref* (порядок, заголовок, цвет, теги)
pages/<uuid>.xml n:page: холст свободной компоновки
ink/<uuid>.bin рукописные штрихи (бинарно: точки x,y,p,t дельта-кодированы, zstd)
media/ вложения, вырезки, аудио
<n:page xmlns:n="urn:meridian:office:notes:1" xmlns:t="urn:meridian:office:text:1" xmlns:tb="urn:meridian:office:table:1"
id="…" title="Встреча 3 октября" created="…" modified="…" width="auto" background="rule:lined" level="0" parent="…">
<n:outline id="…" x="914400" y="914400" width="5486400">
<t:p style="Normal"><t:r>Решения:</t:r></t:p>
<t:p list="L1" level="0"><t:r>Запустить корпус тестов</t:r><n:tag kind="todo" done="0"/></t:p>
<tb:table>…</tb:table>
</n:outline>
<n:ink id="…" src="ink/0192….bin" x="…" y="…" width="…" height="…" color="#3B5BDB" width-pt="12700" tool="pen"/>
<n:clip id="…" src-url="https://…" captured="…" x="…" y="…"><t:p>…</t:p></n:clip>
<n:attachment id="…" src="media/doc-….pdf" name="Договор.pdf" x="…" y="…" preview="media/doc-…-p1.png"/>
<n:audio id="…" src="media/rec-….ogg" duration="PT12M"/>
</n:page>3.7 Base — .mbx (SQLite)
.mbx — обычный файл SQLite 3 (заголовок
SQLite format 3\0),
application_id = 0x4D524442 («MRDB»),
user_version = версия схемы. Пользовательские таблицы —
обычные; всё служебное — в таблицах с префиксом
meridian_:
-- Служебные таблицы Meridian Base / Meridian Base system tables
CREATE TABLE meridian_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL); -- format-version, document-id (UUID v7), created, app
CREATE TABLE meridian_objects (
id TEXT PRIMARY KEY, -- UUID v7
kind TEXT NOT NULL CHECK (kind IN ('form','query','report','script','macro','relation-diagram','view-settings')),
name TEXT NOT NULL UNIQUE,
xml TEXT NOT NULL, -- определение в urn:meridian:office:base:1 (форма/отчёт — g:/t:-разметка, запрос — SQL + визуальная модель)
modified TEXT NOT NULL, -- ISO 8601
author TEXT
);
CREATE TABLE meridian_table_props ( -- то, чего нет в SQL-схеме: подписи, форматы, подстановки, валидация
table_name TEXT NOT NULL, column_name TEXT, -- NULL = свойства таблицы
prop TEXT NOT NULL, value TEXT NOT NULL, -- caption, description, format, input-mask, lookup, default-expr, validation-rule, hidden, width
PRIMARY KEY (table_name, column_name, prop)
);
CREATE TABLE meridian_relations ( -- связи и ссылочная целостность поверх FOREIGN KEY (для отображения и каскадов Access-стиля)
id TEXT PRIMARY KEY, from_table TEXT, from_cols TEXT, to_table TEXT, to_cols TEXT, kind TEXT, on_update TEXT, on_delete TEXT, enforced INTEGER
);
CREATE TABLE meridian_lookups (table_name TEXT, column_name TEXT, source_sql TEXT, bound_col INTEGER, display_cols TEXT, PRIMARY KEY (table_name, column_name));
CREATE TABLE meridian_connections (id TEXT PRIMARY KEY, name TEXT, driver TEXT, dsn TEXT, options TEXT); -- секреты — в хранилище ОС, здесь только ссылка
CREATE TABLE meridian_history (seq INTEGER PRIMARY KEY, at TEXT, author TEXT, kind TEXT, object_id TEXT, payload BLOB); -- журнал изменений объектов (не данных)
CREATE TABLE meridian_media (id TEXT PRIMARY KEY, mime TEXT, name TEXT, data BLOB); -- вложения форм/отчётовТипы столбцов — SQLite STRICT-таблицы там, где возможно
(INTEGER REAL TEXT BLOB ANY), логические типы Base (дата,
деньги, логическое, вложение, гиперссылка, вычисляемое) — через
meridian_table_props.prop='type'. Файл открывается любым
SQLite-клиентом, служебные таблицы не мешают.
3.8 Forms — .mfx /
.mft
<f:form xmlns:f="urn:meridian:office:forms:1" xmlns:t="urn:meridian:office:text:1" id="…" title="Опрос сотрудников"
theme="themes/blue.xml" collect="anonymous" one-response="1" accept-until="2026-10-31T23:59:59+03:00" shuffle="0" progress="1">
<f:description><t:p>Займёт 3 минуты.</t:p></f:description>
<f:section id="…" title="Общее">
<f:question id="q1" type="choice" required="1" multiple="0" other="1"><f:text>Отдел</f:text>
<f:option id="o1">Продажи</f:option><f:option id="o2">Разработка</f:option>
<f:branch option="o2" goto="sec-dev"/>
</f:question>
<f:question id="q2" type="scale" min="1" max="5" min-label="плохо" max-label="отлично"><f:text>Оцените рабочее место</f:text></f:question>
<f:question id="q3" type="text" long="1" max-length="2000" validate="none"><f:text>Комментарий</f:text></f:question>
<f:question id="q4" type="grid" rows="Срок;Качество" cols="1;2;3;4;5"><f:text>Оцените проекты</f:text></f:question>
<f:question id="q5" type="date"/><f:question id="q6" type="file" max-size="10485760" accept="pdf,png"/>
</f:section>
<f:section id="sec-dev" title="Разработка">…</f:section>
<f:responses target="responses.msx" sheet="Ответы" map="auto"/> <!-- ответы хранятся вне формы -->
<f:thanks><t:p>Спасибо!</t:p></f:thanks>
</f:form>Типы вопросов:
choice, checkbox, dropdown, text, number, scale, rating, date, time, file, grid, ranking, likert, nps, section-break.
Ответы в форму не записываются (приватность, размер) — только ссылка на
книгу Sheets или CSV; схема ответов в книге фиксирована
(timestamp, respondent, q<id>…).
3.9 Math — .meq
Пакет MOP с единственной частью содержимого:
<mq:formula xmlns:mq="urn:meridian:office:math:1" xmlns="http://www.w3.org/1998/Math/MathML" id="…" display="block" numbering="(1)">
<mq:source lang="latex">\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}</mq:source> <!-- исходник пользователя, первичен при редактировании -->
<math display="block"> <!-- Presentation MathML 3 (всегда) -->
<mrow><msubsup><mo>∫</mo><mn>0</mn><mi>∞</mi></msubsup><msup><mi>e</mi><mrow><mo>−</mo><msup><mi>x</mi><mn>2</mn></msup></mrow></msup>
<mo></mo><mi>d</mi><mi>x</mi><mo>=</mo><mfrac><msqrt><mi>π</mi></msqrt><mn>2</mn></mfrac></mrow>
</math>
<mq:layout font="STIX Two Math" size="152400" style="tex" break="auto"/>
</mq:formula>Та же разметка (t:math с вложенным
mq:formula) встраивается в Write/Slides/Sheets/Notes.
Content MathML и OMML не хранятся — генерируются при экспорте; OMML из
docx конвертируется в Presentation MathML + восстановленный
LaTeX/UnicodeMath (§4.7).
3.10 Mail — .mmx
Архив (экспорт/перенос/резервная копия), не живое хранилище (живое — профиль Mail в SQLite + maildir):
mimetype application/vnd.meridian.mail
manifest.xml, meta.xml
folders.xml mm:folder* (иерархия, тип: inbox/sent/…/calendar/contacts/tasks, счётчики)
mail/<folder-uuid>/<uuid>.eml RFC 5322 как есть (включая вложения MIME); большие вложения можно вынести: mail/…/<uuid>.eml + attachments/<sha256>
calendar/<uuid>.ics RFC 5545, один VCALENDAR на календарь
contacts/<uuid>.vcf RFC 6350 vCard 4.0, по одному файлу на контакт (или один файл на адресную книгу)
tasks/<uuid>.ics VTODO
index.sqlite метаданные: flags, labels, thread-id, message-id ↔ путь, FTS5-индекс тела и вложений
index.sqlite восстанавливается из
.eml/.ics/.vcf при повреждении;
поэтому он — кэш, а не источник истины.
3.11 Сравнение с OOXML и ODF
| Понятие | OOXML | ODF | MOP |
|---|---|---|---|
| Контейнер | OPC ZIP, [Content_Types].xml,
_rels/*.rels |
ZIP, mimetype первым,
META-INF/manifest.xml |
ZIP, mimetype первым, manifest.xml с
хэшами и ролями; без rels — ссылки прямыми путями |
| Единицы | twip, half-point, EMU, 1/8 pt, символы (ширина столбца) | строки с единицами (cm, pt,
in) |
только EMU (целые) |
| Абзац / прогон | w:p / w:r + w:rPr +
w:t |
text:p / text:span + автостили |
t:p / t:r, свойства — атрибуты |
| Стили | w:style по типам, docDefaults,
latentStyles |
style:style + automatic-styles +
default-style |
m:style/@kind, m:defaults, без автостилей
(auto="1" — оптимизация) |
| Секции | w:sectPr в конце секции |
style:master-page + text:section |
w:section в начале, колонтитулы внутри |
| Колонтитулы | отдельные части header1.xml + rels |
внутри style:master-page |
внутри w:section |
| Нумерация | w:abstractNum + w:num +
lvlOverride |
text:list-style + вложенные text:list |
t:list-style + t:list (плоская привязка
абзаца) |
| Таблицы | w:tbl / w:tr / w:tc,
gridSpan, vMerge |
table:table, covered-table-cell,
number-columns-spanned |
tb:table, явные
colspan/rowspan |
| Сноски | footnotes.xml + w:footnoteReference |
text:note внутри текста |
w:notes в content.xml +
t:note-ref |
| Правки | w:ins/w:del/w:rPrChange +
rsid |
text:tracked-changes +
text:change-start/end |
t:ins/t:del/t:change/t:move-*,
без rsid |
| Комментарии | comments.xml, commentsExtended.xml,
people.xml |
office:annotation внутри текста |
comments.xml + маркеры; авторы в
meta.xml |
| Поля | w:fldSimple / w:fldChar +
w:instrText |
text:page-number, text:date … (элемент на
тип) |
t:field/@type + @instr +
t:result |
| Ячейка | c/@r="B2" @t @s, v, f,
is |
table:table-cell с office:value-type,
table:number-columns-repeated |
s:c/@c @t @s @v, s:f, s:cs,
s:rows |
| Строки | sharedStrings.xml (всегда) + inline |
только inline | общие при повторах + inline |
| Форматы чисел | numFmt коды |
number:*-style элементы |
коды Excel |
| Формулы | строка f (en-US, _xlfn.) |
OpenFormula of:= |
каноническая строка (§6.6) |
| Диаграммы | c:chart (DrawingML) + cx: |
chart:chart |
ch:chart — один модуль для всех приложений |
| Графика | DrawingML (a:, wp:, pic:) +
VML + mc:AlternateContent |
draw:* |
g:*, без VML; альтернативы — в m:x |
| Анимации | p:timing дерево узлов |
anim:* (SMIL) |
плоский список p:anim |
| Темы | theme1.xml + clrSchemeMapping |
нет (цвета явные) | m:theme в styles.xml,
theme(slot, mod) |
| Шифрование | CFB + Agile (AES-CBC, SHA-512) | ZIP-записи Blowfish/AES (manifest:encryption-data) |
пакет целиком AES-256-GCM + Argon2id |
| Подписи | _xmlsignatures/, XMLDSig |
META-INF/documentsignatures.xml, XAdES |
signatures/, XAdES |
| История | нет | нет | history/ (CRDT) |
| Неизвестное | mc:Ignorable, extLst |
расширения в чужих NS (сохраняются LibreOffice) | m:x, foreign/ |
| Идентификаторы | w:id числовые, w14:paraId,
rId |
xml:id |
UUID v7 |
Упростили: единицы; сериализацию свойств (атрибуты
вместо вложенных элементов-флагов); одну форму строк;
колонтитулы/сноски/списки в одном файле; отсутствие rels и
content-types; таблицы без ячеек-продолжений; плоские анимации; стили
без индексных массивов. Добавили: история CRDT и вехи;
стабильные UUID; хэши частей и роли в манифесте; «мешок неизвестного» с
якорями и хэшами; паспорт происхождения (m:provenance);
метрики шрифтов для подмены; профиль совместимости раскладки; шифрование
с несколькими ключами-шифровальщиками (пароль + сертификат +
корпоративное восстановление).
4. Чужие форматы
Обозначения: R/W — полная поддержка, R — чтение, R* — чтение с ограничениями, E — экспорт (по таблице 3.3 библии). Уровень — целевой уровень точности (§1.2). Фаза — по разделу 7 библии. Приоритет — P0/P1/P2 внутри фазы.
4.1 Write
| Формат | Режим | Уровень | Фаза / приоритет |
|---|---|---|---|
.docx .docm .dotx |
R/W | V + R | 1 / P0 |
.doc |
R/W (запись — Word 97-2003 подмножество) | V (чтение), S (запись) | 1 / P0 чтение, P1 запись |
.rtf |
R/W | V | 1 / P0 |
.odt .ott |
R/W | V + R | 1 / P0 |
.txt .md .html |
R/W | T / S | 1 / P0 |
.fb2 .epub |
R/W | S | 1 / P1 |
.pdf .pdf/a |
E (импорт — через Meridian PDF «PDF → Write», фаза 4) | V | 1 / P0 |
.pages |
R | S | 2 / P1 |
.wpd .abw .wps |
R | S | 2 / P2 |
.xml (DocBook) .tex (подмножество) |
R | S | 2 / P2 |
.tex .png/.svg страниц |
E | — | 1 / P1 |
DOCX / DOCM / DOTX (WordprocessingML, ECMA-376 / ISO 29500,
Transitional и Strict). Структура: OPC-пакет;
[Content_Types].xml, _rels/.rels →
word/document.xml (главная часть, w:body),
word/styles.xml, word/numbering.xml,
word/settings.xml, word/fontTable.xml,
word/theme/theme1.xml,
word/header<n>.xml /
footer<n>.xml, word/footnotes.xml /
endnotes.xml, word/comments.xml (+
commentsExtended.xml, commentsIds.xml,
people.xml), word/media/*,
word/embeddings/*, word/glossary/document.xml
(автотекст), word/vbaProject.bin (docm),
customXml/, docProps/core.xml /
app.xml / custom.xml,
word/webSettings.xml,
word/_rels/document.xml.rels (все связи по
r:id). Отображение: w:p→t:p,
w:r→t:r (с объединением соседних прогонов с одинаковыми
свойствами), w:sectPr→w:section (переносится из конца
секции в начало), w:tbl→tb:table,
w:drawing→g:anchor/g:inline,
w:fldSimple/w:fldChar-последовательности →
t:field, w:sdt→t:control,
w:bookmarkStart/End→t:bookmark-*,
w:footnoteReference→t:note-ref,
w:ins/w:del→t:ins/t:del, w:hyperlink→t:link,
m:oMath→t:math (OMML → MathML), стили → §6.1, нумерация →
§6.4. Известные трудности:
| Проблема | Решение |
|---|---|
Нумерация:
abstractNum/num/lvlOverride/startOverride,
numStyleLink, legacy w:lvl/@w:tentative,
перезапуск после уровня (lvlRestart), «нумерация в стиле»
(w:numPr внутри w:pPr стиля) |
полный резолвер §6.4; нумерация стилевая → t:ppr/@list
в стиле |
Стили таблиц: условные части w:tblStylePr/@w:type,
w:cnfStyle битовые маски на строках/ячейках,
w:tblLook |
tb:part/@type, look= на таблице;
cnfStyle пересчитывается при раскладке, при round-trip
сохраняется в m:x-attrs |
Поля: вложенные (IF внутри MERGEFIELD),
многопрогонные fldChar begin/separate/end, «грязные»
поля |
сборка по стеку в t:field с вложенными
t:field; dirty → пересчёт при открытии по
настройке |
SmartArt (DrawingML diagrams: diagrams/data1.xml —
модель dgm:dataModel, layout1.xml,
colors1.xml, quickStyle1.xml,
drawing1.xml — кэш отрисовки dsp:) |
Импорт: data1.xml → наш модуль «Диаграммы»
(g:diagram с узлами и связями, макет по
layout1.xml для 20 самых частых макетов), при неизвестном
макете — кэш drawing1.xml как группа фигур; все 5 частей →
foreign/ для round-trip |
VML (w:pict/v:shape, старые текстовые поля, водяные
знаки, v:imagedata) |
конвертер VML → g:* (подмножество: v:rect,
v:oval, v:line, v:shape с
v:path, v:textbox, v:imagedata,
o:OLEObject); mc:AlternateContent — берём
mc:Choice с DrawingML, Fallback в
m:x |
OLE-объекты (w:object,
embeddings/oleObject1.bin CFB,
.xlsx/.pptx как пакеты) |
пакеты Office → objects/*.msx (конвертация) + оригинал
в foreign/; прочее OLE → g:object с превью
(EMF/WMF → растр) |
Темы: theme1.xml (a:clrScheme,
a:fontScheme major/minor + a:font script=,
a:fmtScheme), w:clrSchemeMapping,
w:themeColor/themeTint/themeShade,
w:asciiTheme |
§6.8; шрифтовые пары темы → `font="theme:major |
Совместимость раскладки:
w:compat/w:compatSetting name="compatibilityMode" val="15",
w:doNotExpandShiftReturn и ещё ~70 флагов |
settings/compat.layout-profile + полный список флагов в
m:group name="compat.ooxml" |
w:rsid*, w14:paraId,
w14:textId |
не моделируются; paraId сохраняется в
m:x-attrs для стабильности ссылок комментариев |
Strict vs Transitional (разные namespace,
http://purl.oclc.org/ooxml/…) |
единый импортёр с таблицей замены пространств имён; запись — Transitional (как Word) |
Защита документа (w:documentProtection, устаревшее
XOR-хэширование, w:cryptProviderType) |
хранится в settings; не «защищает», а переводит в режим
ограничений UI; хэш сохраняется для round-trip |
Шифрование (CFB с
EncryptionInfo/EncryptedPackage,
Standard/Agile, RC4 CryptoAPI для .doc) |
модуль offcrypto: Agile (AES-128/256 CBC,
SHA-1/256/384/512, spinCount) и Standard (AES ECB + SHA-1); запись —
только Agile AES-256/SHA-512, 100 000 итераций |
| Макросы VBA (docm) | vbaProject.bin → foreign/ (essential),
транслятор VBA→MeridianScript для простых макросов (фаза 7),
предупреждение в UI |
DOC (MS-DOC, Word 97-2003, Word 6/95 — R*).
Структура: OLE2/CFB (MS-CFB: заголовок, FAT, mini-FAT, каталог) с
потоками WordDocument (FIB + текст),
0Table/1Table (выбор по
fib.base.fWhichTblStm), Data,
ObjectPool/, \x05SummaryInformation,
\x05DocumentSummaryInformation, Macros/.
Ключевое: FIB (FibBase,
FibRgFcLcb97/2000/2002/2003/2007), piece
table (CLX → Pcdt →
PlcPcd: отображение CP → FC, бит fCompressed —
CP1252 или UTF-16LE), форматирование абзацев/символов —
PlcBteChpx/PlcBtePapx → FKP-страницы 512 байт
→ Chpx/Papx со
sprm-операторами, стили — STSH, секции —
PlcfSed, таблицы — через свойства абзацев
(sprmPFInTable, sprmPFTtp, вложенные —
sprmPTableDepth), поля — PlcFld
(fld с типом в flt), сноски —
PlcffndRef/PlcffndTxt, закладки —
PlcfBkf/PlcfBkl, списки —
PlfLst/PlfLfo, рисунки — PlcSpa +
Office Art (MS-ODRAW) в Data, правки —
свойства символов sprmCFRMark/sprmCIbstRMark/
sprmCDttmRMark в Chpx (отдельной таблицы правок нет; авторы
— в SttbfRMark), шифрование — fEncrypted +
XOR/RC4/CryptoAPI. Отображение: текст по кускам piece table → поток
символов с границами абзацев (\r), затем применение sprm по
диапазонам (CHPX/PAPX) → t:p/t:r; sprm-коды
сопоставлены с OOXML-свойствами (таблица из MS-DOC §2.6). Трудности:
Word 95/6 — другая FIB и кодовые страницы (R* через
libwv-подобный путь); «быстрое сохранение»
(fComplex) с фрагментированной piece table; Office Art в
бинарной форме (OfficeArtSpContainer,
OfficeArtFOPT); таблицы без явной сетки (вычисляются по
sprmTDefTable); поля без результатов. Запись
.doc: подмножество (текст, стили, таблицы, рисунки, сноски,
поля, колонтитулы) — всё, что не поддерживается, в паспорт; приоритет
P1.
RTF (Rich Text Format 1.9.1). Структура: текст с
управляющими словами \word<N>, группами
{…}, \'hh (кодовая страница по
\ansicpg), \uN (Unicode с \ucN
пропуском), таблицы шрифтов \fonttbl, цветов
\colortbl, стилей \stylesheet, списков
\*\listtable/\*\listoverridetable, правок
\revtbl, \*\… — игнорируемые назначения,
картинки \pict (hex) и объекты \object,
таблицы — построчно (\trowd … \cell … \row, вложенные —
\itap), поля \field{\*\fldinst …}{\fldrslt …},
сноски \footnote, колонтитулы \header, секции
\sect, свойства документа \info, формулы —
OMML в \mmath. Отображение: потоковый парсер со стеком
состояний (копия состояния при {), применяемый к той же
модели t:p/t:r; списки RTF → t:list-style
(структура повторяет Word). Трудности: RTF от разных генераторов (Word,
LibreOffice, Apple TextEdit, WordPad, 1С) — разный диалект; вложенные
таблицы (Word 2003+ \nesttableprops); картинки без размеров
(\picwgoal); «старые» RTF без \uN — только
кодовая страница; очень большие файлы (hex-картинки по 100 МБ) —
потоковая запись медиа мимо DOM.
ODT / OTT (ODF 1.3 Text). Структура: ZIP,
mimetype
(application/vnd.oasis.opendocument.text),
META-INF/manifest.xml (manifest:file-entry),
content.xml (office:document-content →
office:automatic-styles,
office:body/office:text), styles.xml
(office:styles, office:automatic-styles для
страниц, office:master-styles), meta.xml,
settings.xml, Pictures/,
Thumbnails/thumbnail.png, Configurations2/ (UI
LibreOffice — в foreign/), Basic/ (макросы —
foreign/). Отображение:
text:p/text:h→t:p (у
text:h — outline-level),
text:span→t:r,
automatic-styles разрешаются в прямое
форматирование (стиль P12 с parent
Standard → t:p style="Standard" align=…),
text:list (вложенные) → плоские list/level
(§6.4), table:table→tb:table
(table:table-column → tb:col,
covered-table-cell отбрасываются,
number-columns-spanned→colspan),
style:master-page+style:page-layout→w:section
(смена мастера на абзаце = новая секция),
text:note→t:note-ref+w:note,
office:annotation→комментарий,
text:tracked-changes→t:ins/t:del/t:change,
draw:frame→g:anchor/g:inline,
text:page-number и прочие элементы полей →
t:field/@type, text:bookmark→закладки,
text:sequence→SEQ,
text:table-of-content → TOC,
draw:object с math:math → t:math.
Трудности: автоматические стили при экспорте (обратная задача: собрать
прямое форматирование в automatic-styles с дедупликацией);
text:soft-page-break (игнорируем);
style:text-properties с тройками атрибутов
(fo:font-size, style:font-size-asian,
style:font-size-complex) ↔︎ наши
size/size-cs; ODF-формулы в полях
(text:formula); text:list c
text:continue-numbering; «изменение мастер-страницы» как
свойство абзаца; шифрование ODF (manifest:encryption-data,
Blowfish CFB / AES-256 CBC, PBKDF2 / Argon2id в ODF 1.4 — уточнить);
подписи META-INF/documentsignatures.xml.
TXT / Markdown / HTML. TXT: детект кодировки (BOM →
UTF-8/16; иначе эвристика chardetng; для ru —
CP1251/KOI8-R/CP866), концы строк, табуляции; запись — UTF-8 без BOM по
умолчанию. Markdown: CommonMark 0.31 + GFM (таблицы, зачёркивание,
списки задач, сноски, автоссылки) + математика $…$;
отображение на стили Heading1..6, Code,
Quote, списки → t:list; обратный экспорт —
лучший эквивалент. HTML: html5ever → DOM → CSS-каскад
(подмножество CSS 2.1 + flex не поддерживается) → блоки/инлайны →
t:p/t:r/tb:table;
.mht/.mhtml — через MIME. Экспорт HTML — семантический
(стили → классы в <style>, без
<font>), опция «как в Word» для совместимости с
почтовыми клиентами.
FB2 (FictionBook 2.1). XML:
FictionBook/description (метаданные: title-info,
document-info, publish-info), body (+
body name="notes"), section (вложенные),
title, p, empty-line,
poem/stanza, cite,
epigraph, table,
image l:href="#id", binary id (base64).
Отображение: section→заголовки по глубине,
body name="notes" + a type="note" → сноски
w:note, binary→media/. Трудности:
вложенные секции произвольной глубины, кодировки CP1251,
.fb2.zip. Запись: по тем же правилам (стили Heading →
section).
EPUB (3.3; чтение также 2.0.1). ZIP,
mimetype = application/epub+zip,
META-INF/container.xml → *.opf (package
document: metadata, manifest,
spine, nav), XHTML-главы, CSS, шрифты,
toc.ncx (EPUB 2). Отображение: главы последовательно →
секции (break="next-page"), CSS через HTML-путь,
nav→оглавление (TOC-поле), сноски
epub:type="footnote" → w:note. Экспорт: главы
по заголовкам уровня 1, reflowable; fixed-layout — не поддерживается.
Трудности: DRM (не открываем, сообщение), шрифты с обфускацией
(IDPF-алгоритм — поддерживаем), CSS-раскладка, которая не ложится на
модель абзацев (многоколоночные, float).
Pages (iWork ’13+). ZIP (или папка-пакет на macOS):
Index/Document.iwa,
Index/DocumentStylesheet.iwa,
Index/Metadata.iwa,
Index/AnnotationAuthorStorage.iwa,
Metadata/Properties.plist,
Metadata/DocumentIdentifier, Data/ (медиа),
preview.jpg. IWA: чанки «0x00 + 3 байта длины
LE» → Snappy без стандартного framing → поток
protobuf-сообщений (ArchiveInfo с
MessageInfo{type, version, object_references} + payload);
типы — из .proto, восстановленных из бинарников iWork (TSP,
TSK, TSWP — текст, TSD — drawables, TST — таблицы, TP — Pages).
Отображение: TSWP.StorageArchive (текст + таблицы атрибутов
по диапазонам) → t:p/t:r; стили —
TSWP.ParagraphStyleArchive/CharacterStyleArchive;
плавающие объекты — TSD.DrawableArchive; таблицы —
TST.TableModelArchive (с tile-хранилищем ячеек). Трудности:
отсутствие официальной спецификации (всё — реверс:
obriensp/iWorkFileFormat, dunhamsteve/iwork, pyiwa); версии протоколов
меняются с каждым релизом (сохраняем номер версии и падаем в «только
текст + картинки»); старый формат Pages ’09 (XML index.xml
в ZIP) — отдельный R*-путь.
WPD / ABW / WPS / DocBook / TeX. WPD (WordPerfect
5.x–X): сигнатура FF 57 50 43 («ÿWPC»), заголовок с
индексом, поток байтов с функциональными кодами (библиотека-ориентир —
libwpd); R* текст+форматирование. ABW (AbiWord): XML
(abiword/section/p/c), часто gzip (.zabw). WPS
(MS Works 4–9): CFB с потоками CONTENTS/MN0,
структура близка к старому Word — R* через libwps-подобный путь. DocBook
4/5: book/chapter/section/para/table → стили; TeX:
подмножество (\section, \textbf,
itemize, tabular, математика →
t:math) через собственный разборщик; экспорт TeX — только
структурный.
Экспорт PDF/PDF-A — через Meridian Print (список отображения → PDF 1.7/2.0; PDF/A-2b/3b, PDF/UA с тегами из структуры документа); экспорт страниц в PNG/SVG — через Meridian Render.
4.2 Sheets
| Формат | Режим | Уровень | Фаза / приоритет |
|---|---|---|---|
.xlsx .xlsm .xltx |
R/W | V + R | 2 / P0 |
.xls |
R/W | V (чтение), S (запись BIFF8) | 2 / P0 чтение, P1 запись |
.ods .ots |
R/W | V + R | 2 / P0 |
.csv .tsv |
R/W | T | 2 / P0 |
.xlsb |
R | S | 2 / P1 |
.numbers |
R | S | 2 / P1 |
.gnumeric .dif .sylk
.dbf |
R | S | 2 / P2 |
.parquet .json .xml |
R | T | 2 / P2 |
.pdf .html .csv
.json .parquet .png диаграмм |
E | — | 2 / P0 (pdf, csv), P1 остальные |
XLSX / XLSM / XLTX (SpreadsheetML). Структура:
xl/workbook.xml (листы, definedNames,
workbookPr/@date1904, calcPr),
xl/worksheets/sheet<n>.xml
(sheetData/row/c, mergeCells,
conditionalFormatting, dataValidations,
hyperlinks, pageSetup,
autoFilter, sheetProtection,
drawing r:id, tableParts, extLst
с x14: условным форматированием и валидациями),
xl/sharedStrings.xml (sst/si с t
или r rich-прогонами), xl/styles.xml
(numFmts, fonts, fills,
borders, cellStyleXfs, cellXfs —
индексы c/@s, cellStyles, dxfs,
tableStyles, colors/indexedColors),
xl/theme/theme1.xml, xl/calcChain.xml (порядок
вычислений — при импорте игнорируется, при экспорте не пишется: Excel
пересчитывает), xl/tables/table<n>.xml,
xl/pivotTables/pivotTable<n>.xml +
xl/pivotCache/pivotCacheDefinition<n>.xml +
pivotCacheRecords<n>.xml,
xl/charts/chart<n>.xml (+ chartEx для
новых типов), xl/drawings/drawing<n>.xml
(xdr:twoCellAnchor), xl/comments<n>.xml
+ VML vmlDrawing<n>.vml (размер/позиция заметок!),
xl/threadedComments/, xl/persons/,
xl/externalLinks/, xl/metadata.xml
(динамические массивы), xl/vbaProject.bin,
xl/connections.xml, xl/queryTables/,
customXml/. Отображение: row/@r,
c/@r → s:row/@r, s:c/@c
(буквенный адрес → номер); типы c/@t:
n→n, s→s (индекс в
strings.xml после дедупликации),
str→str, b, e,
inlineStr→str/rich,
d→дата; f → каноническая формула (§6.6):
_xlfn./_xlfn._xlws. удаляются,
t="shared" → @shared, t="array" +
ref → @array, c/@cm +
metadata.xml (XLDAPR,
fDynamic="1") → @dynamic="1";
cellXfs[s] → стиль ячейки (s:cpr) с
дедупликацией в m:style auto; dxfs →
cell-diff; definedNames → s:names
(_xlnm.Print_Area → builtin="print-area");
sheetView (pane, selection) →
s:view; cols → s:cols (ширина из
«символов» → EMU по формуле §6.3); pivotTableDefinition →
s:pivot; table → s:table;
conditionalFormatting +
x14:conditionalFormatting (объединение по
x14:id) → s:cf; legacyDrawing
(VML) → размеры заметок; drawing → s:drawings
(twoCellAnchor/oneCellAnchor/absoluteAnchor).
Трудности:
| Проблема | Решение |
|---|---|
Shared strings: si с r-прогонами и
rPh (фуригана), phoneticPr; индекс строк в
c/@v |
rich + s:rt; фонетика →
s:rt/@ruby; при записи — дедупликация с хэш-таблицей, лимит
2^31 |
styles.xml: индексы cellXfs, флаги
applyFont и т.п., indexedColors, старые
палитры, numFmtId встроенные (0–49, 164+ пользовательские,
локализованные встроенные 5–8 для валют) |
таблица встроенных форматов по локали; applyX
игнорируются при чтении, при записи всегда 1 |
calcChain.xml |
не читается; при записи удаляется из foreign/ (иначе
Excel ругается при несогласованности) |
Динамические массивы: cm="1",
metadata.xml, _xlfn._xlws.SORT/FILTER/UNIQUE,
#SPILL!, оператор # (A1#),
неявное пересечение @ (_xlfn.SINGLE) |
@dynamic, функции без префиксов в каноне; при записи —
префиксы и metadata.xml генерируются из таблицы функций с
версией появления (§6.6) |
Сводные кэши: pivotCacheDefinition (поля,
sharedItems), pivotCacheRecords (записи по
индексам), кэш может быть больше данных |
переносим в pivots/cache-<n>.bin (колонки +
словари), пересчёт по требованию |
Внешние ссылки [1]Sheet1!A1 →
externalLink<n>.xml с кэшем |
s:external-links + external/x<n>.xml
кэш; формулы — [x1]Sheet1!A1 → канонически
'[budget.msx]Sheet1'!A1 |
Условное форматирование x14: (иконки, data bars с
отрицательными, cfvo формулы), приоритеты,
stopIfTrue |
слияние базового и x14 правил; priority
сохраняется |
Диаграммы: c:chart с кэшами, c:extLst,
cx:chartSpace (2016+) |
ch:chart; cx: — импорт 6 типов, иначе
картинка-превью из foreign/ |
Защита листа (хэши
algorithmName/hashValue/saltValue/spinCount
или legacy 16-бит) |
хранится как есть; защита — только UI-ограничения |
Даты: date1904, число 60 = «29.02.1900» |
§6.7 |
| Очень большие файлы (1 048 576 × 16 384, 500 МБ XML) | потоковый разбор sheetData (SAX), ячейки — в
колонно-блочное хранилище ядра, строки общих — отображение
mmap; память ≤ 3× размера данных |
«Rich value» (2022+: изображения в ячейках
xl/richData/, типы данных «Акции», vm= на
ячейке) |
foreign/ + значение-заглушка (текст/картинка), паспорт
«потеряны богатые типы данных» |
Формулы с локализованными именами — нет (в файле всегда en-US), но
разделители в c/@v? |
нет: v всегда инвариантен |
XLS (BIFF8, Excel 97-2003; BIFF5/BIFF2-4 — R*).
Структура: CFB с потоком Workbook (BIFF8) или
Book (BIFF5), \x05SummaryInformation,
_VBA_PROJECT_CUR/, MBD… (OLE-объекты). Записи
[id:u16][len:u16][data], продолжения CONTINUE;
глобальная секция (BOF типа workbook,
CODEPAGE, DATEMODE (1904), FONT,
FORMAT, XF, STYLE,
PALETTE, BOUNDSHEET (смещения листов),
SST (общие строки с Unicode/compressed флагами, rich-run'ы,
расширения), EXTERNSHEET/SUPBOOK (внешние
ссылки), NAME (имена, формулы в RPN),
WINDOW1), затем листы (BOF worksheet,
DIMENSIONS, ROW, COLINFO,
RK/MULRK/NUMBER/LABELSST/BOOLERR/BLANK/MULBLANK/FORMULA(+STRING/ARRAY/SHRFMLA/TABLE),
MERGEDCELLS, CF/CONDFMT,
DV/DVAL, HLINK,
NOTE+TXO+OBJ,
WINDOW2, PANE, SETUP,
MSODRAWING (Office Art), CHART-подпоток,
PIVOT*-записи (SXVIEW, SXVD, …),
FEAT/FEATHEADR (защита, таблицы)). Формулы —
RPN-токены Ptg* (PtgRef,
PtgArea, PtgRef3d, PtgFunc,
PtgFuncVar, PtgAdd…, PtgStr,
PtgInt, PtgNum, PtgName,
PtgMemFunc, PtgAttr для
IF/CHOOSE/пробелов), декодируются стековой
машиной в AST (§6.6). Отображение: как xlsx после декодирования записей;
XF (cell XF с ixfParent → style XF) → стили;
RK (упакованные числа: ×100, целые) → n;
PALETTE (56 цветов, индексы 8–63) → sRGB. Трудности: BIFF5
и ниже (кодовые страницы, без SST); «CONTINUE» внутри строк SST с
переключением флага Unicode; шифрование RC4/XOR (FILEPASS);
Office Art в MSODRAWING/MSODRAWINGGROUP с
разбиением по записям; диаграммы в BIFF (CHART,
SERIES, AI …) — импорт 8 основных типов;
запись .xls — только подмножество (без сводных, без
x14).
XLSB (MS-XLSB, BIFF12). OPC-пакет как xlsx, но части
.bin: xl/workbook.bin,
xl/worksheets/sheet<n>.bin,
xl/sharedStrings.bin, xl/styles.bin; записи
[id: varint 1–2 байта][len: varint 1–4 байта][data];
BrtBeginSheetData → BrtRowHdr (строка: номер,
высота, стиль, флаги) → ячейки
BrtCellBlank/BrtCellRk/BrtCellError/BrtCellBool/BrtCellReal/BrtCellSt/BrtCellIsst
(индекс SST)/
BrtFmlaString/BrtFmlaNum/BrtFmlaBool/BrtFmlaError
(формулы — в RPN как в BIFF8, с расширениями
PtgList для таблиц), BrtShrFmla,
BrtArrFmla, BrtMergeCell,
BrtBeginAFilter, BrtCellMeta (динамические
массивы). Отображение — общий с BIFF8 декодер RPN; стили —
BrtXF/BrtFont/BrtFill/BrtBorder.
Приоритет P1 (встречается в корпоративных отчётах); только чтение.
ODS / OTS (ODF Spreadsheet).
content.xml:
office:spreadsheet/table:table/table:table-row/table:table-cell
с office:value-type
(float|string|date|time|boolean|percentage|currency),
office:value,
table:formula="of:=SUM([.A1:.B2])" (OpenFormula, ISO/IEC
26300-2),
table:number-columns-repeated/number-rows-repeated
(сжатие пустых областей — ровно наши
s:cs/s:rows),
table:covered-table-cell, table:table-column
(style:column-width,
table:default-cell-style-name), стили ячеек в
automatic-styles
(style:style family="table-cell" +
style:data-style-name → number:number-style),
условное форматирование — style:map (старое) и
calcext:conditional-formats (LibreOffice),
table:database-ranges (автофильтр),
table:named-expressions,
table:data-pilot-tables (сводные), диаграммы —
draw:frame/draw:object с вложенным chart:chart
(отдельный подпакет Object 1/), валидации
table:content-validations, печать —
style:page-layout + table:print-ranges.
Отображение: OpenFormula → AST (своя грамматика: [.A1],
[Sheet.A1:.B2], ; разделитель,
$-абсолюты) → канон; number-*-style → коды
Excel; repeated → серии. Трудности: экспорт автостилей;
формулы с функциями LibreOffice (ORG.OPENOFFICE.*,
COM.MICROSOFT.* префиксы) — таблица соответствий; даты как
ISO-строки (office:date-value) → серийные числа;
table:table-cell с text:p в несколько строк;
очень большие ODS (XML 1 ГБ) — потоковый разбор.
CSV / TSV. Сниффер: разделитель
(, ; \t |), кавычки, кодировка, десятичный разделитель,
формат дат (по локали и выборке 100 строк), BOM; диалог импорта с
предпросмотром; типизация столбцов (число/дата/текст/bool) с
возможностью «всё как текст» (ведущие нули!); RFC 4180 при записи;
экспорт с выбором разделителя и кодировки (UTF-8 BOM для
Excel RU).
Numbers (iWork). Как Pages, типы TN.*
(документ, листы), TST.* (таблицы:
TableModelArchive, TileArchive — ячейки в
тайлах с бинарным кодированием CellStorage, строковые
таблицы TableDataList), формулы —
TSCE.FormulaArchive (AST узлами, не строкой → наш AST
напрямую), стили — TSS, диаграммы — TSCH.
Трудности: несколько таблиц на лист с произвольным положением → один
лист Sheets на таблицу (или размещение по координатам с выбором
пользователя); форматы чисел Numbers (не коды Excel) → таблица;
версионность.
Gnumeric / DIF / SYLK / DBF. Gnumeric — gzip XML
(gnm:Workbook/gnm:Sheets/gnm:Sheet/gnm:Cells/gnm:Cell с
формулами в синтаксисе Excel и ExprID для общих), стили —
gnm:StyleRegion. DIF — текстовые заголовки
TABLE/VECTORS/TUPLES/DATA + пары строк; SYLK — записи
ID;P, C;Y1;X1;K"text", F;
форматы, P; (формулы в синтаксисе R1C1!). DBF (dBase
III/IV, FoxPro): заголовок 32 байта (версия, дата, число записей),
дескрипторы полей по 32 байта (имя, тип C N F D L M, длина,
десятичные), записи фиксированной длины, * удалённые, memo
в .dbt/.fpt, кодовая страница по байту
language driver.
Parquet / JSON / XML. Parquet — через крейт
parquet (Arrow) → столбцы → лист (типизация, вложенные
структуры → JSON-текст); JSON — массив объектов → таблица (ключи →
заголовки, вложенность через точку), «JSON-линии»; XML — повторяющиеся
элементы → строки (как Excel «Из XML») с выбором пути.
4.3 Slides
| Формат | Режим | Уровень | Фаза / приоритет |
|---|---|---|---|
.pptx .pptm .potx |
R/W | V + R | 3 / P0 |
.ppt |
R/W (запись — подмножество) | V (чтение) | 3 / P0 чтение, P2 запись |
.odp .otp |
R/W | V | 3 / P0 |
.key |
R | S | 3 / P1 |
.sxi (OpenOffice 1.x) |
R | S | 3 / P2 |
.pdf .mp4/.webm
.png/.svg/.jpg слайдов .html |
E | — | 3 / P0 (pdf, png), P1 (видео, html) |
PPTX / PPTM / POTX (PresentationML).
ppt/presentation.xml (sldIdLst,
sldMasterIdLst, sldSz, notesSz,
sectionLst в extLst),
ppt/slideMasters/slideMaster<n>.xml
(p:cSld фигуры, p:txStyles:
titleStyle/bodyStyle/otherStyle по 9 уровней,
p:clrMap, sldLayoutIdLst),
ppt/slideLayouts/slideLayout<n>.xml
(@type, плейсхолдеры p:ph type= idx=),
ppt/slides/slide<n>.xml (p:cSld/p:spTree
с p:sp, p:pic, p:graphicFrame
(таблицы a:tbl, диаграммы, SmartArt, OLE),
p:grpSp, p:cxnSp; p:clrMapOvr,
p:transition, p:timing),
ppt/notesSlides/, ppt/notesMasters/,
ppt/handoutMasters/, ppt/comments/ +
commentAuthors.xml, ppt/media/,
ppt/embeddings/, ppt/theme/theme<n>.xml
(у каждого мастера свой), ppt/tableStyles.xml,
ppt/presProps.xml, ppt/viewProps.xml.
Отображение: наследование плейсхолдеров слайд → макет → мастер →
txStyles → тема (разрешается при импорте в
g:shape без копирования —
placeholder/idx сохраняются, свойства
наследуются и у нас); a:p/a:r → t:p/t:r с
уровнями a:pPr/@lvl; a:tbl →
tb:table; p:transition →
p:transition; p:timing:
p:tnLst/p:par/p:cTn (дерево: главная последовательность
p:seq → click-группы p:par → эффекты
p:par/p:cTn с
presetID/presetClass/presetSubtype и
nodeType="clickEffect|withEffect|afterEffect",
p:childTnLst с
p:set/p:anim/p:animEffect/p:animMotion/p:animRot/p:animScale/p:animClr,
цели p:spTgt + p:txEl/p:pRg для построения по
абзацам, p:bldLst) → обход дерева в порядке документа,
каждый cTn с presetClass даёт
p:anim (эффект = presetClass:presetID по
таблице имён PowerPoint, триггер по nodeType, задержка —
p:stCondLst/@delay, длительность — @dur,
повтор — @repeatCount, build — по
p:bldP/@build); интерактивные последовательности
(p:seq с p:cond/@evt="onClick" на фигуре) →
trigger="click:<id>"; всё, что не легло, —
m:x на слайде (отдаётся при экспорте в pptx, если анимации
не редактировались). Трудности: мастер/макет — у каждого мастера своя
тема (слайды разных мастеров → цвета разрешаются по своему мастеру);
p:clrMapOvr; a:bodyPr автоподбор
(normAutofit/@fontScale, @lnSpcReduction) —
нужно воспроизводить, иначе текст «вылезает»; текст на пути
(a:prstTxWarp), 3D (a:scene3d,
a:sp3d) — только через кэш-изображение; видео/аудио
(p:videoFile, p:media + p14:) —
медиа в media/, обрезка и постеры; p:pic с
a:blip/@r:link (внешние картинки); разделы
p14:sectionLst; «варианты»/дизайн-идеи (p15:,
p16:) — m:x; слайды скрытые
(@show="0"); комментарии «modern comments»
(p188:) — comments.xml.
PPT (MS-PPT, PowerPoint 97-2003). CFB с потоками
PowerPoint Document, Current User (указатель
на последний UserEditAtom), Pictures (Office
Art BLIP: OfficeArtBStoreContainer),
\x05SummaryInformation, VBA. Записи с
заголовком RecordHeader (recVer,
recInstance, recType, recLen) в
контейнерах: DocumentContainer (DocumentAtom,
SlideListWithText с SlidePersistAtom и текстом
TextHeaderAtom/TextCharsAtom/TextBytesAtom/StyleTextPropAtom),
MainMasterContainer, SlideContainer
(SlideAtom с masterId/layout,
PPDrawing — Office Art OfficeArtDgContainer),
NotesContainer, PersistDirectoryAtom (persist
id → смещение), цепочка UserEditAtom (инкрементальные
сохранения!), анимации — PP10 расширения в
ProgTags (TimeNodeContainer), старые —
AnimationInfoAtom. Отображение: по
Current User → последний UserEditAtom →
PersistDirectory → слайды; текст из
SlideListWithText или TextCharsAtom внутри
Office Art OfficeArtClientTextbox; стили —
StyleTextPropAtom (пары мастеров
TxMasterStyleAtom) → t:r; Office Art →
g:shape (общий с doc/xls декодер MS-ODRAW).
Трудности: кодировки (TextBytesAtom — CP по языку),
инкрементальные версии (нужно идти по UserEditAtom, а не
читать подряд), шифрование (CryptSession10Container, RC4
CryptoAPI), PowerPoint 95 — отдельный формат (не поддерживаем).
ODP / OTP.
office:presentation/draw:page (атрибуты
draw:master-page-name,
presentation:presentation-page-layout-name),
draw:frame с
presentation:class="title|outline|subtitle|notes|…"
(плейсхолдеры), draw:custom-shape +
draw:enhanced-geometry (формулы draw:formula,
draw:modifiers — покрывает ODF-эквивалент presetGeom),
presentation:notes, анимации —
anim:par/anim:seq/ anim:set/anim:animate/anim:transitionFilter
(SMIL) с
presentation:node-type="main-sequence|on-click|with-previous|after-previous",
presentation:preset-class/preset-id (имена
совпадают с PowerPoint — удобно для нашей таблицы эффектов), переходы —
smil:type/ smil:subtype в стиле страницы.
Трудности: draw:enhanced-geometry ↔︎ g:geom
формулы (ODF ?f0, $0 модификаторы),
мастер-страницы без «макетов» (ODF:
style:presentation-page-layout — только плейсхолдеры),
текстовые автостили.
Keynote (.key). IWA, типы KN.*
(ShowArchive, SlideArchive,
SlideNodeArchive, MasterSlideArchive),
анимации — KN.BuildArchive (эффекты Keynote — другие имена;
таблица соответствий «Magic Move → motion», неизвестные →
appear), переходы — KN.TransitionArchive.
Трудности — как у Pages, плюс темы Keynote с градиентами/тенями, не
представимыми точно → растр-подложка.
SXI (OpenOffice.org 1.x) — ранний ODF-подобный XML
(office:document-content с namespace
http://openoffice.org/2000/…) — через импортёр ODF с
таблицей переименования namespace. Экспорт видео (mp4/webm)
— рендер слайдов с анимациями в кадры (Meridian Render) + кодирование
(ffmpeg как внешний процесс, опционально устанавливаемый компонент из-за
патентов H.264; webm/VP9 — встроенный
rav1e/vpx? — уточнить).
4.4 Draw
| Формат | Режим | Уровень | Фаза / приоритет |
|---|---|---|---|
.svg |
R/W | V | 3 / P0 |
.odg |
R/W | V | 3 / P0 |
.vsdx |
R/W | S (чтение V для базовых фигур) | 3 / P0 чтение, P1 запись |
.vsd .vsdm |
R | S | 3 / P1 |
.emf .wmf |
R (+E emf) | V | 3 / P1 |
.dxf |
R | S | 3 / P2 |
.drawio |
R | S | 3 / P1 |
.cdr (подмножество) |
R | S | 3 / P2 |
.pdf .png .svg
.emf .vsdx |
E | — | 3 / P0 (pdf, png, svg), P1 (emf, vsdx) |
SVG 1.1/2. usvg-подобная нормализация
(resvg):
svg/g/path/rect/circle/ellipse/line/polyline/polygon/text/tspan/image/use/defs/ symbol/clipPath/mask/linearGradient/radialGradient/pattern/filter/marker/style.
Отображение: path → g:geom, примитивы →
preset, g → g:group,
text → g:text-box (с попыткой восстановить
абзацы), CSS (style элемент и атрибут) → свойства,
use → экземпляр (@master), фильтры SVG —
только feGaussianBlur/feDropShadow как
эффекты, прочее — растр-кэш. Запись: SVG 1.1 с meridian:
метаданными (id, коннекторы, порты) в собственном namespace — наш
.mvx восстанавливается из такого SVG почти без потерь.
Трудности: foreignObject, скрипты/анимации SMIL
(отбрасываются), текст на пути, em/% единицы,
шрифты по CSS.
ODG. office:drawing/draw:page,
draw:custom-shape+draw:enhanced-geometry,
draw:connector
(draw:start-shape/draw:start-glue-point,
draw:type="standard|lines|line|curve"),
draw:layer-set, draw:glue-point,
svg:viewBox, стили graphic. Хорошо ложится на
d:/g:; трудности — формулы
draw:enhanced-geometry (конвертер в g:guide),
текстовые рамки с автоподбором.
VSDX (Visio 2013+). OPC:
visio/document.xml (DocumentSettings,
Colors, FaceNames, StyleSheets с
ячейками Cell N= V= F=), visio/pages/pages.xml
(список страниц + PageSheet),
visio/pages/page<n>.xml (Shapes/Shape с
Type="Shape|Group|Foreign|Guide",
Master=/MasterShape=, ячейки
PinX PinY Width Height Angle FlipX FlipY LocPinX…,
Section N="Geometry" с
Row T="MoveTo|LineTo| ArcTo|EllipticalArcTo|NURBSTo|…",
Text с cp/pp/tp маркерами форматирования,
Connects/Connect FromSheet ToSheet FromCell ToCell),
visio/masters/masters.xml +
master<n>.xml, visio/theme/,
visio/media/, docProps/. Единицы — дюймы
(double). Ячейки — формулы ShapeSheet
(F="Width*0.5") — результат V=
используем как кэш; формулы — подмножество (арифметика,
GUARD, Sheet.5!Width, функции геометрии) →
g:guide; неподдержанные — m:x-attrs.
Отображение: Shape → g:shape
(master → стенсил из masters/),
Connect → g:connector (порты по
Connections-секции), Group →
g:group, слои — Layer-секция страницы.
Трудности: координатная система «Y вверх» (переворот),
LocPin/Pin трансформации, текстовые блоки с
TxtPinX, 1-D фигуры (BeginX/EndX), темы Visio
(ThemeIndex, QuickStyle*), данные фигур
(Section N="Property") → g:data,
Foreign (метафайлы), страницы-фоны
(BackPage).
VSD / VSDM. VSD — CFB с потоком
VisioDocument (сжатый LZ77-подобный VisioLZW,
затем бинарные «чанки» с теми же ячейками ShapeSheet по индексам,
документация — реверс libvisio); VSDM — vsdx с макросами. R* через
портирование подхода libvisio.
EMF / WMF. EMF (MS-EMF): записи EMR_*
(EMR_HEADER с rclBounds/rclFrame в 0,01 мм,
EMR_POLYBEZIER, EMR_EXTTEXTOUTW,
EMR_SELECTOBJECT, EMR_STRETCHDIBITS, EMF+ в
EMR_COMMENT с GDI+ записями EmfPlus*); WMF
(MS-WMF): placeable-заголовок 9AC6CDD7, записи
META_* в единицах inch dpi. Отображение —
интерпретатор GDI-состояния → g:* (пути, текст с шрифтами,
растры); EMF+ — приоритетно над дублирующим GDI. Трудности: шрифты GDI
(LOGFONT → наш шрифт), режимы растяжения, clip-регионы,
EMR_GDICOMMENT.
DXF (AutoCAD, R12–2018 ASCII/binary). Пары «код
группы / значение»; секции
HEADER/CLASSES/TABLES/BLOCKS/ENTITIES/OBJECTS; сущности
LINE ARC CIRCLE LWPOLYLINE POLYLINE SPLINE TEXT MTEXT INSERT (блоки) HATCH DIMENSION ELLIPSE;
слои из TABLES/LAYER, цвета ACI (индекс → таблица 256),
$INSUNITS. Отображение: ENTITIES → фигуры, слои →
d:layer, INSERT → экземпляры блоков (стенсил), масштаб →
d:page/@scale. Трудности: paper space/model space,
размерности (DIMENSION — растрируем из блока), MTEXT-разметка, 3D
(проецируем XY).
draw.io (.drawio,
.drawio.svg/.png). XML mxfile/diagram
(содержимое часто: deflate → base64 → URL-encoded
mxGraphModel), mxGraphModel/root/mxCell с
@style (shape=;fillColor=;…),
@vertex/@edge, mxGeometry,
@parent, @source/@target (рёбра); встроено в
SVG/PNG (content атрибут / tEXt-чанк
mxfile). Отображение: vertex →
g:shape (стили draw.io → наши: таблица 60 ключей),
edge → g:connector
(edgeStyle=orthogonalEdgeStyle → orthogonal),
страницы diagram → d:page, слои
(mxCell с parent=0).
CDR (CorelDRAW, X4+ подмножество). RIFF-контейнер
(RIFF….CDR), чанки vrsn, DISP,
page, obj → loda (свойства по
смещениям), кривые bbox/fild/outl
таблицы; реверс — libcdr. R* — пути, заливки, текст (простой); P2.
4.5 Paint
| Формат | Режим | Уровень | Фаза / приоритет |
|---|---|---|---|
.png .jpg .webp
.gif .bmp .tiff
.ico |
R/W | V | 4 / P0 |
.ora |
R/W | R | 4 / P0 |
.psd |
R (+E базово) | S | 4 / P1 |
.xcf |
R | S | 4 / P2 |
.heic .avif |
R | V | 4 / P1 |
.svg (растрирование) |
R | V | 4 / P0 |
.raw (libraw) |
R | V | 4 / P2 |
.gif анимация, .pdf |
E | — | 4 / P1 |
Растровые форматы. Декодеры: png (+
APNG), zune-jpeg/jpeg-decoder +
mozjpeg для записи,
image-webp/libwebp, gif,
tiff (многостраничные, 16/32-бит, LZW/Deflate/JPEG,
профили), bmp, ico/cur (несколько
размеров), heic/avif через libheif (плагин,
лицензионные кодеки HEVC — только системные декодеры на Windows/macOS),
RAW — libraw (плагин). Метаданные: EXIF/XMP/ICC читаются и
сохраняются (pt:image/m:x + media/);
ориентация EXIF применяется при открытии. Запись — выбор формата,
качества, субдискретизации, метаданных (удаление геотегов по умолчанию
при «экспорте для веба»).
ORA (OpenRaster 0.0.6). ZIP: mimetype =
image/openraster (stored, первый), stack.xml
(image/@w @h @xres @yres → stack с вложенными
stack (группы) и layer (src,
name, x, y, opacity
0–1, visibility="visible|hidden",
composite-op="svg:multiply|…", edit-locked),
первый ребёнок — верхний), data/*.png,
mergedimage.png (обязателен),
Thumbnails/thumbnail.png (≤ 256 px). Отображение: 1:1 с
pt:stack/pt:group/pt:layer (имена
composite-op svg:* ↔︎ наши имена); маски,
корректирующие и текстовые слои при записи ORA растрируются (паспорт:
«корректирующие слои сведены»). Чтение mergedimage.png
достаточно для Hub/превью.
PSD / PSB (Adobe). Big-endian; заголовок 26 байт
(8BPS, версия 1 = PSD / 2 = PSB, каналы, высота, ширина,
глубина 1/8/16/32, цветовой режим
Bitmap/Grayscale/Indexed/RGB/CMYK/Multichannel/Duotone/Lab),
секция Color Mode Data, Image Resources (8BIM-блоки:
разрешение 0x03ED, ICC 0x040F, слои-наборы,
направляющие, EXIF 0x0422, XMP 0x0424),
Layer and Mask Information (длина 4/8 байт; Layer Info:
число слоёв (отрицательное = первый альфа-канал — прозрачность
сведённого), записи слоёв: прямоугольник, каналы (id −1 альфа, −2 маска
пользователя, −3 векторная маска), режим наложения 8BIM + 4
символа (norm mul scrn over dark lite …), прозрачность,
флаги, маска, blending ranges, имя (Pascal) + Additional Layer
Information (luni Unicode-имя, lsct
группы (начало/конец), lyid, shmd,
fxrp, lfx2/lrFX эффекты,
TySh текст,
SoCo/GdFl/PtFl заливочные слои,
levl/curv/brit/hue2/… корректирующие, lclr,
vmsk векторные маски, SoLd/PlLd
смарт-объекты, vogk), затем данные каналов (сжатие 0 raw /
1 RLE PackBits / 2 ZIP / 3 ZIP+предикция)), Image Data (сведённое).
Отображение: слои → pt:layer (каналы → RGBA, CMYK → sRGB
через профиль с пометкой), lsct → pt:group (в
PSD порядок снизу вверх, у нас сверху вниз), маски →
masks/, режимы → имена W3C, корректирующие
(levl curv brit hue2 blnc phfl) → pt:adjust с
параметрами, TySh → pt:text (движок текста
Photoshop — только базовое: шрифт/размер/цвет; кэш-растр из данных
слоя), эффекты (lfx2: тень, обводка, свечение) →
pt:effects (подмножество) + растр. Трудности: 16/32-бит и
CMYK/Lab; смарт-объекты (вложенные PSB) — как растр + оригинал в
foreign/; «пустые» слои (0×0 с данными в эффектах); PSB
> 2 ГБ (потоковое чтение); текстовые движки. Запись PSD «базово»:
сведённое изображение + растровые слои и группы без эффектов (Photoshop
и другие редакторы открывают).
XCF (GIMP 2.10 — v011, GIMP 3 — v012+).
gimp xcf v011\0, big-endian, свойства
(PROP_*), указатели на слои/каналы (64-бит с v011), слои:
тайлы 64×64 с RLE или zlib (v011+ может быть zlib), режимы наложения
GIMP (GIMP_LAYER_MODE_* — таблица → наши), группы
(PROP_GROUP_ITEM), маски, текстовые слои
(PROP_TEXT_LAYER_FLAGS + парасайты
gimp-text-layer), высокая битность (float). R*: слои,
группы, маски.
SVG → растр — через наш рендер (resvg-подобный) с
выбором DPI; результат — один слой + оригинал SVG как «векторный слой»
(pt:vector, перерисовывается при изменении размера). RAW —
libraw (демозаика, баланс белого, 16-бит линейный → слой EXR/16-бит
PNG).
4.6 PDF
| Формат | Режим | Уровень | Фаза / приоритет |
|---|---|---|---|
.pdf (1.0–2.0) |
R/W | V | 4 / P0 (просмотр и аннотации с фазы 1 как библиотека для экспорта) |
.xps .oxps |
R | V | 4 / P2 |
.djvu |
R | V | 4 / P2 |
.cbz |
R | V | 4 / P2 |
.pdf/a .pdf/ua .png
.jpg .txt .docx (через
Write) |
E | — | 4 / P0 (pdf/a, png), P1 (docx) |
PDF (ISO 32000-2:2020). Структура: заголовок
%PDF-1.x, тело из косвенных объектов
(N G obj … endobj: словари, массивы, имена, строки, потоки
с фильтрами
FlateDecode/LZWDecode/ASCII85/ASCIIHex/RunLength/DCTDecode/JPXDecode/JBIG2Decode/CCITTFax),
таблица xref (классическая или XRef-поток с
ObjStm объектными потоками, PDF 1.5+), трейлер
(/Root → каталог → /Pages дерево →
/Page с /Contents, /Resources
(шрифты /Font: Type1/TrueType/Type0 (CID) /Type3,
ToUnicode, кодировки; /XObject изображения и
формы; /ExtGState; /ColorSpace;
/Pattern; /Shading), /Annots
(аннотации:
Text Link FreeText Highlight Underline StrikeOut Ink Square Circle Stamp FileAttachment Widget
— с /AP appearance streams), /AcroForm (поля
Tx Btn Ch Sig, /DA, /XFA — не
поддерживаем), /Outlines, /StructTreeRoot
(тегированный PDF), /Metadata XMP, /Names,
инкрементальные обновления (несколько
xref/startxref), шифрование
(/Encrypt: RC4 40/128, AES-128 (R4), AES-256
(R5/R6, SHA-256 с итерациями), /Perms), подписи
(/Sig PKCS#7/CAdES, /ByteRange, PAdES).
Отображение (Meridian PDF — своя модель страниц, не документ Write):
страница → список операторов контента (графическое состояние, пути,
текст BT/ET, Tf/Tj/TJ, XObject) → наш список
отображения (Skia); текст → извлечение с порядком чтения (по
StructTree если есть, иначе геометрическая кластеризация);
аннотации — редактируемые объекты. «PDF → Write»: реконструкция
абзацев/таблиц/картинок из геометрии (эвристики: строки → абзацы по
интервалам, колонки, таблицы по линиям) — уровень S «насколько
возможно», паспорт сообщает, что документ восстановлен из PDF. Запись:
Meridian Print пишет PDF 1.7 (по умолчанию) / 2.0; PDF/A-2b/2u/3b
(встроенные шрифты, OutputIntent, XMP, без прозрачности в A-1), PDF/UA-1
(теги из структуры документа, альтернативный текст, порядок чтения);
инкрементальное сохранение аннотаций (без переписывания исходного файла
— важно для подписанных); редактирование текста — замена операторов с
переукладкой строки. Трудности: шрифты без ToUnicode
(символы по глифам → OCR-подобное восстановление через кэш глифов),
Type3, CJK CMap, повреждённые xref (§6.10), формы XFA, JavaScript
(игнорируется), прозрачность/группы наложения, оттенки
Separation/DeviceN (→ sRGB через альтернативу), очень большие сканы
(JBIG2/JPX — декодеры jbig2dec/openjpeg как
плагины).
XPS / OXPS. OPC:
FixedDocumentSequence.fdseq →
FixedDocument.fdoc → Pages/<n>.fpage
(XAML: Canvas, Path с Data,
Glyphs с
Indices/UnicodeString/FontUri),
шрифты obfuscated (.odttf, XOR с GUID), изображения;
единицы 1/96 дюйма. Отображение — в модель страниц PDF (рендер + текст).
DjVu: AT&TFORM/DJVU/DJVM
(многостраничный), слои Sjbz (JB2 bitonal),
BG44 (IW44), FG44, текстовый слой
TXTz; через djvulibre (плагин, GPL — отдельный
процесс). CBZ/CBR: ZIP/RAR с изображениями, порядок по именам (natural
sort).
4.7 Math
| Формат | Режим | Фаза / приоритет |
|---|---|---|
.meq .mml (MathML 3/4 Presentation)
.tex (LaTeX math) |
R/W | 4 / P0 |
ODF-формулы (math:math в .odf/объектах,
StarMath annotation) |
R | 4 / P1 |
OMML (m:oMath из docx/pptx/xlsx) |
R (+W при экспорте docx) | 1 / P0 (как часть docx) |
.mml .tex .svg
.png |
E | 4 / P0 |
MathML: Presentation
(mi mn mo mrow msup msub msubsup mfrac msqrt mroot mtable mtr mtd munderover mover munder mtext mspace mstyle mpadded mphantom menclose semantics/annotation)
— прямое хранение; Content MathML — конвертация в Presentation. LaTeX —
парсер подмножества amsmath
(\frac \sqrt \sum \int \left \right \begin{matrix,pmatrix,cases,align} ^ _ \mathbb \mathrm \text,
греческие, операторы, \operatorname) ↔︎ MathML —
двусторонний (при MathML без исходника LaTeX восстанавливается
генератором). UnicodeMath (ввод как в Word) — парсер по правилам Unicode
Technical Note #28. OMML: m:oMath/m:r/m:t, m:f
(дробь), m:rad, m:nary, m:d
(скобки), m:sSup/m:sSub/m:sSubSup, m:m
(матрица), m:eqArr, m:func,
m:acc, m:bar, m:box,
m:groupChr, m:limLow/limUpp ↔︎ MathML;
m:oMathPara → display="block"; запись OMML при
экспорте docx/pptx — обязательна (Word не читает MathML внутри docx).
StarMath 5 (annotation encoding="StarMath 5.0") — парсер
для точного импорта формул LibreOffice (иначе — по MathML).
4.8 Notes
| Формат | Режим | Фаза / приоритет |
|---|---|---|
.enex (Evernote) |
R | 5 / P0 |
.one / .onepkg (OneNote) |
R* | 5 / P1 |
папки .md, .html |
R/W | 5 / P0 |
.pdf .md .html |
E | 5 / P0 |
ENEX. XML en-export/note
(title, content — CDATA с ENML:
en-note корень, XHTML-подмножество,
en-media hash= type=, en-todo checked=,
en-crypt), created/updated
(YYYYMMDDTHHMMSSZ), tag,
note-attributes (source-url,
author, latitude), resource
(data encoding="base64", mime,
width/height, recognition,
resource-attributes/file-name). Отображение: нота →
n:page с одним n:outline (ENML →
t:p/tb:table), en-media по
MD5-хэшу → n:attachment/изображение в media/,
en-todo → n:tag kind="todo", теги → теги
страницы, блокноты (из имени файла экспорта) → разделы. Трудности:
большие файлы (ГБ base64) — потоковый разбор с выгрузкой ресурсов на
диск; en-crypt (RC2-64/AES-128 с паролем пользователя) —
расшифровка по запросу.
ONE / ONEPKG (MS-ONESTORE + MS-ONE).
.one — revision store: заголовок с
guidFileType ({7B5C52E4-D88C-4DA7-AEB1-5378D02996D3} для
.one, другой — для .onetoc2),
guidFileFormat {109ADD3F-911B-49F5-A5D0-1791EDC8AED8},
fcrFileNodeListRoot (корневой список узлов по смещению
0x400, длина 0x400), списки файловых узлов (FileNode с
FileNodeID:
ObjectSpaceManifestListReferenceFND,
RevisionManifestListReferenceFND,
ObjectGroupListReferenceFND,
ObjectDeclaration2*FND,
FileDataStoreObjectReferenceFND для вложений),
транзакционный журнал TransactionLog,
HashedChunkList; поверх — MS-ONE: объектные пространства =
разделы/страницы, объекты с PropertySet (свойства
jcid: jcidPageNode,
jcidOutlineNode, jcidOutlineElementNode,
jcidRichTextOENode (текст +
TextRunFormatting), jcidImageNode,
jcidTableNode, jcidInkStrokeNode…).
Отображение: страницы → n:page, OutlineNode →
n:outline, RichTextOENode → t:p,
таблицы → tb:table, чернила → n:ink (пересчёт
координат: 1/2540 дюйма?), изображения/вложения → media/.
R*: текст, картинки, таблицы, вложения, структура; теги, чернила — по
возможности. .onepkg — CAB-архив (MSCF) с
.one и .onetoc2 (библиотека cab
крейт). Облачные записные книжки OneNote (только Graph API) — вне
формата.
Папки Markdown / HTML. Папка → раздел, файл →
страница, относительные ссылки → ссылки между страницами, вложения —
рядом; при экспорте — обратно (Obsidian-совместимо:
[[wikilinks]] опционально).
4.9 Mail
| Формат | Режим | Фаза / приоритет |
|---|---|---|
.eml .mbox .ics
.vcf |
R/W | 5 / P0 |
.pst .ost |
R* | 5 / P1 |
.msg |
R | 5 / P1 |
.ldif |
R | 5 / P2 |
.eml .mbox .ics
.vcf .pdf |
E | 5 / P0 |
EML / MBOX. RFC 5322 заголовки + MIME (RFC
2045–2049: multipart/*,
Content-Transfer-Encoding base64/quoted-printable, RFC 2047
кодированные заголовки, RFC 2231 параметры, message/rfc822
вложенные), HTML-тела → наш HTML-импорт в режиме «письмо»
(таблицы-раскладка), text/calendar → приглашения, S/MIME
(application/pkcs7-mime — проверка подписи/расшифровка
через провайдер). MBOX: mboxrd/mboxo/
mboxcl2 — разделитель From +
>From экранирование, Content-Length; импорт
потоком (ГБ-файлы). Запись — eml как есть,
mboxrd. ICS (RFC 5545 + RFC 7986):
VCALENDAR/VEVENT/VTODO/VJOURNAL/VFREEBUSY/VTIMEZONE,
RRULE (полный разбор повторений,
EXDATE/RDATE), VALARM,
ATTENDEE/ORGANIZER (iTIP методы
REQUEST/REPLY/CANCEL), часовые пояса —
VTIMEZONE ↔︎ IANA через таблицу. VCF (vCard 3.0/4.0,
RFC 6350):
N/FN/ORG/TEL/EMAIL/ADR/PHOTO (data: URI или base64 3.0)/BDAY/X-*;
Apple/Google расширения (X-ABLabel, item1.) —
поддержка.
PST / OST (MS-PST). Три слоя: NDB
(страницы 512 байт; заголовок с магией !BDN, версия: ANSI
(BID/IB 32-бит, Outlook ≤ 2002, ≤ 2 ГБ) / Unicode (64-бит), Node BTree
(NBT) и Block BTree (BBT) — B-деревья страниц BTPAGE, блоки
данных с шифрованием NDB_CRYPT_PERMUTE/CYCLIC
(обфускация, не защита), XBLOCK/XXBLOCK цепочки,
SLBLOCK субузлы), LTP (Heap-on-Node
HN, BTree-on-Heap BTH, Property Context
PC — набор свойств узла, Table Context TC —
таблицы содержимого папок/вложений/получателей),
Messaging (корень NID_MESSAGE_STORE,
дерево папок NID_ROOT_FOLDER, папка = PC +
TC иерархии/содержимого/ассоциированного содержимого,
сообщение = PC со свойствами MAPI
(PidTagSubject 0x0037, PidTagBody 0x1000,
PidTagHtml 0x1013, PidTagRtfCompressed 0x1009
— сжатый RTF с LZFu), получатели TC, вложения — субузлы
TC + PC (PidTagAttachDataBinary),
календарь/контакты/задачи — те же сообщения с классами
IPM.Appointment/IPM.Contact/
IPM.Task и именованными свойствами (NAMEID в
NID_NAME_TO_ID_MAP)). Отображение: папки →
mm:folder, сообщение → .eml (реконструкция
MIME из MAPI-свойств: заголовки из
PidTagTransportMessageHeaders если есть, иначе синтез; тело
— HTML/RTF→HTML/текст; вложения), встречи → VEVENT (свойства
PidLidAppointmentStartWhole, повторения —
PidLidAppointmentRecur бинарный
AppointmentRecurrencePattern → RRULE), контакты → vCard.
OST — тот же формат с другими константами и, в новых версиях,
шифрованием, привязанным к профилю (R* — только незашифрованные/
старые). Трудности: размер (50 ГБ) — только по требованию через
B-деревья, без загрузки в память; повреждённые файлы (журнал
BBT обрезан); RTF-сжатие LZFu;
PidTagRtfInSync; именованные свойства; пароли PST (хранится
CRC — игнорируем, формат не шифрует).
MSG (MS-OXMSG). CFB с потоками
__substg1.0_<PropID 4 hex><Type 4 hex>
(например __substg1.0_0037001F — тема UTF-16),
__properties_version1.0 (значения фиксированной длины),
__recip_version1.0_#00000000/ получатели,
__attach_version1.0_#00000000/ вложения (с вложенными
.msg), __nameid_version1.0/ именованные
свойства. Отображение — как PST-сообщение → .eml. LDIF (RFC
2849): записи dn: + атрибуты
(cn sn mail telephoneNumber, base64 ::) →
vCard.
4.10 Base
| Формат | Режим | Фаза / приоритет |
|---|---|---|
.mbx .sqlite .csv |
R/W | 5 / P0 |
.accdb .mdb |
R | 5 / P1 |
.odb |
R | 5 / P2 |
.xlsx (импорт таблиц) |
R | 5 / P0 |
| ODBC / PostgreSQL / MySQL / MS SQL (коннекторы) | R/W (живое) | 5 / P1 |
.csv .xlsx .sql
.pdf отчётов |
E | 5 / P0 |
SQLite — нативно (rusqlite, bundled
SQLite 3.4x): любая база открывается, служебные таблицы создаются при
первом сохранении объектов Base (с разрешения). ACCDB / MDB (Jet
3/4, ACE 12–16). Страницы фиксированного размера (2048 байт —
Jet3 .mdb Access 97; 4096 — Jet4
.mdb Access 2000–2003 и ACE .accdb), страница
0 — заголовок с магией \x00\x01\x00\x00Standard Jet DB /
Standard ACE DB и частично XOR-обфусцированной областью
(ключ известен), типы страниц: 0x01 данные (записи
переменной длины с картой nullability и смещениями, строки UCS-2 с
компрессией «unicode compression» в Jet4), 0x02 определение
таблицы (TDEF: столбцы, типы
(BOOL BYTE INT LONG MONEY FLOAT DOUBLE DATETIME BINARY TEXT OLE MEMO GUID NUMERIC COMPLEX),
индексы, usage map), 0x03 промежуточная,
0x04 карта использования (usage map), системные таблицы
MSysObjects (каталог: таблицы, запросы (SQL в
MSysQueries), формы/отчёты — как бинарные объекты формата
Access UI, не воспроизводимые), MSysRelationships,
MSysACEs; LVAL-страницы для MEMO/OLE; индексы — B-дерево с
упаковкой ключей. Реализация по подходу MDB Tools / Jackcess
(поведенческая документация, не официальная спецификация). Отображение:
таблицы → SQLite с типами, связи → meridian_relations,
запросы MSysQueries → SQL (диалект Jet → SQLite:
IIf, Nz, #dates#, *
подстановка, Like), формы/отчёты — не импортируются
(паспорт: «формы и отчёты Access не переносятся; создайте заново
мастером»), макросы/VBA — нет. Трудности: пароли БД (Jet: XOR-маска в
заголовке — читается; ACE с шифрованием RC4/«Office crypto» — только с
паролем), .accde, Attachment/ Multi-value
(COMPLEX) типы → JSON-текст, большие OLE.
ODB. ZIP: content.xml
(office:database: db:data-source,
db:forms, db:reports,
db:queries), database/ — встроенный HSQLDB 1.8
(script — SQL DDL + данные текстом, data,
properties) или Firebird embedded
(firebird.fbk?, LibreOffice 6.1+:
database/firebird.fdb); импорт: HSQLDB script
→ SQL (парсер DDL/INSERT), Firebird — через библиотеку
fbclient (плагин) или только структура. Формы ODB (XML
office:document с form:form) → базовый перенос
полей.
4.11 Forms
Своего обмена с чужими форматами почти нет: импорт вопросов из
.docx/.xlsx (эвристика: нумерованные абзацы →
вопросы, строки таблицы → варианты), экспорт ответов →
.msx/.xlsx/.csv через кодеки Sheets, формы → PDF через
Print (с полями AcroForm для заполнения офлайн — P1).
4.12 Сводная таблица приоритетов и фаз
| Фаза | P0 (без этого фаза не закрывается) | P1 | P2 |
|---|---|---|---|
| 1 | docx/dotx R/W, odt R/W, rtf R/W, doc R, txt/md/html, pdf E, OMML R, offcrypto Agile | doc W, fb2/epub, tex E, png/svg E | — |
| 2 | xlsx/xltx R/W, ods R/W, xls R, csv/tsv, pdf/csv E, pages R | xlsb, numbers, xls W, html/json/parquet E | gnumeric/dif/sylk/dbf, parquet/json/xml R, wpd/abw/wps/docbook/tex R |
| 3 | pptx/potx R/W, odp R/W, ppt R, svg R/W, odg R/W, vsdx R, pdf/png/svg E | key, vsd, emf/wmf, drawio, vsdx W, видео E, html E | sxi, dxf, cdr, ppt W |
| 4 | растры R/W, ora R/W, svg→растр, pdf R/W, pdf/a E, mml/tex | psd R, heic/avif, gif-анимация E, pdf→docx, psd E | xcf, raw, xps/oxps, djvu, cbz |
| 5 | enex, md/html папки, eml/mbox/ics/vcf, sqlite/csv, xlsx-импорт, формы: docx/xlsx-импорт вопросов | one/onepkg, pst/ost, msg, accdb/mdb, ODBC-коннекторы, pdf-формы E | ldif, odb |
5. Архитектура кодеков (Rust)
5.1 Крейт
meridian-codecs и рабочее пространство
kernel/crates/
meridian-codecs/ ← фасад: реестр кодеков, детект, API импорта/экспорта/конвертации, паспорт
codecs/
codec-core/ ← трейты Importer/Exporter, Source, DocumentSink, FidelityReport, ошибки, утилиты (zip, xml, cfb)
codec-mop/ ← свой формат (чтение/запись, история, шифрование, подписи, плоский вариант)
codec-ooxml/ ← docx/xlsx/pptx/vsdx (общие части: opc, drawingml, theme, offcrypto, vml)
codec-odf/ ← odt/ods/odp/odg/odb (общие: manifest, styles, openformula)
codec-msbinary/ ← cfb, doc, xls (biff8), ppt, msg, offart (MS-ODRAW), rtf-независимо: нет
codec-xlsb/
codec-rtf/
codec-pdf/ ← разбор объектов, фильтры, шрифты, контент → список отображения; запись (Print → PDF)
codec-html/ ← html/mhtml, markdown, epub, fb2, enex, docbook
codec-images/ ← png/jpg/webp/gif/bmp/tiff/ico/ora/psd/xcf (+ плагины heif/raw)
codec-vector/ ← svg, emf/wmf, dxf, drawio, cdr
codec-iwork/ ← iwa (snappy + protobuf), pages/numbers/key
codec-mail/ ← eml/mbox/ics/vcf/pst/msg/ldif
codec-onenote/
codec-access/
codec-text/ ← txt/csv/tsv/dif/sylk/dbf/json/parquet
meridian-codec-host/ ← бинарник песочного процесса (§5.4)
tools/meridian-convert/ ← CLI (§5.7)
Зависимости: кодеки зависят только от codec-core и
meridian-kernel-api (типы модели:
DocumentSink, идентификаторы, длины, цвета), но
не от реализации ядра — это позволяет собирать
meridian-convert без UI и тестировать кодеки на
«записывающем» синке (JSON-дамп событий). Библиотеки: zip
(с ZIP64), quick-xml (потоковый XML,
Reader::from_reader/from_str, events),
memmap2, cfb (OLE2), roxmltree
(мелкие деревья), prost (protobuf iWork), snap
(snappy), zstd, ciborium (CBOR),
yrs, aes-gcm, argon2,
ring/rustls (подписи, TLS для коннекторов),
rusqlite,
image/png/jpeg-decoder/webp/tiff/gif,
resvg/usvg, html5ever,
pulldown-cmark, encoding_rs,
chardetng, mailparse, ical,
vcard, calamine (справочно для xls/xlsb; своя
реализация предпочтительна из-за потокового режима),
pdf-писатель — свой.
5.2 Трейты Importer /
Exporter
// codec-core/src/lib.rs — ключевые типы / key types
/// Описание формата / Format descriptor
pub struct FormatDescriptor {
pub id: &'static str, // "docx", "xlsx", "mop-write"
pub names: &'static [&'static str], // ["Документ Word", "Word Document"] — ключи i18n
pub extensions: &'static [&'static str],
pub mime: &'static [&'static str],
pub app: AppKind, // Write | Sheets | Slides | Draw | Paint | Pdf | Math | Notes | Mail | Base | Forms
pub family: Family, // Ooxml | Odf | MsBinary | Pdf | Image | Text | Mop | …
pub support: Support, // ReadWrite | Read | ReadLimited | ExportOnly
pub phase: u8,
}
/// Откуда читаем / Input source
pub enum Source<'a> {
Path(&'a Path), // mmap по возможности / mmap when possible
Bytes(Cow<'a, [u8]>),
Reader(Box<dyn ReadSeek + Send + 'a>),
}
/// Подсказка для детекта / Probe hint
pub struct ProbeHint<'a> { pub file_name: Option<&'a str>, pub declared_mime: Option<&'a str>, pub size: Option<u64> }
/// Уверенность детекта / Detection confidence
pub enum Confidence { No, Extension, Magic, Content } // возрастает / ascending
pub struct ImportOptions {
pub password: Option<SecretString>,
pub recovery: RecoveryMode, // Off | Auto | Force
pub keep_foreign: bool, // наполнять мешок неизвестного / fill unknown bag
pub limits: Limits, // max_bytes, max_xml_depth, max_entries, timeout
pub locale: LocaleId, // для csv/txt/дат / for csv/txt/dates
pub progress: Option<ProgressSink>,
pub cancel: CancelToken,
}
/// Импортёр: чужой формат → события модели / Importer: foreign format → model events
pub trait Importer: Send + Sync {
fn descriptor(&self) -> &FormatDescriptor;
/// Быстрая проверка по первым байтам/именам ZIP; без полного разбора / cheap probe
fn probe(&self, head: &[u8], zip_names: Option<&[String]>, hint: &ProbeHint) -> Confidence;
/// Полный импорт; результат — документ, собранный синком, плюс паспорт / full import
fn import(&self, src: Source<'_>, opts: &ImportOptions, sink: &mut dyn DocumentSink,
report: &mut FidelityReport) -> Result<ImportSummary, CodecError>;
}
/// Экспортёр: модель → чужой формат / Exporter: model → foreign format
pub trait Exporter: Send + Sync {
fn descriptor(&self) -> &FormatDescriptor;
/// Что умеем записать; используется паспортом до сохранения / capability matrix for pre-save check
fn capabilities(&self) -> &Capabilities;
fn export(&self, doc: &dyn DocumentView, opts: &ExportOptions, dst: &mut dyn WriteSeek,
report: &mut FidelityReport) -> Result<ExportSummary, CodecError>;
}
/// Приёмник модели (builder) — кодек не знает структуры ядра / model sink (builder)
pub trait DocumentSink {
fn begin_document(&mut self, meta: DocumentMeta) -> Result<(), SinkError>;
fn styles(&mut self, styles: StyleSet) -> Result<(), SinkError>;
fn begin_section(&mut self, props: SectionProps) -> Result<(), SinkError>;
fn paragraph(&mut self, p: Paragraph) -> Result<ParaId, SinkError>; // абзац целиком с прогонами / whole paragraph
fn begin_table(&mut self, props: TableProps) -> Result<TableId, SinkError>;
fn end_table(&mut self) -> Result<(), SinkError>;
fn media(&mut self, blob: MediaBlob) -> Result<MediaRef, SinkError>; // дедупликация по sha256 / dedup by sha256
fn foreign_part(&mut self, path: &str, data: Bytes, info: ForeignInfo) -> Result<(), SinkError>;
fn unknown(&mut self, anchor: AnchorRef, xml: &str, mode: ReturnMode) -> Result<(), SinkError>;
fn sheet_cells(&mut self, sheet: SheetId, batch: CellBatch) -> Result<(), SinkError>; // пакетами по 64К ячеек / 64K-cell batches
// … slides, shapes, notes, mail items
fn end_document(&mut self) -> Result<(), SinkError>;
}Правила реализации кодека: никаких
panic!/unwrap на пользовательских данных
(clippy-линт unwrap_used = deny в крейтах кодеков); все
размеры из файла проверяются до выделения памяти (Limits);
каждый импортёр имеет fuzz-цель (cargo-fuzz) и минимум 30
файлов корпуса в gate-ярусе; время импорта линейно от
размера (запрещены квадратичные проходы по абзацам/ячейкам).
5.3 Потоковый разбор больших файлов
- XML:
quick-xmlв событийном режиме; DOM строится только для мелких частей (styles.xml,theme,manifest). Содержимое (document.xml,sheet<n>.xml,content.xml) разбирается конечным автоматом по событиям с явным стеком контекстов; абзац собирается целиком и передаётся в синк;sheetData→ батчи ячеек. Память: O(размер одного абзаца/строки), а не O(файла). - ZIP: чтение по центральному каталогу, части —
mmap(приSource::Path) + потоковая инфляция; большие медиа не копируются, а передаются синку какMediaBlob::Mmap { range }и позднее записываются в MOP напрямую. - CFB:
cfbкрейт с ленивым чтением потоков;WordDocument/Workbook— последовательные записи сSeek. - Параллелизм: листы xlsx/ods, слайды pptx, страницы
PDF разбираются параллельно (
rayon), если синк потокобезопасен (SheetIdнезависимы); порядок результатов восстанавливается по индексу. - Запись: экспортёры пишут потоком в
WriteSeek(ZIP-писатель с отложенным центральным каталогом); для xlsxsheetDataпишется прямо из колонно-блочного хранилища ядра без материализации XML-дерева;sharedStringsсобираются по ходу (строки — по индексам ядра). - Прогресс и отмена:
ProgressSinkполучает(done_bytes, total_bytes, stage),CancelTokenпроверяется на границе абзаца/строки/страницы.
5.4 Песочница процесса парсера
Парсеры чужих форматов работают в отдельном процессе
meridian-codec-host (библия §6): сбой парсера не роняет
документ-окно.
| Аспект | Решение |
|---|---|
| Протокол | Главный процесс → хост: `ImportRequest { format_hint, source: Path |
| Почему MOP как «провод» | один формат обмена вместо сериализации всех событий синка; результат сразу пригоден для автосохранения/восстановления; тесты кодеков сравнивают MOP-выход с эталоном |
| Изоляция | Linux: seccomp-bpf (только
read/write/mmap/brk/futex/exit), unshare user/net/pid
namespaces или Landlock (без доступа к ФС, кроме переданных fd); macOS:
sandbox_init профиль
((deny default) (allow file-read* (subpath …tmp))), без
сети; Windows: Job Object (лимиты памяти/CPU/процессов), AppContainer
или Low Integrity Level + PROCESS_MITIGATION_* (CFG, без
динамического кода) |
| Лимиты | память (по умолчанию 4 ГиБ, зависит от размера файла), CPU-время (60
с + 1 с/МБ), число открытых файлов; превышение →
CodecError::ResourceLimit → предложение «открыть в
безопасном режиме» (только текст) |
| Экспорт | тоже в хосте (экспортёр читает MOP, пишет чужой формат в переданный fd) — симметрично, чтобы баг писателя не ронял окно |
| Переиспользование | пул хостов (один «тёплый» процесс на приложение), один импорт на процесс одновременно; после краша — перезапуск и повтор в режиме восстановления |
| Отладка | MERIDIAN_CODEC_INPROC=1 — всё в одном процессе (тесты,
профилирование) |
5.5 Детект формата
Порядок: содержимое → контейнер → расширение; расширение лишь повышает уверенность и выбирает между кандидатами.
/// Определение формата по сигнатуре, содержимому контейнера и расширению / format detection
pub fn detect(head: &[u8], src: &Source, hint: &ProbeHint) -> DetectResult {
// 1. Сигнатуры / magic bytes
let magic = match head {
b if b.starts_with(b"PK\x03\x04") => Magic::Zip,
b if b.starts_with(&[0xD0,0xCF,0x11,0xE0,0xA1,0xB1,0x1A,0xE1]) => Magic::Cfb,
b if b.starts_with(b"%PDF-") => Magic::Pdf,
b if b.starts_with(b"{\\rtf") => Magic::Rtf,
b if b.starts_with(b"8BPS") => Magic::Psd,
b if b.starts_with(b"SQLite format 3\0") => Magic::Sqlite,
b if b.starts_with(b"!BDN") => Magic::Pst,
b if b.len() > 20 && &b[4..19] == b"Standard Jet DB" => Magic::Mdb,
b if b.len() > 20 && &b[4..19] == b"Standard ACE DB" => Magic::Accdb,
b if b.starts_with(b"\x89PNG\r\n\x1a\n") => Magic::Png,
b if b.starts_with(&[0xFF,0xD8,0xFF]) => Magic::Jpeg,
b if b.starts_with(b"RIFF") && &b[8..12] == b"WEBP" => Magic::WebP,
b if b.starts_with(b"RIFF") && &b[8..11] == b"CDR" => Magic::Cdr,
b if b.starts_with(b"GIF8") => Magic::Gif,
b if b.starts_with(b"BM") => Magic::Bmp,
b if b.starts_with(b"II*\0") || b.starts_with(b"MM\0*") => Magic::Tiff,
b if b.starts_with(b"\0\0\x01\0") => Magic::Ico,
b if b.len() > 12 && &b[4..8] == b"ftyp" => Magic::Isobmff, // heic/avif/mp4 — по brand
b if b.starts_with(b"gimp xcf ") => Magic::Xcf,
b if b.starts_with(b"AT&TFORM") => Magic::Djvu,
b if b.starts_with(b"MSCF") => Magic::Cab, // .onepkg
b if b.starts_with(b"\x1f\x8b") => Magic::Gzip, // .gnumeric, .zabw, .svgz
b if b.starts_with(b"PAR1") => Magic::Parquet,
b if b.starts_with(&[0xD7,0xCD,0xC6,0x9A]) => Magic::WmfPlaceable,
b if b.len() > 44 && &b[40..44] == b" EMF" => Magic::Emf,
b if b.starts_with(b"\xFFWPC") => Magic::Wpd,
b if is_onestore_header(b) => Magic::OneStore, // guidFileFormat {109ADD3F-…}
b if looks_like_xml(b) => Magic::Xml,
b if is_text(b) => Magic::Text,
_ => Magic::Unknown,
};
// 2. Контейнеры / containers
match magic {
Magic::Zip => detect_zip(src), // по именам записей: mimetype (ODF/MOP/ORA/EPUB), [Content_Types].xml + главная часть rels (docx/xlsx/pptx/vsdx/xps), Index/Document.iwa (iWork), META-INF/container.xml (epub), comic (cbz: только изображения)
Magic::Cfb => detect_cfb(src), // по потокам: WordDocument / Workbook|Book / PowerPoint Document / VisioDocument / __substg1.0_ / EncryptedPackage (OOXML с паролем) / CONTENTS (wps)
Magic::Xml => detect_xml_root(head),// FictionBook / svg / mxfile / en-export / office:document (flat ODF) / m:flat (наш *xf) / math / book|article (DocBook) / abiword / Workbook (gnumeric)
Magic::Text => detect_text(head, hint), // csv/tsv (сниффер), md, html (<html|<!DOCTYPE), eml (заголовки RFC 5322), mbox ("From "), ics (BEGIN:VCALENDAR), vcf, ldif (dn:), sylk (ID;P), dif (TABLE), dxf ("0\r\nSECTION"), tex (\documentclass)
m => DetectResult::from_magic(m, hint),
}
}Расширение в конфликте с содержимым (например .doc, а
внутри RTF или HTML — частый случай) → побеждает содержимое, в паспорт
пишется «Файл имеет расширение .doc, но является RTF». Для текстовых
файлов без уверенного детекта показывается диалог выбора (как в
LibreOffice).
5.6 Паспорт фидельности
Структура (общая для импорта и экспорта):
pub struct FidelityReport {
pub direction: Direction, // Import | Export
pub format: &'static str,
pub items: Vec<FidelityItem>,
pub stats: FidelityStats, // objects_total, objects_lossy, bytes_foreign
}
pub struct FidelityItem {
pub severity: Severity, // Info | Minor | Major | Loss (Loss — данные потеряны; Major — заметно изменится вид)
pub feature: FeatureId, // стабильный ключ: "write.numbering.legal", "sheets.cf.icon-set", "slides.anim.motion-path"
pub message: MessageKey, // ключ локализации + параметры / i18n key + args
pub location: Option<Locator>, // Paragraph(id) | Page(n) | Cell(sheet, "B12") | Slide(n, shape) | Part(path)
pub count: u32, // сколько раз (сворачиваем одинаковые) / collapsed duplicates
pub recoverable: bool, // вернётся ли при round-trip из мешка неизвестного
}Поведение в приложении:
- Импорт: если есть
Major/Loss— ненавязчивая полоса под лентой «Открыто с ограничениями: 3 элемента. Подробнее» → панель с переходом к месту (Locator). Сводка пишется вmeta.xml/m:provenance. - Экспорт («Проверка совместимости»): перед записью в
чужой формат экспортёр прогоняет «сухой» проход
(
Capabilities× содержимое) и показывает диалог как Word/LibreOffice: список потерь по степени, «Сохранить всё равно», «Сохранить копию в .mwx», «Отмена»; галочка «Не показывать для этого документа» хранится вsettings/compat.suppress-passport. Паспорт доступен в «Файл → Сведения». - Пакетный режим / CLI: отчёт в JSON
(
--report), код возврата 3 приLoss, если задан--strict. - Элементы, вернувшиеся из мешка неизвестного, помечаются
recoverable=trueи показываются какInfo.
5.7 Конвертер
командной строки meridian-convert
meridian-convert [OPTIONS] <INPUT>...
--to <FORMAT> целевой формат: pdf, pdfa-2b, pdfa-3b, docx, odt, rtf, html, md, epub, txt, xlsx, ods, csv, json, parquet,
pptx, odp, png, svg, jpg, mp4, mwx, msx, mpx, mvx, mwxf, …
--out-dir <DIR> | --out <FILE> | --stdout
--filter k=v[,k=v] параметры экспорта: pdf:tagged=true, pdf:embed-fonts=all, csv:sep=';', csv:encoding=utf-8-bom,
png:dpi=150, png:pages=1-3, docx:compat=2016, html:inline-css=true, xlsx:recalc=true
--password <PW> | --password-file <F>
--recover auto|force|off
--strict код возврата ≠ 0 при потерях уровня Loss
--report <FILE.json> паспорт фидельности по каждому файлу
--jobs N параллельные задания (по умолчанию = ядра)
--timeout <SEC> на файл
--sandbox on|off песочный хост (по умолчанию on)
--fonts <DIR> дополнительные шрифты; --font-map <FILE> — свои подмены
--locale ru-RU для csv/дат/имён функций в сообщениях
--list-formats | --probe <FILE> (печатает детект и уверенность)
serve --listen 127.0.0.1:7700 [--token …] HTTP API: POST /convert (multipart: file, to, filter) → файл + заголовок X-Meridian-Report
Коды возврата: 0 — успех; 1 — ошибка аргументов; 2 — файл не открыт
(повреждён/не поддержан/пароль); 3 — потери при --strict; 4
— таймаут/лимит; 5 — внутренняя ошибка кодека (с дампом для отчёта).
Поведение как у soffice --headless --convert-to, но с
паспортом и параллелизмом. Docker-образ meridian/convert
(Linux, без Qt) — для серверной конвертации; лицензируется как
Business/Enterprise-функция (Feature::ServerConvert), CLI
на рабочей станции — в любой редакции.
5.8 API пакетной конвертации
// meridian-codecs/src/batch.rs
pub struct Job { pub input: Source<'static>, pub target: FormatId, pub options: ExportOptions, pub import: ImportOptions, pub output: Output }
pub struct Pipeline { jobs: usize, sandbox: bool, font_config: FontConfig, on_progress: Option<Box<dyn Fn(JobProgress) + Send + Sync>> }
impl Pipeline {
pub fn run(&self, jobs: Vec<Job>) -> Vec<JobResult>; // параллельно, с изоляцией по заданиям / parallel, isolated
pub fn run_stream(&self, jobs: impl Iterator<Item = Job>) -> impl Iterator<Item = JobResult>; // для миллионов файлов / for huge batches
}
pub struct JobResult { pub input: PathBuf, pub output: Option<PathBuf>, pub report: FidelityReport, pub error: Option<CodecError>, pub timing: Timing }Тот же API доступен: из MeridianScript —
app.convert({input, to, filter}) /
app.convertMany([...]) (возвращает паспорта); через C ABI —
mrd_convert(const MrdJob*, MrdResult**) для интеграций
(Enterprise); из Hub — «Пакетная конвертация» (окно с перетаскиванием
файлов).
6. Алгоритмы
6.1 Сопоставление стилей (style mapping)
Задача: чужие стили → наши m:style так, чтобы (а) вид не
изменился, (б) встроенные стили узнавались как наши встроенные (галерея,
оглавление, нумерация заголовков), (в) round-trip сохранил имена.
- Построение графа чужих стилей: узлы — стили, рёбра
—
basedOn(OOXML) /style:parent-style-name(ODF) /\sbasedon(RTF) /istdBase(DOC). Циклы разрываются (OOXML запрещает, но файлы встречаются) — ребро с меньшимpriorityудаляется, в паспортMinor. - Резолвер наследования — порядок применения свойств
(ISO 29500 §17.7.2):
docDefaults→ стиль таблицы (с условными частями) → стиль нумерации (w:lvl/w:pPr,w:rPr) → стиль абзаца (цепочкаbasedOnот корня к листу) → стиль символов → прямое форматирование абзаца → свойства нумерации абзаца (w:indизnumPrимеют приоритет над стилем, но не над прямым) → прямое форматирование прогона. ODF:default-style→parent-style-nameцепочка → автостиль (прямое). Реализация —StyleResolverс кэшем разрешённых наборов (FxHashMap<StyleId, ResolvedProps>), мемоизация по цепочке. - Отображение на встроенные: таблица
builtinключей (normal, heading-1..9, title, subtitle, quote, intense-quote, list-paragraph, caption, toc-1..9, footnote-text, header, footer, hyperlink, emphasis, strong, code, table-grid, no-spacing …) ↔︎ имена чужих форматов на всех локалях Word (Heading 1=Заголовок 1=Überschrift 1: Word хранит английское имя вw:name, локализует UI; LibreOffice — программное имяHeading 1+display-name; RTF — локализованные имена!) → словарь из 40 языков вcodec-core/data/style-names.toml. - Нормализация: свойства приводятся к нашим атрибутам
(единицы → EMU,
w:szhalf-points ×6350,w:spacing/@line240-е доли →x1.0; цвета → §2.13); тройкиascii/hAnsi/eastAsia/cs→font/font-ea/font-cs(приascii == hAnsi— одно значение);w:lang→lang/lang-ea/lang-cs. - Автостили ODF → прямое форматирование на объекте;
если один автостиль используется > 50 раз (типично для Calc) —
остаётся стилем
auto="1". - Экспорт — обратная задача: наши стили → чужие;
прямое форматирование →
w:pPr/w:rPr(OOXML) или собранные автостили с дедупликацией по хэшу свойств (ODF);builtin→ правильное английское имя Word (w:name="heading 1"в нижнем регистре, как пишет Word) сw:uiPriority,w:qFormat. - Конфликты имён при вставке между документами: стиль
с тем же
id, но другими свойствами →id-2с пометкойconflict, по политике «использовать стили назначения» (как Word).
6.2 Сопоставление шрифтов (font mapping)
resolve_font(requested, script, weight, style) -> FontFace:
1. точное имя среди установленных + встроенных в документ (учитывая `altName`, PostScript-имя, семейство в `name`-таблице на всех языках)
2. таблица метрических эквивалентов (двусторонняя):
Arial ↔ Liberation Sans ↔ Arimo; Times New Roman ↔ Liberation Serif ↔ Tinos; Courier New ↔ Liberation Mono ↔ Cousine
Calibri ↔ Carlito; Cambria ↔ Caladea; Georgia ↔ Gelasio; Segoe UI ↔ Selawik; Helvetica ↔ Arial (метрически близко)
Symbol/Wingdings/Webdings → OpenSymbol (+ таблица перекодировки PUA U+F0xx → Unicode); MS Mincho ↔ Noto Serif CJK JP (приближённо)
3. PANOSE-расстояние (10 байт, взвешенное) среди установленных с покрытием нужной письменности
4. обобщённое семейство (serif/sans/mono/cursive/fantasy) из `w:family` / `style:font-family-generic` / эвристики по имени
5. fallback по письменности через fontdb + Noto: для каждого символа без глифа — цепочка (Noto Sans → Noto Sans <Script> → Symbola)
результат: FontFace + `substituted: bool` + `metric_compatible: bool`
- Имя шрифта в документе не меняется при подмене; подмена — свойство сеанса (панель «Шрифты документа» показывает, что чем заменено).
- Если подмена не метрически совместима, а в документе есть
m:font/m:metrics(§2.15), раскладка использует сохранённые метрики для высоты строки и интервалов (ascent/descent/lineGap), а ширины глифов — подменного шрифта: страницы совпадают по вертикали, что критично для нумерации страниц и разрывов. - Пользовательские подмены —
~/.config/meridian/font-map.toml; корпоративные — политикой.
6.3 Пересчёт единиц
Все коэффициенты — целые относительно EMU
(spec/formats/units-and-ids.md); округление — «половина от
нуля» (round half away from zero), только при переводе
из EMU в более крупную единицу. Особые случаи:
| Источник | Формула |
|---|---|
OOXML w:sz (half-points) |
emu = sz × 6350 |
OOXML w:spacing/@line при
lineRule=auto |
множитель line / 240 → x1.15; при
exact/atLeast — twip → ×635 |
OOXML w:ind/@left (twip), @leftChars
(1/100 символа) |
twip ×635; chars — относительная единица, хранится как
indent-left="ch:150" (ширина символа шрифта абзаца при
раскладке) |
OOXML border w:sz (1/8 pt) |
emu = round(sz × 1587.5); обратно —
round(emu / 1587.5) |
DrawingML a:off/a:ext (EMU), a:rot
(1/60000°) |
как есть |
| Excel ширина столбца (символы) | px =
Truncate(((256 × width + Truncate(128 / MDW)) / 256) × MDW)
где MDW — ширина максимальной цифры шрифта «Обычный» в px (ISO 29500
§18.3.1.13); emu = px × 9525; хранится EMU, обратно — через
MDW шрифта по умолчанию книги; исходное значение в символах кэшируется в
s:col/@width-chars для round-trip |
| Excel высота строки (pt) | emu = pt × 12700 (дробные pt допускаются) |
ODF (cm, mm, in,
pt, pc, px) |
строка → число → ×коэффициент; % — относительная
(хранится x<pct>%) |
| RTF (twip) | ×635 |
| DOC/XLS (twip, 1/256 символа, pt×20) | ×635; ширина столбца XLS в 1/256 символа → как Excel |
| PDF (pt, матрицы) | ×12700; пользовательское пространство через
UserUnit |
| Visio (in, double) | round(in × 914400) |
SVG (px при 96 dpi, em, %,
mm) |
×9525; относительные — при разрешении контекста |
| iWork (pt, float) | round(pt × 12700) |
6.4 Нумерация
OOXML → наша модель:
w:abstractNum→t:list-style(id = "an<abstractNumId>", уровниw:lvl→t:level:w:start,w:numFmt→format,w:lvlText→text(плейсхолдеры%1..%9сохраняются),w:lvlJc→align,w:pPr/w:ind→indent-left/hanging,w:suff,w:lvlRestart→restart-after,w:isLgl→legal,w:rPr→t:rprуровня,w:lvlPicBulletId→bullet-image).w:abstractNum/w:numStyleLink→ стиль-список ссылается на стиль абзаца сw:numPr→ разрешается вt:list-style/@linked-style;w:styleLink— обратная ссылка (сохраняем).w:num→t:list(id = "n<numId>",style = "an…"),w:lvlOverride→t:override level start(изw:startOverride) или полная замена уровня (t:overrideс вложеннымt:level).- Абзац:
w:numPr/w:numId+w:ilvl→list/level;numId="0"— отмена нумерации (list="none"). Нумерация из стиля абзаца (w:style/w:pPr/w:numPr) →t:ppr/@listв стиле, абзац не дублирует. - Подсчёт номеров при раскладке: для каждого
t:list— счётчики по уровням; переход на уровеньnсбрасывает счётчики> n(с учётомrestart-after);t:override/@startприменяется при первом появлении уровня;continue="L1"— общие счётчики;legal— все уровни выше показываются арабскими. - Word-специфика: списки с одинаковым
abstractNum, но разнымиnum— отдельные последовательности (как в Word); «перезапуск нумерации» в Word создаёт новыйnumсstartOverride— у нас аналогичноt:list+t:override.
ODF → наша модель: вложенные text:list (с
text:style-name только у внешнего!) → при обходе глубина =
level, ближайший предок с style-name даёт
t:list-style; text:list/@xml:id +
text:continue-list/text:continue-numbering="true"
→ continue; text:list-item/@text:start-value →
t:override; text:list-header → абзац с
level, но без номера (list-header="1"). Стили
text:list-level-style-number/-bullet/-image →
t:level (style:list-level-properties с
text:list-level-position-and-space-mode="label-alignment" →
indent-left/hanging из
fo:margin-left/fo:text-indent).
Экспорт в OOXML: t:list-style →
abstractNum, каждый t:list → num
(+ lvlOverride); в ODF: плоские абзацы → восстановление
вложенности text:list (стек по уровням; смена
t:list закрывает вложение).
6.5 Таблицы
- Сетка из OOXML:
w:tblGrid/w:gridColзадаёт столбцы; ячейка занимаетgridSpanстолбцов (→colspan);w:gridBefore/gridAfter→ пустые ячейки без границ (tb:cell/@phantom="1");w:vMerge="restart"начинает вертикальное объединение,w:vMerge(без значения /continue) — продолжение → при импорте проход по столбцам сверху вниз, подсчётrowspan, ячейки-продолжения удаляются (их содержимое, если непустое — Word его скрывает — присоединяется в паспортMinorи вm:x). При экспорте в OOXML — обратная генерацияvMergeи пустыхw:tcдля продолжений. - Сетка из ODF:
table:table-column(сnumber-columns-repeated) →tb:col;number-columns-spanned/rows-spanned→colspan/rowspan;covered-table-cell→ отбрасываются;table:table-header-rows→header="1"; вложенные таблицы иtable:sub-table(старый ODF) →tb:table. - Нормализация сетки: если суммы
colspanпо строкам различаются (кривые файлы), сетка достраивается до максимума; «рваные» строки дополняютсяphantom-ячейками. - Авто-подбор ширин (
layout="auto"): алгоритм автоматической раскладки CSS 2.1 §17.5.2.2: для каждой ячейкиmin-content(самое длинное слово/непереносимый объект + поля) иmax-content(без переносов); для столбцов — максимумы по ячейкам (объединённые распределяют пропорционально); затем доступная ширинаW: еслиΣmax ≤ W— столбцы поmax(и растяжение приwidth="x100%"), еслиΣmin ≥ W— поmin(переполнение допускается), иначе —min + (max − min) × (W − Σmin)/(Σmax − Σmin); предпочтительные шириныtb:col/@widthиtb:cell/@width— как ограничения снизу. Word-режим (compat.layout-profile=ooxml-*) добавляет поведениеw:tblW type="auto"иw:tcWсtype="pct|dxa|auto". layout="fixed": ширины — ровноtb:col/@width, содержимое переносится/обрезается.- Вложенность — рекурсивно с тем же алгоритмом, внутренняя таблица
участвует в
min/maxвнешней ячейки. - Повторяющиеся строки-заголовки (
header="1") и запрет разрыва строки (cant-split) — на раскладке; плавающие таблицы — через@frameкак объекты с обтеканием.
6.6 Формулы
Канонический синтаксис (в файле и в ядре): имена
функций — английские, верхний регистр, без префиксов
(_xlfn., _xlws., of:,
COM.MICROSOFT.); разделитель аргументов — ,;
десятичный — .; ссылки — A1 ($A$1,
A1:B2, A:A, 1:1), лист —
'Имя листа'!A1 (апострофы — только при необходимости,
экранирование ''), внешняя книга —
'[budget.msx]Лист1'!A1, имена — как есть, структурированные
ссылки — Таблица[[#Headers],[Столбец]], оператор
переливания — A1#, неявное пересечение —
@A1:A10, массивы-константы — {1,2;3,4}
(, столбец, ; строка), строки —
"…" ("" экранирование), ошибки —
#N/A и т.д., проценты 5%, объединение
диапазонов — , внутри скобок (A1:A2,B1:B2),
пересечение — пробел. Хранимая строка всегда без ведущего
=.
Конвейер импорта
(codec-core::formula):
xlsx "SUM(_xlfn.XLOOKUP(A1,B:B,C:C),@D1:D5)" ─┐
ods "of:=SUM([.A1:.B2];[Sheet2.C3])" ─┼─> лексер (по диалекту) ─> парсер Пратта ─> AST ─> нормализатор ─> канон. строка
xls PtgRef/PtgArea/PtgFuncVar… (RPN) ─┤ │
xlsb те же Ptg + PtgList ─┘ стековая машина ┘
AST:
Num, Str, Bool, Err, Ref{sheet, col, row, abs}, Range, Name, StructRef, Func{id, args}, Unary, Binary, Array, Spill, Implicit, Union, Intersect, Missing.
Таблица функций (functions.toml, ~520 записей): канон. имя,
идентификатор BIFF (iftab), число аргументов, категория,
версия появления и префикс для экспорта в xlsx (prefix):
функции после Excel 2007 получают _xlfn.
(XLOOKUP, UNIQUE, SEQUENCE,
IFS, …), а SORT и FILTER —
_xlfn._xlws. (имена конфликтуют с методами объектной
модели; список _xlws.-функций уточнять при появлении новых
версий Excel), ODF-имя
(ORG.OPENOFFICE.*/COM.MICROSOFT.*),
локализованные имена (ru: СУММ, ЕСЛИ,
ВПР, …; de, fr, es, …) для UI.
Особенности: OpenFormula [.A1] → A1,
[Sheet.A1:.B2] → Sheet!A1:B2,
[$Sheet.$A$1], ; → ,;
| (union) → ,; ! (intersection) →
пробел. BIFF: PtgAttr (tAttrSpace — пробелы
для round-trip — отбрасываем;
tAttrIf/tAttrChoose — пропуск для оптимизации
— игнорируем при декодировании); PtgExp — ссылка на
общую/массивную формулу по якорю; PtgRef3d с индексом
EXTERNSHEET → имя листа;
PtgName/PtgNameX; PtgMemArea —
кэш диапазонов, пропускаем. Экспорт в BIFF8 — обратная генерация RPN
(обход AST в постфиксе с выбором PtgRefV/R/A классов по
контексту — по правилам MS-XLS §2.5.198.1).
Локализация в UI: отображение формулы = канон →
имена функций по локали + разделитель (; если десятичный
,) + локальные булевы (ИСТИНА), ошибки
(#ДЕЛ/0!), R1C1 при включённом режиме; ввод пользователя —
обратный путь; файл всегда хранит канон.
Динамические массивы: s:f/@dynamic +
@array (диапазон переливания — кэш); при экспорте в xlsx:
t="array" ref=… cm="1" + генерация
xl/metadata.xml (XLDAPR,
fDynamic="1") и префиксы; при экспорте в формат без
динамических массивов (xls, ods ≤ 1.3) — CSE-массив фиксированного
размера + паспорт Major.
6.7 Даты
- Внутри — серийные числа от
date-base(1899-12-30по умолчанию); функции ядраserial_to_date/date_to_serialбез «дня 60». - Импорт xlsx/xls 1900-книги: значения ≥ 61 — как есть (совпадают:
Excel 1900-система от «1900-01-00» с фиктивным 29.02.1900 даёт для
1900-03-01 = 61, что в нашей шкале от 1899-12-30 — тоже 61); значения
1..60 (01.01.1900–29.02.1900 по Excel) — помечаются
settings/compat.excel-1900-leap=1, отображение таких ячеек воспроизводит Excel (дата = serial−1 в нашей шкале, с показом «29.02.1900» для 60) только в режиме совместимости; ядро хранит число без изменений, чтобы round-trip был точным (поведение LibreOffice при импорте таких дат — уточнить). date1904→date-base="1904-01-01"; при экспорте в формат, не поддерживающий 1904 (ODF хранит ISO-строки — поддерживает всё; csv — текст), пересчёт+1462не нужен — конвертируем через календарь.- ODF:
office:date-value="2026-10-01T09:00:00"→date_to_serialс учётомtable:null-dateизsettings.xml(обычно 1899-12-30). - Экспорт в xlsx: всегда 1900-система (если
date-base1904 — пересчёт серийных+1462иdate1904="0", либо сохранитьdate1904="1"— по настройке; по умолчанию сохраняем систему источника). - Часовые пояса: в Sheets дата-время — «наивное» (без зоны), как в Excel/ODF; Mail/Notes/Base — ISO 8601 с зоной.
6.8 Цвета и темы
theme1.xml/a:clrScheme→m:theme/m:colors(12 слотов,a:sysClr→lastClr);a:fontScheme→m:fonts(latin/ea/cs +a:font scriptкакm:font-script script="Cyrl" face="…"). У пакета одинm:theme; pptx с несколькими мастерами и темами — тема мастера сохраняется вp:master/m:theme, а слайды ссылаются на свой мастер.- OOXML
w:clrSchemeMapping(bg1→lt1,tx1→dk1,bg2→lt2,tx2→dk2, …): ссылкиw:themeColor="text1"переводятся в слотыdk1через карту; при экспорте карта пишется стандартная. w:themeTint/w:themeShade(hex 00–FF) →theme(slot, tint N%)/theme(slot, shade N%), гдеN = 100 − tint/255·100; Word применяет их в пространстве HSL по яркости, а DrawingMLa:tint/a:shade— в линейном RGB, поэтому исходное значение сохраняется вm:x-attrsдля точного round-trip, а рендер использует формулу DrawingML (расхождение — единицы канала; точные формулы Word — уточнить по ISO 29500 §17.3.2.6 и проверить на корпусе). DrawingMLa:schemeClrсa:tint/a:shade/a:lumMod/a:lumOff/a:satMod/a:alpha→ те же модификаторы (линейный RGB для tint/shade, HSL для lum/sat).- Индексированные цвета Excel (
indexed="N", палитра 0–63 по умолчанию илиcolors/indexedColors), BIFFPALETTE,icv→ sRGB. w:color="auto"/w:fill="auto"→auto(контрастный к фону при рендере); VMLfillcolor="#rrggbb [N]"→ hex.- Экспорт в формат без тем (rtf, html, pdf) — вычисленный sRGB; в ODF
— sRGB (ODF 1.3 тем не имеет; LibreOffice
loext:theme— уточнить для 1.4 — не пишем).
6.9 Размеры страниц
- Хранятся явно (
w:page/@width @heightв EMU) + опциональный@name(A4,Letter,Legal,A3,A5,B5,Executive,Tabloid,Envelope DL,Envelope C5,Envelope #10,JIS B5, …). Определение имени: допуск ±1 мм к таблице ISO 216 / ANSI. Ориентация хранится отдельно (portrait|landscape), ноwidth/height— всегда реальные размеры страницы (как в OOXML), а не «портретная» запись:orientation— подсказка для UI и принтера, раскладка её не использует. - Импорт: OOXML
w:pgSz(twip +w:orient); xlsxpageSetup/@paperSize— коды ISO 29500 §18.18.50 (1Letter,5Legal,8A3,9A4,11A5,13B5 JIS,70A6 … — таблица из 118 значений); ODFfo:page-width/height; RTF\paperw/\paperh; DOCsprmSXaPage/sprmSYaPage; PDFMediaBox/CropBox; pptxp:sldSz(+@type). - Экспорт: xlsx — ближайший код
paperSize(иначеpaperWidth/paperHeightвextLstне поддерживается — выбираем код ближайшего и паспортMinor); PDF —MediaBoxв pt. - Принтер: Meridian Print сопоставляет имя/размер с доступными лотками
(CUPS/Windows
DEVMODE), масштабирование «по размеру бумаги».
6.10 Восстановление повреждённых файлов (recovery mode)
Включается автоматически при ошибке штатного разбора
(RecoveryMode::Auto), результат помечается в паспорте «Файл
восстановлен: …».
| Контейнер | Приём |
|---|---|
| ZIP с повреждённым/отсутствующим центральным каталогом | линейное сканирование по сигнатурам локальных заголовков
PK\x03\x04; имя/размеры из локального заголовка; если
размер в заголовке 0 (поток с дескриптором) — инфляция до конца потока;
CRC игнорируется с пометкой; дубликаты имён — берётся последний |
| Усечённый deflate-поток | инфляция до ошибки, частичный буфер отдаётся XML-парсеру в терпимом режиме |
| ZIP64 с неверными смещениями | повторный поиск по сигнатурам
PK\x06\x06/PK\x06\x07, иначе линейное
сканирование |
| XML с ошибками | «терпимый» режим quick-xml
(check_end_names=false, trim_markup_names),
недопустимые символы заменяются U+FFFD, незакрытые элементы закрываются
по стеку в конце, незнакомые сущности → литерал, обрыв файла — разбор
прекращается после последнего полного абзаца/строки; содержимое после
ошибки ищется по следующему
<w:p/<row |
OOXML без [Content_Types].xml или rels |
части определяются по путям по умолчанию
(word/document.xml, xl/workbook.xml, …),
r:id разрешаются по именам (rId5 → порядковая
картинка image5.*) |
| CFB с повреждённой FAT/каталогом | реконструкция каталога по сигнатурам записей каталога (128-байтные
записи с UTF-16 именами), цепочки FAT проверяются на циклы;
WordDocument без 0Table/1Table — текст
извлекается напрямую из потока по piece table (если CLX
потерян — сканирование UTF-16/CP1252 текста между FIB-смещениями
fcMin..fcMac) |
| DOC с «быстрым сохранением» и битой piece table | куски сортируются по CP, перекрытия отбрасываются, дыры заполняются
текстом из WordDocument последовательно |
| XLS/BIFF с усечёнными записями | запись с длиной за пределами потока — остановка листа, предыдущие листы сохраняются |
PDF с неверным xref/startxref |
реконструкция: линейное сканирование N G obj и
trailer/XRef-потоков, последний найденный
объект побеждает; /Root ищется по
/Type /Catalog; потоки с неверным /Length — до
endstream |
| RTF с несбалансированными группами | закрытие групп по стеку, обрыв — завершение текущего абзаца |
SQLite с ошибкой PRAGMA integrity_check |
.recover-подобный проход (чтение страниц напрямую через
sqlite3_recover API) в новую базу |
| Медиа с ошибкой декодера | частичный кадр (PNG/JPEG progressive — что успели) + серая заглушка,
паспорт Loss |
После восстановления документ открывается «без имени» (чтобы случайно не перезаписать оригинал), с предложением сохранить копию.
7. План тестирования форматов и критерии готовности по фазам
7.1 Виды тестов
| Вид | Инструмент | Что проверяет | Когда |
|---|---|---|---|
| Unit | cargo test в каждом крейте кодека |
разбор записей, конвертация единиц, формулы (AST ↔︎ строка), стили, нумерация, даты (таблицы истинности) | каждый коммит |
| Схемные | jing/xmllint + RNC/XSD из
spec/schemas |
каждый записанный MOP валиден по схеме; примеры из
spec/formats/*.example.xml валидны |
каждый коммит |
| Round-trip MOP | модель → MOP → модель структурно равны;
MOP → MOP байт-идентичны (детерминизм) |
целостность своего формата | каждый коммит |
| Снимочные рендера (fidelity) | fidelity-runner (§1.4) на корпусе |
уровень V по формату | gate — PR; nightly — весь корпус |
| Структурные | нормализованный diff деревьев (§1.4) | уровень S | PR для gate-файлов |
| Round-trip чужих форматов | docx → mwx → docx, сравнение с исходным после
нормализации (XML c14n, игнор rsid, порядка атрибутов);
открытие результата в MS Office через COM на Windows-агенте без диалога
восстановления; LibreOffice headless — без предупреждений в логе |
уровень R | nightly |
| Фаззинг | cargo-fuzz (libFuzzer) по одной цели на импортёр +
структурно-осведомлённые мутаторы для ZIP/CFB; корпус семян —
gate-файлы |
отсутствие паник/UB/зависаний; лимиты памяти | непрерывно на отдельной машине, 72 ч перед релизом |
| Производительность | criterion бенчмарки: docx 100 стр., xlsx 1 млн ячеек,
pptx 200 слайдов, pdf 500 стр.; память — dhat/пиковая
RSS |
бюджеты времени и памяти | nightly, регрессия > 10 % блокирует |
| Песочница | тесты краша хоста (abort() внутри кодека), лимитов,
отмены |
изоляция и восстановление | каждый коммит |
| Совместимость версий | MOP, записанные каждой выпущенной версией, открываются текущей;
текущие — предыдущей minor (с паспортом) |
§2.11 | релизная ветка |
| Шифрование/подписи | тест-векторы Argon2 (RFC 9106), AES-GCM (NIST), XAdES — проверка внешними валидаторами (DSS от ЕС, КриптоПро для ГОСТ) | корректность и интероперабельность | каждый коммит / релиз |
| Интероперабельность | матрица «мы пишем → открывают»: Word 2016/2019/365, Excel, PowerPoint, LibreOffice 7.x/24.x, Google Docs (загрузка через API), Pages/Numbers/Keynote (macOS-агент), WPS Office | отсутствие ошибок открытия, визуальный скор ≥ порога | еженедельно + релиз |
7.2 Критерии готовности (gate) по фазам
| Фаза | Форматы | Критерии (все обязательны) |
|---|---|---|
| 0 | корпус, инфраструктура | корпус ≥ 10 000 файлов с манифестом лицензий; эталоны для ≥ 6 000
(Office COM + LibreOffice); fidelity-runner в CI с
HTML-отчётом; codec-mop читает/пишет Write-подмножество;
детект формата покрывает всю матрицу 3.3 (тесты по сигнатурам);
фаззинг-цели для zip/cfb/xml-утилит без падений 24 ч |
| 1 | docx/dotx, odt, rtf, doc (R), txt/md/html, pdf (E), MOP Write | импорт docx: медиана fidelity ≥ 0.97, p10 ≥ 0.90 на корпусе docx (≥
2 500 файлов); odt: медиана ≥ 0.96; rtf: ≥ 0.95; doc: ≥ 0.93 (V), текст
— 100 % совпадение на 99 % файлов; round-trip docx → docx: 100 %
открываются в Word без восстановления, структурный diff без
Loss на 95 %; экспорт PDF: PDF/A-2b проходит veraPDF на 100
% корпуса экспорта; открытие docx 100 стр. ≤ 1,0 с (p95, ноутбук 2020
г.), память ≤ 10× размера XML; фаззинг 72 ч без падений; каждый
Loss в паспорте имеет локализованное сообщение и
Locator |
| 2 | xlsx/xltx, xls (R), ods, csv/tsv, xlsb (R), numbers (R), pdf/csv/html (E) | импорт xlsx: значения ячеек 100 % совпадение (после пересчёта — 99,9 % совпадение с кэшем Excel на корпусе с формулами, расхождения задокументированы по функциям), fidelity рендера ≥ 0.95 медиана; формулы: 100 % парсятся в AST и сериализуются обратно без изменений (нормализованных) на корпусе 1 млн формул; 500+ функций с тестами против Excel-таблиц истинности; xls: значения 100 %, формулы (RPN→AST) 99,9 %; ods: значения 100 %, OpenFormula → канон 99,9 %; книга 1 048 576 × 50 столбцов открывается ≤ 8 с, память ≤ 1,5 ГБ; сводные из xlsx воспроизводят итоги на 100 % тестовых книг; экспорт xlsx открывается в Excel без восстановления 100 % |
| 3 | pptx/potx, ppt (R), odp, svg, odg, vsdx (R), key (R) | pptx fidelity ≥ 0.95 медиана (слайды без 3D/WordArt — ≥ 0.98); анимации: 100 % эффектов из таблицы PowerPoint (≈ 150) сопоставлены, порядок и триггеры совпадают на тестовом наборе; экспорт pptx: 100 % открываются, анимации сохраняются round-trip; svg: W3C test suite (static) ≥ 95 % визуально; vsdx: базовые фигуры и коннекторы — S-уровень на 100 % корпуса Visio-стенсилов «Basic/Flowchart/Network»; odp ≥ 0.95 |
| 4 | растры, ora, psd (R), pdf (R/W), mml/tex | растровые форматы — побитовое совпадение декодирования с эталонными декодерами (libpng/libjpeg-turbo) на тест-сюитах; ora round-trip без потерь; psd: слои/группы/маски/режимы — 100 % тестового набора Adobe + 200 реальных файлов, корректирующие — 8 типов; PDF: pdf.js/pdfium корпус рендера ≥ 0.98, текст извлекается на 99 % файлов; PDF/A и PDF/UA валидны (veraPDF, PAC); MathML: MathML test suite — рендер ≥ 95 %, LaTeX ↔︎ MathML round-trip на 1 000 формул |
| 5 | enex, one (R*), eml/mbox/ics/vcf, pst (R*), msg, sqlite, accdb (R) | enex: 100 % нот импортируются с вложениями; one:
текст/картинки/таблицы на 95 % тестовых записных книжек; eml/mbox: 100 %
RFC-тест-наборов (включая кодировки и вложенные), mbox 5 ГБ — импорт
потоком ≤ 10 мин; ics: RRULE — 100 % тестов rrule
(iCalendar test suite), vcf 3.0/4.0 — 100 %; pst: Enron PST — 100 %
сообщений/вложений, календарь/контакты ≥ 99 %; accdb:
таблицы/типы/связи/запросы — 100 % на корпусе Northwind и 100 реальных
баз |
| 6–7 | история CRDT в MOP, подписи, шифрование | история: 1 млн операций — загрузка ≤ 3 с, усечение не меняет содержимое (хэш); слияние двух историй — конвергенция (property-тесты); шифрование: интероперабельность с эталонной реализацией (вектора), скорость ≥ 200 МБ/с; подписи: валидируются DSS/КриптоПро; плоский формат: детерминированный diff (1 000 последовательных правок — только затронутые строки) |
7.3 Выпуск и сопровождение
- Каждый релиз публикует «Отчёт о совместимости» (как Fidelity Report): распределение скоров по форматам, топ-20 потерь по частоте, изменения относительно прошлого релиза; данные из nightly.
- Проблемные файлы пользователей (с согласия) попадают в закрытый корпус и в трекер с тегом формата; исправление сопровождается новым синтетическим тестом в открытом корпусе (воспроизводящим проблему без пользовательских данных).
- Формат MOP замораживается на
1.0к выпуску Write 1.0 (фаза 1); все изменения до этого —0.xбез гарантий совместимости (документы бета-версий конвертируются мигратором).
8. Открытые вопросы (не противоречат библии, требуют решения)
- Хранить ли кэш результатов формул для всех ячеек в
.msx(как xlsx) или только для «внешне видимых» (значения + печать)? Текущее решение — хранить всё (быстрое открытие без пересчёта, совместимость при экспорте). - Порог включения строки в
strings.xml(сейчас: ≥ 2 повторов или > 32 байт) — проверить на корпусе по размеру/скорости. - Нужен ли отдельный
comments.xmlдля Sheets при наличииs:notesв листе — да для «потоковых» комментариев (threaded), нет для заметок; подтвердить вdocs/apps/sheets.md. - Формулы Visio ShapeSheet: какое подмножество поддерживать «живым»
(
g:guide), а какое — только кэшем значений (фаза 3). - Политика
history/для документов > 100 МБ операций: автоматическое усечение или только по запросу.
9. Вопросы к библии
Отступлений от разделов 3 библии нет; ниже — дополнения, которые предлагается внести в библию для фиксации:
- Общие пространства имён ядра
(
urn:meridian:office:core:1,:text:1,:table:1,:graphic:1,:chart:1) дополняют таблицу 3.2: они не принадлежат приложению, а разделяются всеми (одна модель текста/таблиц/графики — библия §1.2). Предлагается добавить строку «Общие модули ядра» в таблицу 3.2. - MIME зашифрованного пакета —
application/vnd.meridian.encrypted(внутренний тип — вencryption.xml), так какmimetypeдолжен выдавать тип по первым байтам, а содержимое зашифровано целиком. Предлагается добавить в §3.1 библии. .meq— ZIP-пакет MOP (а не одиночный XML): единообразие с остальными форматами, место дляmedia/(растровые превью) иmeta.xml; одиночный MathML — через экспорт.mml. Библия говорит «MathML + своя разметка» — это выполнено внутриcontent.xml..mmxуточнён как ZIP сindex.sqlite(кэш) + EML/ICS/VCF; формулировка библии «архив … + индекс» сохранена.- Единицы — библия единиц не фиксирует; решение «EMU целые» (§2.12) предлагается внести в библию §3.1 как норму для всех документов.
- Экспорт видео в Slides (
.mp4/.webm): кодек H.264 требует лицензируемого компонента — предлагается уточнить в библии, что.mp4экспорт доступен при наличии системного кодировщика (Media Foundation / VideoToolbox / GStreamer), а.webm(VP9/AV1) — встроенно. - PDF-импорт в Write («PDF → Write») в таблице 3.3
библии не указан явно для Write (есть «.docx (через Write)» у PDF) —
предлагается добавить в колонку «Чтение» Write как
.pdf (реконструкция, R*), фаза 4.