@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 +25 -0
- package/dist/chunk-K657YEVU.js +135 -0
- package/dist/chunk-K657YEVU.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +103 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
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
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
|
package/dist/cli.js.map
ADDED
|
@@ -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":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|