Meridian Office

Meridian Paint — растровый редактор

Документ приложения по соглашениям раздела 8 библии (docs/00-foundation.md). Версия 0.1 · 2026-10-03 · фаза 4 · редакция: Home и выше (инструмент «Снимок» — см. «Вопросы к библии»). Цвет приложения --app-paint #C2309B, значок — капля-кисть, формат документа .mpi (urn:meridian:office:paint:1), код ядра — kernel/crates/meridian-raster/, оболочка — shell/src/paint/.


1. Назначение и аналоги

1.1 Назначение

Meridian Paint — растровый редактор для повседневных задач «нарисовать, поправить, вырезать, подписать, снять экран»: скриншоты с пометками, обработка фотографий для документов и презентаций, иллюстрации для Write/Slides/Notes, пиксельная графика, значки, обложки, простые анимации. Это не замена Photoshop и не Krita для художников: слои, маски, выделения, коррекции и фильтры — да; CMYK-допечатная подготовка, 3D, видео, узлы композитинга — нет.

Paint работает в двух ролях:

Роль Как запускается Что умеет
Отдельное приложение из Hub, по ассоциации с файлами изображений, глобальной клавишей «Снимок» всё, описанное в этом документе
Встроенный редактор изображений двойной щелчок по картинке в Write/Slides/Notes/Draw → «Изменить в Paint» тот же код (meridian-raster + панель инструментов Paint во всплывающем окне); результат возвращается в документ как PNG/WebP с сохранением исходника .mpi в objects/ пакета

1.2 Аналоги

Продукт Сильные стороны Слабые стороны (с нашей точки зрения)
Microsoft Paint (Win 11) мгновенный запуск, прост, слои и удаление фона (2023+) нет масок, выделений с растушёвкой, коррекций, 16 бит, ICC; история ограничена
Paint.NET слои, режимы наложения, плагины, быстрый только Windows, нет масок слоя и групп, нет корректирующих слоёв, 8 бит, нет планшетного нажима, нет текстовых слоёв (текст растрируется сразу)
Krita лучшие кисти, 16/32 бит, анимация, OCIO тяжёлый и сложный для «поправить скриншот»; интерфейс для художников
GIMP 3 мощный, свободный устаревший UX, медленный на больших холстах, неединообразие диалогов
Pinta простой, кроссплатформенный мало функций, GTK-зависимость, нет слоёв-масок
Snipping Tool / Greenshot / ShareX снимки экрана, пометки не редакторы изображений
Photoshop Elements коррекции, автофункции платный, тяжёлый, только Win/macOS

1.3 Чем лучше Paint и Paint.NET

Возможность MS Paint Paint.NET Meridian Paint
Три ОС нет нет Windows, macOS, Linux
Слои: растровые / текстовые / векторные / корректирующие растр растр все четыре, текст и фигуры редактируемы до растрирования
Маски слоя, группы, блокировки нет нет есть (P0/P1)
Режимы наложения нет 14 полный Porter–Duff + 24 режима (разделённые и неразделённые)
Глубина цвета 8 бит 8 бит 8 бит и 16 бит float (линейное пространство)
Управление цветом (ICC) нет нет sRGB / Display P3 / Adobe RGB, профиль монитора
Выделения прямоугольник, лассо прямоугольник, лассо, эллипс, палочка + многоугольник, по цвету, быстрое (P2), операции, растушёвка, обводка/заливка
Кисть базовая базовая дабы с интервалом, нажим/наклон планшета, стабилизатор, свои кисти
Снимки экрана нет (отдельное приложение) нет область/окно/экран/с задержкой, наложение выбора, пометки
История ограниченная панель истории панель + снимки состояния + undo по тайлам без ограничения глубины (лимит по памяти)
Большие холсты тормозит терпимо тайлы 256×256, разреженные слои, GPU-композитинг; 8000×8000 × 20 слоёв — отклик < 16 мс
Скрипты нет плагины C# MeridianScript (TypeScript/Python), пакетная обработка
Анимация нет нет кадры GIF/WebP/APNG (P1)
Форматы базовые базовые + плагины PSD/ORA/XCF чтение, PSD запись (базово), HEIC/AVIF/RAW
Печать, PDF базовая базовая Meridian Print: поля, масштаб, несколько на лист, PDF

1.4 Границы продукта (чего не делаем)


2. Функции

Приоритеты: P0 — без этого нельзя выпустить Paint 1.0; P1 — в 1.x; P2 — позже.

2.1 Создание, открытие, захват экрана

Функция Описание Приоритет
Новый холст ширина/высота (px, мм, см, дюймы при заданном DPI), DPI, ориентация, фон (белый, прозрачный, цвет), цветовое пространство, глубина (8/16), пресеты (A4 300 dpi, Full HD, 4K, значки, соцсети, «как в буфере», «как последний») P0
Из буфера обмена Ctrl+Shift+V — новый документ размером с изображение в буфере; поддержка CF_DIB/PNG/TIFF/HTML-img/file-list P0
Открыть все форматы раздела 7; несколько файлов → вкладки; drag-and-drop файлов на окно (на холст — как новый слой) P0
Снимок области наложение выбора области поверх всех мониторов, лупа у курсора, размер выделения, привязка к краям окон (Alt — без привязки), Enter/двойной щелчок — снять P0
Снимок окна подсветка окна под курсором, снятие с тенью (macOS) / без рамки; на Wayland — через портал (выбор окна системным диалогом) P0
Снимок экрана текущий монитор или все мониторы одним изображением P0
С задержкой 1–60 с, обратный отсчёт в трее/строке состояния; для меню и всплывающих окон P0
Инструмент «Снимок» (Snip) лёгкий режим как Snipping Tool: глобальная клавиша → наложение → после снятия мини-окно пометок (стрелка, рамка, номер-кружок, маркер, размытие области, текст, обрезка) → копировать / сохранить / открыть в Paint / отправить в Notes; без открытия полного окна Paint P0
Снимок с курсором включить/выключить курсор мыши на снимке P1
Снимок прокручиваемого окна склейка снимков при прокрутке (сопоставление перекрытий) P2
Запись экрана в GIF/WebP до 60 с, область/окно, 10–30 fps P2
Со сканера TWAIN (Windows), ImageCaptureCore (macOS), SANE (Linux) P1
Импорт RAW через libraw (LGPL, динамически), базовые параметры проявки (экспозиция, баланс белого) P1

2.2 Инструменты рисования

Вертикальная колонка инструментов (см. PT-01). У каждого инструмента — панель «Свойства инструмента» (PT-05).

Инструмент Клавиша Свойства Приоритет
Карандаш P размер 1–64 px, без сглаживания (жёсткие пиксели), режим «пиксельная линия» (Брезенхем), прозрачность P0
Кисть B размер 1–2000 px, жёсткость 0–100 %, интервал 1–200 % диаметра, непрозрачность, поток (flow), нажим → размер/непрозрачность/поток (кривые), наклон → угол/форма, стабилизатор 0–100 %, сглаживание пути, форма (круг/эллипс/изображение-штамп), текстура, разброс (scatter), динамика угла, режим наложения мазка, «мазок на отдельном слое» (предпросмотр) P0 (нажим/наклон P0, текстура/разброс P1)
Аэрограф Shift+B как кисть с накоплением по времени (flow × dt) P1
Ластик E те же свойства, что кисть; на слое без альфы — рисует фоновым цветом; «ластик фона» (стирает похожий цвет) P1 P0
Заливка G допуск 0–255 (RGB) или 0–100 ΔE (LAB), связность 4/8, режим «все слои» (образец по композиту), «непрерывная/глобальная», сглаживание края, закрытие разрывов до N px (P1), заливка узором P0
Градиент Shift+G линейный / радиальный / конический / отражённый / ромбовидный (P1); редактор градиента (точки цвета и прозрачности, интерполяция sRGB / линейная / LAB / OKLab), дизеринг, повтор, предустановки, рисование через выделение P0
Текст T создаёт/редактирует текстовый слой: шрифт, размер, стиль, цвет, обводка, выравнивание, межстрочный/межбуквенный, абзацный текст в рамке или строчный, вертикальный (P1), текст по кривой (P2); раскладка — движок Write (text-layout) P0
Фигуры U векторный слой: прямоугольник (скруглённый), эллипс, многоугольник/звезда, линия, стрелка (виды наконечников), кривая Безье, произвольный путь; заливка/обводка/штрих/скругления углов; редактирование узлов до растрирования P0
Выделение M/L/W/Q см. 2.3 P0
Перемещение V перемещает слой/выделенные пиксели; Ctrl+T — трансформация P0
Обрезка C рамка с правилами третей/золотого сечения, фиксированные пропорции, выравнивание горизонта (поворот рамкой), «удалить обрезанное» или «скрыть» (слои за холстом остаются) P0
Пипетка I образец 1×1 / 3×3 / 5×5 / 11×11, со слоя или композита, в основной/фоновый цвет, с экрана (вне окна) P0
Лупа Z клик — увеличить, Alt — уменьшить, рамка — в область, двойной клик — 100 % P0
Рука H / пробел панорамирование; R + перетаскивание — поворот вида P0
Клонирующий штамп S Alt+клик — источник, выровненный/невыровненный, образец: слой/все слои, те же свойства кисти P0
Восстанавливающая кисть J клонирование с подгонкой яркости/цвета по краям (Пуассон-подобное смешивание, P1 — упрощённое) P1
Осветление/затемнение O диапазон: тени/средние/света, экспозиция 1–100 %, Alt — переключение P0
Размытие/резкость кистью R сила, Alt — переключение; «палец» (smudge) P1 P0
Губка (насыщенность) Shift+O насытить/обесцветить кистью P1
Измеритель Shift+I расстояние/угол между точками, в px/мм при DPI P1

Общие свойства: цвета основной/фоновый (X — поменять, D — чёрный/белый), режим наложения мазка, «рисовать только в выделении», сглаживание (антиалиасинг) вкл/выкл, «сохранить прозрачность» (lock alpha).

2.3 Выделение

Функция Описание Приоритет
Прямоугольник M с фиксированным соотношением/размером, от центра (Alt), квадрат (Shift), скруглённые углы P0
Эллипс Shift+M те же модификаторы P0
Лассо L свободная форма P0
Многоугольник Shift+L щелчки по вершинам, Backspace — отменить вершину P0
Магнитное лассо прилипание к краям (градиент Собеля, живая обводка — Intelligent Scissors / Dijkstra по карте стоимости) P1
Волшебная палочка W flood по допуску от точки щелчка, связность 4/8, непрерывно/глобально, образец слой/композит P0
По цвету все пиксели похожего цвета (глобально), с «мягкостью» (fuzziness → градиентная маска) P0
Быстрое выделение Q мазок кистью → GrabCut-подобное выделение объекта с уточнением (+/−) P2
Операции новое / сложение (Shift) / вычитание (Alt) / пересечение (Shift+Alt) P0
Выделить всё / снять / инвертировать Ctrl+A / Ctrl+D / Ctrl+Shift+I P0
Растушёвка, расширить/сузить, сгладить, граница диалоги с предпросмотром; растушёвка — гауссово размытие маски P0
Обводка выделения ширина, цвет, положение (внутри/центр/снаружи), на новый слой P0
Заливка выделения цвет / узор / «с учётом содержимого» (P2) P0
Сохранить/загрузить выделение как канал (маска) в документе .mpi; «из маски слоя», «из альфы слоя» P1
Трансформировать выделение масштаб/поворот только контура без пикселей P1
Выделение в путь / путь в выделение связь с векторным слоем (трассировка контура маски — Potrace-подобно, P2) P2
Режим «быстрая маска» Shift+Q (Q занята быстрым выделением): выделение рисуется кистью/заливкой как красная полупрозрачная маска; повторное Shift+Q — обратно в выделение P1
Показ: «марширующие муравьи» + полупрозрачная заливка вне выделения (опция) — P0

2.4 Трансформации, обрезка, холст

Функция Описание Приоритет
Свободная трансформация Ctrl+T масштаб (угловые/боковые маркеры, Shift — пропорции, Alt — от центра), поворот (вне рамки; шаг 15° с Shift), наклон (Ctrl + боковой), перспектива (Ctrl + угловой), отражение; числовой ввод в панели свойств; интерполяция: ближайший / билинейная / бикубическая / Lanczos-3; предпросмотр в реальном времени на GPU, окончательный ресамплинг при подтверждении P0 (перспектива P1)
Деформация сеткой (warp) сетка 3×3…8×8 с Безье-кривыми P2
Размер изображения по px / % / мм при DPI, пропорции, ресамплинг (те же фильтры + «Box» для уменьшения, «Mitchell»), «только изменить DPI», предупреждение о потере качества P0
Размер холста новый размер, якорь 3×3, цвет расширения (фон/прозрачный), относительно/абсолютно P0
Обрезка по выделению / по содержимому (trim) удалить прозрачные/однотонные поля P0
Поворот холста 90°/180°/произвольный с ресамплингом, с расширением холста P0
Отражение по горизонтали/вертикали — холст или слой P0
Выровнять слои / распределить по краям/центрам выделения или холста P1
Автовыравнивание горизонта по линии, проведённой пользователем (как в Lightroom) P1

2.5 Слои

Функция Описание Приоритет
Растровые слои неограниченно; разреженные (тайлы вне содержимого не занимают память), могут выходить за холст P0
Текстовые слои редактируемый текст, растрируются при экспорте/по команде P0
Векторные слои фигуры из 2.2; растрирование по команде; импорт SVG как векторный слой P0
Корректирующие слои уровни, кривые, оттенок/насыщенность, яркость/контраст, цветовой баланс, обесцвечивание (ч/б с микшером), инверсия, порог, постеризация, карта градиента (P2), фотофильтр (P2), сочность (vibrance, P2); действуют на всё ниже (или на слой ниже — «обтравочная маска») P1
Группы вложенность, свои режим/непрозрачность/маска; «сквозной» режим (pass-through) или изолированная P0
Режимы наложения Porter–Duff: Clear, Source, Destination, Source-over (Normal), Destination-over (Behind), Source-in, Destination-in, Source-out, Destination-out, Source-atop, Destination-atop, XOR, Plus (Lighter); разделённые: Multiply, Screen, Overlay, Darken, Lighten, Color Dodge, Color Burn, Hard Light, Soft Light, Difference, Exclusion, Linear Dodge (Add), Linear Burn, Vivid Light, Linear Light, Pin Light, Hard Mix, Subtract, Divide, Darker Color, Lighter Color; неразделённые: Hue, Saturation, Color, Luminosity; служебные: Dissolve (стохастический), Erase (P1) P0
Непрозрачность 0–100 %, «заливка» (fill) отдельно от непрозрачности — P2 P0
Маски слоя 8-бит (или 16-бит float в документах 16 бит) растровая маска, редактируется кистью/градиентом/заливкой, из выделения, инверсия, «показать маску» (Alt+клик), отключить (Shift+клик), связь с слоем (перемещение вместе), применить P0
Векторные маски путь как маска P2
Обтравочная маска (clipping) слой ограничивается альфой слоя ниже P1
Блокировки пиксели, прозрачность (lock alpha), положение, всё P0
Операции новый, дублировать, удалить, объединить вниз, объединить видимые, свести, слить группу, растрировать, порядок (вверх/вниз/наверх/вниз), переименовать (двойной щелчок), цветовая метка, скрыть все другие (Alt+клик по глазу), выбрать слой щелчком по холсту (Ctrl+клик / авто-выбор), миниатюры, фильтр по типу/имени, поиск P0
Стили слоя (тень, обводка, свечение, тиснение) как неразрушающие эффекты (P2); обводка и тень — P1 P1/P2
Смарт-объекты (вложенные .mpi) слой-ссылка на другой документ/файл с неразрушающими трансформациями P2
Копирование слоёв между документами перетаскивание между вкладками; буфер обмена со слоями (внутренний формат) P0

2.6 Коррекции (меню «Изображение → Коррекция», применяются к слою или как корректирующий слой)

Коррекция Параметры Приоритет
Яркость/контраст яркость −150…+150, контраст −50…+100, «устаревший» режим (линейный) P0
Уровни по каналам RGB/R/G/B/альфа: вход (чёрный, гамма, белый), выход (чёрный, белый), пипетки чёрной/серой/белой точки, гистограмма, автоуровни, обрезка % P0
Кривые по каналам, до 16 точек, сплайн (монотонный кубический) или ломаная, пипетка «точка на кривой», гистограмма, предустановки, карандаш P0
Цветовой баланс тени/средние/света × (голубой–красный, пурпурный–зелёный, жёлтый–синий), сохранять яркость P0
Оттенок/насыщенность общий или по 6 диапазонам (красные, жёлтые, …), оттенок −180…+180, насыщенность, яркость, «тонирование» (colorize) P0
Обесцвечивание способы: яркость (Rec.709), светлота (LAB L), среднее, максимум, микшер каналов (6 цветов, как «Ч/Б» Photoshop) P0
Инверсия все каналы или по каналу P0
Постеризация уровней 2–255 P0
Порог 0–255, предпросмотр P0
Автоуровни / автоконтраст / автоцвет обрезка 0,1 % гистограммы по каналам (автоуровни), по яркости (автоконтраст), с нейтрализацией серого (автоцвет) P0
Экспозиция (16 бит) экспозиция в EV, смещение, гамма P1
Сочность (vibrance) насыщенность с защитой телесных тонов P1
Выборочная коррекция цвета CMYK-сдвиги по цветовым диапазонам P2
Температура/оттенок (баланс белого) через LAB/Bradford P1
Тени/света локальная коррекция с размытым каналом яркости P1
Карта градиента, фотофильтр, микшер каналов, замена цвета — P2

2.7 Фильтры (меню «Фильтры», все с предпросмотром на холсте и в диалоге, см. PT-11)

Группа Фильтры Приоритет
Размытие по Гауссу (σ 0,1–250, разделяемое; ≥ 8 px — box×3), размытие в движении (угол, длина), радиальное (вращение/зум, центр), поверхностное (P1), среднее/бокс, «умное» (билатеральное, P1), размытие по объективу (P2) P0
Резкость нерезкая маска (количество, радиус, порог), резкость, «умная резкость» (P1), высокочастотный (high pass) P0
Шум добавить шум (гауссов/равномерный, монохромный, %), удаление шума (медианный 3–15, P0; нелокальные средние P2), пыль и царапины (медианный с порогом) P0
Пикселизация мозаика (размер ячейки), кристаллизация (Вороной, P1), точечный растр (P1) P0
Художественные масляная краска (размер кисти, детализация — алгоритм по гистограмме интенсивности), тиснение (угол, высота), карандашный набросок (P1), акварель (P2), витраж (P2) P0
Края обнаружение краёв (Собель / Прюитт / Лаплас), свечение краёв, трассировка контуров (P1) P0
Искажения волны, рябь, скручивание (twirl), сферизация, полярные координаты, коррекция объектива (бочка/подушка, P1), смещение по карте (P2) P0 (базовые)
Стилизация виньетка (сила, радиус, мягкость, форма, цвет), размытие краёв, зернистость плёнки, рамка P0
Рендер облака (Перлин), шум Перлина/симплекс, блик объектива (P2), узор P1
Прочее смещение (offset с заворачиванием — для бесшовных текстур), пользовательское ядро 3×3…7×7, минимум/максимум (морфология), «применить скрипт» — фильтр на MeridianScript P1
Повторить последний фильтр Ctrl+F; «последний фильтр с настройками» Ctrl+Alt+F P0
Галерея фильтров стек фильтров с предпросмотром (P2) P2

2.8 Цвет

Функция Описание Приоритет
Панель «Цвет» круг HSV (кольцо оттенка + треугольник/квадрат), ползунки RGB/HSV/HSL/LAB/OKLCH, HEX с альфой, пипетка, предыдущий/текущий цвет, предупреждение «вне охвата» P0
Палитры образцы (swatches): текущая палитра документа, системные (Material, Tailwind, Pantone-подобные — без торговых марок), импорт .gpl (GIMP), .aco/.ase (Adobe), .pal, экспорт; создание из изображения (квантование) P0
История цветов последние 32 P0
Цветовые профили рабочее пространство документа: sRGB (по умолчанию), Display P3, Adobe RGB (1998), ProPhoto (P2), линейный sRGB (16 бит); ICC v2/v4 через lcms2; при открытии: встроенный профиль → «сохранить / преобразовать в рабочее / игнорировать»; назначение профиля, преобразование в профиль с намерением (perceptual/relative/absolute/saturation) и компенсацией чёрной точки; профиль монитора из ОС, цветопроба (soft proof, P1) P0
Глубина цвета 8 бит на канал (sRGB-кодированный) и 16 бит float (линейный); преобразование 8↔︎16 с дизерингом вниз; все инструменты и фильтры работают в обеих глубинах (внутренние вычисления f32) P0 (16 бит P0, но часть фильтров — P1)
Индексированный режим палитра ≤ 256 цветов для GIF/PNG-8/ICO с дизерингом; редактирование палитры P1
Градации серого режим документа с одним каналом + альфа (хранится как RGB с флагом — упрощение P0, настоящий 1-канальный режим P2) P1
Смешивание в линейном пространстве опция документа (по умолчанию: 8 бит — в sRGB-кодированном, 16 бит — линейное) P0

2.9 Вид, навигация, направляющие

Функция Описание Приоритет
Масштаб 1 %–6400 %, колесо + Ctrl, Ctrl++/−/0/1, по ширине/по размеру окна, плавный зум к курсору; пиксельная сетка при ≥ 800 % P0
Поворот вида R+перетаскивание, Shift+колесо, сброс Ctrl+Shift+R; не меняет пиксели P0
Отражение вида Shift+F — для проверки рисунка P1
Навигатор миниатюра с рамкой вида, ползунок масштаба (PT-06) P0
Линейки Ctrl+R, единицы px/мм/см/дюймы/%, начало координат перетаскиванием P0
Направляющие перетаскивание с линейки, числовой ввод, привязка к ним, блокировка, цвет; «новая направляющая по выделению» P0
Сетка шаг/подшаг, цвет, стиль (линии/точки), привязка; изометрическая (P2) P0
Привязки к сетке, направляющим, краям холста, краям слоёв, центрам, пиксельной сетке (для фигур/выделений) P0
Два окна одного документа Вид → Новое окно — разный масштаб одного документа P1
Полноэкранный режим / режим «Фокус» F11 / Tab скрывает панели P0
Предупреждения охвата, печатные поля «Вид → Поля печати» показывает рамку страницы с текущими настройками принтера P1

2.10 История, автосохранение, восстановление

Функция Описание Приоритет
Undo/Redo неограниченно, лимит памяти (настройка, по умолчанию 25 % ОЗУ или 2 ГБ), старые записи сбрасываются на диск, затем удаляются P0
Панель «История» список действий с миниатюрами, щелчок — вернуться; «снимок» (snapshot) — именованное состояние; ветвление не поддерживается (переход после отката затирает хвост — с предупреждением), «кисть истории» P2 P0
Автосохранение журнал операций каждые 2 с (как в библии) в ~/.cache/meridian/paint/recovery/<uuid>/: тайлы-дельты + ops.log; при запуске — «Восстановить несохранённые документы» P0
Автосохранение в файл опция «сохранять документ каждые N минут» (для .mpi) P1
Версии документа .mpi может хранить снимки в history/ (опционально) P2

2.11 Анимация (кадры)

Функция Описание Приоритет
Панель «Кадры» таймлайн: кадр = набор видимых слоёв (группа «Кадр N») + длительность в мс; режим «каждый слой — кадр» для простых GIF P1
Импорт анимированных GIF/WebP/APNG кадры → группы слоёв с длительностями; disposal учитывается P1
Экспорт GIF/WebP/APNG палитра глобальная/локальная, дизеринг, оптимизация кадров (разности, прозрачность неизменных областей), циклы, fps P1
Калька (onion skin) предыдущий/следующий кадры полупрозрачно P1
Воспроизведение на холсте, с ограничением fps P1
Твининг (положение/непрозрачность) между ключевыми кадрами для слоёв P2

2.12 Печать и экспорт

Функция Описание Приоритет
Печать Meridian Print: масштаб (по размеру/реальный при DPI/произвольный), положение на листе, поля, несколько копий, несколько изображений на лист (контактный лист P1), цветопроба профилем принтера (P2) P0
Экспорт Ctrl+Shift+E формат, качество/сжатие, метаданные (EXIF/XMP/ICC — включить/удалить), размер на выходе, предпросмотр с оценкой размера файла и сравнением «до/после» для JPEG/WebP (PT-12) P0
Экспорт слоёв/групп в файлы каждый слой — файл (имя по шаблону), обрезка по содержимому P1
Быстрый экспорт Ctrl+Alt+Shift+E — PNG в ту же папку с последними настройками P0
Копировать как PNG / SVG (векторные слои) / Base64 data-URI / «для Write» (с .mpi исходником) P0
Поделиться Mail (вложение), Notes (вырезка), системное меню «Поделиться» (macOS/Windows) P1
Экспорт в PDF одно/многостраничный (кадры или слои — страницы), с ICC P0

2.13 Доступность

Функция Описание Приоритет
Клавиатурное управление всем обход панелей, инструменты — буквы, все диалоги по Tab P0
Экранный диктор имена слоёв, свойств, значений; холст описывается как «Холст, 1920×1080, слой "Фон"» P0
Высокий контраст и масштаб интерфейса темы MDS, масштаб 100–300 % P0
Крупные маркеры и курсоры размер курсора кисти, контрастный контур курсора на любом фоне (XOR-обводка) P0
Дальтонизм симуляция (протанопия/дейтеранопия/тританопия) в «Вид → Проба» P1
Уменьшение движения отключение анимаций интерфейса P0
Альтернативный текст в документе .mpi и при экспорте в Write/PDF (meta.xml/description) P1

2.14 Интеграция с пакетом и расширяемость


3. Экраны

Обозначения: PT-NN. Макеты — mockups/paint.html, каждый экран — <section class="screen">. Каркас окна — по разделу 5.3 библии: заголовок, ribbon (вкладки «Файл · Главная · Выделение · Слой · Изображение · Фильтры · Вид · Скрипты»), навигационная панель слева заменена колонкой инструментов (40 px, две колонки по 20 px в компактном режиме), холст, боковая панель справа, строка состояния.

PT-01 Главное окно

┌──────────────────────────────────────────────────────────────────────────────────┐
│ ● Безымянный-1.mpi — Meridian Paint                      [поиск ⌘K]  [☁] [👤]   │
├──────────────────────────────────────────────────────────────────────────────────┤
│ Файл  Главная  Выделение  Слой  Изображение  Фильтры  Вид  Скрипты     [⌄]       │
│ [Буфер] [Инструменты] [Кисть: размер жёсткость непрозр.] [Цвет] [Коррекции] [Снимок] │
├────┬──────────────────────────────────────────────────────────┬──────────────────┤
│ P  │  ◦ Безымянный-1 ×  │ фото.jpg ×  │ +                      │ Слои  Цвет  Ист. │
│ B  ├──────────────────────────────────────────────────────────┤──────────────────│
│ E  │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │ Normal ▾ 100 % │
│ G  │ ▒▒┌──────────────────────────────────────────────┐▒▒▒▒▒▒ │ 🔒 ▢ 👁 Текст    │
│ T  │ ▒▒│ ░░░░░░ холст на «шахматке» ░░░░░░░░░░░░░░░░░░ │▒▒▒▒▒▒ │ 👁 ▣ Фигуры     │
│ U  │ ▒▒│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │▒▒▒▒▒▒ │ 👁 ▣ Слой 1     │
│ M  │ ▒▒│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │▒▒▒▒▒▒ │ 👁 ▣ Фон     🔒 │
│ L  │ ▒▒└──────────────────────────────────────────────┘▒▒▒▒▒▒ │──────────────────│
│ W  │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │ Свойства инстр.  │
│ V  │                                                          │ Навигатор        │
│ …  │                                                          │                  │
├────┴──────────────────────────────────────────────────────────┴──────────────────┤
│ 1920×1080 px · 8 бит sRGB · Слой 1 · X: 412 Y: 230 · RGB 34 120 200 · [▭ 100 % ▾] · ↻0° │
└──────────────────────────────────────────────────────────────────────────────────┘
Зона Содержимое
Колонка инструментов вертикальная, 40 px; группы инструментов с длинным нажатием/правой кнопкой — варианты (кисть/аэрограф); внизу — основной/фоновый цвет (квадраты 28 px), кнопки «поменять» и «сброс», переключатель «быстрая маска»
Вкладки документов над холстом; перетаскивание вкладки наружу — новое окно; индикатор несохранённых изменений ●
Холст область с «шахматкой» (клетки 8 px, серые #CCCCCC/#FFFFFF, настраиваемые) за холстом — фон --m-surface-2; поля вокруг холста — прокручиваемые на половину окна; тень холста 0 2 8 px
Ribbon «Главная» группы: Буфер (вставить/копировать/вырезать/вставить как слой), Инструменты (дублирует колонку для режима «лента»), Параметры текущего инструмента (размер, жёсткость, непрозрачность — компактные ползунки), Цвет (основной/фоновый), Коррекции (кнопки-меню), Снимок (область/окно/экран/задержка)
Ribbon «Выделение» инструменты выделения, операции, растушёвка, обводка/заливка, сохранить/загрузить
Ribbon «Слой» новый/дублировать/удалить, группы, маски, порядок, объединение, растрировать, свойства
Ribbon «Изображение» размер изображения/холста, обрезка, поворот/отражение, коррекции (галерея), режим/глубина, профиль
Ribbon «Фильтры» галерея групп (2.7), повторить последний
Ribbon «Вид» масштаб, поворот, линейки/сетка/направляющие, привязки, панели, режимы плотности
Ribbon «Скрипты» запуск, редактор, записать действие (макрорекордер → MeridianScript), магазин
Боковая панель вкладки Слои / Цвет / История / Свойства инструмента / Навигатор / Кадры / Каналы (P1) / Кисти (P1); панели отделяемые, запоминаются раскладки (Вид → Раскладка: Рисование / Фото / Скриншоты / Своя)
Строка состояния размер и режим документа, активный слой, координаты курсора, цвет под курсором, выделение (размер), масштаб (ползунок + поле), угол вида, индикатор памяти/истории, предупреждение «профиль не совпадает»
Контекстная мини-панель при выделении/трансформации: кнопки «Применить / Отмена / Растушёвка / Инвертировать» рядом с выделением
Контекстная вкладка ribbon «Текст» (при активном текстовом слое), «Фигура» (векторный), «Трансформация», «Обрезка»

PT-02 Панель «Слои»

PT-03 Панель «Цвет»

PT-04 Панель «История»

PT-05 Панель «Свойства инструмента»

Содержимое зависит от инструмента (см. 2.2). Общие элементы: предустановки инструмента (сохранить/загрузить), «Сбросить инструмент». Для кисти: предпросмотр мазка 200×60 px, кривые нажима (мини-редактор), выбор формы (галерея кистей, импорт), стабилизатор (ползунок + «режим верёвки» P1).

PT-06 Панель «Навигатор»

Миниатюра композита (обновляется с задержкой 100 мс), рамка вида (перетаскиваемая), ползунок масштаба, кнопки «по ширине/по размеру/100 %», поле угла поворота вида.

PT-07 Диалог «Новый»

Поле Значение по умолчанию
Пресет «Как последний» · Буфер обмена · A4 300 dpi · Full HD · 4K · Квадрат 1080 · Значок 256 · Свой
Ширина / Высота 1920 × 1080, единицы px/мм/см/дюйм/%, кнопка «поменять местами»
Разрешение 96 ppi (px) / 300 ppi (мм)
Ориентация книжная/альбомная
Цветовое пространство sRGB IEC61966-2.1 ▾
Глубина 8 бит / 16 бит float
Фон белый / прозрачный / цвет фона / свой
Сводка «1920×1080 px · 8 бит · 7,9 МБ в памяти»

PT-08 Диалог «Размер изображения»

Поля: ширина/высота (px, %, мм при ppi), пропорции (замок), разрешение, «пересчитать пиксели» (resample) вкл/выкл, метод (Автоматически / Ближайший / Билинейный / Бикубический / Lanczos-3 / Box / Mitchell), предпросмотр фрагмента 1:1 с ползунком «до/после», итоговый размер в памяти.

PT-09 Диалог «Размер холста»

Поля: текущий размер, новый размер (абсолютно/относительно), якорь (сетка 3×3 со стрелками), цвет расширения (фон/прозрачный/цвет), «обрезать слои, выходящие за холст» (выкл по умолчанию).

PT-10 Диалог «Поворот холста»

90° по/против часовой, 180°, произвольный (поле угла, −360…360, слайдер), «расширить холст» / «обрезать», метод интерполяции, цвет фона, предпросмотр.

PT-11 Диалог фильтра (общий каркас)

PT-12 Диалог «Экспорт»

PT-13 Снимок экрана — наложение выбора области

PT-14 Контекстная вкладка «Текст» и панель текста

Шрифт (с предпросмотром, недавние), размер, стиль, цвет, обводка (цвет/ширина), выравнивание, межстрочный, межбуквенный, направление, «абзацный/строчный», сглаживание (нет/серое/субпиксельное — последнее выкл. для прозрачных слоёв), «Растрировать». Редактирование — прямо на холсте (каретка, выделение, IME).

PT-15 Рамка трансформации

Маркеры 8 шт. + центр вращения (перетаскиваемый), угол/масштаб/наклон в контекстной вкладке с полями ввода, кнопки «Применить ✓ / Отмена ✗», предпросмотр интерполяции («черновой» на GPU билинейный, финальный — выбранный метод).

PT-16 Панель «Кадры»

Горизонтальная лента миниатюр кадров, длительность под каждым, кнопки «Новый кадр / Дублировать / Удалить / Воспроизвести / Калька / fps», режим «слой = кадр».

PT-17 Редактор градиента

Полоса градиента с точками цвета (снизу) и прозрачности (сверху), поля положения/цвета/прозрачности, тип интерполяции, предустановки, «инвертировать», «дизеринг».

PT-18 Печать

Общий диалог Meridian Print с предпросмотром листа: принтер, формат, ориентация, масштаб (по размеру / 100 % при DPI / произвольный %), положение (центр / координаты), копии, цвет/ч-б.

PT-19 Параметры Paint

Разделы: Общие (единицы, поведение буфера, автосохранение), Интерфейс (цвет «шахматки», раскладка панелей), Производительность (лимит памяти истории, тайлы на GPU, число потоков, папка кэша), Планшет (API: Windows Ink / WinTab; кривая нажима глобальная; тест), Цвет (рабочее пространство по умолчанию, политика профилей, профиль монитора), Снимки (папка, шаблон имени, формат, глобальные клавиши), Курсоры (контур кисти/перекрестие).

PT-20 Галерея кистей

Сетка предпросмотров мазков, группы (Основные, Текстурные, Пиксельные, Пользовательские), импорт .abr/.gbr/.gih, создание кисти из выделения, свойства кисти (форма, интервал, динамика).

PT-21 Свойства слоя / корректирующего слоя

Общие: имя, цветовая метка, режим, непрозрачность, блокировки, обтравочная маска. Для корректирующего — панель параметров коррекции (как в PT-11, но без OK — изменения применяются сразу, неразрушающе).

PT-22 Диалоги выделения

«Растушёвка» (радиус), «Расширить/Сузить» (px), «Граница» (ширина), «Сгладить» (радиус), «Обводка» (ширина, цвет, положение, режим), «Заливка» (цвет/узор/содержимое), «Цветовой диапазон» (пипетки, размытость, предпросмотр маски).

PT-23 Несоответствие профиля / назначение профиля

При открытии файла с профилем, отличным от рабочего: «Использовать встроенный · Преобразовать в рабочее · Игнорировать (назначить рабочее)», флажок «запомнить». Диалог «Преобразовать в профиль»: профиль, намерение, компенсация чёрной точки, предпросмотр.

PT-24 Восстановление после сбоя

Список несохранённых документов с миниатюрами и временем, «Восстановить все / выбранные / удалить».


4. Модель данных

4.1 Иерархия

Image (документ)
 ├── meta: размер холста, DPI, цветовое пространство (ICC), глубина, фон, направляющие, сетка
 ├── layers: дерево LayerNode (корневая группа)
 │    ├── RasterLayer  → TileMap (разреженные тайлы 256×256, премультиплицированный RGBA)
 │    ├── TextLayer    → TextModel (из Write) + кэш растра (TileMap)
 │    ├── VectorLayer  → Scene (из Draw) + кэш растра
 │    ├── AdjustmentLayer → Adjustment (параметры) + LUT-кэш
 │    └── GroupLayer   → children + кэш композита группы
 │    (у каждого: mask: Option<MaskMap>, opacity, blend, flags, offset, bounds)
 ├── selection: Option<MaskMap> (8/16 бит, покрытие 0..1) + bounds + «муравьи» (контур, кэш)
 ├── channels: сохранённые выделения (имя → MaskMap)
 ├── frames: Option<Animation> (кадры → наборы видимости, длительности)
 ├── history: HistoryStack (команды + снимки тайлов)
 └── caches: projection (композит), below/above-кэши активного слоя, GPU-текстуры

4.2 Тайлы и слои (Rust)

// kernel/crates/meridian-raster/src/tile.rs

pub const TILE: usize = 256;
pub const TILE_PIXELS: usize = TILE * TILE;

/// Премультиплицированный 8-бит пиксель в sRGB-кодировке / Premultiplied 8-bit sRGB-encoded pixel
#[repr(C)] #[derive(Clone, Copy, Default, PartialEq)]
pub struct Rgba8 { pub r: u8, pub g: u8, pub b: u8, pub a: u8 }

/// Премультиплицированный 16-бит float (half) линейный пиксель / Premultiplied linear f16 pixel
#[repr(C)] #[derive(Clone, Copy, Default, PartialEq)]
pub struct Rgba16F { pub r: f16, pub g: f16, pub b: f16, pub a: f16 }

/// Данные тайла. Неизменяемы: запись = копирование при записи (Arc::make_mut).
/// Tile payload. Immutable: writes go through copy-on-write (Arc::make_mut).
pub enum TileData {
    U8(Box<[Rgba8; TILE_PIXELS]>),
    F16(Box<[Rgba16F; TILE_PIXELS]>),
}

#[derive(Clone)]
pub struct Tile(pub Arc<TileData>);

/// Координаты тайла в пространстве слоя (могут быть отрицательными — холст бесконечный)
/// Tile coordinates in layer space (may be negative — canvas is unbounded)
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct TileKey { pub tx: i32, pub ty: i32 }

#[derive(Clone, Default)]
pub struct TileMap {
    pub depth: Depth,                       // U8 | F16
    pub tiles: HashMap<TileKey, Tile>,      // отсутствие ключа = прозрачный тайл / missing = transparent
    pub bounds: Option<IRect>,              // ограничивающий прямоугольник непустых пикселей (ленивое уточнение)
}

impl TileMap {
    /// Получить изменяемый тайл: создаёт пустой, если нет; клонирует, если разделяется с историей
    /// Get a mutable tile: creates empty if absent; clones if shared with history
    pub fn tile_mut(&mut self, key: TileKey, undo: &mut UndoRecorder) -> &mut TileData {
        let depth = self.depth;
        let tile = self.tiles.entry(key).or_insert_with(|| Tile::empty(depth));
        undo.remember(key, tile.clone());          // старый Arc сохраняется — нулевое копирование / old Arc kept — zero copy
        Arc::make_mut(&mut tile.0)                 // копирование только если Arc разделён / clones only when shared
    }

    pub fn read(&self, key: TileKey) -> Option<&TileData> { self.tiles.get(key).map(|t| &*t.0) }

    /// Пробежать по тайлам, пересекающим прямоугольник / Iterate tiles intersecting a rect
    pub fn keys_in(&self, r: IRect) -> impl Iterator<Item = TileKey> {
        let (x0, y0) = (r.x0.div_euclid(TILE as i32), r.y0.div_euclid(TILE as i32));
        let (x1, y1) = ((r.x1 - 1).div_euclid(TILE as i32), (r.y1 - 1).div_euclid(TILE as i32));
        (y0..=y1).flat_map(move |ty| (x0..=x1).map(move |tx| TileKey { tx, ty }))
    }
}
// kernel/crates/meridian-raster/src/layer.rs

pub type LayerId = u64;

pub struct LayerNode {
    pub id: LayerId,
    pub name: String,
    pub kind: LayerKind,
    pub visible: bool,
    pub opacity: f32,                 // 0..1
    pub blend: BlendMode,
    pub mask: Option<Mask>,           // маска слоя / layer mask
    pub mask_enabled: bool,
    pub mask_linked: bool,            // перемещается вместе со слоем / moves with the layer
    pub clip_to_below: bool,          // обтравочная маска / clipping mask
    pub locks: Locks,                 // pixels | alpha | position | all
    pub offset: IVec2,                // смещение содержимого относительно холста / content offset vs canvas
    pub color_tag: Option<ColorTag>,
    pub alt_text: Option<String>,
}

pub enum LayerKind {
    Raster(TileMap),
    Text { model: TextModel, transform: Affine, cache: RefCell<Option<TileMap>> },
    Vector { scene: Scene, cache: RefCell<Option<TileMap>> },
    Adjustment { adj: Adjustment, lut: RefCell<Option<Lut>> },
    Group { children: Vec<LayerNode>, pass_through: bool, cache: RefCell<Option<TileMap>> },
}

pub struct Mask { pub map: MaskMap, pub offset: IVec2, pub default: f32 /* значение вне тайлов: 1 = показать */ }

/// Маска: один канал покрытия / Mask: single coverage channel
pub struct MaskMap { pub depth: Depth, pub tiles: HashMap<TileKey, Arc<MaskTile>> }

pub enum Adjustment {
    BrightnessContrast { brightness: f32, contrast: f32, legacy: bool },
    Levels { per_channel: [LevelsChannel; 4] },
    Curves { per_channel: [CurveSpline; 4] },
    ColorBalance { shadows: Vec3, midtones: Vec3, highlights: Vec3, preserve_luminosity: bool },
    HueSaturation { master: Hsl, ranges: [HslRange; 6], colorize: Option<Hsl> },
    BlackWhite { weights: [f32; 6] },
    Invert, Threshold(u8), Posterize(u8),
    GradientMap(Gradient), PhotoFilter { color: Rgb, density: f32 }, Vibrance { vibrance: f32, saturation: f32 },
}
// kernel/crates/meridian-raster/src/image.rs

pub struct Image {
    pub width: u32, pub height: u32,
    pub dpi: f32,
    pub color: ColorSpaceInfo,        // профиль ICC (байты + разобранный), название, намерение / ICC profile, name, intent
    pub depth: Depth,
    pub linear_blend: bool,           // смешивать в линейном пространстве / blend in linear light
    pub root: LayerNode,              // корневая группа / root group
    pub active: Vec<LayerId>,         // выбранные слои / selected layers
    pub selection: Selection,
    pub channels: Vec<(String, MaskMap)>,
    pub guides: Vec<Guide>, pub grid: GridSettings,
    pub frames: Option<Animation>,
    pub history: History,
    pub meta: DocMeta,                // Dublin Core + EXIF/XMP, alt-text
    caches: Caches,
}

pub struct Selection { pub mask: Option<MaskMap>, pub bounds: Option<IRect>, pub outline: RefCell<Option<Outline>> }

pub struct Animation { pub frames: Vec<Frame>, pub fps_cap: u16, pub loops: u16 /* 0 = бесконечно */ }
pub struct Frame { pub name: String, pub duration_ms: u32, pub visible: HashSet<LayerId>, pub group: Option<LayerId> }

Инварианты:

4.3 История

pub struct History { pub entries: Vec<HistoryEntry>, pub cursor: usize, pub snapshots: Vec<Snapshot>, pub mem_limit: usize, pub mem_used: usize }

pub struct HistoryEntry {
    pub label: String, pub icon: IconId,
    pub tiles: Vec<(LayerId, TileKey, Option<Tile>)>,  // старая версия тайла (None = тайла не было) / old tile version
    pub masks: Vec<(LayerId, TileKey, Option<Arc<MaskTile>>)>,
    pub structural: Option<StructuralDiff>,             // слои/порядок/свойства/выделение/холст — обратимые команды
    pub redo_tiles: Vec<(LayerId, TileKey, Option<Tile>)>, // заполняется при undo / filled on undo
    pub spilled: Option<PathBuf>,                        // сброшено на диск / spilled to disk
}

Структурные изменения (переименование, порядок, режим, размер холста, выделение) описываются обратимыми командами (apply/revert), пиксельные — снимками тайлов. Память: при превышении mem_limit самые старые записи сериализуются в recovery/<uuid>/hist-NNN.bin (LZ4), при нехватке диска — удаляются с начала.

4.4 Формат документа .mpi

ZIP-пакет по правилам MOP (раздел 3.1 библии):

mimetype                      application/vnd.meridian.paint (stored, первый элемент)
manifest.xml                  список частей, версия формата
meta.xml                      Dublin Core, alt-text, EXIF/XMP (как есть), история изменений (кратко)
image.xml                     структура документа (ниже)
layers/<id>.png               растровые слои 8 бит (PNG, RGBA непремультиплицированный, с iCCP = рабочий профиль)
layers/<id>.exr               слои 16 бит (OpenEXR half, PIZ/ZIP, линейный, премультиплированный — стандарт EXR)
masks/<id>.png                маски (PNG, 8 бит серый / 16 бит для F16-документов)
text/<id>.xml                 текстовые слои (разметка Write: тот же text-model, `urn:meridian:office:write:1`)
vector/<id>.xml               векторные слои (разметка Draw: `urn:meridian:office:draw:1`)
channels/<name>.png           сохранённые выделения
mergedimage.png               композит (для просмотра другими приложениями, Hub, проводника)
thumbnail.png                 256×256
settings.xml                  вид (масштаб, положение), раскладка панелей, активный слой
history/                      опционально: снимки состояний
scripts/                      опционально: скрипты документа (не запускаются без разрешения)

image.xml (сокращённый пример):

<image xmlns="urn:meridian:office:paint:1" version="1.0"
       width="1920" height="1080" dpi="96" depth="u8" colorspace="sRGB" icc="meta/sRGB.icc" linear-blend="false">
  <layers>
    <layer id="7" type="text" name="Заголовок" visible="true" opacity="1" blend="normal" x="120" y="80"
           src="text/7.xml" rasterized="layers/7.png"/>
    <group id="5" name="Пометки" blend="normal" opacity="0.9" pass-through="false">
      <layer id="6" type="vector" name="Стрелки" src="vector/6.xml" rasterized="layers/6.png"/>
      <layer id="4" type="adjustment" name="Кривые" kind="curves" clip="true">
        <curves channel="rgb" points="0,0 64,48 192,210 255,255" spline="monotone"/>
      </layer>
    </group>
    <layer id="2" type="raster" name="Слой 1" x="-40" y="12" src="layers/2.png" lock="alpha">
      <mask src="masks/2.png" enabled="true" linked="true" default="1"/>
    </layer>
    <layer id="1" type="raster" name="Фон" src="layers/1.png" lock="all"/>
  </layers>
  <selection src="channels/_selection.png" x="300" y="200"/>
  <guides><h y="540"/><v x="960"/></guides>
  <grid step="32" sub="4" visible="false" snap="true"/>
  <frames fps-cap="30" loops="0">
    <frame name="1" duration="100" layers="1 2"/><frame name="2" duration="100" layers="1 6"/>
  </frames>
</image>

Правила: rasterized у текстовых/векторных слоёв — растровая копия для чтения старыми версиями и для mergedimage.png; неизвестные элементы и атрибуты сохраняются при пересохранении; слои хранятся обрезанными по bounds с координатами x/y (экономия); PNG пишется с iCCP; EXR — с chromaticities рабочего пространства.

4.5 Кэши композитинга

Кэш Содержимое Инвалидация
projection композит всего документа, TileMap dirty-тайлы при любом изменении
below[active] композит всех слоёв ниже активного (с учётом групп) смена активного слоя; изменение слоёв ниже
above[active] композит слоёв выше активного (премультиплированный «поверх») аналогично
group.cache композит группы (если не pass-through) изменение детей
text/vector.cache растр текстового/векторного слоя изменение модели/трансформации
adjustment.lut таблица 256/4096 записей изменение параметров
gpu.textures текстуры тайлов для видимых тайлов projection (атлас 4096×4096, LRU) dirty-тайлы
selection.outline полилинии «муравьёв» в координатах холста изменение выделения/вида

Рисование на активном слое: result(tile) = above ⊕ (layer ⊕_blend below) только для dirty-тайлов мазка — стоимость не зависит от числа слоёв (ключ к критерию «20 слоёв < 16 мс»).


5. Алгоритмы

5.1 Композитинг слоёв с режимами наложения и масками

Обозначения: Cs, αs — цвет и альфа источника (слой, непремультиплицированный), Cb, αb — фон (backdrop), B(Cb, Cs) — функция смешивания режима, m — значение маски (0..1), o — непрозрачность слоя. Эффективная альфа источника: αs' = αs · m · o.

Общая формула (W3C Compositing and Blending Level 1, совместима с PDF/Photoshop):

Cs'  = (1 − αb) · Cs + αb · B(Cb, Cs)          // цвет источника с учётом смешивания с фоном
Co   = αs' · Cs' + αb · (1 − αs') · Cb           // премультиплированный результат (source-over)
αo   = αs' + αb · (1 − αs')

Для прочих операторов Porter–Duff (Fa, Fb — коэффициенты):

Режим Fa Fb Режим Fa Fb
Clear 0 0 Source-out 1−αb 0
Source 1 0 Destination-out 0 1−αs
Destination 0 1 Source-atop αb 1−αs
Source-over 1 1−αs Destination-atop 1−αb αs
Destination-over 1−αb 1 XOR 1−αb 1−αs
Source-in αb 0 Plus (Lighter) 1 1
Destination-in 0 αs

co = Fa · αs · Cs' + Fb · αb · Cb, αo = Fa · αs + Fb · αb (премультиплированный выход; для Plus — clamp).

Разделённые режимы B(Cb, Cs) (по каналам, значения 0..1):

Режим B(Cb, Cs)
Normal Cs
Multiply Cb · Cs
Screen Cb + Cs − Cb · Cs
Overlay HardLight(Cs, Cb)
Darken / Lighten min(Cb, Cs) / max(Cb, Cs)
Color Dodge Cb = 0 → 0; Cs = 1 → 1; иначе min(1, Cb / (1 − Cs))
Color Burn Cb = 1 → 1; Cs = 0 → 0; иначе 1 − min(1, (1 − Cb) / Cs)
Hard Light Cs ≤ ½ → Multiply(Cb, 2Cs); иначе Screen(Cb, 2Cs − 1)
Soft Light Cs ≤ ½ → Cb − (1 − 2Cs) · Cb · (1 − Cb); иначе Cb + (2Cs − 1) · (D(Cb) − Cb), где D(x) = x ≤ ¼ ? ((16x − 12)x + 4)x : √x
Difference |Cb − Cs|
Exclusion Cb + Cs − 2 · Cb · Cs
Linear Dodge (Add) min(1, Cb + Cs)
Linear Burn max(0, Cb + Cs − 1)
Vivid Light Cs ≤ ½ → ColorBurn(Cb, 2Cs); иначе ColorDodge(Cb, 2Cs − 1)
Linear Light Cs ≤ ½ → LinearBurn(Cb, 2Cs); иначе LinearDodge(Cb, 2Cs − 1)
Pin Light Cs ≤ ½ → min(Cb, 2Cs); иначе max(Cb, 2Cs − 1)
Hard Mix VividLight(Cb, Cs) < ½ → 0; иначе 1
Subtract max(0, Cb − Cs)
Divide Cs = 0 → 1; иначе min(1, Cb / Cs)
Darker Color / Lighter Color по сумме каналов: Lum(Cs) < Lum(Cb) ? Cs : Cb (и наоборот) — неразделённый по сути, но без изменения оттенка
Dissolve Cs с вероятностью αs' (хэш координат), иначе Cb; альфа 0/1

Неразделённые (по всем каналам сразу):

Lum(C)      = 0.3·R + 0.59·G + 0.11·B
ClipColor(C): l = Lum(C); n = min(R,G,B); x = max(R,G,B)
              если n < 0: C = l + (C − l)·l / (l − n)
              если x > 1: C = l + (C − l)·(1 − l) / (x − l)
SetLum(C, l) = ClipColor(C + (l − Lum(C)))
Sat(C)      = max(R,G,B) − min(R,G,B)
SetSat(C, s): для упорядоченных Cmax ≥ Cmid ≥ Cmin:
              если Cmax > Cmin: Cmid = (Cmid − Cmin)·s / (Cmax − Cmin); Cmax = s; иначе Cmid = Cmax = 0; Cmin = 0

Hue:        B = SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb))
Saturation: B = SetLum(SetSat(Cb, Sat(Cs)), Lum(Cb))
Color:      B = SetLum(Cs, Lum(Cb))
Luminosity: B = SetLum(Cb, Lum(Cs))

Группы: изолированная группа компонуется в прозрачный буфер (backdrop = 0), затем результат накладывается на фон с режимом/непрозрачностью/маской группы. Pass-through группа: дети компонуются прямо на текущий фон; непрозрачность группы в этом режиме = интерполяция между фоном и результатом (lerp(backdrop, result, o)). Обтравочная маска: αs' = αs · m · o · αbelow, где αbelow — альфа базового слоя. Корректирующий слой: B = f_adj(Cb) (LUT или функция), Cs' = B, αs' = m · o — то есть коррекция смешивается с фоном по маске и непрозрачности.

Псевдокод композитинга одного тайла:

/// Композитинг стека слоёв в один тайл / Composite a layer stack into one tile
fn composite_tile(stack: &[LayerRef], key: TileKey, out: &mut TileF32, clip: Option<&TileF32>) {
    out.clear();                                                   // backdrop = прозрачный / transparent
    for layer in stack.iter().filter(|l| l.visible && l.opacity > 0.0) {
        let src: Cow<TileF32> = match &layer.kind {
            Raster(map)        => map.read_f32(key, layer.offset),         // перевод в f32, учёт смещения
            Text{..}|Vector{..} => layer.rasterized(key),                   // из кэша / from cache
            Adjustment{adj,..} => adj.apply_tile(out),                      // f(backdrop)
            Group{children, pass_through: false, ..} => group_cache_or(|| composite_tile(children, key, &mut tmp, None)),
            Group{children, pass_through: true, ..}  => { composite_pass_through(children, key, out, layer); continue; }
        };
        let mask = layer.mask_tile(key);                                     // Option<&MaskTileF32>
        let base_alpha = layer.clip_to_below.then(|| clip_alpha_below(stack, layer, key));
        blend_into(out, &src, layer.blend, layer.opacity, mask, base_alpha.as_ref());
    }
}

#[inline]
fn blend_pixel(cb: Rgba, cs: Rgba, mode: BlendMode, a_mul: f32) -> Rgba {
    let a_s = cs.a * a_mul;                     // αs' = αs·m·o(·αbelow)
    if a_s <= 0.0 { return cb; }
    let (cs_u, cb_u) = (cs.unpremul(), cb.unpremul());
    let b = if mode.is_separable() { mode.separable(cb_u.rgb, cs_u.rgb) } else { mode.non_separable(cb_u.rgb, cs_u.rgb) };
    let cs_p = (1.0 - cb.a) * cs_u.rgb + cb.a * b;        // Cs'
    let co   = a_s * cs_p + cb.a * (1.0 - a_s) * cb_u.rgb; // премультиплированный / premultiplied
    let ao   = a_s + cb.a * (1.0 - a_s);
    Rgba { r: co.x, g: co.y, b: co.z, a: ao }
}

Реализация: внутренняя точность f32 (SIMD через std::simd/wide, 8 пикселей за раз), для 8-бит документов Normal/Multiply/Screen — специализированные целочисленные пути ((a·b + 127) / 255 через (t + (t >> 8)) >> 8). Тайлы распараллелены через rayon. На GPU (Skia): Porter–Duff и 16 разделённых/неразделённых режимов — SkBlendMode; остальные (Linear Burn, Vivid/Linear/Pin Light, Hard Mix, Subtract, Divide, Darker/Lighter Color, Dissolve) — SkRuntimeEffect (SkSL) с теми же формулами; результат CPU и GPU сверяется снимочными тестами (допуск 1/255).

5.2 Кисть на основе дабов

Входные события (InputSample { pos: Vec2, pressure: f32 (0..1), tilt: Vec2, rotation: f32, t: f64 }) приходят из Qt (QTabletEvent/QPointerEvent) с частотой до 240 Гц в выделенный поток ввода; мышь — нажим 1.0.

Конвейер мазка:

  1. Стабилизатор (one-euro фильтр, Casiez et al. 2012) по pos отдельно по x и y; параметры из ползунка «стабилизатор» s∈[0,1]: min_cutoff = lerp(1.5, 0.05, s) Гц, beta = lerp(0.1, 0.001, s), d_cutoff = 1.
pub struct OneEuro { min_cutoff: f32, beta: f32, d_cutoff: f32, x_prev: Option<f32>, dx_prev: f32, t_prev: f64 }
impl OneEuro {
    fn alpha(cutoff: f32, dt: f32) -> f32 { let tau = 1.0 / (2.0 * PI * cutoff); 1.0 / (1.0 + tau / dt) }
    pub fn filter(&mut self, x: f32, t: f64) -> f32 {
        let Some(xp) = self.x_prev else { self.x_prev = Some(x); self.t_prev = t; return x; };
        let dt = ((t - self.t_prev) as f32).max(1e-4);
        let dx = (x - xp) / dt;                                         // скорость / velocity
        let edx = lerp(self.dx_prev, dx, Self::alpha(self.d_cutoff, dt));
        let cutoff = self.min_cutoff + self.beta * edx.abs();           // быстрее движение — меньше запаздывание
        let xh = lerp(xp, x, Self::alpha(cutoff, dt));
        self.x_prev = Some(xh); self.dx_prev = edx; self.t_prev = t; xh
    }
}

Режим «верёвка» (P1): даб ставится, только когда курсор отошёл от последней точки на радиус r_rope.

  1. Сглаживание пути: сглаженные точки P₀…Pₙ соединяются сплайном Catmull-Rom (centripetal, α = 0,5) по четвёркам (Pᵢ₋₁, Pᵢ, Pᵢ₊₁, Pᵢ₊₂); сегмент Pᵢ→Pᵢ₊₁ дискретизируется по длине дуги (накопленная длина L сохраняется между сегментами, чтобы интервал не «сбивался» на стыках). Нажим/наклон интерполируются линейно вдоль сегмента.

  2. Размещение дабов: интервал spacing = max(0.01, s) · diameter(pressure); даб ставится при L ≥ next, next += spacing. Параметры даба по кривым: d = d_max · curve_size(p), opacity = o · curve_opacity(p), flow = f · curve_flow(p), угол = фиксированный / по направлению / по наклону; разброс (scatter) — смещение по нормали на U(−k, k)·d; джиттер размера/угла.

  3. Растеризация даба в буфер мазка (отдельный TileMap на время мазка, та же глубина): для круглой кисти с жёсткостью h:

r   = d / 2;  dist = |p − c| (с учётом эллиптичности и угла: p' = R(−θ)·(p − c) / (1, roundness))
cov = dist ≤ r·h ? 1 : smoothstep(r, r·h, dist)       // мягкая граница / soft edge
cov *= aa(dist, r)                                     // субпиксельное покрытие по площади: clamp(r − dist + 0.5, 0, 1)
value = cov · flow

Для кистей-изображений — сэмплирование маски с билинейной интерполяцией, субпиксельное смещение центра учитывается (нет «дрожания» при медленных мазках). Текстура кисти (P1) умножает value на выборку из текстуры (в координатах холста).

  1. Накопление в буфере мазка: два режима — «обычный» (buf = max(buf, value) по альфе: повторные дабы не накапливаются сверх opacity, как кисть Photoshop) и «аэрограф/накопление» (buf = buf + value·(1 − buf)). Цвет дабов в буфере — текущий цвет (премультиплированный на альфу покрытия).

  2. Наложение на слой: layer = blend(layer, buf · opacity, mode) только для dirty-тайлов, с учётом выделения (маска выделения умножает альфу) и блокировки альфы (lock alpha: результат альфы = альфа слоя). На экране до завершения мазка показывается above ⊕ (buf ⊕ layer) ⊕ below — слой не трогается; при отпускании — одно применение, одна запись истории.

Ластик = кисть с режимом Destination-out; штамп = кисть, у которой цвет даба берётся из источника (смещение Δ = dst − src, образец со слоя или композита, билинейная выборка при дробном смещении); осветление/затемнение/размытие/резкость/губка = кисть, где value управляет силой локального фильтра, применяемого к пикселям под дабом (выборка из «снимка» слоя до мазка, чтобы фильтр не накапливался многократно за один проход).

Производительность: даб растеризуется только в пересекаемые тайлы (обычно 1–4), буфер мазка и слой — в rayon-задачах; поток ввода не блокируется рендером (очередь событий с таймстемпами; при отставании рендер обрабатывает пачку, не теряя дабы). Цель: 10 000 дабов/с при диаметре 50 px без просадок.

5.3 Заливка (сканлайн flood fill)

/// Заливка с допуском, 4/8-связность, результат — маска покрытия (0/255), затем сглаживание края
/// Scanline flood fill with tolerance; returns coverage mask, then optional edge anti-aliasing
pub fn flood_fill(src: &dyn PixelSource, seed: IVec2, tol: Tolerance, conn8: bool, limit: IRect) -> MaskMap {
    let seed_color = src.get(seed);
    let similar = |p: IVec2| tol.matches(seed_color, src.get(p));   // RGB max-abs / евклид / ΔE в LAB
    let mut mask = MaskMap::new(Depth::U8);
    let mut stack: Vec<(i32, i32, i32, i32)> = vec![(seed.x, seed.x, seed.y, 1), (seed.x, seed.x, seed.y - 1, -1)];
    while let Some((x1, x2, y, dy)) = stack.pop() {
        if y < limit.y0 || y >= limit.y1 { continue; }
        let mut x = x1;
        // расширение влево от x1 / extend left of x1
        if !mask.get(x, y) && similar((x, y).into()) {
            while x > limit.x0 && !mask.get(x - 1, y) && similar((x - 1, y).into()) { x -= 1; }
            if x < x1 { stack.push((x, x1 - 1, y - dy, -dy)); }        // "перелив" назад / U-turn
        }
        let mut start = x;
        while start <= x2 {
            // заполнить сегмент вправо / fill run to the right
            while start < limit.x1 && !mask.get(start, y) && similar((start, y).into()) { mask.set(start, y); start += 1; }
            let run_end = start - 1;
            if run_end >= x { stack.push((x, run_end, y + dy, dy)); }
            if start - 1 > x2 { stack.push((x2 + 1, start - 1, y - dy, -dy)); }
            start += 1;
            while start <= x2 && !similar((start, y).into()) { start += 1; }
            x = start;
            if conn8 && run_end >= x1 - 1 && run_end < x2 + 1 {        // 8-связность: диагональные соседи
                stack.push((run_end.max(x1 - 1), (run_end + 1).min(x2 + 1), y + dy, dy));
            }
        }
    }
    mask
}

5.4 Быстрое выделение (GrabCut-подобное, P2)

  1. Пользователь проводит мазок «объект» (и при необходимости «фон»); начальная тримап: мазок — точно объект, прямоугольник вокруг с полями 20 % — «вероятно фон», остальное — точно фон.
  2. Работа на уменьшенной копии (≤ 1 Мпикс; масштаб s), в цветовом пространстве LAB.
  3. GMM по 5 компонент для объекта и фона (k-means инициализация, 2 итерации EM на каждом шаге).
  4. Граф: узлы — пиксели, рёбра к соседям (8-связность) с весом w = γ · exp(−β · ‖cᵢ − cⱼ‖²) / dist, β = 1 / (2·⟨‖cᵢ − cⱼ‖²⟩), γ = 50; t-рёбра — −log P(c | GMM_fg/bg), точные метки — бесконечные.
  5. Min-cut (Boykov–Kolmogorov, crate maxflow/своя реализация), 3–5 итераций «GMM → cut».
  6. Уточнение границы на полном разрешении: guided filter (радиус 8, ε = 1e-3) по маске в полосе ±4·s px от границы → мягкая маска; «+/−» мазки добавляют/убирают метки и пересчитывают только затронутую область.

5.5 Трансформации с ресамплингом

Обратное отображение: для каждого пикселя назначения q вычисляем p = M⁻¹·q (аффинная 3×2 или гомография 3×3 с делением на w), сэмплируем источник ядром K:

Метод Ядро Применение
Ближайший K = 1 при |x| < ½ пиксельная графика, увеличение с чёткими пикселями
Билинейный K(x) = 1 − |x| на [−1, 1] черновой предпросмотр, GPU
Бикубический (Keys, a = −0,5) K(x) = (a+2)|x|³ − (a+3)|x|² + 1 на [0,1); a|x|³ − 5a|x|² + 8a|x| − 4a на [1,2) по умолчанию для фотографий
Lanczos-3 K(x) = sinc(x)·sinc(x/3) на [−3, 3] максимальная резкость, увеличение
Mitchell–Netravali (B=C=⅓) кубический B-сплайн мягкое уменьшение
Box (area) среднее по площади уменьшение, превью

При уменьшении (масштаб < 1) ядро расширяется в 1/scale раз (предфильтрация, эквивалент area-averaging для Box); для сильных уменьшений — сначала мип-уровни (box ×2 до ближайшей степени двойки), затем выбранное ядро. Сэмплирование — премультиплированных значений (иначе ореолы у прозрачных краёв); результат клампится, для Lanczos/бикубического — опционально «без звона» (clamp к min/max соседей 2×2).

/// Ресамплинг слоя обратным отображением / Resample a layer by inverse mapping
pub fn transform(src: &TileMap, m: &Mat3, kernel: Kernel, dst_rect: IRect, out: &mut TileMap) {
    let inv = m.inverse();
    let (sx, sy) = m.scale_factors();                         // для предфильтра / for prefilter width
    let (kw, kh) = (kernel.radius() / sx.min(1.0), kernel.radius() / sy.min(1.0));
    out.keys_in(dst_rect).par_bridge().for_each(|key| {
        let tile = out.tile_mut_parallel(key);
        for (py, px) in tile.pixels() {
            let q = Vec2::new(px as f32 + 0.5, py as f32 + 0.5);
            let p = inv.apply_point(q);                          // p = M⁻¹·q (с делением на w) / with w-division
            tile.set(px, py, sample(src, p, kernel, kw, kh));    // Σ K(dx)K(dy)·src / Σ K·K
        }
    });
}

Предпросмотр во время перетаскивания — на GPU (Skia drawImage с матрицей, билинейно + мипмапы); финальный ресамплинг — CPU по выбранному ядру (или GPU-шейдер с тем же ядром, P1), одна запись истории.

5.6 Разделяемое размытие по Гауссу и box-blur ×3

Ядро 1D: g(i) = exp(−i² / 2σ²) / Σ, радиус r = ⌈3σ⌉; два прохода (горизонталь, вертикаль) по тайлам с апроном r (читаем соседние тайлы), промежуточный буфер f32. Края: «продление» (clamp) для холста, прозрачность — для слоя (размытие премультиплированных значений, чтобы прозрачные области не темнили).

Для σ ≥ 2.5 (r ≥ 8) — три последовательных box-blur (скользящая сумма, O(1) на пиксель независимо от σ), ширины по Kovesi (2010):

/// Ширины трёх боксов, приближающих гаусс с σ / Box widths approximating a Gaussian of sigma
fn boxes_for_gauss(sigma: f32, n: usize) -> Vec<usize> {
    let w_ideal = (12.0 * sigma * sigma / n as f32 + 1.0).sqrt();
    let mut wl = w_ideal.floor() as usize; if wl % 2 == 0 { wl -= 1; }
    let wu = wl + 2;
    let m_ideal = (12.0 * sigma * sigma - (n * wl * wl + 4 * n * wl + 3 * n) as f32) / (-4.0 * wl as f32 - 4.0);
    let m = m_ideal.round() as usize;
    (0..n).map(|i| if i < m { wl } else { wu }).collect()
}

/// Один проход бокса по строке скользящей суммой / One box pass along a row via running sum
fn box_row(src: &[f32], dst: &mut [f32], r: usize) {
    let n = src.len(); let inv = 1.0 / (2 * r + 1) as f32;
    let mut acc: f32 = src[0] * (r as f32 + 1.0) + src[1..=r.min(n - 1)].iter().sum::<f32>(); // clamp-край
    for i in 0..n {
        let add = src[(i + r + 1).min(n - 1)]; let sub = src[i.saturating_sub(r + 1).max(0)];
        dst[i] = acc * inv; acc += add - sub;
    }
}

Погрешность приближения относительно истинного гаусса < 1 % (3 бокса), визуально неотличимо. Движение (motion blur): усреднение вдоль направления — та же скользящая сумма по повёрнутой решётке (с билинейной выборкой). Радиальное (зум/вращение): N выборок (адаптивно 8–64 по радиусу) вдоль траектории от центра. На GPU (предпросмотр): Skia ImageFilters::blur (сам реализует разделяемый гаусс).

5.7 Нерезкая маска и резкость

blurred = gauss(src, radius)                      // radius — σ в px
diff    = src − blurred
out     = |diff| > threshold/255 ? src + amount · diff : src     // amount 0..5 (0–500 %)

Порог применяется к яркости (Lum), чтобы не усиливать шум в гладких областях; клампинг по каналам; для 16-бит — без клампа выше 1 (сохраняем HDR). «Резкость» = нерезкая маска с radius = 1, amount = 0,5. High pass: out = 0.5 + diff (серый с деталями) — для наложения в режиме Overlay.

5.8 Медианный фильтр (удаление шума, «пыль и царапины»)

Алгоритм Перро–Эбера (Perreault & Hébert, 2007) — O(1) на пиксель: для каждого столбца ведётся гистограмма окна высотой 2r+1; гистограмма ядра = сумма 2r+1 столбцовых гистограмм, обновляемая при сдвиге вправо (добавить столбец справа, убрать слева). Для 8 бит — двухуровневая гистограмма (16 «грубых» + 256 «тонких» корзин), медиана — поиск по грубому уровню, затем по тонкому. 16-бит float — квантование в 12 бит для гистограммы, затем точное значение из окна (select_nth_unstable по 2r+1 ≤ 31 — быстро) — компромисс точности/скорости. Каналы обрабатываются независимо (RGB) или по яркости с переносом цветности (опция «сохранять цвет»). «Пыль и царапины»: out = |src − median| > threshold ? median : src.

5.9 Уровни, кривые, автоуровни через LUT

/// Построение LUT уровней / Build a levels LUT
fn levels_lut(ch: &LevelsChannel) -> Lut {             // in_lo, in_hi, gamma, out_lo, out_hi в 0..1
    Lut::from_fn(|x| {
        let t = ((x - ch.in_lo) / (ch.in_hi - ch.in_lo).max(1e-6)).clamp(0.0, 1.0);
        let g = t.powf(1.0 / ch.gamma);
        ch.out_lo + g * (ch.out_hi - ch.out_lo)
    })
}

5.10 Преобразования цветовых пространств

sRGB → линейный:  c ≤ 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055)^2.4
линейный → sRGB:  c ≤ 0.0031308 ? 12.92·c : 1.055·c^(1/2.4) − 0.055
линейный sRGB (D65) → XYZ:  [0.4124 0.3576 0.1805; 0.2126 0.7152 0.0722; 0.0193 0.1192 0.9505] · [R G B]ᵀ
XYZ → LAB (D65: Xn=0.9505, Yn=1.0, Zn=1.089):
   f(t) = t > (6/29)³ ? t^(1/3) : t / (3·(6/29)²) + 4/29
   L = 116·f(Y/Yn) − 16;  a = 500·(f(X/Xn) − f(Y/Yn));  b = 200·(f(Y/Yn) − f(Z/Zn))
ΔE76 = √(ΔL² + Δa² + Δb²);  ΔE2000 — по Sharma et al. (для «точного» допуска)
OKLab/OKLCH (Björn Ottosson) — для круга цвета и интерполяции градиентов (равномернее LAB по оттенку)

5.11 Квантование и дизеринг при экспорте в GIF / PNG-8 / ICO

  1. Квантование — октодерево (Gervautz–Purgathofer): вставка пикселей в дерево глубиной 8 по битам RGB, при превышении max_colors (≤ 255 при прозрачности) слияние самых «дешёвых» узлов (минимальная сумма пикселей на уровне с наибольшей глубиной); альтернатива — median cut (опция, лучше для градиентов); для анимации — глобальная палитра по всем кадрам (выборка ≤ 2 Мпикс), локальная — если кадр отклоняется (ΔE_mean > 4).
  2. Дизеринг — Флойд–Стайнберг с серпантинным обходом, распределение ошибки 7/16 → (x+1,y), 3/16 → (x−1,y+1), 5/16 → (x,y+1), 1/16 → (x+1,y+1); ошибка в линейном пространстве (меньше артефактов); сила 0–100 %; альтернативы: упорядоченный (Bayer 8×8) для пиксель-арта, без дизеринга.
  3. Поиск ближайшего цвета палитры — k-d дерево по RGB (или LAB при «точном» режиме), кэш последних 64.
  4. Прозрачность: порог альфы (по умолчанию 128) → индекс прозрачного цвета; для WebP/APNG — полная альфа.
  5. Анимация: оптимизация кадров — ограничивающий прямоугольник изменений относительно предыдущего (disposal «не удалять»), неизменённые пиксели внутри прямоугольника → прозрачный индекс (если delta экономит > 10 %); длительность в сотых секунды (GIF) — округление с накоплением ошибки.
fn floyd_steinberg(img: &mut [Vec3], w: usize, h: usize, pal: &Palette, strength: f32) -> Vec<u8> {
    let mut out = vec![0u8; w * h];
    for y in 0..h {
        let ltr = y % 2 == 0;                                                 // серпантин / serpentine
        let xs: Box<dyn Iterator<Item = usize>> = if ltr { Box::new(0..w) } else { Box::new((0..w).rev()) };
        for x in xs {
            let old = img[y * w + x];
            let idx = pal.nearest(old); let new = pal.color(idx); out[y * w + x] = idx as u8;
            let err = (old - new) * strength;
            let dx: isize = if ltr { 1 } else { -1 };
            diffuse(img, w, h, x as isize + dx, y as isize, err * (7.0 / 16.0));
            diffuse(img, w, h, x as isize - dx, y as isize + 1, err * (3.0 / 16.0));
            diffuse(img, w, h, x as isize,      y as isize + 1, err * (5.0 / 16.0));
            diffuse(img, w, h, x as isize + dx, y as isize + 1, err * (1.0 / 16.0));
        }
    }
    out
}

5.12 Undo через diff тайлов

5.13 Рендер на GPU через Skia

5.14 Вид: масштаб и поворот холста

Матрица вида V = T(center_screen) · S(zoom · dpr) · R(θ) · T(−center_canvas); все координаты событий переводятся V⁻¹ в пространство холста (дробные — кисть рисует с субпиксельной точностью). Зум к курсору: center_canvas фиксируется под курсором (c' = c + (p_canvas − c)·(1 − z_old/z_new)). Ступени зума: 1, 2, 3, 4, 5, 6.25, 8.33, 12.5, 16.7, 25, 33.3, 50, 66.7, 100, 150, 200, 300, 400, 600, 800, 1200, 1600, 2400, 3200, 4800, 6400 %; колесо с Ctrl — плавно (×1.1 на тик). Поворот — любой угол, привязка к 15° при Shift, индикатор угла в строке состояния; линейки при повороте скрываются (как в Krita). Отражение вида — S(−1, 1). Пиксельная сетка/направляющие рисуются в экранных координатах после преобразования точек (1 px линии).

5.15 Чтение PSD и ORA, запись PSD (базово)

PSD (Adobe Photoshop File Format Specification; PSB — те же структуры с 64-бит длинами, P1):

Секция Что берём
File Header версия (1 — PSD, 2 — PSB), каналы, высота/ширина, глубина (1/8/16/32), цветовой режим (Bitmap, Grayscale, Indexed, RGB, CMYK, Multichannel, Duotone, Lab)
Color Mode Data палитра для Indexed, Duotone — как Grayscale
Image Resources 1005 разрешение (DPI), 1039 ICC-профиль, 1032 сетка и направляющие, 1036 миниатюра, 1058 EXIF, 1060 XMP, 1026 группы слоёв (старые), 1045 unicode alpha names, 1057 version info
Layer and Mask Info записи слоёв: прямоугольник, каналы (−1 альфа, −2 маска, −3 realmask), ключ режима (norm, diss, dark, mul , idiv, lbrn, dkCl, lite, scrn, div , lddg, lgCl, over, sLit, hLit, vLit, lLit, pLit, hMix, diff, smud, fsub, fdiv, hue , sat , colr, lum ), непрозрачность, клиппинг, флаги (видимость, прозрачность защищена), маска (прямоугольник, значение по умолчанию, флаги, плотность), blending ranges (игнорируем, предупреждение), имя (Pascal) + luni (Unicode), доп. записи: lsct (разделитель группы: open/closed/bounding), lyid, clbl, knko, lspf, lclr (цветовая метка), shmd, fxrp, TySh (текст: читаем текст и шрифт для информации, слой — растр; редактируемый текст из PSD — P2), SoCo/GdFl/PtFl (слои-заливки: как растр из композита), lfx2/lrFX (эффекты: не поддерживаются — слой импортируется без эффектов, предупреждение; опция «импортировать с эффектами как растр» — нет данных растра эффектов, поэтому предлагается открыть «только композит»), iOpa (fill opacity → P2), vmsk (векторная маска → растрируем), lmgr…
Image Data композит (сжатие 0 raw, 1 RLE/PackBits, 2 ZIP, 3 ZIP с предсказанием) — для «открыть как композит» и для миниатюры

Чтение каналов: PackBits построчно (таблица длин строк), ZIP через flate2; 16-бит — big-endian u16 → f16 линейный (PSD 16-бит хранит гамма-кодированные значения → линеаризуем через профиль), 32-бит — float линейный; CMYK/Lab → RGB через lcms2 (встроенный профиль или стандартный «U.S. Web Coated SWOP»/Lab D50); Indexed/Bitmap/Duotone — разворачиваем в RGB. Слои с «clipping» → clip_to_below; lsct — восстанавливаем дерево групп (стек: «закрытая/открытая папка» начинает группу, «bounding divider» завершает). Режимы без аналога (smud — Dissolve есть; fsub/fdiv — Subtract/Divide есть) — все 27 отображаются.

Запись PSD (базово): RGB 8/16 бит, растровые слои (текст/вектор/коррекции — растрируются, с сохранением имени), группы (lsct), маски, режимы, непрозрачность, клиппинг, luni-имена, lyid, ICC (1039), DPI (1005), композит в Image Data (чтобы файл открывался везде), сжатие RLE. Файл > 2 ГБ или сторона > 30 000 — PSB (P1).

ORA (OpenRaster, спецификация freedesktop):

mimetype             image/openraster
stack.xml            <image w h xres yres> <stack> <layer src opacity visibility composite-op x y name/> …
data/*.png           слои (PNG)
mergedimage.png      композит
Thumbnails/thumbnail.png

composite-op: svg:src-over, svg:multiply, svg:screen, svg:overlay, svg:darken, svg:lighten, svg:color-dodge, svg:color-burn, svg:hard-light, svg:soft-light, svg:difference, svg:color, svg:luminosity, svg:hue, svg:saturation, svg:plus, svg:dst-in, svg:dst-out, svg:src-atop, svg:dst-atop ↔︎ наши режимы один-в-один; вложенные <stack> — группы (isolation="isolate|auto" → pass-through), opacity, visibility, x/y; расширения .mpi при записи ORA теряются (маски — применяются или в data/ отдельным PNG с атрибутом mask — не стандарт; пишем применёнными), предупреждение при экспорте.

XCF (GIMP, чтение): версии до 12, слои, группы, маски, режимы (legacy и default), свойства, 8/16/32 бит, precision; аналогично PSD — таблица соответствия режимов, непереносимое — предупреждение.

5.16 Снимки экрана — платформенные API

ОС Основной API Резерв Особенности
Windows 10 1903+ / 11 Windows.Graphics.Capture (WinRT): GraphicsCaptureItem для монитора/окна, Direct3D11CaptureFramePool, один кадр → ID3D11Texture2D → staging → CPU DXGI Output Duplication (монитор), PrintWindow/BitBlt (окно, старые версии) HDR-мониторы: кадр в R16G16B16A16_FLOAT → tone-map в sRGB (или сохранить 16 бит); курсор — IsCursorCaptureEnabled; жёлтая рамка захвата отключается (IsBorderRequired = false, 2022+); масштаб DPI per-monitor через GetDpiForMonitor; список окон — EnumWindows + DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS) + DWMWA_CLOAKED для фильтра
macOS 13+ ScreenCaptureKit: SCShareableContent (окна/дисплеи), SCScreenshotManager.captureImage (14+) или SCStream на один кадр (13) CGWindowListCreateImage (устарел в 14, работает до удаления), CGDisplayCreateImage разрешение «Запись экрана» (TCC) — запрос при первом снимке с объяснением; Retina — CGImage в точках × scale; тень окна — опция (SCContentFilter + shadow); цвет — профиль дисплея (прикрепляем ICC, конвертируем в sRGB по умолчанию)
Linux Wayland org.freedesktop.portal.Screenshot (Screenshot с interactive, PickColor), окно/область — org.freedesktop.portal.ScreenCast (PipeWire, один кадр) с системным выбором источника — без портала (старые композиторы) — wlr-screencopy (wlroots) напрямую; наше наложение выбора области — только после получения кадра всего экрана (композитор не даёт рисовать поверх произвольно)
Linux X11 XGetImage/XShmGetImage на корневом окне, XFixesGetCursorImage для курсора, _NET_CLIENT_LIST + XGetWindowAttributes для окон портал (если есть) составные менеджеры (picom) — читаем с корневого окна после compose; мультимонитор — XRandR

Общий сценарий: capture(target: Area|Window|Screen, delay) -> Frame { pixels, scale, color_profile, cursor? }; наложение выбора области — отдельное окно Qt Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint на каждом экране с предварительно снятым кадром (так область выбирается по «замороженному» экрану, как в Snipping Tool), на Wayland — только через портал (кадр экрана → наше окно с кадром). Глобальная клавиша: Windows — RegisterHotKey (в Hub-брокере), macOS — CGEventTap/Carbon RegisterEventHotKey (нужны права «Универсальный доступ» только для перехвата — используем Carbon без них), Linux — org.freedesktop.portal.GlobalShortcuts (Wayland), XGrabKey (X11). «Снимок» живёт в Hub-брокере (всегда запущен в трее по желанию) и открывает лёгкое окно мини-редактора; полный Paint — по кнопке.

5.17 Градиенты, текст и фигуры

5.18 Прочие фильтры (сводно)

Фильтр Алгоритм
Масляная краска для каждого пикселя: гистограмма интенсивности (уровни 20) в окне r, цвет = среднее пикселей самого частого уровня; ускорение — интегральные гистограммы по тайлам
Тиснение свёртка ядром направления (например [−2 −1 0; −1 1 1; 0 1 2] при 135°) по яркости + смещение 128; «высота» масштабирует ядро
Края (Собель) Gx = [−1 0 1; −2 0 2; −1 0 1], Gy = Gxᵀ, `
Пикселизация среднее по ячейкам n×n (интегральное изображение), выравнивание ячеек по сетке холста/выделения
Виньетка out = src · lerp(1, 1 − strength, smoothstep(r0, r1, dist_norm(p))) где dist_norm — эллиптическое расстояние от центра, форма (круг/прямоугольник со скруглением), цвет виньетки вместо чёрного
Волны/рябь/скручивание/сферизация/полярные обратное отображение p = f⁻¹(q) + бикубическая выборка (общий код с 5.5), края — прозрачно/clamp/wrap
Шум PRNG xoshiro256++ с seed по координатам тайла (детерминированно и параллельно), гауссов через Бокса–Мюллера, «монохромный» — одно значение на пиксель
Облака/Перлин симплекс-шум (noise crate) с октавами (fBm), бесшовный режим через 4D-шум
Морфология (мин/макс) van Herk/Gil–Werman O(1) по строкам и столбцам
Пользовательское ядро прямая свёртка до 7×7 с делителем и смещением; SIMD по строкам

6. MeridianScript API

Пространство имён paint в MeridianScript (QuickJS-ng, TypeScript-типы в meridian-script/types/paint.d.ts). Все тяжёлые операции асинхронны (Promise), выполняются в ядре с отменой через AbortSignal; доступ к пикселям — через типизированные массивы (копия региона, без прямого указателя в тайлы).

declare namespace paint {
  function activeImage(): Image | null;
  function images(): Image[];
  function newImage(opts: NewImageOptions): Image;
  function open(path: string, opts?: { asLayersInto?: Image; colorPolicy?: "keep" | "convert" | "ignore" }): Promise<Image>;
  function fromClipboard(): Promise<Image | null>;
  function screenshot(opts: { target: "area" | "window" | "screen"; delayMs?: number; cursor?: boolean; windowTitle?: string }): Promise<Image>;
  const brushes: BrushLibrary;            // галерея кистей / brush gallery
  const palettes: PaletteLibrary;
  const ui: PaintUi;                      // диалоги параметров, прогресс, панели (общий Shell API)
}

interface NewImageOptions { width: number; height: number; dpi?: number; depth?: "u8" | "f16";
  colorSpace?: "sRGB" | "DisplayP3" | "AdobeRGB" | "linear-sRGB" | { icc: ArrayBuffer }; background?: Color | "transparent"; }

interface Image {
  readonly id: string; readonly path: string | null; readonly modified: boolean;
  width: number; height: number; dpi: number; readonly depth: "u8" | "f16"; readonly colorSpace: ColorSpaceInfo;
  readonly layers: LayerList;                       // корневая группа / root group
  activeLayer: Layer; selection: Selection; readonly history: History; readonly frames: FrameList;
  guides: Guide[]; grid: GridSettings; meta: DocMeta;

  resize(w: number, h: number, opts?: { method?: Interp; keepDpi?: boolean }): Promise<void>;
  resizeCanvas(w: number, h: number, anchor?: Anchor, fill?: Color | "transparent"): void;
  rotate(deg: number, opts?: { expand?: boolean; method?: Interp }): Promise<void>;
  flip(axis: "h" | "v"): void;
  crop(rect: Rect): void;  trim(mode?: "transparent" | "color"): void;
  flatten(): Layer;  mergeVisible(): Layer;
  convertDepth(depth: "u8" | "f16", dither?: boolean): Promise<void>;
  convertProfile(target: ColorSpaceInfo, intent?: Intent, bpc?: boolean): Promise<void>;
  composite(rect?: Rect): Promise<Bitmap>;          // плоский результат / flattened result
  save(): Promise<void>;  saveAs(path: string): Promise<void>;
  export(path: string, opts?: ExportOptions): Promise<{ bytes: number }>;
  toBlob(format: ExportFormat, opts?: ExportOptions): Promise<Uint8Array>;
  close(discard?: boolean): void;
}

type Interp = "nearest" | "bilinear" | "bicubic" | "lanczos3" | "box" | "mitchell";
type BlendMode = "normal" | "dissolve" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "colorDodge" | "colorBurn"
  | "hardLight" | "softLight" | "difference" | "exclusion" | "linearDodge" | "linearBurn" | "vividLight" | "linearLight"
  | "pinLight" | "hardMix" | "subtract" | "divide" | "darkerColor" | "lighterColor" | "hue" | "saturation" | "color"
  | "luminosity" | "clear" | "source" | "destination" | "srcIn" | "dstIn" | "srcOut" | "dstOut" | "srcAtop" | "dstAtop" | "xor" | "plus";

interface LayerList extends Iterable<Layer> {
  readonly length: number; at(i: number): Layer; byName(name: string): Layer | null; byId(id: string): Layer | null;
  add(kind: "raster", opts?: { name?: string; index?: number }): RasterLayer;
  addText(text: string, opts?: TextLayerOptions): TextLayer;
  addVector(opts?: { name?: string }): VectorLayer;
  addAdjustment(adj: AdjustmentSpec, opts?: { name?: string; clip?: boolean }): AdjustmentLayer;
  addGroup(name?: string, children?: Layer[]): GroupLayer;
  remove(layer: Layer): void;  move(layer: Layer, index: number, parent?: GroupLayer): void;
}

interface Layer {
  readonly id: string; readonly kind: "raster" | "text" | "vector" | "adjustment" | "group";
  name: string; visible: boolean; opacity: number; blendMode: BlendMode; locks: Locks; colorTag: ColorTag | null;
  readonly parent: GroupLayer | null; readonly bounds: Rect | null; offset: Point; altText: string | null;
  mask: Mask | null;  clipToBelow: boolean;
  addMask(from?: "selection" | "alpha" | "white" | "black" | Bitmap): Mask;
  duplicate(): Layer;  mergeDown(): Layer;  rasterize(): RasterLayer;
  move(dx: number, dy: number): void;
  transform(m: Matrix, opts?: { method?: Interp }): Promise<void>;
  applyFilter(name: FilterName, params?: object, opts?: { selectionOnly?: boolean }): Promise<void>;
  adjust(adj: AdjustmentSpec): Promise<void>;
}

interface RasterLayer extends Layer {
  getPixels(rect: Rect): PixelBuffer;               // копия, непремультиплированные / copy, straight alpha
  putPixels(rect: Rect, buf: PixelBuffer): void;     // одна запись истории / one history entry
  getPixel(x: number, y: number): Color;  setPixel(x: number, y: number, c: Color): void;
  fill(color: Color | Gradient | Pattern, opts?: { selectionOnly?: boolean; mode?: BlendMode; opacity?: number }): void;
  floodFill(x: number, y: number, color: Color, opts?: { tolerance?: number; space?: "rgb" | "lab"; conn8?: boolean; allLayers?: boolean; global?: boolean }): void;
  stroke(points: StrokePoint[], brush: BrushSpec, color: Color): Promise<void>;  // программный мазок / programmatic stroke
  clear(rect?: Rect): void;
}
interface PixelBuffer { width: number; height: number; depth: "u8" | "f16"; data: Uint8ClampedArray | Float32Array; }
interface StrokePoint { x: number; y: number; pressure?: number; tiltX?: number; tiltY?: number; t?: number; }

interface TextLayer extends Layer { text: string; font: FontSpec; color: Color; align: "left" | "center" | "right" | "justify"; box: Rect | null; lineHeight: number; stroke?: { color: Color; width: number }; }
interface VectorLayer extends Layer { readonly shapes: ShapeList; addRect(r: Rect, style?: ShapeStyle): Shape; addEllipse(r: Rect, style?: ShapeStyle): Shape; addLine(a: Point, b: Point, style?: ShapeStyle & { arrowEnd?: ArrowKind; arrowStart?: ArrowKind }): Shape; addPath(svgPathData: string, style?: ShapeStyle): Shape; importSvg(svg: string): void; }
interface AdjustmentLayer extends Layer { params: AdjustmentSpec; }
interface GroupLayer extends Layer { readonly children: LayerList; passThrough: boolean; }

type AdjustmentSpec =
  | { type: "brightnessContrast"; brightness: number; contrast: number; legacy?: boolean }
  | { type: "levels"; channel?: "rgb" | "r" | "g" | "b" | "a"; inLow: number; gamma: number; inHigh: number; outLow?: number; outHigh?: number }
  | { type: "curves"; channel?: "rgb" | "r" | "g" | "b"; points: [number, number][]; spline?: "monotone" | "linear" }
  | { type: "colorBalance"; shadows?: [number, number, number]; midtones?: [number, number, number]; highlights?: [number, number, number]; preserveLuminosity?: boolean }
  | { type: "hueSaturation"; hue: number; saturation: number; lightness: number; range?: "master" | "reds" | "yellows" | "greens" | "cyans" | "blues" | "magentas"; colorize?: boolean }
  | { type: "desaturate"; method?: "luminosity" | "lightness" | "average" | "max" | { weights: number[] } }
  | { type: "invert" } | { type: "posterize"; levels: number } | { type: "threshold"; level: number }
  | { type: "autoLevels"; clip?: number } | { type: "autoContrast" } | { type: "autoColor" };

type FilterName = "gaussianBlur" | "motionBlur" | "radialBlur" | "boxBlur" | "unsharpMask" | "sharpen" | "highPass"
  | "addNoise" | "median" | "dustScratches" | "pixelate" | "oilPaint" | "emboss" | "edges" | "vignette" | "wave" | "ripple"
  | "twirl" | "spherize" | "polar" | "offset" | "customKernel" | "minimum" | "maximum" | "clouds" | "perlin" | string /* скриптовые */;

interface Selection {
  readonly isEmpty: boolean; readonly bounds: Rect | null;
  all(): void; none(): void; invert(): void;
  rect(r: Rect, op?: SelOp): void; ellipse(r: Rect, op?: SelOp): void; polygon(pts: Point[], op?: SelOp): void;
  magicWand(x: number, y: number, opts?: { tolerance?: number; space?: "rgb" | "lab"; conn8?: boolean; allLayers?: boolean; global?: boolean }, op?: SelOp): void;
  byColor(color: Color, opts?: { tolerance?: number; fuzziness?: number }, op?: SelOp): void;
  fromMask(mask: Bitmap | Mask, op?: SelOp): void;  fromLayerAlpha(layer: Layer, op?: SelOp): void;
  feather(radius: number): void; grow(px: number): void; shrink(px: number): void; smooth(radius: number): void; border(width: number): void;
  stroke(opts: { width: number; color: Color; position?: "inside" | "center" | "outside"; layer?: RasterLayer }): void;
  fill(color: Color | Gradient | Pattern, layer?: RasterLayer): void;
  toMask(): Bitmap;  save(name: string): void;  load(name: string, op?: SelOp): void;
}
type SelOp = "replace" | "add" | "subtract" | "intersect";

declare namespace paint.filters {         // фильтры как функции над Bitmap (вне документа) / filters as pure functions over bitmaps
  function gaussianBlur(src: Bitmap, sigma: number): Promise<Bitmap>;
  function unsharpMask(src: Bitmap, p: { amount: number; radius: number; threshold?: number }): Promise<Bitmap>;
  function median(src: Bitmap, radius: number): Promise<Bitmap>;
  function resize(src: Bitmap, w: number, h: number, method?: Interp): Promise<Bitmap>;
  function quantize(src: Bitmap, colors: number, opts?: { dither?: "floydSteinberg" | "ordered" | "none"; method?: "octree" | "medianCut" }): Promise<IndexedBitmap>;
  function convertColorSpace(src: Bitmap, to: ColorSpaceInfo, intent?: Intent): Promise<Bitmap>;
  function register(name: string, fn: (src: PixelBuffer, params: object, ctx: FilterContext) => PixelBuffer | Promise<PixelBuffer>, ui?: ParamSchema): void; // свой фильтр в меню / custom filter in menu
}

Примеры:

// 1. Пакетная обработка папки: уменьшить до 1600 px по ширине, водяной знак, экспорт в WebP
// 1. Batch: resize to 1600 px wide, watermark, export WebP
import { fs, path } from "meridian/host";
const dir = await paint.ui.pickFolder("Папка с фотографиями");
for (const file of await fs.list(dir, { ext: ["jpg", "png", "heic"] })) {
  const img = await paint.open(file);
  const k = 1600 / img.width;
  await img.resize(1600, Math.round(img.height * k), { method: "lanczos3" });
  const wm = img.layers.addText("© Meridian", { font: { family: "Inter", size: 24, weight: 600 }, color: "#FFFFFFB3" });
  wm.move(img.width - wm.bounds!.width - 24, img.height - wm.bounds!.height - 24);
  wm.blendMode = "overlay";
  await img.export(path.join(dir, "web", path.stem(file) + ".webp"), { format: "webp", quality: 82, metadata: "strip", profile: "sRGB" });
  img.close(true);
}

// 2. Свой фильтр «Сепия с зерном» в меню Фильтры
// 2. Custom "Sepia + grain" filter in the Filters menu
paint.filters.register("sepiaGrain", async (src, { strength = 0.8, grain = 12 }, ctx) => {
  const out = ctx.clone(src);
  const d = out.data as Uint8ClampedArray; const rnd = ctx.random(ctx.seed);
  for (let i = 0; i < d.length; i += 4) {
    const [r, g, b] = [d[i], d[i + 1], d[i + 2]];
    const sr = 0.393 * r + 0.769 * g + 0.189 * b, sg = 0.349 * r + 0.686 * g + 0.168 * b, sb = 0.272 * r + 0.534 * g + 0.131 * b;
    const n = (rnd() - 0.5) * grain;
    d[i] = r + (sr - r) * strength + n; d[i + 1] = g + (sg - g) * strength + n; d[i + 2] = b + (sb - b) * strength + n;
  }
  return out;
}, { strength: { type: "slider", min: 0, max: 1, step: 0.01, default: 0.8, label: "Сила" }, grain: { type: "slider", min: 0, max: 64, default: 12, label: "Зерно" } });

// 3. Скриншот окна каждые 10 минут в Notes (журнал работы)
// 3. Window screenshot every 10 minutes into Notes (work log)
import { notes } from "meridian/apps";
setInterval(async () => {
  const shot = await paint.screenshot({ target: "screen", cursor: false });
  const png = await shot.toBlob("png");
  await notes.append("Журнал", { image: png, caption: new Date().toLocaleString() });
  shot.close(true);
}, 10 * 60 * 1000);

// 4. Выделить фон палочкой, инвертировать, вырезать объект на новый слой с растушёвкой
const img = paint.activeImage()!; const bg = img.activeLayer as RasterLayer;
img.selection.magicWand(2, 2, { tolerance: 24, space: "lab", conn8: true });
img.selection.invert(); img.selection.feather(1.5);
const cut = img.layers.add("raster", { name: "Объект" });
cut.putPixels(img.selection.bounds!, bg.getPixels(img.selection.bounds!));   // маска применяется автоматически / mask auto-applied
cut.addMask("selection");

Запись действий (макрорекордер) генерирует такой же код: каждое действие интерфейса имеет скриптовый эквивалент (paint.ui.record()), что служит и регрессионными тестами.


7. Форматы

Формат Чтение Запись Библиотека (Rust) Особенности
.mpi ✔ ✔ свой (zip, quick-xml, png, exr) полная модель; mergedimage.png для других приложений
PNG ✔ ✔ png (+ zopfli опционально для «максимального сжатия») 1/2/4/8/16 бит, палитра, iCCP, gAMA/cHRM/sRGB, eXIf, tEXt/iTXt; APNG (анимация) чтение/запись
JPEG ✔ ✔ zune-jpeg (декод, SIMD), jpeg-encoder / mozjpeg (кодирование, опц.) прогрессивный, EXIF-ориентация (авто-поворот), ICC (APP2), CMYK/YCCK → RGB, arithmetic coding (чтение), 12-бит — нет
WebP ✔ ✔ libwebp (через webp crate, BSD) lossy/lossless/alpha/анимация, ICC/EXIF/XMP chunks
GIF ✔ ✔ gif + свой квантователь анимация, disposal, прозрачность, loop count (NETSCAPE2.0)
BMP ✔ ✔ image::bmp / свой 1/4/8/16/24/32 бит, RLE, BITMAPV4/V5 с альфой и ICC; «обратный» порядок строк
TIFF ✔ ✔ tiff 8/16/32 бит, LZW/ZIP/PackBits/JPEG/none, многостраничный (страницы → документы/кадры), ICC, CMYK (через lcms2), float, BigTIFF (чтение)
ICO / CUR ✔ ✔ свой наборы размеров, PNG-внутри (256), курсоры с hotspot
ORA ✔ ✔ свой см. 5.15
PSD / PSB ✔ ✔ (базово) свой (meridian-codecs/psd) см. 5.15; PSB — P1
XCF ✔ — свой GIMP ≤ 2.10 (v11–12), группы, маски, режимы
HEIC / HEIF ✔ ✔ (macOS через ImageIO; Windows — при наличии расширения HEVC; Linux — libheif + libde265, опц.) libheif (LGPL, динамически) лицензия HEVC — запись только через системные кодеки (см. риски)
AVIF ✔ ✔ dav1d/rav1e через libavif (или ravif для записи) 8/10/12 бит → 8/16, альфа, ICC/CICP
JPEG XL ✔ ✔ jxl-oxide (чтение, Rust), libjxl (запись, опц.) P1
SVG ✔ (растрирование) ✔ (векторные/текстовые слои) resvg/usvg (чтение), свой писатель диалог размера при импорте; импорт как векторный слой — подмножество (пути, фигуры, текст, градиенты)
RAW (CR2/CR3/NEF/ARW/DNG/RAF/ORF/…) ✔ — libraw (LGPL, динамически) проявка в 16 бит линейный, баланс белого, экспозиция; DNG через libraw
PDF ✔ (растрирование страниц через Meridian PDF/pdfium) ✔ Meridian Codecs импорт: диалог страницы/DPI; экспорт: ICC, многостраничный
DDS / TGA / PNM / QOI / EXR ✔ ✔ (TGA, PNM, QOI, EXR) image, exr EXR — 16/32 float, для 16-бит обмена
Буфер обмена ✔ ✔ Qt + свои MIME image/png (с альфой), CF_DIBV5, public.tiff, text/uri-list, image/svg+xml, application/vnd.meridian.paint-layers (слои между документами)
Кисти .abr/.gbr/.gih ✔ ✔ (.gbr) свой ABR v6+ (сэмплированные), ABR v1/2, GIH — анимированные трубки
Палитры .gpl/.aco/.ase/.pal/.txt ✔ ✔ свой ASE — RGB/CMYK/LAB группы

Метаданные: EXIF/XMP/IPTC — чтение через kamadak-exif/свой XMP-парсер, хранение «как есть» в meta.xml, запись обратно при экспорте с правкой ориентации (всегда 1 после авто-поворота), DPI, ICC; опция «удалить геопозицию» и «удалить всё» в экспорте. Приоритет: PNG/JPEG/WebP/GIF/BMP/TIFF/ICO/ORA/.mpi/PSD(чтение)/ буфер — P0; XCF, HEIC, AVIF, SVG, RAW, PDF-импорт, PSD-запись — P1; JPEG XL, DDS, PSB — P1/P2.


8. Горячие клавиши

Схема по умолчанию — знакомая пользователям Photoshop/Paint.NET (на macOS Ctrl → ⌘, Alt → ⌥); схемы «GIMP», «Krita» и своя — в настройках. Полный список редактируется в «Параметры → Клавиши».

Действие Клавиши Действие Клавиши
Новый / из буфера Ctrl+N / Ctrl+Shift+V Открыть / Сохранить / Сохранить как Ctrl+O / Ctrl+S / Ctrl+Shift+S
Экспорт / быстрый экспорт Ctrl+Shift+E / Ctrl+Alt+Shift+E Печать Ctrl+P
Отменить / повторить Ctrl+Z / Ctrl+Y (Ctrl+Shift+Z) Повторить фильтр / с настройками Ctrl+F / Ctrl+Alt+F
Вырезать / копировать / вставить / вставить как слой Ctrl+X / Ctrl+C / Ctrl+V / Ctrl+Alt+V Копировать совмещённые Ctrl+Shift+C
Выделить всё / снять / инвертировать / повторно Ctrl+A / Ctrl+D / Ctrl+Shift+I / Ctrl+Shift+D Растушёвка Shift+F6
Свободная трансформация / применить / отмена Ctrl+T / Enter / Esc Повторить трансформацию Ctrl+Shift+T
Новый слой / дублировать / удалить Ctrl+Shift+N / Ctrl+J / Delete (в панели) Группа / разгруппировать Ctrl+G / Ctrl+Shift+G
Объединить вниз / видимые / свести Ctrl+E / Ctrl+Shift+E→ конфликт с экспортом: Ctrl+Shift+M / Ctrl+Shift+Alt+E Растрировать Ctrl+Shift+R→ занято поворотом вида: Ctrl+Alt+R
Слой вверх / вниз / наверх / вниз Ctrl+] / Ctrl+[ / Ctrl+Shift+] / Ctrl+Shift+[ Следующий / предыдущий слой Alt+] / Alt+[
Скрыть/показать слой Ctrl+, Маска слоя: показать / отключить Alt+клик / Shift+клик по миниатюре
Уровни / кривые / оттенок-насыщенность / баланс Ctrl+L / Ctrl+M / Ctrl+U / Ctrl+B Обесцветить / инвертировать Ctrl+Shift+U / Ctrl+I
Автоуровни / автоконтраст / автоцвет Ctrl+Shift+L / Ctrl+Alt+Shift+L / Ctrl+Shift+B Яркость/контраст Ctrl+Shift+Alt+B
Размер изображения / холста Ctrl+Alt+I / Ctrl+Alt+C Обрезать по выделению Ctrl+Shift+X
Поворот 90° по / против Ctrl+Alt+] / Ctrl+Alt+[ Отразить по горизонтали / вертикали Ctrl+Alt+H / Ctrl+Alt+J
Масштаб + / − / 100 % / по размеру Ctrl++ / Ctrl+− / Ctrl+1 / Ctrl+0 Поворот вида / сброс R+мышь, Shift+колесо / Ctrl+Shift+R
Линейки / сетка / направляющие / привязка Ctrl+R / Ctrl+' / Ctrl+; / Ctrl+Shift+; Полный экран / фокус (скрыть панели) F11 / Tab
Инструменты P B E G T U M L W Q V C I Z H S J O R (см. 2.2), Shift+буква — вариант Поменять цвета / сброс X / D
Размер кисти − / + [ / ] Жёсткость − / + Shift+[ / Shift+]
Непрозрачность кисти цифры 1…0 (10 %…100 %) Поток Shift+цифры
Пипетка временно / рука временно Alt (в кистях) / пробел Прямая линия кистью Shift+клик
Снимок области / окна / экрана Ctrl+Shift+Alt+S / Ctrl+Shift+Alt+W / Ctrl+Shift+Alt+F Глобальный «Снимок» Windows Ctrl+Shift+PrtScn, macOS ⌃⇧⌘S, Linux Ctrl+Shift+PrtScn (настраивается)
Кадры: следующий / предыдущий / новый . / , / Alt+N Воспроизвести Shift+Space
Командная палитра Ctrl+K Параметры Ctrl+,→ занято: Ctrl+Alt+,

Примечание: конфликты, отмеченные стрелкой, разрешены в пользу привычки Photoshop; окончательная таблица фиксируется в docs/shell/keymap.md (общий реестр клавиш пакета).


9. Критерии готовности

# Критерий Метрика / способ проверки
1 Отклик при рисовании холст 8000×8000, 8 бит, 20 растровых слоёв с масками и режимами, кисть 50 px, жёсткость 50 %: задержка «событие → кадр» ≤ 16 мс (p95), ≤ 30 мс (p99); измеряется автотестом с синтетическим планшетным вводом 240 Гц на эталонных машинах (Windows с iGPU Intel Iris Xe, MacBook Air M1, Linux с AMD iGPU)
2 Память тот же документ ≤ 1,5 × размера непустых тайлов + 300 МБ; история с лимитом не превышает настройку
3 Открытие JPEG 24 Мпикс ≤ 300 мс; PSD 100 слоёв 4000×4000 ≤ 3 с; .mpi 20 слоёв 8000×8000 ≤ 2 с (ленивые тайлы)
4 Фильтры гаусс σ=20 на 24 Мпикс ≤ 400 мс (8 потоков); медиана r=5 ≤ 1,2 с; предпросмотр любого фильтра ≤ 100 мс на видимой области
5 Точность композитинга все режимы совпадают с эталоном (W3C/PDF формулы, PSD-эталоны из Photoshop) с допуском 1/255; CPU = GPU ± 1/255
6 Совместимость корпус 500 PSD (слои/маски/группы/режимы) — визуальное совпадение композита с Image Data ≥ 99 % пикселей в допуске ΔE < 2; ORA round-trip без потерь; PNG/JPEG/WebP/GIF/TIFF/BMP/ICO — тест-наборы форматов (PngSuite, libjpeg, libwebp) открываются полностью
7 Скриншоты все три ОС: область/окно/экран/задержка, мультимонитор с разными DPI, HDR на Windows; Wayland через портал на GNOME 45+/KDE 6
8 Планшеты Wacom (Intuos, Cintiq), XP-Pen, Huion, Apple Pencil через Sidecar, Surface Pen: нажим, наклон, ластик-конец, без «залипаний»
9 Стабильность фаззинг всех парсеров (PSD, XCF, ORA, ABR, ICC) 72 ч без падений; парсеры в песочном процессе; восстановление после краха — 100 % сохранённых операций
10 История 1000 мазков undo/redo без утечек; снимки истории; сброс на диск при лимите
11 Скрипты API раздела 6 покрыт тестами; макрорекордер воспроизводит 50 эталонных сценариев
12 Доступность все команды с клавиатуры; NVDA/VoiceOver/Orca озвучивают панели; масштаб UI 200 % без обрезаний
13 Цвет ICC-преобразования совпадают с lcms2 эталоном (ΔE < 0,5), экран с профилем монитора; 16-бит документ без полос в градиентах
14 Качество ресамплинга Lanczos/бикубический — PSNR ≥ эталона ImageMagick −0,5 дБ на наборе из 50 изображений

10. Риски

Риск Вероятность Влияние Снижение
Производительность на интегрированных GPU и в виртуальных машинах (нет GPU) средняя высокое CPU-путь Skia равноправен; тайловый композитинг не зависит от GPU; тесты на слабых машинах в CI
Планшетный ввод в Qt 6 на Wayland (tablet-v2) и Windows Ink/WinTab — расхождения, потеря событий высокая среднее свой слой над QTabletEvent, переключатель API, тест-стенд с устройствами, журнал событий для отчётов
Точность чтения PSD (недокументированные блоки, эффекты слоёв, смарт-объекты) высокая среднее «открыть как композит» всегда работает; предупреждения о потерях; корпус PSD из реальных файлов; эффекты — P2
Лицензии кодеков: HEVC (HEIC) — патенты; libheif/libde265 LGPL средняя среднее запись HEIC только системными кодеками ОС; чтение — через LGPL-библиотеки динамически, отдельный загружаемый пакет
Управление цветом: неверная политика → «картинки выглядят иначе» средняя высокое политика «не трогать байты», явные диалоги, тесты на эталонах с профилями, profile-aware экран
Ограничения скриншотов на Wayland (нет наложения, нет окна по выбору) высокая среднее портал как основной путь, честные ограничения в UI, X11-режим полный
Память при 16 бит и больших холстах (8000×8000×20 слоёв f16 = до 10 ГБ плотных данных) средняя высокое разреженные тайлы, сброс истории на диск, предупреждение при создании, mmap-кэш тайлов (P1)
Расхождение CPU/GPU-рендера (фильтры предпросмотра) средняя низкое предпросмотр помечен; финал — всегда CPU-алгоритм; снимочные тесты
Объём работ: число инструментов и фильтров велико высокая высокое P0 — минимальный набор (карандаш, кисть, ластик, заливка, градиент, текст, фигуры, выделения, трансформация, 10 коррекций, 12 фильтров); общий код с Draw (векторы) и Write (текст)
Текстовый движок Write на момент фазы 4 должен поддерживать обводку/вертикальный текст низкая среднее требования переданы в docs/apps/write.md (text-layout: stroke paint, vertical — P1)
Конфликты глобальных клавиш снимка с системными высокая низкое настраиваемые сочетания, определение занятости при регистрации, подсказка

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

  1. Инструмент «Снимок» и просмотр изображений в Free. Paint целиком — в Home (раздел 9.1). Предлагается выделить «Снимок экрана с пометками» и просмотр/конвертацию изображений (без слоёв) в Free как Feature::PaintSnip и Feature::PaintView: это точка входа для продаж Home и привычная системная функция.
  2. Движок управления цветом. В стеке (раздел 4) не назван ICC-движок; Paint, PDF и Print нуждаются в общем: предлагается lcms2 (MIT) как часть Meridian Render/Codecs (kernel/crates/meridian-color/).
  3. Глубина 32 бит float. Не планируется в Paint 1.0 (только 8 и 16 float); если HDR-рабочие процессы (EXR, HDR-скриншоты Windows) станут приоритетом — потребуется решение владельца.
  4. Расширение .mpi совпадает с проприетарными расширениями некоторых программ (например, MapInfo); при регистрации ассоциаций файлов стоит это учесть (не блокер).
  5. Общий реестр горячих клавиш (docs/shell/keymap.md) и общий диалог экспорта изображений для всех приложений (Slides/Draw экспортируют PNG/SVG тем же диалогом PT-12) — предлагается закрепить в Meridian Shell.