├── .changeset └── config.json ├── .commitlintrc.yml ├── .github ├── actions │ ├── build │ │ └── action.yml │ ├── download-pkg │ │ └── action.yml │ ├── setup │ │ └── action.yml │ └── upload-pkg │ │ └── action.yml └── workflows │ ├── checks.yml │ ├── deploy-site.yml │ └── release.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── biome.json ├── bundle.js ├── docs ├── 01-svg-support.md ├── 02-getting-started.md ├── 03-basic-usage.md ├── 04-font-settings.md └── 05-advanced-usage.md ├── lib ├── index.ts └── types.ts ├── logo.svg ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── site ├── .gitignore ├── .npmrc ├── README.md ├── package.json ├── src │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── custom.d.ts │ ├── lib │ │ ├── components │ │ │ ├── Demos.svelte │ │ │ ├── Header.svelte │ │ │ ├── HighlightCodeSnippet.svelte │ │ │ ├── InstallCommand.svelte │ │ │ ├── Markdown.svelte │ │ │ ├── Playground.svelte │ │ │ ├── SelectableCodeSnippet.svelte │ │ │ └── markdown-plugins │ │ │ │ ├── code │ │ │ │ ├── Blockquote.svelte │ │ │ │ ├── BlockquoteAdapter.svelte │ │ │ │ ├── Code.svelte │ │ │ │ ├── CodeAdapter.svelte │ │ │ │ ├── Pre.svelte │ │ │ │ ├── PreAdapter.svelte │ │ │ │ └── index.ts │ │ │ │ ├── highlight │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ ├── stores │ │ │ ├── index.ts │ │ │ ├── packageManager.ts │ │ │ └── svg2png.ts │ │ └── svg2png │ │ │ ├── index.ts │ │ │ └── worker.ts │ └── routes │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ ├── +page.svelte │ │ ├── docs │ │ ├── +page.server.ts │ │ └── +page.svelte │ │ ├── favicon.png │ │ └── +server.ts │ │ ├── icon.svg │ │ └── +server.ts │ │ ├── ogp.png │ │ └── +server.ts │ │ └── svg2png.wasm │ │ └── +server.ts ├── static │ ├── Roboto-Regular.ttf │ ├── gradient.svg │ ├── ogp.svg │ └── parrot.svg ├── svelte.config.js ├── tsconfig.json └── vite.config.ts ├── src ├── converter.rs └── lib.rs ├── test ├── README.md ├── data │ ├── NotoSerif-Regular.ttf │ ├── Roboto-Regular.ttf │ ├── edge-circle.svg │ ├── fe-turblence.svg │ ├── text-default-sans-serif.svg │ ├── text-default-serif.svg │ ├── text-not-spec-font.svg │ └── text.svg ├── node │ └── index.test.ts └── vrt │ ├── .gitignore │ ├── expected │ ├── edge-circle.1x.png │ ├── edge-circle.2x.png │ ├── edge-circle.30h.png │ ├── edge-circle.50w.png │ ├── edge-circle.50w30h.png │ ├── edge-circle.blue.png │ ├── edge-circle.green.png │ ├── edge-circle.red.png │ ├── fe-turblence.1x.png │ ├── fe-turblence.2x.png │ ├── fe-turblence.30h.png │ ├── fe-turblence.50w.png │ ├── fe-turblence.50w30h.png │ ├── fe-turblence.blue.png │ ├── fe-turblence.green.png │ ├── fe-turblence.red.png │ ├── text-default-sans-serif.1x.png │ ├── text-default-sans-serif.2x.png │ ├── text-default-sans-serif.30h.png │ ├── text-default-sans-serif.50w.png │ ├── text-default-sans-serif.50w30h.png │ ├── text-default-sans-serif.blue.png │ ├── text-default-sans-serif.green.png │ ├── text-default-sans-serif.red.png │ ├── text-default-serif.1x.png │ ├── text-default-serif.2x.png │ ├── text-default-serif.30h.png │ ├── text-default-serif.50w.png │ ├── text-default-serif.50w30h.png │ ├── text-default-serif.blue.png │ ├── text-default-serif.green.png │ ├── text-default-serif.red.png │ ├── text-not-spec-font.1x.png │ ├── text-not-spec-font.2x.png │ ├── text-not-spec-font.30h.png │ ├── text-not-spec-font.50w.png │ ├── text-not-spec-font.50w30h.png │ ├── text-not-spec-font.blue.png │ ├── text-not-spec-font.green.png │ ├── text-not-spec-font.red.png │ ├── text.1x.png │ ├── text.2x.png │ ├── text.30h.png │ ├── text.50w.png │ ├── text.50w30h.png │ ├── text.blue.png │ ├── text.green.png │ └── text.red.png │ └── index.mjs ├── tsconfig.json └── vitest.config.ts /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "ssssota/svg2png-wasm" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: ['@commitlint/config-conventional'] 2 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: Build package 2 | description: Build svg2png-wasm 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - uses: actions/cache@v3 7 | with: 8 | path: | 9 | ~/.cargo/bin/ 10 | ~/.cargo/registry/index/ 11 | ~/.cargo/registry/cache/ 12 | ~/.cargo/git/db/ 13 | target/ 14 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 15 | - name: WASM target 16 | run: rustup target add wasm32-unknown-unknown 17 | shell: bash 18 | - name: Build package 19 | run: pnpm build 20 | shell: bash 21 | -------------------------------------------------------------------------------- /.github/actions/download-pkg/action.yml: -------------------------------------------------------------------------------- 1 | name: Download svg2png-wasm 2 | description: Download svg2png-wasm files from artifact 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - uses: actions/download-artifact@v4 7 | with: 8 | name: build-artifact 9 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup and Install deps 2 | description: Setup Node.js/pnpm. Install dependencies. 3 | inputs: 4 | node-version: 5 | default: lts/* 6 | runs: 7 | using: 'composite' 8 | steps: 9 | - uses: pnpm/action-setup@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | cache: pnpm 13 | - name: Install deps 14 | run: pnpm install 15 | shell: bash 16 | -------------------------------------------------------------------------------- /.github/actions/upload-pkg/action.yml: -------------------------------------------------------------------------------- 1 | name: Upload svg2png-wasm 2 | description: Upload svg2png-wasm files as artifact 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - uses: actions/upload-artifact@v4 7 | with: 8 | name: build-artifact 9 | path: | 10 | dist 11 | dist-wasm 12 | svg2png_wasm_bg.wasm 13 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | setup: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 15 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: ./.github/actions/setup 16 | - uses: ./.github/actions/build 17 | - uses: ./.github/actions/upload-pkg 18 | - run: pnpm lint 19 | 20 | test: 21 | runs-on: ${{ matrix.os }} 22 | timeout-minutes: 15 23 | needs: [setup] 24 | strategy: 25 | matrix: 26 | os: [windows-latest, ubuntu-latest, macos-latest] 27 | node-version: [20, 22] 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: ./.github/actions/setup 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - uses: ./.github/actions/download-pkg 35 | - run: pnpm test 36 | 37 | other-check: 38 | runs-on: ubuntu-latest 39 | timeout-minutes: 15 40 | needs: [setup] 41 | env: 42 | WORKING_DIR_SITE: site 43 | steps: 44 | - uses: actions/checkout@v4 45 | - uses: ./.github/actions/setup 46 | - uses: ./.github/actions/download-pkg 47 | 48 | # site checks 49 | - run: pnpm build 50 | working-directory: ${{ env.WORKING_DIR_SITE }} 51 | -------------------------------------------------------------------------------- /.github/workflows/deploy-site.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Demo site 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | site-deploy: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 15 13 | defaults: 14 | run: 15 | working-directory: site 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: ./.github/actions/setup 19 | - uses: ./.github/actions/build 20 | 21 | - run: pnpm build 22 | - name: Deploy to GitHub Pages 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: site/build 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | concurrency: ${{ github.workflow }}-${{ github.ref }} 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/setup 14 | - uses: ./.github/actions/build 15 | - name: Create Release Pull Request or Publish to npm 16 | id: changesets 17 | uses: changesets/action@v1 18 | with: 19 | publish: pnpm release 20 | env: 21 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /dist* 3 | node_modules 4 | test-results 5 | Cargo.lock 6 | reg.json 7 | svg2png_wasm_bg.wasm 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "matklad.rust-analyzer", 5 | "svelte.svelte-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "biomejs.biome", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.biome": "explicit", 6 | "source.organizeImports.biome": "explicit" 7 | }, 8 | "[rust]": { 9 | "editor.defaultFormatter": "rust-lang.rust-analyzer" 10 | }, 11 | "[svelte]": { 12 | "editor.defaultFormatter": "svelte.svelte-vscode" 13 | }, 14 | "typescript.tsdk": "node_modules\\typescript\\lib" 15 | } 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # svg2png-wasm 2 | 3 | ## 1.4.1 4 | 5 | ### Patch Changes 6 | 7 | - [#125](https://github.com/ssssota/svg2png-wasm/pull/125) [`571a1b7`](https://github.com/ssssota/svg2png-wasm/commit/571a1b76ebb41511b23aa68219c1bcf37bf36f04) Thanks [@ssssota](https://github.com/ssssota)! - Update resvg 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "resvg wrapper for svg2png wasm module" 3 | edition = "2018" 4 | license = "MIT" 5 | name = "svg2png-wasm" 6 | version = "0.1.0" 7 | repository = "https://github.com/ssssota/svg2png-wasm" 8 | 9 | [lib] 10 | crate-type = ["cdylib", "rlib"] 11 | 12 | [features] 13 | default = ["console_error_panic_hook"] 14 | 15 | [dependencies] 16 | resvg = { version = "0.36.0", default-features = false, features = ["text", "raster-images"] } 17 | svgtypes = "0.12.0" 18 | wasm-bindgen = "0.2.87" 19 | 20 | console_error_panic_hook = {version = "0.1.7", optional = true} 21 | wee_alloc = {version = "0.4.5", optional = true} 22 | 23 | [dev-dependencies] 24 | wasm-bindgen-test = "0.3.37" 25 | 26 | # ref. https://github.com/yisibl/resvg-js 27 | [profile.release] 28 | lto = true # Enable Link Time Optimization 29 | opt-level = 3 30 | # Setting this to 1 may improve the performance of generated code, but may be slower to compile. 31 | # https://doc.rust-lang.org/rustc/codegen-options/index.html#codegen-units 32 | codegen-units = 1 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sotaro Tommykawa 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | svg2png-wasm 4 | 5 | [![](https://img.shields.io/npm/v/svg2png-wasm)](https://npmjs.com/svg2png-wasm) 6 | [![](https://img.shields.io/npm/l/svg2png-wasm)](https://npmjs.com/svg2png-wasm) 7 | 8 | **[Demo site](https://ssssota.github.io/svg2png-wasm/)** 9 | 10 | SVG to PNG converter JS library made with WASM + [resvg](https://crates.io/crates/resvg). 11 | 12 | See [resvg](https://github.com/RazrFalcon/resvg#svg-support) for SVG support status. 13 | 14 | ## 💻 Usage 15 | 16 | ### Installation 17 | 18 | #### Node.js / Browser 19 | 20 | ```sh 21 | npm install svg2png-wasm 22 | # yarn add svg2png-wasm 23 | # pnpm add svg2png-wasm 24 | ``` 25 | 26 | Or, using a script tag in the browser and load from unpkg. 27 | 28 | ```html 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | #### Deno 36 | 37 | ```ts 38 | // from esm.sh 39 | export * from 'https://esm.sh/svg2png-wasm@0.6.1'; 40 | // from skypack.dev 41 | export * from 'https://cdn.skypack.dev/svg2png-wasm@0.6.1?dts'; 42 | ``` 43 | 44 | ### Examples 45 | 46 | - Use in Web Worker [Demo site](https://ssssota.github.io/svg2png-wasm/) ([source](./site/)) 47 | - Use with Cloudflare Workers [ssssota/svg2png-worker](https://github.com/ssssota/svg2png-worker) 48 | - Use with Deno Deploy [ssssota/svg2png-deno-deploy](https://github.com/ssssota/svg2png-deno-deploy) 49 | - Or [test directory](./test/) 50 | 51 | #### Node.js 52 | 53 | ```js 54 | import { svg2png, initialize } from 'svg2png-wasm'; 55 | // const { svg2png, initialize } = require('svg2png-wasm'); 56 | import { readFileSync, writeFileSync } from 'fs'; 57 | 58 | await initialize( 59 | readFileSync('./node_modules/svg2png-wasm/svg2png_wasm_bg.wasm'), 60 | ); 61 | 62 | /** @type {Uint8Array} */ 63 | const png = await svg2png( 64 | ' ... ', 65 | { 66 | scale: 2, // optional 67 | width: 400, // optional 68 | height: 400, // optional 69 | backgroundColor: 'white', // optional 70 | fonts: [ 71 | // optional 72 | readFileSync('./Roboto.ttf'), // require, If you use text in svg 73 | ], 74 | defaultFontFamily: { 75 | // optional 76 | sansSerif: 'Roboto', 77 | }, 78 | }, 79 | ); 80 | writeFileSync('./output.png', png); 81 | ``` 82 | 83 | #### Browser 84 | 85 | ```js 86 | import { createSvg2png, initialize } from 'svg2png-wasm'; 87 | 88 | // put wasm to your assets directory 89 | await initialize(fetch('/assets/svg2png_wasm_bg.wasm')); 90 | const svgs = [ 91 | ' ... ', 92 | ' ... ', 93 | ' ... ', 94 | // and more ... 95 | ]; 96 | const font = await fetch('./Roboto.ttf').then((res) => res.arrayBuffer()); 97 | const svg2png = createSvg2png({ 98 | fonts: [new Uint8Array(font)], // require, If you use text in svg 99 | }); 100 | /** @type {Uint8Array[]} */ 101 | const pngs = await Promise.all(svgs.map((svg) => svg2png(svg, { scale: 2 }))); 102 | svg2png.dispose(); // You should dispose svg2png, if you will not use it in the future 103 | ``` 104 | 105 | Or, using a script tag in the browser and load from esm.sh. 106 | 107 | ```html 108 | 109 | 124 | ``` 125 | 126 | ### API 127 | 128 | The library has two main APIs (`svg2png` and `createSvg2png`). 129 | 130 | Basically, you can use `svg2png`, but if you want to process a lot of data continuously, consider using `createSvg2png`. 131 | It can reduce the overhead of font loading. 132 | Converters generated by `createSvg2png` should be disposed of after use by calling the `dispose` method. 133 | 134 | ```ts 135 | export type InitInput = 136 | | RequestInfo 137 | | URL 138 | | Response 139 | | BufferSource 140 | | WebAssembly.Module; 141 | export type DefaultFontFamily = { 142 | serifFamily?: string; 143 | sansSerifFamily?: string; 144 | cursiveFamily?: string; 145 | fantasyFamily?: string; 146 | monospaceFamily?: string; 147 | }; 148 | export type ConverterOptions = { 149 | fonts?: Uint8Array[]; 150 | defaultFontFamily?: DefaultFontFamily; 151 | }; 152 | export type ConvertOptions = { 153 | scale?: number; 154 | width?: number; 155 | height?: number; 156 | backgroundColor?: string; 157 | }; 158 | export type Svg2png = (( 159 | svg: string, 160 | options?: ConvertOptions, 161 | ) => Promise) & { 162 | dispose: () => void; 163 | }; 164 | /** 165 | * Initialize WASM module 166 | * @param mod WebAssembly Module or WASM url 167 | */ 168 | export const initialize: (mod: Promise | InitInput) => Promise; 169 | /** 170 | * @param opts Converter options (e.g. font settings) 171 | * @returns svg2png converter 172 | */ 173 | export const createSvg2png: (opts?: ConverterOptions | undefined) => Svg2png; 174 | export const svg2png: ( 175 | svg: string, 176 | opts?: (ConverterOptions & ConvertOptions) | undefined, 177 | ) => Promise; 178 | ``` 179 | 180 | ## 📄 LICENSE 181 | 182 | MIT 183 | 184 | This library uses [resvg](https://github.com/RazrFalcon/resvg), which is licensed unser MPL-2.0. 185 | The source code for resvg can be found [here](https://github.com/RazrFalcon/resvg). 186 | 187 | ## 🙋‍♂️ Contributing 188 | 189 | WELCOME! 190 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": ["dist", ".svelte-kit", "*.svelte"] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { buildSync } from "esbuild"; 3 | 4 | /** @type {import('esbuild').BuildOptions} */ 5 | const commonOptions = { 6 | bundle: true, 7 | logLevel: "error", 8 | entryPoints: ["lib/index.ts"], 9 | define: { "import.meta.url": "undefined" }, 10 | }; 11 | 12 | buildSync({ 13 | ...commonOptions, 14 | format: "cjs", 15 | outfile: "dist/index.cjs", 16 | }); 17 | buildSync({ 18 | ...commonOptions, 19 | format: "esm", 20 | outfile: "dist/index.mjs", 21 | }); 22 | buildSync({ 23 | ...commonOptions, 24 | format: "iife", 25 | minify: true, 26 | globalName: "svg2pngWasm", 27 | outfile: "dist/index.min.js", 28 | }); 29 | -------------------------------------------------------------------------------- /docs/01-svg-support.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SVG Support 3 | --- 4 | 5 | svg2png-wasm uses [resvg](https://crates.io/crates/resvg) for SVG conversion. 6 | 7 | So, please check [resvg document](https://crates.io/crates/resvg#svg-support) for SVG support status. 8 | -------------------------------------------------------------------------------- /docs/02-getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | --- 4 | 5 | You can use this library in Node.js, Deno and Browser. 6 | 7 | ### Node.js 8 | 9 | ``` 10 | npm install svg2png-wasm 11 | # yarn add svg2png-wasm 12 | # pnpm add svg2png-wasm 13 | ``` 14 | 15 | ### Deno 16 | 17 | ```js 18 | // Skypack 19 | export * from 'https://cdn.skypack.dev/svg2png-wasm?dts'; 20 | // or 21 | // esm.sh 22 | export * from 'https://esm.sh/svg2png-wasm'; 23 | ``` 24 | 25 | ### Browser 26 | 27 | You can use npm (like Node.js) to bundle it with bundler (Webpack, Vite, etc.). 28 | 29 | It can also be used directly as follows. 30 | 31 | ```html 32 | 33 | ``` 34 | 35 | ```js 36 | const { initialize, svg2png, createSvg2png } = svg2pngWasm; 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/03-basic-usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Usage 3 | --- 4 | 5 | ### 1. Prepare wasm 6 | 7 | You will need to prepare a wasm module. 8 | 9 | e.g. 10 | 11 | ```js 12 | // Node.js 13 | import { readFileSync } from 'fs'; 14 | const wasm = readFileSync('./node_modules/svg2png-wasm/svg2png_wasm_bg.wasm'); 15 | ``` 16 | 17 | ```js 18 | // Deno (For example, fetch from unpkg CDN) 19 | const wasm = await fetch( 20 | 'https://unpkg.com/svg2png-wasm/svg2png_wasm_bg.wasm', 21 | ).then((res) => res.arrayBuffer()); 22 | ``` 23 | 24 | ```js 25 | // browser (For example, we have a wasm file in the assets directory) 26 | const wasm = await fetch('/assets/svg2png_wasm_bg.wasm').then((res) => 27 | res.arrayBuffer(), 28 | ); 29 | ``` 30 | 31 | ### 2. Initialize wasm 32 | 33 | ```js 34 | import { initialize } from 'svg2png-wasm'; 35 | await initialize(wasm); 36 | ``` 37 | 38 | ### 3. Convert SVG 39 | 40 | ```js 41 | import { svg2png } from 'svg2png-wasm'; 42 | const png = await svg2png(svg); 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/04-font-settings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Font Settings 3 | --- 4 | 5 | > If you want to use text in SVG, you need to check this section. 6 | 7 | WebAssembly is run in a sandbox for safety reasons. Therefore, WebAssembly does not have access to system information, for example, it cannot use the system fonts. 8 | 9 | For this reason, you must pass the fonts when you want to use text in SVG. 10 | 11 | The first entry in the `fonts` field array will be the fallback font. 12 | 13 | The `defaultFontFamily` field is used when the font is specified as `sans-serif`, `serif` etc. 14 | 15 | ### Example 16 | 17 | ```js 18 | import { svg2png, createSvg2png } from 'svg2png-wasm'; 19 | 20 | // Prepare font data 21 | const robotoFont = await fetch('/assets/Roboto.ttf').then((res) => 22 | res.arrayBuffer(), 23 | ); 24 | 25 | // convert directly 26 | const png = await svg2png(svg, { 27 | fonts: [new Uint8Array(robotoFont)], 28 | defaultFontFamily: { sansSerifFamily: 'Roboto' }, 29 | scale: 2, 30 | }); 31 | 32 | // OR 33 | // create font loaded svg2png 34 | const fontLoadedSvg2png = createSvg2png({ 35 | fonts: [new Uint8Array(robotoFont)], 36 | defaultFontFamily: { sansSerifFamily: 'Roboto' }, 37 | }); 38 | // convert 39 | const png = await fontLoadedSvg2png(svg, { scale: 2 }); 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/05-advanced-usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Advanced usage 3 | --- 4 | 5 | ## Initialize only once 6 | 7 | The `initialize` function can be called only once. 8 | 9 | If it is called more than once, `Promise` will be rejected. 10 | 11 | ## Initialize without buffer 12 | 13 | The argument of the `initialize` function can be something other than a buffer. 14 | 15 | ### Types 16 | 17 | ```js 18 | export type InitInput = 19 | | RequestInfo 20 | | URL 21 | | Response 22 | | BufferSource 23 | | WebAssembly.Module; 24 | export const initialize: (mod: Promise | InitInput) => Promise; 25 | ``` 26 | 27 | ### Examples 28 | 29 | ```js 30 | // full code (WebAsembly.Module) 31 | const response = await fetch( 32 | 'https://unpkg.com/svg2png-wasm/svg2png_wasm_bg.wasm', 33 | ); 34 | const buffer = await response.arrayBuffer(); 35 | const { module } = await WebAssembly.instantiate(buffer); 36 | await initialize(module); 37 | 38 | // BufferSource 39 | const response = await fetch( 40 | 'https://unpkg.com/svg2png-wasm/svg2png_wasm_bg.wasm', 41 | ); 42 | const buffer = await response.arrayBuffer(); 43 | await initialize(buffer); 44 | 45 | // Response 46 | const response = await fetch( 47 | 'https://unpkg.com/svg2png-wasm/svg2png_wasm_bg.wasm', 48 | ); 49 | await initialize(response); 50 | 51 | // Promise 52 | await initialize(fetch('https://unpkg.com/svg2png-wasm/svg2png_wasm_bg.wasm')); 53 | 54 | // in Deno (Promise) 55 | await initialize(Deno.readFile('./svg2png_wasm_bg.wasm')); 56 | 57 | // in Deno and Browser (RequestInfo (=string/URL)) 58 | await initialize('https://unpkg.com/svg2png-wasm/svg2png_wasm_bg.wasm'); 59 | ``` 60 | 61 | ## Convert options 62 | 63 | The output image can be adjusted by specifying an option as the second argument of the function `svg2png`. 64 | 65 | ### Size option 66 | 67 | You can specify output image size. 68 | 69 | Specifying the width and height will not stretch the image. 70 | 71 | ```js 72 | // 2x scale 73 | await svg2png(svgData, { scale: 2 }); 74 | 75 | // Fit to 500px width 76 | await svg2png(svgData, { width: 500 }); 77 | 78 | // Fit to 128px height 79 | await svg2png(svgData, { height: 128 }); 80 | 81 | // Fit to 300px width (width and height have priority) 82 | await svg2png(svgData, { width: 300, scale: 10 }); 83 | ``` 84 | 85 | ### Background color option 86 | 87 | You can specify background color of the output image when the SVG is transparent. 88 | 89 | ```js 90 | // Compatible with CSS Color Module 3 91 | // with color keyword 92 | await svg2png(svgData, { backgroundColor: 'lightskyblue' }); 93 | 94 | // with hex values 95 | await svg2png(svgData, { backgroundColor: '#3cf' }); 96 | 97 | // with hsla 98 | await svg2png(svgData, { backgroundColor: 'hsla(240, 100%, 50%, 0.5)' }); 99 | ``` 100 | 101 | ## Custom svg2png 102 | 103 | You can create a custom svg2png function. 104 | 105 | Basically, you can use `svg2png` , but if you want to process a lot of data continuously, consider using `createSvg2png` . It can reduce the overhead of font loading. Converters generated by `createSvg2png` should be disposed of after use by calling the dispose method. 106 | 107 | ```js 108 | const svgs = [ 109 | ' ... ', 110 | ' ... ', 111 | ' ... ', 112 | // and more ... 113 | ]; 114 | const font = await fetch('./Roboto.ttf').then((res) => res.arrayBuffer()); 115 | const svg2png = createSvg2png({ 116 | fonts: [new Uint8Array(font)], 117 | }); 118 | 119 | const pngs = await Promise.all(svgs.map((svg) => svg2png(svg, { scale: 2 }))); 120 | svg2png.dispose(); // You should dispose svg2png, if you will not use it in the future 121 | ``` 122 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import init, { 2 | type Converter, 3 | createConverter, 4 | type InitInput, 5 | } from "../dist-wasm/svg2png_wasm"; 6 | import { ConvertOptions, ConverterOptions, Svg2png } from "./types"; 7 | 8 | let initialized = false; 9 | 10 | /** 11 | * Initialize WASM module 12 | * @param mod WebAssembly Module or WASM url 13 | */ 14 | export const initialize = async ( 15 | mod: Promise | InitInput, 16 | ): Promise => { 17 | if (initialized) { 18 | throw new Error( 19 | "Already initialized. The `initialize` function can be used only once.", 20 | ); 21 | } 22 | await init(await mod); 23 | initialized = true; 24 | }; 25 | 26 | /** 27 | * @param opts Converter options (e.g. font settings) 28 | * @returns svg2png converter 29 | */ 30 | export const createSvg2png = (opts?: ConverterOptions): Svg2png => { 31 | if (!initialized) 32 | throw new Error( 33 | "WASM has not been initialized. Call `initialize` function.", 34 | ); 35 | let converter: Converter | undefined; 36 | converter = createConverter( 37 | opts?.defaultFontFamily?.serifFamily, 38 | opts?.defaultFontFamily?.sansSerifFamily, 39 | opts?.defaultFontFamily?.cursiveFamily, 40 | opts?.defaultFontFamily?.fantasyFamily, 41 | opts?.defaultFontFamily?.monospaceFamily, 42 | ); 43 | for (const font of opts?.fonts ?? []) { 44 | converter.registerFont(font); 45 | } 46 | 47 | const svg2png = (svg: string, options?: ConvertOptions) => 48 | new Promise((resolve, reject) => { 49 | try { 50 | const result = converter?.convert( 51 | svg, 52 | options?.scale, 53 | options?.width, 54 | options?.height, 55 | options?.backgroundColor, 56 | ); 57 | if (result) resolve(result); 58 | else throw new Error("Converter already disposed."); 59 | } catch (e) { 60 | if (e instanceof Error) reject(e); 61 | else reject(new Error(`${e}`)); 62 | } 63 | }); 64 | svg2png.dispose = () => { 65 | converter?.free(); 66 | converter = undefined; 67 | }; 68 | svg2png.getLoadedFontFamilies = () => converter?.list_fonts() ?? []; 69 | 70 | return svg2png; 71 | }; 72 | 73 | export const svg2png = ( 74 | svg: string, 75 | opts?: ConverterOptions & ConvertOptions, 76 | ): Promise => { 77 | const convert = createSvg2png(opts); 78 | return convert(svg, opts).finally(() => convert.dispose()); 79 | }; 80 | 81 | // types re-export 82 | export { ConverterOptions, ConvertOptions, Svg2png }; 83 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | export type DefaultFontFamily = { 2 | serifFamily?: string; 3 | sansSerifFamily?: string; 4 | cursiveFamily?: string; 5 | fantasyFamily?: string; 6 | monospaceFamily?: string; 7 | }; 8 | 9 | export type ConverterOptions = { 10 | fonts?: Uint8Array[]; 11 | defaultFontFamily?: DefaultFontFamily; 12 | }; 13 | 14 | export type ConvertOptions = { 15 | scale?: number; 16 | width?: number; 17 | height?: number; 18 | backgroundColor?: string; 19 | }; 20 | 21 | export type Svg2png = (( 22 | svg: string, 23 | options?: ConvertOptions, 24 | ) => Promise) & { 25 | getLoadedFontFamilies: () => string[]; 26 | dispose: () => void; 27 | }; 28 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg2png-wasm", 3 | "version": "1.4.1", 4 | "description": "A svg to png converter made with wasm.", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.mjs", 7 | "unpkg": "dist/index.min.js", 8 | "types": "dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.mjs", 12 | "require": "./dist/index.cjs", 13 | "node": "./dist/index.mjs", 14 | "default": "./dist/index.mjs", 15 | "types": "./dist/index.d.ts" 16 | }, 17 | "./svg2png_wasm_bg.wasm": "./svg2png_wasm_bg.wasm" 18 | }, 19 | "scripts": { 20 | "prepack": "pnpm clean && pnpm build", 21 | "build": "pnpm build:wasm && pnpm build:ts", 22 | "build:wasm": "wasm-pack build --target web --out-dir dist-wasm && npm run copy-wasm", 23 | "build:ts": "pnpm bundle", 24 | "bundle": "pnpm bundle:js && pnpm bundle:dts", 25 | "bundle:js": "node bundle", 26 | "bundle:dts": "dts-bundle-generator --no-banner --external-types -o dist/index.d.ts lib/index.ts", 27 | "clean": "pnpm clean:wasm && pnpm clean:bundle", 28 | "clean:wasm": "rimraf dist-wasm svg2png_wasm_bg.wasm", 29 | "clean:bundle": "rimraf dist", 30 | "copy-wasm": "copyfiles -f dist-wasm/svg2png_wasm_bg.wasm .", 31 | "format": "pnpm format:all && pnpm format:rust", 32 | "format:all": "biome check --write --unsafe .", 33 | "format:rust": "cargo fmt", 34 | "lint": "pnpm lint:js && pnpm lint:rust", 35 | "lint:js": "biome check .", 36 | "lint:rust": "cargo fmt -- --check && cargo clippy", 37 | "test": "pnpm test:vrt && pnpm test:node", 38 | "test:node": "vitest", 39 | "test:vrt": "pnpm vrt:gen && pnpm vrt:comp", 40 | "vrt:gen": "node test/vrt/index.mjs", 41 | "vrt:comp": "reg-cli test/vrt/actual test/vrt/expected test/vrt/diff", 42 | "vrt:clean": "rimraf test/vrt/diff test/vrt/actual", 43 | "vrt-update": "pnpm vrt:clean && pnpm vrt:gen && reg-cli test/vrt/actual test/vrt/expected test/vrt/diff -U", 44 | "changeset": "changeset", 45 | "release": "pnpm prepack && pnpm changeset publish" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/ssssota/svg2png-wasm.git" 50 | }, 51 | "keywords": ["wasm", "svg", "png"], 52 | "files": ["svg2png_wasm_bg.wasm", "dist"], 53 | "author": "ssssota", 54 | "license": "MIT", 55 | "bugs": { 56 | "url": "https://github.com/ssssota/svg2png-wasm/issues" 57 | }, 58 | "homepage": "https://ssssota.github.io/svg2png-wasm/", 59 | "devDependencies": { 60 | "@biomejs/biome": "^1.9.4", 61 | "@changesets/changelog-github": "^0.5.1", 62 | "@changesets/cli": "^2.28.1", 63 | "@commitlint/cli": "^19.7.1", 64 | "@commitlint/config-conventional": "^19.7.1", 65 | "copyfiles": "^2.4.1", 66 | "dts-bundle-generator": "^9.5.1", 67 | "esbuild": "^0.25.0", 68 | "glob": "^11.0.1", 69 | "reg-cli": "^0.18.10", 70 | "rimraf": "^6.0.1", 71 | "serve": "^14.2.4", 72 | "svelte": "^5.20.5", 73 | "tslib": "^2.8.1", 74 | "typescript": "^5.8.2", 75 | "vitest": "^3.0.7", 76 | "wasm-pack": "^0.13.1" 77 | }, 78 | "type": "module", 79 | "packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b", 80 | "pnpm": { 81 | "onlyBuiltDependencies": ["esbuild"] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '.' 3 | - 'site' 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "dependencyDashboard": false 5 | } 6 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /site/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /site/README.md: -------------------------------------------------------------------------------- 1 | # svg2png-wasm demo site 2 | 3 | ## Development 4 | 5 | ```sh 6 | pnpm install 7 | pnpm dev 8 | ``` 9 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "site", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "prepare": "svelte-kit sync || echo ''", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 13 | }, 14 | "dependencies": { 15 | "front-matter": "^4.0.2", 16 | "highlight.js": "^11.11.1", 17 | "rehype-highlight": "^7.0.2", 18 | "svelte-exmarkdown": "^4.0.3", 19 | "svg2png-wasm": "workspace:*" 20 | }, 21 | "devDependencies": { 22 | "@sveltejs/adapter-auto": "^4.0.0", 23 | "@sveltejs/kit": "^2.17.3", 24 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 25 | "carbon-components-svelte": "^0.87.6", 26 | "carbon-icons-svelte": "^13.3.0", 27 | "svelte": "^5.20.5", 28 | "svelte-check": "^4.1.4", 29 | "typescript": "^5.8.2", 30 | "vite": "^6.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /site/src/app.css: -------------------------------------------------------------------------------- 1 | :where(*, *::before, *::after) { 2 | box-sizing: border-box; 3 | } 4 | -------------------------------------------------------------------------------- /site/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /site/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /site/src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*?u8" { 2 | const content: Uint8Array; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /site/src/lib/components/Demos.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | {#each Object.entries(imagesPromiseMap) as [url, promises] (url)} 17 |
18 |

{url} svg

19 |

20 | 39 |

40 |

41 | {#each promises as promise} 42 | {#await promise} 43 | 44 | {:then buf} 45 | {#if browser} 46 | converted 52 | {/if} 53 | {/await} 54 | {/each} 55 |

56 |
57 | {/each} 58 |
59 | 60 | 73 | -------------------------------------------------------------------------------- /site/src/lib/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 |

10 | 11 | svg2png-wasm 12 | 13 |

14 |
15 | 18 |

19 | 24 | 25 | 26 |

27 |
28 |
29 | 30 | 57 | -------------------------------------------------------------------------------- /site/src/lib/components/HighlightCodeSnippet.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /site/src/lib/components/InstallCommand.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | -------------------------------------------------------------------------------- /site/src/lib/components/Markdown.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 |
11 | 12 | 26 | -------------------------------------------------------------------------------- /site/src/lib/components/Playground.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 |
34 | 41 | {#each fileInfoList as { id, name, value } (id)} 42 | {#await value} 43 | 44 | {:then text} 45 | 46 | {#await $svg2png(text)} 47 | 48 | {:then buf} 49 | {#if browser} 50 | {name} 56 | {/if} 57 | {/await} 58 | {:catch e} 59 | 60 | {/await} 61 | {/each} 62 |
63 | 64 | 70 | -------------------------------------------------------------------------------- /site/src/lib/components/SelectableCodeSnippet.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | 19 | {#each Object.keys(titleValueMap) as title (title)} 20 | 21 | {/each} 22 | 23 | 24 | {#if highlight !== undefined} 25 | 29 | {:else} 30 | 31 | {/if} 32 |
33 | 34 | 39 | -------------------------------------------------------------------------------- /site/src/lib/components/markdown-plugins/code/Blockquote.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 12 | 13 | 14 |
15 | -------------------------------------------------------------------------------- /site/src/lib/components/markdown-plugins/code/BlockquoteAdapter.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
{@render children?.()}
7 | -------------------------------------------------------------------------------- /site/src/lib/components/markdown-plugins/code/Code.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /site/src/lib/components/markdown-plugins/code/CodeAdapter.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {@render children?.()} 7 | -------------------------------------------------------------------------------- /site/src/lib/components/markdown-plugins/code/Pre.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /site/src/lib/components/markdown-plugins/code/PreAdapter.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
{#each get(getAstNode()).children?.[0].children ?? [] as astNode}{/each}
13 | -------------------------------------------------------------------------------- /site/src/lib/components/markdown-plugins/code/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "svelte-exmarkdown"; 2 | import Blockquote from "./BlockquoteAdapter.svelte"; 3 | import Code from "./CodeAdapter.svelte"; 4 | import Pre from "./PreAdapter.svelte"; 5 | 6 | export const codePlugin: Plugin = { 7 | renderer: { 8 | pre: Pre, 9 | code: Code, 10 | blockquote: Blockquote, 11 | h1: "h2", 12 | h2: "h3", 13 | h3: "h4", 14 | h4: "h5", 15 | h5: "h6", 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /site/src/lib/components/markdown-plugins/highlight/index.ts: -------------------------------------------------------------------------------- 1 | import typescript from "highlight.js/lib/languages/typescript"; 2 | import rehypeHighlight from "rehype-highlight"; 3 | import type { Plugin } from "svelte-exmarkdown"; 4 | import "highlight.js/styles/github.css"; 5 | 6 | export const highlightPlugin: Plugin = { 7 | rehypePlugin: [rehypeHighlight, { languages: { typescript } }], 8 | }; 9 | -------------------------------------------------------------------------------- /site/src/lib/components/markdown-plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./highlight"; 2 | export * from "./code"; 3 | -------------------------------------------------------------------------------- /site/src/lib/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./packageManager"; 2 | export * from "./svg2png"; 3 | -------------------------------------------------------------------------------- /site/src/lib/stores/packageManager.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | import { writable } from "svelte/store"; 3 | 4 | export const packageManagers = ["npm", "yarn", "pnpm"] as const; 5 | 6 | export type PackageManager = (typeof packageManagers)[number]; 7 | 8 | const storeKey = "packageManager"; 9 | const npmOrYarnOrPnpm = (pm: unknown): PackageManager => 10 | pm === "npm" || pm === "yarn" || pm === "pnpm" ? pm : "npm"; 11 | 12 | export const packageManager = (() => { 13 | const { subscribe, set } = writable( 14 | browser ? npmOrYarnOrPnpm(localStorage.getItem(storeKey)) : "npm", 15 | ); 16 | 17 | const setWithStorage = (pm: PackageManager) => { 18 | set(pm); 19 | localStorage.setItem(storeKey, pm); 20 | }; 21 | 22 | return { 23 | subscribe, 24 | set: setWithStorage, 25 | }; 26 | })(); 27 | -------------------------------------------------------------------------------- /site/src/lib/stores/svg2png.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | import { base } from "$app/paths"; 3 | import { createSvg2png } from "$lib/svg2png"; 4 | import { writable } from "svelte/store"; 5 | import type { ConverterOptions } from "svg2png-wasm"; 6 | 7 | export const svg2png = (() => { 8 | const { subscribe, set } = writable>(); 9 | 10 | if (browser) { 11 | fetch(`${base}/Roboto-Regular.ttf`) 12 | .then((res) => res.arrayBuffer()) 13 | .then((buf) => { 14 | const options = { 15 | fonts: [new Uint8Array(buf)], 16 | defaultFontFamily: { 17 | serifFamily: "Roboto", 18 | sansSerifFamily: "Roboto", 19 | monospaceFamily: "Roboto", 20 | fantasyFamily: "Roboto", 21 | cursiveFamily: "Roboto", 22 | }, 23 | } satisfies ConverterOptions; 24 | const svg2png = createSvg2png(options); 25 | set(svg2png); 26 | }); 27 | } 28 | return { subscribe }; 29 | })(); 30 | -------------------------------------------------------------------------------- /site/src/lib/svg2png/index.ts: -------------------------------------------------------------------------------- 1 | import { base } from "$app/paths"; 2 | import type { ConvertOptions, ConverterOptions } from "svg2png-wasm"; 3 | import type { Svg2pngRequest, Svg2pngResponse } from "./worker"; 4 | import Svg2pngWorker from "./worker?worker"; 5 | 6 | export const createSvg2png = ( 7 | options?: ConverterOptions, 8 | ): (( 9 | svg: string, 10 | options?: ConvertOptions | undefined, 11 | ) => Promise) & { 12 | dispose: () => void; 13 | } => { 14 | const svg2pngWorker = new Svg2pngWorker(); 15 | const initialize = async () => { 16 | svg2pngWorker.postMessage({ 17 | type: "initialize", 18 | options, 19 | wasm: await fetch(`${base}/svg2png.wasm`) 20 | .then((res) => res.arrayBuffer()) 21 | .then((buf) => new Uint8Array(buf)), 22 | } satisfies Svg2pngRequest); 23 | }; 24 | 25 | const initializePromise = initialize(); 26 | // worker wrapper 27 | const svg2png = async ( 28 | svg: string, 29 | options?: ConvertOptions, 30 | ): Promise => { 31 | await initializePromise; 32 | const id = `svg2png-${Math.random().toString(36)}`; 33 | let fulfilled: (value: Uint8Array | PromiseLike) => void; 34 | let rejected: (reason?: unknown) => void; 35 | 36 | const onMessage = (ev: MessageEvent) => { 37 | const response = ev.data as Svg2pngResponse; 38 | if (response.id !== id) return; 39 | switch (response.type) { 40 | case "success": 41 | fulfilled(response.data); 42 | break; 43 | case "error": 44 | rejected(response.error); 45 | break; 46 | default: 47 | rejected(response); 48 | } 49 | }; 50 | const onMessageError = (e: MessageEvent) => rejected(e.data); 51 | const onError = (e: ErrorEvent) => rejected(e.error); 52 | try { 53 | return await new Promise((resolve, reject) => { 54 | fulfilled = resolve; 55 | rejected = reject; 56 | svg2pngWorker.addEventListener("message", onMessage); 57 | svg2pngWorker.addEventListener("messageerror", onMessageError); 58 | svg2pngWorker.addEventListener("error", onError); 59 | 60 | svg2pngWorker.postMessage({ 61 | id, 62 | type: "svg2png", 63 | svg, 64 | options, 65 | } satisfies Svg2pngRequest); 66 | }); 67 | } finally { 68 | svg2pngWorker.removeEventListener("message", onMessage); 69 | svg2pngWorker.removeEventListener("messageerror", onMessageError); 70 | svg2pngWorker.removeEventListener("error", onError); 71 | } 72 | }; 73 | 74 | svg2png.dispose = () => svg2pngWorker.terminate(); 75 | 76 | return svg2png; 77 | }; 78 | -------------------------------------------------------------------------------- /site/src/lib/svg2png/worker.ts: -------------------------------------------------------------------------------- 1 | import { createSvg2png, initialize } from "svg2png-wasm"; 2 | import type { ConvertOptions, ConverterOptions, Svg2png } from "svg2png-wasm"; 3 | 4 | console.log("worker initializing"); 5 | export type Svg2pngRequest = 6 | | { 7 | id: string; 8 | type: "svg2png"; 9 | svg: string; 10 | options?: ConvertOptions; // type importing will be error 11 | } 12 | | { 13 | type: "initialize"; 14 | wasm: Uint8Array; 15 | options?: ConverterOptions; 16 | }; 17 | 18 | export type Svg2pngResponse = 19 | | { id: string; type: "success"; data: Uint8Array } 20 | | { id: string; type: "error"; error: Error }; 21 | 22 | let svg2png: Svg2png | undefined; 23 | 24 | self.addEventListener("message", (ev) => { 25 | const data = ev.data as Svg2pngRequest; 26 | if (typeof data !== "object" || data == null) return; 27 | 28 | console.log(`worker ${data.type}`); 29 | 30 | switch (data.type) { 31 | case "initialize": 32 | initialize(data.wasm).then(() => { 33 | svg2png = createSvg2png(data.options); 34 | }); 35 | break; 36 | case "svg2png": 37 | if (svg2png === undefined) 38 | self.postMessage({ 39 | type: "error", 40 | error: "svg2png has not been initialized.", 41 | }); 42 | else 43 | svg2png(data.svg, data.options) 44 | .then((res) => 45 | self.postMessage({ id: data.id, type: "success", data: res }), 46 | ) 47 | .catch((e) => 48 | self.postMessage({ id: data.id, type: "error", error: `${e}` }), 49 | ); 50 | break; 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /site/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | svg2png-wasm 13 | 14 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 45 | 46 | 47 |
48 | 49 | {@render children?.()} 50 | 51 |
52 | 53 | 68 | -------------------------------------------------------------------------------- /site/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /site/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 |

svg2png-wasm

10 |

Convert svg to png using WebAssembly.

11 | 12 |
13 | 14 |
15 |

Demo

16 | 17 |
18 | 19 |
20 |

Playground

21 |

Runs in the browser, file is not sent to the server.

22 | 23 |
24 |
25 | 26 | 38 | -------------------------------------------------------------------------------- /site/src/routes/docs/+page.server.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import frontMatter from "front-matter"; 3 | import type { PageServerLoad } from "./$types"; 4 | 5 | export const load: PageServerLoad = async () => { 6 | const files = fs 7 | .readdirSync("../docs", { withFileTypes: true }) 8 | .filter((dirent) => dirent.isFile()) 9 | .map((dirent) => { 10 | const content = fs.readFileSync(`../docs/${dirent.name}`, "utf-8"); 11 | const { attributes, body } = frontMatter>(content); 12 | return { 13 | meta: attributes, 14 | slug: dirent.name.replace(/\.md$/, ""), 15 | body, 16 | }; 17 | }); 18 | return { sections: files }; 19 | }; 20 | -------------------------------------------------------------------------------- /site/src/routes/docs/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 20 | {#each data.sections as { body, meta, slug } (slug)} 21 |
22 |

23 | {meta.title} 24 | 25 | # 26 | 27 |

28 | 29 |
30 | {/each} 31 |
32 | 33 | 57 | -------------------------------------------------------------------------------- /site/src/routes/favicon.png/+server.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import { initialize, svg2png } from "svg2png-wasm"; 3 | import wasm from "svg2png-wasm/svg2png_wasm_bg.wasm?u8"; 4 | import type { RequestHandler } from "./$types"; 5 | 6 | const svgPromise = fs.readFileSync("../logo.svg", "utf8"); 7 | 8 | export const GET: RequestHandler = async () => { 9 | await initialize(wasm).catch(() => undefined); 10 | const png = await svg2png(svgPromise); 11 | return new Response(png, { 12 | headers: { "Content-Type": "image/png" }, 13 | }); 14 | }; 15 | 16 | export const prerender = true; 17 | -------------------------------------------------------------------------------- /site/src/routes/icon.svg/+server.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import type { RequestHandler } from "./$types"; 3 | 4 | const svgPromise = fs.readFileSync("../logo.svg", "utf8"); 5 | 6 | export const GET: RequestHandler = async () => { 7 | return new Response(svgPromise, { 8 | headers: { "Content-Type": "image/svg+xml" }, 9 | }); 10 | }; 11 | 12 | export const prerender = true; 13 | -------------------------------------------------------------------------------- /site/src/routes/ogp.png/+server.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import { initialize, svg2png } from "svg2png-wasm"; 3 | import wasm from "svg2png-wasm/svg2png_wasm_bg.wasm?u8"; 4 | import type { RequestHandler } from "./$types"; 5 | 6 | const svgPromise = fs.readFileSync("./static/ogp.svg", "utf8"); 7 | 8 | export const GET: RequestHandler = async () => { 9 | await initialize(wasm).catch(() => undefined); 10 | const png = await svg2png(svgPromise); 11 | return new Response(png, { 12 | headers: { "Content-Type": "image/png" }, 13 | }); 14 | }; 15 | 16 | export const prerender = true; 17 | -------------------------------------------------------------------------------- /site/src/routes/svg2png.wasm/+server.ts: -------------------------------------------------------------------------------- 1 | import wasm from "svg2png-wasm/svg2png_wasm_bg.wasm?u8"; 2 | import type { RequestHandler } from "./$types"; 3 | 4 | export const GET: RequestHandler = async () => { 5 | return new Response(wasm, { 6 | headers: { "Content-Type": "application/wasm" }, 7 | }); 8 | }; 9 | 10 | export const prerender = true; 11 | -------------------------------------------------------------------------------- /site/static/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/site/static/Roboto-Regular.ttf -------------------------------------------------------------------------------- /site/static/gradient.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /site/static/ogp.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /site/static/parrot.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /site/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-auto"; 2 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter(), 15 | paths: { 16 | base: "/svg2png-wasm", 17 | }, 18 | }, 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /site/vite.config.ts: -------------------------------------------------------------------------------- 1 | import fsp from "node:fs/promises"; 2 | import { sveltekit } from "@sveltejs/kit/vite"; 3 | import { defineConfig } from "vite"; 4 | import type { Plugin } from "vite"; 5 | 6 | const postfixRE = /[?#].*$/s; 7 | function cleanUrl(url: string): string { 8 | return url.replace(postfixRE, ""); 9 | } 10 | const u8RE = /(?:\?|&)u8(?:&|$)/; 11 | const u8: Plugin = { 12 | name: "u8", 13 | async load(id) { 14 | // raw requests, read from disk 15 | if (u8RE.test(id)) { 16 | const file = cleanUrl(id); 17 | const buffer = await fsp.readFile(file); 18 | // raw query, read file and return as string 19 | return `\ 20 | const base64 = ${JSON.stringify(buffer.toString("base64"))}; 21 | const u8 = new Uint8Array(atob(base64).split('').map(c => c.charCodeAt(0))); 22 | export default u8;`; 23 | } 24 | }, 25 | }; 26 | 27 | export default defineConfig({ 28 | plugins: [u8, sveltekit()], 29 | }); 30 | -------------------------------------------------------------------------------- /src/converter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::str::FromStr; 3 | 4 | use resvg::tiny_skia::{Color, IntSize, Pixmap, Transform}; 5 | use resvg::usvg::fontdb::Database; 6 | use resvg::usvg::{Options, Size, Tree, TreeParsing, TreeTextToPath}; 7 | use wasm_bindgen::prelude::*; 8 | 9 | #[wasm_bindgen] 10 | #[derive(Clone)] 11 | pub struct Converter { 12 | fonts: Vec>, 13 | serif_family: Option, 14 | sans_serif_family: Option, 15 | cursive_family: Option, 16 | fantasy_family: Option, 17 | monospace_family: Option, 18 | } 19 | 20 | #[wasm_bindgen] 21 | impl Converter { 22 | #[wasm_bindgen(js_name = registerFont)] 23 | pub fn register_font(&mut self, font: &[u8]) { 24 | self.fonts.push(font.to_vec()); 25 | } 26 | 27 | #[wasm_bindgen] 28 | pub fn convert( 29 | &self, 30 | svg: &str, 31 | scale: Option, 32 | width: Option, 33 | height: Option, 34 | background: Option, 35 | ) -> Result, JsValue> { 36 | let fontdb = load_fonts( 37 | &self.fonts, 38 | self.serif_family.as_deref(), 39 | self.sans_serif_family.as_deref(), 40 | self.cursive_family.as_deref(), 41 | self.fantasy_family.as_deref(), 42 | self.monospace_family.as_deref(), 43 | ); 44 | let faces = &mut fontdb.faces(); 45 | let default_font_family = if fontdb.is_empty() { 46 | "sans-serif".to_string() 47 | } else { 48 | faces.next().unwrap().families[0].0.to_string() 49 | }; 50 | let svg_options = Options { 51 | resources_dir: None, 52 | dpi: 96.0, 53 | font_family: default_font_family.clone(), 54 | font_size: 12.0, 55 | languages: vec!["en".to_string()], 56 | shape_rendering: resvg::usvg::ShapeRendering::GeometricPrecision, 57 | text_rendering: resvg::usvg::TextRendering::OptimizeLegibility, 58 | image_rendering: resvg::usvg::ImageRendering::OptimizeQuality, 59 | default_size: Size::from_wh(width.unwrap_or(100.0), height.unwrap_or(100.0)) 60 | .ok_or_else(|| JsValue::from_str("Invalid width or height"))?, 61 | image_href_resolver: resvg::usvg::ImageHrefResolver::default(), 62 | }; 63 | 64 | let scale = scale.unwrap_or(1.0); 65 | let mut tree = 66 | Tree::from_str(svg, &svg_options).map_err(|e| JsValue::from_str(&e.to_string()))?; 67 | tree.convert_text(&fontdb); 68 | 69 | let svg_size = tree.size; 70 | let (width, height) = match (width, height) { 71 | (Some(w), Some(h)) => (w.round() as u32, h.round() as u32), 72 | (Some(w), _) => ( 73 | w.round() as u32, 74 | (svg_size.height() * (w / svg_size.width())) as u32, 75 | ), 76 | (_, Some(h)) => ( 77 | (svg_size.width() * (h / svg_size.height())) as u32, 78 | h.round() as u32, 79 | ), 80 | _ => ( 81 | ((svg_size.width().round() as f32) * scale) as u32, 82 | ((svg_size.height().round() as f32) * scale) as u32, 83 | ), 84 | }; 85 | 86 | let mut pixmap = Pixmap::new(width, height) 87 | .ok_or_else(|| JsValue::from_str("Invalid width or height"))?; 88 | 89 | if let Some(color) = background { 90 | pixmap.fill(parse_color_string(&color)); 91 | } 92 | 93 | let rtree = resvg::Tree::from_usvg(&tree); 94 | 95 | let size = tree.size.to_int_size(); 96 | let size1 = size.to_size(); 97 | let fit_to_size = IntSize::from_wh(width, height).map(|s| size.scale_to(s)); 98 | let size2 = match fit_to_size { 99 | Some(v) => v.to_size(), 100 | None => size.to_size(), 101 | }; 102 | 103 | let tx = Transform::from_scale( 104 | (size2.width() / size1.width()) as f32, 105 | (size2.height() / size1.height()) as f32, 106 | ); 107 | 108 | rtree.render(tx, &mut pixmap.as_mut()); 109 | 110 | pixmap 111 | .encode_png() 112 | .map_err(|e| JsValue::from_str(&e.to_string())) 113 | } 114 | 115 | #[wasm_bindgen] 116 | pub fn list_fonts(&self) -> Box<[JsValue]> { 117 | load_fonts(&self.fonts, None, None, None, None, None) 118 | .faces() 119 | .map(|f| &f.families[0].0) 120 | .collect::>() 121 | .iter() 122 | .map(|s| JsValue::from_str(s)) 123 | .collect::>() 124 | .into_boxed_slice() 125 | } 126 | } 127 | 128 | #[wasm_bindgen(js_name = createConverter)] 129 | pub fn create_converter( 130 | default_serif_family: Option, 131 | default_sans_serif_family: Option, 132 | default_cursive_family: Option, 133 | default_fantasy_family: Option, 134 | default_monospace_family: Option, 135 | ) -> Converter { 136 | Converter { 137 | fonts: Vec::new(), 138 | serif_family: default_serif_family, 139 | sans_serif_family: default_sans_serif_family, 140 | cursive_family: default_cursive_family, 141 | fantasy_family: default_fantasy_family, 142 | monospace_family: default_monospace_family, 143 | } 144 | } 145 | 146 | pub fn load_fonts( 147 | fonts: &[Vec], 148 | default_serif_family: Option<&str>, 149 | default_sans_serif_family: Option<&str>, 150 | default_cursive_family: Option<&str>, 151 | default_fantasy_family: Option<&str>, 152 | default_monospace_family: Option<&str>, 153 | ) -> Database { 154 | let mut db = Database::new(); 155 | for font in fonts { 156 | db.load_font_data(font.to_vec()); 157 | } 158 | if let Some(f) = default_serif_family { 159 | db.set_serif_family(f.to_string()) 160 | } 161 | if let Some(f) = default_sans_serif_family { 162 | db.set_sans_serif_family(f.to_string()) 163 | } 164 | if let Some(f) = default_cursive_family { 165 | db.set_cursive_family(f.to_string()) 166 | } 167 | if let Some(f) = default_fantasy_family { 168 | db.set_fantasy_family(f.to_string()) 169 | } 170 | if let Some(f) = default_monospace_family { 171 | db.set_monospace_family(f.to_string()) 172 | } 173 | db 174 | } 175 | 176 | fn parse_color_string(color: &str) -> Color { 177 | match svgtypes::Color::from_str(color) { 178 | Err(_) => Color::TRANSPARENT, 179 | Ok(c) => Color::from_rgba8(c.red, c.green, c.blue, c.alpha), 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod converter; 2 | 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen(start)] 6 | pub fn initialize() { 7 | #[cfg(feature = "console_error_panic_hook")] 8 | console_error_panic_hook::set_once(); 9 | } 10 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # svg2png-wasm tests 2 | 3 | - node / Using Node.js 4 | - browser / Using playwright 5 | - vrt / Visual Regression Testing with Node.js 6 | -------------------------------------------------------------------------------- /test/data/NotoSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/data/NotoSerif-Regular.ttf -------------------------------------------------------------------------------- /test/data/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/data/Roboto-Regular.ttf -------------------------------------------------------------------------------- /test/data/edge-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/data/fe-turblence.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/data/text-default-sans-serif.svg: -------------------------------------------------------------------------------- 1 | A 2 | -------------------------------------------------------------------------------- /test/data/text-default-serif.svg: -------------------------------------------------------------------------------- 1 | A 2 | -------------------------------------------------------------------------------- /test/data/text-not-spec-font.svg: -------------------------------------------------------------------------------- 1 | A 2 | -------------------------------------------------------------------------------- /test/data/text.svg: -------------------------------------------------------------------------------- 1 | A 2 | -------------------------------------------------------------------------------- /test/node/index.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { resolve } from "node:path"; 3 | import { beforeAll, describe, expect, it } from "vitest"; 4 | import { createSvg2png, initialize, svg2png } from "../.."; 5 | 6 | beforeAll(async () => { 7 | await initialize(readFileSync("./svg2png_wasm_bg.wasm")); 8 | }); 9 | 10 | it("promise should be rejected when initialize twice", async () => { 11 | await expect(initialize("")).rejects.toThrow( 12 | "Already initialized. The `initialize` function can be used only once.", 13 | ); 14 | }); 15 | 16 | describe("svg2png", () => { 17 | it("should throw because invalid svg (blank string)", async () => { 18 | await expect(svg2png("")).rejects.toThrow( 19 | "SVG data parsing failed cause the document does not have a root node", 20 | ); 21 | }); 22 | 23 | it("should convert with default sizes", async () => { 24 | const actual = await svg2png( 25 | '', 26 | ); 27 | const expected = await svg2png( 28 | '', 29 | ); 30 | expect(actual).toStrictEqual(expected); 31 | }); 32 | 33 | it("should convert because specify width and height", async () => { 34 | await expect( 35 | svg2png('', { 36 | width: 1, 37 | height: 1, 38 | }), 39 | ).resolves.toBeInstanceOf(Uint8Array); 40 | }); 41 | 42 | it("shoud convert with minimal svg", async () => { 43 | await expect( 44 | svg2png( 45 | '', 46 | ), 47 | ).resolves.toBeInstanceOf(Uint8Array); 48 | }); 49 | 50 | it("shoud throw because invalid width", async () => { 51 | await expect( 52 | svg2png( 53 | '', 54 | { width: 0 }, 55 | ), 56 | ).rejects.toThrow("Invalid width or height"); 57 | }); 58 | 59 | it("shoud throw because invalid scale", async () => { 60 | await expect( 61 | svg2png( 62 | '', 63 | { scale: 0 }, 64 | ), 65 | ).rejects.toThrow("Invalid width or height"); 66 | }); 67 | 68 | it("shoud convert with specified width because width have priority", async () => { 69 | const expected = await svg2png( 70 | '', 71 | { width: 10 }, 72 | ); 73 | const actual = await svg2png( 74 | '', 75 | { width: 10, scale: 100 }, 76 | ); 77 | expect(actual).toStrictEqual(expected); 78 | }); 79 | }); 80 | 81 | describe("createSvg2png", () => { 82 | it("no fonts", async () => { 83 | const convert = createSvg2png(); 84 | expect(convert.getLoadedFontFamilies()).toStrictEqual([]); 85 | convert.dispose(); 86 | }); 87 | 88 | it("only roboto", async () => { 89 | const convert = createSvg2png({ 90 | fonts: [ 91 | new Uint8Array( 92 | readFileSync(resolve(__dirname, "../data/Roboto-Regular.ttf")), 93 | ), 94 | ], 95 | }); 96 | expect(convert.getLoadedFontFamilies()).toStrictEqual(["Roboto"]); 97 | convert.dispose(); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/vrt/.gitignore: -------------------------------------------------------------------------------- 1 | actual 2 | diff 3 | -------------------------------------------------------------------------------- /test/vrt/expected/edge-circle.1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/edge-circle.1x.png -------------------------------------------------------------------------------- /test/vrt/expected/edge-circle.2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/edge-circle.2x.png -------------------------------------------------------------------------------- /test/vrt/expected/edge-circle.30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/edge-circle.30h.png -------------------------------------------------------------------------------- /test/vrt/expected/edge-circle.50w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/edge-circle.50w.png -------------------------------------------------------------------------------- /test/vrt/expected/edge-circle.50w30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/edge-circle.50w30h.png -------------------------------------------------------------------------------- /test/vrt/expected/edge-circle.blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/edge-circle.blue.png -------------------------------------------------------------------------------- /test/vrt/expected/edge-circle.green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/edge-circle.green.png -------------------------------------------------------------------------------- /test/vrt/expected/edge-circle.red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/edge-circle.red.png -------------------------------------------------------------------------------- /test/vrt/expected/fe-turblence.1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/fe-turblence.1x.png -------------------------------------------------------------------------------- /test/vrt/expected/fe-turblence.2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/fe-turblence.2x.png -------------------------------------------------------------------------------- /test/vrt/expected/fe-turblence.30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/fe-turblence.30h.png -------------------------------------------------------------------------------- /test/vrt/expected/fe-turblence.50w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/fe-turblence.50w.png -------------------------------------------------------------------------------- /test/vrt/expected/fe-turblence.50w30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/fe-turblence.50w30h.png -------------------------------------------------------------------------------- /test/vrt/expected/fe-turblence.blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/fe-turblence.blue.png -------------------------------------------------------------------------------- /test/vrt/expected/fe-turblence.green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/fe-turblence.green.png -------------------------------------------------------------------------------- /test/vrt/expected/fe-turblence.red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/fe-turblence.red.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-sans-serif.1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-sans-serif.1x.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-sans-serif.2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-sans-serif.2x.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-sans-serif.30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-sans-serif.30h.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-sans-serif.50w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-sans-serif.50w.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-sans-serif.50w30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-sans-serif.50w30h.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-sans-serif.blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-sans-serif.blue.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-sans-serif.green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-sans-serif.green.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-sans-serif.red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-sans-serif.red.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-serif.1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-serif.1x.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-serif.2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-serif.2x.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-serif.30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-serif.30h.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-serif.50w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-serif.50w.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-serif.50w30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-serif.50w30h.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-serif.blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-serif.blue.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-serif.green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-serif.green.png -------------------------------------------------------------------------------- /test/vrt/expected/text-default-serif.red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-default-serif.red.png -------------------------------------------------------------------------------- /test/vrt/expected/text-not-spec-font.1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-not-spec-font.1x.png -------------------------------------------------------------------------------- /test/vrt/expected/text-not-spec-font.2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-not-spec-font.2x.png -------------------------------------------------------------------------------- /test/vrt/expected/text-not-spec-font.30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-not-spec-font.30h.png -------------------------------------------------------------------------------- /test/vrt/expected/text-not-spec-font.50w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-not-spec-font.50w.png -------------------------------------------------------------------------------- /test/vrt/expected/text-not-spec-font.50w30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-not-spec-font.50w30h.png -------------------------------------------------------------------------------- /test/vrt/expected/text-not-spec-font.blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-not-spec-font.blue.png -------------------------------------------------------------------------------- /test/vrt/expected/text-not-spec-font.green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-not-spec-font.green.png -------------------------------------------------------------------------------- /test/vrt/expected/text-not-spec-font.red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text-not-spec-font.red.png -------------------------------------------------------------------------------- /test/vrt/expected/text.1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text.1x.png -------------------------------------------------------------------------------- /test/vrt/expected/text.2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text.2x.png -------------------------------------------------------------------------------- /test/vrt/expected/text.30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text.30h.png -------------------------------------------------------------------------------- /test/vrt/expected/text.50w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text.50w.png -------------------------------------------------------------------------------- /test/vrt/expected/text.50w30h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text.50w30h.png -------------------------------------------------------------------------------- /test/vrt/expected/text.blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text.blue.png -------------------------------------------------------------------------------- /test/vrt/expected/text.green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text.green.png -------------------------------------------------------------------------------- /test/vrt/expected/text.red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssssota/svg2png-wasm/c3eb2b4da957112e62503dc6456e61d2a1baed01/test/vrt/expected/text.red.png -------------------------------------------------------------------------------- /test/vrt/index.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { 3 | existsSync, 4 | mkdirSync, 5 | readFileSync, 6 | rmSync, 7 | writeFileSync, 8 | } from "node:fs"; 9 | import { dirname, join } from "node:path"; 10 | import { exit } from "node:process"; 11 | import { fileURLToPath } from "node:url"; 12 | import { glob } from "glob"; 13 | import { createSvg2png, initialize } from "../../dist/index.mjs"; 14 | 15 | /** 16 | * Remove(if exists) and Make dir 17 | * @param {string} dir - dirname 18 | */ 19 | const refreshDir = (dir) => { 20 | if (existsSync(dir)) rmSync(dir, { recursive: true, force: true }); 21 | mkdirSync(dir); 22 | }; 23 | 24 | /** 25 | * @param {(svg: string, options?: import('../../dist/index').ConvertOptions) => Promise} svg2png 26 | * @param {string} fileSuffix 27 | * @param {import('../../dist/index').ConvertOptions} options 28 | * @returns {(svgPath: string) => Promise} 29 | */ 30 | const convert = 31 | (svg2png, fileSuffix, options = undefined) => 32 | async (svgPath) => { 33 | const pngPath = svgPath 34 | .replace(/\/data\//g, "/vrt/actual/") 35 | .replace(/\.svg$/i, `${fileSuffix}.png`); 36 | console.log(`[convert] ${pngPath}`); 37 | const png = await svg2png(readFileSync(svgPath, "utf8"), options); 38 | writeFileSync(pngPath, png); 39 | }; 40 | 41 | const main = async () => { 42 | const __dirname = dirname(fileURLToPath(import.meta.url)); 43 | const fontPaths = glob 44 | .sync(join(__dirname, "../data/**/*.@(ttf|otf)")) 45 | .sort(); 46 | const svgs = glob.sync(join(__dirname, "../data/**/*.svg")); 47 | 48 | await initialize(readFileSync("./svg2png_wasm_bg.wasm")); 49 | 50 | /** @type {import('../../dist').DefaultFontFamily} */ 51 | const defaultFontFamily = { 52 | sansSerifFamily: "Roboto", 53 | serifFamily: "Noto Serif", 54 | }; 55 | 56 | const fonts = fontPaths.map((path) => { 57 | console.log("[font]", path); 58 | return new Uint8Array(readFileSync(path)); 59 | }); 60 | 61 | const svg2png = createSvg2png({ 62 | fonts, 63 | defaultFontFamily, 64 | }); 65 | 66 | refreshDir(join(__dirname, "actual")); 67 | refreshDir(join(__dirname, "diff")); 68 | 69 | await Promise.all([ 70 | ...svgs.map(convert(svg2png, ".1x")), 71 | ...svgs.map(convert(svg2png, ".2x", { scale: 2 })), 72 | ...svgs.map(convert(svg2png, ".50w", { width: 50 })), 73 | ...svgs.map(convert(svg2png, ".30h", { height: 30 })), 74 | ...svgs.map(convert(svg2png, ".50w30h", { width: 50, height: 30 })), 75 | ...svgs.map(convert(svg2png, ".red", { backgroundColor: "tomato" })), 76 | ...svgs.map( 77 | convert(svg2png, ".green", { backgroundColor: "rgba(15,255,0,0.25)" }), 78 | ), 79 | ...svgs.map(convert(svg2png, ".blue", { backgroundColor: "#33f" })), 80 | ]); 81 | 82 | svg2png.dispose(); 83 | }; 84 | 85 | main() 86 | .then(() => exit(0)) 87 | .catch((e) => { 88 | console.error(e); 89 | exit(1); 90 | }); 91 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["es2020", "dom"], 5 | "module": "esnext", 6 | "declaration": false, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "node" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { watch: false }, 5 | }); 6 | --------------------------------------------------------------------------------