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 Границы продукта (чего не делаем)
- CMYK как рабочее пространство документа (только конвертация при экспорте в PDF/TIFF — P2).
- Узловой композитинг, 3D, видео, векторный редактор (это Draw).
- Нейросетевые функции (удаление фона, апскейл) — через Meridian Assist (фаза 7), не в ядре Paint.
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 Интеграция с пакетом и расширяемость
- Встроенное редактирование из Write/Slides/Notes/Draw (см. 1.1) — P0.
- «Вставить скриншот» в любом приложении пакета открывает наложение
Paint (
Вставка → Снимок экрана) — P0. - Hub: быстрый просмотр изображений (без открытия Paint), «Открыть в Paint» — P0.
- Скрипты MeridianScript (раздел 6): фильтры, пакетная обработка, свои инструменты (P1), панели (P2) — P0 для API ядра.
- Плагины фильтров — только через MeridianScript и WASM-модули (песочница), без нативных DLL — P1.
- Кисти: импорт
.abr(Photoshop, версии 6+),.gbr/.gih(GIMP),.myb? (MyPaint — P2),.kpp(Krita, частично P2) — P1.
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 Панель «Слои»
- Шапка: режим наложения (выпадающий список с группами и предпросмотром при наведении), непрозрачность (ползунок + поле), блокировки (4 значка), фильтр по типу, поиск по имени.
- Список: строка слоя — глаз, миниатюра (40 px, «шахматка»), миниатюра маски (если есть, со связью-цепочкой), имя (двойной щелчок — переименовать), значки типа (T — текст, ✎ — вектор, ◐ — корректирующий, ▸ — группа с раскрытием), цветовая метка (полоска слева), замок.
- Перетаскивание: порядок, в группу/из группы, между документами;
Ctrlпри перетаскивании — копия. - Низ панели: новый слой, новая группа, маска, корректирующий слой
(меню), дублировать, удалить; выделение нескольких
(
Ctrl/Shift) — групповые операции. - Контекстное меню: все операции 2.5 + «Свойства слоя» (PT-21).
PT-03 Панель «Цвет»
- Верх: большой образец основного/фонового цвета, кнопка «поменять», «сброс».
- Селектор: режимы «Круг» (кольцо H + треугольник SV), «Квадрат»
(H-полоса + SV), «Ползунки» (RGB / HSV / HSL / LAB / OKLCH /
CMYK-проба), «HEX» (
#RRGGBBAA, вставка из буфера), «Образцы». - Образцы: сетка 16 px, группы (палитра документа, «Недавние»,
«Пользовательские», «Material», …), добавление текущего
(
+), импорт/экспорт, создать из изображения. - Индикатор «вне охвата» (для документов с профилем ≠ sRGB) и «вне sRGB» при экспорте.
PT-04 Панель «История»
- Список действий: значок операции, имя («Кисть», «Гауссово размытие σ=4», «Новый слой»), миниатюра (опционально); текущее состояние выделено; записи ниже текущей — серые (redo).
- Снимки (snapshots) вверху: «Исходное», пользовательские (кнопка «фотоаппарат»).
- Низ: «Создать снимок», «Удалить», «Очистить историю» (освободить память), показатель памяти истории.
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 Диалог фильтра (общий каркас)
- Левая часть: параметры (ползунок + числовое поле + «связать»), предустановки (выпадающий список, сохранить/удалить), «Сброс».
- Правая часть: предпросмотр — фрагмент 1:1 (перетаскиваемый) с кнопкой «до/после» и переключатель «на холсте» (рендер в реальном времени на видимой области — на GPU где возможно).
- Низ: «Применить к: слой / выделение / как корректирующий слой (где применимо)», OK/Отмена, время расчёта последнего предпросмотра (для диагностики).
- Отзывчивость: предпросмотр считается на уменьшенной копии, если полный расчёт > 100 мс; полный — в фоне с индикатором.
PT-12 Диалог «Экспорт»
- Формат (PNG/JPEG/WebP/GIF/BMP/TIFF/ICO/PSD/PDF/AVIF/HEIC(macOS)/SVG(вектор)), настройки сжатия: JPEG — качество 0–100, прогрессивный, субдискретизация 4:4:4/4:2:0, оптимизация Хаффмана; PNG — уровень, 8-бит палитра (с дизерингом), чересстрочный; WebP — lossy/lossless, качество, метод 0–6; GIF — палитра, дизеринг, прозрачность, анимация; TIFF — сжатие LZW/ZIP/none, многостраничный; ICO — набор размеров (16/24/32/48/64/128/256); PDF — ICC, страницы.
- Метаданные: EXIF (сохранить/удалить/удалить геопозицию), XMP, ICC (встроить/преобразовать в sRGB).
- Масштаб при экспорте (×0,5, ×2, в px), «обрезать по содержимому».
- Предпросмотр «до/после» с оценкой размера файла (пересчёт при изменении параметров, в фоне).
- Кнопки: «Экспорт», «Экспорт и открыть папку», «Копировать в буфер».
PT-13 Снимок экрана — наложение выбора области
- Полноэкранное полупрозрачное окно (затемнение 40 %) на каждом мониторе, курсор-перекрестие, лупа 120×120 (×8) с координатами и цветом под курсором, подсказка «Выделите область · Enter — снять · Esc — отмена · Пробел — переместить».
- Рамка выделения с размером (например «1280 × 720») и маркерами
изменения; привязка к окнам/элементам (Windows UIAutomation, macOS
Accessibility, X11 — только окна);
Alt— без привязки;Shift— квадрат. - Режимы в верхней плавающей панели: Область · Окно · Экран · Произвольная форма (лассо) · Задержка (0/3/5/10 с) · Курсор вкл/выкл · После снимка: «мини-редактор» / «в Paint» / «в буфер» / «сохранить».
- Мини-редактор (режим «Снимок»): компактное окно с изображением и
панелью: стрелка, прямоугольник, эллипс, маркер, карандаш, номер-кружок
(автонумерация), текст, размытие/пикселизация области, обрезка, отмена;
кнопки «Копировать», «Сохранить» (папка снимков, имя по шаблону
Снимок {дата} {время}.png), «Открыть в Paint», «Поделиться».
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> }Инварианты:
- Тайлы неизменяемы; любая запись идёт через
tile_mut, который фиксирует старую версию в текущей записи истории. Две истории (документа и мазка-предпросмотра) не пересекаются. boundsслоя — надмножество реальных непустых пикселей; уточняется лениво фоновой задачей (подсчёт пустых тайлов), пустые тайлы удаляются из карты.- Слой может быть больше холста; холст — лишь область экспорта и композитинга по умолчанию.
- Все цвета в тайлах премультиплицированы; непремультиплицированное представление нужно только для формул разделённых режимов наложения, цветокоррекций и экспорта (делим на α с защитой от 0).
- 8-бит хранится в sRGB-кодировке (значения 0–255 — гамма-кодированные), 16-бит float — линейные.
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.
Конвейер мазка:
- Стабилизатор (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.
Сглаживание пути: сглаженные точки P₀…Pₙ соединяются сплайном Catmull-Rom (centripetal, α = 0,5) по четвёркам (Pᵢ₋₁, Pᵢ, Pᵢ₊₁, Pᵢ₊₂); сегмент Pᵢ→Pᵢ₊₁ дискретизируется по длине дуги (накопленная длина
Lсохраняется между сегментами, чтобы интервал не «сбивался» на стыках). Нажим/наклон интерполируются линейно вдоль сегмента.Размещение дабов: интервал
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; джиттер размера/угла.Растеризация даба в буфер мазка (отдельный
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
на выборку из текстуры (в координатах холста).
Накопление в буфере мазка: два режима — «обычный» (
buf = max(buf, value)по альфе: повторные дабы не накапливаются сверхopacity, как кисть Photoshop) и «аэрограф/накопление» (buf = buf + value·(1 − buf)). Цвет дабов в буфере — текущий цвет (премультиплированный на альфу покрытия).Наложение на слой:
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
}- Допуск:
Tolerance::Rgb(t)—max(|ΔR|,|ΔG|,|ΔB|,|Δα|) ≤ t;Tolerance::Lab(ΔE)— CIE76 в LAB (быстро) или CIEDE2000 (опция); цвет сравнивается непремультиплированным; прозрачные пиксели сравниваются по альфе. - «Все слои» —
PixelSource = projection, иначе активный слой. «Глобально» — без связности:mask[p] = similar(p)для всего прямоугольника. - Сглаживание края: маска →
blur(0.5 px)→ порог-гладкость (значения 0..255 как покрытие) — заливка без «лесенки». Закрытие разрывов (P1): морфологическое закрытие границыне-similarрадиусом N перед заливкой. - Заливка применяется как
layer = over(layer, color · mask)(или узор/градиент через маску), с учётом выделения. - Волшебная палочка: та же функция, результат идёт в
Selectionс операцией (новое/+/−/∩); «мягкость» (fuzziness, для «По цвету») даёт градиентную маску:cov = clamp(1 − (ΔE − t)/(f), 0, 1). - Ограничение
limit— холст либо объединение bounds слоёв (Alt— без ограничения холстом).
5.4 Быстрое выделение (GrabCut-подобное, P2)
- Пользователь проводит мазок «объект» (и при необходимости «фон»); начальная тримап: мазок — точно объект, прямоугольник вокруг с полями 20 % — «вероятно фон», остальное — точно фон.
- Работа на уменьшенной копии (≤ 1 Мпикс; масштаб
s), в цветовом пространстве LAB. - GMM по 5 компонент для объекта и фона (k-means инициализация, 2 итерации EM на каждом шаге).
- Граф: узлы — пиксели, рёбра к соседям (8-связность) с весом
w = γ · exp(−β · ‖cᵢ − cⱼ‖²) / dist,β = 1 / (2·⟨‖cᵢ − cⱼ‖²⟩),γ = 50; t-рёбра —−log P(c | GMM_fg/bg), точные метки — бесконечные. - Min-cut (Boykov–Kolmogorov, crate
maxflow/своя реализация), 3–5 итераций «GMM → cut». - Уточнение границы на полном разрешении: 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)
})
}- LUT: 256 записей (8 бит) или 4096 с линейной интерполяцией между узлами (16 бит float; значения > 1 — экстраполяция по последнему сегменту). Канал RGB (мастер) применяется после индивидуальных каналов.
- Кривые: точки (x, y) → монотонный кубический сплайн Фрича–Карлсона (нет перелётов) → выборка в LUT; режим «карандаш» — прямая запись LUT с последующим сглаживанием.
- Автоуровни: гистограмма каждого канала (по выделению, если есть),
отсечение
clip_lo = clip_hi = 0,1 %пикселей с каждой стороны →in_lo,in_hi; автоконтраст — те же пороги, но по яркости (единые для каналов — не меняет цвет); автоцвет — автоуровни + сдвиг средних так, чтобы средний серый (медиана по каналам) стал нейтральным (gammaподбирается бинарным поиском). - Коррекции применяются к непремультиплированным значениям; в 8-бит с дизерингом (упорядоченный 4×4, амплитуда ½ LSB) против полос.
- Корректирующий слой хранит LUT в кэше, инвалидирует при изменении параметров; стек нескольких LUT-коррекций подряд сворачивается в одну LUT (композиция функций) — экономия на 16-бит.
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 по оттенку)
- 8-бит ↔︎ линейный: таблицы 256 → f32 и обратная 4096 → u8 с дизерингом.
- Display P3 / Adobe RGB: матрицы RGB→XYZ из профилей (через
lcms2:cmsCreateTransformс намерением, BPC), кэшируем преобразование как 3D-LUT 33³ для фильтров и предпросмотра (точность ΔE < 0,5), точный путь — при экспорте. - Отображение: композит (в пространстве документа) → профиль монитора
(из ОС: Windows ICM
GetICMProfile, macOSNSScreen.colorSpace, Linux_ICC_PROFILE/colord) → GPU-шейдер с 3D-LUT (SkiaSkColorSpaceдля матричных профилей, свой шейдер для LUT-профилей). - Политика: «smart» — не трогать байты изображения при открытии (сохраняем встроенный профиль), конверсия только по выбору пользователя; экспорт — встроить профиль документа или преобразовать в sRGB (по умолчанию для JPEG/WebP/GIF — sRGB без профиля, для PNG/TIFF/PSD/PDF — встроить).
5.11 Квантование и дизеринг при экспорте в GIF / PNG-8 / ICO
- Квантование — октодерево (Gervautz–Purgathofer): вставка пикселей в
дерево глубиной 8 по битам RGB, при превышении
max_colors(≤ 255 при прозрачности) слияние самых «дешёвых» узлов (минимальная сумма пикселей на уровне с наибольшей глубиной); альтернатива — median cut (опция, лучше для градиентов); для анимации — глобальная палитра по всем кадрам (выборка ≤ 2 Мпикс), локальная — если кадр отклоняется (ΔE_mean > 4). - Дизеринг — Флойд–Стайнберг с серпантинным обходом, распределение
ошибки
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) для пиксель-арта, без дизеринга. - Поиск ближайшего цвета палитры — k-d дерево по RGB (или LAB при «точном» режиме), кэш последних 64.
- Прозрачность: порог альфы (по умолчанию 128) → индекс прозрачного цвета; для WebP/APNG — полная альфа.
- Анимация: оптимизация кадров — ограничивающий прямоугольник
изменений относительно предыдущего (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 тайлов
- Каждая команда получает
UndoRecorder; первое изменение тайла в рамках команды фиксирует его старыйArc(см.tile_mut): это O(1), без копирования — копируется сам тайл приArc::make_mut, только если его ещё кто-то держит (история или кэш). Повторные изменения того же тайла в той же команде не фиксируются. - Undo: для каждой записи
(layer, key, old)текущий тайл кладётся вredo_tiles, старый возвращается в карту (None→ удаление тайла). Redo — симметрично. Структурные изменения —revert()команд в обратном порядке. Dirty-регион = объединение тайлов → перекомпозитинг и обновление GPU-текстур. - Память:
mem_usedсчитает уникальные Arc (тайл, разделяемый с текущим состоянием, не считается). При превышении лимита: самые старые записи сериализуются на диск (LZ4, в фоне), затем удаляются; панель «История» отражает это (серая «корзина» у записей, которые уже недоступны). - Длинный мазок = одна запись (все тайлы, которых он коснулся). Фильтр на весь холст 8000×8000 8 бит = до 1024 тайлов × 256 КБ = 256 МБ — допустимо; для 16 бит (512 МБ) — предупреждение при лимите.
- Снимки (snapshots) панели «История» — просто ссылки на
Arc-тайлы всех слоёв на момент снимка (O(число тайлов), без копирования). - Автосохранение каждые 2 с пишет в журнал восстановления те же
diff-записи (тайлы в LZ4) + структурные команды в CBOR; после удачного
сохранения
.mpiжурнал обнуляется.
5.13 Рендер на GPU через Skia
- Поверхность холста —
SkSurfaceна GPU-контексте Qt (QRhi→ Metal/Vulkan/D3D12/OpenGL черезskia-safegpu::DirectContext); при отсутствии GPU — CPU-растр Skia (тот же код). - Тайлы
projectionзагружаются как текстуры в атласы 4096×4096 (256 тайлов на атлас, RGBA8 или RGBA16F); dirty-тайлы —updateTextureпо прямоугольнику; LRU-вытеснение невидимых; бюджет VRAM 512 МБ (настройка). - Кадр:
canvas.concat(view_matrix)(масштаб, поворот, панорамирование, HiDPI); «шахматка» —SkShader(повтор 16×16, в экранных координатах, не масштабируется); затем видимые тайлы (cullпо видимому прямоугольнику в координатах холста) рисуютсяdrawImageRectсSkSamplingOptions: при масштабе < 100 % —mipmap linear(мипы генерирует Skia для атласа — при зуме 1 % 8000×8000 не читается целиком), 100 % — nearest, > 100 % — nearest + при ≥ 800 % наложение пиксельной сетки (линии 1 px черезdrawPath). - Активный мазок: буфер мазка отрисовывается отдельной текстурой
поверх
below, затемabove— без перекомпозитинга CPU (три слоя текстур: below / stroke+layer / above; на CPU считаем только dirty-тайлы слоя+мазка). - Оверлеи (выделение-«муравьи», направляющие, рамки трансформации,
курсор кисти) — векторный слой Skia поверх, анимация муравьёв — смещение
dash phase60 fps без перерисовки тайлов. - Цветовое управление: поверхность с
SkColorSpaceдокумента → при презентации Skia преобразует к пространству окна (SkSurfaceProps+SkColorSpaceэкрана); для LUT-профилей — финальный проход своим SkSL. - Предпросмотр фильтров на GPU:
SkImageFilters(blur, color matrix, LUT черезSkRuntimeShader) на видимую область с запасом 64 px; окончательный расчёт — CPU-алгоритм (единая точность результата), расхождение допускается только в предпросмотре. - Кадр ≤ 16 мс: измеряем
frame_time, при превышении — понижаем качество предпросмотра (билинейный вместо Lanczos, предпросмотр в половинном разрешении).
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 Градиенты, текст и фигуры
- Градиент: параметризация
t(p)— линейный:t = ((p − a)·(b − a)) / |b − a|²; радиальный:t = |p − c| / r; конический:t = atan2(p − c)/2π; отражённый:t = |2·lin − 1|; ромбовидный:max(|dx|, |dy|) / r; режим повтора (clamp/repeat/reflect); цвет = интерполяция стопов в выбранном пространстве (sRGB / линейный / OKLab — по умолчанию OKLab), стопы прозрачности отдельно; дизеринг (синий шум 64×64) против полос в 8 бит; заливка через выделение; на GPU — SkiaShader::linear_gradientдля предпросмотра, CPU — финал. - Текстовый слой:
TextModel(абзацы, прогоны, стили) →text-layout(шейпинг HarfBuzz, переносы, bidi) → глифы → растрирование Skia в кэш-TileMap (сглаживание серое; субпиксельное — только на непрозрачном фоне), трансформация слоя — аффинная (растр перерисовывается из векторных контуров — без потери качества); обводка —SkPaint::Strokeвторым проходом под заливкой. - Векторный слой:
Sceneиз Draw (узлы: Path с заливкой/обводкой, стрелки как маркеры); растрирование — Skia в кэш; редактирование узлов — инструментом Draw в режиме Paint (общая панель свойств фигуры); «Растрировать» переводит слой в Raster и чистит модель; экспорт SVG — только векторные/текстовые слои.
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 |
| ✔ (растрирование страниц через 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. Вопросы к библии
- Инструмент «Снимок» и просмотр изображений в Free.
Paint целиком — в Home (раздел 9.1). Предлагается выделить «Снимок
экрана с пометками» и просмотр/конвертацию изображений (без слоёв) в
Free как
Feature::PaintSnipиFeature::PaintView: это точка входа для продаж Home и привычная системная функция. - Движок управления цветом. В стеке (раздел 4) не
назван ICC-движок; Paint, PDF и Print нуждаются в общем: предлагается
lcms2(MIT) как часть Meridian Render/Codecs (kernel/crates/meridian-color/). - Глубина 32 бит float. Не планируется в Paint 1.0 (только 8 и 16 float); если HDR-рабочие процессы (EXR, HDR-скриншоты Windows) станут приоритетом — потребуется решение владельца.
- Расширение
.mpiсовпадает с проприетарными расширениями некоторых программ (например, MapInfo); при регистрации ассоциаций файлов стоит это учесть (не блокер). - Общий реестр горячих клавиш
(
docs/shell/keymap.md) и общий диалог экспорта изображений для всех приложений (Slides/Draw экспортируют PNG/SVG тем же диалогом PT-12) — предлагается закрепить в Meridian Shell.