diff --git a/web/src/lib/core/textio.ts b/web/src/lib/core/textio.ts index dd28d85..53221ed 100644 --- a/web/src/lib/core/textio.ts +++ b/web/src/lib/core/textio.ts @@ -85,7 +85,7 @@ export function stripDataUri(text: string): string { return m ? text.slice(m[0].length) : text.trim(); } -export function base64ToBytes(text: string): Uint8Array { +export function base64ToBytes(text: string): Uint8Array { const clean = text.replace(/\s+/g, ""); const binary = atob(clean); const bytes = new Uint8Array(binary.length); diff --git a/web/src/lib/registry-new/analyze.ts b/web/src/lib/registry-new/analyze.ts index 4a9047b..66b0cda 100644 --- a/web/src/lib/registry-new/analyze.ts +++ b/web/src/lib/registry-new/analyze.ts @@ -7,6 +7,9 @@ import { rarityPredicate, renderPredicateMask, } from "../core/masks"; +import { hasTransparency, isGrayscale, orientationOf } from "../core/analyze"; +import { encode } from "../core/io"; +import { base64ToBytes, looksLikePng, stripDataUri } from "../core/textio"; interface ExtractColorParams { color: string; @@ -176,6 +179,83 @@ const uniqueColorMask: ToolEntry = { run: (img, p) => renderMask(img, p, rarityPredicate(img, p.rarity)), }; +interface NoParams {} + +const emptySchema = toolSchema({}); + +const verifyIsPng: ToolEntry = { + id: "verify-is-png", + title: "Verify If Image Is a PNG", + description: + "Checks the signature of pasted base64 / data-uri content and reports whether it is a real PNG.", + category: "analyze", + schema: emptySchema, + input: "text", + result: "verdict", + textToText: (text) => + looksLikePng(base64ToBytes(stripDataUri(text))) + ? "Yes — valid PNG signature." + : "No — the content is not a PNG.", +}; + +const pngIsGrayscale: ToolEntry = { + id: "png-is-grayscale", + title: "Check: is PNG grayscale?", + description: "Reports whether the image consists only of shades of gray.", + category: "analyze", + schema: emptySchema, + result: "verdict", + toText: (img) => + isGrayscale(img) ? "Yes — grayscale." : "No — contains colors.", +}; + +const pngFileSize: ToolEntry = { + id: "png-file-size", + title: "PNG File Size", + description: "Encodes the image as PNG and reports the resulting file size.", + category: "analyze", + schema: emptySchema, + domOnly: true, + result: "verdict", + toText: 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 = { + id: "png-is-transparent", + title: "Check: is PNG transparent?", + description: + "Reports whether the image contains transparent or semi-transparent pixels.", + category: "analyze", + schema: emptySchema, + result: "verdict", + toText: (img) => + hasTransparency(img) ? "Yes — has transparency." : "No — fully opaque.", +}; + +const pngOrientation: ToolEntry = { + id: "png-orientation", + title: "PNG orientation", + description: "Reports whether it is portrait, landscape or square.", + category: "analyze", + schema: emptySchema, + result: "verdict", + toText: (img) => { + switch (orientationOf(img)) { + case "portrait": + return "Portrait"; + case "landscape": + return "Landscape"; + default: + return "Square"; + } + }, +}; + export const analyzeEntries = [ extractColor, showTransparent, @@ -184,4 +264,9 @@ export const analyzeEntries = [ lightPixelMask, darkPixelMask, uniqueColorMask, + verifyIsPng, + pngIsGrayscale, + pngFileSize, + pngIsTransparent, + pngOrientation, ]; diff --git a/web/src/lib/registry-new/convert.ts b/web/src/lib/registry-new/convert.ts index c154d09..3b1a8a7 100644 --- a/web/src/lib/registry-new/convert.ts +++ b/web/src/lib/registry-new/convert.ts @@ -1,7 +1,20 @@ import type { ToolEntry } from "./types"; import { field, toolSchema } from "../registry-schema"; import { flattenOntoColor } from "../core/alpha"; +import { + base64ToBytes, + bytesToImage, + imageToByteRows, + imageToRgbValues, + rgbValuesToImage, + stripDataUri, +} from "../core/textio"; +import { hexToPixels, pixelsToHex } from "../core/text"; import { clonePixelImage } from "../core/types"; +import { decodeBytes, decodeSvgText, toBase64, toDataUrl } from "../core/io"; + +const widthProps = (defaultValue: number) => + field.number({ min: 1, max: 10000, step: 1, default: defaultValue }); interface ConvertToJpgParams { background: string; @@ -58,4 +71,175 @@ const convertToBmp: ToolEntry = { output: { mime: "image/bmp", ext: "bmp" }, }; -export const convertEntries = [convertToJpg, convertToWebp, convertToBmp]; +interface NoParams {} + +const emptySchema = toolSchema({}); + +const pngToBase64: ToolEntry = { + id: "png-to-base64", + title: "PNG to Base64", + description: + "Encodes the image into a base64 string for embedding in code or styles.", + category: "convert", + schema: emptySchema, + result: "text", + toText: (img) => toBase64(img), +}; + +const pngToDataUri: ToolEntry = { + id: "png-to-data-uri", + title: "PNG to Data URI", + description: + "Builds a full data-uri (data:image/png;base64,…) for embedding in HTML/CSS.", + category: "convert", + schema: emptySchema, + result: "text", + toText: (img) => toDataUrl(img), +}; + +const pngToHex: ToolEntry = { + id: "png-to-hex", + title: "PNG to HEX pixels", + description: + "Shows all pixels as rrggbbaa hex values — row by row, space separated.", + category: "convert", + schema: emptySchema, + result: "text", + toText: (img) => pixelsToHex(img), +}; + +const pngToBytes: ToolEntry = { + id: "png-to-bytes", + title: "PNG to Bytes", + description: + "Lists every pixel as four decimal bytes (R G B A), one image row per line.", + category: "convert", + schema: emptySchema, + result: "text", + toText: (img) => imageToByteRows(img), +}; + +const pngToRgbValues: ToolEntry = { + id: "png-to-rgb-values", + title: "PNG to RGB Values", + description: "Lists every pixel as rgba(r, g, b, a), one image row per line.", + category: "convert", + schema: emptySchema, + result: "text", + toText: (img) => imageToRgbValues(img), +}; + +const base64ToPng: ToolEntry = { + id: "base64-to-png", + title: "Base64 to PNG", + description: + "Decodes a base64 string or data-uri back into an image. Paste the string on the left.", + category: "convert", + schema: emptySchema, + input: "text", + runFromText: async (text) => decodeBytes(base64ToBytes(stripDataUri(text))), +}; + +const dataUriToPng: ToolEntry = { + id: "data-uri-to-png", + title: "Data URI to PNG", + description: "Decodes data:image/…;base64,… back into an image file.", + category: "convert", + schema: emptySchema, + input: "text", + runFromText: async (text) => decodeBytes(base64ToBytes(stripDataUri(text))), +}; + +interface HexToPngParams { + width: number; +} + +export const hexToPngSchema = toolSchema({ + width: widthProps(1), +}); + +const hexToPng: ToolEntry = { + id: "hex-to-png", + title: "HEX pixels to PNG", + description: + "Assembles an image from rrggbbaa hex values (space separated). Set the width — the height is computed automatically.", + category: "convert", + schema: hexToPngSchema, + input: "text", + runFromText: (text, p) => hexToPixels(text, Math.trunc(p.width)), +}; + +interface BytesToPngParams { + width: number; +} + +export const bytesToPngSchema = toolSchema({ + width: widthProps(32), +}); + +const bytesToPng: ToolEntry = { + id: "bytes-to-png", + title: "Bytes to PNG", + description: + "Assembles an image from decimal RGBA byte numbers (any separators). Set the width — height is computed automatically.", + category: "convert", + schema: bytesToPngSchema, + input: "text", + runFromText: (text, p) => bytesToImage(text, Math.trunc(p.width)), +}; + +interface RgbValuesToPngParams { + width: number; +} + +export const rgbValuesToPngSchema = toolSchema({ + width: widthProps(32), +}); + +const rgbValuesToPng: ToolEntry = { + id: "rgb-values-to-png", + title: "RGB Values to PNG", + description: + "Assembles an image from rgba(r, g, b, a) numbers. Set the width — height is computed automatically.", + category: "convert", + schema: rgbValuesToPngSchema, + input: "text", + runFromText: (text, p) => rgbValuesToImage(text, Math.trunc(p.width)), +}; + +interface SvgToPngParams { + width: number; +} + +export const svgToPngSchema = toolSchema({ + width: widthProps(512), +}); + +const svgToPng: ToolEntry = { + id: "svg-to-png", + title: "SVG to PNG", + description: + "Decodes SVG markup into a raster image. Paste the SVG code on the left.", + category: "convert", + schema: svgToPngSchema, + input: "text", + domOnly: true, + runFromText: (text, p) => decodeSvgText(text, Math.trunc(p.width)), +}; + +export const convertEntries = [ + convertToJpg, + convertToWebp, + convertToBmp, + pngToBase64, + pngToDataUri, + pngToHex, + pngToBytes, + pngToRgbValues, + base64ToPng, + dataUriToPng, + hexToPng, + bytesToPng, + rgbValuesToPng, + svgToPng, +];