From 116d7c72aac7ba2463f249c962251ee9d4a56744 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Wed, 26 Aug 2026 07:21:43 +0500 Subject: [PATCH] feat: add watermarks and text tools --- web/src/lib/components/ToolPage.svelte | 17 +++- web/src/lib/components/stage/ToolStage.svelte | 17 ++++ .../lib/components/tool/OverlayCard.svelte | 63 +++++++++++++ web/src/lib/core/domText.ts | 32 +++++++ web/src/lib/i18n/en.ts | 6 +- web/src/lib/i18n/ru.ts | 49 +++++++++- web/src/lib/registry.ts | 93 ++++++++++++++++++- web/src/lib/tools/overlay-store.svelte.ts | 20 ++++ web/src/lib/tools/tool-icons.ts | 3 + 9 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 web/src/lib/components/tool/OverlayCard.svelte create mode 100644 web/src/lib/tools/overlay-store.svelte.ts diff --git a/web/src/lib/components/ToolPage.svelte b/web/src/lib/components/ToolPage.svelte index 88059f2..47b0826 100644 --- a/web/src/lib/components/ToolPage.svelte +++ b/web/src/lib/components/ToolPage.svelte @@ -12,6 +12,7 @@ import ToolSearch from './search/ToolSearch.svelte'; import { createAutoRunner } from '$lib/tools/auto-run'; import { executeStep } from '$lib/tools/executor'; + import { clearOverlay, setOverlay } from '$lib/tools/overlay-store.svelte'; import { t } from '$lib/i18n/t'; import { toolDescription, toolTitle } from '$lib/i18n/tool-strings'; import type { StageStatus } from './stage/stage-props'; @@ -51,6 +52,7 @@ let showMask = $state(false); let info = $state(null); let errorText = $state(''); + let overlayImage = $state(null); // svelte-ignore state_referenced_locally let values = $state>({ ...defaultParams(tool), ...presetBaseValues }); // svelte-ignore state_referenced_locally @@ -272,7 +274,16 @@ info = null; errorText = ''; status = 'idle'; - values = defaultParams(tool); + clearOverlay(); + values = { ...defaultParams(tool), ...presetBaseValues }; + } + + async function handleOverlayFile(file: File) { + try { + setOverlay(await decodeFile(file)); + } catch (e) { + errorText = errorMessage(e); + } } function handlePaste(event: ClipboardEvent) { @@ -332,6 +343,10 @@ handlePickColor={handlePickColor} showError={showError} errorMessage={errorMessage} + overlayImage={overlayImage} + onOverlayFile={(f) => void handleOverlayFile(f)} + onOverlayError={(e) => (errorText = errorMessage(e))} + onOverlayClear={clearOverlay} />
diff --git a/web/src/lib/components/stage/ToolStage.svelte b/web/src/lib/components/stage/ToolStage.svelte index dd660f1..d230c24 100644 --- a/web/src/lib/components/stage/ToolStage.svelte +++ b/web/src/lib/components/stage/ToolStage.svelte @@ -9,6 +9,7 @@ import DownloadButton from '../DownloadButton.svelte'; import Button from '../ui/Button.svelte'; import EmptyState from '../ui/EmptyState.svelte'; + import OverlayCard from '../tool/OverlayCard.svelte'; import ParamsCard from '../tool/ParamsCard.svelte'; import ResultCard from '../tool/ResultCard.svelte'; import SourceCard from '../tool/SourceCard.svelte'; @@ -43,6 +44,10 @@ handlePipetteToggle?: (id: string) => void; handlePickColor?: (hex: string) => void; showError?: (e: unknown) => void; + overlayImage?: PixelImage | null; + onOverlayFile?: (file: File) => void; + onOverlayError?: (e: unknown) => void; + onOverlayClear?: () => void; index?: number; input?: PixelImage | null; result?: PixelImage | null; @@ -81,6 +86,10 @@ handlePipetteToggle, handlePickColor, showError, + overlayImage = null, + onOverlayFile, + onOverlayError, + onOverlayClear, index = 0, input = null, result = null, @@ -138,6 +147,14 @@ pipetteActive={!!pipetteTargetId} onPickColor={(hex) => handlePickColor?.(hex)} /> + {#if tool.needsOverlaySource && source} + onOverlayFile?.(f)} + onError={(e) => onOverlayError?.(e)} + onClear={() => onOverlayClear?.()} + /> + {/if} {/if}
diff --git a/web/src/lib/components/tool/OverlayCard.svelte b/web/src/lib/components/tool/OverlayCard.svelte new file mode 100644 index 0000000..140eed3 --- /dev/null +++ b/web/src/lib/components/tool/OverlayCard.svelte @@ -0,0 +1,63 @@ + + +
+ + {#if !overlay} + + {:else} +
+ + +
+ {/if} +
+ + diff --git a/web/src/lib/core/domText.ts b/web/src/lib/core/domText.ts index 640f88b..51cca4f 100644 --- a/web/src/lib/core/domText.ts +++ b/web/src/lib/core/domText.ts @@ -92,6 +92,38 @@ export interface TileTextOptions extends Omit + drawTextTile(img, { + text: str(p, 'text'), + fontSize: num(p, 'fontSize'), + font: str(p, 'font') as TextFont, + bold: bool(p, 'bold'), + color: str(p, 'color'), + opacityPercent: num(p, 'opacity'), + stepX: num(p, 'stepX'), + stepY: num(p, 'stepY'), + angleDeg: num(p, 'angle') + }) + }, + { + id: 'watermark-image-png', + title: 'Watermark Image PNG', + description: + 'Overlays another PNG (logo/signature) on top: scale from canvas width, opacity, 3×3 position. The mark lives only while the page is open — after restoring a chain, pick it again.', + category: 'text', + domOnly: true, + needsOverlaySource: true, + params: [ + { id: 'scale', label: 'Mark width, % of canvas', type: 'slider', min: 5, max: 100, step: 1, default: 30 }, + { id: 'opacity', label: 'Opacity, %', type: 'slider', min: 5, max: 100, step: 5, default: 60 }, + { + id: 'position', + label: 'Position', + type: 'select', + default: 'bottom-right', + options: [ + { value: 'top-left', label: 'Top left' }, + { value: 'top-center', label: 'Top center' }, + { value: 'top-right', label: 'Top right' }, + { value: 'middle-left', label: 'Middle left' }, + { value: 'center', label: 'Center' }, + { value: 'middle-right', label: 'Middle right' }, + { value: 'bottom-left', label: 'Bottom left' }, + { value: 'bottom-center', label: 'Bottom center' }, + { value: 'bottom-right', label: 'Bottom right' } + ] + }, + { id: 'margin', label: 'Margin, px', type: 'slider', min: 0, max: 200, step: 1, default: 24 } + ], + run: (img, p) => { + const mark = getOverlay(); + if (!mark) throw new ToolError('errors.noWatermark'); + return drawImageWatermark(img, { + mark, + scalePercent: num(p, 'scale'), + opacityPercent: num(p, 'opacity'), + position: str(p, 'position') as Position9, + margin: num(p, 'margin') + }); + } + }, { id: 'png-is-grayscale', title: 'Check: is PNG grayscale?', diff --git a/web/src/lib/tools/overlay-store.svelte.ts b/web/src/lib/tools/overlay-store.svelte.ts new file mode 100644 index 0000000..b93586d --- /dev/null +++ b/web/src/lib/tools/overlay-store.svelte.ts @@ -0,0 +1,20 @@ +import type { PixelImage } from '$lib/core/types'; + +/** + * Картинка-знак для watermark-image-png. Живёт только в состоянии страницы: + * сознательно не сохраняется в pipeline/localStorage — после восстановления + * цепочки знак нужно выбрать заново. + */ +let overlay = $state(null); + +export function getOverlay(): PixelImage | null { + return overlay; +} + +export function setOverlay(img: PixelImage): void { + overlay = img; +} + +export function clearOverlay(): void { + overlay = null; +} diff --git a/web/src/lib/tools/tool-icons.ts b/web/src/lib/tools/tool-icons.ts index 230d458..dcdd78e 100644 --- a/web/src/lib/tools/tool-icons.ts +++ b/web/src/lib/tools/tool-icons.ts @@ -37,6 +37,7 @@ import { Scissors, SearchCheck, Square, + Stamp, Sun, Type, ZoomIn, @@ -98,6 +99,8 @@ export const TOOL_ICONS: Record = { 'add-stroke-png': Square, 'add-text-png': Type, 'date-stamp-png': CalendarDays, + 'watermark-tile-png': Stamp, + 'watermark-image-png': FileImage, 'find-contour-png': Scan, 'make-thicker-png': ZoomIn, 'make-thinner-png': ZoomOut,