@visant/logo-trace 0.1.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 +87 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/presets.d.ts +13 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +31 -0
- package/dist/presets.js.map +1 -0
- package/dist/sanitize.d.ts +20 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +135 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/trace.d.ts +17 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +152 -0
- package/dist/trace.js.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# @visant/logo-trace
|
|
2
|
+
|
|
3
|
+
Raster → **SVG** vectorization tuned for logos. [Potrace](https://www.npmjs.com/package/potrace) under the hood, wrapped with the bits that make a logo trace cleanly:
|
|
4
|
+
|
|
5
|
+
- **Otsu auto-threshold** — picks the black/white cutoff per image instead of a fixed 128.
|
|
6
|
+
- **Auto-invert** — light-on-light art (e.g. a gray wordmark on white) is inverted automatically before tracing, then inverted-retried if the first pass comes back empty.
|
|
7
|
+
- **Calibrated presets** — `logo`, `lettering`, `lineArt`, `stamp`.
|
|
8
|
+
- **Sanitize + optimize** — output is run through DOMPurify and a numeric/editor-cruft minifier, so you get a small, safe SVG string.
|
|
9
|
+
|
|
10
|
+
It powers the Visant Labs PNG→SVG trace endpoint and the Studio 3D GLB export (image → traced SVG → extruded mesh).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm i @visant/logo-trace
|
|
16
|
+
# node-first peers (the server already has these):
|
|
17
|
+
npm i sharp jsdom dompurify
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`potrace` is a direct dependency. `sharp` (Otsu grayscale + invert), `jsdom` and `dompurify` (the sanitize/optimize pass) are **optional peer dependencies**, imported lazily. Without `sharp`, `threshold: 'auto'` degrades to a fixed 128; the sanitize pass requires `jsdom` + `dompurify`.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { trace } from '@visant/logo-trace';
|
|
26
|
+
import { readFile } from 'node:fs/promises';
|
|
27
|
+
|
|
28
|
+
const png = await readFile('logo.png');
|
|
29
|
+
|
|
30
|
+
// Named preset:
|
|
31
|
+
const svg = await trace(png, { preset: 'logo' });
|
|
32
|
+
// → '<svg ...><path d="..."/></svg>'
|
|
33
|
+
|
|
34
|
+
// Or explicit options (override any preset value):
|
|
35
|
+
const svg2 = await trace(png, {
|
|
36
|
+
preset: 'lettering',
|
|
37
|
+
threshold: 'auto', // Otsu + auto-invert
|
|
38
|
+
turdSize: 2, // drop speckles < 2px²
|
|
39
|
+
optTolerance: 0.2, // curve smoothing
|
|
40
|
+
alphaMax: 0.8, // corner roundness
|
|
41
|
+
color: '#111111',
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`trace` is an alias of `tracePipeline` (trace → sanitize → optimize). Use `traceImage` if you want the raw potrace output without the cleanup pass.
|
|
46
|
+
|
|
47
|
+
## Exports
|
|
48
|
+
|
|
49
|
+
| Entry | Purpose |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `.` | everything below, flat |
|
|
52
|
+
| `./presets` | `TRACE_PRESETS`, `resolveTraceOptions` |
|
|
53
|
+
| `./sanitize` | `parseBase64Image`, `sanitizeSvg`, `optimizeSvg`, `cleanSvg` |
|
|
54
|
+
|
|
55
|
+
### API
|
|
56
|
+
|
|
57
|
+
| Export | Signature |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `trace` / `tracePipeline` | `(buffer, opts?) => Promise<string>` — full pipeline |
|
|
60
|
+
| `traceImage` | `(buffer, opts?) => Promise<string>` — raw potrace SVG, no cleanup |
|
|
61
|
+
| `cleanSvgPipeline` / `cleanSvg` | `(rawSvg) => Promise<string>` — sanitize + optimize an existing SVG |
|
|
62
|
+
| `parseBase64Image` | `(dataUri) => Buffer \| null` — parse a `data:image/...;base64,...` URI |
|
|
63
|
+
| `TRACE_PRESETS` | calibrated parameter sets per preset |
|
|
64
|
+
| `resolveTraceOptions` | `(opts) => TraceOptions` — merge a preset under explicit options |
|
|
65
|
+
|
|
66
|
+
## Presets
|
|
67
|
+
|
|
68
|
+
| Preset | turdSize | optTolerance | threshold | alphaMax |
|
|
69
|
+
| --- | --- | --- | --- | --- |
|
|
70
|
+
| `logo` | 3 | 0.3 | auto | 0.8 |
|
|
71
|
+
| `lettering` | 1 | 0.15 | auto | 0.5 |
|
|
72
|
+
| `lineArt` | 0 | 0.1 | 128 | 1.0 |
|
|
73
|
+
| `stamp` | 5 | 0.5 | auto | 0.8 |
|
|
74
|
+
|
|
75
|
+
## From a base64 data URI
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { parseBase64Image, trace } from '@visant/logo-trace';
|
|
79
|
+
|
|
80
|
+
const buffer = parseBase64Image(dataUri); // null on a non-image / prefix-less string
|
|
81
|
+
if (!buffer) throw new Error('Invalid base64 image format');
|
|
82
|
+
const svg = await trace(buffer, { preset: 'logo' });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { trace, tracePipeline, traceImage, cleanSvgPipeline } from './trace.js';
|
|
2
|
+
export { TRACE_PRESETS, resolveTraceOptions } from './presets.js';
|
|
3
|
+
export { parseBase64Image, sanitizeSvg, optimizeSvg, cleanSvg } from './sanitize.js';
|
|
4
|
+
export type { TracePreset, TraceOptions, SvgOptimizeOptions } from './types.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGhF,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAGlE,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGrF,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @visant/logo-trace — public surface.
|
|
2
|
+
// Raster → SVG vectorization tuned for logos: potrace + Otsu auto-threshold
|
|
3
|
+
// (with auto-invert for light-on-light art) + calibrated presets + a
|
|
4
|
+
// sanitize/optimize pass that returns a clean, minified SVG string.
|
|
5
|
+
// ── Core ──
|
|
6
|
+
export { trace, tracePipeline, traceImage, cleanSvgPipeline } from './trace.js';
|
|
7
|
+
// ── Presets ──
|
|
8
|
+
export { TRACE_PRESETS, resolveTraceOptions } from './presets.js';
|
|
9
|
+
// ── SVG sanitize / optimize / data-URI parsing ──
|
|
10
|
+
export { parseBase64Image, sanitizeSvg, optimizeSvg, cleanSvg } from './sanitize.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,4EAA4E;AAC5E,qEAAqE;AACrE,oEAAoE;AAEpE,aAAa;AACb,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEhF,gBAAgB;AAChB,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAElE,mDAAmD;AACnD,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TracePreset, TraceOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Calibrated potrace parameter sets per use-case. Values are tuned for clean
|
|
4
|
+
* logo/lettering vectorization and must not drift — they are the same numbers
|
|
5
|
+
* the Visant trace endpoint has shipped.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TRACE_PRESETS: Record<Exclude<TracePreset, 'custom'>, Required<Omit<TraceOptions, 'color' | 'preset'>>>;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a preset into concrete options. Explicit options always win over the
|
|
10
|
+
* preset's base values. Non-preset (or 'custom') input is returned untouched.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveTraceOptions(opts: TraceOptions): TraceOptions;
|
|
13
|
+
//# sourceMappingURL=presets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAChC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,EAC9B,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAC,CAMjD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAapE"}
|
package/dist/presets.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @visant/logo-trace — calibrated trace presets (single source of truth).
|
|
2
|
+
/**
|
|
3
|
+
* Calibrated potrace parameter sets per use-case. Values are tuned for clean
|
|
4
|
+
* logo/lettering vectorization and must not drift — they are the same numbers
|
|
5
|
+
* the Visant trace endpoint has shipped.
|
|
6
|
+
*/
|
|
7
|
+
export const TRACE_PRESETS = {
|
|
8
|
+
logo: { turdSize: 3, optTolerance: 0.3, threshold: 'auto', alphaMax: 0.8 },
|
|
9
|
+
lettering: { turdSize: 1, optTolerance: 0.15, threshold: 'auto', alphaMax: 0.5 },
|
|
10
|
+
lineArt: { turdSize: 0, optTolerance: 0.1, threshold: 128, alphaMax: 1.0 },
|
|
11
|
+
stamp: { turdSize: 5, optTolerance: 0.5, threshold: 'auto', alphaMax: 0.8 },
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a preset into concrete options. Explicit options always win over the
|
|
15
|
+
* preset's base values. Non-preset (or 'custom') input is returned untouched.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveTraceOptions(opts) {
|
|
18
|
+
if (opts.preset && opts.preset !== 'custom' && TRACE_PRESETS[opts.preset]) {
|
|
19
|
+
const base = TRACE_PRESETS[opts.preset];
|
|
20
|
+
return {
|
|
21
|
+
turdSize: opts.turdSize ?? base.turdSize,
|
|
22
|
+
optTolerance: opts.optTolerance ?? base.optTolerance,
|
|
23
|
+
threshold: opts.threshold ?? base.threshold,
|
|
24
|
+
alphaMax: opts.alphaMax ?? base.alphaMax,
|
|
25
|
+
color: opts.color,
|
|
26
|
+
preset: opts.preset,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return opts;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=presets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presets.js","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAI1E;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAGtB;IACF,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE;IAC1E,SAAS,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE;IAChF,OAAO,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE;IAC1E,KAAK,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE;CAC5E,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAkB;IACpD,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1E,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxC,OAAO;YACL,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ;YACxC,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY;YACpD,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS;YAC3C,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ;YACxC,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,MAAM,EAAE,IAAI,CAAC,MAAM;SACpB,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SvgOptimizeOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a `data:image/<mime>;base64,<payload>` string into a Buffer.
|
|
4
|
+
* Returns `null` when the string is not a base64 image data-URI — callers use
|
|
5
|
+
* that to reject bad input (e.g. respond 400). It validates the prefix rather
|
|
6
|
+
* than stripping it, so a raw (prefix-less) base64 blob is intentionally
|
|
7
|
+
* rejected. The MIME pattern is wide enough to match e.g. `image/svg+xml`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseBase64Image(image: string): Buffer | null;
|
|
10
|
+
/** Strip scripts / unsafe content from an SVG string, keeping SVG + filters + <use>. */
|
|
11
|
+
export declare function sanitizeSvg(svg: string): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Strip editor cruft, comments, metadata, hidden/empty nodes and minify numeric
|
|
14
|
+
* literals from an SVG string. Pure string transforms except the optional
|
|
15
|
+
* `removeHiddenElements` pass, which needs jsdom.
|
|
16
|
+
*/
|
|
17
|
+
export declare function optimizeSvg(svgString: string, options?: Partial<SvgOptimizeOptions>): Promise<string>;
|
|
18
|
+
/** sanitize → optimize. Used to clean SVG pasted from Figma/Illustrator/etc. */
|
|
19
|
+
export declare function cleanSvg(raw: string): Promise<string>;
|
|
20
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIrD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI7D;AAsBD,wFAAwF;AACxF,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9D;AAmBD;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,GACpC,OAAO,CAAC,MAAM,CAAC,CA2EjB;AAED,gFAAgF;AAChF,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG3D"}
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// @visant/logo-trace — SVG sanitization + optimization + data-URI parsing.
|
|
2
|
+
//
|
|
3
|
+
// `jsdom` and `dompurify` are optional peers, imported lazily. They are only
|
|
4
|
+
// needed for the sanitize/optimize pass (the server always has them); the trace
|
|
5
|
+
// core can run without them if you skip cleanup.
|
|
6
|
+
// ── data-URI parsing ────────────────────────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Parse a `data:image/<mime>;base64,<payload>` string into a Buffer.
|
|
9
|
+
* Returns `null` when the string is not a base64 image data-URI — callers use
|
|
10
|
+
* that to reject bad input (e.g. respond 400). It validates the prefix rather
|
|
11
|
+
* than stripping it, so a raw (prefix-less) base64 blob is intentionally
|
|
12
|
+
* rejected. The MIME pattern is wide enough to match e.g. `image/svg+xml`.
|
|
13
|
+
*/
|
|
14
|
+
export function parseBase64Image(image) {
|
|
15
|
+
const match = image.match(/^data:image\/[^;]+;base64,(.+)$/i);
|
|
16
|
+
if (!match)
|
|
17
|
+
return null;
|
|
18
|
+
return Buffer.from(match[1], 'base64');
|
|
19
|
+
}
|
|
20
|
+
// ── SVG sanitization (DOMPurify) ─────────────────────────────────────────────
|
|
21
|
+
let _purify = null;
|
|
22
|
+
async function getPurify() {
|
|
23
|
+
if (_purify)
|
|
24
|
+
return _purify;
|
|
25
|
+
let JSDOM;
|
|
26
|
+
let createDOMPurify;
|
|
27
|
+
try {
|
|
28
|
+
JSDOM = (await import('jsdom')).JSDOM;
|
|
29
|
+
createDOMPurify = (await import('dompurify')).default;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error('@visant/logo-trace sanitize requires the optional peer dependencies ' +
|
|
33
|
+
'"jsdom" and "dompurify". Install them with `npm i jsdom dompurify`.');
|
|
34
|
+
}
|
|
35
|
+
_purify = createDOMPurify(new JSDOM('').window);
|
|
36
|
+
return _purify;
|
|
37
|
+
}
|
|
38
|
+
/** Strip scripts / unsafe content from an SVG string, keeping SVG + filters + <use>. */
|
|
39
|
+
export async function sanitizeSvg(svg) {
|
|
40
|
+
const purify = await getPurify();
|
|
41
|
+
return purify.sanitize(svg, {
|
|
42
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
43
|
+
ADD_TAGS: ['use'],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// ── SVG optimization ─────────────────────────────────────────────────────────
|
|
47
|
+
function escapeRegexSpecial(s) {
|
|
48
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
49
|
+
}
|
|
50
|
+
const EDITOR_XMLNS = [
|
|
51
|
+
'xmlns:inkscape',
|
|
52
|
+
'xmlns:sodipodi',
|
|
53
|
+
'xmlns:sketch',
|
|
54
|
+
'xmlns:dc',
|
|
55
|
+
'xmlns:cc',
|
|
56
|
+
'xmlns:rdf',
|
|
57
|
+
];
|
|
58
|
+
const EDITOR_ATTR_PREFIXES = ['inkscape:', 'sodipodi:', 'sketch:', 'data-name'];
|
|
59
|
+
/**
|
|
60
|
+
* Strip editor cruft, comments, metadata, hidden/empty nodes and minify numeric
|
|
61
|
+
* literals from an SVG string. Pure string transforms except the optional
|
|
62
|
+
* `removeHiddenElements` pass, which needs jsdom.
|
|
63
|
+
*/
|
|
64
|
+
export async function optimizeSvg(svgString, options) {
|
|
65
|
+
const opts = {
|
|
66
|
+
removeComments: true,
|
|
67
|
+
removeMetadata: true,
|
|
68
|
+
removeEditorData: true,
|
|
69
|
+
removeEmptyGroups: true,
|
|
70
|
+
minifyPaths: true,
|
|
71
|
+
removeHiddenElements: true,
|
|
72
|
+
...options,
|
|
73
|
+
};
|
|
74
|
+
let svg = svgString;
|
|
75
|
+
svg = svg.replace(/<\?xml[^?]*\?>\s*/gi, '');
|
|
76
|
+
svg = svg.replace(/<!DOCTYPE[^>]*>\s*/gi, '');
|
|
77
|
+
if (opts.removeComments) {
|
|
78
|
+
let prev = '';
|
|
79
|
+
while (prev !== svg) {
|
|
80
|
+
prev = svg;
|
|
81
|
+
svg = svg.replace(/<!--[\s\S]*?-->/g, '');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (opts.removeMetadata) {
|
|
85
|
+
svg = svg.replace(/<metadata[\s\S]*?<\/metadata>\s*/gi, '');
|
|
86
|
+
}
|
|
87
|
+
if (opts.removeEditorData) {
|
|
88
|
+
for (const ns of EDITOR_XMLNS) {
|
|
89
|
+
const escaped = escapeRegexSpecial(ns);
|
|
90
|
+
svg = svg.replace(new RegExp(`\\s+${escaped}="[^"]*"`, 'gi'), '');
|
|
91
|
+
}
|
|
92
|
+
for (const prefix of EDITOR_ATTR_PREFIXES) {
|
|
93
|
+
const escaped = escapeRegexSpecial(prefix);
|
|
94
|
+
svg = svg.replace(new RegExp(`\\s+${escaped}[a-z-]*="[^"]*"`, 'gi'), '');
|
|
95
|
+
}
|
|
96
|
+
const withoutXlinkDecl = svg.replace(/\s+xmlns:xlink="[^"]*"/gi, '');
|
|
97
|
+
if (!/xlink:/i.test(withoutXlinkDecl)) {
|
|
98
|
+
svg = withoutXlinkDecl;
|
|
99
|
+
}
|
|
100
|
+
svg = svg.replace(/<sodipodi:[^>]*\/>\s*/gi, '');
|
|
101
|
+
svg = svg.replace(/<sodipodi:[^>]*>[\s\S]*?<\/sodipodi:[^>]*>\s*/gi, '');
|
|
102
|
+
svg = svg.replace(/<inkscape:[^>]*\/>\s*/gi, '');
|
|
103
|
+
svg = svg.replace(/<inkscape:[^>]*>[\s\S]*?<\/inkscape:[^>]*>\s*/gi, '');
|
|
104
|
+
}
|
|
105
|
+
if (opts.removeEmptyGroups) {
|
|
106
|
+
let prev = '';
|
|
107
|
+
while (prev !== svg) {
|
|
108
|
+
prev = svg;
|
|
109
|
+
svg = svg.replace(/<g[^>]*>\s*<\/g>\s*/gi, '');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (opts.removeHiddenElements) {
|
|
113
|
+
const { JSDOM } = await import('jsdom');
|
|
114
|
+
const dom = new JSDOM(svg, { contentType: 'image/svg+xml' });
|
|
115
|
+
const doc = dom.window.document;
|
|
116
|
+
doc.querySelectorAll('[display="none"], [visibility="hidden"]').forEach((el) => el.remove());
|
|
117
|
+
svg = doc.documentElement.outerHTML;
|
|
118
|
+
}
|
|
119
|
+
if (opts.minifyPaths) {
|
|
120
|
+
svg = svg.replace(/\b(\d+)\.0+\b/g, '$1');
|
|
121
|
+
svg = svg.replace(/\b0+(\.\d*[1-9])0+\b/g, '$1');
|
|
122
|
+
svg = svg.replace(/\b0+(\.\d+)\b/g, '$1');
|
|
123
|
+
svg = svg.replace(/(?<=[\s,(":]|^)0+(\.\d+)/g, '$1');
|
|
124
|
+
}
|
|
125
|
+
svg = svg.replace(/\s{2,}/g, ' ');
|
|
126
|
+
svg = svg.replace(/>\s+</g, '><');
|
|
127
|
+
svg = svg.trim();
|
|
128
|
+
return svg;
|
|
129
|
+
}
|
|
130
|
+
/** sanitize → optimize. Used to clean SVG pasted from Figma/Illustrator/etc. */
|
|
131
|
+
export async function cleanSvg(raw) {
|
|
132
|
+
const sanitized = await sanitizeSvg(raw);
|
|
133
|
+
return optimizeSvg(sanitized);
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"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"}
|
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TraceOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Vectorize a raster image buffer to an SVG string. Honors a named `preset`
|
|
4
|
+
* and/or explicit options. Does NOT sanitize/optimize — call `tracePipeline`
|
|
5
|
+
* for the full cleaned result.
|
|
6
|
+
*/
|
|
7
|
+
export declare function traceImage(buffer: Buffer, opts?: TraceOptions): Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Full pipeline: trace → sanitize → optimize → clean SVG string.
|
|
10
|
+
* The primary entry point for raster → SVG logo vectorization.
|
|
11
|
+
*/
|
|
12
|
+
export declare function tracePipeline(buffer: Buffer, opts?: TraceOptions): Promise<string>;
|
|
13
|
+
/** Alias for `tracePipeline` — the package's headline `trace(buffer, preset|opts)`. */
|
|
14
|
+
export declare const trace: typeof tracePipeline;
|
|
15
|
+
/** sanitize → optimize a raw SVG string (no tracing). */
|
|
16
|
+
export declare function cleanSvgPipeline(raw: string): Promise<string>;
|
|
17
|
+
//# sourceMappingURL=trace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA2F/C;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CzF;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAI5F;AAED,uFAAuF;AACvF,eAAO,MAAM,KAAK,sBAAgB,CAAC;AAEnC,yDAAyD;AACzD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGnE"}
|
package/dist/trace.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @visant/logo-trace — raster → SVG core.
|
|
2
|
+
//
|
|
3
|
+
// Pipeline: Otsu auto-threshold (+ auto-invert for light-on-light art) →
|
|
4
|
+
// potrace vectorization (with an invert-retry fallback) → sanitize → optimize.
|
|
5
|
+
//
|
|
6
|
+
// `sharp` is an optional peer used only for the grayscale histogram / invert;
|
|
7
|
+
// without it, 'auto' threshold gracefully degrades to a fixed 128.
|
|
8
|
+
import { resolveTraceOptions } from './presets.js';
|
|
9
|
+
import { sanitizeSvg, optimizeSvg } from './sanitize.js';
|
|
10
|
+
function clamp(value, min, max, fallback) {
|
|
11
|
+
const n = Number(value);
|
|
12
|
+
if (isNaN(n))
|
|
13
|
+
return fallback;
|
|
14
|
+
return Math.max(min, Math.min(max, n));
|
|
15
|
+
}
|
|
16
|
+
// ── Otsu auto-threshold ──────────────────────────────────────────────────────
|
|
17
|
+
function computeOtsuFromGrayscale(pixels) {
|
|
18
|
+
const histogram = new Array(256).fill(0);
|
|
19
|
+
let totalPixels = 0;
|
|
20
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
21
|
+
histogram[pixels[i]]++;
|
|
22
|
+
totalPixels++;
|
|
23
|
+
}
|
|
24
|
+
if (totalPixels === 0)
|
|
25
|
+
return 128;
|
|
26
|
+
let sum = 0;
|
|
27
|
+
for (let i = 0; i < 256; i++)
|
|
28
|
+
sum += i * histogram[i];
|
|
29
|
+
let sumB = 0;
|
|
30
|
+
let wB = 0;
|
|
31
|
+
let maxVariance = 0;
|
|
32
|
+
let bestThreshold = 128;
|
|
33
|
+
for (let t = 0; t < 256; t++) {
|
|
34
|
+
wB += histogram[t];
|
|
35
|
+
if (wB === 0)
|
|
36
|
+
continue;
|
|
37
|
+
const wF = totalPixels - wB;
|
|
38
|
+
if (wF === 0)
|
|
39
|
+
break;
|
|
40
|
+
sumB += t * histogram[t];
|
|
41
|
+
const meanB = sumB / wB;
|
|
42
|
+
const meanF = (sum - sumB) / wF;
|
|
43
|
+
const variance = wB * wF * (meanB - meanF) * (meanB - meanF);
|
|
44
|
+
if (variance > maxVariance) {
|
|
45
|
+
maxVariance = variance;
|
|
46
|
+
bestThreshold = t;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return bestThreshold;
|
|
50
|
+
}
|
|
51
|
+
async function loadSharp() {
|
|
52
|
+
return (await import('sharp')).default;
|
|
53
|
+
}
|
|
54
|
+
async function computeOtsuFromImage(buffer) {
|
|
55
|
+
try {
|
|
56
|
+
const sharp = await loadSharp();
|
|
57
|
+
const grayscale = await sharp(buffer).grayscale().raw().toBuffer();
|
|
58
|
+
const threshold = computeOtsuFromGrayscale(grayscale);
|
|
59
|
+
// If threshold > 230, the image is light-on-light (e.g. gray logo on white).
|
|
60
|
+
// A designer would invert it first — we do the same automatically.
|
|
61
|
+
return { threshold, needsInvert: threshold > 230 };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { threshold: 128, needsInvert: false };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function invertImage(buffer) {
|
|
68
|
+
const sharp = await loadSharp();
|
|
69
|
+
return sharp(buffer).negate({ alpha: false }).toBuffer();
|
|
70
|
+
}
|
|
71
|
+
function svgHasContent(svg) {
|
|
72
|
+
const match = svg.match(/\bd="([^"]*)"/);
|
|
73
|
+
return !!match && match[1].trim().length > 5;
|
|
74
|
+
}
|
|
75
|
+
// ── Potrace ──────────────────────────────────────────────────────────────────
|
|
76
|
+
async function potraceTrace(buffer, potraceOpts) {
|
|
77
|
+
const potrace = await import('potrace');
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
potrace.trace(buffer, potraceOpts, (err, svg) => {
|
|
80
|
+
if (err)
|
|
81
|
+
reject(err);
|
|
82
|
+
else
|
|
83
|
+
resolve(svg);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Vectorize a raster image buffer to an SVG string. Honors a named `preset`
|
|
89
|
+
* and/or explicit options. Does NOT sanitize/optimize — call `tracePipeline`
|
|
90
|
+
* for the full cleaned result.
|
|
91
|
+
*/
|
|
92
|
+
export async function traceImage(buffer, opts = {}) {
|
|
93
|
+
const resolved = resolveTraceOptions(opts);
|
|
94
|
+
let threshold;
|
|
95
|
+
let imageBuffer = buffer;
|
|
96
|
+
if (resolved.threshold === 'auto') {
|
|
97
|
+
const otsu = await computeOtsuFromImage(buffer);
|
|
98
|
+
if (otsu.needsInvert) {
|
|
99
|
+
// Light-on-light image (like gray logo on white bg) — invert first
|
|
100
|
+
imageBuffer = await invertImage(buffer);
|
|
101
|
+
// After inversion, re-compute Otsu on the inverted image
|
|
102
|
+
const otsu2 = await computeOtsuFromImage(imageBuffer);
|
|
103
|
+
threshold = otsu2.threshold;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
threshold = otsu.threshold;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
threshold = clamp(Number(resolved.threshold) || 128, 0, 255, 128);
|
|
111
|
+
}
|
|
112
|
+
const potraceOpts = {
|
|
113
|
+
turdSize: clamp(resolved.turdSize, 0, 20, 2),
|
|
114
|
+
alphaMax: clamp(resolved.alphaMax, 0, 1.334, 1),
|
|
115
|
+
optCurve: true,
|
|
116
|
+
optTolerance: clamp(resolved.optTolerance, 0, 2, 0.2),
|
|
117
|
+
color: resolved.color || '#000000',
|
|
118
|
+
threshold,
|
|
119
|
+
};
|
|
120
|
+
const svg = await potraceTrace(imageBuffer, potraceOpts);
|
|
121
|
+
// Fallback: if trace produced empty paths and we didn't already invert, try inverting
|
|
122
|
+
if (!svgHasContent(svg) && imageBuffer === buffer) {
|
|
123
|
+
try {
|
|
124
|
+
const inverted = await invertImage(buffer);
|
|
125
|
+
const otsu = await computeOtsuFromImage(inverted);
|
|
126
|
+
const retrySvg = await potraceTrace(inverted, { ...potraceOpts, threshold: otsu.threshold });
|
|
127
|
+
if (svgHasContent(retrySvg))
|
|
128
|
+
return retrySvg;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* fall through to original empty result */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return svg;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Full pipeline: trace → sanitize → optimize → clean SVG string.
|
|
138
|
+
* The primary entry point for raster → SVG logo vectorization.
|
|
139
|
+
*/
|
|
140
|
+
export async function tracePipeline(buffer, opts = {}) {
|
|
141
|
+
const raw = await traceImage(buffer, opts);
|
|
142
|
+
const sanitized = await sanitizeSvg(raw);
|
|
143
|
+
return optimizeSvg(sanitized);
|
|
144
|
+
}
|
|
145
|
+
/** Alias for `tracePipeline` — the package's headline `trace(buffer, preset|opts)`. */
|
|
146
|
+
export const trace = tracePipeline;
|
|
147
|
+
/** sanitize → optimize a raw SVG string (no tracing). */
|
|
148
|
+
export async function cleanSvgPipeline(raw) {
|
|
149
|
+
const sanitized = await sanitizeSvg(raw);
|
|
150
|
+
return optimizeSvg(sanitized);
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=trace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.js","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type TracePreset = 'logo' | 'lettering' | 'lineArt' | 'stamp' | 'custom';
|
|
2
|
+
export interface TraceOptions {
|
|
3
|
+
/** Suppress speckles smaller than N px². Higher = cleaner, loses detail. */
|
|
4
|
+
turdSize?: number;
|
|
5
|
+
/** Curve optimization tolerance. Higher = fewer, smoother curves. */
|
|
6
|
+
optTolerance?: number;
|
|
7
|
+
/** Black/white cutoff 0–255, or 'auto' for Otsu (+ auto-invert). */
|
|
8
|
+
threshold?: number | 'auto';
|
|
9
|
+
/** Corner threshold 0–1.334. Higher = rounder corners. */
|
|
10
|
+
alphaMax?: number;
|
|
11
|
+
/** Fill color of the traced paths. Default '#000000'. */
|
|
12
|
+
color?: string;
|
|
13
|
+
/** Named preset; merged under any explicit option above. */
|
|
14
|
+
preset?: TracePreset;
|
|
15
|
+
}
|
|
16
|
+
export interface SvgOptimizeOptions {
|
|
17
|
+
removeComments?: boolean;
|
|
18
|
+
removeMetadata?: boolean;
|
|
19
|
+
removeEditorData?: boolean;
|
|
20
|
+
removeEmptyGroups?: boolean;
|
|
21
|
+
minifyPaths?: boolean;
|
|
22
|
+
removeHiddenElements?: boolean;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhF,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qCAAqC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@visant/logo-trace",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Raster → SVG vectorization tuned for logos. Potrace under the hood, with Otsu auto-threshold (+ auto-invert for light-on-light art), logo/lettering/lineArt/stamp presets, and a sanitize + optimize pass that yields a clean, minified SVG string.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Visant Labs",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/pedrojaques99/visantlabs-os",
|
|
11
|
+
"directory": "packages/logo-trace"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"potrace",
|
|
15
|
+
"trace",
|
|
16
|
+
"vectorize",
|
|
17
|
+
"raster-to-svg",
|
|
18
|
+
"png-to-svg",
|
|
19
|
+
"logo",
|
|
20
|
+
"svg",
|
|
21
|
+
"otsu",
|
|
22
|
+
"threshold"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./presets": {
|
|
31
|
+
"types": "./dist/presets.d.ts",
|
|
32
|
+
"import": "./dist/presets.js"
|
|
33
|
+
},
|
|
34
|
+
"./sanitize": {
|
|
35
|
+
"types": "./dist/sanitize.d.ts",
|
|
36
|
+
"import": "./dist/sanitize.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": ["dist", "README.md"],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"prepare": "tsc -p tsconfig.json",
|
|
43
|
+
"prepublishOnly": "npm run build"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"potrace": "^2.1.8"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"sharp": ">=0.32",
|
|
50
|
+
"jsdom": ">=22",
|
|
51
|
+
"dompurify": ">=3"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"sharp": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"jsdom": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"dompurify": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"typescript": "~5.8.2"
|
|
66
|
+
}
|
|
67
|
+
}
|