├── .cargo └── config.toml ├── .eslintrc.yml ├── .gitattributes ├── .github ├── renovate.json └── workflows │ ├── CI.yml │ └── lint.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .taplo.toml ├── .yarn └── releases │ └── yarn-4.6.0.cjs ├── .yarnrc.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── __test__ ├── data │ ├── firefox-logo.png │ ├── result-bw.svg │ ├── result-firefox.svg │ ├── result-photo.svg │ ├── result-poster.svg │ ├── result-raw.svg │ ├── result.svg │ └── sample.png ├── index.spec.ts ├── package.json └── tsconfig.json ├── benchmark ├── bench.ts ├── data │ ├── sample-imagetracer.svg │ ├── sample-vectorizer.svg │ └── sample.png ├── package.json └── tsconfig.json ├── build.rs ├── cli └── index.mjs ├── example ├── anime-girl.png ├── anime-girl.ts ├── package.json ├── result.svg └── tsconfig.json ├── index.d.ts ├── index.js ├── npm ├── android-arm-eabi │ ├── README.md │ └── package.json ├── android-arm64 │ ├── README.md │ └── package.json ├── darwin-arm64 │ ├── README.md │ └── package.json ├── darwin-x64 │ ├── README.md │ └── package.json ├── freebsd-x64 │ ├── README.md │ └── package.json ├── linux-arm-gnueabihf │ ├── README.md │ └── package.json ├── linux-arm64-gnu │ ├── README.md │ └── package.json ├── linux-arm64-musl │ ├── README.md │ └── package.json ├── linux-x64-gnu │ ├── README.md │ └── package.json ├── linux-x64-musl │ ├── README.md │ └── package.json ├── win32-arm64-msvc │ ├── README.md │ └── package.json ├── win32-ia32-msvc │ ├── README.md │ └── package.json └── win32-x64-msvc │ ├── README.md │ └── package.json ├── package.json ├── rustfmt.toml ├── src ├── config.rs ├── converter.rs ├── lib.rs └── svg.rs ├── tsconfig.json └── yarn.lock /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-musl] 2 | linker = "aarch64-linux-musl-gcc" 3 | rustflags = ["-C", "target-feature=-crt-static"] -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: '@typescript-eslint/parser' 2 | 3 | parserOptions: 4 | ecmaFeatures: 5 | jsx: true 6 | ecmaVersion: latest 7 | sourceType: module 8 | project: ./tsconfig.json 9 | 10 | env: 11 | browser: true 12 | es6: true 13 | node: true 14 | jest: true 15 | 16 | ignorePatterns: ['index.js'] 17 | 18 | plugins: 19 | - import 20 | - '@typescript-eslint' 21 | 22 | extends: 23 | - eslint:recommended 24 | - plugin:prettier/recommended 25 | 26 | rules: 27 | # 0 = off, 1 = warn, 2 = error 28 | 'space-before-function-paren': 0 29 | 'no-useless-constructor': 0 30 | 'no-undef': 2 31 | 'no-console': [2, { allow: ['error', 'warn', 'info', 'assert'] }] 32 | 'comma-dangle': ['error', 'only-multiline'] 33 | 'no-unused-vars': 0 34 | 'no-var': 2 35 | 'one-var-declaration-per-line': 2 36 | 'prefer-const': 2 37 | 'no-const-assign': 2 38 | 'no-duplicate-imports': 2 39 | 'no-use-before-define': [2, { 'functions': false, 'classes': false }] 40 | 'eqeqeq': [2, 'always', { 'null': 'ignore' }] 41 | 'no-case-declarations': 0 42 | 'no-restricted-syntax': 43 | [ 44 | 2, 45 | { 46 | 'selector': 'BinaryExpression[operator=/(==|===|!=|!==)/][left.raw=true], BinaryExpression[operator=/(==|===|!=|!==)/][right.raw=true]', 47 | 'message': Don't compare for equality against boolean literals, 48 | }, 49 | ] 50 | 51 | # https://github.com/benmosher/eslint-plugin-import/pull/334 52 | 'import/no-duplicates': 2 53 | 'import/first': 2 54 | 'import/newline-after-import': 2 55 | 'import/order': 56 | [ 57 | 2, 58 | { 59 | 'newlines-between': 'always', 60 | 'alphabetize': { 'order': 'asc' }, 61 | 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 62 | }, 63 | ] 64 | 65 | overrides: 66 | - files: 67 | - ./**/*{.ts,.tsx} 68 | rules: 69 | 'no-unused-vars': [2, { varsIgnorePattern: '^_', argsIgnorePattern: '^_', ignoreRestSiblings: true }] 70 | 'no-undef': 0 71 | # TypeScript declare merge 72 | 'no-redeclare': 0 73 | 'no-useless-constructor': 0 74 | 'no-dupe-class-members': 0 75 | 'no-case-declarations': 0 76 | 'no-duplicate-imports': 0 77 | # TypeScript Interface and Type 78 | 'no-use-before-define': 0 79 | 80 | '@typescript-eslint/adjacent-overload-signatures': 2 81 | '@typescript-eslint/await-thenable': 2 82 | '@typescript-eslint/consistent-type-assertions': 2 83 | '@typescript-eslint/ban-types': 84 | [ 85 | 'error', 86 | { 87 | 'types': 88 | { 89 | 'String': { 'message': 'Use string instead', 'fixWith': 'string' }, 90 | 'Number': { 'message': 'Use number instead', 'fixWith': 'number' }, 91 | 'Boolean': { 'message': 'Use boolean instead', 'fixWith': 'boolean' }, 92 | 'Function': { 'message': 'Use explicit type instead' }, 93 | }, 94 | }, 95 | ] 96 | '@typescript-eslint/explicit-member-accessibility': 97 | [ 98 | 'error', 99 | { 100 | accessibility: 'explicit', 101 | overrides: 102 | { 103 | accessors: 'no-public', 104 | constructors: 'no-public', 105 | methods: 'no-public', 106 | properties: 'no-public', 107 | parameterProperties: 'explicit', 108 | }, 109 | }, 110 | ] 111 | '@typescript-eslint/method-signature-style': 2 112 | '@typescript-eslint/no-floating-promises': 2 113 | '@typescript-eslint/no-implied-eval': 2 114 | '@typescript-eslint/no-for-in-array': 2 115 | '@typescript-eslint/no-inferrable-types': 2 116 | '@typescript-eslint/no-invalid-void-type': 2 117 | '@typescript-eslint/no-misused-new': 2 118 | '@typescript-eslint/no-misused-promises': 2 119 | '@typescript-eslint/no-namespace': 2 120 | '@typescript-eslint/no-non-null-asserted-optional-chain': 2 121 | '@typescript-eslint/no-throw-literal': 2 122 | '@typescript-eslint/no-unnecessary-boolean-literal-compare': 2 123 | '@typescript-eslint/prefer-for-of': 2 124 | '@typescript-eslint/prefer-nullish-coalescing': 2 125 | '@typescript-eslint/switch-exhaustiveness-check': 2 126 | '@typescript-eslint/prefer-optional-chain': 2 127 | '@typescript-eslint/prefer-readonly': 2 128 | '@typescript-eslint/prefer-string-starts-ends-with': 0 129 | '@typescript-eslint/no-array-constructor': 2 130 | '@typescript-eslint/require-await': 2 131 | '@typescript-eslint/return-await': 2 132 | '@typescript-eslint/ban-ts-comment': 133 | [2, { 'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': true, 'ts-check': false }] 134 | '@typescript-eslint/naming-convention': 135 | [ 136 | 2, 137 | { 138 | selector: 'memberLike', 139 | format: ['camelCase', 'PascalCase'], 140 | modifiers: ['private'], 141 | leadingUnderscore: 'forbid', 142 | }, 143 | ] 144 | '@typescript-eslint/no-unused-vars': 145 | [2, { varsIgnorePattern: '^_', argsIgnorePattern: '^_', ignoreRestSiblings: true }] 146 | '@typescript-eslint/member-ordering': 147 | [ 148 | 2, 149 | { 150 | default: 151 | [ 152 | 'public-static-field', 153 | 'protected-static-field', 154 | 'private-static-field', 155 | 'public-static-method', 156 | 'protected-static-method', 157 | 'private-static-method', 158 | 'public-instance-field', 159 | 'protected-instance-field', 160 | 'private-instance-field', 161 | 'public-constructor', 162 | 'protected-constructor', 163 | 'private-constructor', 164 | 'public-instance-method', 165 | 'protected-instance-method', 166 | 'private-instance-method', 167 | ], 168 | }, 169 | ] 170 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | 5 | *.ts text eol=lf merge=union 6 | *.tsx text eol=lf merge=union 7 | *.rs text eol=lf merge=union 8 | *.js text eol=lf merge=union 9 | *.json text eol=lf merge=union 10 | *.debug text eol=lf merge=union 11 | 12 | *.js linguist-language=TypeScript 13 | *.mjs linguist-language=TypeScript -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", "group:allNonMajor", ":preserveSemverRanges", ":disablePeerDependencies"], 4 | "labels": ["dependencies"], 5 | "packageRules": [ 6 | { 7 | "matchPackageNames": ["@napi/cli", "napi", "napi-build", "napi-derive"], 8 | "addLabels": ["napi-rs"], 9 | "groupName": "napi-rs" 10 | }, 11 | { 12 | "matchPackagePatterns": ["^eslint", "^@typescript-eslint"], 13 | "groupName": "linter" 14 | } 15 | ], 16 | "commitMessagePrefix": "chore: ", 17 | "commitMessageAction": "bump up", 18 | "commitMessageTopic": "{{depName}} version", 19 | "ignoreDeps": [] 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | env: 3 | DEBUG: napi:* 4 | APP_NAME: vectorizer 5 | MACOSX_DEPLOYMENT_TARGET: '10.13' 6 | CARGO_INCREMENTAL: '1' 7 | permissions: 8 | contents: write 9 | id-token: write 10 | 'on': 11 | push: 12 | branches: 13 | - main 14 | tags-ignore: 15 | - '**' 16 | paths-ignore: 17 | - '**/*.md' 18 | - LICENSE 19 | - '**/*.gitignore' 20 | - .editorconfig 21 | - docs/** 22 | pull_request: null 23 | concurrency: 24 | group: ${{ github.workflow }}-${{ github.ref }} 25 | cancel-in-progress: true 26 | jobs: 27 | build: 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | settings: 32 | - host: macos-latest 33 | target: x86_64-apple-darwin 34 | build: yarn build --target x86_64-apple-darwin 35 | - host: windows-latest 36 | build: yarn build --target x86_64-pc-windows-msvc 37 | target: x86_64-pc-windows-msvc 38 | - host: windows-latest 39 | build: yarn build --target i686-pc-windows-msvc 40 | target: i686-pc-windows-msvc 41 | - host: ubuntu-latest 42 | target: x86_64-unknown-linux-gnu 43 | docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian 44 | build: yarn build --target x86_64-unknown-linux-gnu 45 | - host: ubuntu-latest 46 | target: x86_64-unknown-linux-musl 47 | docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine 48 | build: yarn build --target x86_64-unknown-linux-musl 49 | - host: macos-latest 50 | target: aarch64-apple-darwin 51 | build: yarn build --target aarch64-apple-darwin 52 | - host: ubuntu-latest 53 | target: aarch64-unknown-linux-gnu 54 | docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 55 | build: yarn build --target aarch64-unknown-linux-gnu 56 | - host: ubuntu-latest 57 | target: armv7-unknown-linux-gnueabihf 58 | setup: | 59 | sudo apt-get update 60 | sudo apt-get install gcc-arm-linux-gnueabihf -y 61 | build: | 62 | yarn build --target armv7-unknown-linux-gnueabihf 63 | - host: ubuntu-latest 64 | target: aarch64-linux-android 65 | build: yarn build --target aarch64-linux-android 66 | - host: ubuntu-latest 67 | target: armv7-linux-androideabi 68 | build: yarn build --target armv7-linux-androideabi 69 | - host: ubuntu-latest 70 | target: aarch64-unknown-linux-musl 71 | docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine 72 | build: |- 73 | set -e && 74 | rustup target add aarch64-unknown-linux-musl && 75 | yarn build --target aarch64-unknown-linux-musl 76 | - host: windows-latest 77 | target: aarch64-pc-windows-msvc 78 | build: yarn build --target aarch64-pc-windows-msvc 79 | name: stable - ${{ matrix.settings.target }} - node@18 80 | runs-on: ${{ matrix.settings.host }} 81 | steps: 82 | - uses: actions/checkout@v4 83 | - name: Setup node 84 | uses: actions/setup-node@v4 85 | if: ${{ !matrix.settings.docker }} 86 | with: 87 | node-version: 20 88 | cache: yarn 89 | - name: Install 90 | uses: dtolnay/rust-toolchain@stable 91 | if: ${{ !matrix.settings.docker }} 92 | with: 93 | toolchain: stable 94 | targets: ${{ matrix.settings.target }} 95 | - name: Cache cargo 96 | uses: actions/cache@v4 97 | with: 98 | path: | 99 | ~/.cargo/registry/index/ 100 | ~/.cargo/registry/cache/ 101 | ~/.cargo/git/db/ 102 | .cargo-cache 103 | target/ 104 | key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} 105 | - uses: goto-bus-stop/setup-zig@v2 106 | if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }} 107 | with: 108 | version: 0.13.0 109 | - name: Setup toolchain 110 | run: ${{ matrix.settings.setup }} 111 | if: ${{ matrix.settings.setup }} 112 | shell: bash 113 | - name: Install dependencies 114 | run: yarn install 115 | - name: Build in docker 116 | uses: addnab/docker-run-action@v3 117 | if: ${{ matrix.settings.docker }} 118 | with: 119 | image: ${{ matrix.settings.docker }} 120 | options: '--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build' 121 | run: ${{ matrix.settings.build }} 122 | - name: Build 123 | run: ${{ matrix.settings.build }} 124 | if: ${{ !matrix.settings.docker }} 125 | shell: bash 126 | - name: Upload artifact 127 | uses: actions/upload-artifact@v4 128 | with: 129 | name: bindings-${{ matrix.settings.target }} 130 | path: ${{ env.APP_NAME }}.*.node 131 | if-no-files-found: error 132 | build-freebsd: 133 | runs-on: ubuntu-latest 134 | name: Build FreeBSD 135 | steps: 136 | - uses: actions/checkout@v4 137 | - name: Build 138 | id: build 139 | uses: cross-platform-actions/action@v0.26.0 140 | env: 141 | DEBUG: napi:* 142 | RUSTUP_IO_THREADS: 1 143 | with: 144 | operating_system: freebsd 145 | version: '14.2' 146 | memory: 8G 147 | cpu_count: 3 148 | environment_variables: 'DEBUG RUSTUP_IO_THREADS' 149 | shell: bash 150 | run: | 151 | sudo pkg install -y -f curl node libnghttp2 npm 152 | sudo npm install -g yarn --ignore-scripts 153 | curl https://sh.rustup.rs -sSf --output rustup.sh 154 | sh rustup.sh -y --profile minimal --default-toolchain beta 155 | source "$HOME/.cargo/env" 156 | echo "~~~~ rustc --version ~~~~" 157 | rustc --version 158 | echo "~~~~ node -v ~~~~" 159 | node -v 160 | echo "~~~~ yarn --version ~~~~" 161 | yarn --version 162 | pwd 163 | ls -lah 164 | whoami 165 | env 166 | freebsd-version 167 | yarn install 168 | yarn build 169 | rm -rf node_modules 170 | rm -rf target 171 | rm -rf .yarn/cache 172 | - name: Upload artifact 173 | uses: actions/upload-artifact@v4 174 | with: 175 | name: bindings-freebsd 176 | path: ${{ env.APP_NAME }}.*.node 177 | if-no-files-found: error 178 | test-macOS-windows-binding: 179 | name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} 180 | needs: 181 | - build 182 | strategy: 183 | fail-fast: false 184 | matrix: 185 | settings: 186 | - host: windows-latest 187 | target: x86_64-pc-windows-msvc 188 | architecture: x64 189 | - host: macos-latest 190 | target: aarch64-apple-darwin 191 | architecture: arm64 192 | - host: macos-latest 193 | target: x86_64-apple-darwin 194 | architecture: x64 195 | node: 196 | - '18' 197 | - '20' 198 | runs-on: ${{ matrix.settings.host }} 199 | steps: 200 | - uses: actions/checkout@v4 201 | - name: Setup node 202 | uses: actions/setup-node@v4 203 | with: 204 | node-version: ${{ matrix.node }} 205 | cache: yarn 206 | architecture: ${{ matrix.settings.architecture }} 207 | - name: Install dependencies 208 | run: yarn install 209 | - name: Download artifacts 210 | uses: actions/download-artifact@v4 211 | with: 212 | name: bindings-${{ matrix.settings.target }} 213 | path: . 214 | - name: List packages 215 | run: ls -R . 216 | shell: bash 217 | - name: Test bindings 218 | run: yarn test 219 | test-linux-x64-gnu-binding: 220 | name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }} 221 | needs: 222 | - build 223 | strategy: 224 | fail-fast: false 225 | matrix: 226 | node: 227 | - '18' 228 | - '20' 229 | runs-on: ubuntu-latest 230 | steps: 231 | - uses: actions/checkout@v4 232 | - name: Setup node 233 | uses: actions/setup-node@v4 234 | with: 235 | node-version: ${{ matrix.node }} 236 | cache: yarn 237 | - name: Install dependencies 238 | run: yarn install 239 | - name: Download artifacts 240 | uses: actions/download-artifact@v4 241 | with: 242 | name: bindings-x86_64-unknown-linux-gnu 243 | path: . 244 | - name: List packages 245 | run: ls -R . 246 | shell: bash 247 | - name: Test bindings 248 | run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-slim yarn test 249 | test-linux-x64-musl-binding: 250 | name: Test bindings on x86_64-unknown-linux-musl - node@${{ matrix.node }} 251 | needs: 252 | - build 253 | strategy: 254 | fail-fast: false 255 | matrix: 256 | node: 257 | - '18' 258 | - '20' 259 | runs-on: ubuntu-latest 260 | steps: 261 | - uses: actions/checkout@v4 262 | - name: Setup node 263 | uses: actions/setup-node@v4 264 | with: 265 | node-version: ${{ matrix.node }} 266 | cache: yarn 267 | - name: Install dependencies 268 | run: | 269 | yarn config set supportedArchitectures.libc "musl" 270 | yarn install 271 | - name: Download artifacts 272 | uses: actions/download-artifact@v4 273 | with: 274 | name: bindings-x86_64-unknown-linux-musl 275 | path: . 276 | - name: List packages 277 | run: ls -R . 278 | shell: bash 279 | - name: Test bindings 280 | run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-alpine yarn test 281 | test-linux-aarch64-gnu-binding: 282 | name: Test bindings on aarch64-unknown-linux-gnu - node@${{ matrix.node }} 283 | needs: 284 | - build 285 | strategy: 286 | fail-fast: false 287 | matrix: 288 | node: 289 | - '18' 290 | - '20' 291 | runs-on: ubuntu-latest 292 | steps: 293 | - uses: actions/checkout@v4 294 | - name: Download artifacts 295 | uses: actions/download-artifact@v4 296 | with: 297 | name: bindings-aarch64-unknown-linux-gnu 298 | path: . 299 | - name: List packages 300 | run: ls -R . 301 | shell: bash 302 | - name: Install dependencies 303 | run: | 304 | yarn config set supportedArchitectures.cpu "arm64" 305 | yarn config set supportedArchitectures.libc "glibc" 306 | yarn install 307 | - name: Set up QEMU 308 | uses: docker/setup-qemu-action@v3 309 | with: 310 | platforms: arm64 311 | - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 312 | - name: Setup and run tests 313 | uses: addnab/docker-run-action@v3 314 | with: 315 | image: node:${{ matrix.node }}-slim 316 | options: '--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build' 317 | run: | 318 | set -e 319 | yarn test 320 | ls -la 321 | test-linux-aarch64-musl-binding: 322 | name: Test bindings on aarch64-unknown-linux-musl - node@lts 323 | needs: 324 | - build 325 | runs-on: ubuntu-latest 326 | steps: 327 | - uses: actions/checkout@v4 328 | - name: Download artifacts 329 | uses: actions/download-artifact@v4 330 | with: 331 | name: bindings-aarch64-unknown-linux-musl 332 | path: . 333 | - name: List packages 334 | run: ls -R . 335 | shell: bash 336 | - name: Install dependencies 337 | run: | 338 | yarn config set supportedArchitectures.cpu "arm64" 339 | yarn config set supportedArchitectures.libc "musl" 340 | yarn install 341 | - name: Set up QEMU 342 | uses: docker/setup-qemu-action@v3 343 | with: 344 | platforms: arm64 345 | - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 346 | - name: Setup and run tests 347 | uses: addnab/docker-run-action@v3 348 | with: 349 | image: node:lts-alpine 350 | options: '--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build' 351 | run: | 352 | set -e 353 | yarn test 354 | test-linux-arm-gnueabihf-binding: 355 | name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }} 356 | needs: 357 | - build 358 | strategy: 359 | fail-fast: false 360 | matrix: 361 | node: 362 | - '18' 363 | - '20' 364 | runs-on: ubuntu-latest 365 | steps: 366 | - uses: actions/checkout@v4 367 | - name: Download artifacts 368 | uses: actions/download-artifact@v4 369 | with: 370 | name: bindings-armv7-unknown-linux-gnueabihf 371 | path: . 372 | - name: List packages 373 | run: ls -R . 374 | shell: bash 375 | - name: Install dependencies 376 | run: | 377 | yarn config set supportedArchitectures.cpu "arm" 378 | yarn install 379 | - name: Set up QEMU 380 | uses: docker/setup-qemu-action@v3 381 | with: 382 | platforms: arm 383 | - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 384 | - name: Setup and run tests 385 | uses: addnab/docker-run-action@v3 386 | with: 387 | image: node:${{ matrix.node }}-bullseye-slim 388 | options: '--platform linux/arm/v7 -v ${{ github.workspace }}:/build -w /build' 389 | run: | 390 | set -e 391 | yarn test 392 | ls -la 393 | publish: 394 | name: Publish 395 | runs-on: ubuntu-latest 396 | needs: 397 | - build-freebsd 398 | - test-macOS-windows-binding 399 | - test-linux-x64-gnu-binding 400 | - test-linux-x64-musl-binding 401 | - test-linux-aarch64-gnu-binding 402 | - test-linux-aarch64-musl-binding 403 | - test-linux-arm-gnueabihf-binding 404 | steps: 405 | - uses: actions/checkout@v4 406 | - name: Setup node 407 | uses: actions/setup-node@v4 408 | with: 409 | node-version: 20 410 | cache: yarn 411 | - name: Install dependencies 412 | run: yarn install 413 | - name: Download all artifacts 414 | uses: actions/download-artifact@v4 415 | with: 416 | path: artifacts 417 | - name: Move artifacts 418 | run: yarn artifacts 419 | - name: List packages 420 | run: ls -R ./npm 421 | shell: bash 422 | - name: Publish 423 | run: | 424 | npm config set provenance true 425 | if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$"; 426 | then 427 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 428 | npm publish --access public 429 | elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; 430 | then 431 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 432 | npm publish --tag next --access public 433 | else 434 | echo "Not a release, skipping publish" 435 | fi 436 | env: 437 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 438 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 439 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - '**' 9 | pull_request: 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | jobs: 14 | lint: 15 | name: Lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Setup node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | cache: 'yarn' 25 | 26 | - name: Install 27 | uses: dtolnay/rust-toolchain@stable 28 | with: 29 | components: clippy, rustfmt 30 | 31 | - name: Install dependencies 32 | run: yarn install 33 | 34 | - name: ESLint 35 | run: yarn lint 36 | 37 | - name: Cargo fmt 38 | run: cargo fmt -- --check 39 | 40 | - name: Clippy 41 | run: cargo clippy 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # End of https://www.toptal.com/developers/gitignore/api/node 114 | 115 | # Created by https://www.toptal.com/developers/gitignore/api/macos 116 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos 117 | 118 | ### macOS ### 119 | # General 120 | .DS_Store 121 | .AppleDouble 122 | .LSOverride 123 | 124 | # Icon must end with two 125 | Icon 126 | 127 | 128 | # Thumbnails 129 | ._* 130 | 131 | # Files that might appear in the root of a volume 132 | .DocumentRevisions-V100 133 | .fseventsd 134 | .Spotlight-V100 135 | .TemporaryItems 136 | .Trashes 137 | .VolumeIcon.icns 138 | .com.apple.timemachine.donotpresent 139 | 140 | # Directories potentially created on remote AFP share 141 | .AppleDB 142 | .AppleDesktop 143 | Network Trash Folder 144 | Temporary Items 145 | .apdisk 146 | 147 | ### macOS Patch ### 148 | # iCloud generated files 149 | *.icloud 150 | 151 | # End of https://www.toptal.com/developers/gitignore/api/macos 152 | 153 | # Created by https://www.toptal.com/developers/gitignore/api/windows 154 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows 155 | 156 | ### Windows ### 157 | # Windows thumbnail cache files 158 | Thumbs.db 159 | Thumbs.db:encryptable 160 | ehthumbs.db 161 | ehthumbs_vista.db 162 | 163 | # Dump file 164 | *.stackdump 165 | 166 | # Folder config file 167 | [Dd]esktop.ini 168 | 169 | # Recycle Bin used on file shares 170 | $RECYCLE.BIN/ 171 | 172 | # Windows Installer files 173 | *.cab 174 | *.msi 175 | *.msix 176 | *.msm 177 | *.msp 178 | 179 | # Windows shortcuts 180 | *.lnk 181 | 182 | # End of https://www.toptal.com/developers/gitignore/api/windows 183 | 184 | #Added by cargo 185 | 186 | /target 187 | Cargo.lock 188 | 189 | .pnp.* 190 | .yarn/* 191 | !.yarn/patches 192 | !.yarn/plugins 193 | !.yarn/releases 194 | !.yarn/sdks 195 | !.yarn/versions 196 | 197 | *.node 198 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .cargo 4 | .github 5 | npm 6 | .eslintrc 7 | .prettierignore 8 | rustfmt.toml 9 | yarn.lock 10 | *.node 11 | .yarn 12 | __test__ 13 | renovate.json 14 | example -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | target 2 | .yarn 3 | node_modules -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | exclude = ["node_modules/**/*.toml"] 2 | 3 | # https://taplo.tamasfe.dev/configuration/formatter-options.html 4 | [formatting] 5 | align_entries = true 6 | indent_tables = true 7 | reorder_keys = true -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | npmAuditRegistry: 'https://registry.npmjs.org' 3 | yarnPath: .yarn/releases/yarn-4.6.0.cjs 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at info@neplextech.com 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for showing interests in contributing to this project. Please follow this guide before contributing to this project. 4 | 5 | ## Don't know where to start? 6 | 7 | If you dont know from where to start, check the issue tracker. 8 | 9 | ## Check for existing PRs 10 | 11 | Before making a pull request, make sure to check if someone else has already made a PR for that specific topic. Avoid duplicated PRs. 12 | 13 | ## Commits 14 | 15 | Follow [conventional commits](https://www.conventionalcommits.org/en) format while committing. Conventional commit dovetails with semver, by describing the features, fixes, and breaking changes made in commit messages. The following specification is adapted from [conventionalcommits.org](https://www.conventionalcommits.org/en). 16 | 17 | The commit message should be structured as follows: 18 | 19 | ```text 20 | [optional scope]: 21 | 22 | [optional body] 23 | 24 | [optional footer] 25 | ``` 26 | 27 | ##### The commit contains the following structural elements, to communicate intent to the consumers of your library: 28 | 29 | 1. `fix`: a commit of the type fix patches a bug in your codebase (this correlates with `PATCH` in semantic versioning). 30 | 2. `feat`: a commit of the type feat introduces a new feature to the codebase (this correlates with `MINOR` in semantic versioning). 31 | 3. `BREAKING CHANGE`: a commit that has the text `BREAKING CHANGE`: at the beginning of its optional body or footer section introduces a breaking API change (correlating with MAJOR in semantic versioning). A `BREAKING CHANGE` can be part of commits of any type. 32 | 4. Others: commit types other than `fix:` and `feat:` are allowed, for example [@commitlint/config-conventional](https://npm.im/@commitlint/config-conventional) (based on the [Angular convention](https://github.com/angular/angular/blob/68a6a07/CONTRIBUTING.md#commit)) recommends `chore:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others. 33 | 34 | ### Examples 35 | 36 | #### Commit message with description and breaking change in body 37 | 38 | ```text 39 | feat: allow provided config object to extend other configs 40 | 41 | BREAKING CHANGE: `extends` key in config file is now used for extending other config files 42 | ``` 43 | 44 | #### Commit message with optional `!` to draw attention to breaking change 45 | 46 | ```text 47 | chore!: drop Node 6 from testing matrix 48 | 49 | BREAKING CHANGE: dropping Node 6 which hits end of life in April 50 | ``` 51 | 52 | #### Commit message with no body 53 | 54 | ```text 55 | docs: correct spelling of CHANGELOG 56 | ``` 57 | 58 | #### Commit message with scope 59 | 60 | ```text 61 | feat(lang): add polish language 62 | ``` 63 | 64 | #### Commit message for a fix using an (optional) issue number. 65 | 66 | ```text 67 | fix: correct minor typos in code 68 | 69 | see the issue for details on the typos fixed 70 | 71 | closes issue #12 72 | ``` 73 | 74 | ### Specification 75 | 76 | The key words `“MUST”`, `“MUST NOT”`, `“REQUIRED”`, `“SHALL”`, `“SHALL NOT”`, `“SHOULD”`, `“SHOULD NOT”`, `“RECOMMENDED”`, `“MAY”`, and `“OPTIONAL”` in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). 77 | 78 | 1. Commits MUST be prefixed with a type, which consists of a noun, `feat`, `fix`, etc., followed by an OPTIONAL scope, and a REQUIRED terminal colon and space. 79 | 2. The type `feat` MUST be used when a commit adds a new feature to your application or library. 80 | 3. The type `fix` MUST be used when a commit represents a bug fix for your application. 81 | 4. A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., `fix(parser):` 82 | 5. A description MUST immediately follow the space after the type/scope prefix. The description is a short summary of the code changes, e.g., `fix: array parsing issue when multiple spaces were contained in string`. 83 | 6. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description. 84 | 7. A footer of one or more lines MAY be provided one blank line after the body. The footer MUST contain meta-information about the commit, e.g., related pull-requests, reviewers, breaking changes, with one piece of meta-information per-line. 85 | 8. Breaking changes MUST be indicated at the very beginning of the body section, or at the beginning of a line in the footer section. A breaking change MUST consist of the uppercase text `BREAKING CHANGE`, followed by a colon and a space. 86 | 9. A description MUST be provided after the `BREAKING CHANGE:`, describing what has changed about the API, e.g., `BREAKING CHANGE: environment variables now take precedence over config files`. 87 | 10. Types other than `feat` and `fix` MAY be used in your commit messages. 88 | 11. The units of information that make up conventional commits MUST NOT be treated as case sensitive by implementors, with the exception of `BREAKING CHANGE` which MUST be uppercase. 89 | 12. A `!` MAY be appended prior to the `:` in the type/scope prefix, to further draw attention to breaking changes. `BREAKING CHANGE: description` MUST also be included in the body or footer, along with the `!` in the prefix. 90 | 91 | ## Formatting, Linting, Testing 92 | 93 | Make sure to properly format the source code, check for linter errors and test the code before pushing. 94 | 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 | 101 | > 🎉 Happy coding! 102 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Twilight "] 3 | edition = "2021" 4 | name = "vectrace" 5 | version = "0.0.0" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | fastrand = "2" 12 | image = "0.25.5" 13 | napi = "2" 14 | napi-derive = "2" 15 | visioncortex = "0.8.8" 16 | 17 | [build-dependencies] 18 | napi-build = "2" 19 | 20 | [profile.release] 21 | lto = true 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Neplex Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @neplex/vectorizer 2 | 3 | Node.js library to convert raster images to svg using [VTracer](https://github.com/visioncortex/vtracer), with time complexity of `O(n)`. 4 | 5 | ## Benchmark 6 | 7 | ```js 8 | clk: ~5.11 GHz 9 | cpu: Intel(R) Core(TM) i7-14700K 10 | runtime: node 22.12.0 (x64-win32) 11 | 12 | benchmark avg (min … max) p75 p99 (min … top 1%) 13 | ------------------------------------------- ------------------------------- 14 | @neplex/vectorizer 543.89 µs/iter 542.50 µs ▆█ 15 | (517.20 µs … 778.50 µs) 719.00 µs ▃██▅▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁ 16 | imagetracerjs 2.54 ms/iter 2.61 ms ▃█▃▂▄ 17 | (2.38 ms … 4.09 ms) 2.93 ms ██████████▄▅▄▅▂▂▁▁▁▂▁ 18 | 19 | summary 20 | @neplex/vectorizer 21 | 4.67x faster than imagetracerjs 22 | ``` 23 | 24 | See [benchmark](https://github.com/neplextech/vectorizer/blob/main/benchmark/bench.ts) for more details. 25 | 26 | ## CLI 27 | 28 | ```bash 29 | npx @neplex/vectorizer ./raster.png ./vector.svg 30 | ``` 31 | 32 | Use `--help` to see all available options. 33 | 34 | ## Installation 35 | 36 | ```bash 37 | npm install @neplex/vectorizer 38 | ``` 39 | 40 | ## Usage 41 | 42 | ```js 43 | import { vectorize, ColorMode, Hierarchical, PathSimplifyMode } from '@neplex/vectorizer'; 44 | import { readFile, writeFile } from 'node:fs/promises'; 45 | 46 | const src = await readFile('./raster.png'); 47 | 48 | const svg = await vectorize(src, { 49 | colorMode: ColorMode.Color, 50 | colorPrecision: 6, 51 | filterSpeckle: 4, 52 | spliceThreshold: 45, 53 | cornerThreshold: 60, 54 | hierarchical: Hierarchical.Stacked, 55 | mode: PathSimplifyMode.Spline, 56 | layerDifference: 5, 57 | lengthThreshold: 5, 58 | maxIterations: 2, 59 | pathPrecision: 5, 60 | }); 61 | 62 | console.log(svg); // ... 63 | await writeFile('./vector.svg', svg); 64 | ``` 65 | 66 | If you want to use a synchronous API, you can use `vectorizeSync` instead. 67 | 68 | ## API 69 | 70 | ### `vectorize(data: Buffer, config?: Config | Preset): Promise` 71 | 72 | Takes an image buffer and returns a promise that resolves to an SVG string. 73 | 74 | ### `vectorizeSync(data: Buffer, config?: Config | Preset): string` 75 | 76 | Takes an image buffer and returns an SVG string synchronously. 77 | 78 | ### `vectorizeRaw(data: Buffer, args: RawDataConfig, config?: Config | Preset): Promise` 79 | 80 | Takes a raw pixel data buffer and returns a promise that resolves to an SVG string. 81 | 82 | ### `vectorizeRawSync(data: Buffer, args: RawDataConfig, config?: Config | Preset): string` 83 | 84 | Takes a raw pixel data buffer and returns an SVG string synchronously. 85 | 86 | ## Demo 87 | 88 | Generated under the following configuration: 89 | 90 | ```js 91 | { 92 | colorMode: ColorMode.Color, 93 | colorPrecision: 8, 94 | filterSpeckle: 4, 95 | spliceThreshold: 45, 96 | cornerThreshold: 60, 97 | hierarchical: Hierarchical.Stacked, 98 | mode: PathSimplifyMode.Spline, 99 | layerDifference: 6, 100 | lengthThreshold: 4, 101 | maxIterations: 2 102 | } 103 | ``` 104 | 105 | | Raster Image (PNG Input) | Vector Image (Generated SVG) | 106 | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 107 | | ![Raster Image](https://raw.githubusercontent.com/neplextech/vectorizer/main/example/anime-girl.png)
[CC-BY-SA 3.0](https://creativecommons.org/licenses/by/3.0) by [Niabot](https://commons.wikimedia.org/wiki/User:Niabot) | ![Vector Image](https://raw.githubusercontent.com/neplextech/vectorizer/main/example/result.svg)
[CC-BY-SA 3.0](https://creativecommons.org/licenses/by/3.0) by [Niabot](https://commons.wikimedia.org/wiki/User:Niabot) | 108 | -------------------------------------------------------------------------------- /__test__/data/firefox-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neplextech/vectorizer/46d04f1b286b678cf3e692b3b0b6e6f4f3b9961d/__test__/data/firefox-logo.png -------------------------------------------------------------------------------- /__test__/data/result-bw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /__test__/data/result-firefox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /__test__/data/result-photo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /__test__/data/result-raw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /__test__/data/result.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /__test__/data/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neplextech/vectorizer/46d04f1b286b678cf3e692b3b0b6e6f4f3b9961d/__test__/data/sample.png -------------------------------------------------------------------------------- /__test__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { writeFile, readFile } from 'node:fs/promises'; 3 | import { ColorMode, vectorize, PathSimplifyMode, Hierarchical, Preset, vectorizeRaw } from '../index.js'; 4 | import { Transformer } from '@napi-rs/image'; 5 | 6 | const src = await readFile('./__test__/data/firefox-logo.png'); 7 | const config = { 8 | colorMode: ColorMode.Color, 9 | colorPrecision: 6, 10 | filterSpeckle: 4, 11 | spliceThreshold: 45, 12 | cornerThreshold: 60, 13 | hierarchical: Hierarchical.Stacked, 14 | mode: PathSimplifyMode.Spline, 15 | layerDifference: 5, 16 | lengthThreshold: 5, 17 | maxIterations: 2, 18 | pathPrecision: 5, 19 | }; 20 | 21 | const configCircle = { ...config, width: 100, height: 100 }; 22 | const configFirefox = { 23 | ...config, 24 | filterSpeckle: 14, 25 | colorPrecision: 8, 26 | mode: PathSimplifyMode.Polygon, 27 | layerDifference: 0, 28 | }; 29 | 30 | test('should vectorize image (simple)', async (t) => { 31 | const src = await readFile('./__test__/data/sample.png'); 32 | const result = await vectorize(src, configCircle); 33 | 34 | await writeFile('./__test__/data/result.svg', result); 35 | 36 | t.pass(); 37 | }); 38 | 39 | test('should vectorize raw pixels data', async (t) => { 40 | const src = await readFile('./__test__/data/sample.png'); 41 | const raw = await new Transformer(src).rawPixels(); 42 | const result = await vectorizeRaw( 43 | raw, 44 | { 45 | height: 100, 46 | width: 100, 47 | }, 48 | configCircle, 49 | ); 50 | 51 | await writeFile('./__test__/data/result-raw.svg', result); 52 | 53 | t.pass(); 54 | }); 55 | 56 | test('should vectorize image', async (t) => { 57 | const result = await vectorize(src, configFirefox); 58 | 59 | await writeFile('./__test__/data/result-firefox.svg', result); 60 | 61 | t.pass(); 62 | }); 63 | 64 | test('should vectorize image with preset bw', async (t) => { 65 | const result = await vectorize(src, Preset.Bw); 66 | 67 | await writeFile('./__test__/data/result-bw.svg', result); 68 | 69 | t.pass(); 70 | }); 71 | 72 | test('should vectorize image with preset Photo', async (t) => { 73 | const result = await vectorize(src, Preset.Photo); 74 | 75 | await writeFile('./__test__/data/result-photo.svg', result); 76 | 77 | t.pass(); 78 | }); 79 | 80 | test('should vectorize image with preset Poster', async (t) => { 81 | const result = await vectorize(src, Preset.Poster); 82 | 83 | await writeFile('./__test__/data/result-poster.svg', result); 84 | 85 | t.pass(); 86 | }); 87 | -------------------------------------------------------------------------------- /__test__/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /__test__/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "outDir": "lib", 7 | "rootDir": "." 8 | }, 9 | "include": ["*.ts"], 10 | "exclude": ["lib"] 11 | } 12 | -------------------------------------------------------------------------------- /benchmark/bench.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import { run, bench, summary } from 'mitata'; 4 | import { vectorizeRawSync } from '../index.js'; 5 | // @ts-ignore 6 | import ImageTracer from 'imagetracerjs'; 7 | import { Transformer } from '@napi-rs/image'; 8 | 9 | const data = await readFile(join(import.meta.dirname, 'data', 'sample.png')); 10 | const image = await new Transformer(data).rawPixels(); 11 | const imageData = { 12 | width: 100, 13 | height: 100, 14 | data: image, 15 | }; 16 | 17 | const { data: pixels, ...size } = imageData; 18 | 19 | summary(() => { 20 | bench('@neplex/vectorizer', () => { 21 | vectorizeRawSync(pixels, size); 22 | }); 23 | 24 | bench('imagetracerjs', () => { 25 | ImageTracer.imagedataToSVG(imageData); 26 | }); 27 | }); 28 | 29 | await run(); 30 | -------------------------------------------------------------------------------- /benchmark/data/sample-imagetracer.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 31 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 73 | 75 | 77 | 79 | 81 | 83 | 85 | 87 | 89 | -------------------------------------------------------------------------------- /benchmark/data/sample-vectorizer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | -------------------------------------------------------------------------------- /benchmark/data/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neplextech/vectorizer/46d04f1b286b678cf3e692b3b0b6e6f4f3b9961d/benchmark/data/sample.png -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "NodeNext", 5 | "module": "NodeNext", 6 | "outDir": "lib" 7 | }, 8 | "include": ["."], 9 | "exclude": ["lib"] 10 | } 11 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate napi_build; 2 | 3 | fn main() { 4 | napi_build::setup(); 5 | } 6 | -------------------------------------------------------------------------------- /cli/index.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command } from 'commander'; 4 | import pc from 'picocolors'; 5 | import fs from 'node:fs/promises'; 6 | import { vectorize, Preset, ColorMode, Hierarchical, PathSimplifyMode } from '../index.js'; 7 | 8 | const { version } = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url))); 9 | const program = new Command(); 10 | 11 | program 12 | .name('vectorizer') 13 | .description('Convert raster images to SVG vector graphics') 14 | .argument('', 'Input image file path') 15 | .argument('[output]', 'Output SVG file path (defaults to input path with .svg extension)') 16 | .option('--print', 'Print the SVG output to terminal instead of writing to file') 17 | .option('-p, --preset ', 'Use preset configuration (bw|poster|photo)', 'photo') 18 | .option('--color-mode ', 'Color mode (color|binary)', 'color') 19 | .option('--hierarchical ', 'Hierarchical mode (stacked|cutout)', 'stacked') 20 | .option('--filter-speckle ', 'Filter speckles smaller than X pixels', parseInt) 21 | .option('--color-precision ', 'Color precision in bits', parseInt) 22 | .option('--layer-difference ', 'Color difference between layers', parseFloat) 23 | .option('--mode ', 'Path simplify mode (none|polygon|spline)', 'spline') 24 | .option('--corner-threshold ', 'Corner threshold in degrees', parseFloat) 25 | .option('--length-threshold ', 'Length threshold', parseFloat) 26 | .option('--max-iterations ', 'Maximum iterations', parseInt) 27 | .option('--splice-threshold ', 'Splice threshold in degrees', parseFloat) 28 | .option('--path-precision ', 'Path precision decimal places', parseInt) 29 | .version(version, '-v, --version', 'Output the current version'); 30 | 31 | async function main() { 32 | try { 33 | program.parse(); 34 | 35 | const [inputPath, outputPath] = program.args; 36 | const options = program.opts(); 37 | 38 | if (options.print && outputPath) { 39 | console.error(pc.yellow('Warning: Output path is ignored when using --print flag')); 40 | } 41 | 42 | // Validate input file 43 | try { 44 | await fs.access(inputPath); 45 | } catch { 46 | console.error(pc.red(`Error: Input file '${inputPath}' does not exist`)); 47 | process.exit(1); 48 | } 49 | 50 | // Determine output path 51 | const finalOutputPath = outputPath || inputPath.replace(/\.[^.]+$/, '.svg'); 52 | 53 | // Read input file 54 | const inputBuffer = await fs.readFile(inputPath); 55 | 56 | // Parse preset 57 | let config = null; 58 | if (options.preset) { 59 | const presetMap = { bw: Preset.Bw, poster: Preset.Poster, photo: Preset.Photo }; 60 | const preset = presetMap[options.preset.toLowerCase()]; 61 | if (preset !== undefined) { 62 | config = preset; 63 | } 64 | } 65 | 66 | // If not using preset, build config object 67 | if (config === null && Object.keys(options).length > 1) { 68 | config = { 69 | colorMode: options.colorMode === 'binary' ? ColorMode.Binary : ColorMode.Color, 70 | hierarchical: options.hierarchical === 'cutout' ? Hierarchical.Cutout : Hierarchical.Stacked, 71 | filterSpeckle: options.filterSpeckle, 72 | colorPrecision: options.colorPrecision, 73 | layerDifference: options.layerDifference, 74 | mode: 75 | { 76 | none: PathSimplifyMode.None, 77 | polygon: PathSimplifyMode.Polygon, 78 | spline: PathSimplifyMode.Spline, 79 | }[options.mode] || PathSimplifyMode.Spline, 80 | cornerThreshold: options.cornerThreshold, 81 | lengthThreshold: options.lengthThreshold, 82 | maxIterations: options.maxIterations, 83 | spliceThreshold: options.spliceThreshold, 84 | pathPrecision: options.pathPrecision, 85 | }; 86 | 87 | // Remove undefined values 88 | Object.keys(config).forEach((key) => config[key] === undefined && delete config[key]); 89 | } 90 | 91 | console.log(pc.blue(`Converting image ${pc.underline(inputPath)} to svg...`), '\n'); 92 | 93 | // Process the image 94 | const start = performance.now(); 95 | const svg = await vectorize(inputBuffer, config); 96 | const end = performance.now() - start; 97 | 98 | // Handle output based on print flag 99 | if (options.print) { 100 | console.log(pc.green(svg)); 101 | console.log(pc.blue(`Vectorization took ${end.toFixed(2)}ms`)); 102 | } else { 103 | // Write output file 104 | await fs.writeFile(finalOutputPath, svg, 'utf8'); 105 | console.log(pc.green(`Successfully converted to ${finalOutputPath} in ${end.toFixed(2)}ms`)); 106 | } 107 | } catch (error) { 108 | console.error(pc.red('Error:'), error.message); 109 | process.exit(1); 110 | } 111 | } 112 | 113 | main(); 114 | -------------------------------------------------------------------------------- /example/anime-girl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neplextech/vectorizer/46d04f1b286b678cf3e692b3b0b6e6f4f3b9961d/example/anime-girl.png -------------------------------------------------------------------------------- /example/anime-girl.ts: -------------------------------------------------------------------------------- 1 | import { vectorize, ColorMode, Hierarchical, PathSimplifyMode } from '../index.js'; 2 | import { readFile, writeFile } from 'node:fs/promises'; 3 | 4 | const src = await readFile('./example/anime-girl.png'); 5 | 6 | const config = { 7 | colorMode: ColorMode.Color, 8 | colorPrecision: 8, 9 | filterSpeckle: 4, 10 | spliceThreshold: 45, 11 | cornerThreshold: 60, 12 | hierarchical: Hierarchical.Stacked, 13 | mode: PathSimplifyMode.Spline, 14 | layerDifference: 6, 15 | lengthThreshold: 4, 16 | maxIterations: 2, 17 | }; 18 | 19 | const begin = performance.now(); 20 | const result = await vectorize(src, config); 21 | const end = performance.now(); 22 | 23 | console.log(`[Anime Girl Vectorization] Time: ${(end - begin).toFixed(2)}ms`); 24 | 25 | await writeFile('./example/result.svg', result); 26 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "NodeNext", 5 | "module": "NodeNext", 6 | "outDir": "lib" 7 | }, 8 | "include": ["."], 9 | "exclude": ["lib"] 10 | } 11 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | 4 | /* auto-generated by NAPI-RS */ 5 | 6 | export const enum Preset { 7 | Bw = 0, 8 | Poster = 1, 9 | Photo = 2, 10 | } 11 | export const enum ColorMode { 12 | Color = 0, 13 | Binary = 1, 14 | } 15 | export const enum Hierarchical { 16 | Stacked = 0, 17 | Cutout = 1, 18 | } 19 | export const enum PathSimplifyMode { 20 | None = 0, 21 | Polygon = 1, 22 | Spline = 2, 23 | } 24 | export interface Config { 25 | /** True color image or binary image (black and white) */ 26 | colorMode: ColorMode; 27 | /** Hierarchial clustering or non-stacked. Only applicable to color images. */ 28 | hierarchical: Hierarchical; 29 | /** Discard patches smaller than X pixels in size (cleaner) */ 30 | filterSpeckle: number; 31 | /** The number of significant bits to use in an RGB channel (more accurate) */ 32 | colorPrecision: number; 33 | /** The color difference between gradient layers (less layers) */ 34 | layerDifference: number; 35 | /** Curve fitting mode */ 36 | mode: PathSimplifyMode; 37 | /** Minimum momentary angle (degree) to be considered a corner (smoother) */ 38 | cornerThreshold: number; 39 | /** Perform iterative subdivide smooth until all segments are shorter than this length */ 40 | lengthThreshold: number; 41 | /** The maximum number of iterations to perform */ 42 | maxIterations: number; 43 | /** Minimum angle displacement (degree) to splice a spline (less accurate) */ 44 | spliceThreshold: number; 45 | /** Number of decimal places to use in path string */ 46 | pathPrecision?: number; 47 | } 48 | export interface RawDataConfig { 49 | width: number; 50 | height: number; 51 | } 52 | export declare function vectorize(source: Buffer, config?: Config | Preset | undefined | null): Promise; 53 | export declare function vectorizeRaw( 54 | source: Buffer, 55 | args: RawDataConfig, 56 | config?: Config | Preset | undefined | null, 57 | ): Promise; 58 | export declare function vectorizeSync(source: Buffer, config?: Config | Preset | undefined | null): string; 59 | export declare function vectorizeRawSync( 60 | source: Buffer, 61 | args: RawDataConfig, 62 | config?: Config | Preset | undefined | null, 63 | ): string; 64 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /* prettier-ignore */ 4 | 5 | /* auto-generated by NAPI-RS */ 6 | 7 | const { existsSync, readFileSync } = require('fs') 8 | const { join } = require('path'); 9 | 10 | const { platform, arch } = process; 11 | 12 | let nativeBinding = null; 13 | let localFileExisted = false; 14 | let loadError = null; 15 | 16 | function isMusl() { 17 | // For Node 10 18 | if (!process.report || typeof process.report.getReport !== 'function') { 19 | try { 20 | const lddPath = require('child_process').execSync('which ldd').toString().trim(); 21 | return readFileSync(lddPath, 'utf8').includes('musl'); 22 | } catch (e) { 23 | return true; 24 | } 25 | } else { 26 | const { glibcVersionRuntime } = process.report.getReport().header; 27 | return !glibcVersionRuntime; 28 | } 29 | } 30 | 31 | switch (platform) { 32 | case 'android': 33 | switch (arch) { 34 | case 'arm64': 35 | localFileExisted = existsSync(join(__dirname, 'vectorizer.android-arm64.node')); 36 | try { 37 | if (localFileExisted) { 38 | nativeBinding = require('./vectorizer.android-arm64.node'); 39 | } else { 40 | nativeBinding = require('@neplex/vectorizer-android-arm64'); 41 | } 42 | } catch (e) { 43 | loadError = e; 44 | } 45 | break; 46 | case 'arm': 47 | localFileExisted = existsSync(join(__dirname, 'vectorizer.android-arm-eabi.node')); 48 | try { 49 | if (localFileExisted) { 50 | nativeBinding = require('./vectorizer.android-arm-eabi.node'); 51 | } else { 52 | nativeBinding = require('@neplex/vectorizer-android-arm-eabi'); 53 | } 54 | } catch (e) { 55 | loadError = e; 56 | } 57 | break; 58 | default: 59 | throw new Error(`Unsupported architecture on Android ${arch}`); 60 | } 61 | break; 62 | case 'win32': 63 | switch (arch) { 64 | case 'x64': 65 | localFileExisted = existsSync(join(__dirname, 'vectorizer.win32-x64-msvc.node')); 66 | try { 67 | if (localFileExisted) { 68 | nativeBinding = require('./vectorizer.win32-x64-msvc.node'); 69 | } else { 70 | nativeBinding = require('@neplex/vectorizer-win32-x64-msvc'); 71 | } 72 | } catch (e) { 73 | loadError = e; 74 | } 75 | break; 76 | case 'ia32': 77 | localFileExisted = existsSync(join(__dirname, 'vectorizer.win32-ia32-msvc.node')); 78 | try { 79 | if (localFileExisted) { 80 | nativeBinding = require('./vectorizer.win32-ia32-msvc.node'); 81 | } else { 82 | nativeBinding = require('@neplex/vectorizer-win32-ia32-msvc'); 83 | } 84 | } catch (e) { 85 | loadError = e; 86 | } 87 | break; 88 | case 'arm64': 89 | localFileExisted = existsSync(join(__dirname, 'vectorizer.win32-arm64-msvc.node')); 90 | try { 91 | if (localFileExisted) { 92 | nativeBinding = require('./vectorizer.win32-arm64-msvc.node'); 93 | } else { 94 | nativeBinding = require('@neplex/vectorizer-win32-arm64-msvc'); 95 | } 96 | } catch (e) { 97 | loadError = e; 98 | } 99 | break; 100 | default: 101 | throw new Error(`Unsupported architecture on Windows: ${arch}`); 102 | } 103 | break; 104 | case 'darwin': 105 | localFileExisted = existsSync(join(__dirname, 'vectorizer.darwin-universal.node')); 106 | try { 107 | if (localFileExisted) { 108 | nativeBinding = require('./vectorizer.darwin-universal.node'); 109 | } else { 110 | nativeBinding = require('@neplex/vectorizer-darwin-universal'); 111 | } 112 | break; 113 | } catch {} 114 | switch (arch) { 115 | case 'x64': 116 | localFileExisted = existsSync(join(__dirname, 'vectorizer.darwin-x64.node')); 117 | try { 118 | if (localFileExisted) { 119 | nativeBinding = require('./vectorizer.darwin-x64.node'); 120 | } else { 121 | nativeBinding = require('@neplex/vectorizer-darwin-x64'); 122 | } 123 | } catch (e) { 124 | loadError = e; 125 | } 126 | break; 127 | case 'arm64': 128 | localFileExisted = existsSync(join(__dirname, 'vectorizer.darwin-arm64.node')); 129 | try { 130 | if (localFileExisted) { 131 | nativeBinding = require('./vectorizer.darwin-arm64.node'); 132 | } else { 133 | nativeBinding = require('@neplex/vectorizer-darwin-arm64'); 134 | } 135 | } catch (e) { 136 | loadError = e; 137 | } 138 | break; 139 | default: 140 | throw new Error(`Unsupported architecture on macOS: ${arch}`); 141 | } 142 | break; 143 | case 'freebsd': 144 | if (arch !== 'x64') { 145 | throw new Error(`Unsupported architecture on FreeBSD: ${arch}`); 146 | } 147 | localFileExisted = existsSync(join(__dirname, 'vectorizer.freebsd-x64.node')); 148 | try { 149 | if (localFileExisted) { 150 | nativeBinding = require('./vectorizer.freebsd-x64.node'); 151 | } else { 152 | nativeBinding = require('@neplex/vectorizer-freebsd-x64'); 153 | } 154 | } catch (e) { 155 | loadError = e; 156 | } 157 | break; 158 | case 'linux': 159 | switch (arch) { 160 | case 'x64': 161 | if (isMusl()) { 162 | localFileExisted = existsSync(join(__dirname, 'vectorizer.linux-x64-musl.node')); 163 | try { 164 | if (localFileExisted) { 165 | nativeBinding = require('./vectorizer.linux-x64-musl.node'); 166 | } else { 167 | nativeBinding = require('@neplex/vectorizer-linux-x64-musl'); 168 | } 169 | } catch (e) { 170 | loadError = e; 171 | } 172 | } else { 173 | localFileExisted = existsSync(join(__dirname, 'vectorizer.linux-x64-gnu.node')); 174 | try { 175 | if (localFileExisted) { 176 | nativeBinding = require('./vectorizer.linux-x64-gnu.node'); 177 | } else { 178 | nativeBinding = require('@neplex/vectorizer-linux-x64-gnu'); 179 | } 180 | } catch (e) { 181 | loadError = e; 182 | } 183 | } 184 | break; 185 | case 'arm64': 186 | if (isMusl()) { 187 | localFileExisted = existsSync(join(__dirname, 'vectorizer.linux-arm64-musl.node')); 188 | try { 189 | if (localFileExisted) { 190 | nativeBinding = require('./vectorizer.linux-arm64-musl.node'); 191 | } else { 192 | nativeBinding = require('@neplex/vectorizer-linux-arm64-musl'); 193 | } 194 | } catch (e) { 195 | loadError = e; 196 | } 197 | } else { 198 | localFileExisted = existsSync(join(__dirname, 'vectorizer.linux-arm64-gnu.node')); 199 | try { 200 | if (localFileExisted) { 201 | nativeBinding = require('./vectorizer.linux-arm64-gnu.node'); 202 | } else { 203 | nativeBinding = require('@neplex/vectorizer-linux-arm64-gnu'); 204 | } 205 | } catch (e) { 206 | loadError = e; 207 | } 208 | } 209 | break; 210 | case 'arm': 211 | if (isMusl()) { 212 | localFileExisted = existsSync(join(__dirname, 'vectorizer.linux-arm-musleabihf.node')); 213 | try { 214 | if (localFileExisted) { 215 | nativeBinding = require('./vectorizer.linux-arm-musleabihf.node'); 216 | } else { 217 | nativeBinding = require('@neplex/vectorizer-linux-arm-musleabihf'); 218 | } 219 | } catch (e) { 220 | loadError = e; 221 | } 222 | } else { 223 | localFileExisted = existsSync(join(__dirname, 'vectorizer.linux-arm-gnueabihf.node')); 224 | try { 225 | if (localFileExisted) { 226 | nativeBinding = require('./vectorizer.linux-arm-gnueabihf.node'); 227 | } else { 228 | nativeBinding = require('@neplex/vectorizer-linux-arm-gnueabihf'); 229 | } 230 | } catch (e) { 231 | loadError = e; 232 | } 233 | } 234 | break; 235 | case 'riscv64': 236 | if (isMusl()) { 237 | localFileExisted = existsSync(join(__dirname, 'vectorizer.linux-riscv64-musl.node')); 238 | try { 239 | if (localFileExisted) { 240 | nativeBinding = require('./vectorizer.linux-riscv64-musl.node'); 241 | } else { 242 | nativeBinding = require('@neplex/vectorizer-linux-riscv64-musl'); 243 | } 244 | } catch (e) { 245 | loadError = e; 246 | } 247 | } else { 248 | localFileExisted = existsSync(join(__dirname, 'vectorizer.linux-riscv64-gnu.node')); 249 | try { 250 | if (localFileExisted) { 251 | nativeBinding = require('./vectorizer.linux-riscv64-gnu.node'); 252 | } else { 253 | nativeBinding = require('@neplex/vectorizer-linux-riscv64-gnu'); 254 | } 255 | } catch (e) { 256 | loadError = e; 257 | } 258 | } 259 | break; 260 | case 's390x': 261 | localFileExisted = existsSync(join(__dirname, 'vectorizer.linux-s390x-gnu.node')); 262 | try { 263 | if (localFileExisted) { 264 | nativeBinding = require('./vectorizer.linux-s390x-gnu.node'); 265 | } else { 266 | nativeBinding = require('@neplex/vectorizer-linux-s390x-gnu'); 267 | } 268 | } catch (e) { 269 | loadError = e; 270 | } 271 | break; 272 | default: 273 | throw new Error(`Unsupported architecture on Linux: ${arch}`); 274 | } 275 | break; 276 | default: 277 | throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`); 278 | } 279 | 280 | if (!nativeBinding) { 281 | if (loadError) { 282 | throw loadError; 283 | } 284 | throw new Error(`Failed to load native binding`); 285 | } 286 | 287 | const { Preset, ColorMode, Hierarchical, PathSimplifyMode, vectorize, vectorizeRaw, vectorizeSync, vectorizeRawSync } = 288 | nativeBinding; 289 | 290 | module.exports.Preset = Preset; 291 | module.exports.ColorMode = ColorMode; 292 | module.exports.Hierarchical = Hierarchical; 293 | module.exports.PathSimplifyMode = PathSimplifyMode; 294 | module.exports.vectorize = vectorize; 295 | module.exports.vectorizeRaw = vectorizeRaw; 296 | module.exports.vectorizeSync = vectorizeSync; 297 | module.exports.vectorizeRawSync = vectorizeRawSync; 298 | -------------------------------------------------------------------------------- /npm/android-arm-eabi/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-android-arm-eabi` 2 | 3 | This is the **armv7-linux-androideabi** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/android-arm-eabi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-android-arm-eabi", 3 | "version": "0.0.4", 4 | "os": [ 5 | "android" 6 | ], 7 | "cpu": [ 8 | "arm" 9 | ], 10 | "main": "vectorizer.android-arm-eabi.node", 11 | "files": [ 12 | "vectorizer.android-arm-eabi.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /npm/android-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-android-arm64` 2 | 3 | This is the **aarch64-linux-android** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/android-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-android-arm64", 3 | "version": "0.0.4", 4 | "os": [ 5 | "android" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "vectorizer.android-arm64.node", 11 | "files": [ 12 | "vectorizer.android-arm64.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /npm/darwin-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-darwin-arm64` 2 | 3 | This is the **aarch64-apple-darwin** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/darwin-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-darwin-arm64", 3 | "version": "0.0.4", 4 | "os": [ 5 | "darwin" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "vectorizer.darwin-arm64.node", 11 | "files": [ 12 | "vectorizer.darwin-arm64.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /npm/darwin-x64/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-darwin-x64` 2 | 3 | This is the **x86_64-apple-darwin** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/darwin-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-darwin-x64", 3 | "version": "0.0.4", 4 | "os": [ 5 | "darwin" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "vectorizer.darwin-x64.node", 11 | "files": [ 12 | "vectorizer.darwin-x64.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /npm/freebsd-x64/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-freebsd-x64` 2 | 3 | This is the **x86_64-unknown-freebsd** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/freebsd-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-freebsd-x64", 3 | "version": "0.0.4", 4 | "os": [ 5 | "freebsd" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "vectorizer.freebsd-x64.node", 11 | "files": [ 12 | "vectorizer.freebsd-x64.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /npm/linux-arm-gnueabihf/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-linux-arm-gnueabihf` 2 | 3 | This is the **armv7-unknown-linux-gnueabihf** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/linux-arm-gnueabihf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-linux-arm-gnueabihf", 3 | "version": "0.0.4", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "arm" 9 | ], 10 | "main": "vectorizer.linux-arm-gnueabihf.node", 11 | "files": [ 12 | "vectorizer.linux-arm-gnueabihf.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /npm/linux-arm64-gnu/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-linux-arm64-gnu` 2 | 3 | This is the **aarch64-unknown-linux-gnu** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/linux-arm64-gnu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-linux-arm64-gnu", 3 | "version": "0.0.4", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "vectorizer.linux-arm64-gnu.node", 11 | "files": [ 12 | "vectorizer.linux-arm64-gnu.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | }, 28 | "libc": [ 29 | "glibc" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /npm/linux-arm64-musl/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-linux-arm64-musl` 2 | 3 | This is the **aarch64-unknown-linux-musl** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/linux-arm64-musl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-linux-arm64-musl", 3 | "version": "0.0.4", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "vectorizer.linux-arm64-musl.node", 11 | "files": [ 12 | "vectorizer.linux-arm64-musl.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | }, 28 | "libc": [ 29 | "musl" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /npm/linux-x64-gnu/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-linux-x64-gnu` 2 | 3 | This is the **x86_64-unknown-linux-gnu** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/linux-x64-gnu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-linux-x64-gnu", 3 | "version": "0.0.4", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "vectorizer.linux-x64-gnu.node", 11 | "files": [ 12 | "vectorizer.linux-x64-gnu.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | }, 28 | "libc": [ 29 | "glibc" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /npm/linux-x64-musl/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-linux-x64-musl` 2 | 3 | This is the **x86_64-unknown-linux-musl** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/linux-x64-musl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-linux-x64-musl", 3 | "version": "0.0.4", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "vectorizer.linux-x64-musl.node", 11 | "files": [ 12 | "vectorizer.linux-x64-musl.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | }, 28 | "libc": [ 29 | "musl" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /npm/win32-arm64-msvc/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-win32-arm64-msvc` 2 | 3 | This is the **aarch64-pc-windows-msvc** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/win32-arm64-msvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-win32-arm64-msvc", 3 | "version": "0.0.4", 4 | "os": [ 5 | "win32" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "vectorizer.win32-arm64-msvc.node", 11 | "files": [ 12 | "vectorizer.win32-arm64-msvc.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /npm/win32-ia32-msvc/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-win32-ia32-msvc` 2 | 3 | This is the **i686-pc-windows-msvc** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/win32-ia32-msvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-win32-ia32-msvc", 3 | "version": "0.0.4", 4 | "os": [ 5 | "win32" 6 | ], 7 | "cpu": [ 8 | "ia32" 9 | ], 10 | "main": "vectorizer.win32-ia32-msvc.node", 11 | "files": [ 12 | "vectorizer.win32-ia32-msvc.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /npm/win32-x64-msvc/README.md: -------------------------------------------------------------------------------- 1 | # `@neplex/vectorizer-win32-x64-msvc` 2 | 3 | This is the **x86_64-pc-windows-msvc** binary for `@neplex/vectorizer` 4 | -------------------------------------------------------------------------------- /npm/win32-x64-msvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer-win32-x64-msvc", 3 | "version": "0.0.4", 4 | "os": [ 5 | "win32" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "vectorizer.win32-x64-msvc.node", 11 | "files": [ 12 | "vectorizer.win32-x64-msvc.node" 13 | ], 14 | "description": "Node.js library to convert raster images to svg", 15 | "author": "twlite ", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">= 10" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/", 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/neplextech/vectorizer" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neplex/vectorizer", 3 | "version": "0.0.5", 4 | "description": "Node.js library to convert raster images to svg", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "bin": "cli/index.mjs", 8 | "files": [ 9 | "index.js", 10 | "index.d.ts", 11 | "cli/index.mjs" 12 | ], 13 | "napi": { 14 | "name": "vectorizer", 15 | "triples": { 16 | "defaults": true, 17 | "additional": [ 18 | "x86_64-unknown-linux-musl", 19 | "aarch64-unknown-linux-gnu", 20 | "i686-pc-windows-msvc", 21 | "armv7-unknown-linux-gnueabihf", 22 | "aarch64-apple-darwin", 23 | "aarch64-linux-android", 24 | "x86_64-unknown-freebsd", 25 | "aarch64-unknown-linux-musl", 26 | "aarch64-pc-windows-msvc", 27 | "armv7-linux-androideabi" 28 | ] 29 | } 30 | }, 31 | "license": "MIT", 32 | "author": "twlite ", 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/neplextech/vectorizer" 36 | }, 37 | "engines": { 38 | "node": ">= 10" 39 | }, 40 | "publishConfig": { 41 | "registry": "https://registry.npmjs.org/", 42 | "access": "public" 43 | }, 44 | "scripts": { 45 | "artifacts": "napi artifacts", 46 | "benchmark": "node --expose-gc --import @swc-node/register/esm-register benchmark/bench.ts", 47 | "build": "napi build --platform --release", 48 | "build:debug": "napi build --platform", 49 | "example": "node --import @swc-node/register/esm-register example/anime-girl.ts", 50 | "format": "run-p format:prettier format:rs format:toml", 51 | "format:prettier": "prettier . -w", 52 | "format:toml": "taplo format", 53 | "format:rs": "cargo fmt", 54 | "lint": "oxlint .", 55 | "prepublishOnly": "napi prepublish -t npm", 56 | "test": "ava", 57 | "universal": "napi universal", 58 | "version": "napi version", 59 | "prepare": "husky" 60 | }, 61 | "devDependencies": { 62 | "@napi-rs/cli": "^2.18.4", 63 | "@napi-rs/image": "^1.9.2", 64 | "@swc-node/register": "^1.10.6", 65 | "@swc/core": "^1.6.13", 66 | "@taplo/cli": "^0.7.0", 67 | "@types/node": "^22.10.5", 68 | "ava": "^6.1.3", 69 | "chalk": "^5.3.0", 70 | "husky": "^9.0.11", 71 | "imagetracerjs": "^1.2.6", 72 | "lint-staged": "^15.2.7", 73 | "mitata": "^1.0.26", 74 | "npm-run-all2": "^7.0.0", 75 | "oxlint": "^0.15.0", 76 | "prettier": "^3.3.3", 77 | "typescript": "^5.5.3" 78 | }, 79 | "dependencies": { 80 | "commander": "^13.0.0", 81 | "picocolors": "^1.1.1" 82 | }, 83 | "packageManager": "yarn@4.6.0", 84 | "ava": { 85 | "extensions": { 86 | "ts": "module" 87 | }, 88 | "timeout": "2m", 89 | "workerThreads": false, 90 | "environmentVariables": { 91 | "TS_NODE_PROJECT": "./tsconfig.json" 92 | }, 93 | "nodeArguments": [ 94 | "--import", 95 | "@swc-node/register/esm-register" 96 | ] 97 | }, 98 | "prettier": { 99 | "printWidth": 120, 100 | "semi": true, 101 | "trailingComma": "all", 102 | "singleQuote": true, 103 | "arrowParens": "always" 104 | }, 105 | "lint-staged": { 106 | "*.@(js|ts|tsx)": [ 107 | "oxlint --fix" 108 | ], 109 | "*.@(js|ts|tsx|yml|yaml|md|json)": [ 110 | "prettier --write" 111 | ], 112 | "*.toml": [ 113 | "taplo format" 114 | ] 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | edition = "2021" 3 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/visioncortex/vtracer/blob/74f2a04a17d8c246d80c439fb162780160a7c3e9/cmdapp/src/config.rs 2 | 3 | use std::str::FromStr; 4 | use visioncortex::PathSimplifyMode; 5 | 6 | use napi_derive::*; 7 | 8 | #[napi] 9 | pub enum Preset { 10 | Bw, 11 | Poster, 12 | Photo, 13 | } 14 | 15 | #[napi] 16 | pub enum ColorMode { 17 | Color, 18 | Binary, 19 | } 20 | 21 | #[napi] 22 | pub enum Hierarchical { 23 | Stacked, 24 | Cutout, 25 | } 26 | 27 | /// Converter config 28 | pub struct Config { 29 | pub color_mode: ColorMode, 30 | pub hierarchical: Hierarchical, 31 | pub filter_speckle: usize, 32 | pub color_precision: i32, 33 | pub layer_difference: i32, 34 | pub mode: PathSimplifyMode, 35 | pub corner_threshold: i32, 36 | pub length_threshold: f64, 37 | pub max_iterations: usize, 38 | pub splice_threshold: i32, 39 | pub path_precision: Option, 40 | } 41 | 42 | pub(crate) struct ConverterConfig { 43 | pub color_mode: ColorMode, 44 | pub hierarchical: Hierarchical, 45 | pub filter_speckle_area: usize, 46 | pub color_precision_loss: i32, 47 | pub layer_difference: i32, 48 | pub mode: PathSimplifyMode, 49 | pub corner_threshold: f64, 50 | pub length_threshold: f64, 51 | pub max_iterations: usize, 52 | pub splice_threshold: f64, 53 | pub path_precision: Option, 54 | } 55 | 56 | impl Default for Config { 57 | fn default() -> Self { 58 | Self { 59 | color_mode: ColorMode::Color, 60 | hierarchical: Hierarchical::Stacked, 61 | mode: PathSimplifyMode::Spline, 62 | filter_speckle: 4, 63 | color_precision: 6, 64 | layer_difference: 16, 65 | corner_threshold: 60, 66 | length_threshold: 4.0, 67 | splice_threshold: 45, 68 | max_iterations: 10, 69 | path_precision: Some(2), 70 | } 71 | } 72 | } 73 | 74 | impl FromStr for ColorMode { 75 | type Err = String; 76 | 77 | fn from_str(s: &str) -> Result { 78 | match s { 79 | "color" => Ok(Self::Color), 80 | "binary" => Ok(Self::Binary), 81 | _ => Err(format!("unknown ColorMode {}", s)), 82 | } 83 | } 84 | } 85 | 86 | impl FromStr for Hierarchical { 87 | type Err = String; 88 | 89 | fn from_str(s: &str) -> Result { 90 | match s { 91 | "stacked" => Ok(Self::Stacked), 92 | "cutout" => Ok(Self::Cutout), 93 | _ => Err(format!("unknown Hierarchical {}", s)), 94 | } 95 | } 96 | } 97 | 98 | impl FromStr for Preset { 99 | type Err = String; 100 | 101 | fn from_str(s: &str) -> Result { 102 | match s { 103 | "bw" => Ok(Self::Bw), 104 | "poster" => Ok(Self::Poster), 105 | "photo" => Ok(Self::Photo), 106 | _ => Err(format!("unknown Preset {}", s)), 107 | } 108 | } 109 | } 110 | 111 | impl Config { 112 | pub fn from_preset(preset: Preset) -> Self { 113 | match preset { 114 | Preset::Bw => Self { 115 | color_mode: ColorMode::Binary, 116 | hierarchical: Hierarchical::Stacked, 117 | filter_speckle: 4, 118 | color_precision: 6, 119 | layer_difference: 16, 120 | mode: PathSimplifyMode::Spline, 121 | corner_threshold: 60, 122 | length_threshold: 4.0, 123 | max_iterations: 10, 124 | splice_threshold: 45, 125 | path_precision: Some(2), 126 | }, 127 | Preset::Poster => Self { 128 | color_mode: ColorMode::Color, 129 | hierarchical: Hierarchical::Stacked, 130 | filter_speckle: 4, 131 | color_precision: 8, 132 | layer_difference: 16, 133 | mode: PathSimplifyMode::Spline, 134 | corner_threshold: 60, 135 | length_threshold: 4.0, 136 | max_iterations: 10, 137 | splice_threshold: 45, 138 | path_precision: Some(2), 139 | }, 140 | Preset::Photo => Self { 141 | color_mode: ColorMode::Color, 142 | hierarchical: Hierarchical::Stacked, 143 | filter_speckle: 10, 144 | color_precision: 8, 145 | layer_difference: 48, 146 | mode: PathSimplifyMode::Spline, 147 | corner_threshold: 180, 148 | length_threshold: 4.0, 149 | max_iterations: 10, 150 | splice_threshold: 45, 151 | path_precision: Some(2), 152 | }, 153 | } 154 | } 155 | 156 | pub(crate) fn into_converter_config(self) -> ConverterConfig { 157 | ConverterConfig { 158 | color_mode: self.color_mode, 159 | hierarchical: self.hierarchical, 160 | filter_speckle_area: self.filter_speckle * self.filter_speckle, 161 | color_precision_loss: 8 - self.color_precision, 162 | layer_difference: self.layer_difference, 163 | mode: self.mode, 164 | corner_threshold: deg2rad(self.corner_threshold), 165 | length_threshold: self.length_threshold, 166 | max_iterations: self.max_iterations, 167 | splice_threshold: deg2rad(self.splice_threshold), 168 | path_precision: self.path_precision, 169 | } 170 | } 171 | } 172 | 173 | fn deg2rad(deg: i32) -> f64 { 174 | deg as f64 / 180.0 * std::f64::consts::PI 175 | } 176 | -------------------------------------------------------------------------------- /src/converter.rs: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/visioncortex/vtracer/blob/74f2a04a17d8c246d80c439fb162780160a7c3e9/cmdapp/src/converter.rs 2 | 3 | use crate::RawDataConfig; 4 | 5 | use super::config::{ColorMode, Config, ConverterConfig, Hierarchical}; 6 | use super::svg::SvgFile; 7 | use fastrand::Rng; 8 | use image; 9 | use visioncortex::color_clusters::{KeyingAction, Runner, RunnerConfig, HIERARCHICAL_MAX}; 10 | use visioncortex::{ 11 | approximate_circle_with_spline, Color, ColorImage, ColorName, CompoundPath, PathSimplifyMode, 12 | }; 13 | 14 | const NUM_UNUSED_COLOR_ITERATIONS: usize = 6; 15 | /// The fraction of pixels in the top/bottom rows of the image that need to be transparent before 16 | /// the entire image will be keyed. 17 | const KEYING_THRESHOLD: f32 = 0.2; 18 | 19 | const SMALL_CIRCLE: i32 = 12; 20 | 21 | /// Convert an in-memory image into an in-memory SVG 22 | pub fn convert(img: ColorImage, config: Config) -> Result { 23 | let config = config.into_converter_config(); 24 | match config.color_mode { 25 | ColorMode::Color => color_image_to_svg(img, config), 26 | ColorMode::Binary => binary_image_to_svg(img, config), 27 | } 28 | } 29 | 30 | /// Convert an image file into svg file 31 | pub fn convert_image_to_svg( 32 | input: &[u8], 33 | config: Config, 34 | raw: Option, 35 | ) -> Result { 36 | let img = read_image(input, raw)?; 37 | let svg = convert(img, config)?; 38 | let str = svg.to_string().map_err(|e| e.to_string())?; 39 | Ok(str) 40 | } 41 | 42 | fn color_exists_in_image(img: &ColorImage, color: Color) -> bool { 43 | for y in 0..img.height { 44 | for x in 0..img.width { 45 | let pixel_color = img.get_pixel(x, y); 46 | if pixel_color.r == color.r && pixel_color.g == color.g && pixel_color.b == color.b { 47 | return true; 48 | } 49 | } 50 | } 51 | false 52 | } 53 | 54 | fn find_unused_color_in_image(img: &ColorImage) -> Result { 55 | let special_colors = IntoIterator::into_iter([ 56 | Color::new(255, 0, 0), 57 | Color::new(0, 255, 0), 58 | Color::new(0, 0, 255), 59 | Color::new(255, 255, 0), 60 | Color::new(0, 255, 255), 61 | Color::new(255, 0, 255), 62 | ]); 63 | let mut rng = Rng::new(); 64 | let random_colors = 65 | (0..NUM_UNUSED_COLOR_ITERATIONS).map(|_| Color::new(rng.u8(..), rng.u8(..), rng.u8(..))); 66 | for color in special_colors.chain(random_colors) { 67 | if !color_exists_in_image(img, color) { 68 | return Ok(color); 69 | } 70 | } 71 | Err(String::from( 72 | "unable to find unused color in image to use as key", 73 | )) 74 | } 75 | 76 | fn should_key_image(img: &ColorImage) -> bool { 77 | if img.width == 0 || img.height == 0 { 78 | return false; 79 | } 80 | 81 | // Check for transparency at several scanlines 82 | let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize; 83 | let mut num_transparent_boundary_pixels = 0; 84 | let y_positions = [ 85 | 0, 86 | img.height / 4, 87 | img.height / 2, 88 | 3 * img.height / 4, 89 | img.height - 1, 90 | ]; 91 | for y in y_positions { 92 | for x in 0..img.width { 93 | if img.get_pixel(x, y).a == 0 { 94 | num_transparent_boundary_pixels += 1; 95 | } 96 | if num_transparent_boundary_pixels >= threshold { 97 | return true; 98 | } 99 | } 100 | } 101 | 102 | false 103 | } 104 | 105 | fn color_image_to_svg(mut img: ColorImage, config: ConverterConfig) -> Result { 106 | let width = img.width; 107 | let height = img.height; 108 | 109 | let key_color = if should_key_image(&img) { 110 | let key_color = find_unused_color_in_image(&img)?; 111 | for y in 0..height { 112 | for x in 0..width { 113 | if img.get_pixel(x, y).a == 0 { 114 | img.set_pixel(x, y, &key_color); 115 | } 116 | } 117 | } 118 | key_color 119 | } else { 120 | // The default color is all zeroes, which is treated by visioncortex as a special value meaning no keying will be applied. 121 | Color::default() 122 | }; 123 | 124 | let runner = Runner::new( 125 | RunnerConfig { 126 | diagonal: config.layer_difference == 0, 127 | hierarchical: HIERARCHICAL_MAX, 128 | batch_size: 25600, 129 | good_min_area: config.filter_speckle_area, 130 | good_max_area: (width * height), 131 | is_same_color_a: config.color_precision_loss, 132 | is_same_color_b: 1, 133 | deepen_diff: config.layer_difference, 134 | hollow_neighbours: 1, 135 | key_color, 136 | keying_action: if matches!(config.hierarchical, Hierarchical::Cutout) { 137 | KeyingAction::Keep 138 | } else { 139 | KeyingAction::Discard 140 | }, 141 | }, 142 | img, 143 | ); 144 | 145 | let mut clusters = runner.run(); 146 | 147 | match config.hierarchical { 148 | Hierarchical::Stacked => {} 149 | Hierarchical::Cutout => { 150 | let view = clusters.view(); 151 | let image = view.to_color_image(); 152 | let runner = Runner::new( 153 | RunnerConfig { 154 | diagonal: false, 155 | hierarchical: 64, 156 | batch_size: 25600, 157 | good_min_area: 0, 158 | good_max_area: (image.width * image.height) as usize, 159 | is_same_color_a: 0, 160 | is_same_color_b: 1, 161 | deepen_diff: 0, 162 | hollow_neighbours: 0, 163 | key_color, 164 | keying_action: KeyingAction::Discard, 165 | }, 166 | image, 167 | ); 168 | clusters = runner.run(); 169 | } 170 | } 171 | 172 | let view = clusters.view(); 173 | 174 | let mut svg = SvgFile::new(width, height, config.path_precision); 175 | for &cluster_index in view.clusters_output.iter().rev() { 176 | let cluster = view.get_cluster(cluster_index); 177 | let paths = if matches!(config.mode, PathSimplifyMode::Spline) 178 | && cluster.rect.width() < SMALL_CIRCLE 179 | && cluster.rect.height() < SMALL_CIRCLE 180 | && cluster.to_shape(&view).is_circle() 181 | { 182 | let mut paths = CompoundPath::new(); 183 | paths.add_spline(approximate_circle_with_spline( 184 | cluster.rect.left_top(), 185 | cluster.rect.width(), 186 | )); 187 | paths 188 | } else { 189 | cluster.to_compound_path( 190 | &view, 191 | false, 192 | config.mode, 193 | config.corner_threshold, 194 | config.length_threshold, 195 | config.max_iterations, 196 | config.splice_threshold, 197 | ) 198 | }; 199 | svg.add_path(paths, cluster.residue_color()); 200 | } 201 | 202 | Ok(svg) 203 | } 204 | 205 | fn binary_image_to_svg(img: ColorImage, config: ConverterConfig) -> Result { 206 | let img = img.to_binary_image(|x| x.r < 128); 207 | let width = img.width; 208 | let height = img.height; 209 | 210 | let clusters = img.to_clusters(false); 211 | 212 | let mut svg = SvgFile::new(width, height, config.path_precision); 213 | for i in 0..clusters.len() { 214 | let cluster = clusters.get_cluster(i); 215 | if cluster.size() >= config.filter_speckle_area { 216 | let paths = cluster.to_compound_path( 217 | config.mode, 218 | config.corner_threshold, 219 | config.length_threshold, 220 | config.max_iterations, 221 | config.splice_threshold, 222 | ); 223 | svg.add_path(paths, Color::color(&ColorName::Black)); 224 | } 225 | } 226 | 227 | Ok(svg) 228 | } 229 | 230 | fn read_image(input: &[u8], raw: Option) -> Result { 231 | match raw { 232 | Some(raw) => { 233 | let img = ColorImage { 234 | pixels: input.to_vec(), 235 | width: raw.width as usize, 236 | height: raw.height as usize, 237 | }; 238 | Ok(img) 239 | } 240 | None => { 241 | let img = image::load_from_memory(input); 242 | let img = match img { 243 | Ok(file) => file.to_rgba8(), 244 | Err(_) => return Err(String::from("unable to read this image")), 245 | }; 246 | 247 | let (width, height) = (img.width() as usize, img.height() as usize); 248 | let img = ColorImage { 249 | pixels: img.as_raw().to_vec(), 250 | width, 251 | height, 252 | }; 253 | Ok(img) 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | use config::{ColorMode, Config, Hierarchical, Preset}; 4 | use converter::convert_image_to_svg; 5 | use napi::{ 6 | bindgen_prelude::{AsyncTask, Buffer}, 7 | Either, Result, Task, 8 | }; 9 | use std::panic; 10 | use visioncortex::PathSimplifyMode; 11 | 12 | #[macro_use] 13 | extern crate napi_derive; 14 | 15 | pub mod config; 16 | pub mod converter; 17 | pub mod svg; 18 | 19 | #[napi(js_name = "PathSimplifyMode")] 20 | pub enum JsPathSimplifyMode { 21 | None, 22 | Polygon, 23 | Spline, 24 | } 25 | 26 | #[derive(Clone)] 27 | #[napi(object, js_name = "Config")] 28 | pub struct JsConfig { 29 | /// True color image or binary image (black and white) 30 | pub color_mode: ColorMode, 31 | /// Hierarchial clustering or non-stacked. Only applicable to color images. 32 | pub hierarchical: Hierarchical, 33 | /// Discard patches smaller than X pixels in size (cleaner) 34 | pub filter_speckle: i32, 35 | /// The number of significant bits to use in an RGB channel (more accurate) 36 | pub color_precision: i32, 37 | /// The color difference between gradient layers (less layers) 38 | pub layer_difference: i32, 39 | /// Curve fitting mode 40 | pub mode: JsPathSimplifyMode, 41 | /// Minimum momentary angle (degree) to be considered a corner (smoother) 42 | pub corner_threshold: i32, 43 | /// Perform iterative subdivide smooth until all segments are shorter than this length 44 | pub length_threshold: f64, 45 | /// The maximum number of iterations to perform 46 | pub max_iterations: i32, 47 | /// Minimum angle displacement (degree) to splice a spline (less accurate) 48 | pub splice_threshold: i32, 49 | /// Number of decimal places to use in path string 50 | pub path_precision: Option, 51 | } 52 | 53 | #[derive(Clone)] 54 | #[napi(object)] 55 | pub struct RawDataConfig { 56 | pub width: i32, 57 | pub height: i32, 58 | } 59 | 60 | pub struct VectorizeTask { 61 | data: Buffer, 62 | config: Option>, 63 | args: Option, 64 | } 65 | 66 | #[napi] 67 | impl Task for VectorizeTask { 68 | type Output = String; 69 | type JsValue = String; 70 | 71 | fn compute(&mut self) -> Result { 72 | let res = vectorize_inner(self.data.as_ref(), self.config.clone(), self.args.clone()); 73 | res 74 | } 75 | 76 | fn resolve(&mut self, _env: napi::Env, output: Self::Output) -> Result { 77 | Ok(output) 78 | } 79 | } 80 | 81 | #[napi(catch_unwind)] 82 | pub fn vectorize( 83 | source: Buffer, 84 | config: Option>, 85 | ) -> AsyncTask { 86 | AsyncTask::new(VectorizeTask { 87 | data: source, 88 | config, 89 | args: None, 90 | }) 91 | } 92 | 93 | #[napi(catch_unwind)] 94 | pub fn vectorize_raw( 95 | source: Buffer, 96 | args: RawDataConfig, 97 | config: Option>, 98 | ) -> AsyncTask { 99 | AsyncTask::new(VectorizeTask { 100 | data: source, 101 | config, 102 | args: Some(args), 103 | }) 104 | } 105 | 106 | #[napi(catch_unwind)] 107 | pub fn vectorize_sync(source: Buffer, config: Option>) -> Result { 108 | vectorize_inner(source.as_ref(), config, None) 109 | } 110 | 111 | #[napi(catch_unwind)] 112 | pub fn vectorize_raw_sync( 113 | source: Buffer, 114 | args: RawDataConfig, 115 | config: Option>, 116 | ) -> Result { 117 | vectorize_inner(source.as_ref(), config, Some(args)) 118 | } 119 | 120 | fn vectorize_inner( 121 | source: &[u8], 122 | config: Option>, 123 | raw_args: Option, 124 | ) -> Result { 125 | panic::set_hook(Box::new(|_info| {})); 126 | 127 | let result = 128 | panic::catch_unwind(|| convert_image_to_svg(source, resolve_config(config), raw_args)); 129 | 130 | let result = match result { 131 | Ok(res) => res, 132 | Err(_) => Err(napi::Error::new( 133 | napi::Status::GenericFailure, 134 | "Unknown error occurred", 135 | ))?, 136 | }; 137 | 138 | let svg = result.map_err(|e| { 139 | napi::Error::new( 140 | napi::Status::GenericFailure, 141 | format!("Error: {:?}", e).as_str(), 142 | ) 143 | })?; 144 | 145 | Ok(svg) 146 | } 147 | 148 | fn resolve_config(config: Option>) -> Config { 149 | match config { 150 | Some(Either::A(config)) => Config { 151 | color_mode: match config.color_mode { 152 | ColorMode::Color => ColorMode::Color, 153 | ColorMode::Binary => ColorMode::Binary, 154 | }, 155 | hierarchical: match config.hierarchical { 156 | Hierarchical::Stacked => Hierarchical::Stacked, 157 | Hierarchical::Cutout => Hierarchical::Cutout, 158 | }, 159 | filter_speckle: config.filter_speckle as usize, 160 | color_precision: config.color_precision, 161 | layer_difference: config.layer_difference, 162 | mode: match config.mode { 163 | JsPathSimplifyMode::None => PathSimplifyMode::None, 164 | JsPathSimplifyMode::Polygon => PathSimplifyMode::Polygon, 165 | JsPathSimplifyMode::Spline => PathSimplifyMode::Spline, 166 | }, 167 | corner_threshold: config.corner_threshold, 168 | length_threshold: config.length_threshold, 169 | max_iterations: config.max_iterations as usize, 170 | splice_threshold: config.splice_threshold, 171 | path_precision: config.path_precision, 172 | }, 173 | Some(Either::B(preset)) => Config::from_preset(preset), 174 | None => Config::default(), 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/svg.rs: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/visioncortex/vtracer/blob/74f2a04a17d8c246d80c439fb162780160a7c3e9/cmdapp/src/svg.rs 2 | 3 | use std::fmt; 4 | use visioncortex::{Color, CompoundPath, PointF64}; 5 | 6 | pub struct SvgFile { 7 | pub paths: Vec, 8 | pub width: usize, 9 | pub height: usize, 10 | pub path_precision: Option, 11 | } 12 | 13 | pub struct SvgPath { 14 | pub path: CompoundPath, 15 | pub color: Color, 16 | } 17 | 18 | impl SvgFile { 19 | pub fn new(width: usize, height: usize, path_precision: Option) -> Self { 20 | SvgFile { 21 | paths: vec![], 22 | width, 23 | height, 24 | path_precision, 25 | } 26 | } 27 | 28 | pub fn add_path(&mut self, path: CompoundPath, color: Color) { 29 | self.paths.push(SvgPath { path, color }) 30 | } 31 | 32 | pub fn to_string(&self) -> Result { 33 | Ok(format!("{}", self)) 34 | } 35 | } 36 | 37 | impl fmt::Display for SvgFile { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 39 | writeln!(f, r#""#)?; 40 | writeln!( 41 | f, 42 | r#""#, 43 | )?; 44 | writeln!( 45 | f, 46 | r#""#, 47 | self.width, self.height 48 | )?; 49 | 50 | for path in &self.paths { 51 | path.fmt_with_precision(f, self.path_precision)?; 52 | } 53 | 54 | writeln!(f, "") 55 | } 56 | } 57 | 58 | impl fmt::Display for SvgPath { 59 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 60 | self.fmt_with_precision(f, None) 61 | } 62 | } 63 | 64 | impl SvgPath { 65 | fn fmt_with_precision(&self, f: &mut fmt::Formatter, precision: Option) -> fmt::Result { 66 | let (string, offset) = self 67 | .path 68 | .to_svg_string(true, PointF64::default(), precision); 69 | writeln!( 70 | f, 71 | "", 72 | string, 73 | self.color.to_hex_string(), 74 | offset.x, 75 | offset.y 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "strict": true, 5 | "moduleResolution": "node", 6 | "module": "CommonJS", 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "include": ["."], 13 | "exclude": ["node_modules", "bench", "__test__"] 14 | } 15 | --------------------------------------------------------------------------------