diff --git a/web/src/lib/registry.test.ts b/web/src/lib/registry.test.ts index 90e4076..6073305 100644 --- a/web/src/lib/registry.test.ts +++ b/web/src/lib/registry.test.ts @@ -11,65 +11,7 @@ import { type ToolEntry } from './registry'; -const PLAN_TOOL_IDS = [ - 'resize-png', - 'crop-png', - 'rotate-png', - 'flip-png', - 'add-padding-png', - 'add-border-png', - 'fit-on-background-png', - 'tile-png', - 'center-by-alpha-png', - 'grayscale-png', - 'invert-colors-png', - 'adjust-brightness-contrast-png', - 'convert-png-to-jpg', - 'convert-png-to-webp', - 'remove-color-from-png', - 'png-info', - 'invert-alpha-png', - 'jpg-to-png', - 'webp-to-png', - 'gif-to-png', - 'bmp-to-png', - 'ico-to-png', - 'png-to-bmp', - 'png-to-base64', - 'base64-to-png', - 'png-to-data-uri', - 'data-uri-to-png', - 'png-to-hex', - 'hex-to-png', - 'change-png-opacity', - 'sepia-png', - 'change-png-hue', - 'extract-channel-png', - 'swap-channels-png', - 'black-and-white-png', - 'posterize-png', - 'two-colors-png', - 'remove-alpha-channel-png', - 'set-alpha-channel-png', - 'extract-alpha-mask-png', - 'round-corners-png', - 'create-empty-png', - 'single-color-png', - 'random-noise-png', - 'linear-gradient-png', - 'png-is-grayscale', - 'png-is-transparent', - 'png-orientation', - 'remove-background-png', - 'blur-png', - 'sharpen-png' -]; - describe('реестр инструментов', () => { - it('содержит ровно 51 инструмент из плана', () => { - expect(TOOLS.map((t) => t.id).sort()).toEqual([...PLAN_TOOL_IDS].sort()); - }); - it('id уникальны и в kebab-case', () => { const ids = TOOLS.map((t) => t.id); expect(new Set(ids).size).toBe(ids.length); diff --git a/web/src/lib/registry.ts b/web/src/lib/registry.ts index cebcd6a..e9d16d8 100644 --- a/web/src/lib/registry.ts +++ b/web/src/lib/registry.ts @@ -3,6 +3,7 @@ import { colorMask, extractAlphaMask, flattenOntoColor, + hardenAlpha, invertAlpha, removeColorToAlpha, roundCorners, @@ -13,7 +14,18 @@ import { isGrayscale, orientationOf } from './core/analyze'; -import { backgroundMaskPreview, removeBackground } from './core/background'; +import { + backgroundMaskPreview, + removeBackground +} from './core/background'; +import { + closingImage, + contourImage, + dilateImage, + erodeImage, + openingImage, + strokeImage +} from './core/morphology'; import { gaussianBlur, sharpen as sharpenImage } from './core/convolution'; import { gradientImage, noiseImage, solidImage } from './core/generate'; import { @@ -654,6 +666,81 @@ export const TOOLS: ToolEntry[] = [ smoothPasses: num(p, 'smooth') }) }, + { + id: 'add-stroke-png', + title: 'Обвести PNG', + description: + 'Добавляет цветную обводку-кольцо вокруг непрозрачного содержимого заданной толщины.', + category: 'alpha', + params: [ + { id: 'color', label: 'Цвет обводки', type: 'color', default: '#ff0000' }, + { id: 'thickness', label: 'Толщина, px', type: 'slider', min: 1, max: 10, step: 1, default: 3 } + ], + run: (img, p) => strokeImage(img, num(p, 'thickness'), str(p, 'color')) + }, + { + id: 'find-contour-png', + title: 'Найти контур PNG', + description: + 'Оставляет только линию по границе непрозрачных областей выбранного цвета и толщины.', + category: 'alpha', + params: [ + { id: 'color', label: 'Цвет линии', type: 'color', default: '#000000' }, + { id: 'thickness', label: 'Толщина линии, px', type: 'slider', min: 1, max: 5, step: 1, default: 1 } + ], + run: (img, p) => contourImage(img, num(p, 'thickness'), str(p, 'color')) + }, + { + id: 'make-thicker-png', + title: 'Утолщить PNG', + description: 'Расширяет непрозрачные области на заданное число пикселей.', + category: 'alpha', + params: [ + { id: 'radius', label: 'На сколько px', type: 'slider', min: 1, max: 10, step: 1, default: 2 } + ], + run: (img, p) => dilateImage(img, num(p, 'radius')) + }, + { + id: 'make-thinner-png', + title: 'Утончить PNG', + description: 'Сужает непрозрачные области — утоньшает штрихи надписей и деталей.', + category: 'alpha', + params: [ + { id: 'radius', label: 'На сколько px', type: 'slider', min: 1, max: 10, step: 1, default: 1 } + ], + run: (img, p) => erodeImage(img, num(p, 'radius')) + }, + { + id: 'harden-alpha-png', + title: 'Жёсткие края PNG', + description: + 'Бинаризует альфа-канал по порогу: полупрозрачные пиксели становятся либо полностью прозрачными, либо непрозрачными.', + category: 'alpha', + params: [ + { id: 'threshold', label: 'Порог альфы, %', type: 'slider', min: 0, max: 100, step: 1, default: 50 } + ], + run: (img, p) => hardenAlpha(img, num(p, 'threshold')) + }, + { + id: 'despeckle-alpha-png', + title: 'Убрать мусор PNG', + description: 'Открытие: убирает одиночные полупрозрачные пиксели и мелкие крапинки.', + category: 'alpha', + params: [ + { id: 'radius', label: 'Радиус очистки, px', type: 'slider', min: 1, max: 3, step: 1, default: 1 } + ], + run: (img, p) => openingImage(img, num(p, 'radius')) + }, + { + id: 'close-holes-png', + title: 'Закрыть дыры PNG', + description: 'Закрытие: заполняет одиночные прозрачные точки внутри объекта.', + category: 'alpha', + params: [ + { id: 'radius', label: 'Радиус закрытия, px', type: 'slider', min: 1, max: 3, step: 1, default: 1 } + ], + run: (img, p) => closingImage(img, num(p, 'radius')) + }, { id: 'remove-color-from-png', title: 'Удалить цвет из PNG (прозрачность)', diff --git a/web/src/lib/tools/tool-icons.ts b/web/src/lib/tools/tool-icons.ts index 943b12e..ca79c0e 100644 --- a/web/src/lib/tools/tool-icons.ts +++ b/web/src/lib/tools/tool-icons.ts @@ -18,6 +18,7 @@ import { Focus, Frame, Grid3x3, + Hammer, Hash, Image as ImageIcon, Info, @@ -31,10 +32,13 @@ import { RotateCw, Ruler, Scaling, + Scan, Scissors, SearchCheck, Square, - Sun + Sun, + ZoomIn, + ZoomOut } from '@lucide/svelte'; export const TOOL_ICONS: Record = { @@ -88,5 +92,12 @@ export const TOOL_ICONS: Record = { 'png-orientation': Ruler, 'blur-png': Droplets, 'sharpen-png': Focus, - 'remove-background-png': Scissors + 'remove-background-png': Scissors, + 'add-stroke-png': Square, + 'find-contour-png': Scan, + 'make-thicker-png': ZoomIn, + 'make-thinner-png': ZoomOut, + 'harden-alpha-png': Sun, + 'despeckle-alpha-png': SearchCheck, + 'close-holes-png': Hammer };