Meridian Office

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 pdf 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 свойства. Три пути генерации:

  1. Библиотеки (python-docx, openpyxl, python-pptx, odfpy) — быстро, без лицензий, но файлы «идеальные».
  2. COM-автоматизация Microsoft Office на Windows-агенте CI (лицензия Office на агенте) — файлы «как из Word», эталонный рендер тем же Office (ExportAsFixedFormat → PDF → PNG через pdfium). Это главный источник эталонов для уровня V.
  3. 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

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) —
Mail 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

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

2.14 Идентификаторы (UUID v7)

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>

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>

Алгоритм записи:

  1. Сформировать обычный пакет MOP в памяти/временном файле (P).
  2. DEK ← 32 случайных байта (CSPRNG ОС). nonce-prefix ← 8 случайных байт.
  3. Для чанка 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 с номером и общим числом чанков защищает от перестановки и усечения.
  4. Для пароля: 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.
  5. Для сертификата: wrapped-key = RSA-OAEP(pub, DEK) или HPKE (RFC 9180) с X25519 — корпоративное восстановление по политике.
  6. thumbnail.png и meta.xml в открытом виде не сохраняются (всё внутри P).

Чтение — симметрично; неверный пароль обнаруживается на шаге разворачивания DEK (неверный тег) без расшифровки данных. Параметры Argon2 ограничены при чтении (memory ≤ 4 GiB, time ≤ 16), чтобы файл не стал DoS-инструментом. Шифрованный документ открывается только после ввода пароля; история и совместная работа с шифрованными документами — через Meridian Cloud с серверным ключом (фаза 6, вне этого документа).

2.17 Подписи

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 &quot;1-3&quot; \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>

Нумерация. Два уровня, как 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&amp;", "&amp;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)&gt;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 Потоковый разбор больших файлов

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 из мешка неизвестного
}

Поведение в приложении:

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 сохранил имена.

  1. Построение графа чужих стилей: узлы — стили, рёбра — basedOn (OOXML) / style:parent-style-name (ODF) / \sbasedon (RTF) / istdBase (DOC). Циклы разрываются (OOXML запрещает, но файлы встречаются) — ребро с меньшим priority удаляется, в паспорт Minor.
  2. Резолвер наследования — порядок применения свойств (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>), мемоизация по цепочке.
  3. Отображение на встроенные: таблица 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.
  4. Нормализация: свойства приводятся к нашим атрибутам (единицы → EMU, w:sz half-points ×6350, w:spacing/@line 240-е доли → x1.0; цвета → §2.13); тройки ascii/hAnsi/eastAsia/cs → font/font-ea/font-cs (при ascii == hAnsi — одно значение); w:lang → lang/lang-ea/lang-cs.
  5. Автостили ODF → прямое форматирование на объекте; если один автостиль используется > 50 раз (типично для Calc) — остаётся стилем auto="1".
  6. Экспорт — обратная задача: наши стили → чужие; прямое форматирование → w:pPr/w:rPr (OOXML) или собранные автостили с дедупликацией по хэшу свойств (ODF); builtin → правильное английское имя Word (w:name="heading 1" в нижнем регистре, как пишет Word) с w:uiPriority, w:qFormat.
  7. Конфликты имён при вставке между документами: стиль с тем же 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`

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 → наша модель:

  1. 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).
  2. w:abstractNum/w:numStyleLink → стиль-список ссылается на стиль абзаца с w:numPr → разрешается в t:list-style/@linked-style; w:styleLink — обратная ссылка (сохраняем).
  3. w:num → t:list (id = "n<numId>", style = "an…"), w:lvlOverride → t:override level start (из w:startOverride) или полная замена уровня (t:override с вложенным t:level).
  4. Абзац: w:numPr/w:numId + w:ilvl → list/level; numId="0" — отмена нумерации (list="none"). Нумерация из стиля абзаца (w:style/w:pPr/w:numPr) → t:ppr/@list в стиле, абзац не дублирует.
  5. Подсчёт номеров при раскладке: для каждого t:list — счётчики по уровням; переход на уровень n сбрасывает счётчики > n (с учётом restart-after); t:override/@start применяется при первом появлении уровня; continue="L1" — общие счётчики; legal — все уровни выше показываются арабскими.
  6. 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 Таблицы

  1. Сетка из 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 для продолжений.
  2. Сетка из 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.
  3. Нормализация сетки: если суммы colspan по строкам различаются (кривые файлы), сетка достраивается до максимума; «рваные» строки дополняются phantom-ячейками.
  4. Авто-подбор ширин (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".
  5. layout="fixed": ширины — ровно tb:col/@width, содержимое переносится/обрезается.
  6. Вложенность — рекурсивно с тем же алгоритмом, внутренняя таблица участвует в min/max внешней ячейки.
  7. Повторяющиеся строки-заголовки (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 Даты

6.8 Цвета и темы

  1. 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, а слайды ссылаются на свой мастер.
  2. OOXML w:clrSchemeMapping (bg1→lt1, tx1→dk1, bg2→lt2, tx2→dk2, …): ссылки w:themeColor="text1" переводятся в слоты dk1 через карту; при экспорте карта пишется стандартная.
  3. w:themeTint/w:themeShade (hex 00–FF) → theme(slot, tint N%) / theme(slot, shade N%), где N = 100 − tint/255·100; Word применяет их в пространстве HSL по яркости, а DrawingML a:tint/a:shade — в линейном RGB, поэтому исходное значение сохраняется в m:x-attrs для точного round-trip, а рендер использует формулу DrawingML (расхождение — единицы канала; точные формулы Word — уточнить по ISO 29500 §17.3.2.6 и проверить на корпусе). DrawingML a:schemeClr с a:tint/a:shade/a:lumMod/a:lumOff/a:satMod/a:alpha → те же модификаторы (линейный RGB для tint/shade, HSL для lum/sat).
  4. Индексированные цвета Excel (indexed="N", палитра 0–63 по умолчанию или colors/indexedColors), BIFF PALETTE, icv → sRGB.
  5. w:color="auto"/w:fill="auto" → auto (контрастный к фону при рендере); VML fillcolor="#rrggbb [N]" → hex.
  6. Экспорт в формат без тем (rtf, html, pdf) — вычисленный sRGB; в ODF — sRGB (ODF 1.3 тем не имеет; LibreOffice loext:theme — уточнить для 1.4 — не пишем).

6.9 Размеры страниц

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 Выпуск и сопровождение


8. Открытые вопросы (не противоречат библии, требуют решения)

  1. Хранить ли кэш результатов формул для всех ячеек в .msx (как xlsx) или только для «внешне видимых» (значения + печать)? Текущее решение — хранить всё (быстрое открытие без пересчёта, совместимость при экспорте).
  2. Порог включения строки в strings.xml (сейчас: ≥ 2 повторов или > 32 байт) — проверить на корпусе по размеру/скорости.
  3. Нужен ли отдельный comments.xml для Sheets при наличии s:notes в листе — да для «потоковых» комментариев (threaded), нет для заметок; подтвердить в docs/apps/sheets.md.
  4. Формулы Visio ShapeSheet: какое подмножество поддерживать «живым» (g:guide), а какое — только кэшем значений (фаза 3).
  5. Политика history/ для документов > 100 МБ операций: автоматическое усечение или только по запросу.

9. Вопросы к библии

Отступлений от разделов 3 библии нет; ниже — дополнения, которые предлагается внести в библию для фиксации:

  1. Общие пространства имён ядра (urn:meridian:office:core:1, :text:1, :table:1, :graphic:1, :chart:1) дополняют таблицу 3.2: они не принадлежат приложению, а разделяются всеми (одна модель текста/таблиц/графики — библия §1.2). Предлагается добавить строку «Общие модули ядра» в таблицу 3.2.
  2. MIME зашифрованного пакета — application/vnd.meridian.encrypted (внутренний тип — в encryption.xml), так как mimetype должен выдавать тип по первым байтам, а содержимое зашифровано целиком. Предлагается добавить в §3.1 библии.
  3. .meq — ZIP-пакет MOP (а не одиночный XML): единообразие с остальными форматами, место для media/ (растровые превью) и meta.xml; одиночный MathML — через экспорт .mml. Библия говорит «MathML + своя разметка» — это выполнено внутри content.xml.
  4. .mmx уточнён как ZIP с index.sqlite (кэш) + EML/ICS/VCF; формулировка библии «архив … + индекс» сохранена.
  5. Единицы — библия единиц не фиксирует; решение «EMU целые» (§2.12) предлагается внести в библию §3.1 как норму для всех документов.
  6. Экспорт видео в Slides (.mp4/.webm): кодек H.264 требует лицензируемого компонента — предлагается уточнить в библии, что .mp4 экспорт доступен при наличии системного кодировщика (Media Foundation / VideoToolbox / GStreamer), а .webm (VP9/AV1) — встроенно.
  7. PDF-импорт в Write («PDF → Write») в таблице 3.3 библии не указан явно для Write (есть «.docx (через Write)» у PDF) — предлагается добавить в колонку «Чтение» Write как .pdf (реконструкция, R*), фаза 4.