mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
294 lines
6.9 KiB
TypeScript
294 lines
6.9 KiB
TypeScript
import type { ToolEntry } from "../registry";
|
|
import {
|
|
extractByColor,
|
|
isGrayscaleish,
|
|
luma01,
|
|
rarityPredicate,
|
|
renderPredicateMask,
|
|
} from "../../core/masks";
|
|
import {
|
|
hasTransparency,
|
|
isGrayscale,
|
|
orientationOf,
|
|
} from "../../core/analyze";
|
|
import { looksLikePng, base64ToBytes, stripDataUri } from "../../core/textio";
|
|
import { clonePixelImage, type PixelImage } from "../../core/types";
|
|
import { encode } from "../../core/io";
|
|
import { t } from "../i18n/t";
|
|
import { num, str } from "../registry-helpers";
|
|
|
|
type MaskToolSpec = {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
defaultMode: "binary" | "highlight";
|
|
predicate: (
|
|
img: PixelImage,
|
|
p: Record<string, unknown>,
|
|
) => (r: number, g: number, b: number, a: number) => boolean;
|
|
extraParams?: ToolEntry["params"];
|
|
};
|
|
|
|
const MASK_TOOLS: MaskToolSpec[] = [
|
|
{
|
|
id: "show-transparent-png",
|
|
title: "Show Transparent Areas PNG",
|
|
description:
|
|
"Highlights every transparent or semi-transparent pixel with the chosen color so gaps become obvious.",
|
|
defaultMode: "highlight",
|
|
predicate: (_img, _p) => (_r, _g, _b, a) => a < 255,
|
|
},
|
|
{
|
|
id: "show-grayscale-pixels-png",
|
|
title: "Show Grayscale Pixels PNG",
|
|
description:
|
|
"Finds pixels whose channels are nearly equal and renders them as a mask. Tolerance is in channel units.",
|
|
defaultMode: "binary",
|
|
predicate: (_i, p) => (r, g, b) =>
|
|
isGrayscaleish(r, g, b, num(p, "tolerance")),
|
|
extraParams: [
|
|
{
|
|
id: "tolerance",
|
|
label: "Channel tolerance",
|
|
type: "slider",
|
|
min: 0,
|
|
max: 64,
|
|
step: 1,
|
|
default: 0,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "show-color-pixels-png",
|
|
title: "Show Color Pixels PNG",
|
|
description:
|
|
"Finds colored (non-gray) pixels beyond the channel tolerance and renders them as a mask.",
|
|
defaultMode: "binary",
|
|
predicate: (_i, p) => (r, g, b) =>
|
|
!isGrayscaleish(r, g, b, num(p, "tolerance")),
|
|
extraParams: [
|
|
{
|
|
id: "tolerance",
|
|
label: "Channel tolerance",
|
|
type: "slider",
|
|
min: 0,
|
|
max: 64,
|
|
step: 1,
|
|
default: 8,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "light-pixel-mask-png",
|
|
title: "Light Pixel Mask PNG",
|
|
description: "Selects pixels brighter than the luminance threshold.",
|
|
defaultMode: "binary",
|
|
predicate: (_i, p) => (r, g, b) =>
|
|
luma01(r, g, b) >= num(p, "threshold") / 100,
|
|
extraParams: [
|
|
{
|
|
id: "threshold",
|
|
label: "Luminance threshold, %",
|
|
type: "slider",
|
|
min: 0,
|
|
max: 100,
|
|
step: 1,
|
|
default: 70,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "dark-pixel-mask-png",
|
|
title: "Dark Pixel Mask PNG",
|
|
description: "Selects pixels darker than the luminance threshold.",
|
|
defaultMode: "binary",
|
|
predicate: (_i, p) => (r, g, b) =>
|
|
luma01(r, g, b) <= num(p, "threshold") / 100,
|
|
extraParams: [
|
|
{
|
|
id: "threshold",
|
|
label: "Luminance threshold, %",
|
|
type: "slider",
|
|
min: 0,
|
|
max: 100,
|
|
step: 1,
|
|
default: 30,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "unique-color-mask-png",
|
|
title: "Unique Color Mask PNG",
|
|
description:
|
|
"Selects colors that occur no more than the given number of times — rare and one-off pixels.",
|
|
defaultMode: "binary",
|
|
predicate: (img, p) => rarityPredicate(img, num(p, "rarity")),
|
|
extraParams: [
|
|
{
|
|
id: "rarity",
|
|
label: "Max occurrences",
|
|
type: "slider",
|
|
min: 1,
|
|
max: 50,
|
|
step: 1,
|
|
default: 1,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
function maskEntries(): ToolEntry[] {
|
|
return MASK_TOOLS.map((spec) => ({
|
|
id: spec.id,
|
|
title: spec.title,
|
|
description: spec.description,
|
|
category: "analyze" as const,
|
|
params: [
|
|
...(spec.extraParams ?? []),
|
|
{
|
|
id: "mode",
|
|
label: "Mask mode",
|
|
type: "select" as const,
|
|
default: spec.defaultMode,
|
|
options: [
|
|
{ value: "binary", label: "Black & white mask" },
|
|
{ value: "highlight", label: "Color highlight" },
|
|
],
|
|
},
|
|
{
|
|
id: "color",
|
|
label: "Highlight color",
|
|
type: "color",
|
|
default: "#ff00aa",
|
|
},
|
|
{
|
|
id: "opacity",
|
|
label: "Highlight opacity, %",
|
|
type: "slider",
|
|
min: 0,
|
|
max: 100,
|
|
step: 5,
|
|
default: 70,
|
|
},
|
|
],
|
|
run: (img, p) =>
|
|
renderPredicateMask(img, spec.predicate(img, p), {
|
|
mode: str(p, "mode") === "highlight" ? "highlight" : "binary",
|
|
color: str(p, "color"),
|
|
opacityPercent: num(p, "opacity"),
|
|
}),
|
|
}));
|
|
}
|
|
|
|
export function analyzeEntries(): ToolEntry[] {
|
|
return [
|
|
...maskEntries(),
|
|
{
|
|
id: "extract-color-from-png",
|
|
title: "Extract Color from PNG",
|
|
description:
|
|
"Keeps only pixels close to the chosen color and makes everything else transparent — the inverse of Remove Color.",
|
|
category: "analyze",
|
|
params: [
|
|
{
|
|
id: "color",
|
|
label: "Color to keep",
|
|
type: "color",
|
|
default: "#00ff88",
|
|
},
|
|
{
|
|
id: "tolerance",
|
|
label: "Similarity tolerance, %",
|
|
type: "slider",
|
|
min: 0,
|
|
max: 50,
|
|
step: 1,
|
|
default: 10,
|
|
},
|
|
],
|
|
run: (img, p) =>
|
|
extractByColor(img, str(p, "color"), num(p, "tolerance")),
|
|
},
|
|
{
|
|
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",
|
|
sourceMode: "text",
|
|
params: [],
|
|
resultType: "text",
|
|
textToText: (text) =>
|
|
looksLikePng(base64ToBytes(stripDataUri(text)))
|
|
? "verifyYes"
|
|
: "verifyNo",
|
|
},
|
|
{
|
|
id: "png-info",
|
|
title: "PNG info",
|
|
description:
|
|
"Shows dimensions, alpha presence and the number of unique colors of the uploaded image.",
|
|
category: "analyze",
|
|
params: [],
|
|
resultType: "info",
|
|
run: (img) => clonePixelImage(img),
|
|
},
|
|
{
|
|
id: "png-is-grayscale",
|
|
title: "Check: is PNG grayscale?",
|
|
description: "Reports whether the image consists only of shades of gray.",
|
|
category: "analyze",
|
|
params: [],
|
|
resultType: "text",
|
|
toText: (img) => (isGrayscale(img) ? "grayscaleYes" : "grayscaleNo"),
|
|
},
|
|
{
|
|
id: "png-file-size",
|
|
title: "PNG File Size",
|
|
description:
|
|
"Encodes the image as PNG and reports the resulting file size.",
|
|
category: "analyze",
|
|
domOnly: true,
|
|
params: [],
|
|
resultType: "text",
|
|
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 t("tools.png-file-size.results.line", {
|
|
kb: kbText,
|
|
});
|
|
},
|
|
},
|
|
{
|
|
id: "png-is-transparent",
|
|
title: "Check: is PNG transparent?",
|
|
description:
|
|
"Reports whether the image contains transparent or semi-transparent pixels.",
|
|
category: "analyze",
|
|
params: [],
|
|
resultType: "text",
|
|
toText: (img) =>
|
|
hasTransparency(img) ? "transparentYes" : "transparentNo",
|
|
},
|
|
{
|
|
id: "png-orientation",
|
|
title: "PNG orientation",
|
|
description: "Reports whether it is portrait, landscape or square.",
|
|
category: "analyze",
|
|
params: [],
|
|
resultType: "text",
|
|
toText: (img) => {
|
|
switch (orientationOf(img)) {
|
|
case "portrait":
|
|
return "orientationPortrait";
|
|
case "landscape":
|
|
return "orientationLandscape";
|
|
default:
|
|
return "orientationSquare";
|
|
}
|
|
},
|
|
},
|
|
];
|
|
}
|