mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
merge: resolve conflict with main branch in tool-registry.tsx
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import sharp from "sharp";
|
||||
import { brightness } from "./operations/brightness.js";
|
||||
import { colorBlindness } from "./operations/color-blindness.js";
|
||||
import { colorChannels } from "./operations/color-channels.js";
|
||||
import { compress } from "./operations/compress.js";
|
||||
import { contrast } from "./operations/contrast.js";
|
||||
@@ -17,6 +18,7 @@ import { sharpen, sharpenAdvanced } from "./operations/sharpen.js";
|
||||
import { stripMetadata } from "./operations/strip-metadata.js";
|
||||
import type {
|
||||
BrightnessOptions,
|
||||
ColorBlindnessOptions,
|
||||
ColorChannelOptions,
|
||||
CompressOptions,
|
||||
ContrastOptions,
|
||||
@@ -55,6 +57,7 @@ const OPERATION_MAP: Record<
|
||||
brightness: (img, opts) => brightness(img, opts as unknown as BrightnessOptions),
|
||||
contrast: (img, opts) => contrast(img, opts as unknown as ContrastOptions),
|
||||
saturation: (img, opts) => saturation(img, opts as unknown as SaturationOptions),
|
||||
"color-blindness": (img, opts) => colorBlindness(img, opts as unknown as ColorBlindnessOptions),
|
||||
"color-channels": (img, opts) => colorChannels(img, opts as unknown as ColorChannelOptions),
|
||||
grayscale: (img) => grayscale(img),
|
||||
sepia: (img) => sepia(img),
|
||||
|
||||
@@ -20,6 +20,53 @@ const MAGIC_BYTES: Array<{ bytes: number[]; offset: number; format: string }> =
|
||||
{ bytes: [0x38, 0x42, 0x50, 0x53], offset: 0, format: "psd" },
|
||||
// OpenEXR
|
||||
{ bytes: [0x76, 0x2f, 0x31, 0x01], offset: 0, format: "exr" },
|
||||
// CR3 (Canon ISOBMFF RAW) - ftyp box at offset 4
|
||||
{ bytes: [0x66, 0x74, 0x79, 0x70], offset: 4, format: "cr3" },
|
||||
// Fujifilm RAF: "FUJIFILMCCD-RAW" at offset 0
|
||||
{
|
||||
bytes: [
|
||||
0x46, 0x55, 0x4a, 0x49, 0x46, 0x49, 0x4c, 0x4d, 0x43, 0x43, 0x44, 0x2d, 0x52, 0x41, 0x57,
|
||||
],
|
||||
offset: 0,
|
||||
format: "raf",
|
||||
},
|
||||
// Sigma X3F: "FOVb" at offset 0
|
||||
{ bytes: [0x46, 0x4f, 0x56, 0x62], offset: 0, format: "x3f" },
|
||||
// Minolta MRW: "\x00MRM" at offset 0
|
||||
{ bytes: [0x00, 0x4d, 0x52, 0x4d], offset: 0, format: "mrw" },
|
||||
// JP2 box signature
|
||||
{
|
||||
bytes: [0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a],
|
||||
offset: 0,
|
||||
format: "jp2",
|
||||
},
|
||||
// J2K raw codestream
|
||||
{ bytes: [0xff, 0x4f, 0xff, 0x51], offset: 0, format: "jp2" },
|
||||
// QOI
|
||||
{ bytes: [0x71, 0x6f, 0x69, 0x66], offset: 0, format: "qoi" },
|
||||
// DDS
|
||||
{ bytes: [0x44, 0x44, 0x53, 0x20], offset: 0, format: "dds" },
|
||||
// CUR
|
||||
{ bytes: [0x00, 0x00, 0x02, 0x00], offset: 0, format: "cur" },
|
||||
// DPX forward
|
||||
{ bytes: [0x53, 0x44, 0x50, 0x58], offset: 0, format: "dpx" },
|
||||
// DPX reverse
|
||||
{ bytes: [0x58, 0x50, 0x44, 0x53], offset: 0, format: "dpx" },
|
||||
// Cineon
|
||||
{ bytes: [0x80, 0x2a, 0x5f, 0xd7], offset: 0, format: "cin" },
|
||||
// FITS
|
||||
{ bytes: [0x53, 0x49, 0x4d, 0x50, 0x4c, 0x45], offset: 0, format: "fits" },
|
||||
// EPS ASCII
|
||||
{
|
||||
bytes: [0x25, 0x21, 0x50, 0x53, 0x2d, 0x41, 0x64, 0x6f, 0x62, 0x65],
|
||||
offset: 0,
|
||||
format: "eps",
|
||||
},
|
||||
// EPS binary (DOS)
|
||||
{ bytes: [0xc5, 0xd0, 0xd3, 0xc6], offset: 0, format: "eps" },
|
||||
// PPM (P3/P6)
|
||||
{ bytes: [0x50, 0x33], offset: 0, format: "ppm" },
|
||||
{ bytes: [0x50, 0x36], offset: 0, format: "ppm" },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -67,6 +114,12 @@ function detectByMagicBytes(buffer: Buffer): string {
|
||||
const brand = buffer.slice(8, 12).toString("ascii");
|
||||
if (brand !== "avif" && brand !== "avis") continue;
|
||||
}
|
||||
// For ftyp, verify CR3 brand at bytes 8-11
|
||||
if (entry.format === "cr3") {
|
||||
if (buffer.length < 12) continue;
|
||||
const brand = buffer.slice(8, 12).toString("ascii");
|
||||
if (brand !== "crx ") continue;
|
||||
}
|
||||
return entry.format;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
const QOI_MAGIC = 0x716f6966; // "qoif"
|
||||
const QOI_OP_INDEX = 0x00;
|
||||
const QOI_OP_DIFF = 0x40;
|
||||
const QOI_OP_LUMA = 0x80;
|
||||
const QOI_OP_RUN = 0xc0;
|
||||
const QOI_OP_RGB = 0xfe;
|
||||
const QOI_OP_RGBA = 0xff;
|
||||
const QOI_END_MARKER = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]);
|
||||
|
||||
function hash(r: number, g: number, b: number, a: number): number {
|
||||
return (r * 3 + g * 5 + b * 7 + a * 11) % 64;
|
||||
}
|
||||
|
||||
export interface QoiHeader {
|
||||
width: number;
|
||||
height: number;
|
||||
channels: 3 | 4;
|
||||
colorspace: 0 | 1;
|
||||
}
|
||||
|
||||
export function qoiDecode(data: Uint8Array): { header: QoiHeader; pixels: Uint8Array } {
|
||||
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
||||
if (view.getUint32(0) !== QOI_MAGIC) throw new Error("Not a QOI file");
|
||||
|
||||
const width = view.getUint32(4);
|
||||
const height = view.getUint32(8);
|
||||
const channels = data[12] as 3 | 4;
|
||||
const colorspace = data[13] as 0 | 1;
|
||||
|
||||
if (width === 0 || height === 0) throw new Error("Invalid QOI dimensions");
|
||||
if (channels !== 3 && channels !== 4) throw new Error("Invalid QOI channels");
|
||||
|
||||
const totalPixels = width * height;
|
||||
const pixels = new Uint8Array(totalPixels * 4);
|
||||
const index = new Uint8Array(64 * 4);
|
||||
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0,
|
||||
a = 255;
|
||||
let pos = 14;
|
||||
let px = 0;
|
||||
|
||||
while (px < totalPixels) {
|
||||
const byte = data[pos++];
|
||||
|
||||
if (byte === QOI_OP_RGB) {
|
||||
r = data[pos++];
|
||||
g = data[pos++];
|
||||
b = data[pos++];
|
||||
} else if (byte === QOI_OP_RGBA) {
|
||||
r = data[pos++];
|
||||
g = data[pos++];
|
||||
b = data[pos++];
|
||||
a = data[pos++];
|
||||
} else {
|
||||
const op = byte & 0xc0;
|
||||
if (op === QOI_OP_INDEX) {
|
||||
const idx = (byte & 0x3f) * 4;
|
||||
r = index[idx];
|
||||
g = index[idx + 1];
|
||||
b = index[idx + 2];
|
||||
a = index[idx + 3];
|
||||
} else if (op === QOI_OP_DIFF) {
|
||||
r = (r + ((byte >> 4) & 0x03) - 2) & 0xff;
|
||||
g = (g + ((byte >> 2) & 0x03) - 2) & 0xff;
|
||||
b = (b + (byte & 0x03) - 2) & 0xff;
|
||||
} else if (op === QOI_OP_LUMA) {
|
||||
const b2 = data[pos++];
|
||||
const dg = (byte & 0x3f) - 32;
|
||||
r = (r + dg + ((b2 >> 4) & 0x0f) - 8) & 0xff;
|
||||
g = (g + dg) & 0xff;
|
||||
b = (b + dg + (b2 & 0x0f) - 8) & 0xff;
|
||||
} else {
|
||||
// QOI_OP_RUN
|
||||
let run = (byte & 0x3f) + 1;
|
||||
while (run-- > 0 && px < totalPixels) {
|
||||
const off = px * 4;
|
||||
pixels[off] = r;
|
||||
pixels[off + 1] = g;
|
||||
pixels[off + 2] = b;
|
||||
pixels[off + 3] = a;
|
||||
px++;
|
||||
}
|
||||
const h = hash(r, g, b, a) * 4;
|
||||
index[h] = r;
|
||||
index[h + 1] = g;
|
||||
index[h + 2] = b;
|
||||
index[h + 3] = a;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const h = hash(r, g, b, a) * 4;
|
||||
index[h] = r;
|
||||
index[h + 1] = g;
|
||||
index[h + 2] = b;
|
||||
index[h + 3] = a;
|
||||
const off = px * 4;
|
||||
pixels[off] = r;
|
||||
pixels[off + 1] = g;
|
||||
pixels[off + 2] = b;
|
||||
pixels[off + 3] = a;
|
||||
px++;
|
||||
}
|
||||
|
||||
return { header: { width, height, channels, colorspace }, pixels };
|
||||
}
|
||||
|
||||
export function qoiEncode(
|
||||
pixels: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
channels: 3 | 4 = 4,
|
||||
): Uint8Array {
|
||||
const maxSize = 14 + width * height * (channels + 1) + QOI_END_MARKER.length;
|
||||
const out = new Uint8Array(maxSize);
|
||||
const view = new DataView(out.buffer);
|
||||
|
||||
view.setUint32(0, QOI_MAGIC);
|
||||
view.setUint32(4, width);
|
||||
view.setUint32(8, height);
|
||||
out[12] = channels;
|
||||
out[13] = 0; // sRGB
|
||||
|
||||
const index = new Uint8Array(64 * 4);
|
||||
let pos = 14;
|
||||
let prevR = 0,
|
||||
prevG = 0,
|
||||
prevB = 0,
|
||||
prevA = 255;
|
||||
let run = 0;
|
||||
const totalPixels = width * height;
|
||||
|
||||
for (let px = 0; px < totalPixels; px++) {
|
||||
const off = px * channels;
|
||||
const r = pixels[off];
|
||||
const g = pixels[off + 1];
|
||||
const b = pixels[off + 2];
|
||||
const a = channels === 4 ? pixels[off + 3] : 255;
|
||||
|
||||
if (r === prevR && g === prevG && b === prevB && a === prevA) {
|
||||
run++;
|
||||
if (run === 62 || px === totalPixels - 1) {
|
||||
out[pos++] = QOI_OP_RUN | (run - 1);
|
||||
run = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (run > 0) {
|
||||
out[pos++] = QOI_OP_RUN | (run - 1);
|
||||
run = 0;
|
||||
}
|
||||
|
||||
const h = hash(r, g, b, a);
|
||||
const idx = h * 4;
|
||||
|
||||
if (index[idx] === r && index[idx + 1] === g && index[idx + 2] === b && index[idx + 3] === a) {
|
||||
out[pos++] = QOI_OP_INDEX | h;
|
||||
} else {
|
||||
index[idx] = r;
|
||||
index[idx + 1] = g;
|
||||
index[idx + 2] = b;
|
||||
index[idx + 3] = a;
|
||||
|
||||
if (a !== prevA) {
|
||||
out[pos++] = QOI_OP_RGBA;
|
||||
out[pos++] = r;
|
||||
out[pos++] = g;
|
||||
out[pos++] = b;
|
||||
out[pos++] = a;
|
||||
} else {
|
||||
const dr = r - prevR;
|
||||
const dg = g - prevG;
|
||||
const db = b - prevB;
|
||||
const drDg = dr - dg;
|
||||
const dbDg = db - dg;
|
||||
|
||||
if (dr > -3 && dr < 2 && dg > -3 && dg < 2 && db > -3 && db < 2) {
|
||||
out[pos++] = QOI_OP_DIFF | ((dr + 2) << 4) | ((dg + 2) << 2) | (db + 2);
|
||||
} else if (dg > -33 && dg < 32 && drDg > -9 && drDg < 8 && dbDg > -9 && dbDg < 8) {
|
||||
out[pos++] = QOI_OP_LUMA | (dg + 32);
|
||||
out[pos++] = ((drDg + 8) << 4) | (dbDg + 8);
|
||||
} else {
|
||||
out[pos++] = QOI_OP_RGB;
|
||||
out[pos++] = r;
|
||||
out[pos++] = g;
|
||||
out[pos++] = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevR = r;
|
||||
prevG = g;
|
||||
prevB = b;
|
||||
prevA = a;
|
||||
}
|
||||
|
||||
out.set(QOI_END_MARKER, pos);
|
||||
pos += QOI_END_MARKER.length;
|
||||
return out.slice(0, pos);
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
export * from "./engine.js";
|
||||
export * from "./formats/detect.js";
|
||||
export type { QoiHeader } from "./formats/qoi.js";
|
||||
export { qoiDecode, qoiEncode } from "./formats/qoi.js";
|
||||
export { analyzeImage, applyCorrections, scaleCorrections } from "./operations/auto-enhance.js";
|
||||
export { brightness } from "./operations/brightness.js";
|
||||
export { COLOR_BLINDNESS_MATRICES, colorBlindness } from "./operations/color-blindness.js";
|
||||
export { colorChannels } from "./operations/color-channels.js";
|
||||
export { compress } from "./operations/compress.js";
|
||||
export { contrast } from "./operations/contrast.js";
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ColorBlindnessOptions, ColorBlindnessType, Sharp } from "../types.js";
|
||||
|
||||
type Matrix3x3 = [[number, number, number], [number, number, number], [number, number, number]];
|
||||
|
||||
export const COLOR_BLINDNESS_MATRICES: Record<ColorBlindnessType, Matrix3x3> = {
|
||||
protanopia: [
|
||||
[0.152286, 1.052583, -0.204868],
|
||||
[0.114503, 0.786281, 0.099216],
|
||||
[-0.003882, -0.048116, 1.051998],
|
||||
],
|
||||
deuteranopia: [
|
||||
[0.367322, 0.860646, -0.227968],
|
||||
[0.280085, 0.672501, 0.047413],
|
||||
[-0.01182, 0.04294, 0.968881],
|
||||
],
|
||||
tritanopia: [
|
||||
[1.255528, -0.076749, -0.178779],
|
||||
[-0.078411, 0.930809, 0.147602],
|
||||
[0.004733, 0.691367, 0.3039],
|
||||
],
|
||||
protanomaly: [
|
||||
[0.458064, 0.679578, -0.137642],
|
||||
[0.092785, 0.846313, 0.060902],
|
||||
[-0.007494, -0.016807, 1.024301],
|
||||
],
|
||||
deuteranomaly: [
|
||||
[0.547494, 0.607765, -0.155259],
|
||||
[0.181692, 0.781742, 0.036566],
|
||||
[-0.01041, 0.027275, 0.983136],
|
||||
],
|
||||
tritanomaly: [
|
||||
[1.017277, 0.027029, -0.044306],
|
||||
[-0.006113, 0.958479, 0.047634],
|
||||
[0.006379, 0.248708, 0.744913],
|
||||
],
|
||||
achromatopsia: [
|
||||
[0.2126, 0.7152, 0.0722],
|
||||
[0.2126, 0.7152, 0.0722],
|
||||
[0.2126, 0.7152, 0.0722],
|
||||
],
|
||||
blueConeMonochromacy: [
|
||||
[0.01775, 0.10945, 0.87262],
|
||||
[0.01775, 0.10945, 0.87262],
|
||||
[0.01775, 0.10945, 0.87262],
|
||||
],
|
||||
};
|
||||
|
||||
export async function colorBlindness(image: Sharp, options: ColorBlindnessOptions): Promise<Sharp> {
|
||||
return image.recomb(COLOR_BLINDNESS_MATRICES[options.type]);
|
||||
}
|
||||
@@ -10,10 +10,11 @@ const FORMAT_MAP: Record<string, string> = {
|
||||
heif: "avif",
|
||||
tiff: "tiff",
|
||||
gif: "gif",
|
||||
jxl: "jxl",
|
||||
};
|
||||
|
||||
/** Formats that Sharp cannot encode — fall back to PNG for output. */
|
||||
const NO_ENCODER = new Set(["svg", "bmp", "raw", "tga", "psd", "exr", "hdr", "ico"]);
|
||||
const NO_ENCODER = new Set(["svg", "raw", "tga", "psd", "exr", "hdr"]);
|
||||
|
||||
function formatOpts(format: string, quality: number): Record<string, unknown> {
|
||||
const opts: Record<string, unknown> = { quality };
|
||||
|
||||
@@ -39,6 +39,8 @@ export async function optimizeForWeb(image: Sharp, options: OptimizeForWebOption
|
||||
return pipeline.avif({ quality, effort: 4 });
|
||||
case "png":
|
||||
return pipeline.png({ compressionLevel: 9, palette: true });
|
||||
case "jxl":
|
||||
return pipeline.jxl({ quality, effort: 7 });
|
||||
default:
|
||||
throw new Error(`Unsupported format: ${format}`);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,11 @@ export type OutputFormat =
|
||||
| "gif"
|
||||
| "heic"
|
||||
| "heif"
|
||||
| "jxl";
|
||||
| "jxl"
|
||||
| "bmp"
|
||||
| "ico"
|
||||
| "jp2"
|
||||
| "qoi";
|
||||
|
||||
export interface ResizeOptions {
|
||||
width?: number;
|
||||
@@ -183,10 +187,24 @@ export interface CorrectionParams {
|
||||
}
|
||||
|
||||
export interface OptimizeForWebOptions {
|
||||
format: "webp" | "jpeg" | "avif" | "png";
|
||||
format: "webp" | "jpeg" | "avif" | "png" | "jxl";
|
||||
quality: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
progressive?: boolean;
|
||||
stripMetadata?: boolean;
|
||||
}
|
||||
|
||||
export type ColorBlindnessType =
|
||||
| "protanopia"
|
||||
| "deuteranopia"
|
||||
| "tritanopia"
|
||||
| "protanomaly"
|
||||
| "deuteranomaly"
|
||||
| "tritanomaly"
|
||||
| "achromatopsia"
|
||||
| "blueConeMonochromacy";
|
||||
|
||||
export interface ColorBlindnessOptions {
|
||||
type: ColorBlindnessType;
|
||||
}
|
||||
|
||||
@@ -19,10 +19,51 @@ const EXT_TO_MIME: Record<string, string> = {
|
||||
arw: "image/x-sony-arw",
|
||||
orf: "image/x-olympus-orf",
|
||||
rw2: "image/x-panasonic-rw2",
|
||||
cr3: "image/x-canon-cr3",
|
||||
raf: "image/x-fuji-raf",
|
||||
pef: "image/x-pentax-pef",
|
||||
"3fr": "image/x-hasselblad-3fr",
|
||||
iiq: "image/x-phaseone-iiq",
|
||||
srw: "image/x-samsung-srw",
|
||||
x3f: "image/x-sigma-x3f",
|
||||
rwl: "image/x-leica-rwl",
|
||||
nrw: "image/x-nikon-nrw",
|
||||
gpr: "image/x-gopro-gpr",
|
||||
fff: "image/x-hasselblad-fff",
|
||||
mrw: "image/x-minolta-mrw",
|
||||
mef: "image/x-mamiya-mef",
|
||||
kdc: "image/x-kodak-kdc",
|
||||
dcr: "image/x-kodak-dcr",
|
||||
erf: "image/x-epson-erf",
|
||||
ptx: "image/x-pentax-ptx",
|
||||
tga: "image/x-tga",
|
||||
psd: "image/vnd.adobe.photoshop",
|
||||
exr: "image/x-exr",
|
||||
hdr: "image/vnd.radiance",
|
||||
jp2: "image/jp2",
|
||||
j2k: "image/jp2",
|
||||
j2c: "image/jp2",
|
||||
jpc: "image/jp2",
|
||||
jpf: "image/jp2",
|
||||
jpx: "image/jpx",
|
||||
qoi: "image/qoi",
|
||||
eps: "application/postscript",
|
||||
epsf: "application/postscript",
|
||||
dds: "image/vnd.ms-dds",
|
||||
cur: "image/x-icon",
|
||||
apng: "image/apng",
|
||||
dpx: "image/x-dpx",
|
||||
cin: "image/x-cineon",
|
||||
fits: "image/fits",
|
||||
fit: "image/fits",
|
||||
fts: "image/fits",
|
||||
ppm: "image/x-portable-pixmap",
|
||||
pgm: "image/x-portable-graymap",
|
||||
pbm: "image/x-portable-bitmap",
|
||||
pnm: "image/x-portable-anymap",
|
||||
pam: "image/x-portable-anymap",
|
||||
pfm: "image/x-portable-floatmap",
|
||||
svgz: "image/svg+xml",
|
||||
};
|
||||
|
||||
const MIME_TO_EXT: Record<string, string> = {
|
||||
@@ -44,10 +85,41 @@ const MIME_TO_EXT: Record<string, string> = {
|
||||
"image/x-sony-arw": "arw",
|
||||
"image/x-olympus-orf": "orf",
|
||||
"image/x-panasonic-rw2": "rw2",
|
||||
"image/x-canon-cr3": "cr3",
|
||||
"image/x-fuji-raf": "raf",
|
||||
"image/x-pentax-pef": "pef",
|
||||
"image/x-hasselblad-3fr": "3fr",
|
||||
"image/x-phaseone-iiq": "iiq",
|
||||
"image/x-samsung-srw": "srw",
|
||||
"image/x-sigma-x3f": "x3f",
|
||||
"image/x-leica-rwl": "rwl",
|
||||
"image/x-nikon-nrw": "nrw",
|
||||
"image/x-gopro-gpr": "gpr",
|
||||
"image/x-hasselblad-fff": "fff",
|
||||
"image/x-minolta-mrw": "mrw",
|
||||
"image/x-mamiya-mef": "mef",
|
||||
"image/x-kodak-kdc": "kdc",
|
||||
"image/x-kodak-dcr": "dcr",
|
||||
"image/x-epson-erf": "erf",
|
||||
"image/x-pentax-ptx": "ptx",
|
||||
"image/x-tga": "tga",
|
||||
"image/vnd.adobe.photoshop": "psd",
|
||||
"image/x-exr": "exr",
|
||||
"image/vnd.radiance": "hdr",
|
||||
"image/jp2": "jp2",
|
||||
"image/jpx": "jpx",
|
||||
"image/qoi": "qoi",
|
||||
"application/postscript": "eps",
|
||||
"image/vnd.ms-dds": "dds",
|
||||
"image/apng": "apng",
|
||||
"image/x-dpx": "dpx",
|
||||
"image/x-cineon": "cin",
|
||||
"image/fits": "fits",
|
||||
"image/x-portable-pixmap": "ppm",
|
||||
"image/x-portable-graymap": "pgm",
|
||||
"image/x-portable-bitmap": "pbm",
|
||||
"image/x-portable-anymap": "pnm",
|
||||
"image/x-portable-floatmap": "pfm",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -128,6 +128,14 @@ export const TOOLS: Tool[] = [
|
||||
icon: "Pipette",
|
||||
route: "/replace-color",
|
||||
},
|
||||
{
|
||||
id: "color-blindness",
|
||||
name: "Color Blindness Simulation",
|
||||
description: "Simulate how images appear with color vision deficiency",
|
||||
category: "adjustments",
|
||||
icon: "Eye",
|
||||
route: "/color-blindness",
|
||||
},
|
||||
// AI Tools
|
||||
{
|
||||
id: "remove-background",
|
||||
@@ -380,6 +388,14 @@ export const TOOLS: Tool[] = [
|
||||
icon: "Frame",
|
||||
route: "/border",
|
||||
},
|
||||
{
|
||||
id: "beautify",
|
||||
name: "Beautify Screenshot",
|
||||
description: "Add gradient backgrounds, device frames, shadows, and social media sizing",
|
||||
category: "layout",
|
||||
icon: "Sparkles",
|
||||
route: "/beautify",
|
||||
},
|
||||
// Format & Conversion
|
||||
{
|
||||
id: "svg-to-raster",
|
||||
|
||||
@@ -72,6 +72,10 @@ export const en = {
|
||||
name: "Replace & Invert Color",
|
||||
description: "Replace specific colors or invert",
|
||||
},
|
||||
"color-blindness": {
|
||||
name: "Color Blindness Simulation",
|
||||
description: "Simulate how images appear with different types of color vision deficiency",
|
||||
},
|
||||
"remove-background": {
|
||||
name: "Remove Background",
|
||||
description: "AI-powered background removal",
|
||||
@@ -168,6 +172,11 @@ export const en = {
|
||||
name: "Border & Frame",
|
||||
description: "Add borders, padding, rounded corners, and shadows with one-click presets",
|
||||
},
|
||||
beautify: {
|
||||
name: "Beautify Screenshot",
|
||||
description:
|
||||
"Add gradient backgrounds, device frames, shadows, and social media sizing to screenshots",
|
||||
},
|
||||
"svg-to-raster": {
|
||||
name: "SVG to Raster",
|
||||
description:
|
||||
@@ -211,7 +220,7 @@ export const en = {
|
||||
regenerateKey: "Regenerate",
|
||||
copyKey: "Copy Key",
|
||||
aboutDescription:
|
||||
"SnapOtter is a self-hosted, privacy-first image processing suite with 48 tools.",
|
||||
"SnapOtter is a self-hosted, privacy-first image processing suite with 50 tools.",
|
||||
aboutLinks: "Links",
|
||||
github: "GitHub",
|
||||
documentation: "Documentation",
|
||||
@@ -306,6 +315,7 @@ export const en = {
|
||||
tools: "Tools",
|
||||
reader: "Reader",
|
||||
automate: "Automate",
|
||||
editor: "Editor",
|
||||
files: "Files",
|
||||
help: "Help",
|
||||
settings: "Settings",
|
||||
|
||||
Reference in New Issue
Block a user