Meridian Office

MOP — единицы измерения, идентификаторы, цвета

Нормативный текст — docs/04-file-formats.md §2.12–2.14; здесь — справочные таблицы и формулы для реализации.

1. Единицы измерения

1.1 Базовая единица длины — EMU

Все длины в пакете — целые EMU (English Metric Unit), тип i64; 1 дюйм = 914 400 EMU. Запись в XML — десятичное целое без суффикса.

Единица EMU в единице Единиц в дюйме Примечание
дюйм (in) 914 400 1
сантиметр (cm) 360 000 2,54
миллиметр (mm) 36 000 25,4
1/100 мм 360 2 540 внутренняя единица LibreOffice Draw/Impress
пункт (pt) 12 700 72 PostScript-пункт
пика (pc) 152 400 6
twip (1/20 pt) 635 1 440 OOXML WordprocessingML, RTF, DOC
полупункт (1/2 pt) 6 350 144 w:sz, w:szCs
1/8 pt 1 587,5 576 толщина границ w:sz в w:bdr/w:tcBorders — единственная нецелая, округляется
пиксель при 96 dpi (px) 9 525 96 CSS/HTML/SVG по умолчанию, Excel-пиксели

Проверка делимости: 914 400 = 2⁵ · 3² · 5² · 127; 360 = 2³·3²·5, 635 = 5·127, 9 525 = 3·5²·127, 12 700 = 2²·5²·127, 36 000 = 2⁵·3²·5³ — нет, 36 000 содержит 5³, однако 914 400 / 36 000 = 25,4 не целое: миллиметр не делит дюйм, но это и не требуется — EMU делит и мм (36 000 EMU), и дюйм (914 400 EMU) нацело; важно лишь, что все единицы таблицы выражаются целым числом EMU (кроме 1/8 pt).

1.2 Пересчёт и округление

/// Длина в EMU / Length in EMU
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Length(pub i64);

pub const EMU_PER_INCH: i64 = 914_400;
pub const EMU_PER_PT: i64 = 12_700;
pub const EMU_PER_TWIP: i64 = 635;
pub const EMU_PER_HALF_PT: i64 = 6_350;
pub const EMU_PER_MM: i64 = 36_000;
pub const EMU_PER_CM: i64 = 360_000;
pub const EMU_PER_PX96: i64 = 9_525;
pub const EMU_PER_HMM: i64 = 360;          // 1/100 мм / hundredths of mm

impl Length {
    // В EMU — умножение без потерь / into EMU: lossless multiplication
    pub const fn from_twips(v: i64) -> Self { Length(v * EMU_PER_TWIP) }
    pub const fn from_half_pt(v: i64) -> Self { Length(v * EMU_PER_HALF_PT) }
    pub const fn from_hmm(v: i64) -> Self { Length(v * EMU_PER_HMM) }
    /// Границы OOXML в 1/8 pt: единственный нецелый коэффициент / OOXML border eighths of a point
    pub fn from_eighth_pt(v: i64) -> Self { Length((v * 12_700 + 4) / 8) }     // round half away from zero для v ≥ 0
    /// Дробные значения (pt из PDF/iWork, in из Visio): округление к ближайшему / fractional input
    pub fn from_pt_f64(v: f64) -> Self { Length((v * EMU_PER_PT as f64).round() as i64) }
    pub fn from_inch_f64(v: f64) -> Self { Length((v * EMU_PER_INCH as f64).round() as i64) }

    // Из EMU — деление с округлением «половина от нуля» / out of EMU: round half away from zero
    pub fn to_twips(self) -> i64 { div_round(self.0, EMU_PER_TWIP) }
    pub fn to_half_pt(self) -> i64 { div_round(self.0, EMU_PER_HALF_PT) }
    pub fn to_eighth_pt(self) -> i64 { div_round(self.0 * 8, EMU_PER_PT) }
    pub fn to_pt_f64(self) -> f64 { self.0 as f64 / EMU_PER_PT as f64 }        // для раскладки / for layout
}

/// Деление с округлением половины от нуля / integer division, round half away from zero
fn div_round(a: i64, b: i64) -> i64 {
    let (q, r) = (a / b, a % b);
    if 2 * r.abs() >= b.abs() { q + a.signum() * b.signum() } else { q }
}

Правила: умножение (в EMU) — всегда точно; деление (из EMU) — один раз, при экспорте; ядро никогда не хранит результаты деления. Раскладка считает в f64 pt, переводя из EMU один раз на входе; обратно в EMU результаты раскладки не записываются (они не часть документа).

1.3 Другие величины

Величина Хранение Пример Соответствие
Угол целое, 1/60 000 градуса (i32) rotation="5400000" = 90° DrawingML ST_Angle; ODF draw:transform rotate(rad) → rad·180/π·60000
Процент целое, 1/1000 % (i32) scale="50000" = 50 % DrawingML ST_Percentage; OOXML w:w (1/50 %?) — через пересчёт
Доля ширины строка x<число>% width="x100%" w:tblW type="pct" (в 1/50 %), ODF rel-column-width
Множитель интервала строка x<число> line="x1.15" OOXML w:spacing/@line (240 = 1,0) при lineRule=auto
Относительная ширина символа строка ch:<число> indent-left="ch:150" OOXML @leftChars (1/100 символа)
Выравнивание вместо координаты строка align:<…> x="align:center" wp:positionH/wp:align
Время/длительность мс (i32) duration="700" анимации, переходы
Дата-время ISO 8601 UTC (Z) date="2026-10-02T14:03:00Z" w:date, dc:date
Серийная дата (Sheets) f64 дней от date-base v="46298" Excel/ODF serial
Размер шрифта EMU size="152400" = 12 pt w:sz="24" (половинки пункта), ODF fo:font-size="12pt"
Кернинг/трекинг EMU spacing="-6350" = −0,5 pt w:spacing (twip), w:kern (половинки пункта)
Разрешение dpi (u32) dpi="300" Paint
Пиксели Paint u32 width="4000" слои, маски, выделение

1.4 Входной синтаксис с суффиксом (чтение и режим pretty)

length := integer                           ; EMU (канон)
        | number unit                       ; суффиксная форма
unit   := "pt" | "mm" | "cm" | "in" | "px" | "pc" | "twip" | "emu"
number := "-"? digits ("." digits)?

Читатель переводит суффиксную форму в EMU с округлением к ближайшему. Писатель пакета всегда пишет целые EMU; писатель плоского формата в режиме units="pretty" пишет суффиксную форму, только если значение точно представимо (emu % 12700 == 0 → pt, emu % 36000 == 0 → mm, иначе EMU). Это гарантирует, что pretty → emu → pretty не меняет файл.

1.5 Ширина столбцов Excel

Excel хранит ширину в «символах» максимальной цифры шрифта стиля «Обычный» (MDW, в пикселях). Пересчёт (ISO 29500 §18.3.1.13):

px  = Truncate(((256 × width + Truncate(128 / MDW)) / 256) × MDW)
emu = px × 9525
width (обратно) = Truncate((px − 5) / MDW × 100 + 0.5) / 100        ; 5 px — поля ячейки

MDW для Calibri 11 при 96 dpi = 7 px (Carlito совпадает). Исходное значение в символах кэшируется в s:col/@width-chars для round-trip.

2. Идентификаторы

2.1 UUID v7 (RFC 9562)

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-------------------------------------------------------------+
|                         unix_ts_ms (48 бит)                  |
+---------------+---------------+-------------------------------+
| unix_ts_ms    |  ver = 0111   |         rand_a (12 бит)       |
+---+-----------+---------------+-------------------------------+
|var|                      rand_b (62 бит)                      |
+---+---------------------------------------------------------+
|                           rand_b                             |
+-------------------------------------------------------------+
/// Генерация UUID v7 с монотонным счётчиком / UUID v7 with a monotonic counter
pub fn next(&mut self) -> Uuid7 {
    let mut ts = now_unix_ms();
    if ts <= self.last_ts { ts = self.last_ts; self.counter += 1; } else { self.last_ts = ts; self.counter = random_u16() & 0x0FFF; }
    if self.counter > 0x0FFF { self.last_ts += 1; ts = self.last_ts; self.counter = 0; }   // переполнение → следующая мс / overflow → next ms
    let mut b = [0u8; 16];
    b[0..6].copy_from_slice(&ts.to_be_bytes()[2..8]);
    b[6] = 0x70 | ((self.counter >> 8) as u8 & 0x0F);
    b[7] = self.counter as u8;
    fill_random(&mut b[8..16]);
    b[8] = (b[8] & 0x3F) | 0x80;                                                         // variant 10
    Uuid7(b)
}

2.2 Что получает UUID

Обязательно Опционально (по требованию) Никогда (адресуется иначе)
документ (m:document/@id, постоянен между сохранениями), секции, таблицы, листы, слайды, мастера, макеты, фигуры/изображения/группы/коннекторы, объекты (objects/), комментарии (@target), сноски? — нет (локальный id), именованные диапазоны, сводные, диаграммы, страницы Notes/Draw, слои Paint/Draw, письма/события/контакты Mail, вопросы Forms абзацы, прогоны — при участии в совместном редактировании, при появлении ссылки (закладка, комментарий, перекрёстная ссылка) или при collab.ids=all ячейки Sheets (координаты), строки/столбцы, точки геометрии, уровни списков

2.3 Локальные идентификаторы

Для сущностей, видимых только внутри пакета, — короткие ASCII-идентификаторы [A-Za-z0-9_-]{1,64}, уникальные в своём пространстве:

Пространство Префикс по соглашению Примеры
стили (m:style/@id, t:list-style/@id) по смыслу / a<n> для автоматических Heading1, TableGrid, a17, dxf-hot
авторы (meta.xml) a<n> a1
заметки (сноски) n<n> n1
комментарии c<n>, ответы c<n>-<k> c1, c1-1
правки r<n> r12
закладки bm-<имя> bm-summary
экземпляры списков L<n> L1
общие формулы Sheets sf<n> sf1
внешние ссылки Sheets x<n> x1

Ссылки на локальные идентификаторы — атрибуты ref, style, author, list, parent, continue, shared. При слиянии документов (вставка из другого файла) локальные идентификаторы переименовываются писателем; UUID — сохраняются (конфликт UUID = один и тот же объект).

2.4 Правила стабильности

3. Цвета

3.1 Грамматика

color  := "#" HEX{6} | "#" HEX{8}                     ; sRGB, альфа FF по умолчанию
        | "theme(" slot ("," WS* mod)* ")"
        | "system(" sysname ")"
        | "auto" | "none"
slot   := "dk1" | "lt1" | "dk2" | "lt2" | "accent1" .. "accent6" | "hlink" | "folHlink"
mod    := "tint" pct | "shade" pct | "lum" spct | "sat" spct | "hue" angle | "alpha" pct
pct    := number "%"            ; 0–100
spct   := ("+" | "-") number "%" ; −100..+100
angle  := number "deg"
sysname:= "window" | "window-text" | "highlight" | "highlight-text" | "gray-text" | "button-face" | "button-text"

Примеры: #3B5BDB, #3B5BDB80 (50 % прозрачности), theme(accent1), theme(accent1, tint 40%), theme(dk2, lum +25%, sat -10%), system(window-text), auto.

3.2 Модификаторы — формулы

Применяются слева направо к разрешённому цвету слота; tint/shade — в линейном RGB (как DrawingML a:tint/a:shade), lum/sat/hue — в HSL (как a:lumMod/a:lumOff/a:satMod/a:hue):

lin(c)       = c ≤ 0.04045 ? c/12.92 : ((c + 0.055)/1.055)^2.4          ; sRGB → линейный
srgb(l)      = l ≤ 0.0031308 ? 12.92·l : 1.055·l^(1/2.4) − 0.055
tint p:      l' = l·(1 − p) + p            по каждому каналу в линейном пространстве (p = pct/100) — осветление к белому
shade p:     l' = l·(1 − p)                                                                     — затемнение к чёрному
lum ±p:      L' = clamp(L + p)              в HSL (L ∈ [0,1])
sat ±p:      S' = clamp(S·(1 + p))
hue a:       H' = (H + a) mod 360
alpha p:     A' = p

Для OOXML WordprocessingML w:themeTint="99" (hex 00–FF, применяется Word в HSL по яркости — формулы отличаются от DrawingML; уточнить по ISO 29500 §17.3.2.6) импортёр записывает theme(slot, tint N%) с дополнительным атрибутом объекта m:x-attrs="w:themeTint=99" для точного round-trip; при рендере используется формула DrawingML (разница в пределах 1–2 единиц канала на типичных значениях — проверить на корпусе).

3.3 Слоты темы и сопоставление с OOXML/ODF

Слот MOP OOXML DrawingML OOXML w:themeColor (через clrSchemeMapping по умолчанию) ODF
dk1 a:dk1 text1, dark1 — (явный sRGB)
lt1 a:lt1 background1, light1 —
dk2 a:dk2 text2, dark2 —
lt2 a:lt2 background2, light2 —
accent1..6 a:accent1..6 accent1..6 —
hlink a:hlink hyperlink —
folHlink a:folHlink followedHyperlink —

a:sysClr в теме → @lastClr; a:srgbClr → hex; a:prstClr (red, blue, …) → таблица 140 именованных цветов → hex. Экспорт в ODF/RTF/HTML/PDF — вычисленный sRGB (#RRGGBB), прозрачность — отдельным свойством формата (loext:opacity, CSS rgba).

3.4 Цветовые пространства