logo-soup 0.1.0 → 0.2.1
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 +50 -5
- package/dist/cli.mjs +24 -11
- package/dist/index.mjs +1 -1
- package/dist/{normalize-BN3StFfP.mjs → normalize-B5apzI38.mjs} +2 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ logo-soup analyzes SVG/PNG images with `sharp`, detects their content bounding b
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
pnpm add logo-soup
|
|
13
|
+
pnpm add -D logo-soup
|
|
14
14
|
|
|
15
15
|
# Or run directly
|
|
16
16
|
npx logo-soup ./logos
|
|
@@ -22,19 +22,64 @@ npx logo-soup ./logos
|
|
|
22
22
|
logo-soup ./public/logos -o logo-metrics.json
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Output
|
|
25
|
+
Output keys are filenames only (e.g. `"logo.svg"`), so you can prepend your own base path.
|
|
26
26
|
|
|
27
27
|
```
|
|
28
28
|
logo-soup <dir> [options]
|
|
29
29
|
|
|
30
30
|
Options:
|
|
31
31
|
--output, -o Output JSON file path (default: "logo-metrics.json")
|
|
32
|
-
--base-size Base size for normalization in px (default:
|
|
32
|
+
--base-size Base size for normalization in px (default: 64)
|
|
33
33
|
--scale-factor Aspect ratio normalization 0-1 (default: 0.5)
|
|
34
34
|
--density-factor Density compensation 0-1 (default: 0.5)
|
|
35
35
|
--extensions, -e Comma-separated file extensions (default: "svg,png")
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Running the CLI produces a JSON file mapping each filename to its normalized dimensions:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"acme-wordmark.svg": { "width": 143, "height": 28, "offsetX": 0, "offsetY": 0.5 },
|
|
45
|
+
"globex-icon.svg": { "width": 64, "height": 64, "offsetX": 0, "offsetY": 0 },
|
|
46
|
+
"initech-monogram.svg": { "width": 52, "height": 79, "offsetX": -0.4, "offsetY": 0 }
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`width` and `height` are pixel dimensions normalized so every logo feels the same visual size – use them directly as `width`/`height` attributes. `offsetX`/`offsetY` are optional sub-pixel corrections (see [Visual Center Offsets](#visual-center-offsets)).
|
|
51
|
+
|
|
52
|
+
### Logo Strip
|
|
53
|
+
|
|
54
|
+
The most common use case: a "trusted by" row or partner logo strip. Apply `width` and `height` directly – the normalization ensures all logos feel balanced side by side.
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<div class="flex flex-wrap items-center justify-center gap-8">
|
|
58
|
+
<img src="/logos/acme-wordmark.svg" width="143" height="28" alt="Acme">
|
|
59
|
+
<img src="/logos/globex-icon.svg" width="64" height="64" alt="Globex">
|
|
60
|
+
<img src="/logos/initech-monogram.svg" width="52" height="79" alt="Initech">
|
|
61
|
+
</div>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> [!TIP]
|
|
65
|
+
> The default `baseSize` is 64px, so a square logo renders at 64×64px. For a different base, pass `--base-size <n>` to the CLI (or `{ baseSize }` to `normalize()`), or scale uniformly with CSS – the proportions stay correct either way.
|
|
66
|
+
|
|
67
|
+
### Visual Center Offsets
|
|
68
|
+
|
|
69
|
+
Some logos have visual weight that doesn't match their geometric center – a play button or an arrow, for instance. `offsetX`/`offsetY` correct for this by nudging the logo toward its optical center.
|
|
70
|
+
|
|
71
|
+
These offsets are typically small (< 2px) and `width`/`height` alone handle most of the balancing. For pixel-perfect alignment, apply them as CSS transforms:
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<img src="/logos/acme-wordmark.svg" width="143" height="28" style="transform: translate(0px, 0.5px)">
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For horizontal strips where only vertical alignment matters, you can apply just the Y offset:
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<img src="/logos/acme-wordmark.svg" width="143" height="28" style="transform: translateY(0.5px)">
|
|
81
|
+
```
|
|
82
|
+
|
|
38
83
|
## Programmatic API
|
|
39
84
|
|
|
40
85
|
```ts
|
|
@@ -43,7 +88,7 @@ import { analyze, analyzeDirectory, normalize } from 'logo-soup'
|
|
|
43
88
|
// Single file
|
|
44
89
|
const metrics = await analyze('./logo.svg')
|
|
45
90
|
if (metrics) {
|
|
46
|
-
const dimensions = normalize(metrics
|
|
91
|
+
const dimensions = normalize(metrics)
|
|
47
92
|
console.log(dimensions) // { width, height, offsetX, offsetY }
|
|
48
93
|
}
|
|
49
94
|
|
|
@@ -104,7 +149,7 @@ Converts raw metrics into display dimensions using aspect ratio normalization wi
|
|
|
104
149
|
function normalize(metrics: Metrics, options?: NormalizeOptions): NormalizedDimensions
|
|
105
150
|
|
|
106
151
|
interface NormalizeOptions {
|
|
107
|
-
/** Base size in pixels (default:
|
|
152
|
+
/** Base size in pixels (default: 64) */
|
|
108
153
|
baseSize?: number
|
|
109
154
|
/** Aspect ratio normalization factor, 0–1 (default: 0.5) */
|
|
110
155
|
scaleFactor?: number
|
package/dist/cli.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { c as DENSITY_FACTOR, d as SCALE_FACTOR, i as BASE_SIZE, o as DEFAULT_EXTENSIONS, r as analyzeDirectory, t as normalize } from "./normalize-
|
|
1
|
+
import { c as DENSITY_FACTOR, d as SCALE_FACTOR, i as BASE_SIZE, o as DEFAULT_EXTENSIONS, r as analyzeDirectory, t as normalize } from "./normalize-B5apzI38.mjs";
|
|
2
2
|
import * as fsp from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
6
6
|
import { consola } from "consola";
|
|
7
|
+
import { colors } from "consola/utils";
|
|
7
8
|
|
|
8
9
|
//#region package.json
|
|
9
10
|
var name = "logo-soup";
|
|
10
|
-
var version = "0.1
|
|
11
|
+
var version = "0.2.1";
|
|
11
12
|
var description = "Normalize logo dimensions for visual balance";
|
|
12
13
|
|
|
13
14
|
//#endregion
|
|
@@ -62,10 +63,9 @@ const command = defineCommand({
|
|
|
62
63
|
const baseSize = parseNumericArg(args["base-size"], "base-size", BASE_SIZE);
|
|
63
64
|
const scaleFactor = parseNumericArg(args["scale-factor"], "scale-factor", SCALE_FACTOR);
|
|
64
65
|
const densityFactor = parseNumericArg(args["density-factor"], "density-factor", DENSITY_FACTOR);
|
|
65
|
-
const
|
|
66
|
-
consola.start(`Analyzing logos in ${dirPath}`);
|
|
67
|
-
const metricsMap = await analyzeDirectory(dirPath, { extensions });
|
|
66
|
+
const metricsMap = await analyzeDirectory(dirPath, { extensions: args.extensions ? args.extensions.split(",").map((ext) => ext.trim().toLowerCase()) : DEFAULT_EXTENSIONS });
|
|
68
67
|
const results = {};
|
|
68
|
+
const entries = [];
|
|
69
69
|
for (const [file, metrics] of metricsMap) {
|
|
70
70
|
const dimensions = normalize(metrics, {
|
|
71
71
|
baseSize,
|
|
@@ -73,22 +73,35 @@ const command = defineCommand({
|
|
|
73
73
|
densityFactor
|
|
74
74
|
});
|
|
75
75
|
results[file] = dimensions;
|
|
76
|
-
|
|
76
|
+
entries.push([file, dimensions]);
|
|
77
|
+
}
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(`${colors.cyan("●")} ${colors.bold(name)} ${colors.dim(`v${version}`)}`);
|
|
80
|
+
console.log();
|
|
81
|
+
const maxEntryLength = Math.max(...entries.map(([entry]) => entry.length));
|
|
82
|
+
const total = entries.length;
|
|
83
|
+
for (const [i, [file, dimensions]] of entries.entries()) {
|
|
84
|
+
const branch = i === total - 1 ? "└─" : "├─";
|
|
85
|
+
const dimStr = `${dimensions.width}${colors.dim("×")}${dimensions.height}`;
|
|
86
|
+
const padding = " ".repeat(maxEntryLength - file.length + 2);
|
|
87
|
+
console.log(` ${colors.dim(branch)} ${colors.cyan(file)}${padding}${dimStr}`);
|
|
77
88
|
}
|
|
78
89
|
const outputPath = path.resolve(args.output);
|
|
79
90
|
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
|
|
80
91
|
await fsp.writeFile(outputPath, `${JSON.stringify(results, null, 2)}\n`);
|
|
81
|
-
|
|
92
|
+
const relativeOutput = path.relative(process.cwd(), outputPath);
|
|
93
|
+
console.log();
|
|
94
|
+
consola.success(`Wrote ${colors.bold(String(total))} entries to ${colors.cyan(relativeOutput)}`);
|
|
82
95
|
}
|
|
83
96
|
});
|
|
84
97
|
function parseNumericArg(value, name, fallback) {
|
|
85
|
-
if (value
|
|
86
|
-
const
|
|
87
|
-
if (Number.isNaN(
|
|
98
|
+
if (value == null) return fallback;
|
|
99
|
+
const parsedNumber = Number(value);
|
|
100
|
+
if (Number.isNaN(parsedNumber)) {
|
|
88
101
|
consola.error(`Invalid value for --${name}: "${value}" (expected a number)`);
|
|
89
102
|
process.exit(1);
|
|
90
103
|
}
|
|
91
|
-
return
|
|
104
|
+
return parsedNumber;
|
|
92
105
|
}
|
|
93
106
|
runMain(command);
|
|
94
107
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as CONTRAST_THRESHOLD, c as DENSITY_FACTOR, d as SCALE_FACTOR, i as BASE_SIZE, l as REFERENCE_DENSITY, n as analyze, o as DEFAULT_EXTENSIONS, r as analyzeDirectory, s as DENSITY_DAMPENING, t as normalize, u as SAMPLE_MAX_SIZE } from "./normalize-
|
|
1
|
+
import { a as CONTRAST_THRESHOLD, c as DENSITY_FACTOR, d as SCALE_FACTOR, i as BASE_SIZE, l as REFERENCE_DENSITY, n as analyze, o as DEFAULT_EXTENSIONS, r as analyzeDirectory, s as DENSITY_DAMPENING, t as normalize, u as SAMPLE_MAX_SIZE } from "./normalize-B5apzI38.mjs";
|
|
2
2
|
|
|
3
3
|
export { BASE_SIZE, CONTRAST_THRESHOLD, DEFAULT_EXTENSIONS, DENSITY_DAMPENING, DENSITY_FACTOR, REFERENCE_DENSITY, SAMPLE_MAX_SIZE, SCALE_FACTOR, analyze, analyzeDirectory, normalize };
|
|
@@ -8,7 +8,7 @@ const SAMPLE_MAX_SIZE = 200;
|
|
|
8
8
|
/** Minimum contrast threshold to consider a pixel as content. */
|
|
9
9
|
const CONTRAST_THRESHOLD = 10;
|
|
10
10
|
/** Base size in pixels for the normalized output. */
|
|
11
|
-
const BASE_SIZE =
|
|
11
|
+
const BASE_SIZE = 64;
|
|
12
12
|
/** Aspect ratio normalization factor (0–1). */
|
|
13
13
|
const SCALE_FACTOR = .5;
|
|
14
14
|
/** Density compensation factor (0–1). */
|
|
@@ -38,10 +38,7 @@ async function analyze(filePath, options = {}) {
|
|
|
38
38
|
}
|
|
39
39
|
async function analyzeDirectory(dirPath, options = {}) {
|
|
40
40
|
const { extensions = DEFAULT_EXTENSIONS, ...analyzeOptions } = options;
|
|
41
|
-
const files = (await fsp.readdir(dirPath)).filter((
|
|
42
|
-
const ext = path.extname(fileName).slice(1).toLowerCase();
|
|
43
|
-
return extensions.includes(ext);
|
|
44
|
-
});
|
|
41
|
+
const files = (await fsp.readdir(dirPath)).filter((entry) => extensions.includes(path.extname(entry).slice(1).toLowerCase()));
|
|
45
42
|
const results = /* @__PURE__ */ new Map();
|
|
46
43
|
for (const file of files) {
|
|
47
44
|
const absolutePath = path.resolve(dirPath, file);
|
package/package.json
CHANGED