Skip to content

Commit ff11b89

Browse files
committed
feat: add optimization api
1 parent 24829bf commit ff11b89

25 files changed

Lines changed: 1759 additions & 735 deletions

CONTRIBUTING.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,4 @@ The key words `“MUST”`, `“MUST NOT”`, `“REQUIRED”`, `“SHALL”`, `
9292

9393
Make sure to properly format the source code, check for linter errors and test the code before pushing.
9494

95-
## File Names
96-
97-
1. Use `PascalCase` format if the file belongs to a class. Ex: `NobuBrowser.ts`, `ProtocolService.ts`, etc.
98-
2. Use `camelCase` format for the files that belong to functions. Ex: `contextMenu`, `appMenu`, etc.
99-
3. Use `lowercase` format for other cases.
100-
10195
> 🎉 Happy coding!

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ crate-type = ["cdylib"]
1111
[dependencies]
1212
fastrand = "2.4.1"
1313
image = "0.25.10"
14-
napi = "3.0.0"
14+
napi = { version = "3.0.0", features = ["serde-json"] }
1515
napi-derive = "3.0.0"
16+
oxvg_ast = { version = "0.0.5", features = ["parse", "roxmltree"] }
17+
oxvg_optimiser = "0.0.5"
18+
serde_json = "1.0"
1619
visioncortex = "0.8.10"
1720

1821
[build-dependencies]

README.md

Lines changed: 114 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ Node.js library to convert raster images to svg using [VTracer](https://github.c
88
npx @neplex/vectorizer ./raster.png ./vector.svg
99
```
1010

11+
Optimize while vectorizing:
12+
13+
```bash
14+
npx @neplex/vectorizer ./raster.png ./vector.svg --optimize --multipass --multipass-iterations 5
15+
```
16+
17+
Optimize an existing SVG:
18+
19+
```bash
20+
npx @neplex/vectorizer optimize ./vector.svg ./vector.optimized.svg --plugin preset-default
21+
```
22+
1123
Use `--help` to see all available options.
1224

1325
## Installation
@@ -42,112 +54,170 @@ console.log(svg); // <svg>...</svg>
4254
await writeFile('./vector.svg', svg);
4355
```
4456

45-
If you want to use a synchronous API, you can use `vectorizeSync` instead.
57+
The generated SVG string is generally quite large, so it's recommended to optimize it before use. See the [SVG Optimization section](#svg-optimization) for more details. If you want to use a synchronous API, you can use `vectorizeSync` instead. See the [API section](#api) for more details on available functions and options.
4658

4759
## Benchmark
4860

4961
```js
5062
yarn bench
51-
clk: ~5.30 GHz
63+
clk: ~5.32 GHz
5264
cpu: Intel(R) Core(TM) i7-14700K
5365
runtime: node 24.14.1 (x64-win32)
5466

55-
benchmark avg (min … max) p75 / p99 (min … top 1%)
56-
----------------------------------------------- -------------------------------
57-
@neplex/vectorizer raw sync 532.14 µs/iter 533.70 µs ▂▇█▃▃
58-
(524.20 µs … 590.90 µs) 550.90 µs ▂█████
59-
( 1.65 kb … 168.67 kb) 1.94 kb ▂▄███████▇▅▃▄▂▁▁▂▁▁▁
67+
benchmark avg (min … max) p75 / p99 (min … top 1%)
68+
-------------------------------------------------- -------------------------------
69+
@neplex/vectorizer raw sync 529.82 µs/iter 533.70 µs ▅█▅
70+
(517.00 µs … 607.00 µs) 552.90 µs ▅██▆▇████▄
71+
( 1.65 kb … 168.67 kb) 1.94 kb ███████████▆▄▃▂▁▂▂▂
6072

61-
@neplex/vectorizer encoded sync 552.92 µs/iter 554.70 µs █▄▂
62-
(544.90 µs … 613.00 µs) 569.60 µs ████▆
63-
( 1.60 kb … 74.57 kb) 1.70 kb ▂████████▇▅▃▃▂▂▁▁▁▁▁
73+
@neplex/vectorizer encoded sync 549.19 µs/iter 552.70 µs ▃█▆▇
74+
(534.80 µs … 597.10 µs) 573.50 µs ▃▆▆████▆
75+
( 1.60 kb … 74.57 kb) 1.70 kb ▂██████████▆▄▂▂▂▂▂▁▁
6476

65-
@neplex/vectorizer encoded async 617.69 µs/iter 650.90 µs █▆
66-
(572.30 µs … 785.10 µs) 728.20 µs ██▄▂
67-
( 2.27 kb … 76.16 kb) 2.35 kb ▅████▆▅▄▄▅▄▅██▅▃▂▁▂▁▂
77+
@neplex/vectorizer encoded async 585.94 µs/iter 586.00 µs ▇█
78+
(561.70 µs … 745.10 µs) 691.10 µs ██
79+
( 2.27 kb … 76.16 kb) 2.34 kb ▅███▅▃▂▂▁▁▁▁▁▁▂▁▁▁▁▁
6880

69-
@neplex/vectorizer raw async 583.75 µs/iter 593.00 µs █▃
70-
(551.50 µs … 721.00 µs) 677.90 µs ████
71-
( 2.33 kb … 76.23 kb) 2.40 kb █████▆▆▅▃▂▂▂▅▄▃▂▁▁▁
81+
@neplex/vectorizer encoded callback 684.02 µs/iter 698.80 µs ▃▅▃▃█▂
82+
(632.30 µs … 844.90 µs) 770.60 µs ▂▅██████▇▃
83+
( 4.27 kb … 44.40 kb) 4.48 kb ████████████▇▅▆▆▅▃▃▂
7284

73-
@neplex/vectorizer raw callback 601.52 µs/iter 604.30 µs █
74-
(561.30 µs … 1.18 ms) 1.11 ms ▇█
75-
( 5.02 kb … 217.06 kb) 5.48 kb ███▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
85+
@neplex/vectorizer raw async 565.45 µs/iter 565.20 µs
86+
(548.70 µs … 706.00 µs) 646.00 µs ██▃
87+
( 2.33 kb … 186.42 kb) 2.58 kb ███▅▃▃▂▂▁▂▁▂▁▁▂▁▁▁▁▁
7688

77-
imagetracerjs 2.37 ms/iter 2.36 ms █
78-
(2.32 ms2.92 ms) 2.65 ms █▇
79-
( 1.17 mb 6.89 mb) 4.52 mb ▆███▄▂▂▁▁▁▂▂▂▂▂▁▁▁▁▁▁
89+
@neplex/vectorizer raw callback 625.91 µs/iter 636.20 µs ▃█▇
90+
(591.60 µs779.20 µs) 704.80 µs ███▇
91+
( 3.98 kb374.13 kb) 4.62 kb ▂████████▄▅▆▅▆▄▄▃▁▂▁▂
8092

81-
potrace trace 3.83 ms/iter 4.72 ms ▃█ ▅▇
82-
(1.82 ms … 7.31 ms) 6.73 ms ▆ ██ ██▂ ▇█
83-
(175.78 kb … 12.23 mb) 2.34 mb ▄▇████▇▄███▄██▇▅▄▇▂▁▂
93+
imagetracerjs 2.41 ms/iter 2.42 ms █▇▂
94+
(2.33 ms … 2.93 ms) 2.73 ms ████▂
95+
( 1.14 mb … 6.92 mb) 4.52 mb ▂█████▃▃▃▃▂▂▂▂▁▂▁▁▁▁▁
96+
97+
potrace trace 3.59 ms/iter 4.45 ms █▅
98+
(1.70 ms … 7.43 ms) 6.36 ms ▆▃█▂██▃ ▃▅▂ ▄
99+
(736.00 b … 11.91 mb) 2.27 mb ▃██████████████▇▃█▁▆▃
84100

85101
summary
86102
@neplex/vectorizer raw sync
87103
1.04x faster than @neplex/vectorizer encoded sync
88-
1.1x faster than @neplex/vectorizer raw async
89-
1.13x faster than @neplex/vectorizer raw callback
90-
1.16x faster than @neplex/vectorizer encoded async
91-
4.45x faster than imagetracerjs
92-
7.19x faster than potrace trace
104+
1.07x faster than @neplex/vectorizer raw async
105+
1.11x faster than @neplex/vectorizer encoded async
106+
1.18x faster than @neplex/vectorizer raw callback
107+
1.29x faster than @neplex/vectorizer encoded callback
108+
4.54x faster than imagetracerjs
109+
6.78x faster than potrace trace
93110
```
94111

95112
See [benchmark](https://github.com/neplextech/vectorizer/blob/main/benchmark/bench.ts) for more details.
96113

97114
## API
98115

99-
### `vectorize(data: Buffer, config?: Config | Preset): Promise<string>`
116+
### `vectorize(data: Buffer, config?: Config | Preset, signal?: AbortSignal): Promise<string>`
100117

101118
Takes an image buffer and returns a promise that resolves to an SVG string.
102119

103120
### `vectorizeSync(data: Buffer, config?: Config | Preset): string`
104121

105122
Takes an image buffer and returns an SVG string synchronously.
106123

107-
### `vectorizeRaw(data: Buffer, args: RawDataConfig, config?: Config | Preset): Promise<string>`
124+
### `vectorizeRaw(data: Buffer, args: RawDataConfig, config?: Config | Preset, signal?: AbortSignal): Promise<string>`
108125

109126
Takes a raw pixel data buffer and returns a promise that resolves to an SVG string.
110127

111128
### `vectorizeRawSync(data: Buffer, args: RawDataConfig, config?: Config | Preset): string`
112129

113130
Takes a raw pixel data buffer and returns an SVG string synchronously.
114131

115-
### `vectorizeToCallback(data: Buffer, config: Config | Preset | null | undefined, callback: ([chunk, progress]) => void): Promise<void>`
132+
### `vectorizeToCallback(data: Buffer, config: Config | Preset | null | undefined, callback: (chunk: string, progress: number) => void): void`
133+
134+
Vectorizes an image buffer and emits SVG chunks to the callback. `progress` is a number from 0 to 100 indicating how much of the vectorization has been written.
135+
136+
### `vectorizeRawToCallback(data: Buffer, args: RawDataConfig, config: Config | Preset | null | undefined, callback: (chunk: string, progress: number) => void): void`
137+
138+
Vectorizes raw pixel data and emits SVG chunks to the callback. `progress` is a number from 0 to 100.
139+
140+
### `readImage(data: Buffer, args?: RawDataConfig, signal?: AbortSignal): Promise<ImageData>`
141+
142+
Decodes an encoded image buffer, or a raw RGBA buffer when `args` is provided, and resolves image width, height, and pixels.
143+
144+
### `readImageSync(data: Buffer, args?: RawDataConfig): ImageData`
116145

117-
Vectorizes an image buffer and emits SVG chunks to the callback. `progress` is a number from 0–100 indicating how much of the vectorization has been written.
146+
Decodes an encoded image buffer, or a raw RGBA buffer when `args` is provided, synchronously.
118147

119-
### `vectorizeRawToCallback(data: Buffer, args: RawDataConfig, config: Config | Preset | null | undefined, callback: ([chunk, progress]) => void): Promise<void>`
148+
### `colorExistsInImage(image: ImageData, color: Color, signal?: AbortSignal): Promise<boolean>`
120149

121-
Vectorizes raw pixel data and emits SVG chunks to the callback. `progress` is a number from 0–100.
150+
Checks asynchronously whether a decoded image contains the given RGB color.
122151

123-
### `isEOF(data: [string, number]): boolean`
152+
### `colorExistsInImageSync(image: ImageData, color: Color): boolean`
153+
154+
Checks synchronously whether a decoded image contains the given RGB color.
155+
156+
### `findUnusedColorInImage(image: ImageData, options?: InternalOptions, signal?: AbortSignal): Promise<Color>`
157+
158+
Finds asynchronously a color that does not exist in decoded image data.
159+
160+
### `findUnusedColorInImageSync(image: ImageData, options?: InternalOptions): Color`
161+
162+
Finds synchronously a color that does not exist in decoded image data.
163+
164+
### `optimize(svg: string, options?: OptimizeOptions, signal?: AbortSignal): Promise<string>`
165+
166+
Optimizes an SVG string asynchronously. Supports the same options as `optimizeSync`.
167+
168+
### `optimizeSync(svg: string, options?: OptimizeOptions): string`
169+
170+
Optimizes an SVG string synchronously.
171+
172+
`OptimizeOptions` supports:
173+
174+
- `preset?: OptimizePreset`: `Default`, `Safe`, or `None`.
175+
- `plugins?: Array<string | { name: string, params?: unknown }>`: SVGO `Config["plugins"]` compatible job config. When provided, this defines the optimizer job set.
176+
- `omit?: string[]`: optimizer job names to skip.
177+
- `multipass?: boolean`: run optimization until output stops changing or the iteration limit is reached.
178+
- `multipassIterations?: number`: maximum multipass iterations. Defaults to `10`.
179+
180+
### `isEOF(chunk: string, progress: number): boolean`
124181

125182
Determines if the given chunk data indicates the end of the SVG output. Useful for callback-based vectorization to know when the final chunk has been received. Note that EOF here refers to the final chunk of svg, which is `</svg>\n`.
126183

127184
```js
128-
await vectorizeToCallback(src, config, (chunk) => {
129-
if (isEOF(chunk)) {
130-
console.log('SVG output complete!');
185+
const { promise, resolve } = Promise.withResolvers<void>();
186+
187+
vectorizeToCallback(src, config, (chunk, progress) => {
188+
if (isEOF(chunk, progress)) {
189+
resolve();
131190
}
132191
});
192+
193+
await promise; // block until all the SVG chunks have been received
194+
console.log('SVG output complete!');
133195
```
134196

135197
## SVG Optimization
136198

137-
The raw output from `vectorize` can be significantly reduced in size using optimizers like [`@oxvg/napi`](https://www.npmjs.com/package/@oxvg/napi):
199+
The raw output from `vectorize` can be significantly reduced in size using the built-in optimizer powered by [oxvg_optimiser](https://github.com/noahbald/oxvg):
138200

139201
```js
140-
import { vectorize } from '@neplex/vectorizer';
141-
import { optimise } from '@oxvg/napi';
202+
import { optimize, vectorize } from '@neplex/vectorizer';
142203
import { readFile, writeFile } from 'node:fs/promises';
143204

144205
const src = await readFile('./input.png');
145206
const svg = await vectorize(src);
146-
const optimized = optimise(svg);
207+
const optimized = await optimize(
208+
svg,
209+
/* optional config */ {
210+
plugins: ['preset-default', { name: 'removeTitle' }], // supports svgo plugin config
211+
multipass: true, // enable multipass optimization
212+
multipassIterations: 5, // limit multipass iterations to 5
213+
},
214+
);
147215

148216
await writeFile('./output.svg', optimized);
149217
```
150218

219+
> Note that the optimizer included in this library is a thin wrapper around `oxvg_optimiser` and does not intend to provide all the features of `oxvg`. For advanced needs, consider using `@oxvg/napi` directly.
220+
151221
## Demo
152222

153223
Generated under the following configuration:

__test__/cli.spec.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import test from 'ava';
2+
import { execFile } from 'node:child_process';
3+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4+
import { tmpdir } from 'node:os';
5+
import { join, resolve } from 'node:path';
6+
import { promisify } from 'node:util';
7+
8+
const execFileAsync = promisify(execFile);
9+
const cliPath = resolve('cli/index.mjs');
10+
const redPixel = Buffer.from([255, 0, 0, 255]);
11+
12+
test('cli vectorizes with on-the-fly optimization', async (t) => {
13+
const dir = await mkdtemp(join(tmpdir(), 'vectorizer-cli-'));
14+
t.teardown(() => rm(dir, { recursive: true, force: true }));
15+
const inputPath = join(dir, 'pixel.rgba');
16+
const outputPath = join(dir, 'pixel.svg');
17+
18+
await writeFile(inputPath, redPixel);
19+
20+
const { stderr } = await execFileAsync(process.execPath, [
21+
cliPath,
22+
inputPath,
23+
outputPath,
24+
'--raw',
25+
'1x1',
26+
'--preset',
27+
'poster',
28+
'--optimize',
29+
'--optimize-plugin',
30+
'removeComments',
31+
]);
32+
33+
const svg = await readFile(outputPath, 'utf8');
34+
t.true(svg.includes('<svg'));
35+
t.regex(stderr, /Progress:/);
36+
t.regex(stderr, /Optimized/);
37+
});
38+
39+
test('cli optimize command optimizes an existing svg file', async (t) => {
40+
const dir = await mkdtemp(join(tmpdir(), 'vectorizer-cli-'));
41+
t.teardown(() => rm(dir, { recursive: true, force: true }));
42+
const inputPath = join(dir, 'input.svg');
43+
const outputPath = join(dir, 'output.svg');
44+
45+
await writeFile(inputPath, '<svg xmlns="http://www.w3.org/2000/svg"><!-- remove me --><g><path d="M0 0"/></g></svg>');
46+
47+
const { stderr } = await execFileAsync(process.execPath, [
48+
cliPath,
49+
'optimize',
50+
inputPath,
51+
outputPath,
52+
'--plugin',
53+
'removeComments',
54+
]);
55+
56+
const svg = await readFile(outputPath, 'utf8');
57+
t.true(svg.includes('<svg'));
58+
t.false(svg.includes('remove me'));
59+
t.regex(stderr, /Optimized/);
60+
});
61+
62+
test('cli print streams svg chunks without a progress bar', async (t) => {
63+
const dir = await mkdtemp(join(tmpdir(), 'vectorizer-cli-'));
64+
t.teardown(() => rm(dir, { recursive: true, force: true }));
65+
const inputPath = join(dir, 'pixel.rgba');
66+
67+
await writeFile(inputPath, redPixel);
68+
69+
const { stdout, stderr } = await execFileAsync(process.execPath, [
70+
cliPath,
71+
inputPath,
72+
'--raw',
73+
'1x1',
74+
'--preset',
75+
'poster',
76+
'--print',
77+
]);
78+
79+
t.true(stdout.includes('<svg'));
80+
t.false(stderr.includes('Progress:'));
81+
});

0 commit comments

Comments
 (0)