feat: update convert and analyze tools to new types

This commit is contained in:
2026-09-10 12:01:54 +05:00
parent 6b0b0646d5
commit 60c7d61dab
3 changed files with 66 additions and 40 deletions
+33 -15
View File
@@ -1,4 +1,4 @@
import type { ToolEntry } from "./types"; import { imgTool, textGen, type ToolEntry } from "./types";
import { field, toolSchema } from "../registry-schema"; import { field, toolSchema } from "../registry-schema";
import { import {
extractByColor, extractByColor,
@@ -28,7 +28,8 @@ const extractColor: ToolEntry<ExtractColorParams> = {
"Keeps only pixels close to the chosen color and makes everything else transparent — the inverse of Remove Color.", "Keeps only pixels close to the chosen color and makes everything else transparent — the inverse of Remove Color.",
category: "analyze", category: "analyze",
schema: extractColorSchema, schema: extractColorSchema,
run: (img, p) => extractByColor(img, p.color, p.tolerance), input: "image",
run: imgTool((img, p) => extractByColor(img, p.color, p.tolerance)),
}; };
interface MaskParams { interface MaskParams {
@@ -79,7 +80,8 @@ const showTransparent: ToolEntry<MaskParams> = {
"Highlights every transparent or semi-transparent pixel with the chosen color so gaps become obvious.", "Highlights every transparent or semi-transparent pixel with the chosen color so gaps become obvious.",
category: "analyze", category: "analyze",
schema: showTransparentSchema, schema: showTransparentSchema,
run: (img, p) => renderMask(img, p, (_r, _g, _b, a) => a < 255), input: "image",
run: imgTool((img, p) => renderMask(img, p, (_r, _g, _b, a) => a < 255)),
}; };
interface GrayscalePixelsParams extends MaskParams { interface GrayscalePixelsParams extends MaskParams {
@@ -98,8 +100,10 @@ const showGrayscalePixels: ToolEntry<GrayscalePixelsParams> = {
"Finds pixels whose channels are nearly equal and renders them as a mask. Tolerance is in channel units.", "Finds pixels whose channels are nearly equal and renders them as a mask. Tolerance is in channel units.",
category: "analyze", category: "analyze",
schema: showGrayscalePixelsSchema, schema: showGrayscalePixelsSchema,
run: (img, p) => input: "image",
run: imgTool((img, p) =>
renderMask(img, p, (r, g, b) => isGrayscaleish(r, g, b, p.tolerance)), renderMask(img, p, (r, g, b) => isGrayscaleish(r, g, b, p.tolerance)),
),
}; };
interface ColorPixelsParams extends MaskParams { interface ColorPixelsParams extends MaskParams {
@@ -118,8 +122,10 @@ const showColorPixels: ToolEntry<ColorPixelsParams> = {
"Finds colored (non-gray) pixels beyond the channel tolerance and renders them as a mask.", "Finds colored (non-gray) pixels beyond the channel tolerance and renders them as a mask.",
category: "analyze", category: "analyze",
schema: showColorPixelsSchema, schema: showColorPixelsSchema,
run: (img, p) => input: "image",
run: imgTool((img, p) =>
renderMask(img, p, (r, g, b) => !isGrayscaleish(r, g, b, p.tolerance)), renderMask(img, p, (r, g, b) => !isGrayscaleish(r, g, b, p.tolerance)),
),
}; };
interface LightPixelParams extends MaskParams { interface LightPixelParams extends MaskParams {
@@ -137,8 +143,10 @@ const lightPixelMask: ToolEntry<LightPixelParams> = {
description: "Selects pixels brighter than the luminance threshold.", description: "Selects pixels brighter than the luminance threshold.",
category: "analyze", category: "analyze",
schema: lightPixelMaskSchema, schema: lightPixelMaskSchema,
run: (img, p) => input: "image",
run: imgTool((img, p) =>
renderMask(img, p, (r, g, b) => luma01(r, g, b) >= p.threshold / 100), renderMask(img, p, (r, g, b) => luma01(r, g, b) >= p.threshold / 100),
),
}; };
interface DarkPixelParams extends MaskParams { interface DarkPixelParams extends MaskParams {
@@ -156,8 +164,10 @@ const darkPixelMask: ToolEntry<DarkPixelParams> = {
description: "Selects pixels darker than the luminance threshold.", description: "Selects pixels darker than the luminance threshold.",
category: "analyze", category: "analyze",
schema: darkPixelMaskSchema, schema: darkPixelMaskSchema,
run: (img, p) => input: "image",
run: imgTool((img, p) =>
renderMask(img, p, (r, g, b) => luma01(r, g, b) <= p.threshold / 100), renderMask(img, p, (r, g, b) => luma01(r, g, b) <= p.threshold / 100),
),
}; };
interface UniqueColorParams extends MaskParams { interface UniqueColorParams extends MaskParams {
@@ -176,7 +186,8 @@ const uniqueColorMask: ToolEntry<UniqueColorParams> = {
"Selects colors that occur no more than the given number of times — rare and one-off pixels.", "Selects colors that occur no more than the given number of times — rare and one-off pixels.",
category: "analyze", category: "analyze",
schema: uniqueColorMaskSchema, schema: uniqueColorMaskSchema,
run: (img, p) => renderMask(img, p, rarityPredicate(img, p.rarity)), input: "image",
run: imgTool((img, p) => renderMask(img, p, rarityPredicate(img, p.rarity))),
}; };
interface NoParams {} interface NoParams {}
@@ -192,10 +203,11 @@ const verifyIsPng: ToolEntry<NoParams> = {
schema: emptySchema, schema: emptySchema,
input: "text", input: "text",
result: "verdict", result: "verdict",
textToText: (text) => run: textGen((text) =>
looksLikePng(base64ToBytes(stripDataUri(text))) looksLikePng(base64ToBytes(stripDataUri(text)))
? "Yes — valid PNG signature." ? "Yes — valid PNG signature."
: "No — the content is not a PNG.", : "No — the content is not a PNG.",
),
}; };
const pngIsGrayscale: ToolEntry<NoParams> = { const pngIsGrayscale: ToolEntry<NoParams> = {
@@ -204,9 +216,11 @@ const pngIsGrayscale: ToolEntry<NoParams> = {
description: "Reports whether the image consists only of shades of gray.", description: "Reports whether the image consists only of shades of gray.",
category: "analyze", category: "analyze",
schema: emptySchema, schema: emptySchema,
input: "image",
result: "verdict", result: "verdict",
toText: (img) => run: imgTool((img) =>
isGrayscale(img) ? "Yes — grayscale." : "No — contains colors.", isGrayscale(img) ? "Yes — grayscale." : "No — contains colors.",
),
}; };
const pngFileSize: ToolEntry<NoParams> = { const pngFileSize: ToolEntry<NoParams> = {
@@ -216,13 +230,14 @@ const pngFileSize: ToolEntry<NoParams> = {
category: "analyze", category: "analyze",
schema: emptySchema, schema: emptySchema,
domOnly: true, domOnly: true,
input: "image",
result: "verdict", result: "verdict",
toText: async (img) => { run: imgTool(async (img) => {
const blob = await encode(img, "image/png"); const blob = await encode(img, "image/png");
const kb = blob.size / 1024; const kb = blob.size / 1024;
const kbText = kb >= 100 ? Math.round(kb).toString() : kb.toFixed(1); const kbText = kb >= 100 ? Math.round(kb).toString() : kb.toFixed(1);
return `${kbText} KB`; return `${kbText} KB`;
}, }),
}; };
const pngIsTransparent: ToolEntry<NoParams> = { const pngIsTransparent: ToolEntry<NoParams> = {
@@ -232,9 +247,11 @@ const pngIsTransparent: ToolEntry<NoParams> = {
"Reports whether the image contains transparent or semi-transparent pixels.", "Reports whether the image contains transparent or semi-transparent pixels.",
category: "analyze", category: "analyze",
schema: emptySchema, schema: emptySchema,
input: "image",
result: "verdict", result: "verdict",
toText: (img) => run: imgTool((img) =>
hasTransparency(img) ? "Yes — has transparency." : "No — fully opaque.", hasTransparency(img) ? "Yes — has transparency." : "No — fully opaque.",
),
}; };
const pngOrientation: ToolEntry<NoParams> = { const pngOrientation: ToolEntry<NoParams> = {
@@ -243,8 +260,9 @@ const pngOrientation: ToolEntry<NoParams> = {
description: "Reports whether it is portrait, landscape or square.", description: "Reports whether it is portrait, landscape or square.",
category: "analyze", category: "analyze",
schema: emptySchema, schema: emptySchema,
input: "image",
result: "verdict", result: "verdict",
toText: (img) => { run: imgTool((img) => {
switch (orientationOf(img)) { switch (orientationOf(img)) {
case "portrait": case "portrait":
return "Portrait"; return "Portrait";
@@ -253,7 +271,7 @@ const pngOrientation: ToolEntry<NoParams> = {
default: default:
return "Square"; return "Square";
} }
}, }),
}; };
export const analyzeEntries = [ export const analyzeEntries = [
+23 -15
View File
@@ -1,4 +1,4 @@
import type { ToolEntry } from "./types"; import { imgTool, textGen, type ToolEntry } from "./types";
import { field, toolSchema } from "../registry-schema"; import { field, toolSchema } from "../registry-schema";
import { flattenOntoColor } from "../core/alpha"; import { flattenOntoColor } from "../core/alpha";
import { import {
@@ -33,7 +33,8 @@ const convertToJpg: ToolEntry<ConvertToJpgParams> = {
"Transparency is composited over the chosen backdrop color (white by default) and saved as JPEG.", "Transparency is composited over the chosen backdrop color (white by default) and saved as JPEG.",
category: "convert", category: "convert",
schema: convertToJpgSchema, schema: convertToJpgSchema,
run: (img, p) => flattenOntoColor(img, p.background), input: "image",
run: imgTool((img, p) => flattenOntoColor(img, p.background)),
output: { mime: "image/jpeg", ext: "jpg", qualityParamId: "quality" }, output: { mime: "image/jpeg", ext: "jpg", qualityParamId: "quality" },
}; };
@@ -52,7 +53,8 @@ const convertToWebp: ToolEntry<ConvertToWebpParams> = {
"Re-encodes the image into WebP with adjustable quality. Transparency is preserved.", "Re-encodes the image into WebP with adjustable quality. Transparency is preserved.",
category: "convert", category: "convert",
schema: convertToWebpSchema, schema: convertToWebpSchema,
run: (img) => clonePixelImage(img), input: "image",
run: imgTool((img) => clonePixelImage(img)),
output: { mime: "image/webp", ext: "webp", qualityParamId: "quality" }, output: { mime: "image/webp", ext: "webp", qualityParamId: "quality" },
}; };
@@ -67,7 +69,8 @@ const convertToBmp: ToolEntry<ConvertToBmpParams> = {
"Saves the image as 24-bit BMP without an alpha channel: transparency is replaced with a black background.", "Saves the image as 24-bit BMP without an alpha channel: transparency is replaced with a black background.",
category: "convert", category: "convert",
schema: convertToBmpSchema, schema: convertToBmpSchema,
run: (img) => flattenOntoColor(img, "#000000"), input: "image",
run: imgTool((img) => flattenOntoColor(img, "#000000")),
output: { mime: "image/bmp", ext: "bmp" }, output: { mime: "image/bmp", ext: "bmp" },
}; };
@@ -82,8 +85,9 @@ const pngToBase64: ToolEntry<NoParams> = {
"Encodes the image into a base64 string for embedding in code or styles.", "Encodes the image into a base64 string for embedding in code or styles.",
category: "convert", category: "convert",
schema: emptySchema, schema: emptySchema,
input: "image",
result: "text", result: "text",
toText: (img) => toBase64(img), run: imgTool((img) => toBase64(img)),
}; };
const pngToDataUri: ToolEntry<NoParams> = { const pngToDataUri: ToolEntry<NoParams> = {
@@ -93,8 +97,9 @@ const pngToDataUri: ToolEntry<NoParams> = {
"Builds a full data-uri (data:image/png;base64,…) for embedding in HTML/CSS.", "Builds a full data-uri (data:image/png;base64,…) for embedding in HTML/CSS.",
category: "convert", category: "convert",
schema: emptySchema, schema: emptySchema,
input: "image",
result: "text", result: "text",
toText: (img) => toDataUrl(img), run: imgTool((img) => toDataUrl(img)),
}; };
const pngToHex: ToolEntry<NoParams> = { const pngToHex: ToolEntry<NoParams> = {
@@ -104,8 +109,9 @@ const pngToHex: ToolEntry<NoParams> = {
"Shows all pixels as rrggbbaa hex values — row by row, space separated.", "Shows all pixels as rrggbbaa hex values — row by row, space separated.",
category: "convert", category: "convert",
schema: emptySchema, schema: emptySchema,
input: "image",
result: "text", result: "text",
toText: (img) => pixelsToHex(img), run: imgTool((img) => pixelsToHex(img)),
}; };
const pngToBytes: ToolEntry<NoParams> = { const pngToBytes: ToolEntry<NoParams> = {
@@ -115,8 +121,9 @@ const pngToBytes: ToolEntry<NoParams> = {
"Lists every pixel as four decimal bytes (R G B A), one image row per line.", "Lists every pixel as four decimal bytes (R G B A), one image row per line.",
category: "convert", category: "convert",
schema: emptySchema, schema: emptySchema,
input: "image",
result: "text", result: "text",
toText: (img) => imageToByteRows(img), run: imgTool((img) => imageToByteRows(img)),
}; };
const pngToRgbValues: ToolEntry<NoParams> = { const pngToRgbValues: ToolEntry<NoParams> = {
@@ -125,8 +132,9 @@ const pngToRgbValues: ToolEntry<NoParams> = {
description: "Lists every pixel as rgba(r, g, b, a), one image row per line.", description: "Lists every pixel as rgba(r, g, b, a), one image row per line.",
category: "convert", category: "convert",
schema: emptySchema, schema: emptySchema,
input: "image",
result: "text", result: "text",
toText: (img) => imageToRgbValues(img), run: imgTool((img) => imageToRgbValues(img)),
}; };
const base64ToPng: ToolEntry<NoParams> = { const base64ToPng: ToolEntry<NoParams> = {
@@ -137,7 +145,7 @@ const base64ToPng: ToolEntry<NoParams> = {
category: "convert", category: "convert",
schema: emptySchema, schema: emptySchema,
input: "text", input: "text",
runFromText: async (text) => decodeBytes(base64ToBytes(stripDataUri(text))), run: textGen((text) => decodeBytes(base64ToBytes(stripDataUri(text)))),
}; };
const dataUriToPng: ToolEntry<NoParams> = { const dataUriToPng: ToolEntry<NoParams> = {
@@ -147,7 +155,7 @@ const dataUriToPng: ToolEntry<NoParams> = {
category: "convert", category: "convert",
schema: emptySchema, schema: emptySchema,
input: "text", input: "text",
runFromText: async (text) => decodeBytes(base64ToBytes(stripDataUri(text))), run: textGen((text) => decodeBytes(base64ToBytes(stripDataUri(text)))),
}; };
interface HexToPngParams { interface HexToPngParams {
@@ -166,7 +174,7 @@ const hexToPng: ToolEntry<HexToPngParams> = {
category: "convert", category: "convert",
schema: hexToPngSchema, schema: hexToPngSchema,
input: "text", input: "text",
runFromText: (text, p) => hexToPixels(text, Math.trunc(p.width)), run: textGen((text, p) => hexToPixels(text, Math.trunc(p.width))),
}; };
interface BytesToPngParams { interface BytesToPngParams {
@@ -185,7 +193,7 @@ const bytesToPng: ToolEntry<BytesToPngParams> = {
category: "convert", category: "convert",
schema: bytesToPngSchema, schema: bytesToPngSchema,
input: "text", input: "text",
runFromText: (text, p) => bytesToImage(text, Math.trunc(p.width)), run: textGen((text, p) => bytesToImage(text, Math.trunc(p.width))),
}; };
interface RgbValuesToPngParams { interface RgbValuesToPngParams {
@@ -204,7 +212,7 @@ const rgbValuesToPng: ToolEntry<RgbValuesToPngParams> = {
category: "convert", category: "convert",
schema: rgbValuesToPngSchema, schema: rgbValuesToPngSchema,
input: "text", input: "text",
runFromText: (text, p) => rgbValuesToImage(text, Math.trunc(p.width)), run: textGen((text, p) => rgbValuesToImage(text, Math.trunc(p.width))),
}; };
interface SvgToPngParams { interface SvgToPngParams {
@@ -224,7 +232,7 @@ const svgToPng: ToolEntry<SvgToPngParams> = {
schema: svgToPngSchema, schema: svgToPngSchema,
input: "text", input: "text",
domOnly: true, domOnly: true,
runFromText: (text, p) => decodeSvgText(text, Math.trunc(p.width)), run: textGen((text, p) => decodeSvgText(text, Math.trunc(p.width))),
}; };
export const convertEntries = [ export const convertEntries = [
+9 -9
View File
@@ -1,8 +1,8 @@
import { ToolError } from "../core/errors";
import type { OutputMime } from "../core/io";
import type { PixelImage } from "../core/types";
import type { CategoryId } from "../preview/categories"; import type { CategoryId } from "../preview/categories";
import type { ToolSchema } from "../registry-schema"; import type { ToolSchema } from "../registry-schema";
import type { PixelImage } from "../core/types";
import type { OutputMime } from "../core/io";
import { ToolError } from "../core/errors";
/** Формат скачивания, отличный от PNG (bmp/jpeg/webp). */ /** Формат скачивания, отличный от PNG (bmp/jpeg/webp). */
export interface OutputFormat { export interface OutputFormat {
@@ -83,21 +83,21 @@ export function requireText<P>(ctx: ToolContext<P>): string {
* из контекста. Позволяет оставить тело инструмента в виде `(img, p) => …`. * из контекста. Позволяет оставить тело инструмента в виде `(img, p) => …`.
*/ */
export function imgTool<P>( export function imgTool<P>(
fn: (img: PixelImage, params: P) => ToolResult, fn: (img: PixelImage, params: P) => ToolResult | Promise<ToolResult>,
): (ctx: ToolContext<P>) => ToolResult { ): (ctx: ToolContext<P>) => ToolResult | Promise<ToolResult> {
return (ctx) => fn(requireSource(ctx), ctx.params); return (ctx) => fn(requireSource(ctx), ctx.params);
} }
/** Обёртка для генераторов: инструменту нужны только параметры. */ /** Обёртка для генераторов: инструменту нужны только параметры. */
export function genTool<P>( export function genTool<P>(
fn: (params: P) => ToolResult, fn: (params: P) => ToolResult | Promise<ToolResult>,
): (ctx: ToolContext<P>) => ToolResult { ): (ctx: ToolContext<P>) => ToolResult | Promise<ToolResult> {
return (ctx) => fn(ctx.params); return (ctx) => fn(ctx.params);
} }
/** Обёртка для text-to-image инструментов: подставляет `text` и `params`. */ /** Обёртка для text-to-image инструментов: подставляет `text` и `params`. */
export function textGen<P>( export function textGen<P>(
fn: (text: string, params: P) => ToolResult, fn: (text: string, params: P) => ToolResult | Promise<ToolResult>,
): (ctx: ToolContext<P>) => ToolResult { ): (ctx: ToolContext<P>) => ToolResult | Promise<ToolResult> {
return (ctx) => fn(requireText(ctx), ctx.params); return (ctx) => fn(requireText(ctx), ctx.params);
} }