@achimismaili/easy-web-brand 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @achimismaili/easy-web-brand
2
+
3
+ Logo trimming and favicon generation for the easy-web ecosystem.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add -D @achimismaili/easy-web-brand
9
+ ```
10
+
11
+ ## Usage (CLI)
12
+
13
+ ```bash
14
+ easy-web-brand trim src/assets/logos
15
+ easy-web-brand favicons src/assets/logos --out src/assets/logos/favicons
16
+ ```
17
+
18
+ ## Usage (API)
19
+
20
+ ```ts
21
+ import { trimFolder, generateFaviconsForFolder } from '@achimismaili/easy-web-brand';
22
+
23
+ await trimFolder('src/assets/logos');
24
+ await generateFaviconsForFolder('src/assets/logos', 'src/assets/logos/favicons');
25
+ ```
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/trim.ts
4
+ import sharp from "sharp";
5
+ import { readdir, writeFile } from "fs/promises";
6
+ import { join, extname } from "path";
7
+ async function trimLogos(input, opts) {
8
+ const threshold = opts?.threshold ?? 10;
9
+ const minOutputDim = opts?.minOutputDim ?? 50;
10
+ const paddingPct = opts?.paddingPct ?? 2;
11
+ const minPaddingPx = opts?.minPaddingPx ?? 10;
12
+ const origMeta = await sharp(input).metadata();
13
+ const origW = origMeta.width;
14
+ const origH = origMeta.height;
15
+ const { data: rawData, info: rawInfo } = await sharp(input).raw().toBuffer({ resolveWithObject: true });
16
+ const hasAlpha = rawInfo.channels === 4;
17
+ const topLeftAlpha = hasAlpha ? rawData[3] : 255;
18
+ const mode = hasAlpha && topLeftAlpha === 0 ? "transparent" : "white";
19
+ const { data: trimmedPng, info: trimInfo } = await sharp(input).trim({ threshold }).png().toBuffer({ resolveWithObject: true });
20
+ const trimW = trimInfo.width;
21
+ const trimH = trimInfo.height;
22
+ if (trimW === origW && trimH === origH) {
23
+ const originalBuffer = Buffer.isBuffer(input) ? input : await sharp(input).png().toBuffer();
24
+ return {
25
+ buffer: originalBuffer,
26
+ changed: false,
27
+ before: { w: origW, h: origH },
28
+ after: { w: trimW, h: trimH }
29
+ };
30
+ }
31
+ if (trimW < minOutputDim || trimH < minOutputDim) {
32
+ const label = typeof input === "string" ? input : "<buffer>";
33
+ throw new Error(
34
+ `trim produced pathologically small output: ${trimW}x${trimH}, refusing to overwrite ${label}`
35
+ );
36
+ }
37
+ const pad = Math.max(minPaddingPx, Math.round(paddingPct / 100 * Math.min(trimW, trimH)));
38
+ const background = mode === "transparent" ? { r: 0, g: 0, b: 0, alpha: 0 } : { r: 255, g: 255, b: 255, alpha: 1 };
39
+ const resultBuffer = await sharp(trimmedPng).extend({ top: pad, bottom: pad, left: pad, right: pad, background }).png({ compressionLevel: 9, palette: false }).toBuffer();
40
+ const finalMeta = await sharp(resultBuffer).metadata();
41
+ return {
42
+ buffer: resultBuffer,
43
+ changed: true,
44
+ before: { w: origW, h: origH },
45
+ after: { w: finalMeta.width, h: finalMeta.height }
46
+ };
47
+ }
48
+ async function trimFolder(folderPath, opts) {
49
+ const files = await readdir(folderPath);
50
+ const pngs = files.filter((f) => extname(f).toLowerCase() === ".png");
51
+ const processed = [];
52
+ const skipped = [];
53
+ for (const file of pngs) {
54
+ const fullPath = join(folderPath, file);
55
+ const result = await trimLogos(fullPath, opts);
56
+ if (result.changed) {
57
+ await writeFile(fullPath, result.buffer);
58
+ processed.push(file);
59
+ } else {
60
+ skipped.push(file);
61
+ }
62
+ }
63
+ return { processed, skipped };
64
+ }
65
+
66
+ // src/favicons.ts
67
+ import sharp2 from "sharp";
68
+ import pngToIco from "png-to-ico";
69
+ import { mkdir, readdir as readdir2, writeFile as writeFile2 } from "fs/promises";
70
+ import { join as join2, extname as extname2, basename, resolve } from "path";
71
+ async function generateFavicons(input, outDir, baseName) {
72
+ await mkdir(outDir, { recursive: true });
73
+ const [buf16, buf32, buf48, buf180, buf512] = await Promise.all([
74
+ // 16px: nearest-neighbor for pixel-crisp tiny icon
75
+ sharp2(input).resize(16, 16, {
76
+ fit: "contain",
77
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
78
+ kernel: sharp2.kernel.nearest
79
+ }).png().toBuffer(),
80
+ // 32px and above: lanczos3 for quality
81
+ sharp2(input).resize(32, 32, {
82
+ fit: "contain",
83
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
84
+ kernel: sharp2.kernel.lanczos3
85
+ }).png().toBuffer(),
86
+ sharp2(input).resize(48, 48, {
87
+ fit: "contain",
88
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
89
+ kernel: sharp2.kernel.lanczos3
90
+ }).png().toBuffer(),
91
+ sharp2(input).resize(180, 180, {
92
+ fit: "contain",
93
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
94
+ kernel: sharp2.kernel.lanczos3
95
+ }).png().toBuffer(),
96
+ sharp2(input).resize(512, 512, {
97
+ fit: "contain",
98
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
99
+ kernel: sharp2.kernel.lanczos3
100
+ }).png().toBuffer()
101
+ ]);
102
+ const icoBuffer = await pngToIco([buf16, buf32, buf48]);
103
+ const icoPath = resolve(outDir, `${baseName}.ico`);
104
+ const png32Path = resolve(outDir, `${baseName}-32.png`);
105
+ const png180Path = resolve(outDir, `${baseName}-180.png`);
106
+ const png512Path = resolve(outDir, `${baseName}-512.png`);
107
+ await Promise.all([
108
+ writeFile2(icoPath, icoBuffer),
109
+ writeFile2(png32Path, buf32),
110
+ writeFile2(png180Path, buf180),
111
+ writeFile2(png512Path, buf512)
112
+ ]);
113
+ return { ico: icoPath, png32: png32Path, png180: png180Path, png512: png512Path };
114
+ }
115
+ async function generateFaviconsForFolder(folderPath, outDir) {
116
+ const files = await readdir2(folderPath);
117
+ const sources = files.filter(
118
+ (f) => extname2(f).toLowerCase() === ".png" && !basename(f, ".png").endsWith("-white")
119
+ );
120
+ const results = [];
121
+ for (const file of sources) {
122
+ const base = basename(file, ".png");
123
+ const outputs = await generateFavicons(join2(folderPath, file), outDir, base);
124
+ results.push({ base, outputs });
125
+ }
126
+ return results;
127
+ }
128
+
129
+ export {
130
+ trimLogos,
131
+ trimFolder,
132
+ generateFavicons,
133
+ generateFaviconsForFolder
134
+ };
135
+ //# sourceMappingURL=chunk-K657YEVU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/trim.ts","../src/favicons.ts"],"sourcesContent":["import sharp from 'sharp';\nimport { readdir, writeFile } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\n\nexport interface TrimOptions {\n paddingPct?: number; // default 2\n minPaddingPx?: number; // default 10\n threshold?: number; // default 10\n minOutputDim?: number; // default 50\n}\n\nexport interface TrimResult {\n buffer: Buffer;\n changed: boolean;\n before: { w: number; h: number };\n after: { w: number; h: number };\n}\n\n/**\n * Trim solid-color / transparent borders from a logo PNG and re-pad it with a small\n * background-aware margin. Auto-detects transparent vs white background by inspecting\n * the top-left pixel of the raw input.\n *\n * Sharp API note: `.metadata()` on a chained `.trim()` pipeline returns the ORIGINAL\n * header dimensions (it does not evaluate operations). To read post-trim dimensions we\n * must materialize the buffer via `.toBuffer({ resolveWithObject: true })` and read\n * `info.width` / `info.height`.\n */\nexport async function trimLogos(\n input: string | Buffer,\n opts?: TrimOptions,\n): Promise<TrimResult> {\n const threshold = opts?.threshold ?? 10;\n const minOutputDim = opts?.minOutputDim ?? 50;\n const paddingPct = opts?.paddingPct ?? 2;\n const minPaddingPx = opts?.minPaddingPx ?? 10;\n\n // Read original dimensions from the input header.\n const origMeta = await sharp(input).metadata();\n const origW = origMeta.width!;\n const origH = origMeta.height!;\n\n // Detect background mode from the top-left pixel of the ORIGINAL image.\n const { data: rawData, info: rawInfo } = await sharp(input)\n .raw()\n .toBuffer({ resolveWithObject: true });\n const hasAlpha = rawInfo.channels === 4;\n const topLeftAlpha = hasAlpha ? rawData[3]! : 255;\n const mode: 'transparent' | 'white' = hasAlpha && topLeftAlpha === 0 ? 'transparent' : 'white';\n\n // Perform the trim and capture the ACTUAL post-trim dimensions from the resulting buffer.\n const { data: trimmedPng, info: trimInfo } = await sharp(input)\n .trim({ threshold })\n .png()\n .toBuffer({ resolveWithObject: true });\n const trimW = trimInfo.width;\n const trimH = trimInfo.height;\n\n // NO-OP SKIP: trim found nothing to remove — return the original bytes unchanged.\n if (trimW === origW && trimH === origH) {\n const originalBuffer = Buffer.isBuffer(input) ? input : await sharp(input).png().toBuffer();\n return {\n buffer: originalBuffer,\n changed: false,\n before: { w: origW, h: origH },\n after: { w: trimW, h: trimH },\n };\n }\n\n // OVER-TRIM GUARD: refuse to produce pathologically small outputs (e.g. threshold too high).\n if (trimW < minOutputDim || trimH < minOutputDim) {\n const label = typeof input === 'string' ? input : '<buffer>';\n throw new Error(\n `trim produced pathologically small output: ${trimW}x${trimH}, refusing to overwrite ${label}`,\n );\n }\n\n // Add background-aware padding. paddingPct is a percentage of the shorter trimmed edge;\n // minPaddingPx is a floor so tiny logos still get a comfortable margin.\n const pad = Math.max(minPaddingPx, Math.round((paddingPct / 100) * Math.min(trimW, trimH)));\n const background =\n mode === 'transparent'\n ? { r: 0, g: 0, b: 0, alpha: 0 }\n : { r: 255, g: 255, b: 255, alpha: 1 };\n\n const resultBuffer = await sharp(trimmedPng)\n .extend({ top: pad, bottom: pad, left: pad, right: pad, background })\n .png({ compressionLevel: 9, palette: false })\n .toBuffer();\n\n const finalMeta = await sharp(resultBuffer).metadata();\n return {\n buffer: resultBuffer,\n changed: true,\n before: { w: origW, h: origH },\n after: { w: finalMeta.width!, h: finalMeta.height! },\n };\n}\n\n/**\n * Trim every `*.png` in `folderPath` in-place. Files where the trim is a no-op are left\n * on disk untouched (skipped). Files where trim shrinks the image are overwritten with\n * the padded, re-encoded PNG.\n */\nexport async function trimFolder(\n folderPath: string,\n opts?: TrimOptions,\n): Promise<{ processed: string[]; skipped: string[] }> {\n const files = await readdir(folderPath);\n const pngs = files.filter((f) => extname(f).toLowerCase() === '.png');\n\n const processed: string[] = [];\n const skipped: string[] = [];\n\n for (const file of pngs) {\n const fullPath = join(folderPath, file);\n const result = await trimLogos(fullPath, opts);\n if (result.changed) {\n await writeFile(fullPath, result.buffer);\n processed.push(file);\n } else {\n skipped.push(file);\n }\n }\n\n return { processed, skipped };\n}\n","import sharp from 'sharp';\nimport pngToIco from 'png-to-ico';\nimport { mkdir, readdir, writeFile } from 'node:fs/promises';\nimport { join, extname, basename, resolve } from 'node:path';\n\nexport interface FaviconOutputs {\n ico: string;\n png32: string;\n png180: string;\n png512: string;\n}\n\n/**\n * Generate a multi-resolution ICO (16/32/48 px frames) and three PNG sizes\n * (32×32, 180×180, 512×512) from a single transparent-background source PNG.\n *\n * All outputs are written to `outDir` with the given `baseName`:\n * <baseName>.ico, <baseName>-32.png, <baseName>-180.png, <baseName>-512.png\n *\n * Kernel choice: 16px uses `nearest` for pixel-crisp tiny icons; 32/48/180/512 use\n * `lanczos3` for high-quality downscaling. Background is always transparent so the\n * source PNG's alpha channel is preserved through the resize pipeline.\n */\nexport async function generateFavicons(\n input: string | Buffer,\n outDir: string,\n baseName: string,\n): Promise<FaviconOutputs> {\n await mkdir(outDir, { recursive: true });\n\n // Resize to all required sizes in parallel.\n const [buf16, buf32, buf48, buf180, buf512] = await Promise.all([\n // 16px: nearest-neighbor for pixel-crisp tiny icon\n sharp(input)\n .resize(16, 16, {\n fit: 'contain',\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n kernel: sharp.kernel.nearest,\n })\n .png()\n .toBuffer(),\n // 32px and above: lanczos3 for quality\n sharp(input)\n .resize(32, 32, {\n fit: 'contain',\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n kernel: sharp.kernel.lanczos3,\n })\n .png()\n .toBuffer(),\n sharp(input)\n .resize(48, 48, {\n fit: 'contain',\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n kernel: sharp.kernel.lanczos3,\n })\n .png()\n .toBuffer(),\n sharp(input)\n .resize(180, 180, {\n fit: 'contain',\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n kernel: sharp.kernel.lanczos3,\n })\n .png()\n .toBuffer(),\n sharp(input)\n .resize(512, 512, {\n fit: 'contain',\n background: { r: 0, g: 0, b: 0, alpha: 0 },\n kernel: sharp.kernel.lanczos3,\n })\n .png()\n .toBuffer(),\n ]);\n\n // Build multi-res ICO from 16/32/48 frames. png-to-ico takes an array of PNG buffers\n // and returns a single ICO buffer whose frame count equals the input array length.\n const icoBuffer = await pngToIco([buf16, buf32, buf48]);\n\n const icoPath = resolve(outDir, `${baseName}.ico`);\n const png32Path = resolve(outDir, `${baseName}-32.png`);\n const png180Path = resolve(outDir, `${baseName}-180.png`);\n const png512Path = resolve(outDir, `${baseName}-512.png`);\n\n await Promise.all([\n writeFile(icoPath, icoBuffer),\n writeFile(png32Path, buf32),\n writeFile(png180Path, buf180),\n writeFile(png512Path, buf512),\n ]);\n\n return { ico: icoPath, png32: png32Path, png180: png180Path, png512: png512Path };\n}\n\n/**\n * Generate favicon sets for every transparent-background logo in `folderPath`.\n * Files ending with `-white` are skipped (white-background logos produce ugly favicons).\n * Outputs land in `outDir` named after each source base name.\n */\nexport async function generateFaviconsForFolder(\n folderPath: string,\n outDir: string,\n): Promise<Array<{ base: string; outputs: FaviconOutputs }>> {\n const files = await readdir(folderPath);\n const sources = files.filter(\n (f) => extname(f).toLowerCase() === '.png' && !basename(f, '.png').endsWith('-white'),\n );\n\n const results: Array<{ base: string; outputs: FaviconOutputs }> = [];\n for (const file of sources) {\n const base = basename(file, '.png');\n const outputs = await generateFavicons(join(folderPath, file), outDir, base);\n results.push({ base, outputs });\n }\n return results;\n}\n"],"mappings":"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","names":["sharp","readdir","writeFile","join","extname"]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ generateFaviconsForFolder,
4
+ trimFolder
5
+ } from "./chunk-K657YEVU.js";
6
+
7
+ // src/cli.ts
8
+ import mri from "mri";
9
+ var USAGE = `
10
+ easy-web-brand \u2014 logo trimming and favicon generation for the easy-web ecosystem
11
+
12
+ Usage:
13
+ easy-web-brand trim <folder> [options]
14
+ easy-web-brand favicons <folder> --out=<outDir> [options]
15
+ easy-web-brand --help
16
+
17
+ Commands:
18
+ trim Trim blank canvas from all *.png files in <folder> in-place.
19
+ favicons Generate multi-res ICO + PNG favicon sets from transparent logos in <folder>.
20
+
21
+ Options:
22
+ --out=<dir> Output directory for favicon generation (required for favicons command)
23
+ --padding=<pct> Padding percentage after trim (default: 2)
24
+ --min-pad=<px> Minimum padding in pixels (default: 10)
25
+ --threshold=<n> Trim threshold: color similarity 0-100 (default: 10)
26
+ -h, --help Show this help message
27
+ `.trim();
28
+ async function main() {
29
+ const argv = mri(process.argv.slice(2), {
30
+ boolean: ["help", "h"],
31
+ string: ["out"],
32
+ alias: { h: "help" }
33
+ });
34
+ if (argv.help || argv._.length === 0) {
35
+ process.stdout.write(USAGE + "\n");
36
+ process.exit(0);
37
+ }
38
+ const [command, folder] = argv._;
39
+ if (command !== "trim" && command !== "favicons") {
40
+ process.stderr.write(`Unknown command: "${command}"
41
+
42
+ ${USAGE}
43
+ `);
44
+ process.exit(1);
45
+ }
46
+ if (!folder) {
47
+ process.stderr.write(`Error: <folder> argument is required.
48
+
49
+ ${USAGE}
50
+ `);
51
+ process.exit(1);
52
+ }
53
+ if (command === "trim") {
54
+ const opts = {
55
+ paddingPct: argv["padding"] !== void 0 ? Number(argv["padding"]) : void 0,
56
+ minPaddingPx: argv["min-pad"] !== void 0 ? Number(argv["min-pad"]) : void 0,
57
+ threshold: argv["threshold"] !== void 0 ? Number(argv["threshold"]) : void 0
58
+ };
59
+ try {
60
+ const result = await trimFolder(folder, opts);
61
+ process.stdout.write(
62
+ `Trim complete: ${result.processed.length} processed, ${result.skipped.length} skipped.
63
+ `
64
+ );
65
+ for (const f of result.processed) process.stdout.write(` trimmed: ${f}
66
+ `);
67
+ for (const f of result.skipped) process.stdout.write(` skipped: ${f}
68
+ `);
69
+ } catch (err) {
70
+ process.stderr.write(
71
+ `Error during trim: ${err instanceof Error ? err.message : String(err)}
72
+ `
73
+ );
74
+ process.exit(2);
75
+ }
76
+ } else if (command === "favicons") {
77
+ const outDir = argv["out"];
78
+ if (!outDir) {
79
+ process.stderr.write(`Error: --out=<dir> is required for the favicons command.
80
+
81
+ ${USAGE}
82
+ `);
83
+ process.exit(1);
84
+ }
85
+ try {
86
+ const results = await generateFaviconsForFolder(folder, outDir);
87
+ process.stdout.write(`Favicons complete: ${results.length} sets generated.
88
+ `);
89
+ for (const r of results) {
90
+ process.stdout.write(` ${r.base}: ico, 32, 180, 512
91
+ `);
92
+ }
93
+ } catch (err) {
94
+ process.stderr.write(
95
+ `Error during favicons: ${err instanceof Error ? err.message : String(err)}
96
+ `
97
+ );
98
+ process.exit(2);
99
+ }
100
+ }
101
+ }
102
+ main();
103
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import mri from 'mri';\nimport { trimFolder } from './trim.js';\nimport { generateFaviconsForFolder } from './favicons.js';\n\nconst USAGE = `\neasy-web-brand — logo trimming and favicon generation for the easy-web ecosystem\n\nUsage:\n easy-web-brand trim <folder> [options]\n easy-web-brand favicons <folder> --out=<outDir> [options]\n easy-web-brand --help\n\nCommands:\n trim Trim blank canvas from all *.png files in <folder> in-place.\n favicons Generate multi-res ICO + PNG favicon sets from transparent logos in <folder>.\n\nOptions:\n --out=<dir> Output directory for favicon generation (required for favicons command)\n --padding=<pct> Padding percentage after trim (default: 2)\n --min-pad=<px> Minimum padding in pixels (default: 10)\n --threshold=<n> Trim threshold: color similarity 0-100 (default: 10)\n -h, --help Show this help message\n`.trim();\n\nasync function main(): Promise<void> {\n const argv = mri(process.argv.slice(2), {\n boolean: ['help', 'h'],\n string: ['out'],\n alias: { h: 'help' },\n });\n\n if (argv.help || argv._.length === 0) {\n process.stdout.write(USAGE + '\\n');\n process.exit(0);\n }\n\n const [command, folder] = argv._;\n\n if (command !== 'trim' && command !== 'favicons') {\n process.stderr.write(`Unknown command: \"${command}\"\\n\\n${USAGE}\\n`);\n process.exit(1);\n }\n\n if (!folder) {\n process.stderr.write(`Error: <folder> argument is required.\\n\\n${USAGE}\\n`);\n process.exit(1);\n }\n\n if (command === 'trim') {\n const opts = {\n paddingPct: argv['padding'] !== undefined ? Number(argv['padding']) : undefined,\n minPaddingPx: argv['min-pad'] !== undefined ? Number(argv['min-pad']) : undefined,\n threshold: argv['threshold'] !== undefined ? Number(argv['threshold']) : undefined,\n };\n try {\n const result = await trimFolder(folder, opts);\n process.stdout.write(\n `Trim complete: ${result.processed.length} processed, ${result.skipped.length} skipped.\\n`,\n );\n for (const f of result.processed) process.stdout.write(` trimmed: ${f}\\n`);\n for (const f of result.skipped) process.stdout.write(` skipped: ${f}\\n`);\n } catch (err) {\n process.stderr.write(\n `Error during trim: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n process.exit(2);\n }\n } else if (command === 'favicons') {\n const outDir = argv['out'];\n if (!outDir) {\n process.stderr.write(`Error: --out=<dir> is required for the favicons command.\\n\\n${USAGE}\\n`);\n process.exit(1);\n }\n try {\n const results = await generateFaviconsForFolder(folder, outDir);\n process.stdout.write(`Favicons complete: ${results.length} sets generated.\\n`);\n for (const r of results) {\n process.stdout.write(` ${r.base}: ico, 32, 180, 512\\n`);\n }\n } catch (err) {\n process.stderr.write(\n `Error during favicons: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n process.exit(2);\n }\n }\n}\n\nmain();\n"],"mappings":";;;;;;;AAAA,OAAO,SAAS;AAIhB,IAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBZ,KAAK;AAEP,eAAe,OAAsB;AACnC,QAAM,OAAO,IAAI,QAAQ,KAAK,MAAM,CAAC,GAAG;AAAA,IACtC,SAAS,CAAC,QAAQ,GAAG;AAAA,IACrB,QAAQ,CAAC,KAAK;AAAA,IACd,OAAO,EAAE,GAAG,OAAO;AAAA,EACrB,CAAC;AAED,MAAI,KAAK,QAAQ,KAAK,EAAE,WAAW,GAAG;AACpC,YAAQ,OAAO,MAAM,QAAQ,IAAI;AACjC,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,CAAC,SAAS,MAAM,IAAI,KAAK;AAE/B,MAAI,YAAY,UAAU,YAAY,YAAY;AAChD,YAAQ,OAAO,MAAM,qBAAqB,OAAO;AAAA;AAAA,EAAQ,KAAK;AAAA,CAAI;AAClE,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,CAAC,QAAQ;AACX,YAAQ,OAAO,MAAM;AAAA;AAAA,EAA4C,KAAK;AAAA,CAAI;AAC1E,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,YAAY,QAAQ;AACtB,UAAM,OAAO;AAAA,MACX,YAAY,KAAK,SAAS,MAAM,SAAY,OAAO,KAAK,SAAS,CAAC,IAAI;AAAA,MACtE,cAAc,KAAK,SAAS,MAAM,SAAY,OAAO,KAAK,SAAS,CAAC,IAAI;AAAA,MACxE,WAAW,KAAK,WAAW,MAAM,SAAY,OAAO,KAAK,WAAW,CAAC,IAAI;AAAA,IAC3E;AACA,QAAI;AACF,YAAM,SAAS,MAAM,WAAW,QAAQ,IAAI;AAC5C,cAAQ,OAAO;AAAA,QACb,kBAAkB,OAAO,UAAU,MAAM,eAAe,OAAO,QAAQ,MAAM;AAAA;AAAA,MAC/E;AACA,iBAAW,KAAK,OAAO,UAAW,SAAQ,OAAO,MAAM,cAAc,CAAC;AAAA,CAAI;AAC1E,iBAAW,KAAK,OAAO,QAAS,SAAQ,OAAO,MAAM,cAAc,CAAC;AAAA,CAAI;AAAA,IAC1E,SAAS,KAAK;AACZ,cAAQ,OAAO;AAAA,QACb,sBAAsB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,MACxE;AACA,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF,WAAW,YAAY,YAAY;AACjC,UAAM,SAAS,KAAK,KAAK;AACzB,QAAI,CAAC,QAAQ;AACX,cAAQ,OAAO,MAAM;AAAA;AAAA,EAA+D,KAAK;AAAA,CAAI;AAC7F,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,QAAI;AACF,YAAM,UAAU,MAAM,0BAA0B,QAAQ,MAAM;AAC9D,cAAQ,OAAO,MAAM,sBAAsB,QAAQ,MAAM;AAAA,CAAoB;AAC7E,iBAAW,KAAK,SAAS;AACvB,gBAAQ,OAAO,MAAM,KAAK,EAAE,IAAI;AAAA,CAAuB;AAAA,MACzD;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,OAAO;AAAA,QACb,0BAA0B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,MAC5E;AACA,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF;AACF;AAEA,KAAK;","names":[]}
@@ -0,0 +1,68 @@
1
+ interface TrimOptions {
2
+ paddingPct?: number;
3
+ minPaddingPx?: number;
4
+ threshold?: number;
5
+ minOutputDim?: number;
6
+ }
7
+ interface TrimResult {
8
+ buffer: Buffer;
9
+ changed: boolean;
10
+ before: {
11
+ w: number;
12
+ h: number;
13
+ };
14
+ after: {
15
+ w: number;
16
+ h: number;
17
+ };
18
+ }
19
+ /**
20
+ * Trim solid-color / transparent borders from a logo PNG and re-pad it with a small
21
+ * background-aware margin. Auto-detects transparent vs white background by inspecting
22
+ * the top-left pixel of the raw input.
23
+ *
24
+ * Sharp API note: `.metadata()` on a chained `.trim()` pipeline returns the ORIGINAL
25
+ * header dimensions (it does not evaluate operations). To read post-trim dimensions we
26
+ * must materialize the buffer via `.toBuffer({ resolveWithObject: true })` and read
27
+ * `info.width` / `info.height`.
28
+ */
29
+ declare function trimLogos(input: string | Buffer, opts?: TrimOptions): Promise<TrimResult>;
30
+ /**
31
+ * Trim every `*.png` in `folderPath` in-place. Files where the trim is a no-op are left
32
+ * on disk untouched (skipped). Files where trim shrinks the image are overwritten with
33
+ * the padded, re-encoded PNG.
34
+ */
35
+ declare function trimFolder(folderPath: string, opts?: TrimOptions): Promise<{
36
+ processed: string[];
37
+ skipped: string[];
38
+ }>;
39
+
40
+ interface FaviconOutputs {
41
+ ico: string;
42
+ png32: string;
43
+ png180: string;
44
+ png512: string;
45
+ }
46
+ /**
47
+ * Generate a multi-resolution ICO (16/32/48 px frames) and three PNG sizes
48
+ * (32×32, 180×180, 512×512) from a single transparent-background source PNG.
49
+ *
50
+ * All outputs are written to `outDir` with the given `baseName`:
51
+ * <baseName>.ico, <baseName>-32.png, <baseName>-180.png, <baseName>-512.png
52
+ *
53
+ * Kernel choice: 16px uses `nearest` for pixel-crisp tiny icons; 32/48/180/512 use
54
+ * `lanczos3` for high-quality downscaling. Background is always transparent so the
55
+ * source PNG's alpha channel is preserved through the resize pipeline.
56
+ */
57
+ declare function generateFavicons(input: string | Buffer, outDir: string, baseName: string): Promise<FaviconOutputs>;
58
+ /**
59
+ * Generate favicon sets for every transparent-background logo in `folderPath`.
60
+ * Files ending with `-white` are skipped (white-background logos produce ugly favicons).
61
+ * Outputs land in `outDir` named after each source base name.
62
+ */
63
+ declare function generateFaviconsForFolder(folderPath: string, outDir: string): Promise<Array<{
64
+ base: string;
65
+ outputs: FaviconOutputs;
66
+ }>>;
67
+
68
+ export { type FaviconOutputs, type TrimOptions, type TrimResult, generateFavicons, generateFaviconsForFolder, trimFolder, trimLogos };
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ generateFavicons,
4
+ generateFaviconsForFolder,
5
+ trimFolder,
6
+ trimLogos
7
+ } from "./chunk-K657YEVU.js";
8
+ export {
9
+ generateFavicons,
10
+ generateFaviconsForFolder,
11
+ trimFolder,
12
+ trimLogos
13
+ };
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@achimismaili/easy-web-brand",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./cli": {
13
+ "import": "./dist/cli.js"
14
+ }
15
+ },
16
+ "bin": {
17
+ "easy-web-brand": "./dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "lint": "eslint src"
28
+ },
29
+ "dependencies": {
30
+ "mri": "^1.2.0",
31
+ "png-to-ico": "^3.0.2",
32
+ "sharp": "^0.34.5"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^25.0.0",
36
+ "eslint": "^9.0.0",
37
+ "execa": "^9.6.1",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.5.0",
40
+ "vitest": "^2.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "packageManager": "pnpm@10.34.4",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/achimismaili/easy-web.git",
49
+ "directory": "packages/easy-web-brand"
50
+ },
51
+ "license": "MIT"
52
+ }