├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── update-copyright-years-in-license-file.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .npmrc ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── es2015 │ └── index.mjs └── index.js ├── eslint.config.mjs ├── examples ├── common.cjs ├── esm.mjs └── typescript.ts ├── jest-puppeteer.config.mjs ├── jest.config.ts ├── lib ├── cases.ts └── index.template.ts ├── package-lock.json ├── package.json ├── scripts ├── benchmark.ts ├── build │ ├── index.ts │ └── patch-esm-dist.ts ├── get-simple-icons-segments-stats.ts ├── show-simple-icons-segments-stats.ts └── simple-icons-benchmark.cts ├── src ├── cli.cjs ├── index.ts ├── types.d.mts └── types.d.ts ├── svg-path-bbox.svg ├── tests ├── bbox.test.ts ├── browser.test.ts ├── browser │ └── index.html ├── cases │ └── bbox.ts ├── changelog.test.ts ├── cli.test.ts ├── examples.test.ts ├── optimization.test.ts └── utils │ └── syscall.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | 12 | # Markdown settings 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | package-lock.json -diff 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [18.x, 20.x, 22.x] 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Use NodeJS v${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install 26 | run: npm ci --ignore-scripts --no-audit --no-fund 27 | - name: Build 28 | run: npm run build 29 | - name: Test 30 | run: | 31 | npx puppeteer browsers install chrome 32 | npm test 33 | - name: Coverage 34 | uses: coverallsapp/github-action@v2 35 | with: 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | file: ./tests/coverage/lcov.info 38 | 39 | lint: 40 | name: Lint 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Use NodeJS v22 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version: 22.x 48 | - name: Install 49 | run: npm ci --ignore-scripts --no-audit --no-fund 50 | - name: Lint 51 | run: npm run lint 52 | 53 | npm: 54 | if: startsWith(github.ref, 'refs/tags/') 55 | needs: 56 | - test 57 | - lint 58 | name: NPM Package 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v4 63 | - name: Use NodeJS v22 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: 22.x 67 | - name: Install dependencies 68 | run: | 69 | npm ci --ignore-scripts --no-audit --no-fund 70 | npx puppeteer browsers install chrome 71 | - name: Build 72 | run: npm run build 73 | - name: Test 74 | run: npm test 75 | - name: Deploy to NPM 76 | uses: JS-DevTools/npm-publish@v3 77 | with: 78 | token: ${{ secrets.NPM_TOKEN }} 79 | 80 | release: 81 | name: Release 82 | needs: npm 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v4 86 | - name: Get tag metadata 87 | id: tag 88 | run: | 89 | TAG_TITLE=${GITHUB_REF#refs/*/} 90 | echo "title=$TAG_TITLE" >> $GITHUB_OUTPUT 91 | git -c protocol.version=2 fetch --prune --progress \ 92 | --no-recurse-submodules origin \ 93 | +refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/* 94 | TAG_BODY="$(git tag -l --format='%(contents)' $TAG_TITLE)" 95 | TAG_BODY="${TAG_BODY//'%'/'%25'}" 96 | TAG_BODY="${TAG_BODY//$'\n'/'%0A'}" 97 | TAG_BODY="${TAG_BODY//$'\r'/'%0D'}" 98 | echo "body=$TAG_BODY" >> $GITHUB_OUTPUT 99 | - name: Create Release 100 | uses: softprops/action-gh-release@v2 101 | with: 102 | tag_name: ${{ steps.tag.outputs.title }} 103 | name: ${{ steps.tag.outputs.title }} 104 | body: ${{ steps.tag.outputs.body }} 105 | draft: false 106 | prerelease: false 107 | env: 108 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 109 | -------------------------------------------------------------------------------- /.github/workflows/update-copyright-years-in-license-file.yml: -------------------------------------------------------------------------------- 1 | name: Update copyright years in license file 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 2 1 *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | action-update-license-year: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - uses: FantasticFiasco/action-update-license-year@v2 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | __dev__.js 3 | tests/coverage 4 | deps.txt 5 | emsdk 6 | *.wasm 7 | *.tgz 8 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | npm run build 4 | npm t 5 | git add -u 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !LICENSE 3 | !dist/ 4 | !src/ 5 | !package.json 6 | !CHANGELOG.md 7 | !README.md 8 | !tsconfig.json 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | save-exact=true 3 | save-dev=true 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | tests/coverage 2 | dist/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## [2.1.0] - 2024-09-04 4 | 5 | - Accept a `SvgPath` interface from [svgpath] as argument. This makes 6 | `svgPathBbox` x2 faster when using `SvgPath` instances. 7 | 8 | [svgpath]: https://www.npmjs.com/package/svgpath 9 | 10 | ## [2.0.0] - 2024-06-06 11 | 12 | - No more default export. Now you have to import `svgPathBbox` function 13 | directly. Use `import { svgPathBbox } from "svg-path-bbox";` instead of 14 | `import svgPathBbox from "svg-path-bbox";`. 15 | 16 | ## [1.2.6] - 2024-03-14 17 | 18 | - Support types without enabling `esModuleInterop` in TypeScript. 19 | 20 | ## [1.2.5] - 2024-03-13 21 | 22 | - Fixed error computing some quadratic Bézier curves cases. 23 | 24 | ## [1.2.4] - 2023-02-09 25 | 26 | - Optimized proccesing of segments based on [Simple Icons] data. 27 | 28 | [Simple Icons]: https://github.com/simple-icons/simple-icons 29 | 30 | ## [1.2.3] - 2022-12-23 31 | 32 | - Fixed CLI not being executed in some versions of Node.js < v16. 33 | 34 | ## [1.2.2] - 2022-05-26 35 | 36 | - Fixed edge case computing cubic Bézier curves bounding boxes. 37 | 38 | ## [1.2.1] - 2022-05-12 39 | 40 | - Fixed error computing cubic Bézier curves bounding boxes. 41 | 42 | ## [1.2.0] - 2022-05-07 43 | 44 | - Use default export for better interoperability. 45 | 46 | ## [1.1.0] - 2022-05-05 47 | 48 | - Add support for Typescript. 49 | 50 | ## [1.0.2] - 2022-01-11 51 | 52 | - Add basic options `--version` and `--help` to CLI. 53 | 54 | ## [1.0.1] - 2021-06-21 55 | 56 | - Fixed error computing limits for cubic Bèzier curves of length 0. 57 | 58 | ## [1.0.0] - 2021-06-03 59 | 60 | - Testing with 100% coverage. 61 | - Make `svgPathBbox` function the default export. 62 | 63 | ## [0.2.0] - 2020-12-22 64 | 65 | - Removed almost all public API functions (only keep `svgPathBbox` function). 66 | - Removed `polf` dependency. 67 | - Optimized quadratic Bézier curves minimum and maximum values computation. 68 | - Optimized cubic Bézier curves minimum and maximum values computation. 69 | - Optimized lineal segments minimum and maximum values computation. 70 | 71 | ## [0.1.5] - 2020-11-26 72 | 73 | - Documentation improved. 74 | - Switch CI to Github Actions. 75 | 76 | ## [0.1.4] - 2020-11-23 77 | 78 | - Document and export `quadraticBezierCurveBbox` function. 79 | - Remove development file from NPM package. 80 | - Update acknowledgments. 81 | 82 | ## [0.1.1] - 2020-11-19 83 | 84 | - Fix error computing bounding boxes for Q, T and some C commands. 85 | 86 | ## [0.0.47] - 2020-07-23 87 | 88 | - Separate point on line functions in another package. 89 | - Replaced svg-path-parser dependency with svgpath to optimize parsing time. 90 | - Removed dregrees to radians converter function. 91 | - Fix error in point on line function. 92 | - Fix errors in utility functions. 93 | - Add tests for utilities and command line client. 94 | 95 | ## [0.0.28] - 2020-05-22 96 | 97 | - Update LICENSE. 98 | - Fix error converting quaratic to Bézier coordinates. 99 | - Add tests for some bounding boxes functions. 100 | 101 | ## [0.0.26] - 2020-05-21 102 | 103 | - Removed `quadraticBezierCurveBbox` function. 104 | - Optimized quadratic Bézier curve bounding box computation. 105 | - Optimized cubic Bézier curve bounding box algorithm. 106 | - Fixed error on V and H commands computing SVG path bbox. 107 | 108 | ## [0.0.20] - 2020-05-17 109 | 110 | - Add function to obtain an array of numbers from SVG path. 111 | - Multiple paths as arguments for command line script. 112 | 113 | ## [0.0.13] - 2020-05-17 114 | 115 | - Add command line interface. 116 | - Add linting. 117 | - Released to NPM. 118 | - Add documentation. 119 | - Add changelog. 120 | - Basic functionalities finished. 121 | 122 | [2.1.0]: https://github.com/mondeja/svg-path-bbox/compare/v2.0.0...v2.1.0 123 | [2.0.0]: https://github.com/mondeja/svg-path-bbox/compare/v1.2.6...v2.0.0 124 | [1.2.6]: https://github.com/mondeja/svg-path-bbox/compare/v1.2.5...v1.2.6 125 | [1.2.5]: https://github.com/mondeja/svg-path-bbox/compare/v1.2.4...v1.2.5 126 | [1.2.4]: https://github.com/mondeja/svg-path-bbox/compare/v1.2.3...v1.2.4 127 | [1.2.3]: https://github.com/mondeja/svg-path-bbox/compare/v1.2.2...v1.2.3 128 | [1.2.2]: https://github.com/mondeja/svg-path-bbox/compare/v1.2.1...v1.2.2 129 | [1.2.1]: https://github.com/mondeja/svg-path-bbox/compare/v1.2.0...v1.2.1 130 | [1.2.0]: https://github.com/mondeja/svg-path-bbox/compare/v1.1.0...v1.2.0 131 | [1.1.0]: https://github.com/mondeja/svg-path-bbox/compare/v1.0.2...v1.1.0 132 | [1.0.2]: https://github.com/mondeja/svg-path-bbox/compare/v1.0.1...v1.0.2 133 | [1.0.1]: https://github.com/mondeja/svg-path-bbox/compare/v1.0.0...v1.0.1 134 | [1.0.0]: https://github.com/mondeja/svg-path-bbox/compare/v0.2.0...v1.0.0 135 | [0.2.0]: https://github.com/mondeja/svg-path-bbox/compare/v0.1.5...v0.2.0 136 | [0.1.5]: https://github.com/mondeja/svg-path-bbox/compare/v0.1.4...v0.1.5 137 | [0.1.4]: https://github.com/mondeja/svg-path-bbox/compare/v0.1.1...v0.1.4 138 | [0.1.1]: https://github.com/mondeja/svg-path-bbox/compare/v0.0.47...v0.1.1 139 | [0.0.47]: https://github.com/mondeja/svg-path-bbox/compare/v0.0.28...v0.0.47 140 | [0.0.28]: https://github.com/mondeja/svg-path-bbox/compare/v0.0.26...v0.0.28 141 | [0.0.26]: https://github.com/mondeja/svg-path-bbox/compare/v0.0.20...v0.0.26 142 | [0.0.20]: https://github.com/mondeja/svg-path-bbox/compare/v0.0.13...v0.0.20 143 | [0.0.13]: https://github.com/mondeja/svg-path-bbox/releases/tag/v0.0.13 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020-2025, Álvaro Mondéjar Rubio 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📦 svg-path-bbox 2 | 3 | [![NPM version][npm-version-image]][npm-link] 4 | [![License][license-image]][license-link] 5 | [![NodeJS versions][npm-versions-image]][npm-link] 6 | 7 | SVG paths bounding box calculator. 8 | 9 | ## Status 10 | 11 | [![Tests][tests-image]][tests-link] 12 | [![Coverage status][coveralls-image]][coveralls-link] 13 | 14 | ## Install 15 | 16 | ```sh 17 | npm install svg-path-bbox 18 | ``` 19 | 20 | ## Documentation 21 | 22 | ### Usage 23 | 24 | ```javascript 25 | > import { svgPathBbox } from "svg-path-bbox"; 26 | > svgPathBbox("M5 10l2 3z") 27 | [ 5, 10, 7, 13 ] 28 | > svgPathBbox("M5 10c3 0 3 3 0 3z") 29 | [ 5, 10, 7.25, 13 ] 30 | ``` 31 | 32 | Returned bounding box is an array made up like `viewBox` SVG attributes `[x0, y0, x1, y1]` of unrounded values: 33 | 34 |

35 | 36 |

37 | 38 | ### Command line 39 | 40 | ```bash 41 | $ svg-path-bbox "M5 10c3 0 3 3 0 3z" 42 | 5 10 7.25 13 43 | 44 | $ svg-path-bbox "M5 10c3 0 3 3 0 3z" "M2 8m5 5z" 45 | 5 10 7.25 13 46 | 2 8 7 13 47 | ``` 48 | 49 | ### Typescript usage 50 | 51 | ```typescript 52 | import { svgPathBbox } from "svg-path-bbox"; 53 | import type { BBox } from "svg-path-bbox"; 54 | 55 | const cases: [string, BBox][] = [["M0 0H3V6Z", [0, 0, 3, 6]]]; 56 | console.log(svgPathBbox(cases[0])); 57 | ``` 58 | 59 | ### Reference 60 | 61 | # **svgPathBbox**(d : _string_ | typeof import('svgpath')) ⇒ [minX: _number_, minY: _number_, maxX: _number_, maxY: _number_] 62 | 63 | Computes the bounding box of SVG path following the [SVG 1.1 specification](https://www.w3.org/TR/SVG/paths.html). 64 | 65 | - **d** (_string_ | typeof import('svgpath')) SVG path. Can be a string or a `SvgPath` interface from [svgpath]. 66 | 67 | ## Thanks to 68 | 69 | - [simple-icons/simple-icons](https://github.com/simple-icons/simple-icons) for reference dataset. 70 | - [kpym/SVGPathy](https://github.com/kpym/SVGPathy) for reference implementation. 71 | - [icons8/svg-path-bounding-box](https://github.com/icons8/svg-path-bounding-box) because [their bug](https://github.com/icons8/svg-path-bounding-box/issues/3) has been the source of this library. 72 | - [mathandy/svgpathtools](https://github.com/mathandy/svgpathtools/) for reference implementation to compare with. 73 | - [svgpath](https://www.npmjs.com/package/svgpath) as is used internally by svg-path-bbox to parse and transform paths before compute bounding boxes. 74 | 75 | [npm-link]: https://www.npmjs.com/package/svg-path-bbox 76 | [npm-version-image]: https://img.shields.io/npm/v/svg-path-bbox 77 | [tests-image]: https://img.shields.io/github/actions/workflow/status/mondeja/svg-path-bbox/ci.yml?branch=master&logo=github&label=tests 78 | [tests-link]: https://github.com/mondeja/svg-path-bbox/actions?query=workflow%3ATest 79 | [coveralls-image]: https://coveralls.io/repos/github/mondeja/svg-path-bbox/badge.svg?branch=master 80 | [coveralls-link]: https://coveralls.io/github/mondeja/svg-path-bbox?branch=master 81 | [license-image]: https://img.shields.io/npm/l/svg-path-bbox?color=brightgreen 82 | [license-link]: https://github.com/mondeja/svg-path-bbox/blob/master/LICENSE 83 | [npm-versions-image]: https://img.shields.io/node/v/svg-path-bbox 84 | [svgpath]: https://www.npmjs.com/package/svgpath 85 | -------------------------------------------------------------------------------- /dist/es2015/index.mjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // WARNING: This file is autogenerated, edit lib/index.template.ts 3 | import SvgPath from "svgpath"; 4 | // Precision for consider cubic polynom as quadratic one 5 | const CBEZIER_MINMAX_EPSILON = 0.00000001; 6 | // https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L89 7 | function minmaxQ(A) { 8 | const min = Math.min(A[0], A[2]), max = Math.max(A[0], A[2]); 9 | if (A[1] >= A[0] ? A[2] >= A[1] : A[2] <= A[1]) { 10 | // if no extremum in ]0,1[ 11 | return [min, max]; 12 | } 13 | // check if the extremum E is min or max 14 | const E = (A[0] * A[2] - A[1] * A[1]) / (A[0] - 2 * A[1] + A[2]); 15 | return E < min ? [E, max] : [min, E]; 16 | } 17 | // https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L127 18 | function minmaxC(A) { 19 | const K = A[0] - 3 * A[1] + 3 * A[2] - A[3]; 20 | // if the polynomial is (almost) quadratic and not cubic 21 | if (Math.abs(K) < CBEZIER_MINMAX_EPSILON) { 22 | if (A[0] === A[3] && A[0] === A[1]) { 23 | // no curve, point targeting same location 24 | return [A[0], A[3]]; 25 | } 26 | return minmaxQ([ 27 | A[0], 28 | -0.5 * A[0] + 1.5 * A[1], 29 | A[0] - 3 * A[1] + 3 * A[2], 30 | ]); 31 | } 32 | // the reduced discriminant of the derivative 33 | const T = -A[0] * A[2] + 34 | A[0] * A[3] - 35 | A[1] * A[2] - 36 | A[1] * A[3] + 37 | A[1] * A[1] + 38 | A[2] * A[2]; 39 | // if the polynomial is monotone in [0,1] 40 | if (T <= 0) { 41 | return [Math.min(A[0], A[3]), Math.max(A[0], A[3])]; 42 | } 43 | const S = Math.sqrt(T); 44 | // potential extrema 45 | let min = Math.min(A[0], A[3]), max = Math.max(A[0], A[3]); 46 | const L = A[0] - 2 * A[1] + A[2]; 47 | // check local extrema 48 | for (let R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) { 49 | if (R > 0 && R < 1) { 50 | // if the extrema is for R in [0,1] 51 | const Q = A[0] * (1 - R) * (1 - R) * (1 - R) + 52 | A[1] * 3 * (1 - R) * (1 - R) * R + 53 | A[2] * 3 * (1 - R) * R * R + 54 | A[3] * R * R * R; 55 | if (Q < min) { 56 | min = Q; 57 | } 58 | if (Q > max) { 59 | max = Q; 60 | } 61 | } 62 | } 63 | return [min, max]; 64 | } 65 | /** 66 | * Compute bounding boxes of SVG paths. 67 | * @param {String | typeof SvgPath} d SVG path for which their bounding box will be computed. 68 | * @returns {BBox} 69 | */ 70 | export function svgPathBbox(d) { 71 | const min = [Infinity, Infinity], max = [-Infinity, -Infinity]; 72 | SvgPath.from(d) 73 | .abs() 74 | .unarc() 75 | .unshort() 76 | .iterate((seg, _, x, y) => { 77 | switch (seg[0]) { 78 | // The next cases are ordered based on simple-icons data 79 | case "M": { 80 | if (min[0] > seg[1]) { 81 | min[0] = seg[1]; 82 | } 83 | if (min[1] > seg[2]) { 84 | min[1] = seg[2]; 85 | } 86 | if (max[0] < seg[1]) { 87 | max[0] = seg[1]; 88 | } 89 | if (max[1] < seg[2]) { 90 | max[1] = seg[2]; 91 | } 92 | break; 93 | } 94 | case "H": { 95 | if (min[0] > seg[1]) { 96 | min[0] = seg[1]; 97 | } 98 | if (max[0] < seg[1]) { 99 | max[0] = seg[1]; 100 | } 101 | break; 102 | } 103 | case "V": { 104 | if (min[1] > seg[1]) { 105 | min[1] = seg[1]; 106 | } 107 | if (max[1] < seg[1]) { 108 | max[1] = seg[1]; 109 | } 110 | break; 111 | } 112 | case "L": { 113 | if (min[0] > seg[1]) { 114 | min[0] = seg[1]; 115 | } 116 | if (min[1] > seg[2]) { 117 | min[1] = seg[2]; 118 | } 119 | if (max[0] < seg[1]) { 120 | max[0] = seg[1]; 121 | } 122 | if (max[1] < seg[2]) { 123 | max[1] = seg[2]; 124 | } 125 | break; 126 | } 127 | case "C": { 128 | const cxMinMax = minmaxC([x, seg[1], seg[3], seg[5]]); 129 | if (min[0] > cxMinMax[0]) { 130 | min[0] = cxMinMax[0]; 131 | } 132 | if (max[0] < cxMinMax[1]) { 133 | max[0] = cxMinMax[1]; 134 | } 135 | const cyMinMax = minmaxC([y, seg[2], seg[4], seg[6]]); 136 | if (min[1] > cyMinMax[0]) { 137 | min[1] = cyMinMax[0]; 138 | } 139 | if (max[1] < cyMinMax[1]) { 140 | max[1] = cyMinMax[1]; 141 | } 142 | break; 143 | } 144 | case "Q": { 145 | const qxMinMax = minmaxQ([x, seg[1], seg[3]]); 146 | if (min[0] > qxMinMax[0]) { 147 | min[0] = qxMinMax[0]; 148 | } 149 | if (max[0] < qxMinMax[1]) { 150 | max[0] = qxMinMax[1]; 151 | } 152 | const qyMinMax = minmaxQ([y, seg[2], seg[4]]); 153 | if (min[1] > qyMinMax[0]) { 154 | min[1] = qyMinMax[0]; 155 | } 156 | if (max[1] < qyMinMax[1]) { 157 | max[1] = qyMinMax[1]; 158 | } 159 | break; 160 | } 161 | } 162 | }, true); 163 | return [min[0], min[1], max[0], max[1]]; 164 | } 165 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.svgPathBbox = svgPathBbox; 4 | // WARNING: This file is autogenerated, edit lib/index.template.ts 5 | var SvgPath = require("svgpath"); 6 | // Precision for consider cubic polynom as quadratic one 7 | var CBEZIER_MINMAX_EPSILON = 0.00000001; 8 | // https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L89 9 | function minmaxQ(A) { 10 | var min = Math.min(A[0], A[2]), max = Math.max(A[0], A[2]); 11 | if (A[1] >= A[0] ? A[2] >= A[1] : A[2] <= A[1]) { 12 | // if no extremum in ]0,1[ 13 | return [min, max]; 14 | } 15 | // check if the extremum E is min or max 16 | var E = (A[0] * A[2] - A[1] * A[1]) / (A[0] - 2 * A[1] + A[2]); 17 | return E < min ? [E, max] : [min, E]; 18 | } 19 | // https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L127 20 | function minmaxC(A) { 21 | var K = A[0] - 3 * A[1] + 3 * A[2] - A[3]; 22 | // if the polynomial is (almost) quadratic and not cubic 23 | if (Math.abs(K) < CBEZIER_MINMAX_EPSILON) { 24 | if (A[0] === A[3] && A[0] === A[1]) { 25 | // no curve, point targeting same location 26 | return [A[0], A[3]]; 27 | } 28 | return minmaxQ([ 29 | A[0], 30 | -0.5 * A[0] + 1.5 * A[1], 31 | A[0] - 3 * A[1] + 3 * A[2], 32 | ]); 33 | } 34 | // the reduced discriminant of the derivative 35 | var T = -A[0] * A[2] + 36 | A[0] * A[3] - 37 | A[1] * A[2] - 38 | A[1] * A[3] + 39 | A[1] * A[1] + 40 | A[2] * A[2]; 41 | // if the polynomial is monotone in [0,1] 42 | if (T <= 0) { 43 | return [Math.min(A[0], A[3]), Math.max(A[0], A[3])]; 44 | } 45 | var S = Math.sqrt(T); 46 | // potential extrema 47 | var min = Math.min(A[0], A[3]), max = Math.max(A[0], A[3]); 48 | var L = A[0] - 2 * A[1] + A[2]; 49 | // check local extrema 50 | for (var R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) { 51 | if (R > 0 && R < 1) { 52 | // if the extrema is for R in [0,1] 53 | var Q = A[0] * (1 - R) * (1 - R) * (1 - R) + 54 | A[1] * 3 * (1 - R) * (1 - R) * R + 55 | A[2] * 3 * (1 - R) * R * R + 56 | A[3] * R * R * R; 57 | if (Q < min) { 58 | min = Q; 59 | } 60 | if (Q > max) { 61 | max = Q; 62 | } 63 | } 64 | } 65 | return [min, max]; 66 | } 67 | /** 68 | * Compute bounding boxes of SVG paths. 69 | * @param {String | typeof SvgPath} d SVG path for which their bounding box will be computed. 70 | * @returns {BBox} 71 | */ 72 | function svgPathBbox(d) { 73 | var min = [Infinity, Infinity], max = [-Infinity, -Infinity]; 74 | SvgPath.from(d) 75 | .abs() 76 | .unarc() 77 | .unshort() 78 | .iterate(function (seg, _, x, y) { 79 | switch (seg[0]) { 80 | // The next cases are ordered based on simple-icons data 81 | case "M": { 82 | if (min[0] > seg[1]) { 83 | min[0] = seg[1]; 84 | } 85 | if (min[1] > seg[2]) { 86 | min[1] = seg[2]; 87 | } 88 | if (max[0] < seg[1]) { 89 | max[0] = seg[1]; 90 | } 91 | if (max[1] < seg[2]) { 92 | max[1] = seg[2]; 93 | } 94 | break; 95 | } 96 | case "H": { 97 | if (min[0] > seg[1]) { 98 | min[0] = seg[1]; 99 | } 100 | if (max[0] < seg[1]) { 101 | max[0] = seg[1]; 102 | } 103 | break; 104 | } 105 | case "V": { 106 | if (min[1] > seg[1]) { 107 | min[1] = seg[1]; 108 | } 109 | if (max[1] < seg[1]) { 110 | max[1] = seg[1]; 111 | } 112 | break; 113 | } 114 | case "L": { 115 | if (min[0] > seg[1]) { 116 | min[0] = seg[1]; 117 | } 118 | if (min[1] > seg[2]) { 119 | min[1] = seg[2]; 120 | } 121 | if (max[0] < seg[1]) { 122 | max[0] = seg[1]; 123 | } 124 | if (max[1] < seg[2]) { 125 | max[1] = seg[2]; 126 | } 127 | break; 128 | } 129 | case "C": { 130 | var cxMinMax = minmaxC([x, seg[1], seg[3], seg[5]]); 131 | if (min[0] > cxMinMax[0]) { 132 | min[0] = cxMinMax[0]; 133 | } 134 | if (max[0] < cxMinMax[1]) { 135 | max[0] = cxMinMax[1]; 136 | } 137 | var cyMinMax = minmaxC([y, seg[2], seg[4], seg[6]]); 138 | if (min[1] > cyMinMax[0]) { 139 | min[1] = cyMinMax[0]; 140 | } 141 | if (max[1] < cyMinMax[1]) { 142 | max[1] = cyMinMax[1]; 143 | } 144 | break; 145 | } 146 | case "Q": { 147 | var qxMinMax = minmaxQ([x, seg[1], seg[3]]); 148 | if (min[0] > qxMinMax[0]) { 149 | min[0] = qxMinMax[0]; 150 | } 151 | if (max[0] < qxMinMax[1]) { 152 | max[0] = qxMinMax[1]; 153 | } 154 | var qyMinMax = minmaxQ([y, seg[2], seg[4]]); 155 | if (min[1] > qyMinMax[0]) { 156 | min[1] = qyMinMax[0]; 157 | } 158 | if (max[1] < qyMinMax[1]) { 159 | max[1] = qyMinMax[1]; 160 | } 161 | break; 162 | } 163 | } 164 | }, true); 165 | return [min[0], min[1], max[0], max[1]]; 166 | } 167 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | import js from "@eslint/js"; 5 | import tsParser from "@typescript-eslint/parser"; 6 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: path.dirname(fileURLToPath(import.meta.url)), 10 | recommendedConfig: js.configs.recommended, 11 | allConfig: js.configs.all, 12 | }); 13 | 14 | export default [ 15 | ...compat.extends( 16 | "eslint:recommended", 17 | "plugin:@typescript-eslint/recommended" 18 | ), 19 | { 20 | plugins: { 21 | "@typescript-eslint": typescriptEslint, 22 | }, 23 | 24 | languageOptions: { 25 | globals: {}, 26 | parser: tsParser, 27 | ecmaVersion: 2020, 28 | sourceType: "module", 29 | 30 | parserOptions: { 31 | requireConfigFile: false, 32 | }, 33 | }, 34 | 35 | rules: { 36 | "no-console": [ 37 | "error", 38 | { 39 | allow: ["time", "timeEnd"], 40 | }, 41 | ], 42 | }, 43 | }, 44 | ]; 45 | -------------------------------------------------------------------------------- /examples/common.cjs: -------------------------------------------------------------------------------- 1 | const util = require("util") 2 | const { svgPathBbox } = require("../dist/index.js"); 3 | 4 | const bbox = svgPathBbox("M0 0H3V6Z"); 5 | process.stdout.write(`${util.inspect(bbox)}\n`) 6 | -------------------------------------------------------------------------------- /examples/esm.mjs: -------------------------------------------------------------------------------- 1 | import { inspect } from "node:util"; 2 | import process from "node:process"; 3 | import { svgPathBbox } from "../dist/es2015/index.mjs"; 4 | 5 | const bbox = svgPathBbox("M0 0H3V6Z"); 6 | process.stdout.write(`${inspect(bbox)}\n`) 7 | -------------------------------------------------------------------------------- /examples/typescript.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from "node:util"; 2 | import * as process from "node:process"; 3 | import { svgPathBbox } from "../src"; 4 | import type { BBox } from "../src"; 5 | 6 | const cases: [string, BBox][] = [["M0 0H3V6Z", [0, 0, 3, 6]]]; 7 | 8 | for (const [path, expectedBbox] of cases) { 9 | const bbox = svgPathBbox(path); 10 | if (JSON.stringify(bbox) !== JSON.stringify(expectedBbox)) { 11 | process.stderr.write(`UNEXPECTED BBOX: ${inspect(bbox)}\n`); 12 | process.exit(1); 13 | } else { 14 | process.stdout.write(`${inspect(bbox)}\n`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jest-puppeteer.config.mjs: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | 3 | const PORT = 8080; 4 | 5 | export default { 6 | launch: { 7 | headless: process.env.TEST_HEADLESS !== "false", 8 | args: ['--no-sandbox', '--disable-setuid-sandbox'], 9 | }, 10 | server: { 11 | command: `anywhere -s -p ${PORT} -d tests/browser`, 12 | port: PORT, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: "jest-puppeteer", 3 | transform: { "^.+\\.ts?$": "ts-jest" }, 4 | collectCoverage: true, 5 | coverageDirectory: "/tests/coverage", 6 | collectCoverageFrom: ["src/*.ts"], 7 | }; 8 | -------------------------------------------------------------------------------- /lib/cases.ts: -------------------------------------------------------------------------------- 1 | const ML = `{ 2 | if (min[0] > seg[1]) { 3 | min[0] = seg[1]; 4 | } 5 | if (min[1] > seg[2]) { 6 | min[1] = seg[2]; 7 | } 8 | if (max[0] < seg[1]) { 9 | max[0] = seg[1]; 10 | } 11 | if (max[1] < seg[2]) { 12 | max[1] = seg[2]; 13 | } 14 | break; 15 | }`; 16 | 17 | export default { 18 | M: ML, 19 | L: ML, 20 | H: `{ 21 | if (min[0] > seg[1]) { 22 | min[0] = seg[1]; 23 | } 24 | if (max[0] < seg[1]) { 25 | max[0] = seg[1]; 26 | } 27 | break; 28 | }`, 29 | V: `{ 30 | if (min[1] > seg[1]) { 31 | min[1] = seg[1]; 32 | } 33 | if (max[1] < seg[1]) { 34 | max[1] = seg[1]; 35 | } 36 | break; 37 | }`, 38 | C: `{ 39 | const cxMinMax = minmaxC([x, seg[1], seg[3], seg[5]]); 40 | if (min[0] > cxMinMax[0]) { 41 | min[0] = cxMinMax[0]; 42 | } 43 | if (max[0] < cxMinMax[1]) { 44 | max[0] = cxMinMax[1]; 45 | } 46 | 47 | const cyMinMax = minmaxC([y, seg[2], seg[4], seg[6]]); 48 | if (min[1] > cyMinMax[0]) { 49 | min[1] = cyMinMax[0]; 50 | } 51 | if (max[1] < cyMinMax[1]) { 52 | max[1] = cyMinMax[1]; 53 | } 54 | break; 55 | }`, 56 | Q: `{ 57 | const qxMinMax = minmaxQ([x, seg[1], seg[3]]); 58 | if (min[0] > qxMinMax[0]) { 59 | min[0] = qxMinMax[0]; 60 | } 61 | if (max[0] < qxMinMax[1]) { 62 | max[0] = qxMinMax[1]; 63 | } 64 | 65 | const qyMinMax = minmaxQ([y, seg[2], seg[4]]); 66 | if (min[1] > qyMinMax[0]) { 67 | min[1] = qyMinMax[0]; 68 | } 69 | if (max[1] < qyMinMax[1]) { 70 | max[1] = qyMinMax[1]; 71 | } 72 | break; 73 | }`, 74 | } 75 | -------------------------------------------------------------------------------- /lib/index.template.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import * as SvgPath from "svgpath"; 4 | 5 | type minMax = [min: number, max: number]; 6 | export type BBox = [minX: number, minY: number, maxX: number, maxY: number]; 7 | 8 | // Precision for consider cubic polynom as quadratic one 9 | const CBEZIER_MINMAX_EPSILON = 0.00000001; 10 | 11 | // https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L89 12 | function minmaxQ(A: [number, number, number]): minMax { 13 | const min = Math.min(A[0], A[2]), 14 | max = Math.max(A[0], A[2]); 15 | 16 | if (A[1] >= A[0] ? A[2] >= A[1] : A[2] <= A[1]) { 17 | // if no extremum in ]0,1[ 18 | return [min, max]; 19 | } 20 | 21 | // check if the extremum E is min or max 22 | const E = (A[0] * A[2] - A[1] * A[1]) / (A[0] - 2 * A[1] + A[2]); 23 | return E < min ? [E, max] : [min, E]; 24 | } 25 | 26 | // https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L127 27 | function minmaxC(A: [number, number, number, number]): minMax { 28 | const K = A[0] - 3 * A[1] + 3 * A[2] - A[3]; 29 | 30 | // if the polynomial is (almost) quadratic and not cubic 31 | if (Math.abs(K) < CBEZIER_MINMAX_EPSILON) { 32 | if (A[0] === A[3] && A[0] === A[1]) { 33 | // no curve, point targeting same location 34 | return [A[0], A[3]]; 35 | } 36 | 37 | return minmaxQ([ 38 | A[0], 39 | -0.5 * A[0] + 1.5 * A[1], 40 | A[0] - 3 * A[1] + 3 * A[2], 41 | ]); 42 | } 43 | 44 | // the reduced discriminant of the derivative 45 | const T = 46 | -A[0] * A[2] + 47 | A[0] * A[3] - 48 | A[1] * A[2] - 49 | A[1] * A[3] + 50 | A[1] * A[1] + 51 | A[2] * A[2]; 52 | 53 | // if the polynomial is monotone in [0,1] 54 | if (T <= 0) { 55 | return [Math.min(A[0], A[3]), Math.max(A[0], A[3])]; 56 | } 57 | const S = Math.sqrt(T); 58 | 59 | // potential extrema 60 | let min = Math.min(A[0], A[3]), 61 | max = Math.max(A[0], A[3]); 62 | 63 | const L = A[0] - 2 * A[1] + A[2]; 64 | // check local extrema 65 | for (let R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) { 66 | if (R > 0 && R < 1) { 67 | // if the extrema is for R in [0,1] 68 | const Q = 69 | A[0] * (1 - R) * (1 - R) * (1 - R) + 70 | A[1] * 3 * (1 - R) * (1 - R) * R + 71 | A[2] * 3 * (1 - R) * R * R + 72 | A[3] * R * R * R; 73 | if (Q < min) { 74 | min = Q; 75 | } 76 | if (Q > max) { 77 | max = Q; 78 | } 79 | } 80 | } 81 | 82 | return [min, max]; 83 | } 84 | 85 | /** 86 | * Compute bounding boxes of SVG paths. 87 | * @param {String | typeof SvgPath} d SVG path for which their bounding box will be computed. 88 | * @returns {BBox} 89 | */ 90 | export function svgPathBbox(d: string | typeof SvgPath): BBox { 91 | const min = [Infinity, Infinity], 92 | max = [-Infinity, -Infinity]; 93 | SvgPath.from(d) 94 | .abs() 95 | .unarc() 96 | .unshort() 97 | .iterate((seg, _, x, y) => { 98 | switch (seg[0]) { 99 | /*cases*/ 100 | } 101 | }, true); 102 | return [min[0], min[1], max[0], max[1]]; 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg-path-bbox", 3 | "version": "2.1.0", 4 | "description": "Compute bounding boxes of SVG paths.", 5 | "keywords": [ 6 | "svg", 7 | "path", 8 | "bbox" 9 | ], 10 | "main": "./dist/index.js", 11 | "module": "./dist/es2015/index.mjs", 12 | "types": "./src/types.d.ts", 13 | "exports": { 14 | ".": { 15 | "import": { 16 | "types": "./src/types.d.mts", 17 | "default": "./dist/es2015/index.mjs" 18 | }, 19 | "require": { 20 | "types": "./src/types.d.ts", 21 | "default": "./dist/index.js" 22 | } 23 | } 24 | }, 25 | "bin": { 26 | "svg-path-bbox": "src/cli.cjs" 27 | }, 28 | "scripts": { 29 | "coveralls": "cat ./tests/coverage/lcov.info | coveralls", 30 | "prebuild": "npm run build:index && npm run lint:fix && npm run dist:prepare", 31 | "build": "npm run build:ts && npm run build:ts:esm", 32 | "build:index": "tsx scripts/build/index.ts", 33 | "build:ts": "tsc", 34 | "build:ts:esm": "tsc --module es2015 --target es2015 --outDir dist/es2015 && tsx scripts/build/patch-esm-dist.ts", 35 | "examples": "npm run example:cjs && npm run example:esm && npm run example:ts", 36 | "example:cjs": "node examples/common.cjs", 37 | "example:esm": "node examples/esm.mjs", 38 | "example:ts": "tsx examples/typescript.ts", 39 | "lint": "eslint --max-warnings 0 src/index.ts tests scripts examples/typescript.ts", 40 | "lint:fix": "npm run lint -- --fix", 41 | "dist:prepare": "node -e \"f=require('fs/promises');f.rm('dist',{recursive:true,force:true}).then(()=>f.mkdir('dist'))\"", 42 | "test": "jest", 43 | "prepare": "husky", 44 | "bench:si": "tsx scripts/simple-icons-benchmark.cts" 45 | }, 46 | "author": { 47 | "name": "Álvaro Mondéjar Rubio", 48 | "email": "mondejar1994@gmail.com" 49 | }, 50 | "contributors": [], 51 | "license": "BSD-3-Clause", 52 | "dependencies": { 53 | "svgpath": "^2.6.0" 54 | }, 55 | "devDependencies": { 56 | "@types/jest": "29.5.14", 57 | "@types/jest-environment-puppeteer": "5.0.6", 58 | "@types/node": "22.13.8", 59 | "@types/puppeteer": "5.4.7", 60 | "@types/svg-path-bounding-box": "1.0.2", 61 | "@typescript-eslint/eslint-plugin": "8.25.0", 62 | "@typescript-eslint/parser": "8.25.0", 63 | "anywhere": "1.6.0", 64 | "coveralls": "3.1.1", 65 | "eslint": "9.21.0", 66 | "husky": "9.1.7", 67 | "jest": "29.7.0", 68 | "jest-puppeteer": "11.0.0", 69 | "puppeteer": "24.3.0", 70 | "simple-icons": "14.9.0", 71 | "svg-path-bounding-box": "1.0.4", 72 | "ts-jest": "29.2.6", 73 | "tsx": "4.19.3", 74 | "typescript": "5.6.3" 75 | }, 76 | "repository": { 77 | "type": "git", 78 | "url": "git+https://github.com/mondeja/svg-path-bbox.git" 79 | }, 80 | "bugs": { 81 | "url": "https://github.com/mondeja/svg-path-bbox/issues", 82 | "email": "mondejar1994@gmail.com" 83 | }, 84 | "files": [ 85 | "src", 86 | "dist" 87 | ], 88 | "engines": { 89 | "node": ">=6.17.1" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /scripts/benchmark.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from "node:util"; 2 | 3 | import { svgPathBbox } from "../src"; 4 | import svgPathBoundingBox from "svg-path-bounding-box"; 5 | import type { BBox } from "../src"; 6 | import type { BoundingBoxView } from "svg-path-bounding-box"; 7 | 8 | type LibraryAdapter = { 9 | func: typeof svgPathBbox | typeof svgPathBoundingBox; 10 | resultParser?: (result: BoundingBoxView) => BBox; 11 | }; 12 | 13 | const LIBRARIES: { 14 | [key: string]: LibraryAdapter; 15 | } = { 16 | "svg-path-bbox": { 17 | func: svgPathBbox, 18 | }, 19 | "svg-path-bounding-box": { 20 | func: svgPathBoundingBox, 21 | resultParser(result) { 22 | return [result.minX, result.minY, result.maxX, result.maxY]; 23 | }, 24 | }, 25 | }; 26 | 27 | const PATHS = [ 28 | "M0 0L10 10 20 0Z", 29 | "M100,250 c0,-150 300,-150 300,0", 30 | "M 10 20 C 50 -15 90 45 10 80 L 60 80", 31 | ]; 32 | 33 | const EPOCHS = [10000, 100000]; 34 | 35 | const runLibrariesBenchmark = ( 36 | paths: [string | null, string][], 37 | epochs: number[] 38 | ) => { 39 | for (const [key, path] of paths) { 40 | for (const e of epochs) { 41 | let pathType = path.replace(/[0-9\-.\s,]/g, ""); 42 | pathType = 43 | pathType.length > 25 ? `${pathType.substring(0, 25)}...` : pathType; 44 | const pathSummary = 45 | path.length > 25 ? `${path.substring(0, 25)}...` : path; 46 | 47 | if (key) { 48 | process.stdout.write(`${key} - `); 49 | } 50 | 51 | process.stdout.write(`${pathSummary} (type ${pathType}) [${e} epochs]\n`); 52 | for (const library in LIBRARIES) { 53 | console.time(library); 54 | const func = LIBRARIES[library]["func"]; 55 | let _errorRaised = false, 56 | result; 57 | for (let r = 0; r < e; r++) { 58 | try { 59 | result = func(path); 60 | } catch (err) { 61 | if (!_errorRaised) { 62 | process.stderr.write( 63 | `Error computing bbox with library ${library}:\n` 64 | ); 65 | process.stderr.write(`${err}\n`); 66 | } 67 | _errorRaised = true; 68 | } 69 | } 70 | process.stdout.write(" "); 71 | console.timeEnd(library); 72 | 73 | const resultParser = LIBRARIES[library].resultParser; 74 | if (resultParser !== undefined) { 75 | result = resultParser(result as BoundingBoxView); 76 | } 77 | if (result) { 78 | process.stdout.write( 79 | ` + result: ${inspect(result, { colors: true })}\n` 80 | ); 81 | } 82 | } 83 | process.stdout.write("\n"); 84 | } 85 | } 86 | }; 87 | 88 | export default runLibrariesBenchmark; 89 | 90 | if (require.main === module) { 91 | runLibrariesBenchmark( 92 | PATHS.map((path) => [null, path]), 93 | EPOCHS 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /scripts/build/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import cases from "../../lib/cases"; 3 | import { getSimpleIconsSegmentsStats } from "../get-simple-icons-segments-stats"; 4 | 5 | function buildIndexTs(indexTemplateTs: string): string { 6 | let indexTs = ""; 7 | const [beforeCases, afterCases] = indexTemplateTs.split("/*cases*/"); 8 | indexTs += beforeCases.replace( 9 | '"use strict";', 10 | '"use strict";\n\n// WARNING: This file is autogenerated, edit lib/index.template.ts' 11 | ); 12 | indexTs += 13 | "// The next cases are ordered based on simple-icons data\n "; 14 | const siSegmentsStats = getSimpleIconsSegmentsStats().map(([seg]) => seg); 15 | for (const segment of siSegmentsStats) { 16 | const caseBranch = cases[segment as keyof typeof cases]; 17 | indexTs += `case "${segment}": ${caseBranch}\n `; 18 | } 19 | indexTs = indexTs.trimEnd(); 20 | indexTs += afterCases; 21 | return indexTs; 22 | } 23 | 24 | if (require.main === module) { 25 | const indexTemplateTs = fs.readFileSync("lib/index.template.ts", "utf8"); 26 | const indexTs = buildIndexTs(indexTemplateTs); 27 | fs.writeFileSync("src/index.ts", indexTs); 28 | } 29 | -------------------------------------------------------------------------------- /scripts/build/patch-esm-dist.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | 3 | // .js -> .mjs 4 | fs.renameSync( 5 | "dist/es2015/index.js", 6 | "dist/es2015/index.mjs" 7 | ); 8 | 9 | // import * as SvgPath from "svgpath"; -> import SvgPath from "svgpath"; 10 | fs.writeFileSync( 11 | "dist/es2015/index.mjs", 12 | fs.readFileSync("dist/es2015/index.mjs", "utf-8").replace( 13 | 'import * as SvgPath from "svgpath";', 14 | 'import SvgPath from "svgpath";' 15 | ) 16 | ); 17 | -------------------------------------------------------------------------------- /scripts/get-simple-icons-segments-stats.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shows how many segments of each type are found in 3 | * simple-icons icons. 4 | */ 5 | 6 | import * as icons from "simple-icons"; 7 | 8 | export function getSimpleIconsSegmentsStats(): [string, number][] { 9 | const varNames = Object.keys(icons).slice(1); 10 | const segmentsStats = { 11 | M: 0, 12 | V: 0, 13 | H: 0, 14 | L: 0, 15 | C: 0, 16 | Q: 0, 17 | }; 18 | for (const varName of varNames) { 19 | const icon = icons[varName as keyof typeof icons]; 20 | for (const segment in segmentsStats) { 21 | segmentsStats[segment as keyof typeof segmentsStats] += 22 | icon.path.split(segment).length; 23 | } 24 | }; 25 | return Object.entries(segmentsStats).sort(([, a], [, b]) => b - a); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /scripts/show-simple-icons-segments-stats.ts: -------------------------------------------------------------------------------- 1 | import { getSimpleIconsSegmentsStats } from "./get-simple-icons-segments-stats"; 2 | 3 | const stats = getSimpleIconsSegmentsStats(); 4 | for (const [seg, occ] of stats) { 5 | process.stdout.write(`${seg}: ${occ}\n`); 6 | } 7 | -------------------------------------------------------------------------------- /scripts/simple-icons-benchmark.cts: -------------------------------------------------------------------------------- 1 | import icons from "simple-icons"; 2 | import type { SimpleIcon } from "simple-icons"; 3 | 4 | import runLibrariesBenchmark from "./benchmark"; 5 | 6 | const EPOCHS = [5000]; 7 | const MAX_ICONS = 5; 8 | const FILTER = (icon: SimpleIcon | undefined) => icon !== undefined; 9 | 10 | const titlePaths: [string, string][] = []; 11 | for (const icon of Object.values(icons)) { 12 | if (!FILTER(icon)) { 13 | continue; 14 | } 15 | titlePaths.push([icon.title, icon.path]); 16 | if (titlePaths.length >= MAX_ICONS) { 17 | break; 18 | } 19 | } 20 | 21 | runLibrariesBenchmark(titlePaths, EPOCHS); 22 | 23 | -------------------------------------------------------------------------------- /src/cli.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | /* eslint no-process-exit: 0 */ 5 | /* eslint global-require: 0 */ 6 | 7 | function help() { 8 | const packageJson = require("../package.json"); 9 | 10 | return `${packageJson.description} 11 | Output is redirected to STDOUT. 12 | 13 | Usage: 14 | ${packageJson.name} [-h] [-v] [path] [path] ... 15 | 16 | Options: 17 | -h, --help Display this help text and exit. 18 | -v, --version Show this program version number (${packageJson.version}).`; 19 | } 20 | 21 | if (require.main === module) { 22 | let sliceN = 1; 23 | if ( 24 | process.argv.indexOf(module.filename) > -1 || 25 | require("path").basename(process.argv[1]) === "svg-path-bbox" || 26 | process.argv.indexOf(module.filename.slice(0, -4)) > -1 // rstrip '.cjs' 27 | ) { 28 | sliceN = 2; 29 | } 30 | const args = process.argv.slice(sliceN, process.argv.length); 31 | 32 | if (args.length === 0) { 33 | console.error( 34 | "You must pass SVG paths enclosed between quotes as parameters." 35 | ); 36 | process.exit(1); 37 | } else if (args.includes("--version") || args.includes("-v")) { 38 | console.log(require("../package.json").version); 39 | process.exit(1); 40 | } else if (args.includes("--help") || args.includes("-h")) { 41 | console.error(help()); 42 | process.exit(1); 43 | } 44 | 45 | const { svgPathBbox } = require("../dist/index.js"); 46 | for (let a = 0; a < args.length; a++) { 47 | console.log(svgPathBbox(args[a]).join(" ")); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // WARNING: This file is autogenerated, edit lib/index.template.ts 4 | 5 | import * as SvgPath from "svgpath"; 6 | 7 | type minMax = [min: number, max: number]; 8 | export type BBox = [minX: number, minY: number, maxX: number, maxY: number]; 9 | 10 | // Precision for consider cubic polynom as quadratic one 11 | const CBEZIER_MINMAX_EPSILON = 0.00000001; 12 | 13 | // https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L89 14 | function minmaxQ(A: [number, number, number]): minMax { 15 | const min = Math.min(A[0], A[2]), 16 | max = Math.max(A[0], A[2]); 17 | 18 | if (A[1] >= A[0] ? A[2] >= A[1] : A[2] <= A[1]) { 19 | // if no extremum in ]0,1[ 20 | return [min, max]; 21 | } 22 | 23 | // check if the extremum E is min or max 24 | const E = (A[0] * A[2] - A[1] * A[1]) / (A[0] - 2 * A[1] + A[2]); 25 | return E < min ? [E, max] : [min, E]; 26 | } 27 | 28 | // https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L127 29 | function minmaxC(A: [number, number, number, number]): minMax { 30 | const K = A[0] - 3 * A[1] + 3 * A[2] - A[3]; 31 | 32 | // if the polynomial is (almost) quadratic and not cubic 33 | if (Math.abs(K) < CBEZIER_MINMAX_EPSILON) { 34 | if (A[0] === A[3] && A[0] === A[1]) { 35 | // no curve, point targeting same location 36 | return [A[0], A[3]]; 37 | } 38 | 39 | return minmaxQ([ 40 | A[0], 41 | -0.5 * A[0] + 1.5 * A[1], 42 | A[0] - 3 * A[1] + 3 * A[2], 43 | ]); 44 | } 45 | 46 | // the reduced discriminant of the derivative 47 | const T = 48 | -A[0] * A[2] + 49 | A[0] * A[3] - 50 | A[1] * A[2] - 51 | A[1] * A[3] + 52 | A[1] * A[1] + 53 | A[2] * A[2]; 54 | 55 | // if the polynomial is monotone in [0,1] 56 | if (T <= 0) { 57 | return [Math.min(A[0], A[3]), Math.max(A[0], A[3])]; 58 | } 59 | const S = Math.sqrt(T); 60 | 61 | // potential extrema 62 | let min = Math.min(A[0], A[3]), 63 | max = Math.max(A[0], A[3]); 64 | 65 | const L = A[0] - 2 * A[1] + A[2]; 66 | // check local extrema 67 | for (let R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) { 68 | if (R > 0 && R < 1) { 69 | // if the extrema is for R in [0,1] 70 | const Q = 71 | A[0] * (1 - R) * (1 - R) * (1 - R) + 72 | A[1] * 3 * (1 - R) * (1 - R) * R + 73 | A[2] * 3 * (1 - R) * R * R + 74 | A[3] * R * R * R; 75 | if (Q < min) { 76 | min = Q; 77 | } 78 | if (Q > max) { 79 | max = Q; 80 | } 81 | } 82 | } 83 | 84 | return [min, max]; 85 | } 86 | 87 | /** 88 | * Compute bounding boxes of SVG paths. 89 | * @param {String | typeof SvgPath} d SVG path for which their bounding box will be computed. 90 | * @returns {BBox} 91 | */ 92 | export function svgPathBbox(d: string | typeof SvgPath): BBox { 93 | const min = [Infinity, Infinity], 94 | max = [-Infinity, -Infinity]; 95 | SvgPath.from(d) 96 | .abs() 97 | .unarc() 98 | .unshort() 99 | .iterate((seg, _, x, y) => { 100 | switch (seg[0]) { 101 | // The next cases are ordered based on simple-icons data 102 | case "M": { 103 | if (min[0] > seg[1]) { 104 | min[0] = seg[1]; 105 | } 106 | if (min[1] > seg[2]) { 107 | min[1] = seg[2]; 108 | } 109 | if (max[0] < seg[1]) { 110 | max[0] = seg[1]; 111 | } 112 | if (max[1] < seg[2]) { 113 | max[1] = seg[2]; 114 | } 115 | break; 116 | } 117 | case "H": { 118 | if (min[0] > seg[1]) { 119 | min[0] = seg[1]; 120 | } 121 | if (max[0] < seg[1]) { 122 | max[0] = seg[1]; 123 | } 124 | break; 125 | } 126 | case "V": { 127 | if (min[1] > seg[1]) { 128 | min[1] = seg[1]; 129 | } 130 | if (max[1] < seg[1]) { 131 | max[1] = seg[1]; 132 | } 133 | break; 134 | } 135 | case "L": { 136 | if (min[0] > seg[1]) { 137 | min[0] = seg[1]; 138 | } 139 | if (min[1] > seg[2]) { 140 | min[1] = seg[2]; 141 | } 142 | if (max[0] < seg[1]) { 143 | max[0] = seg[1]; 144 | } 145 | if (max[1] < seg[2]) { 146 | max[1] = seg[2]; 147 | } 148 | break; 149 | } 150 | case "C": { 151 | const cxMinMax = minmaxC([x, seg[1], seg[3], seg[5]]); 152 | if (min[0] > cxMinMax[0]) { 153 | min[0] = cxMinMax[0]; 154 | } 155 | if (max[0] < cxMinMax[1]) { 156 | max[0] = cxMinMax[1]; 157 | } 158 | 159 | const cyMinMax = minmaxC([y, seg[2], seg[4], seg[6]]); 160 | if (min[1] > cyMinMax[0]) { 161 | min[1] = cyMinMax[0]; 162 | } 163 | if (max[1] < cyMinMax[1]) { 164 | max[1] = cyMinMax[1]; 165 | } 166 | break; 167 | } 168 | case "Q": { 169 | const qxMinMax = minmaxQ([x, seg[1], seg[3]]); 170 | if (min[0] > qxMinMax[0]) { 171 | min[0] = qxMinMax[0]; 172 | } 173 | if (max[0] < qxMinMax[1]) { 174 | max[0] = qxMinMax[1]; 175 | } 176 | 177 | const qyMinMax = minmaxQ([y, seg[2], seg[4]]); 178 | if (min[1] > qyMinMax[0]) { 179 | min[1] = qyMinMax[0]; 180 | } 181 | if (max[1] < qyMinMax[1]) { 182 | max[1] = qyMinMax[1]; 183 | } 184 | break; 185 | } 186 | } 187 | }, true); 188 | return [min[0], min[1], max[0], max[1]]; 189 | } 190 | -------------------------------------------------------------------------------- /src/types.d.mts: -------------------------------------------------------------------------------- 1 | import type SvgPath from "svgpath"; 2 | export type BBox = [minX: number, minY: number, maxX: number, maxY: number]; 3 | 4 | /** 5 | * Compute bounding boxes of SVG paths. 6 | * @param {String | typeof SvgPath} d SVG path for which their bounding box will be computed. 7 | * @returns {BBox} 8 | */ 9 | export function svgPathBbox(d: string | typeof SvgPath): BBox; 10 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import type SvgPath from "svgpath"; 2 | export type BBox = [minX: number, minY: number, maxX: number, maxY: number]; 3 | 4 | /** 5 | * Compute bounding boxes of SVG paths. 6 | * @param {String | typeof SvgPath} d SVG path for which their bounding box will be computed. 7 | * @returns {BBox} 8 | */ 9 | export function svgPathBbox(d: string | typeof SvgPath): BBox; 10 | -------------------------------------------------------------------------------- /svg-path-bbox.svg: -------------------------------------------------------------------------------- 1 | x0, y0x1, y1 2 | -------------------------------------------------------------------------------- /tests/bbox.test.ts: -------------------------------------------------------------------------------- 1 | import { svgPathBbox } from "../src"; 2 | import * as SvgPath from "svgpath"; 3 | import { linealCases, pathologicalCases } from "./cases/bbox"; 4 | 5 | describe("svgPathBbox(d) [Lineal cases]", () => { 6 | test.each(linealCases)("svgPathBbox(%p) ⇢ %p", (d, bbox) => { 7 | expect(svgPathBbox(d)).toEqual(bbox); 8 | }); 9 | }); 10 | 11 | describe("svgPathBbox(SvgPath(d)) [Lineal cases]", () => { 12 | test.each(linealCases)("svgPathBbox(SvgPath(%p)) ⇢ %p", (d, bbox) => { 13 | expect(svgPathBbox(new SvgPath(d))).toEqual(bbox); 14 | }); 15 | }); 16 | 17 | describe("svgPathBbox(d) [Pathological cases]", () => { 18 | test.each(pathologicalCases)("svgPathBbox(%p) ⇢ %p", (d, bbox) => { 19 | expect(svgPathBbox(d)).toEqual(bbox); 20 | }); 21 | }); 22 | 23 | describe("svgPathBbox(SvgPath(d)) [Pathological cases]", () => { 24 | test.each(pathologicalCases)("svgPathBbox(SvgPath(%p)) ⇢ %p", (d, bbox) => { 25 | expect(svgPathBbox(new SvgPath(d))).toEqual(bbox); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/browser.test.ts: -------------------------------------------------------------------------------- 1 | import cases from "./cases/bbox"; 2 | import type { BBox } from "../src"; 3 | 4 | describe("Consistency with browser's element.getBBox()", () => { 5 | beforeAll(async () => { 6 | await page.goto("http://localhost:8080"); 7 | }); 8 | 9 | test.each(cases)( 10 | "browserSvgPathBbox(%p) ⇢ %p", 11 | async (d: string, libBbox: BBox) => { 12 | const browserBbox = await page.evaluate((d) => { 13 | document.body.innerHTML = ``; 14 | const bbox = ( 15 | document.querySelector("path") as SVGPathElement 16 | ).getBBox(); 17 | return [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height]; 18 | }, d); 19 | 20 | expect(browserBbox[0]).toBeCloseTo(libBbox[0], 3); 21 | expect(browserBbox[1]).toBeCloseTo(libBbox[1], 3); 22 | expect(browserBbox[2]).toBeCloseTo(libBbox[2], 3); 23 | expect(browserBbox[3]).toBeCloseTo(libBbox[3], 3); 24 | } 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/cases/bbox.ts: -------------------------------------------------------------------------------- 1 | import type { BBox } from "../../src"; 2 | 3 | type CasesTuple = [string, BBox][]; 4 | 5 | export const linealCases: CasesTuple = [ 6 | // Mz 7 | ["M5 3z", [5, 3, 5, 3]], 8 | // Mz negative 9 | ["M-5 -3z", [-5, -3, -5, -3]], 10 | 11 | // Mlz 12 | ["M5 3l5 0z", [5, 3, 10, 3]], 13 | // Mlz negative 14 | ["M-5 -3l10 0z", [-5, -3, 5, -3]], 15 | // MLz 16 | ["M5 1L6 2z", [5, 1, 6, 2]], 17 | // MLz negative 18 | ["M-5 -1L3 2", [-5, -1, 3, 2]], 19 | 20 | // Mmz 21 | ["M5 3m5 0z", [5, 3, 10, 3]], 22 | // Mmz negative 23 | ["M-5 -3m10 0z", [-5, -3, 5, -3]], 24 | // MMz 25 | ["M5 3M10 8z", [5, 3, 10, 8]], 26 | // MMz negative 27 | ["M-5 -3M-10 -8z", [-10, -8, -5, -3]], 28 | 29 | // Mvz 30 | ["M1 5v3z", [1, 5, 1, 8]], 31 | // Mvz negative 32 | ["M-5 -5v3z", [-5, -5, -5, -2]], 33 | // MVz 34 | ["M1 1V5z", [1, 1, 1, 5]], 35 | // MVz negative 36 | ["M-1 -1V-5z", [-1, -5, -1, -1]], 37 | 38 | // Mhz 39 | ["M1 5h3z", [1, 5, 4, 5]], 40 | // Mhz negative 41 | ["M-1 -5h-3z", [-4, -5, -1, -5]], 42 | // MHz 43 | ["M1 5H3z", [1, 5, 3, 5]], 44 | // MHz negative 45 | ["M-1 -5H-2z", [-2, -5, -1, -5]], 46 | ]; 47 | 48 | export const pathologicalCases: CasesTuple = [ 49 | // Pathological tests (https://github.com/simple-icons/simple-icons/issues/3960) 50 | // These paths proves the bug existent in `svg-path-bounding-box` 51 | // that exists in `svg-path-bbox` before v0.1.1 52 | 53 | [ 54 | // arXiv 55 | "M20.7 15.404l-1.894-4.967h1.411l1.39 3.582 1.379-3.582h.96l-1.92 4.967zM16.298 9.6V8.48h1.34V9.6zm0 5.808v-4.971h1.34v4.967zm-6.965-.003l2.146-3.3L9.43 8.707h1.627l1.364 2.254L13.9 8.707h1.12l-2.046 3.156 2.126 3.537h-1.622l-1.45-2.4-1.557 2.4H9.333zm-3.346 0v-4.968h1.338v.937c.344-.7.875-1.051 1.585-1.051a1.401 1.401 0 01.248.026v1.194a1.6 1.6 0 00-.53-.102c-.537 0-.968.267-1.303.8v3.164zm-3.028-.536q-.664.65-1.437.65a1.473 1.473 0 01-1.06-.398 1.376 1.376 0 01-.406-1.03 1.45 1.45 0 01.659-1.271q.657-.447 1.884-.448h.355v-.453q0-.772-.88-.772a3.305 3.305 0 00-1.587.443v-.922a5.016 5.016 0 011.808-.345q1.953 0 1.951 1.55v2.206c0 .39.123.58.376.58a.8.8 0 00.174-.02l.032.751a2.745 2.745 0 01-.751.13c-.552 0-.902-.216-1.06-.65h-.054zm0-.72v-1.01h-.32c-.866 0-1.297.274-1.297.815a.64.64 0 00.64.648c.329.004.647-.15.977-.453z", 56 | [0.05495008158100078, 8.48, 23.946000000000005, 15.520000000000001], 57 | // svg-path-bounding-box: [ 0.05495008158100078, 7.594038058172477, 23.946000000000005, 15.52 ] 58 | ], 59 | [ 60 | // Emlakjet 61 | "M15.65 16.105v-.24a3.543 3.543 0 00-1.267-2.471c-.724-.663-1.69-.965-2.655-.904-1.87.12-3.378 1.747-3.378 3.615 0 .784.12 1.567.422 2.471H4.55V6.946l7.42-5.123 7.482 5.122v11.692h-4.223c.18-.663.422-1.688.422-2.532m5.068-10.244L12.452.136c-.301-.181-.663-.181-.905 0L3.222 5.86c-.242.12-.362.361-.362.663V19.48c0 .482.362.844.844.844H9.92a.824.824 0 00.844-.844c0-.06 0-.18-.06-.24l-.06-.182c-.302-.723-.664-1.627-.664-2.53v-.182c-.06-.542.12-1.084.482-1.446a2.095 2.095 0 011.388-.723c.543-.06 1.026.12 1.448.482.422.362.664.844.724 1.386v.18c.06 1.206-.724 2.954-.845 3.135l-1.146 2.17-.18-.362c-.122-.181-.302-.362-.483-.422-.182-.06-.423-.06-.604.06-.18.12-.362.301-.422.482s-.06.422.06.603l.905 1.687c.121.241.423.422.724.422.302 0 .604-.18.724-.422l1.81-3.375h5.732a.824.824 0 00.844-.843V6.524c-.06-.302-.18-.543-.422-.663", 62 | [2.86, 0.00025000000000001367, 21.1412251544222, 24.000000000000004], 63 | // svg-path-bounding-box: [ 2.86, 0.136, 21.141225154422205, 24.000000000000004 ] 64 | ], 65 | [ 66 | // Microsoft Excel 67 | "M23 1.5q.41 0 .7.3.3.29.3.7v19q0 .41-.3.7-.29.3-.7.3H7q-.41 0-.7-.3-.3-.29-.3-.7V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h5V2.5q0-.41.3-.7.29-.3.7-.3zM6 13.28l1.42 2.66h2.14l-2.38-3.87 2.34-3.8H7.46l-1.3 2.4-.05.08-.04.09-.64-1.28-.66-1.29H2.59l2.27 3.82-2.48 3.85h2.16zM14.25 21v-3H7.5v3zm0-4.5v-3.75H12v3.75zm0-5.25V7.5H12v3.75zm0-5.25V3H7.5v3zm8.25 15v-3h-6.75v3zm0-4.5v-3.75h-6.75v3.75zm0-5.25V7.5h-6.75v3.75zm0-5.25V3h-6.75v3Z", 68 | [5.551115123125783e-17, 1.5, 24.000000000000096, 22.5], 69 | // svg-path-bounding-box (outdated): [ 5.551115123125783e-17, 1.5, 24, 22.769499273762435 ] 70 | ], 71 | [ 72 | // Microsoft Exchange 73 | "M24 7.875q0 .293-.117.58t-.317.486L20.496 12l3.07 3.059q.2.199.317.486.117.287.117.58V21q0 .316-.117.586-.117.27-.322.475-.206.205-.475.322-.27.117-.586.117h-4.875q-.293 0-.58-.117t-.486-.317l-3.059-3.07-3.059 3.07q-.199.2-.486.317-.287.117-.58.117H4.5q-.316 0-.586-.117-.27-.117-.475-.322-.205-.206-.322-.475Q3 21.316 3 21v-3H.996q-.41 0-.703-.293T0 17.004V6.996q0-.41.293-.703T.996 6H3V3q0-.316.117-.586.117-.27.322-.475.206-.205.475-.322.27-.117.586-.117h4.875q.293 0 .58.117t.486.317l3.059 3.07 3.059-3.07q.199-.2.486-.317.287-.117.58-.117H22.5q.316 0 .586.117.27.117.475.322.205.206.322.475Q24 2.684 24 3zM4.5 3v3h6.504q.41 0 .703.293t.293.703V5.625L9.375 3zM3.375 15.938h5.25v-1.583h-3.41v-1.593h3.047V11.18H5.215V9.656H8.46V8.062H3.375zm19.125.187L19.875 13.5h-3.691q-.247 0-.463.094-.217.094-.375.252-.159.158-.252.375-.094.216-.094.463v3.691L17.625 21H22.5zm0-8.25V3h-4.875L13.5 7.125v2.191q0 .774-.404 1.424-.405.65-1.096.99v5.274q0 .41-.293.703t-.703.293H4.5v3h4.875l4.125-4.125v-2.191q0-.563.21-1.05.212-.486.575-.849t.85-.574Q15.62 12 16.184 12h2.191Z", 74 | [0, 1.4999999999999998, 24, 22.5], 75 | // svg-path-bouding-box: [ 0, 1.4999999999999998, 24, 22.67233909145166 ] 76 | ], 77 | [ 78 | // Microsoft OneDrive 79 | "M19.453 9.95q.961.058 1.787.468.826.41 1.442 1.066.615.657.966 1.512.352.856.352 1.816 0 1.008-.387 1.893-.386.885-1.049 1.547-.662.662-1.546 1.049-.885.387-1.893.387H6q-1.242 0-2.332-.475-1.09-.475-1.904-1.29-.815-.814-1.29-1.903Q0 14.93 0 13.688q0-.985.31-1.887.311-.903.862-1.658.55-.756 1.324-1.325.774-.568 1.711-.861.434-.129.85-.187.416-.06.861-.082h.012q.515-.786 1.207-1.413.691-.627 1.5-1.066.808-.44 1.705-.668.896-.229 1.845-.229 1.278 0 2.456.417 1.177.416 2.144 1.16.967.744 1.658 1.78.692 1.038 1.008 2.28zm-7.265-4.137q-1.325 0-2.52.544-1.195.545-2.04 1.565.446.117.85.299.405.181.792.416l4.78 2.86 2.731-1.15q.27-.117.545-.204.276-.088.58-.147-.293-.937-.855-1.705-.563-.768-1.319-1.318-.755-.551-1.658-.856-.902-.304-1.886-.304zM2.414 16.395l9.914-4.184-3.832-2.297q-.586-.351-1.23-.539-.645-.188-1.325-.188-.914 0-1.722.364-.809.363-1.412.978-.604.616-.955 1.436-.352.82-.352 1.723 0 .703.234 1.423.235.721.68 1.284zm16.711 1.793q.563 0 1.078-.176.516-.176.961-.516l-7.23-4.324-10.301 4.336q.527.328 1.13.504.604.175 1.237.175zm3.012-1.852q.363-.727.363-1.523 0-.774-.293-1.407t-.791-1.072q-.498-.44-1.166-.68-.668-.24-1.406-.24-.422 0-.838.1t-.815.252q-.398.152-.785.334-.386.181-.761.345Z", 80 | [0, 4.312000000000001, 24, 19.688000000000002], 81 | // svg-path-bounding-box: [ 0, 4.312000000000001, 24, 19.99655236943747 ] 82 | ], 83 | [ 84 | // Microsoft OneNote 85 | "M23 1.5Q23.41 1.5 23.7 1.8 24 2.09 24 2.5V21.5Q24 21.91 23.7 22.2 23.41 22.5 23 22.5H7Q6.59 22.5 6.3 22.2 6 21.91 6 21.5V18H1Q0.59 18 0.3 17.7 0 17.41 0 17V7Q0 6.59 0.3 6.3 0.58 6 1 6H6V2.5Q6 2.09 6.3 1.8 6.59 1.5 7 1.5ZM4.56 11 7.39 15.93H9.18V8.07H7.44V13.1L4.71 8.07H2.82V15.93H4.56ZM22.5 21V18H19.5V21ZM22.5 16.5V13.5H19.5V16.5ZM22.5 12V9H19.5V12ZM22.5 7.5V3H7.5V6H11Q11.41 6 11.7 6.3 12 6.59 12 7V17Q12 17.41 11.7 17.7 11.41 18 11 18H7.5V21H18V7.5Z", 86 | [0, 1.5, 24.000000000000096, 22.5], 87 | // svg-path-bounding-box (outdated): [ 0, 1.5, 24, 22.769499273762435 ] 88 | ], 89 | [ 90 | // Microsoft Outlook 91 | "M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z", 92 | [ 93 | 5.551115123125783e-17, 1.4999999999999998, 24.000000000000004, 94 | 22.500000000000004, 95 | ], 96 | // svg-path-bounding-box: [ 5.551115123125783e-17, 1.499999999999999, 24.000000000000004, 22.727227619294418 ] 97 | ], 98 | [ 99 | // Microsoft Word 100 | "M23.004 1.5q.41 0 .703.293t.293.703v19.008q0 .41-.293.703t-.703.293H6.996q-.41 0-.703-.293T6 21.504V18H.996q-.41 0-.703-.293T0 17.004V6.996q0-.41.293-.703T.996 6H6V2.496q0-.41.293-.703t.703-.293zM6.035 11.203l1.442 4.735h1.64l1.57-7.876H9.036l-.937 4.653-1.325-4.5H5.38l-1.406 4.523-.938-4.675H1.312l1.57 7.874h1.641zM22.5 21v-3h-15v3zm0-4.5v-3.75H12v3.75zm0-5.25V7.5H12v3.75zm0-5.25V3h-15v3Z", 101 | [0, 1.5, 24, 22.499999999999996], 102 | // svg-path-bounding-box: [ 0, 1.5, 24, 22.772661142370712 ] 103 | ], 104 | [ 105 | // pre-commit 106 | "M23.355 10.444L13.556.645a2.2 2.2 0 0 0-3.112 0L.645 10.444a2.201 2.201 0 0 0 0 3.112l9.799 9.799a2.201 2.201 0 0 0 3.112 0l9.799-9.799a2.2 2.2 0 0 0 0-3.112zm-1.657 2.918l-8.337 8.337a1.922 1.922 0 0 1-1.362.563c-.493 0-.986-.188-1.362-.563L2.3 13.362A1.92 1.92 0 0 1 1.738 12c0-.514.2-.998.564-1.362l8.337-8.337c.363-.363.847-.563 1.361-.563s.998.2 1.362.564l8.337 8.337c.75.75.75 1.972-.001 2.723zM14.195 9.76c.094.173.142.399.142.678s-.047.505-.142.678c-.095.173-.22.306-.376.401a1.485 1.485 0 0 1-.542.191 4.033 4.033 0 0 1-.641.049h-1.504V9.119h1.504c.222 0 .435.017.641.049.205.033.386.097.542.191.156.095.281.228.376.401zm7.062 1.319L12.92 2.742c-.245-.245-.572-.381-.92-.381s-.675.135-.921.381l-8.337 8.337c-.245.246-.381.573-.381.921s.135.675.381.921l8.337 8.337a1.304 1.304 0 0 0 1.842 0l8.337-8.337a1.305 1.305 0 0 0-.001-1.842zm-5.213.4a2.437 2.437 0 0 1-.53.906 2.624 2.624 0 0 1-.943.635c-.386.16-.855.24-1.405.24h-2.034v3.155H9.197v-8.8h3.969c.55 0 1.019.08 1.405.24.386.16.7.372.943.635.242.263.419.563.53.9.111.337.166.686.166 1.048 0 .353-.055.7-.166 1.041z", 107 | [ 108 | 0.0006839756353888008, 0.0002697515222243041, 23.99973024847777, 109 | 23.99931602436461, 110 | ], 111 | // svg-path-bounding-box: [ 0.0035476024103427706, 0.0002697515222243041, 23.99973024847777, 23.999316024364607 ] 112 | ], 113 | [ 114 | // RStudio 115 | "M12.178.002a12.002 12.002 0 0 0-8.662 3.515 12.002 12.002 0 0 0 0 16.97 12.002 12.002 0 0 0 16.97 0 12.002 12.002 0 0 0 0-16.97A12.002 12.002 0 0 0 12.179.002zM7.77 5.995c.562.128 1.05.217 1.663.217.921 0 1.863-.217 2.786-.217 1.79 0 3.45.814 3.45 2.8 0 1.54-.921 2.517-2.35 2.93l2.788 4.107h1.301v1.01h-1.986l-3.293-4.934h-1.757v3.924h1.718v1.01H7.77v-1.01h1.483V7.134L7.77 6.951v-.957zm4.466 1.012c-.596 0-1.213.053-1.864.127v3.798l.941.02c2.298.034 3.183-.85 3.183-2.026 0-1.376-.997-1.919-2.26-1.919z", 116 | [ 117 | 0.0023908369018924135, 0.0007318976900466977, 23.999609163098103, 118 | 24.000609163098108, 119 | ], 120 | // svg-path-bounding-box: [ 0.07409714635287423, 0.0007318976900466973, 23.9996091630981, 24.000609163098105 ] 121 | ], 122 | [ 123 | // Sellfy 124 | "M23.179.818C15.533-.273 8.406-.273.8.818-.266 8.377-.266 15.424.8 22.946 4.511 23.491 8.22 24 12.005 24c3.748 0 7.459-.51 11.17-1.017 1.1-7.56 1.1-14.607 0-22.165h.004zm-11.54 18.314c-2.055 0-4.226-.689-5.179-1.199l.807-3.126c1.064.705 2.682 1.395 4.446 1.395 1.395 0 2.24-.436 2.24-1.305 0-.615-.435-.975-1.575-1.26l-2.279-.631c-2.416-.66-3.557-1.891-3.557-3.855 0-2.365 1.83-4.256 5.619-4.256 1.99 0 3.973.545 5.07 1.092l-.951 2.976c-1.104-.615-2.79-1.125-4.226-1.125-1.365 0-1.95.436-1.95 1.092 0 .619.404.87 1.291 1.092l2.488.734c2.566.736 3.707 1.966 3.707 3.885-.076 2.701-2.461 4.517-5.957 4.517l.006-.026z", 125 | [0.0005000000000000053, -0.0002500000000000062, 23.999999999999993, 24], 126 | // svg-path-bounding-box: [ 0.0005000000000000004, 0.818, 24, 24 ] 127 | ], 128 | [ 129 | // Squarespace 130 | "M22.655 8.719c-1.802-1.801-4.726-1.801-6.564 0l-7.351 7.35c-.45.45-.45 1.2 0 1.65.45.449 1.2.449 1.65 0l7.351-7.351c.899-.899 2.362-.899 3.264 0 .9.9.9 2.364 0 3.264l-7.239 7.239c.9.899 2.362.899 3.263 0l5.589-5.589c1.836-1.838 1.836-4.763.037-6.563zm-2.475 2.437c-.451-.45-1.201-.45-1.65 0l-7.354 7.389c-.9.899-2.361.899-3.262 0-.45-.45-1.2-.45-1.65 0s-.45 1.2 0 1.649c1.801 1.801 4.726 1.801 6.564 0l7.351-7.35c.449-.487.449-1.239.001-1.688zm-2.439-7.35c-1.801-1.801-4.726-1.801-6.564 0l-7.351 7.351c-.45.449-.45 1.199 0 1.649s1.2.45 1.65 0l7.395-7.351c.9-.899 2.371-.899 3.27 0 .451.45 1.201.45 1.65 0 .421-.487.421-1.199-.029-1.649h-.021zm-2.475 2.437c-.45-.45-1.2-.45-1.65 0l-7.351 7.389c-.899.9-2.363.9-3.265 0-.9-.899-.9-2.363 0-3.264l7.239-7.239c-.9-.9-2.362-.9-3.263 0L1.35 8.719c-1.8 1.8-1.8 4.725 0 6.563 1.801 1.801 4.725 1.801 6.564 0l7.35-7.351c.451-.488.451-1.238 0-1.688h.002z", 131 | [4.1119371282413197e-17, 2.4539999999999984, 23.999660308381387, 21.54525], 132 | // svg-path-bounding-box: [ 1.35, 2.453999999999999, 23.999660308381387, 21.54525 ] 133 | ], 134 | [ 135 | // New York Times 136 | "M21.272,14.815h-0.098c-0.747,2.049-2.335,3.681-4.363,4.483v-4.483l2.444-2.182l-2.444-2.182V7.397 c2.138,0.006,3.885-1.703,3.927-3.84c0-2.629-2.509-3.556-3.927-3.556c-0.367-0.007-0.734,0.033-1.091,0.12v0.131h0.556 c0.801-0.141,1.565,0.394,1.706,1.195C17.99,1.491,17.996,1.537,18,1.583c-0.033,0.789-0.7,1.401-1.488,1.367 c-0.02-0.001-0.041-0.002-0.061-0.004c-2.444,0-5.323-1.985-8.454-1.985C5.547,0.83,3.448,2.692,3.284,5.139 C3.208,6.671,4.258,8.031,5.76,8.346v-0.12C5.301,7.931,5.041,7.407,5.084,6.862c0.074-1.015,0.957-1.779,1.973-1.705 C7.068,5.159,7.08,5.16,7.091,5.161c2.629,0,6.872,2.182,9.501,2.182h0.098v3.142l-2.444,2.182l2.444,2.182v4.549 c-0.978,0.322-2.003,0.481-3.033,0.469c-1.673,0.084-3.318-0.456-4.614-1.516l4.429-1.985V7.451l-6.196,2.727 c0.592-1.75,1.895-3.168,3.589-3.905V6.175c-4.516,1.004-8.138,4.243-8.138,8.705c0,5.193,4.025,9.12,9.818,9.12 c6.011,0,8.727-4.363,8.727-8.814V14.815z M8.858,18.186c-1.363-1.362-2.091-3.235-2.007-5.16c-0.016-0.88,0.109-1.756,0.371-2.596 l2.051-0.938v8.476L8.858,18.186z", 137 | [2.727, 0.00021808510638331264, 21.272, 24], 138 | // svg-path-bounding-box: [ 2.7270000000000003, 0.001000000000000334, 21.272, 24 ] 139 | ], 140 | [ 141 | // Treehouse 142 | "M20.537 4.118c-.806-.453-2.092.278-2.871 1.635L16.25 8.215a3.104 3.104 0 0 0 .21 3.18l.041.062c.653.94 1.535 1.808 1.823 2.118a1.613 1.613 0 0 1-.739 2.654 1.603 1.603 0 0 1-2.025-1.747c.045-.35-.067-.927-.574-1.489-.506-.563-1.54.5-1.874 1.61l-.016.061c-.334 1.094-.546 2.05-.482 2.143.037.06.072.12.105.182a1.81 1.81 0 0 1-3.196 1.701 1.806 1.806 0 0 1 .747-2.446l.121-.061c.065-.03.26-.486.423-1.032l.301-.987c.019-.047.033-.107.045-.168l.897-3.19-.957 1.96c-.112-.363-.3-.38-.709-.091-.243.183-.653.531-.85.669-.365.273-.685.788-.851 1.109a1.313 1.313 0 0 1-.41.5c-.684.564-1.687.456-2.234-.227a1.591 1.591 0 0 1 .912-2.552c.409-.092 1.777-.927 2.596-1.52.152-.106.274-.197.38-.304l2.203-1.67-1.914 1.032s-.196-.016-.426.017c-.698.075-1.428.182-1.564.35a.999.999 0 0 1-.29.272c-.637.456-1.519.32-1.989-.317A1.437 1.437 0 0 1 6.29 8.04c.259-.183.577-.274.865-.274.518.016 1.87.29 2.993.092l.288-.045c1.14-.196 2.476-1.186 3.024-2.187l1.184-2.067c.653-1.139.608-2.384-.105-2.795l-1.323-.76c-.653-.363-1.715-.363-2.354 0L2.004 4.97C1.337 5.319.805 6.23.805 6.975v9.744c0 .744.532 1.656 1.178 2.02l8.85 4.983c.652.365 1.716.365 2.354 0l8.826-4.983c.653-.368 1.184-1.276 1.184-2.02v-9.76c0-.744-.531-1.653-1.169-2.02l-1.46-.823", 143 | [0.805, -0.26825000000000065, 23.197000000000003, 23.99575000000006], 144 | // svg-path-bounding-box: [ 0.805, 0.003999999999999337, 23.197000000000003, 23.99575 ] 145 | ], 146 | 147 | // https://github.com/mondeja/svg-path-bbox/issues/61 148 | [ 149 | "M 21.1456 21.1456 C 21.1456 21.1456 21.1456 21.1456 200.0000 0.0000 200.0000 0.0000 200.0000 0.0000 221.1456 221.1456 221.1456 221.1456 221.1456 221.1456 21.1456 221.1456 21.1456 221.1456 21.1456 221.1456 21.1456 21.1456", 150 | [21.1456, 0, 221.1456, 221.1456], 151 | // svg-path-bounding-box: [ 21.1456, 0, 221.1456, 221.1456 ], 152 | ], 153 | 154 | // https://github.com/mondeja/svg-path-bbox/issues/91 155 | [ 156 | "M436.79 417.31C435.89 415.01 438.59 410.91 439.69 409.31C439.89 408.91 439.69 408.41 439.39 408.31C438.09 407.41 435.19 404.71 435.09 401.61C434.99 399.71 437.39 396.41 438.49 394.91C439.19 393.71 437.69 393.11 437.09 392.31C435.69 390.81 433.99 388.31 434.09 385.71C434.19 382.71 436.69 379.51 440.79 377.01C440.89 376.91 440.99 376.71 440.89 376.61C439.79 374.91 439.29 374.01 439.19 371.41C438.99 367.91 440.79 364.81 444.19 362.41C446.59 360.81 449.89 359.81 452.79 359.21C452.79 359.01 452.79 349.41 452.79 348.21V348.01V340.21C452.79 340.01 452.69 339.91 452.49 339.91L209.89 340.21C209.69 340.21 209.59 340.31 209.59 340.51L209.69 896.1H452.49C452.69 896.1 452.79 896 452.79 895.8V420.31C449.79 421.41 446.59 422.11 444.19 422.11C440.79 422.01 437.69 419.51 436.79 417.31Z", 157 | [209.59000000000003, 339.90999999999764, 452.7900000000486, 896.1], 158 | // svg-path-bounding-box: [ 209.59, 339.91, 452.79, 896.1 ] 159 | ], 160 | 161 | // Tapas 162 | // https://github.com/simple-icons/simple-icons/pull/7442#issuecomment-1137971253 163 | [ 164 | "M7.67 1.56c.282-.134.542-.338.81-.513.253-.163.54-.436.894-.33.103.296-.162.503-.331.662-.538.511-1.154.975-1.72 1.456A240.349 240.349 0 0 1 1.5 7.598a7.406 7.406 0 0 1-.612.445c-.183.118-.456.359-.71.165.071-.337.306-.567.512-.778.213-.216.414-.446.629-.66-.248-.427-.473-.821-.662-1.274-.186-.449-.378-.971-.38-1.554-.002-1.109.635-2.043 1.34-2.68C2.34.61 3.306.066 4.429.006 6.015-.078 6.933.71 7.67 1.56zm5.012 18.075v.198c-.278.01-.532-.01-.795-.016v-.198c.277-.008.535.006.795.016zm-1.59 0v.198c-.282-.012-.52.021-.792.018v-.198a9.53 9.53 0 0 1 .793-.018zm3.177.05c-.007.067.013.158-.017.199-.251-.02-.518-.024-.778-.033v-.198c.275.003.542.009.795.032zm-4.763 0v.199c-.274.002-.512.039-.795.032v-.197c.28.001.516-.036.795-.034zm5.555.034c.255.033.544.029.793.064.013.084-.014.129-.015.2-.255-.033-.544-.03-.794-.067a.703.703 0 0 0 .016-.197zm-7.142.065v.2c-.26.02-.517.046-.778.065-.022-.05-.018-.126-.017-.198.265-.024.521-.053.795-.067zm8.73.067c.269.023.537.048.793.082-.02.058-.004.148-.032.199-.25-.036-.518-.053-.778-.083-.01-.083.017-.128.017-.198zm-10.319.082c-.006.08.03.113.017.199-.259.022-.568.082-.793.082.012-.077-.02-.114-.018-.182.252-.045.529-.066.794-.099zm12.684.199c.012.084-.027.114-.017.196-.256-.044-.54-.063-.794-.114.01-.058.025-.109.017-.182.228.008.545.062.795.1zm-14.288 0c.06.022.033.133.05.196-.259.04-.517.08-.777.117a.68.68 0 0 1-.034-.197c.253-.038.515-.072.761-.116zm15.86.233a.628.628 0 0 1-.034.213c-.247-.055-.52-.083-.777-.132a.702.702 0 0 1 .034-.197c.263.032.503.09.776.116zm-17.414.016c.02.057.036.116.034.196-.263.04-.503.105-.778.133-.004-.073-.034-.12-.033-.197.275-.028.515-.092.777-.132zm18.208.132c.255.052.508.109.778.148-.004.072-.034.119-.034.197-.28-.021-.495-.11-.778-.133-.018-.041.016-.15.034-.212zM22.669 16.726c.156.092.47.098.595.246.099.115.144.486.182.744.203 1.296.287 2.808.332 4.219.008.266.016.583.016.891 0 .298.06.704 0 .91-.041.147-.24.194-.363.264a56.558 56.558 0 0 0-.065-2.843c-.124-.101-.444-.047-.464-.166-.044-.252.267-.09.447-.065-.045-1.272-.177-2.46-.33-3.623-.147-.074-.336-.105-.498-.164-.252.259-.636.939-1.223.81-.22-.047-.363-.342-.464-.545a3.243 3.243 0 0 1-.265-.744c-4.88-.936-11.589-1.016-16.502-.05-.153.655-.43 2.053-1.34 1.52a2.014 2.014 0 0 1-.81-.991 8.31 8.31 0 0 1-.547.133c-.192 1.084-.288 2.268-.346 3.489.166.01.416-.122.595-.1.004.066.028.114.033.18-.166.106-.437.105-.645.166a45.286 45.286 0 0 0-.066 2.976c-.08.022-.273-.122-.347-.213.064-2.301.179-4.553.363-6.732.28-.087.568-.17.844-.264-.04-.383-.117-.827.05-1.09.14-.224.531-.352.81-.432.99-.28 1.979-.05 2.63.413.14.102.247.239.396.299.025-.09-.094-.15-.149-.199-.567-.511-1.498-.958-2.612-.761-.348-1.09-.79-2.142-.794-3.538-.005-1.553.562-2.899 1.205-3.953.66-1.078 1.541-1.954 2.498-2.645a11.504 11.504 0 0 1 8.087-2.051c3.01.369 5.008 1.79 6.45 3.853.69.99 1.248 2.174 1.62 3.524.374 1.352.378 3.098-.05 4.53-1.383-.283-2.637.15-3.125 1.026-.004.015-.016.017-.016.033.498-.678 1.736-1.168 2.976-.86.328.082.746.2.908.43.224.317.122.989-.016 1.373zM16.22 9.382c.055.383.227.783.445.944.376.27.602.001.63-.38.035-.504-.174-1.1-.431-1.324-.105-.09-.299-.145-.412-.115-.256.065-.283.528-.232.875zm-8.649 1.092c-.033.556.16 1.277.529 1.472.43.227.633-.095.661-.495.045-.626-.273-1.714-.86-1.605-.25.047-.313.339-.33.628zm6.83 2.579c-.266.06-.633-.058-.926-.117a22.333 22.333 0 0 0-.91-.164c-.567-.088-1.344-.211-1.9.1-.198.11-.444.351-.465.662-.027.46.342.791.612.993.323.237.663.399 1.092.527.917.278 2.293.353 3.075.017.735-.316 1.706-1.062 1.72-2.05.01-.59-.272-1.119-.859-1.042-.65.085-.882.951-1.44 1.074z", 165 | [0.17800000000000005, -0.00016249501660449533, 23.82066666666666, 24], 166 | // svg-path-bounding-box: [ 0.17800000000000005, -0.00016249501660449477, 23.82066666666666, 24 ] 167 | ], 168 | 169 | // https://github.com/mondeja/svg-path-bbox/issues/112 170 | ["M90 100Q109 100 110 110L100 90", [90, 90, 110, 110]], 171 | ["M90 100Q101 100 110 110L100 90", [90, 90, 110, 110]], 172 | ["M90 100Q90 110 110 110L100 90", [90, 90, 110, 110]], 173 | // svg-path-bounding-box: [ 90, 90, 110, 110 ] 174 | ]; 175 | 176 | export default [...linealCases, ...pathologicalCases]; 177 | -------------------------------------------------------------------------------- /tests/changelog.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | 3 | test("Current version has a CHANGELOG entry", () => { 4 | const changelog = fs.readFileSync("CHANGELOG.md", "utf8"); 5 | const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8")); 6 | 7 | const changelogEntry = changelog 8 | .split("\n") 9 | .find((line: string) => line.startsWith(`## [${packageJson.version}]`)); 10 | expect(changelogEntry).toBeDefined(); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/cli.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | 4 | import syscall from "./utils/syscall"; 5 | import type { SysCallOutput, SysCallArgs } from "./utils/syscall"; 6 | 7 | const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8")); 8 | 9 | const cliPath = path.resolve("src", "cli.cjs"); 10 | 11 | type CliCases = [SysCallArgs, 0 | 1, string, string][]; 12 | 13 | const cliParameterCases: CliCases = [ 14 | [['"M5 10c3 0 3 3 0 3z"'], 0, "5 10 7.25 13\n", ""], 15 | [['"M5 10c3 0 3 3 0 3z"', '"M2 8m5 5z"'], 0, "5 10 7.25 13\n2 8 7 13\n", ""], 16 | [ 17 | [], 18 | 1, 19 | "", 20 | "You must pass SVG paths enclosed between quotes as parameters.\n", 21 | ], 22 | [["--version"], 1, `^${packageJson.version}\n$`, ""], 23 | [["-v"], 1, `^${packageJson.version}\n$`, ""], 24 | [["--help"], 1, "", `^${packageJson.description}`], 25 | [["-h"], 1, "", `^${packageJson.description}`], 26 | ]; 27 | 28 | describe("svg-path-bbox", () => { 29 | test.each(cliParameterCases)( 30 | "svg-path-bbox %p [CODE: %p | STDOUT: %p | STDERR: %p]", 31 | async (args, code, stdout, stderr) => { 32 | args.unshift(cliPath); 33 | const proc = (await syscall(args)) as SysCallOutput; 34 | expect(proc.code).toBe(code); 35 | expect(new RegExp(stdout).test(proc.stdout)).toEqual(true); 36 | expect(new RegExp(stderr).test(proc.stderr)).toEqual(true); 37 | } 38 | ); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/examples.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | 4 | import syscall from "./utils/syscall"; 5 | import type { SysCallOutput, SysCallProgram } from "./utils/syscall"; 6 | 7 | const examplesDirname = "examples"; 8 | const expectedOutput = "[ 0, 0, 3, 6 ]\n"; 9 | 10 | type ExamplesCases = [string, SysCallProgram][]; 11 | 12 | const examplesCases: ExamplesCases = fs 13 | .readdirSync(examplesDirname) 14 | .map((fname) => [fname, fname.endsWith(".ts") ? "ts-node" : "node"]); 15 | 16 | describe("svg-path-bbox/examples", () => { 17 | test.each(examplesCases)("svg-path-bbox %p [%p]", async (fname, program) => { 18 | const proc = (await syscall( 19 | [path.resolve(examplesDirname, fname)], 20 | program 21 | )) as SysCallOutput; 22 | expect(proc.code).toBe(0); 23 | expect(proc.stdout).toEqual(expectedOutput); 24 | expect(proc.stderr).toEqual(""); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/optimization.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import { getSimpleIconsSegmentsStats } from "../scripts/get-simple-icons-segments-stats"; 3 | 4 | test("Segments parsing order switch is optimized", () => { 5 | // We use simple-icons data to compute segment stats 6 | const indexTs = fs.readFileSync("src/index.ts", "utf8"); 7 | const siSegmentsByOccurrence = getSimpleIconsSegmentsStats().map( 8 | ([seg]: [string, number]) => seg 9 | ); 10 | 11 | const swithCasesSegmentByLines = indexTs 12 | .split("\n") 13 | .map((line) => { 14 | if (line.startsWith(" case ")) { 15 | return line.split('"')[1]; 16 | } 17 | return null; 18 | }) 19 | .filter((occ) => occ !== null); 20 | 21 | expect(siSegmentsByOccurrence).toStrictEqual(swithCasesSegmentByLines); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/utils/syscall.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "node:child_process"; 2 | 3 | export type SysCallArgs = string[]; 4 | export type SysCallOutput = { code: number; stdout: string; stderr: string }; 5 | export type SysCallProgram = "node" | "ts-node"; 6 | 7 | export default (args: SysCallArgs, program: SysCallProgram = "node") => 8 | new Promise((resolve, reject) => { 9 | const child = exec(`${program} ${args.join(" ")}`); 10 | 11 | const stdoutChunks: string[] = []; 12 | const stderrChunks: string[] = []; 13 | let stdout: string; 14 | let stderr: string; 15 | child.on("exit", (code: number) => { 16 | resolve({ code, stdout, stderr }); 17 | }); 18 | 19 | if (child.stdout === null) { 20 | return reject("'stdout' object of ChildProcess is null"); 21 | } 22 | child.stdout.on("data", (data: string) => { 23 | stdoutChunks.push(data); 24 | }); 25 | child.stdout.on("end", () => { 26 | stdout = stdoutChunks.join(""); 27 | }); 28 | 29 | if (child.stderr === null) { 30 | return reject("'stderr' object of ChildProcess is null"); 31 | } 32 | child.stderr.on("data", (data: string) => { 33 | stderrChunks.push(data); 34 | }); 35 | child.stderr.on("end", () => { 36 | stderr = stderrChunks.join(""); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "strict": true, 6 | "outDir": "dist", 7 | "skipLibCheck": true 8 | }, 9 | "include": ["./src/index.ts"] 10 | } 11 | --------------------------------------------------------------------------------