feat: add 5 high-impact tools (emoji resizer, favicon generator, secret share, palette extractor, svg to png)

This commit is contained in:
katR 2026-09-12 17:02:52 +05:30
parent 6d60115b88
commit fa48c48626
8 changed files with 1669 additions and 0 deletions

105
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"exifr": "^7.1.3", "exifr": "^7.1.3",
"jszip": "^3.10.2",
"lucide-react": "^1.45.0", "lucide-react": "^1.45.0",
"marked": "^18.0.11", "marked": "^18.0.11",
"next": "16.3.3", "next": "16.3.3",
@ -19,6 +20,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/jszip": "^3.4.0",
"@types/node": "^20", "@types/node": "^20",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@types/react": "^19", "@types/react": "^19",
@ -1732,6 +1734,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/jszip": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz",
"integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"jszip": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.43", "version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
@ -2947,6 +2959,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -4275,6 +4293,12 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -4302,6 +4326,12 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@ -4892,6 +4922,18 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/jszip": {
"version": "3.10.2",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz",
"integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -4936,6 +4978,15 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.32.0", "version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@ -5842,6 +5893,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -5930,6 +5987,27 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@ -6088,6 +6166,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safe-push-apply": { "node_modules/safe-push-apply": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@ -6194,6 +6278,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/sharp": { "node_modules/sharp": {
"version": "0.35.4", "version": "0.35.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
@ -6386,6 +6476,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@ -6965,6 +7064,12 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"exifr": "^7.1.3", "exifr": "^7.1.3",
"jszip": "^3.10.2",
"lucide-react": "^1.45.0", "lucide-react": "^1.45.0",
"marked": "^18.0.11", "marked": "^18.0.11",
"next": "16.3.3", "next": "16.3.3",
@ -20,6 +21,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/jszip": "^3.4.0",
"@types/node": "^20", "@types/node": "^20",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@types/react": "^19", "@types/react": "^19",

View file

@ -0,0 +1,275 @@
"use client";
import { useState, useRef, useCallback } from "react";
import ToolLayout from "@/components/ToolLayout";
interface ColorItem {
hex: string;
rgb: { r: number; g: number; b: number };
hsl: { h: number; s: number; l: number };
population: number;
}
function rgbToHex(r: number, g: number, b: number): string {
return (
"#" +
[r, g, b]
.map((x) => {
const hex = Math.round(x).toString(16);
return hex.length === 1 ? "0" + hex : hex;
})
.join("")
);
}
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100),
};
}
// Simple color quantization by bucket clustering
function extractDominantColors(data: Uint8ClampedArray, colorCount = 8): ColorItem[] {
const buckets: { [key: string]: { r: number; g: number; b: number; count: number } } = {};
const step = 4 * 4; // Sample every 4th pixel for speed
for (let i = 0; i < data.length; i += step) {
const a = data[i + 3];
if (a < 128) continue; // Skip transparent pixels
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Quantize into 32-step buckets
const qr = Math.round(r / 32) * 32;
const qg = Math.round(g / 32) * 32;
const qb = Math.round(b / 32) * 32;
const key = `${qr},${qg},${qb}`;
if (!buckets[key]) {
buckets[key] = { r: 0, g: 0, b: 0, count: 0 };
}
buckets[key].r += r;
buckets[key].g += g;
buckets[key].b += b;
buckets[key].count++;
}
const sorted = Object.values(buckets)
.sort((a, b) => b.count - a.count)
.slice(0, colorCount);
return sorted.map((item) => {
const r = Math.round(item.r / item.count);
const g = Math.round(item.g / item.count);
const b = Math.round(item.b / item.count);
return {
hex: rgbToHex(r, g, b),
rgb: { r, g, b },
hsl: rgbToHsl(r, g, b),
population: item.count,
};
});
}
export default function ColorPaletteExtractorPage() {
const [file, setFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [palette, setPalette] = useState<ColorItem[]>([]);
const [hoverColor, setHoverColor] = useState<string | null>(null);
const [copiedText, setCopiedText] = useState<string | null>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const processImage = useCallback((sourceFile: File) => {
const img = new Image();
const url = URL.createObjectURL(sourceFile);
img.src = url;
img.onload = () => {
const canvas = canvasRef.current || document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return;
const maxDim = 400;
const scale = Math.min(maxDim / img.naturalWidth, maxDim / img.naturalHeight, 1);
canvas.width = Math.round(img.naturalWidth * scale);
canvas.height = Math.round(img.naturalHeight * scale);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const colors = extractDominantColors(imgData.data, 8);
setPalette(colors);
};
}, []);
const handleFileSelect = (selected: File) => {
if (!selected.type.startsWith("image/")) return;
setFile(selected);
if (previewUrl) URL.revokeObjectURL(previewUrl);
const url = URL.createObjectURL(selected);
setPreviewUrl(url);
processImage(selected);
};
const copyToClipboard = (text: string, label: string) => {
navigator.clipboard.writeText(text);
setCopiedText(label);
setTimeout(() => setCopiedText(null), 1800);
};
const getCssVariables = () => {
return `:root {\n` + palette.map((c, i) => ` --color-palette-${i + 1}: ${c.hex};`).join("\n") + `\n}`;
};
const getTailwindConfig = () => {
const obj = palette.reduce((acc, c, i) => {
acc[`color-${i + 1}`] = c.hex;
return acc;
}, {} as Record<string, string>);
return `// tailwind.config.js\ncolors: ` + JSON.stringify(obj, null, 2);
};
return (
<ToolLayout
title="Image Color Palette Extractor"
description="Extract dominant color palettes and hex codes from any image or artwork client-side. Export directly to CSS variables or Tailwind configs."
>
<div className="space-y-6">
{/* Upload Box */}
<div
onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
if (e.dataTransfer.files?.[0]) handleFileSelect(e.dataTransfer.files[0]);
}}
className="cursor-pointer border-2 border-dashed border-[#222] bg-[#0c0c0c] hover:border-[#444] transition-colors p-8 text-center"
>
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => {
if (e.target.files?.[0]) handleFileSelect(e.target.files[0]);
}}
/>
<p className="text-sm font-medium text-gray-200">
{file ? file.name : "Drop photo, graphic, or wallpaper here"}
</p>
<p className="mt-1 text-xs text-[#666]">
Supports PNG, JPEG, WebP, SVG.
</p>
</div>
{/* Hidden Canvas for computation */}
<canvas ref={canvasRef} className="hidden" />
{previewUrl && palette.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Image Preview */}
<div className="border border-[#181818] bg-[#0a0a0a] p-4 flex flex-col items-center justify-center">
<img
src={previewUrl}
alt="Uploaded reference"
className="max-h-80 w-auto object-contain border border-[#1e1e1e]"
/>
{hoverColor && (
<div className="mt-3 flex items-center gap-2 text-xs font-mono text-gray-300">
<div className="w-3.5 h-3.5 border border-white/20" style={{ backgroundColor: hoverColor }} />
<span>{hoverColor}</span>
</div>
)}
</div>
{/* Extracted Palette */}
<div className="border border-[#181818] bg-[#0a0a0a] p-6 space-y-4">
<div className="flex items-center justify-between border-b border-[#181818] pb-3">
<h3 className="text-xs font-mono uppercase tracking-wider text-[#666]">
Dominant Colors ({palette.length})
</h3>
{copiedText && (
<span className="text-xs font-mono text-blue-400">Copied {copiedText}!</span>
)}
</div>
{/* Swatches Grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2.5">
{palette.map((color, idx) => (
<div
key={idx}
onMouseEnter={() => setHoverColor(color.hex)}
onMouseLeave={() => setHoverColor(null)}
onClick={() => copyToClipboard(color.hex, color.hex)}
className="cursor-pointer border border-[#181818] bg-[#0e0e0e] hover:border-[#333] transition-all p-2 text-left group"
>
<div
className="w-full h-14 border border-black/20 mb-2 transition-transform group-hover:scale-[1.02]"
style={{ backgroundColor: color.hex }}
/>
<div className="font-mono text-xs text-gray-200 font-medium">{color.hex}</div>
<div className="font-mono text-[10px] text-[#555]">
rgb({color.rgb.r}, {color.rgb.g}, {color.rgb.b})
</div>
</div>
))}
</div>
{/* Export Buttons */}
<div className="border-t border-[#181818] pt-4 flex flex-wrap gap-2">
<button
onClick={() => copyToClipboard(getCssVariables(), "CSS Variables")}
className="px-3 py-1.5 bg-[#141414] hover:bg-[#1f1f1f] border border-[#222] text-xs font-mono text-gray-300 transition-colors"
>
Copy CSS Variables
</button>
<button
onClick={() => copyToClipboard(getTailwindConfig(), "Tailwind Config")}
className="px-3 py-1.5 bg-[#141414] hover:bg-[#1f1f1f] border border-[#222] text-xs font-mono text-gray-300 transition-colors"
>
Copy Tailwind
</button>
<button
onClick={() => copyToClipboard(JSON.stringify(palette.map((c) => c.hex)), "JSON")}
className="px-3 py-1.5 bg-[#141414] hover:bg-[#1f1f1f] border border-[#222] text-xs font-mono text-gray-300 transition-colors"
>
Copy Hex Array
</button>
</div>
</div>
</div>
)}
</div>
</ToolLayout>
);
}

View file

@ -0,0 +1,390 @@
"use client";
import { useState, useRef, useCallback } from "react";
import ToolLayout from "@/components/ToolLayout";
interface Preset {
id: string;
name: string;
dimension: number;
maxBytes: number;
format: "image/png" | "image/webp";
description: string;
}
const PRESETS: Preset[] = [
{
id: "emoji",
name: "Discord Emoji",
dimension: 128,
maxBytes: 256 * 1024,
format: "image/png",
description: "128x128 px, under 256 KB limit",
},
{
id: "sticker",
name: "Discord Sticker",
dimension: 320,
maxBytes: 512 * 1024,
format: "image/png",
description: "320x320 px, under 512 KB limit",
},
{
id: "custom",
name: "High-Res Emoji (WebP)",
dimension: 256,
maxBytes: 256 * 1024,
format: "image/webp",
description: "256x256 px, WebP compression under 256 KB",
},
];
function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
export default function DiscordEmojiResizerPage() {
const [file, setFile] = useState<File | null>(null);
const [originalUrl, setOriginalUrl] = useState<string | null>(null);
const [originalDimensions, setOriginalDimensions] = useState<{ w: number; h: number } | null>(null);
const [resultBlob, setResultBlob] = useState<Blob | null>(null);
const [resultUrl, setResultUrl] = useState<string | null>(null);
const [activePreset, setActivePreset] = useState<Preset>(PRESETS[0]);
const [cropMode, setCropMode] = useState<"crop" | "fit">("crop");
const [isProcessing, setIsProcessing] = useState(false);
const [statusMessage, setStatusMessage] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null);
const processImage = useCallback(
async (sourceFile: File, preset: Preset, mode: "crop" | "fit") => {
setIsProcessing(true);
setStatusMessage("Resizing and optimizing...");
try {
const img = new Image();
const objUrl = URL.createObjectURL(sourceFile);
img.src = objUrl;
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error("Unable to load image file"));
});
const targetDim = preset.dimension;
const canvas = document.createElement("canvas");
canvas.width = targetDim;
canvas.height = targetDim;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("2D canvas context unavailable");
}
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
if (mode === "crop") {
// Center crop to square
const minSide = Math.min(img.naturalWidth, img.naturalHeight);
const sx = (img.naturalWidth - minSide) / 2;
const sy = (img.naturalHeight - minSide) / 2;
ctx.drawImage(img, sx, sy, minSide, minSide, 0, 0, targetDim, targetDim);
} else {
// Fit with transparent background
const scale = Math.min(targetDim / img.naturalWidth, targetDim / img.naturalHeight);
const drawW = img.naturalWidth * scale;
const drawH = img.naturalHeight * scale;
const dx = (targetDim - drawW) / 2;
const dy = (targetDim - drawH) / 2;
ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, dx, dy, drawW, drawH);
}
// Binary search compression for target file size
let finalBlob: Blob | null = null;
let low = 0.1;
let high = 1.0;
let bestUnderLimit: Blob | null = null;
// Try PNG first if format is PNG
if (preset.format === "image/png") {
const pngBlob = await new Promise<Blob | null>((res) => canvas.toBlob(res, "image/png"));
if (pngBlob && pngBlob.size <= preset.maxBytes) {
finalBlob = pngBlob;
}
}
if (!finalBlob) {
// Compress via WebP or JPEG to meet strict size constraints
const compressionFormat = preset.format === "image/png" ? "image/webp" : preset.format;
for (let iter = 0; iter < 7; iter++) {
const mid = (low + high) / 2;
const testBlob = await new Promise<Blob | null>((res) =>
canvas.toBlob(res, compressionFormat, mid)
);
if (!testBlob) break;
if (testBlob.size <= preset.maxBytes) {
bestUnderLimit = testBlob;
low = mid;
} else {
high = mid;
}
}
finalBlob = bestUnderLimit;
}
URL.revokeObjectURL(objUrl);
if (!finalBlob) {
const fallback = await new Promise<Blob | null>((res) => canvas.toBlob(res, "image/webp", 0.5));
finalBlob = fallback;
}
if (finalBlob) {
if (resultUrl) URL.revokeObjectURL(resultUrl);
const resUrl = URL.createObjectURL(finalBlob);
setResultBlob(finalBlob);
setResultUrl(resUrl);
setStatusMessage(`Complete: ${preset.dimension}x${preset.dimension} px (${formatBytes(finalBlob.size)})`);
}
} catch (err: unknown) {
const errorMsg = err instanceof Error ? err.message : "Processing failed";
setStatusMessage(errorMsg);
} finally {
setIsProcessing(false);
}
},
[resultUrl]
);
const handleFileSelect = (selected: File) => {
if (!selected.type.startsWith("image/")) {
setStatusMessage("Please upload a valid image file (PNG, JPG, WebP, SVG, GIF)");
return;
}
setFile(selected);
const url = URL.createObjectURL(selected);
setOriginalUrl(url);
const img = new Image();
img.src = url;
img.onload = () => {
setOriginalDimensions({ w: img.naturalWidth, h: img.naturalHeight });
};
processImage(selected, activePreset, cropMode);
};
const handlePresetChange = (preset: Preset) => {
setActivePreset(preset);
if (file) {
processImage(file, preset, cropMode);
}
};
const handleCropModeChange = (mode: "crop" | "fit") => {
setCropMode(mode);
if (file) {
processImage(file, activePreset, mode);
}
};
const handleDownload = () => {
if (!resultBlob || !resultUrl) return;
const ext = resultBlob.type === "image/webp" ? "webp" : "png";
const link = document.createElement("a");
link.href = resultUrl;
link.download = `${activePreset.id}_${activePreset.dimension}x${activePreset.dimension}.${ext}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<ToolLayout
title="Discord Emoji & Sticker Resizer"
description="Resize, square-crop, and compress images to Discord emoji (128x128, <256KB) and sticker (320x320, <512KB) standards client-side."
>
<div className="space-y-6">
{/* Upload Box */}
<div
onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
if (e.dataTransfer.files?.[0]) handleFileSelect(e.dataTransfer.files[0]);
}}
className="cursor-pointer border-2 border-dashed border-[#222] bg-[#0c0c0c] hover:border-[#444] transition-colors p-8 text-center"
>
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => {
if (e.target.files?.[0]) handleFileSelect(e.target.files[0]);
}}
/>
<p className="text-sm font-medium text-gray-200">
{file ? file.name : "Drop image here or click to browse"}
</p>
<p className="mt-1 text-xs text-[#666]">
Supports PNG, JPEG, WebP, SVG, and static GIF.
</p>
{originalDimensions && (
<p className="mt-2 text-xs font-mono text-blue-400">
Original: {originalDimensions.w}x{originalDimensions.h} px ({formatBytes(file?.size || 0)})
</p>
)}
</div>
{/* Configuration Controls */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="border border-[#181818] bg-[#0c0c0c] p-4">
<label className="block text-xs font-mono uppercase tracking-wider text-[#666] mb-3">
Target Preset
</label>
<div className="space-y-2">
{PRESETS.map((p) => (
<button
key={p.id}
onClick={() => handlePresetChange(p)}
className={`w-full text-left p-3 border transition-colors ${
activePreset.id === p.id
? "border-blue-500/60 bg-blue-950/20 text-blue-200"
: "border-[#1c1c1c] bg-[#101010] text-gray-300 hover:border-[#282828]"
}`}
>
<div className="flex items-center justify-between text-xs font-medium">
<span>{p.name}</span>
<span className="font-mono text-[11px] text-[#666]">{p.dimension}x{p.dimension} px</span>
</div>
<div className="mt-1 text-[11px] text-[#555]">{p.description}</div>
</button>
))}
</div>
</div>
<div className="border border-[#181818] bg-[#0c0c0c] p-4">
<label className="block text-xs font-mono uppercase tracking-wider text-[#666] mb-3">
Fit & Framing Mode
</label>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => handleCropModeChange("crop")}
className={`p-3 text-center border transition-colors ${
cropMode === "crop"
? "border-blue-500/60 bg-blue-950/20 text-blue-200"
: "border-[#1c1c1c] bg-[#101010] text-gray-400 hover:border-[#282828]"
}`}
>
<div className="text-xs font-medium">Center Crop</div>
<div className="mt-1 text-[10px] text-[#555]">Fills square, clips edges</div>
</button>
<button
onClick={() => handleCropModeChange("fit")}
className={`p-3 text-center border transition-colors ${
cropMode === "fit"
? "border-blue-500/60 bg-blue-950/20 text-blue-200"
: "border-[#1c1c1c] bg-[#101010] text-gray-400 hover:border-[#282828]"
}`}
>
<div className="text-xs font-medium">Fit & Pad</div>
<div className="mt-1 text-[10px] text-[#555]">Transparent padding</div>
</button>
</div>
<div className="mt-6 border-t border-[#181818] pt-4">
<div className="flex items-center justify-between text-xs text-[#666]">
<span>Status:</span>
<span className="font-mono text-gray-300">{statusMessage || "Idle"}</span>
</div>
</div>
</div>
</div>
{/* Live Previews */}
{resultUrl && (
<div className="border border-[#181818] bg-[#0a0a0a] p-6 space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-[#181818] pb-4">
<div>
<h3 className="text-sm font-medium text-gray-200">Optimized Output</h3>
<p className="text-xs font-mono text-[#666]">
{activePreset.dimension}x{activePreset.dimension} px | {resultBlob ? formatBytes(resultBlob.size) : ""} | Limit: {formatBytes(activePreset.maxBytes)}
</p>
</div>
<button
onClick={handleDownload}
disabled={isProcessing}
className="px-5 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-xs font-medium transition-colors"
>
Download {activePreset.name}
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 text-center">
{/* Actual Dimension */}
<div className="flex flex-col items-center justify-center p-4 border border-[#161616] bg-[#0e0e0e]">
<p className="text-[11px] font-mono text-[#666] mb-3">Actual Size</p>
<div className="p-2 border border-dashed border-[#222] bg-[radial-gradient(#1a1a1a_1px,transparent_1px)] [background-size:8px_8px]">
<img
src={resultUrl}
alt="Actual size"
width={activePreset.dimension}
height={activePreset.dimension}
className="block"
/>
</div>
</div>
{/* Chat Preview (Dark Background) */}
<div className="flex flex-col items-center justify-center p-4 border border-[#161616] bg-[#2b2d31]">
<p className="text-[11px] font-mono text-gray-400 mb-3">Discord Dark Theme</p>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-[#1e1f22] flex items-center justify-center text-[10px] text-gray-400 font-bold">
U
</div>
<div className="text-left">
<span className="text-xs font-semibold text-white">Member: </span>
<img
src={resultUrl}
alt="Discord preview"
className="inline-block align-middle ml-1"
style={{ width: activePreset.id === "sticker" ? 120 : 32, height: activePreset.id === "sticker" ? 120 : 32 }}
/>
</div>
</div>
</div>
{/* Chat Preview (Light Background) */}
<div className="flex flex-col items-center justify-center p-4 border border-[#161616] bg-[#f2f3f5]">
<p className="text-[11px] font-mono text-gray-600 mb-3">Discord Light Theme</p>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-[#e0e2e5] flex items-center justify-center text-[10px] text-gray-600 font-bold">
U
</div>
<div className="text-left">
<span className="text-xs font-semibold text-[#313338]">Member: </span>
<img
src={resultUrl}
alt="Discord light preview"
className="inline-block align-middle ml-1"
style={{ width: activePreset.id === "sticker" ? 120 : 32, height: activePreset.id === "sticker" ? 120 : 32 }}
/>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</ToolLayout>
);
}

View file

@ -0,0 +1,326 @@
"use client";
import { useState, useRef, useCallback } from "react";
import ToolLayout from "@/components/ToolLayout";
import JSZip from "jszip";
interface IconSize {
name: string;
filename: string;
size: number;
purpose: string;
}
const ICON_SPECS: IconSize[] = [
{ name: "Favicon 16x16", filename: "favicon-16x16.png", size: 16, purpose: "Browser tabs" },
{ name: "Favicon 32x32", filename: "favicon-32x32.png", size: 32, purpose: "Taskbar / bookmarks" },
{ name: "Favicon 48x48", filename: "favicon-48x48.png", size: 48, purpose: "Desktop shortcut" },
{ name: "Apple Touch Icon", filename: "apple-touch-icon.png", size: 180, purpose: "iOS home screen" },
{ name: "Android Chrome 192", filename: "android-chrome-192x192.png", size: 192, purpose: "PWA icon" },
{ name: "Android Chrome 512", filename: "android-chrome-512x512.png", size: 512, purpose: "PWA splash screen" },
];
function createIcoFromPngs(pngBuffers: { size: number; buffer: ArrayBuffer }[]): Blob {
const numImages = pngBuffers.length;
const headerSize = 6;
const dirEntrySize = 16;
let dataOffset = headerSize + dirEntrySize * numImages;
const totalBytes = dataOffset + pngBuffers.reduce((acc, p) => acc + p.buffer.byteLength, 0);
const out = new Uint8Array(totalBytes);
const view = new DataView(out.buffer);
// ICO Header
view.setUint16(0, 0, true); // Reserved
view.setUint16(2, 1, true); // Type 1 = ICO
view.setUint16(4, numImages, true);
let currentOffset = dataOffset;
for (let i = 0; i < numImages; i++) {
const item = pngBuffers[i];
const entryPos = headerSize + i * dirEntrySize;
const w = item.size >= 256 ? 0 : item.size;
const h = item.size >= 256 ? 0 : item.size;
out[entryPos] = w;
out[entryPos + 1] = h;
out[entryPos + 2] = 0; // Colors (0 = no palette)
out[entryPos + 3] = 0; // Reserved
view.setUint16(entryPos + 4, 1, true); // Color planes
view.setUint16(entryPos + 6, 32, true); // Bits per pixel
view.setUint32(entryPos + 8, item.buffer.byteLength, true); // Image data size
view.setUint32(entryPos + 12, currentOffset, true); // Offset
out.set(new Uint8Array(item.buffer), currentOffset);
currentOffset += item.buffer.byteLength;
}
return new Blob([out], { type: "image/x-icon" });
}
export default function FaviconGeneratorPage() {
const [file, setFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [appName, setAppName] = useState("My App");
const [isGenerating, setIsGenerating] = useState(false);
const [generatedIcons, setGeneratedIcons] = useState<{ filename: string; url: string; size: number }[]>([]);
const [zipBlob, setZipBlob] = useState<Blob | null>(null);
const [copiedHtml, setCopiedHtml] = useState(false);
const [statusMessage, setStatusMessage] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null);
const generateIcons = useCallback(
async (sourceFile: File, name: string) => {
setIsGenerating(true);
setStatusMessage("Generating icon packages...");
try {
const img = new Image();
const objUrl = URL.createObjectURL(sourceFile);
img.src = objUrl;
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error("Unable to load source image"));
});
const zip = new JSZip();
const iconsList: { filename: string; url: string; size: number }[] = [];
const icoInputs: { size: number; buffer: ArrayBuffer }[] = [];
// Generate each PNG size
for (const spec of ICON_SPECS) {
const canvas = document.createElement("canvas");
canvas.width = spec.size;
canvas.height = spec.size;
const ctx = canvas.getContext("2d");
if (!ctx) continue;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
// Center crop to square
const minSide = Math.min(img.naturalWidth, img.naturalHeight);
const sx = (img.naturalWidth - minSide) / 2;
const sy = (img.naturalHeight - minSide) / 2;
ctx.drawImage(img, sx, sy, minSide, minSide, 0, 0, spec.size, spec.size);
const blob = await new Promise<Blob | null>((res) => canvas.toBlob(res, "image/png"));
if (!blob) continue;
const buffer = await blob.arrayBuffer();
zip.file(spec.filename, buffer);
const url = URL.createObjectURL(blob);
iconsList.push({ filename: spec.filename, url, size: spec.size });
if ([16, 32, 48].includes(spec.size)) {
icoInputs.push({ size: spec.size, buffer });
}
}
// Generate favicon.ico
if (icoInputs.length > 0) {
const icoBlob = createIcoFromPngs(icoInputs);
const icoBuffer = await icoBlob.arrayBuffer();
zip.file("favicon.ico", icoBuffer);
const icoUrl = URL.createObjectURL(icoBlob);
iconsList.unshift({ filename: "favicon.ico", url: icoUrl, size: 32 });
}
// Generate site.webmanifest
const webManifest = {
name: name || "App",
short_name: name || "App",
icons: [
{ src: "/android-chrome-192x192.png", sizes: "192x192", type: "image/png" },
{ src: "/android-chrome-512x512.png", sizes: "512x512", type: "image/png" },
],
theme_color: "#ffffff",
background_color: "#ffffff",
display: "standalone",
};
zip.file("site.webmanifest", JSON.stringify(webManifest, null, 2));
// Generate README with HTML snippet
const htmlSnippet = getHtmlTags(name);
zip.file("head-tags.html", htmlSnippet);
const packagedZip = await zip.generateAsync({ type: "blob" });
setZipBlob(packagedZip);
setGeneratedIcons(iconsList);
setStatusMessage("Package ready for download");
URL.revokeObjectURL(objUrl);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Generation failed";
setStatusMessage(msg);
} finally {
setIsGenerating(false);
}
},
[]
);
const handleFileSelect = (selected: File) => {
if (!selected.type.startsWith("image/")) {
setStatusMessage("Please upload a valid image file");
return;
}
setFile(selected);
if (previewUrl) URL.revokeObjectURL(previewUrl);
const url = URL.createObjectURL(selected);
setPreviewUrl(url);
generateIcons(selected, appName);
};
const handleDownloadZip = () => {
if (!zipBlob) return;
const link = document.createElement("a");
link.href = URL.createObjectURL(zipBlob);
link.download = "favicons.zip";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const getHtmlTags = (title: string) => {
return `<!-- Favicon & App Icons -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="apple-mobile-web-app-title" content="${title || "App"}">
<meta name="theme-color" content="#ffffff">`;
};
const handleCopyHtml = () => {
navigator.clipboard.writeText(getHtmlTags(appName));
setCopiedHtml(true);
setTimeout(() => setCopiedHtml(false), 2000);
};
return (
<ToolLayout
title="Favicon & App Icon Generator"
description="Convert any square logo or image into standard favicon.ico, Apple touch icons, Android PWA manifests, and HTML tags bundled into a ZIP."
>
<div className="space-y-6">
{/* Upload Box */}
<div
onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
if (e.dataTransfer.files?.[0]) handleFileSelect(e.dataTransfer.files[0]);
}}
className="cursor-pointer border-2 border-dashed border-[#222] bg-[#0c0c0c] hover:border-[#444] transition-colors p-8 text-center"
>
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => {
if (e.target.files?.[0]) handleFileSelect(e.target.files[0]);
}}
/>
<p className="text-sm font-medium text-gray-200">
{file ? file.name : "Drop master image or logo here"}
</p>
<p className="mt-1 text-xs text-[#666]">
Upload an image of at least 512x512 px for best quality.
</p>
</div>
{/* Configurations */}
<div className="border border-[#181818] bg-[#0c0c0c] p-4 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-3 w-full sm:w-auto">
<label className="text-xs font-mono text-[#666] uppercase whitespace-nowrap">App Name:</label>
<input
type="text"
value={appName}
onChange={(e) => {
setAppName(e.target.value);
if (file) generateIcons(file, e.target.value);
}}
className="px-3 py-1.5 bg-[#141414] border border-[#222] text-xs text-gray-200 focus:outline-none focus:border-blue-500 w-full sm:w-64"
placeholder="e.g. My Company"
/>
</div>
<div className="flex items-center gap-3">
<span className="text-xs font-mono text-[#555]">{statusMessage}</span>
{zipBlob && (
<button
onClick={handleDownloadZip}
disabled={isGenerating}
className="px-5 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-medium transition-colors"
>
Download Favicons (ZIP)
</button>
)}
</div>
</div>
{/* Results Display */}
{generatedIcons.length > 0 && (
<div className="space-y-6">
{/* Grid of Generated Icons */}
<div className="border border-[#181818] bg-[#0a0a0a] p-6">
<h3 className="text-xs font-mono uppercase tracking-wider text-[#666] mb-4">
Generated Assets ({generatedIcons.length})
</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-3">
{generatedIcons.map((icon) => (
<div key={icon.filename} className="border border-[#161616] bg-[#0e0e0e] p-3 text-center flex flex-col items-center justify-between">
<div className="h-16 flex items-center justify-center mb-2">
<img
src={icon.url}
alt={icon.filename}
style={{
maxWidth: "100%",
maxHeight: "100%",
width: Math.min(icon.size, 48),
height: Math.min(icon.size, 48),
}}
/>
</div>
<div className="w-full">
<p className="text-[11px] font-mono text-gray-300 truncate">{icon.filename}</p>
<p className="text-[10px] font-mono text-[#555]">{icon.size}x{icon.size}</p>
<a
href={icon.url}
download={icon.filename}
className="mt-2 block text-[10px] text-blue-400 hover:underline"
>
Download
</a>
</div>
</div>
))}
</div>
</div>
{/* HTML Code Snippet */}
<div className="border border-[#181818] bg-[#0a0a0a] p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-mono uppercase tracking-wider text-[#666]">
HTML Head Tags
</h3>
<button
onClick={handleCopyHtml}
className="px-3 py-1 bg-[#141414] hover:bg-[#1f1f1f] border border-[#262626] text-xs text-gray-300 transition-colors"
>
{copiedHtml ? "Copied" : "Copy Tags"}
</button>
</div>
<pre className="p-4 bg-[#050505] border border-[#141414] font-mono text-xs text-gray-300 overflow-x-auto leading-relaxed">
{getHtmlTags(appName)}
</pre>
</div>
</div>
)}
</div>
</ToolLayout>
);
}

View file

@ -0,0 +1,272 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import ToolLayout from "@/components/ToolLayout";
// Utility helpers for Uint8Array <-> Base64URL
function bufferToBase64Url(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
function base64UrlToBuffer(base64url: string): ArrayBuffer {
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
while (base64.length % 4) {
base64 += "=";
}
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
export default function SecretSharePage() {
const [secretText, setSecretText] = useState("");
const [generatedLink, setGeneratedLink] = useState("");
const [isCopied, setIsCopied] = useState(false);
const [mode, setMode] = useState<"create" | "view">("create");
const [decryptedText, setDecryptedText] = useState<string | null>(null);
const [decryptError, setDecryptError] = useState<string | null>(null);
const [isDecrypting, setIsDecrypting] = useState(false);
// Check URL hash on mount for view mode
useEffect(() => {
if (typeof window === "undefined") return;
const hash = window.location.hash.substring(1);
if (!hash) return;
const params = new URLSearchParams(hash);
if (params.get("d") && params.get("k")) {
setMode("view");
}
}, []);
const handleCreateSecret = async () => {
if (!secretText.trim()) return;
try {
// 1. Generate 256-bit AES-GCM key
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
// 2. Export raw key bytes
const exportedKey = await crypto.subtle.exportKey("raw", key);
const keyB64 = bufferToBase64Url(exportedKey);
// 3. Random 12-byte initialization vector (IV)
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoder = new TextEncoder();
const plaintext = encoder.encode(secretText);
// 4. Encrypt
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
);
// Pack IV (12 bytes) + Ciphertext into single payload
const combined = new Uint8Array(iv.byteLength + ciphertext.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ciphertext), iv.byteLength);
const dataB64 = bufferToBase64Url(combined.buffer);
const origin = window.location.origin;
const path = window.location.pathname;
const fullUrl = `${origin}${path}#d=${dataB64}&k=${keyB64}`;
setGeneratedLink(fullUrl);
} catch (err) {
console.error(err);
}
};
const handleDecrypt = useCallback(async () => {
setIsDecrypting(true);
setDecryptError(null);
try {
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const dataB64 = params.get("d");
const keyB64 = params.get("k");
if (!dataB64 || !keyB64) {
throw new Error("Invalid or corrupted secret link parameters");
}
const keyBuffer = base64UrlToBuffer(keyB64);
const dataBuffer = base64UrlToBuffer(dataB64);
if (dataBuffer.byteLength < 13) {
throw new Error("Ciphertext too short or corrupted");
}
const iv = new Uint8Array(dataBuffer.slice(0, 12));
const ciphertext = dataBuffer.slice(12);
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyBuffer,
"AES-GCM",
false,
["decrypt"]
);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
cryptoKey,
ciphertext
);
const decoder = new TextDecoder();
setDecryptedText(decoder.decode(decrypted));
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Decryption failed";
setDecryptError(msg);
} finally {
setIsDecrypting(false);
}
}, []);
const handleDestroy = () => {
setDecryptedText(null);
setDecryptError(null);
window.location.hash = "";
setMode("create");
setSecretText("");
setGeneratedLink("");
};
const handleCopyLink = () => {
navigator.clipboard.writeText(generatedLink);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
return (
<ToolLayout
title="Client-Side Secret Link"
description="Zero-knowledge password & token sharing. Encrypted client-side via AES-256-GCM. The decryption key stays strictly in the URL hash and never touches any server."
>
<div className="space-y-6">
{mode === "create" ? (
<>
<div className="border border-[#181818] bg-[#0c0c0c] p-6 space-y-4">
<label className="block text-xs font-mono uppercase tracking-wider text-[#666]">
Confidential Secret / Note
</label>
<textarea
value={secretText}
onChange={(e) => setSecretText(e.target.value)}
rows={6}
placeholder="Paste API keys, database credentials, passwords, or private messages..."
className="w-full p-4 bg-[#050505] border border-[#222] text-xs font-mono text-gray-200 placeholder-[#444] focus:outline-none focus:border-blue-500"
/>
<div className="flex flex-wrap items-center justify-between gap-4 pt-2">
<p className="text-[11px] text-[#555]">
Encrypted locally using browser Web Crypto API (AES-GCM 256-bit).
</p>
<button
onClick={handleCreateSecret}
disabled={!secretText.trim()}
className="px-6 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white text-xs font-medium transition-colors"
>
Generate Encrypted Link
</button>
</div>
</div>
{generatedLink && (
<div className="border border-[#181818] bg-[#0a0a0a] p-6 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-xs font-mono uppercase tracking-wider text-[#666]">
Shareable Link
</h3>
<button
onClick={handleCopyLink}
className="px-4 py-1.5 bg-[#141414] hover:bg-[#1f1f1f] border border-[#262626] text-xs text-gray-200 transition-colors"
>
{isCopied ? "Copied to Clipboard" : "Copy Link"}
</button>
</div>
<div className="p-3 bg-[#050505] border border-[#141414] font-mono text-xs text-blue-400 break-all select-all">
{generatedLink}
</div>
<div className="p-4 border border-[#1c1c1c] bg-[#101010] text-xs text-[#777] leading-relaxed">
<strong className="text-gray-300">How this works:</strong> The URL hash (`#d=...&k=...`) stores the 256-bit encryption key and ciphertext. Per internet standards (RFC 3986), URL hashes are never transmitted across HTTP requests, meaning neither our servers nor your ISP can intercept or read the message.
</div>
</div>
)}
</>
) : (
<div className="border border-[#181818] bg-[#0c0c0c] p-6 space-y-6">
<div className="border-b border-[#181818] pb-4">
<h3 className="text-sm font-medium text-gray-200">Encrypted Secret Received</h3>
<p className="mt-1 text-xs text-[#666]">
This message was encrypted using AES-256-GCM. Click decrypt to decode using the key embedded in your URL fragment.
</p>
</div>
{decryptedText === null ? (
<div className="text-center py-8">
<button
onClick={handleDecrypt}
disabled={isDecrypting}
className="px-8 py-3 bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold tracking-wider uppercase transition-colors"
>
{isDecrypting ? "Decrypting..." : "Decrypt Secret"}
</button>
{decryptError && (
<p className="mt-4 text-xs font-mono text-red-400">{decryptError}</p>
)}
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-xs font-mono uppercase tracking-wider text-[#666]">
Decrypted Content
</span>
<div className="flex gap-2">
<button
onClick={() => navigator.clipboard.writeText(decryptedText)}
className="px-3 py-1 bg-[#141414] hover:bg-[#1f1f1f] border border-[#262626] text-xs text-gray-300 transition-colors"
>
Copy Secret
</button>
<button
onClick={handleDestroy}
className="px-3 py-1 bg-red-950/30 hover:bg-red-900/40 border border-red-800/40 text-xs text-red-300 transition-colors"
>
Destroy & Clear
</button>
</div>
</div>
<pre className="p-4 bg-[#050505] border border-[#181818] font-mono text-xs text-gray-200 whitespace-pre-wrap break-all select-all leading-relaxed">
{decryptedText}
</pre>
</div>
)}
</div>
)}
</div>
</ToolLayout>
);
}

View file

@ -0,0 +1,282 @@
"use client";
import { useState, useRef, useCallback } from "react";
import ToolLayout from "@/components/ToolLayout";
export default function SvgToPngPage() {
const [svgSource, setSvgSource] = useState<string>("");
const [fileName, setFileName] = useState<string>("vector");
const [naturalDimensions, setNaturalDimensions] = useState<{ w: number; h: number } | null>(null);
const [scaleFactor, setScaleFactor] = useState<number>(2);
const [customWidth, setCustomWidth] = useState<number>(0);
const [customHeight, setCustomHeight] = useState<number>(0);
const [useCustomSize, setUseCustomSize] = useState<boolean>(false);
const [bgType, setBgType] = useState<"transparent" | "white" | "black">("transparent");
const [exportFormat, setExportFormat] = useState<"image/png" | "image/webp">("image/png");
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [statusMessage, setStatusMessage] = useState<string>("");
const fileInputRef = useRef<HTMLInputElement>(null);
const parseSvgDimensions = useCallback((svgText: string): { w: number; h: number } => {
const parser = new DOMParser();
const doc = parser.parseFromString(svgText, "image/svg+xml");
const svgEl = doc.querySelector("svg");
if (!svgEl) return { w: 512, h: 512 };
const viewBox = svgEl.getAttribute("viewBox");
if (viewBox) {
const parts = viewBox.split(/[\s,]+/).map(Number);
if (parts.length >= 4 && parts[2] > 0 && parts[3] > 0) {
return { w: parts[2], h: parts[3] };
}
}
const wAttr = parseFloat(svgEl.getAttribute("width") || "0");
const hAttr = parseFloat(svgEl.getAttribute("height") || "0");
if (wAttr > 0 && hAttr > 0) {
return { w: wAttr, h: hAttr };
}
return { w: 512, h: 512 };
}, []);
const handleFileSelect = (selected: File) => {
if (!selected.name.endsWith(".svg") && selected.type !== "image/svg+xml") {
setStatusMessage("Please upload a valid .svg file");
return;
}
setFileName(selected.name.replace(/\.svg$/i, ""));
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target?.result as string;
setSvgSource(text);
const dims = parseSvgDimensions(text);
setNaturalDimensions(dims);
setCustomWidth(dims.w * scaleFactor);
setCustomHeight(dims.h * scaleFactor);
setStatusMessage(`Loaded SVG (${dims.w}x${dims.h} base)`);
};
reader.readAsText(selected);
};
const handleExport = async () => {
if (!svgSource || !naturalDimensions) return;
setIsProcessing(true);
setStatusMessage("Rendering high-resolution vector...");
try {
const targetW = useCustomSize ? customWidth : naturalDimensions.w * scaleFactor;
const targetH = useCustomSize ? customHeight : naturalDimensions.h * scaleFactor;
const blob = new Blob([svgSource], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(blob);
const img = new Image();
img.src = url;
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error("Unable to rasterize SVG"));
});
const canvas = document.createElement("canvas");
canvas.width = targetW;
canvas.height = targetH;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Canvas context unavailable");
// Fill background if not transparent
if (bgType === "white") {
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, targetW, targetH);
} else if (bgType === "black") {
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, targetW, targetH);
}
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.drawImage(img, 0, 0, targetW, targetH);
URL.revokeObjectURL(url);
const ext = exportFormat === "image/png" ? "png" : "webp";
const outBlob = await new Promise<Blob | null>((res) => canvas.toBlob(res, exportFormat, 0.95));
if (!outBlob) throw new Error("Export failed");
const downloadUrl = URL.createObjectURL(outBlob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = `${fileName}_${targetW}x${targetH}.${ext}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(downloadUrl);
setStatusMessage(`Exported ${targetW}x${targetH} px successfully`);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Render error";
setStatusMessage(msg);
} finally {
setIsProcessing(false);
}
};
return (
<ToolLayout
title="SVG to High-Res Raster Converter"
description="Rasterize scalable SVG vectors into ultra high-resolution PNG or WebP images (up to 8K) with custom dimensions and transparent backgrounds."
>
<div className="space-y-6">
{/* Upload Box */}
<div
onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
if (e.dataTransfer.files?.[0]) handleFileSelect(e.dataTransfer.files[0]);
}}
className="cursor-pointer border-2 border-dashed border-[#222] bg-[#0c0c0c] hover:border-[#444] transition-colors p-8 text-center"
>
<input
ref={fileInputRef}
type="file"
accept=".svg,image/svg+xml"
className="hidden"
onChange={(e) => {
if (e.target.files?.[0]) handleFileSelect(e.target.files[0]);
}}
/>
<p className="text-sm font-medium text-gray-200">
{svgSource ? `${fileName}.svg loaded` : "Drop .svg vector file here"}
</p>
<p className="mt-1 text-xs text-[#666]">
Supports standard SVG paths, shapes, gradients, and inline styles.
</p>
{naturalDimensions && (
<p className="mt-2 text-xs font-mono text-blue-400">
Base dimensions: {naturalDimensions.w}x{naturalDimensions.h} px
</p>
)}
</div>
{svgSource && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* SVG Visual Preview */}
<div className="border border-[#181818] bg-[#0a0a0a] p-6 flex flex-col items-center justify-center min-h-[300px]">
<div
className={`w-full h-full min-h-[240px] flex items-center justify-center p-4 border border-[#1e1e1e] ${
bgType === "transparent"
? "bg-[radial-gradient(#1a1a1a_1px,transparent_1px)] [background-size:12px_12px]"
: bgType === "white"
? "bg-white"
: "bg-black"
}`}
dangerouslySetInnerHTML={{ __html: svgSource }}
/>
</div>
{/* Render Settings */}
<div className="border border-[#181818] bg-[#0a0a0a] p-6 space-y-6">
{/* Scale Presets */}
<div>
<label className="block text-xs font-mono uppercase tracking-wider text-[#666] mb-3">
Resolution Scale
</label>
<div className="grid grid-cols-5 gap-2">
{[1, 2, 4, 8, 16].map((scale) => (
<button
key={scale}
onClick={() => {
setScaleFactor(scale);
setUseCustomSize(false);
}}
className={`py-2 text-center border text-xs font-mono transition-colors ${
!useCustomSize && scaleFactor === scale
? "border-blue-500/60 bg-blue-950/20 text-blue-200"
: "border-[#1c1c1c] bg-[#101010] text-gray-400 hover:border-[#282828]"
}`}
>
{scale}x
</button>
))}
</div>
{naturalDimensions && (
<p className="mt-2 text-xs font-mono text-[#555]">
Output resolution: {naturalDimensions.w * scaleFactor}x{naturalDimensions.h * scaleFactor} px
</p>
)}
</div>
{/* Background Color */}
<div>
<label className="block text-xs font-mono uppercase tracking-wider text-[#666] mb-3">
Background
</label>
<div className="grid grid-cols-3 gap-2">
{(["transparent", "white", "black"] as const).map((bg) => (
<button
key={bg}
onClick={() => setBgType(bg)}
className={`py-2 text-center border text-xs capitalize transition-colors ${
bgType === bg
? "border-blue-500/60 bg-blue-950/20 text-blue-200"
: "border-[#1c1c1c] bg-[#101010] text-gray-400 hover:border-[#282828]"
}`}
>
{bg}
</button>
))}
</div>
</div>
{/* Format Selection */}
<div>
<label className="block text-xs font-mono uppercase tracking-wider text-[#666] mb-3">
Target Format
</label>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setExportFormat("image/png")}
className={`py-2 text-center border text-xs transition-colors ${
exportFormat === "image/png"
? "border-blue-500/60 bg-blue-950/20 text-blue-200"
: "border-[#1c1c1c] bg-[#101010] text-gray-400 hover:border-[#282828]"
}`}
>
PNG (Lossless)
</button>
<button
onClick={() => setExportFormat("image/webp")}
className={`py-2 text-center border text-xs transition-colors ${
exportFormat === "image/webp"
? "border-blue-500/60 bg-blue-950/20 text-blue-200"
: "border-[#1c1c1c] bg-[#101010] text-gray-400 hover:border-[#282828]"
}`}
>
WebP (Compressed)
</button>
</div>
</div>
{/* Action */}
<div className="border-t border-[#181818] pt-4 flex items-center justify-between">
<span className="text-xs font-mono text-[#555]">{statusMessage}</span>
<button
onClick={handleExport}
disabled={isProcessing}
className="px-6 py-2.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-xs font-medium transition-colors"
>
{isProcessing ? "Rendering..." : "Export High-Res Image"}
</button>
</div>
</div>
</div>
)}
</div>
</ToolLayout>
);
}

View file

@ -36,6 +36,20 @@ export const tools: Tool[] = [
category: "Files & Media", category: "Files & Media",
featured: true, featured: true,
}, },
{
name: "Discord Emoji Resizer",
slug: "discord-emoji-resizer",
description: "Resize, square-crop, and compress emojis and stickers to Discord limits",
category: "Files & Media",
featured: true,
},
{
name: "SVG to High-Res",
slug: "svg-to-png",
description: "Rasterize SVG vectors to high-resolution PNG or WebP images up to 8K",
category: "Files & Media",
featured: true,
},
// Formatters & Validators // Formatters & Validators
{ name: "JSON Formatter", slug: "json-formatter", description: "Format, validate, and beautify JSON data", category: "Formatters" }, { name: "JSON Formatter", slug: "json-formatter", description: "Format, validate, and beautify JSON data", category: "Formatters" },
@ -53,10 +67,12 @@ export const tools: Tool[] = [
{ name: "Hash Generator", slug: "hash-generator", description: "Compute MD5, SHA-1, SHA-256, and SHA-512 hashes", category: "Generators" }, { name: "Hash Generator", slug: "hash-generator", description: "Compute MD5, SHA-1, SHA-256, and SHA-512 hashes", category: "Generators" },
{ name: "QR Code Generator", slug: "qr-code", description: "Create QR codes from URLs or plain text", category: "Generators" }, { name: "QR Code Generator", slug: "qr-code", description: "Create QR codes from URLs or plain text", category: "Generators" },
{ name: "Password Generator", slug: "password-generator", description: "Generate randomized secure passwords", category: "Generators" }, { name: "Password Generator", slug: "password-generator", description: "Generate randomized secure passwords", category: "Generators" },
{ name: "Secret Link", slug: "secret-share", description: "Client-side AES-256 encrypted password and credential sharing via URL hash", category: "Generators", featured: true },
// Converters // Converters
{ name: "Unix Timestamp", slug: "timestamp-converter", description: "Convert between Unix epoch and standard date formats", category: "Converters" }, { name: "Unix Timestamp", slug: "timestamp-converter", description: "Convert between Unix epoch and standard date formats", category: "Converters" },
{ name: "Color Converter", slug: "color-converter", description: "Convert colors between HEX, RGB, and HSL formats", category: "Converters" }, { name: "Color Converter", slug: "color-converter", description: "Convert colors between HEX, RGB, and HSL formats", category: "Converters" },
{ name: "Palette Extractor", slug: "color-palette-extractor", description: "Extract dominant color palettes and hex codes from any image", category: "Converters", featured: true },
{ name: "Number Base", slug: "number-base", description: "Convert numbers across binary, octal, decimal, and hex", category: "Converters" }, { name: "Number Base", slug: "number-base", description: "Convert numbers across binary, octal, decimal, and hex", category: "Converters" },
// Text Tools // Text Tools
@ -77,6 +93,7 @@ export const tools: Tool[] = [
{ name: "XML Formatter", slug: "xml-formatter", description: "Format, indent, and validate XML documents", category: "Data Tools" }, { name: "XML Formatter", slug: "xml-formatter", description: "Format, indent, and validate XML documents", category: "Data Tools" },
// Web Tools // Web Tools
{ name: "Favicon Generator", slug: "favicon-generator", description: "Convert images into multi-size favicon.ico, Apple touch icons, and PWA manifests", category: "Web Tools", featured: true },
{ name: "Meta Tag Generator", slug: "meta-tag-generator", description: "Generate OpenGraph, Twitter, and SEO HTML tags", category: "Web Tools" }, { name: "Meta Tag Generator", slug: "meta-tag-generator", description: "Generate OpenGraph, Twitter, and SEO HTML tags", category: "Web Tools" },
{ name: "Slug Generator", slug: "slug-generator", description: "Create URL-safe slugs from plain text strings", category: "Web Tools" }, { name: "Slug Generator", slug: "slug-generator", description: "Create URL-safe slugs from plain text strings", category: "Web Tools" },
{ name: "Cron Parser", slug: "cron-parser", description: "Parse and explain 5-field cron schedules in plain text", category: "Web Tools" }, { name: "Cron Parser", slug: "cron-parser", description: "Parse and explain 5-field cron schedules in plain text", category: "Web Tools" },