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 {
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.",
category: "analyze",
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 {
@@ -79,7 +80,8 @@ const showTransparent: ToolEntry<MaskParams> = {
"Highlights every transparent or semi-transparent pixel with the chosen color so gaps become obvious.",
category: "analyze",
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 {
@@ -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.",
category: "analyze",
schema: showGrayscalePixelsSchema,
run: (img, p) =>
input: "image",
run: imgTool((img, p) =>
renderMask(img, p, (r, g, b) => isGrayscaleish(r, g, b, p.tolerance)),
),
};
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.",
category: "analyze",
schema: showColorPixelsSchema,
run: (img, p) =>
input: "image",
run: imgTool((img, p) =>
renderMask(img, p, (r, g, b) => !isGrayscaleish(r, g, b, p.tolerance)),
),
};
interface LightPixelParams extends MaskParams {
@@ -137,8 +143,10 @@ const lightPixelMask: ToolEntry<LightPixelParams> = {
description: "Selects pixels brighter than the luminance threshold.",
category: "analyze",
schema: lightPixelMaskSchema,
run: (img, p) =>
input: "image",
run: imgTool((img, p) =>
renderMask(img, p, (r, g, b) => luma01(r, g, b) >= p.threshold / 100),
),
};
interface DarkPixelParams extends MaskParams {
@@ -156,8 +164,10 @@ const darkPixelMask: ToolEntry<DarkPixelParams> = {
description: "Selects pixels darker than the luminance threshold.",
category: "analyze",
schema: darkPixelMaskSchema,
run: (img, p) =>
input: "image",
run: imgTool((img, p) =>
renderMask(img, p, (r, g, b) => luma01(r, g, b) <= p.threshold / 100),
),
};
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.",
category: "analyze",
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 {}
@@ -192,10 +203,11 @@ const verifyIsPng: ToolEntry<NoParams> = {
schema: emptySchema,
input: "text",
result: "verdict",
textToText: (text) =>
run: textGen((text) =>
looksLikePng(base64ToBytes(stripDataUri(text)))
? "Yes — valid PNG signature."
: "No — the content is not a PNG.",
),
};
const pngIsGrayscale: ToolEntry<NoParams> = {
@@ -204,9 +216,11 @@ const pngIsGrayscale: ToolEntry<NoParams> = {
description: "Reports whether the image consists only of shades of gray.",
category: "analyze",
schema: emptySchema,
input: "image",
result: "verdict",
toText: (img) =>
run: imgTool((img) =>
isGrayscale(img) ? "Yes — grayscale." : "No — contains colors.",
),
};
const pngFileSize: ToolEntry<NoParams> = {
@@ -216,13 +230,14 @@ const pngFileSize: ToolEntry<NoParams> = {
category: "analyze",
schema: emptySchema,
domOnly: true,
input: "image",
result: "verdict",
toText: async (img) => {
run: imgTool(async (img) => {
const blob = await encode(img, "image/png");
const kb = blob.size / 1024;
const kbText = kb >= 100 ? Math.round(kb).toString() : kb.toFixed(1);
return `${kbText} KB`;
},
}),
};
const pngIsTransparent: ToolEntry<NoParams> = {
@@ -232,9 +247,11 @@ const pngIsTransparent: ToolEntry<NoParams> = {
"Reports whether the image contains transparent or semi-transparent pixels.",
category: "analyze",
schema: emptySchema,
input: "image",
result: "verdict",
toText: (img) =>
run: imgTool((img) =>
hasTransparency(img) ? "Yes — has transparency." : "No — fully opaque.",
),
};
const pngOrientation: ToolEntry<NoParams> = {
@@ -243,8 +260,9 @@ const pngOrientation: ToolEntry<NoParams> = {
description: "Reports whether it is portrait, landscape or square.",
category: "analyze",
schema: emptySchema,
input: "image",
result: "verdict",
toText: (img) => {
run: imgTool((img) => {
switch (orientationOf(img)) {
case "portrait":
return "Portrait";
@@ -253,7 +271,7 @@ const pngOrientation: ToolEntry<NoParams> = {
default:
return "Square";
}
},
}),
};
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 { flattenOntoColor } from "../core/alpha";
import {
@@ -33,7 +33,8 @@ const convertToJpg: ToolEntry<ConvertToJpgParams> = {
"Transparency is composited over the chosen backdrop color (white by default) and saved as JPEG.",
category: "convert",
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" },
};
@@ -52,7 +53,8 @@ const convertToWebp: ToolEntry<ConvertToWebpParams> = {
"Re-encodes the image into WebP with adjustable quality. Transparency is preserved.",
category: "convert",
schema: convertToWebpSchema,
run: (img) => clonePixelImage(img),
input: "image",
run: imgTool((img) => clonePixelImage(img)),
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.",
category: "convert",
schema: convertToBmpSchema,
run: (img) => flattenOntoColor(img, "#000000"),
input: "image",
run: imgTool((img) => flattenOntoColor(img, "#000000")),
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.",
category: "convert",
schema: emptySchema,
input: "image",
result: "text",
toText: (img) => toBase64(img),
run: imgTool((img) => toBase64(img)),
};
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.",
category: "convert",
schema: emptySchema,
input: "image",
result: "text",
toText: (img) => toDataUrl(img),
run: imgTool((img) => toDataUrl(img)),
};
const pngToHex: ToolEntry<NoParams> = {
@@ -104,8 +109,9 @@ const pngToHex: ToolEntry<NoParams> = {
"Shows all pixels as rrggbbaa hex values — row by row, space separated.",
category: "convert",
schema: emptySchema,
input: "image",
result: "text",
toText: (img) => pixelsToHex(img),
run: imgTool((img) => pixelsToHex(img)),
};
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.",
category: "convert",
schema: emptySchema,
input: "image",
result: "text",
toText: (img) => imageToByteRows(img),
run: imgTool((img) => imageToByteRows(img)),
};
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.",
category: "convert",
schema: emptySchema,
input: "image",
result: "text",
toText: (img) => imageToRgbValues(img),
run: imgTool((img) => imageToRgbValues(img)),
};
const base64ToPng: ToolEntry<NoParams> = {
@@ -137,7 +145,7 @@ const base64ToPng: ToolEntry<NoParams> = {
category: "convert",
schema: emptySchema,
input: "text",
runFromText: async (text) => decodeBytes(base64ToBytes(stripDataUri(text))),
run: textGen((text) => decodeBytes(base64ToBytes(stripDataUri(text)))),
};
const dataUriToPng: ToolEntry<NoParams> = {
@@ -147,7 +155,7 @@ const dataUriToPng: ToolEntry<NoParams> = {
category: "convert",
schema: emptySchema,
input: "text",
runFromText: async (text) => decodeBytes(base64ToBytes(stripDataUri(text))),
run: textGen((text) => decodeBytes(base64ToBytes(stripDataUri(text)))),
};
interface HexToPngParams {
@@ -166,7 +174,7 @@ const hexToPng: ToolEntry<HexToPngParams> = {
category: "convert",
schema: hexToPngSchema,
input: "text",
runFromText: (text, p) => hexToPixels(text, Math.trunc(p.width)),
run: textGen((text, p) => hexToPixels(text, Math.trunc(p.width))),
};
interface BytesToPngParams {
@@ -185,7 +193,7 @@ const bytesToPng: ToolEntry<BytesToPngParams> = {
category: "convert",
schema: bytesToPngSchema,
input: "text",
runFromText: (text, p) => bytesToImage(text, Math.trunc(p.width)),
run: textGen((text, p) => bytesToImage(text, Math.trunc(p.width))),
};
interface RgbValuesToPngParams {
@@ -204,7 +212,7 @@ const rgbValuesToPng: ToolEntry<RgbValuesToPngParams> = {
category: "convert",
schema: rgbValuesToPngSchema,
input: "text",
runFromText: (text, p) => rgbValuesToImage(text, Math.trunc(p.width)),
run: textGen((text, p) => rgbValuesToImage(text, Math.trunc(p.width))),
};
interface SvgToPngParams {
@@ -224,7 +232,7 @@ const svgToPng: ToolEntry<SvgToPngParams> = {
schema: svgToPngSchema,
input: "text",
domOnly: true,
runFromText: (text, p) => decodeSvgText(text, Math.trunc(p.width)),
run: textGen((text, p) => decodeSvgText(text, Math.trunc(p.width))),
};
export const convertEntries = [
+10 -10
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 { 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). */
export interface OutputFormat {
@@ -83,21 +83,21 @@ export function requireText<P>(ctx: ToolContext<P>): string {
* из контекста. Позволяет оставить тело инструмента в виде `(img, p) => …`.
*/
export function imgTool<P>(
fn: (img: PixelImage, params: P) => ToolResult,
): (ctx: ToolContext<P>) => ToolResult {
fn: (img: PixelImage, params: P) => ToolResult | Promise<ToolResult>,
): (ctx: ToolContext<P>) => ToolResult | Promise<ToolResult> {
return (ctx) => fn(requireSource(ctx), ctx.params);
}
/** Обёртка для генераторов: инструменту нужны только параметры. */
export function genTool<P>(
fn: (params: P) => ToolResult,
): (ctx: ToolContext<P>) => ToolResult {
fn: (params: P) => ToolResult | Promise<ToolResult>,
): (ctx: ToolContext<P>) => ToolResult | Promise<ToolResult> {
return (ctx) => fn(ctx.params);
}
/** Обёртка для text-to-image инструментов: подставляет `text` и `params`. */
export function textGen<P>(
fn: (text: string, params: P) => ToolResult,
): (ctx: ToolContext<P>) => ToolResult {
fn: (text: string, params: P) => ToolResult | Promise<ToolResult>,
): (ctx: ToolContext<P>) => ToolResult | Promise<ToolResult> {
return (ctx) => fn(requireText(ctx), ctx.params);
}
}