├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── create-draft-release.yml │ └── publish-release-to-crates-io.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENCE ├── README.md ├── src ├── bin │ └── spreet │ │ ├── cli.rs │ │ └── main.rs ├── error.rs ├── fs.rs ├── lib.rs └── sprite │ ├── mod.rs │ └── serialize.rs └── tests ├── cli.rs ├── fixtures ├── output │ ├── default@1x.json │ ├── default@1x.png │ ├── default@2x.json │ ├── default@2x.png │ ├── minify@1x.json │ ├── minify@1x.png │ ├── minify_unique@1x.json │ ├── minify_unique@1x.png │ ├── pngs@2x.json │ ├── pngs@2x.png │ ├── recursive@1x.json │ ├── recursive@1x.png │ ├── sdf@2x.json │ ├── sdf@2x.png │ ├── stretchable@2x.json │ ├── stretchable@2x.png │ ├── unique@1x.json │ ├── unique@1x.png │ ├── unique@2x.json │ └── unique@2x.png ├── pngs │ ├── iceland_flag.png │ ├── iceland_flag.svg │ ├── sweden_flag.png │ └── sweden_flag.svg ├── stretchable │ ├── README.md │ ├── ae-national-3-affinity.svg │ ├── cn-nths-expy-2-affinity.svg │ ├── cn-nths-expy-2-inkscape-plain.svg │ ├── shield-illustrator-rotated-reversed.svg │ ├── shield-illustrator-rotated-translated.svg │ ├── shield-illustrator-rotated.svg │ ├── shield-illustrator.svg │ └── shield-rotated.svg └── svgs │ ├── another_bicycle.svg │ ├── bicycle.svg │ ├── circle.svg │ └── recursive │ └── bear.svg ├── fs.rs └── sprite.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | time: "06:00" 8 | timezone: UTC 9 | open-pull-requests-limit: 0 10 | - package-ecosystem: github-actions 11 | directory: / 12 | schedule: 13 | interval: monthly 14 | time: "05:45" 15 | timezone: UTC 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | check: 9 | name: cargo check 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Install Rust toolchain 14 | uses: dtolnay/rust-toolchain@stable 15 | - name: Run cargo check 16 | run: cargo check 17 | clippy: 18 | name: cargo clippy 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Install Rust toolchain 23 | uses: dtolnay/rust-toolchain@stable 24 | with: 25 | components: clippy 26 | - name: Run cargo clippy 27 | run: cargo clippy 28 | fmt: 29 | name: cargo fmt 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Install Rust toolchain 34 | uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: rustfmt 37 | - name: Run cargo fmt 38 | run: cargo fmt --all --check 39 | msrv: 40 | name: Check minimum supported Rust version is correct 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: taiki-e/install-action@v2 45 | with: 46 | tool: cargo-hack 47 | - run: cargo hack check --rust-version 48 | test: 49 | name: cargo test 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Install Rust toolchain 54 | uses: dtolnay/rust-toolchain@stable 55 | - name: Run cargo test 56 | run: cargo test 57 | - name: Run cargo test in lib mode 58 | run: cargo test --no-default-features 59 | -------------------------------------------------------------------------------- /.github/workflows/create-draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Create new draft release 2 | on: 3 | push: 4 | tags: 5 | - "v[0-9]+.[0-9]+.[0-9]+" 6 | jobs: 7 | build: 8 | name: Build release binaries 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | include: 13 | - target: x86_64-unknown-linux-musl 14 | os: ubuntu-latest 15 | package: spreet-x86_64-unknown-linux-musl.tar.gz 16 | - target: arm-unknown-linux-gnueabihf 17 | os: ubuntu-latest 18 | package: spreet-arm-unknown-linux-gnueabihf.tar.gz 19 | - target: x86_64-apple-darwin 20 | os: macos-latest 21 | package: spreet-x86_64-apple-darwin.tar.gz 22 | - target: aarch64-apple-darwin 23 | os: macos-latest 24 | package: spreet-aarch64-apple-darwin.tar.gz 25 | - target: x86_64-pc-windows-msvc 26 | os: windows-latest 27 | package: spreet-x86_64-pc-windows-msvc.zip 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Install Rust toolchain 32 | uses: dtolnay/rust-toolchain@stable 33 | with: 34 | target: ${{ matrix.target }} 35 | - name: Install Cross 36 | run: cargo install cross 37 | - name: Build Spreet 38 | run: cross build --release --locked --target ${{ matrix.target }} 39 | - name: Prepare Windows package 40 | if: matrix.os == 'windows-latest' 41 | run: | 42 | cd target/${{ matrix.target }}/release 43 | 7z a ../../../${{ matrix.package }} spreet.exe 44 | cd - 45 | - name: Prepare Unix package 46 | if: matrix.os != 'windows-latest' 47 | run: | 48 | cd target/${{ matrix.target }}/release 49 | tar czvf ../../../${{ matrix.package }} spreet 50 | cd - 51 | - name: Upload artifact 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: ${{ matrix.package }} 55 | path: ${{ matrix.package }} 56 | publish: 57 | name: Publish draft release 58 | needs: [build] 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4 62 | - name: Install Rust toolchain 63 | uses: dtolnay/rust-toolchain@stable 64 | - id: version 65 | name: Get version number 66 | run: | 67 | VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2) 68 | echo "version=$VERSION" >> $GITHUB_OUTPUT 69 | - name: Download artifacts 70 | uses: actions/download-artifact@v4 71 | with: 72 | path: ./bin 73 | - name: Create draft release 74 | uses: softprops/action-gh-release@v2 75 | with: 76 | name: v${{ steps.version.outputs.version }} 77 | draft: true 78 | generate_release_notes: true 79 | files: | 80 | ./bin/**/*.zip 81 | ./bin/**/*.tar.gz 82 | -------------------------------------------------------------------------------- /.github/workflows/publish-release-to-crates-io.yml: -------------------------------------------------------------------------------- 1 | name: Publish new release to crates.io 2 | on: 3 | release: 4 | types: [released] 5 | jobs: 6 | publish: 7 | name: cargo publish 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: dtolnay/rust-toolchain@stable 12 | - run: cargo publish 13 | env: 14 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Development version 4 | 5 | - Fix issue creating valid sprite names for symlinks (see [#81](https://github.com/flother/spreet/issues/81)). This has two side effects: 6 | 1. sprite names can now be generated for files that don't exist 7 | 2. `spreet::sprite_name()` now returns a `PathError` instead of an `IoError` when the `abs_path` argument is not an ancestor of the `path` argument. 8 | - Update [clap](https://crates.io/crates/clap) dependency to v4.5 9 | - Update [multimap](https://crates.io/crates/multimap) dependency to v0.10 10 | - Update [oxipng](https://crates.io/crates/oxipng) dependency to v9.1 11 | - Update [resvg](https://crates.io/crates/resvg) dependency to v0.43 12 | - Update [assert_fs](https://crates.io/crates/assert_fs) dev dependency to v1.1 13 | - Update [softprops/action-gh-release](https://github.com/softprops/action-gh-release) to v2 14 | 15 | The minimum supported version of Rust is now 1.79.0 (released June 2024). 16 | 17 | ## v0.11.0 (2023-12-05) 18 | 19 | - Add support for SDF icons (aka [re-colourable images](https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/)). See [#58](https://github.com/flother/spreet/issues/58) 20 | - **Breaking change**: due to the addition of SDF icons, both the `SpriteDescription` and `SpritesheetBuilder` structs have a new boolean field named `sdf`, while `SpriteDescription::new()` also takes a new `sdf` argument. Set these to `false` if you want to match the existing behaviour (i.e. no SDF icons). To create a spritesheet of SDF icons, call `SpritesheetBuilder::make_sdf()`. 21 | - Add a new constructor, `Sprite::new_sdf()`. This rasterises an SVG to a bitmap as usual, but generates a signed distance field for the image and stores that data in the bitmap's alpha channel 22 | 23 | ## v0.10.0 (2023-11-29) 24 | 25 | - **Breaking change:** move all public identifiers to the root of the crate, e.g. `spreet::error::SpreetError` -> `spreet::SpreetError`, with the exception of `resvg`. 26 | - Update [oxipng](https://crates.io/crates/oxipng) dependency to [v9.0.0](https://github.com/shssoichiro/oxipng/blob/master/CHANGELOG.md#version-900). This improves compression of PNG spritesheets without visual changes, but the PNGs won't be byte-to-byte compatible with spritesheets output by earlier versions of Spreet 27 | - Update [resvg](https://crates.io/crates/resvg) dependency to [v0.36.0](https://github.com/RazrFalcon/resvg/blob/master/CHANGELOG.md#user-content-0360---2023-10-01) 28 | - Remove the deprecated function `spreet::sprite::generate_pixmap_from_svg()` 29 | - The `spreet::sprite_name` function (previously available as `spreet::sprite::sprite_name`) now returns `Result` instead of `String`, and will no longer panic 30 | - The `spreet::get_svg_input_paths` function (previously available as `spreet::fs::get_svg_input_paths`) now returns `Result, Error>` instead of `Vec`, and will no longer panic 31 | 32 | ## v0.9.0 (2023-10-08) 33 | 34 | - Support stretchable icons (see [#53](https://github.com/flother/spreet/issues/53)) 35 | - Make the CLI an optional (but default) feature ([#62](https://github.com/flother/spreet/pull/62)). This speeds up the build when using Spreet as a Rust library (see [README](README.md#using-spreet-as-a-rust-library)) 36 | - Fix bug that meant URLs in SVG `` elements were resolved relative to the current working directory, not to the SVG itself (see [#60](https://github.com/flother/spreet/issues/60)) 37 | - Update [resvg](https://crates.io/crates/resvg) dependency to v0.35 38 | - Update [clap](https://crates.io/crates/clap) dependency to v4.4 39 | - Remove [Rayon](https://crates.io/crates/rayon) dependency. This means the Spreet CLI no longer parses SVGs in parallel, but that was a fun-but-unnecessary optimisation in the first place that generally saved only a handful of milliseconds 40 | - **Deprecated**: `spreet::sprite::generate_pixmap_from_svg()` has been deprecated and will be removed in a future version. Use `spreet::sprite::Spreet::pixmap()` instead 41 | 42 | The minimum supported version of Rust is now 1.70.0 (released June 2023). 43 | 44 | ## v0.8.0 (2023-06-15) 45 | 46 | - Improvements to using Spreet as a Rust library (#57 and #59) 47 | - Optimise Oxipng usage to reduce dev dependencies (#61) 48 | - Optimise the `main` function (#56) 49 | - Update [crunch](https://crates.io/crates/crunch) dependency to v0.5.3 50 | - Update [resvg](https://crates.io/crates/resvg) dependency to v0.34 51 | - Update [clap](https://crates.io/crates/clap) dependency to v4.3 52 | - Update [multimap](https://crates.io/crates/multimap) dependency to v0.9.0 53 | - Update [Rayon](https://crates.io/crates/rayon) dependency to v1.7 54 | - Update [assert_fs](https://crates.io/crates/assert_fs) dependency to v1.0.13 55 | 56 | Note: the update to [resvg](https://crates.io/crates/resvg) brings a new image rendering algorithm. This produces smaller images and improves performance, but the PNGs won't be byte-to-byte compatible with spritesheets output by earlier versions of Spreet. There should be no visual change though. 57 | 58 | ## v0.7.0 (2023-03-26) 59 | 60 | - Replace unmaintained [actions-rs/toolchain](https://github.com/actions-rs/toolchain) with [dtolnay/rust-toolchain](https://github.com/dtolnay/rust-toolchain) ([#44](https://github.com/flother/spreet/pull/44) and [#45](https://github.com/flother/spreet/pull/45)) 61 | - Publish to crates.io when new version is released ([#46](https://github.com/flother/spreet/pull/46)) 62 | - Update clap dependency to v4.1 63 | 64 | ## v0.6.0 (2023-02-13) 65 | 66 | - Add `--recursive` argument, to include images in sub-directories (see [#43](https://github.com/flother/spreet/pull/43)) 67 | - **Breaking change**: update [Oxipng](https://github.com/shssoichiro/oxipng) dependency to v8. Spritesheet PNGs output by Spreet are now compressed using [libdeflate](https://github.com/ebiggers/libdeflate). This produces smaller files but the PNGs won't be byte-to-byte compatible with spritesheets output by earlier versions of Spreet. This also causes Spreet's minimum Rust version to be 1.61.0 68 | 69 | ## v0.5.0 (2022-12-11) 70 | 71 | - Rasterize SVGs in parallel 72 | - Add tutorial and benchmarks to README 73 | - Update clap dependency to v4 74 | - Update oxipng dependency to v6 75 | - Use tiny-skia and usvg as re-exported from resvg 76 | - Move predicates to dev-dependencies 77 | - Add CLI tests 78 | 79 | ## v0.4.0 (2022-08-16) 80 | 81 | - Switch to [crunch-rs](https://github.com/ChevyRay/crunch-rs) rectangle-packing library 82 | - Add `--minify-index-file` CLI flag (see [#15](https://github.com/flother/spreet/issues/15)) 83 | 84 | ## v0.3.0 (2022-08-08) 85 | 86 | - Add `--unique` argument (see [#14](https://github.com/flother/spreet/pull/14)) 87 | - Optimise spritesheet PNG using [`oxipng`](https://github.com/shssoichiro/oxipng) 88 | - Match the way [`spritezero-cli`](https://github.com/mapbox/spritezero-cli) traverses the input directory 89 | - Provide a Homebrew formula tap for easy MacOS/Linux installation 90 | 91 | ## v0.2.0 (2022-03-22) 92 | 93 | - Resize the target bin as required, instead of hardcoding a square 1.4 times the size of the sprites 94 | - Trim unused transparent pixels from the spritesheet (excluding transparent pixels within sprites) 95 | - Ensure target bin is at least as wide/tall as the widest/tallest sprite 96 | - Pretty-print the JSON in the sprite index file 97 | - Strip symbols from binaries using Cargo 98 | - Add GitHub Actions workflow to draft a new release when a new tag is created 99 | - Use one parallel code generation unit for release 100 | - Bump clap Rust dependency from version 3.1.5 to version 3.1.6 101 | - Bump actions/checkout GitHub Actions dependency from version 2 to version 3 102 | 103 | ## v0.1.0 (2022-03-18) 104 | 105 | - Initial beta release 106 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.15" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.8" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.5" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 55 | dependencies = [ 56 | "windows-sys 0.52.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.4" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys 0.52.0", 67 | ] 68 | 69 | [[package]] 70 | name = "arrayref" 71 | version = "0.3.8" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" 74 | 75 | [[package]] 76 | name = "arrayvec" 77 | version = "0.7.4" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 80 | 81 | [[package]] 82 | name = "assert_cmd" 83 | version = "2.0.16" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 86 | dependencies = [ 87 | "anstyle", 88 | "bstr", 89 | "doc-comment", 90 | "libc", 91 | "predicates", 92 | "predicates-core", 93 | "predicates-tree", 94 | "wait-timeout", 95 | ] 96 | 97 | [[package]] 98 | name = "assert_fs" 99 | version = "1.1.2" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674" 102 | dependencies = [ 103 | "anstyle", 104 | "doc-comment", 105 | "globwalk", 106 | "predicates", 107 | "predicates-core", 108 | "predicates-tree", 109 | "tempfile", 110 | ] 111 | 112 | [[package]] 113 | name = "assert_matches" 114 | version = "1.5.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" 117 | 118 | [[package]] 119 | name = "autocfg" 120 | version = "1.3.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 123 | 124 | [[package]] 125 | name = "base64" 126 | version = "0.22.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 129 | 130 | [[package]] 131 | name = "bitflags" 132 | version = "1.3.2" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 135 | 136 | [[package]] 137 | name = "bitflags" 138 | version = "2.6.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 141 | 142 | [[package]] 143 | name = "bitvec" 144 | version = "1.0.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 147 | dependencies = [ 148 | "funty", 149 | "radium", 150 | "tap", 151 | "wyz", 152 | ] 153 | 154 | [[package]] 155 | name = "bstr" 156 | version = "1.10.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" 159 | dependencies = [ 160 | "memchr", 161 | "regex-automata", 162 | "serde", 163 | ] 164 | 165 | [[package]] 166 | name = "bumpalo" 167 | version = "3.16.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 170 | 171 | [[package]] 172 | name = "bytemuck" 173 | version = "1.16.3" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" 176 | 177 | [[package]] 178 | name = "byteorder-lite" 179 | version = "0.1.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 182 | 183 | [[package]] 184 | name = "cc" 185 | version = "1.1.12" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7" 188 | dependencies = [ 189 | "shlex", 190 | ] 191 | 192 | [[package]] 193 | name = "cfg-if" 194 | version = "1.0.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 197 | 198 | [[package]] 199 | name = "clap" 200 | version = "4.5.15" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc" 203 | dependencies = [ 204 | "clap_builder", 205 | "clap_derive", 206 | ] 207 | 208 | [[package]] 209 | name = "clap_builder" 210 | version = "4.5.15" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" 213 | dependencies = [ 214 | "anstream", 215 | "anstyle", 216 | "clap_lex", 217 | "strsim", 218 | ] 219 | 220 | [[package]] 221 | name = "clap_derive" 222 | version = "4.5.13" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" 225 | dependencies = [ 226 | "heck", 227 | "proc-macro2", 228 | "quote", 229 | "syn", 230 | ] 231 | 232 | [[package]] 233 | name = "clap_lex" 234 | version = "0.7.2" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 237 | 238 | [[package]] 239 | name = "clap_mangen" 240 | version = "0.2.23" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" 243 | dependencies = [ 244 | "clap", 245 | "roff", 246 | ] 247 | 248 | [[package]] 249 | name = "color_quant" 250 | version = "1.1.0" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 253 | 254 | [[package]] 255 | name = "colorchoice" 256 | version = "1.0.2" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 259 | 260 | [[package]] 261 | name = "core_maths" 262 | version = "0.1.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "e3b02505ccb8c50b0aa21ace0fc08c3e53adebd4e58caa18a36152803c7709a3" 265 | dependencies = [ 266 | "libm", 267 | ] 268 | 269 | [[package]] 270 | name = "crc32fast" 271 | version = "1.4.2" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 274 | dependencies = [ 275 | "cfg-if", 276 | ] 277 | 278 | [[package]] 279 | name = "crossbeam-channel" 280 | version = "0.5.15" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 283 | dependencies = [ 284 | "crossbeam-utils", 285 | ] 286 | 287 | [[package]] 288 | name = "crossbeam-deque" 289 | version = "0.8.5" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 292 | dependencies = [ 293 | "crossbeam-epoch", 294 | "crossbeam-utils", 295 | ] 296 | 297 | [[package]] 298 | name = "crossbeam-epoch" 299 | version = "0.9.18" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 302 | dependencies = [ 303 | "crossbeam-utils", 304 | ] 305 | 306 | [[package]] 307 | name = "crossbeam-utils" 308 | version = "0.8.20" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 311 | 312 | [[package]] 313 | name = "crunch" 314 | version = "0.5.3" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "0dc013e70da3bfe5b552de26a1f34ecf67d61ea811251d2bf75c1324a1ecb425" 317 | 318 | [[package]] 319 | name = "data-url" 320 | version = "0.3.1" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" 323 | 324 | [[package]] 325 | name = "difflib" 326 | version = "0.4.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 329 | 330 | [[package]] 331 | name = "doc-comment" 332 | version = "0.3.3" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 335 | 336 | [[package]] 337 | name = "either" 338 | version = "1.13.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 341 | 342 | [[package]] 343 | name = "equivalent" 344 | version = "1.0.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 347 | 348 | [[package]] 349 | name = "errno" 350 | version = "0.3.9" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 353 | dependencies = [ 354 | "libc", 355 | "windows-sys 0.52.0", 356 | ] 357 | 358 | [[package]] 359 | name = "exitcode" 360 | version = "1.1.2" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" 363 | 364 | [[package]] 365 | name = "fastrand" 366 | version = "2.1.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 369 | 370 | [[package]] 371 | name = "fdeflate" 372 | version = "0.3.4" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" 375 | dependencies = [ 376 | "simd-adler32", 377 | ] 378 | 379 | [[package]] 380 | name = "filetime" 381 | version = "0.2.24" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550" 384 | dependencies = [ 385 | "cfg-if", 386 | "libc", 387 | "libredox", 388 | "windows-sys 0.59.0", 389 | ] 390 | 391 | [[package]] 392 | name = "flate2" 393 | version = "1.0.31" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" 396 | dependencies = [ 397 | "crc32fast", 398 | "miniz_oxide", 399 | ] 400 | 401 | [[package]] 402 | name = "float-cmp" 403 | version = "0.9.0" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 406 | dependencies = [ 407 | "num-traits", 408 | ] 409 | 410 | [[package]] 411 | name = "fontconfig-parser" 412 | version = "0.5.7" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7" 415 | dependencies = [ 416 | "roxmltree", 417 | ] 418 | 419 | [[package]] 420 | name = "fontdb" 421 | version = "0.21.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "37be9fc20d966be438cd57a45767f73349477fb0f85ce86e000557f787298afb" 424 | dependencies = [ 425 | "fontconfig-parser", 426 | "log", 427 | "memmap2", 428 | "slotmap", 429 | "tinyvec", 430 | "ttf-parser", 431 | ] 432 | 433 | [[package]] 434 | name = "funty" 435 | version = "2.0.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 438 | 439 | [[package]] 440 | name = "gif" 441 | version = "0.13.1" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" 444 | dependencies = [ 445 | "color_quant", 446 | "weezl", 447 | ] 448 | 449 | [[package]] 450 | name = "globset" 451 | version = "0.4.14" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" 454 | dependencies = [ 455 | "aho-corasick", 456 | "bstr", 457 | "log", 458 | "regex-automata", 459 | "regex-syntax", 460 | ] 461 | 462 | [[package]] 463 | name = "globwalk" 464 | version = "0.9.1" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" 467 | dependencies = [ 468 | "bitflags 2.6.0", 469 | "ignore", 470 | "walkdir", 471 | ] 472 | 473 | [[package]] 474 | name = "hashbrown" 475 | version = "0.14.5" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 478 | 479 | [[package]] 480 | name = "heck" 481 | version = "0.5.0" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 484 | 485 | [[package]] 486 | name = "ignore" 487 | version = "0.4.22" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" 490 | dependencies = [ 491 | "crossbeam-deque", 492 | "globset", 493 | "log", 494 | "memchr", 495 | "regex-automata", 496 | "same-file", 497 | "walkdir", 498 | "winapi-util", 499 | ] 500 | 501 | [[package]] 502 | name = "image-webp" 503 | version = "0.1.3" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" 506 | dependencies = [ 507 | "byteorder-lite", 508 | "quick-error", 509 | ] 510 | 511 | [[package]] 512 | name = "imagesize" 513 | version = "0.13.0" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" 516 | 517 | [[package]] 518 | name = "indexmap" 519 | version = "2.4.0" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" 522 | dependencies = [ 523 | "equivalent", 524 | "hashbrown", 525 | "rayon", 526 | ] 527 | 528 | [[package]] 529 | name = "is_terminal_polyfill" 530 | version = "1.70.1" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 533 | 534 | [[package]] 535 | name = "itoa" 536 | version = "1.0.11" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 539 | 540 | [[package]] 541 | name = "kurbo" 542 | version = "0.11.0" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" 545 | dependencies = [ 546 | "arrayvec", 547 | "smallvec", 548 | ] 549 | 550 | [[package]] 551 | name = "libc" 552 | version = "0.2.155" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 555 | 556 | [[package]] 557 | name = "libdeflate-sys" 558 | version = "1.21.0" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "7b14a6afa4e2e1d343fd793a1c0a7e5857a73a2697c2ff2c98ac00d6c4ecc820" 561 | dependencies = [ 562 | "cc", 563 | ] 564 | 565 | [[package]] 566 | name = "libdeflater" 567 | version = "1.21.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "a17fe2badabdaf756f620748311e99ef99a5fdd681562dfd343fdb16ed7d4797" 570 | dependencies = [ 571 | "libdeflate-sys", 572 | ] 573 | 574 | [[package]] 575 | name = "libm" 576 | version = "0.2.8" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 579 | 580 | [[package]] 581 | name = "libredox" 582 | version = "0.1.3" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 585 | dependencies = [ 586 | "bitflags 2.6.0", 587 | "libc", 588 | "redox_syscall", 589 | ] 590 | 591 | [[package]] 592 | name = "linux-raw-sys" 593 | version = "0.4.14" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 596 | 597 | [[package]] 598 | name = "lockfree-object-pool" 599 | version = "0.1.6" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" 602 | 603 | [[package]] 604 | name = "log" 605 | version = "0.4.22" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 608 | 609 | [[package]] 610 | name = "memchr" 611 | version = "2.7.4" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 614 | 615 | [[package]] 616 | name = "memmap2" 617 | version = "0.9.4" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" 620 | dependencies = [ 621 | "libc", 622 | ] 623 | 624 | [[package]] 625 | name = "miniz_oxide" 626 | version = "0.7.4" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 629 | dependencies = [ 630 | "adler", 631 | "simd-adler32", 632 | ] 633 | 634 | [[package]] 635 | name = "multimap" 636 | version = "0.10.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" 639 | dependencies = [ 640 | "serde", 641 | ] 642 | 643 | [[package]] 644 | name = "normalize-line-endings" 645 | version = "0.3.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 648 | 649 | [[package]] 650 | name = "num-traits" 651 | version = "0.2.19" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 654 | dependencies = [ 655 | "autocfg", 656 | ] 657 | 658 | [[package]] 659 | name = "once_cell" 660 | version = "1.19.0" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 663 | 664 | [[package]] 665 | name = "oxipng" 666 | version = "9.1.2" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "ec25597808aff9f632f018f0fe8985c6f670598ac5241d220a9f2d32ff46812e" 669 | dependencies = [ 670 | "bitvec", 671 | "clap", 672 | "clap_mangen", 673 | "crossbeam-channel", 674 | "filetime", 675 | "indexmap", 676 | "libdeflater", 677 | "log", 678 | "rayon", 679 | "rgb", 680 | "rustc-hash", 681 | "rustc_version", 682 | "zopfli", 683 | ] 684 | 685 | [[package]] 686 | name = "pico-args" 687 | version = "0.5.0" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 690 | 691 | [[package]] 692 | name = "png" 693 | version = "0.17.13" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" 696 | dependencies = [ 697 | "bitflags 1.3.2", 698 | "crc32fast", 699 | "fdeflate", 700 | "flate2", 701 | "miniz_oxide", 702 | ] 703 | 704 | [[package]] 705 | name = "predicates" 706 | version = "3.1.2" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" 709 | dependencies = [ 710 | "anstyle", 711 | "difflib", 712 | "float-cmp", 713 | "normalize-line-endings", 714 | "predicates-core", 715 | "regex", 716 | ] 717 | 718 | [[package]] 719 | name = "predicates-core" 720 | version = "1.0.8" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" 723 | 724 | [[package]] 725 | name = "predicates-tree" 726 | version = "1.0.11" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" 729 | dependencies = [ 730 | "predicates-core", 731 | "termtree", 732 | ] 733 | 734 | [[package]] 735 | name = "proc-macro2" 736 | version = "1.0.86" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 739 | dependencies = [ 740 | "unicode-ident", 741 | ] 742 | 743 | [[package]] 744 | name = "quick-error" 745 | version = "2.0.1" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 748 | 749 | [[package]] 750 | name = "quote" 751 | version = "1.0.36" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 754 | dependencies = [ 755 | "proc-macro2", 756 | ] 757 | 758 | [[package]] 759 | name = "radium" 760 | version = "0.7.0" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 763 | 764 | [[package]] 765 | name = "rayon" 766 | version = "1.10.0" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 769 | dependencies = [ 770 | "either", 771 | "rayon-core", 772 | ] 773 | 774 | [[package]] 775 | name = "rayon-core" 776 | version = "1.12.1" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 779 | dependencies = [ 780 | "crossbeam-deque", 781 | "crossbeam-utils", 782 | ] 783 | 784 | [[package]] 785 | name = "redox_syscall" 786 | version = "0.5.3" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 789 | dependencies = [ 790 | "bitflags 2.6.0", 791 | ] 792 | 793 | [[package]] 794 | name = "regex" 795 | version = "1.10.6" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 798 | dependencies = [ 799 | "aho-corasick", 800 | "memchr", 801 | "regex-automata", 802 | "regex-syntax", 803 | ] 804 | 805 | [[package]] 806 | name = "regex-automata" 807 | version = "0.4.7" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 810 | dependencies = [ 811 | "aho-corasick", 812 | "memchr", 813 | "regex-syntax", 814 | ] 815 | 816 | [[package]] 817 | name = "regex-syntax" 818 | version = "0.8.4" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 821 | 822 | [[package]] 823 | name = "resvg" 824 | version = "0.43.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "c7314563c59c7ce31c18e23ad3dd092c37b928a0fa4e1c0a1a6504351ab411d1" 827 | dependencies = [ 828 | "gif", 829 | "image-webp", 830 | "log", 831 | "pico-args", 832 | "rgb", 833 | "svgtypes", 834 | "tiny-skia", 835 | "usvg", 836 | "zune-jpeg", 837 | ] 838 | 839 | [[package]] 840 | name = "rgb" 841 | version = "0.8.48" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71" 844 | dependencies = [ 845 | "bytemuck", 846 | ] 847 | 848 | [[package]] 849 | name = "roff" 850 | version = "0.2.2" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" 853 | 854 | [[package]] 855 | name = "roxmltree" 856 | version = "0.20.0" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" 859 | 860 | [[package]] 861 | name = "rustc-hash" 862 | version = "1.1.0" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 865 | 866 | [[package]] 867 | name = "rustc_version" 868 | version = "0.4.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 871 | dependencies = [ 872 | "semver", 873 | ] 874 | 875 | [[package]] 876 | name = "rustix" 877 | version = "0.38.34" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 880 | dependencies = [ 881 | "bitflags 2.6.0", 882 | "errno", 883 | "libc", 884 | "linux-raw-sys", 885 | "windows-sys 0.52.0", 886 | ] 887 | 888 | [[package]] 889 | name = "rustybuzz" 890 | version = "0.18.0" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" 893 | dependencies = [ 894 | "bitflags 2.6.0", 895 | "bytemuck", 896 | "core_maths", 897 | "log", 898 | "smallvec", 899 | "ttf-parser", 900 | "unicode-bidi-mirroring", 901 | "unicode-ccc", 902 | "unicode-properties", 903 | "unicode-script", 904 | ] 905 | 906 | [[package]] 907 | name = "ryu" 908 | version = "1.0.18" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 911 | 912 | [[package]] 913 | name = "same-file" 914 | version = "1.0.6" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 917 | dependencies = [ 918 | "winapi-util", 919 | ] 920 | 921 | [[package]] 922 | name = "sdf_glyph_renderer" 923 | version = "1.0.1" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "5ef220f6a4fff0c984e9fb3ab12cb5c86960b5bb6ec3b30dd7173e3bf603d94f" 926 | dependencies = [ 927 | "thiserror", 928 | ] 929 | 930 | [[package]] 931 | name = "semver" 932 | version = "1.0.23" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 935 | 936 | [[package]] 937 | name = "serde" 938 | version = "1.0.208" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" 941 | dependencies = [ 942 | "serde_derive", 943 | ] 944 | 945 | [[package]] 946 | name = "serde_derive" 947 | version = "1.0.208" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" 950 | dependencies = [ 951 | "proc-macro2", 952 | "quote", 953 | "syn", 954 | ] 955 | 956 | [[package]] 957 | name = "serde_json" 958 | version = "1.0.125" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" 961 | dependencies = [ 962 | "itoa", 963 | "memchr", 964 | "ryu", 965 | "serde", 966 | ] 967 | 968 | [[package]] 969 | name = "shlex" 970 | version = "1.3.0" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 973 | 974 | [[package]] 975 | name = "simd-adler32" 976 | version = "0.3.7" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 979 | 980 | [[package]] 981 | name = "simplecss" 982 | version = "0.2.1" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" 985 | dependencies = [ 986 | "log", 987 | ] 988 | 989 | [[package]] 990 | name = "siphasher" 991 | version = "1.0.1" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 994 | 995 | [[package]] 996 | name = "slotmap" 997 | version = "1.0.7" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 1000 | dependencies = [ 1001 | "version_check", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "smallvec" 1006 | version = "1.13.2" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1009 | 1010 | [[package]] 1011 | name = "spreet" 1012 | version = "0.12.0-dev" 1013 | dependencies = [ 1014 | "assert_cmd", 1015 | "assert_fs", 1016 | "assert_matches", 1017 | "clap", 1018 | "crunch", 1019 | "exitcode", 1020 | "multimap", 1021 | "oxipng", 1022 | "png", 1023 | "predicates", 1024 | "resvg", 1025 | "sdf_glyph_renderer", 1026 | "serde", 1027 | "serde_json", 1028 | "thiserror", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "strict-num" 1033 | version = "0.1.1" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" 1036 | dependencies = [ 1037 | "float-cmp", 1038 | ] 1039 | 1040 | [[package]] 1041 | name = "strsim" 1042 | version = "0.11.1" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1045 | 1046 | [[package]] 1047 | name = "svgtypes" 1048 | version = "0.15.1" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c" 1051 | dependencies = [ 1052 | "kurbo", 1053 | "siphasher", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "syn" 1058 | version = "2.0.74" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" 1061 | dependencies = [ 1062 | "proc-macro2", 1063 | "quote", 1064 | "unicode-ident", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "tap" 1069 | version = "1.0.1" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 1072 | 1073 | [[package]] 1074 | name = "tempfile" 1075 | version = "3.12.0" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" 1078 | dependencies = [ 1079 | "cfg-if", 1080 | "fastrand", 1081 | "once_cell", 1082 | "rustix", 1083 | "windows-sys 0.59.0", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "termtree" 1088 | version = "0.4.1" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 1091 | 1092 | [[package]] 1093 | name = "thiserror" 1094 | version = "1.0.63" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 1097 | dependencies = [ 1098 | "thiserror-impl", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "thiserror-impl" 1103 | version = "1.0.63" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 1106 | dependencies = [ 1107 | "proc-macro2", 1108 | "quote", 1109 | "syn", 1110 | ] 1111 | 1112 | [[package]] 1113 | name = "tiny-skia" 1114 | version = "0.11.4" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" 1117 | dependencies = [ 1118 | "arrayref", 1119 | "arrayvec", 1120 | "bytemuck", 1121 | "cfg-if", 1122 | "log", 1123 | "png", 1124 | "tiny-skia-path", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "tiny-skia-path" 1129 | version = "0.11.4" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" 1132 | dependencies = [ 1133 | "arrayref", 1134 | "bytemuck", 1135 | "strict-num", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "tinyvec" 1140 | version = "1.8.0" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1143 | dependencies = [ 1144 | "tinyvec_macros", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "tinyvec_macros" 1149 | version = "0.1.1" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1152 | 1153 | [[package]] 1154 | name = "ttf-parser" 1155 | version = "0.24.1" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" 1158 | dependencies = [ 1159 | "core_maths", 1160 | ] 1161 | 1162 | [[package]] 1163 | name = "unicode-bidi" 1164 | version = "0.3.15" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1167 | 1168 | [[package]] 1169 | name = "unicode-bidi-mirroring" 1170 | version = "0.3.0" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" 1173 | 1174 | [[package]] 1175 | name = "unicode-ccc" 1176 | version = "0.3.0" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" 1179 | 1180 | [[package]] 1181 | name = "unicode-ident" 1182 | version = "1.0.12" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1185 | 1186 | [[package]] 1187 | name = "unicode-properties" 1188 | version = "0.1.1" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" 1191 | 1192 | [[package]] 1193 | name = "unicode-script" 1194 | version = "0.5.6" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" 1197 | 1198 | [[package]] 1199 | name = "unicode-vo" 1200 | version = "0.1.0" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" 1203 | 1204 | [[package]] 1205 | name = "usvg" 1206 | version = "0.43.0" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "6803057b5cbb426e9fb8ce2216f3a9b4ca1dd2c705ba3cbebc13006e437735fd" 1209 | dependencies = [ 1210 | "base64", 1211 | "data-url", 1212 | "flate2", 1213 | "fontdb", 1214 | "imagesize", 1215 | "kurbo", 1216 | "log", 1217 | "pico-args", 1218 | "roxmltree", 1219 | "rustybuzz", 1220 | "simplecss", 1221 | "siphasher", 1222 | "strict-num", 1223 | "svgtypes", 1224 | "tiny-skia-path", 1225 | "unicode-bidi", 1226 | "unicode-script", 1227 | "unicode-vo", 1228 | "xmlwriter", 1229 | ] 1230 | 1231 | [[package]] 1232 | name = "utf8parse" 1233 | version = "0.2.2" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1236 | 1237 | [[package]] 1238 | name = "version_check" 1239 | version = "0.9.5" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1242 | 1243 | [[package]] 1244 | name = "wait-timeout" 1245 | version = "0.2.0" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1248 | dependencies = [ 1249 | "libc", 1250 | ] 1251 | 1252 | [[package]] 1253 | name = "walkdir" 1254 | version = "2.5.0" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1257 | dependencies = [ 1258 | "same-file", 1259 | "winapi-util", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "weezl" 1264 | version = "0.1.8" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" 1267 | 1268 | [[package]] 1269 | name = "winapi-util" 1270 | version = "0.1.9" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1273 | dependencies = [ 1274 | "windows-sys 0.59.0", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "windows-sys" 1279 | version = "0.52.0" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1282 | dependencies = [ 1283 | "windows-targets", 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "windows-sys" 1288 | version = "0.59.0" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1291 | dependencies = [ 1292 | "windows-targets", 1293 | ] 1294 | 1295 | [[package]] 1296 | name = "windows-targets" 1297 | version = "0.52.6" 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" 1299 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1300 | dependencies = [ 1301 | "windows_aarch64_gnullvm", 1302 | "windows_aarch64_msvc", 1303 | "windows_i686_gnu", 1304 | "windows_i686_gnullvm", 1305 | "windows_i686_msvc", 1306 | "windows_x86_64_gnu", 1307 | "windows_x86_64_gnullvm", 1308 | "windows_x86_64_msvc", 1309 | ] 1310 | 1311 | [[package]] 1312 | name = "windows_aarch64_gnullvm" 1313 | version = "0.52.6" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1316 | 1317 | [[package]] 1318 | name = "windows_aarch64_msvc" 1319 | version = "0.52.6" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1322 | 1323 | [[package]] 1324 | name = "windows_i686_gnu" 1325 | version = "0.52.6" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1328 | 1329 | [[package]] 1330 | name = "windows_i686_gnullvm" 1331 | version = "0.52.6" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1334 | 1335 | [[package]] 1336 | name = "windows_i686_msvc" 1337 | version = "0.52.6" 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" 1339 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1340 | 1341 | [[package]] 1342 | name = "windows_x86_64_gnu" 1343 | version = "0.52.6" 1344 | source = "registry+https://github.com/rust-lang/crates.io-index" 1345 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1346 | 1347 | [[package]] 1348 | name = "windows_x86_64_gnullvm" 1349 | version = "0.52.6" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1352 | 1353 | [[package]] 1354 | name = "windows_x86_64_msvc" 1355 | version = "0.52.6" 1356 | source = "registry+https://github.com/rust-lang/crates.io-index" 1357 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1358 | 1359 | [[package]] 1360 | name = "wyz" 1361 | version = "0.5.1" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 1364 | dependencies = [ 1365 | "tap", 1366 | ] 1367 | 1368 | [[package]] 1369 | name = "xmlwriter" 1370 | version = "0.1.0" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" 1373 | 1374 | [[package]] 1375 | name = "zopfli" 1376 | version = "0.8.1" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" 1379 | dependencies = [ 1380 | "bumpalo", 1381 | "crc32fast", 1382 | "lockfree-object-pool", 1383 | "log", 1384 | "once_cell", 1385 | "simd-adler32", 1386 | ] 1387 | 1388 | [[package]] 1389 | name = "zune-core" 1390 | version = "0.4.12" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" 1393 | 1394 | [[package]] 1395 | name = "zune-jpeg" 1396 | version = "0.4.13" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" 1399 | dependencies = [ 1400 | "zune-core", 1401 | ] 1402 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spreet" 3 | version = "0.12.0-dev" 4 | edition = "2021" 5 | rust-version = "1.79" 6 | description = "Create a spritesheet from a set of SVG images" 7 | readme = "README.md" 8 | repository = "https://github.com/flother/spreet" 9 | documentation = "https://docs.rs/spreet" 10 | license = "MIT" 11 | keywords = ["sprite", "svg", "cartography", "vector-tiles", "maplibre"] 12 | categories = ["command-line-utilities", "encoding", "filesystem", "graphics"] 13 | 14 | [features] 15 | default = ["cli"] 16 | cli = ["dep:clap", "dep:exitcode"] 17 | 18 | [dependencies] 19 | clap = { version = "4.5", features = ["derive"], optional = true } 20 | crunch = "0.5.3" 21 | exitcode = { version = "1.1", optional = true } 22 | multimap = "0.10" 23 | oxipng = { version = "9.1", features = [ 24 | "parallel", 25 | "zopfli", 26 | "filetime", 27 | ], default-features = false } 28 | png = "0.17" 29 | resvg = "0.43" 30 | sdf_glyph_renderer = "1" 31 | serde = { version = "1", features = ["derive"] } 32 | serde_json = "1" 33 | thiserror = "1" 34 | 35 | [dev-dependencies] 36 | assert_cmd = "2.0" 37 | assert_fs = "1.1" 38 | assert_matches = "1.5" 39 | predicates = "3" 40 | 41 | [profile.release] 42 | lto = "thin" 43 | strip = true 44 | codegen-units = 1 45 | 46 | [[bin]] 47 | name = "spreet" 48 | required-features = ["cli"] 49 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT Licence 2 | 3 | Copyright (c) 2022 Matt Riggott 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 | # Spreet: create spritesheets from SVGs 2 | 3 | Spreet is a command-line tool that creates a [spritesheet](https://en.wikipedia.org/wiki/Spritesheet) (aka texture atlas) from a directory of SVG images. You'll need this when you create [MapLibre](https://maplibre.org/) or [Mapbox](https://docs.mapbox.com/) vector web maps, where cartographic stylesheets require that [icons be loaded from a spritesheet](https://maplibre.org/maplibre-gl-js-docs/style-spec/sprite/). 4 | 5 | Compared to other tools for creating spritesheets from SVGs, Spreet: 6 | 7 | - outputs smaller spritesheets (both fewer pixels and fewer bytes) 8 | - is a self-contained ~2.2 MB binary 9 | - is faster 10 | 11 | _Spreet_ (also _spreit_, _spret_, _sprit_) is the [Scots](https://en.wikipedia.org/wiki/Scots_language) word for a sprite, the fairy-like creature from Western folklore. 12 | 13 | [![CI status](https://github.com/flother/spreet/actions/workflows/ci.yml/badge.svg)](https://github.com/flother/spreet/actions/workflows/ci.yml) 14 | [![Latest release](https://img.shields.io/github/v/release/flother/spreet)](https://github.com/flother/spreet/releases) 15 | 16 | ## Table of contents 17 | 18 | - [Installation](#installation) 19 | - [Tutorial](#tutorial) 20 | - [Command-line usage](#command-line-usage) 21 | - [Using Spreet as a Rust library](#using-spreet-as-a-rust-library) 22 | - [Benchmarks](#benchmarks) 23 | 24 | ## Installation 25 | 26 | You can install Spreet using Homebrew, `cargo install`, by downloading pre-built binaries, or by building from source. 27 | 28 | ### Homebrew 29 | 30 | If you use [Homebrew](https://brew.sh/) on MacOS or Linux you can install Spreet from the command-line: 31 | 32 | ``` 33 | brew install flother/taps/spreet 34 | ``` 35 | 36 | (You can review [the code run by the formula](https://github.com/flother/homebrew-taps/blob/master/spreet.rb) before you install.) 37 | 38 | ### Installing from crates.io (`cargo install`) 39 | 40 | Rust's `cargo install` command lets you install a binary crate locally. You can install the latest published version of Spreet with: 41 | 42 | ``` 43 | cargo install spreet 44 | ``` 45 | 46 | ### Download pre-built binaries 47 | 48 | Pre-built binaries are provided for MacOS, Linux, and Windows. The MacOS and Linux binaries are built for both Intel and ARM CPUs. Visit the [releases](https://github.com/flother/spreet/releases) page to download the latest version of Spreet. 49 | 50 | ### Build from source 51 | 52 | You'll need a recent version of the Rust toolchain (try [Rustup](https://rustup.rs/) if you don't have it already). With that, you can check out this repository: 53 | 54 | git clone https://github.com/flother/spreet 55 | cd spreet 56 | 57 | And then build a release: 58 | 59 | cargo build --release 60 | 61 | Once finished, the built binary will be available as `./target/release/spreet`. 62 | 63 | ## Tutorial 64 | 65 | When you're making your own style for a vector map, you'll have icons that you want to appear on top of the map. Symbols for roads or icons for hospitals and schools — that sort of thing. You'll have a directory of SVGs (like the [`icons` directory in the osm-bright-gl-style](https://github.com/openmaptiles/osm-bright-gl-style/tree/8af4769692d0f9219d0936711609d580b34bf365/icons)) and you'll want to convert them into a single raster image (like the [spritesheet from osm-bright-gl-style](https://github.com/openmaptiles/osm-bright-gl-style/blob/03a529f9040cfdfd3a30fb6760fc96d0ae41cf39/sprite%402x.png)). 66 | 67 | Let's say you have a directory of SVGs named `icons` and you want to create a spritesheet named `my_style.png`. Run Spreet like this: 68 | 69 | spreet icons my_style 70 | 71 | Spreet will also create an [index file](https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/#index-file) named `my_style.json` that contains a description of the dimensions and location of each image contained in the spritesheet. 72 | 73 | If you want to create a "retina" version of the spritesheet named `my_style@2x.png`, use the `--retina` option: 74 | 75 | spreet --retina icons my_style@2x 76 | 77 | You might have multiple copies of the same icon — for example, you might use the same "open book" icon for both libraries (`library.svg`) and bookshops (`bookshop.svg`). If you pass the `--unique` option, Spreet will include only the icon once in the spritesheet, but reference it twice from the index file. This helps reduce the size of your spritesheet. 78 | 79 | spreet --retina --unique icons my_style@2x 80 | 81 | By default the JSON index file is pretty-printed, but you can minify it with the `--minify-index-file` option: 82 | 83 | spreet --retina --unique --minify-index-file icons my_style@2x 84 | 85 | When you create a spritesheet for your production environment, use `--unique --minify-index-file` for best results. 86 | 87 | ## Command-line usage 88 | 89 | ``` 90 | $ spreet --help 91 | Create a spritesheet from a set of SVG images 92 | 93 | Usage: spreet [OPTIONS] 94 | 95 | Arguments: 96 | A directory of SVGs to include in the spritesheet 97 | Name of the file in which to save the spritesheet 98 | 99 | Options: 100 | -r, --ratio Set the output pixel ratio [default: 1] 101 | --retina Set the pixel ratio to 2 (equivalent to `--ratio=2`) 102 | --unique Store only unique images in the spritesheet, and map them to multiple names 103 | --recursive Include images in sub-directories 104 | -m, --minify-index-file Remove whitespace from the JSON index file 105 | --sdf Output a spritesheet using a signed distance field for each sprite 106 | -h, --help Print help 107 | -V, --version Print version 108 | ``` 109 | 110 | ## Using Spreet as a Rust library 111 | 112 | The main purpose of Spreet is to be command-line tool, but you can also use it as a library in your own Rust code. To add Spreet as a dependency, include this in your `Cargo.toml`: 113 | 114 | ```toml 115 | spreet = { version = "0.11.0", default-features = false } 116 | ``` 117 | 118 | To learn how to build your spritesheets programmatically, see the [Spreet crate docs on docs.rs](https://docs.rs/spreet) and have a [look at the spritesheet tests](https://github.com/flother/spreet/blob/master/tests/sprite.rs). 119 | 120 | ## Benchmarks 121 | 122 | To compare the output from [spritezero](https://github.com/mapbox/spritezero-cli) and Spreet, benchmarks are run against SVG sprite sets from four diverse map styles: [osm-bright-gl-style](https://github.com/openmaptiles/osm-bright-gl-style), [openstreetmap-americana](https://github.com/ZeLonewolf/openstreetmap-americana), [mapbox-gl-styles (basic)](https://github.com/mapbox/mapbox-gl-styles), and [mapbox-gl-whaam-style](https://github.com/mapbox/mapbox-gl-whaam-style). Unique, retina spritesheets are output (`--unique --retina`), and Spreet also uses `--minify-index-file` (spritezero doesn't have that option). 123 | 124 | ### Spritesheet size (total pixels) 125 | 126 | | Map style | Spritezero pixels | Spreet pixels | Change | 127 | | :----------------------- | ----------------: | ------------: | -----: | 128 | | osm-bright-gl-style | 208,810 | 130,048 | -38% | 129 | | openstreetmap-americana | 577,548 | 389,640 | -33% | 130 | | mapbox-gl-styles (basic) | 271,488 | 258,064 | -5% | 131 | | mapbox-gl-whaam-style] | 90,944 | 59,136 | -35% | 132 | 133 | ### Spritesheet file size (bytes) 134 | 135 | | Map style | Spritezero file size | Spreet file size | Change | 136 | | :----------------------- | -------------------: | ---------------: | -----: | 137 | | osm-bright-gl-style | 43,860 | 24,588 | -44% | 138 | | openstreetmap-americana | 140,401 | 78,617 | -44% | 139 | | mapbox-gl-styles (basic) | 76,383 | 30,771 | -60% | 140 | | mapbox-gl-whaam-style | 17,342 | 5,037 | -71% | 141 | 142 | ### Index file size (bytes) 143 | 144 | | Map style | Spritezero file size | Spreet file size | Change | 145 | | :----------------------- | -------------------: | ---------------: | -----: | 146 | | osm-bright-gl-style | 10,695 | 6,957 | -35% | 147 | | openstreetmap-americana | 20,142 | 13,574 | -33% | 148 | | mapbox-gl-styles (basic) | 17,013 | 11,101 | -35% | 149 | | mapbox-gl-whaam-style | 553 | 372 | -33% | 150 | -------------------------------------------------------------------------------- /src/bin/spreet/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::str::FromStr; 3 | 4 | use clap::{ArgGroup, Parser}; 5 | 6 | /// Container for Spreet's command-line arguments. 7 | #[derive(Parser)] 8 | #[command(version, about)] 9 | #[command(group(ArgGroup::new("pixel_ratio").args(&["ratio", "retina"])))] 10 | pub struct Cli { 11 | /// A directory of SVGs to include in the spritesheet 12 | #[arg(value_parser = is_dir)] 13 | pub input: PathBuf, 14 | /// Name of the file in which to save the spritesheet 15 | pub output: String, 16 | /// Set the output pixel ratio 17 | #[arg(short, long, default_value_t = 1, value_parser = is_positive)] 18 | pub ratio: u8, 19 | /// Set the pixel ratio to 2 (equivalent to `--ratio=2`) 20 | #[arg(long)] 21 | pub retina: bool, 22 | /// Store only unique images in the spritesheet, and map them to multiple names 23 | #[arg(long)] 24 | pub unique: bool, 25 | /// Include images in sub-directories 26 | #[arg(long)] 27 | pub recursive: bool, 28 | /// Remove whitespace from the JSON index file 29 | #[arg(short, long)] 30 | pub minify_index_file: bool, 31 | /// Output a spritesheet using a signed distance field for each sprite 32 | #[arg(long)] 33 | pub sdf: bool, 34 | } 35 | 36 | /// Clap validator to ensure that a string is an existing directory. 37 | fn is_dir(p: &str) -> Result { 38 | if PathBuf::from(p).is_dir() { 39 | Ok(p.into()) 40 | } else { 41 | Err(String::from("must be an existing directory")) 42 | } 43 | } 44 | 45 | /// Clap validator to ensure that an unsigned integer parsed from a string is greater than zero. 46 | fn is_positive(s: &str) -> Result { 47 | u8::from_str(s) 48 | .map_err(|e| e.to_string()) 49 | .and_then(|result| match result { 50 | i if i > 0 => Ok(result), 51 | _ => Err(String::from("must be greater than one")), 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /src/bin/spreet/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use clap::Parser; 4 | use spreet::{get_svg_input_paths, load_svg, sprite_name, Sprite, Spritesheet}; 5 | 6 | mod cli; 7 | 8 | fn main() { 9 | let args = cli::Cli::parse(); 10 | 11 | // The ratio between the pixels in an SVG image and the pixels in the resulting PNG sprite. A 12 | // value of 2 means the PNGs will be double the size of the SVG images. 13 | let pixel_ratio = if args.retina { 2 } else { args.ratio }; 14 | 15 | // Collect the file paths for all SVG images in the input directory. 16 | // Read from all the input SVG files, convert them into bitmaps at the correct pixel ratio, and 17 | // store them in a map. The keys are the SVG filenames without the `.svg` extension. The 18 | // bitmapped SVGs will be added to the spritesheet, and the keys will be used as the unique 19 | // sprite ids in the JSON index file. 20 | let Ok(input_paths) = get_svg_input_paths(&args.input, args.recursive) else { 21 | eprintln!("Error: no valid SVGs found in {:?}", args.input); 22 | std::process::exit(exitcode::NOINPUT); 23 | }; 24 | let sprites = input_paths 25 | .iter() 26 | .map(|svg_path| { 27 | if let Ok(tree) = load_svg(svg_path) { 28 | let sprite = if args.sdf { 29 | Sprite::new_sdf(tree, pixel_ratio).expect("failed to load an SDF sprite") 30 | } else { 31 | Sprite::new(tree, pixel_ratio).expect("failed to load a sprite") 32 | }; 33 | if let Ok(name) = sprite_name(svg_path, args.input.as_path()) { 34 | (name, sprite) 35 | } else { 36 | eprintln!("Error: cannot make a valid sprite name from {svg_path:?}"); 37 | std::process::exit(exitcode::DATAERR); 38 | } 39 | } else { 40 | eprintln!("{svg_path:?}: not a valid SVG image"); 41 | std::process::exit(exitcode::DATAERR); 42 | } 43 | }) 44 | .collect::>(); 45 | 46 | if sprites.is_empty() { 47 | eprintln!("Error: no valid SVGs found in {:?}", args.input); 48 | std::process::exit(exitcode::NOINPUT); 49 | } 50 | 51 | let mut spritesheet_builder = Spritesheet::build(); 52 | spritesheet_builder.sprites(sprites); 53 | if args.unique { 54 | spritesheet_builder.make_unique(); 55 | }; 56 | if args.sdf { 57 | spritesheet_builder.make_sdf(); 58 | }; 59 | 60 | // Generate sprite sheet 61 | let Some(spritesheet) = spritesheet_builder.generate() else { 62 | eprintln!("Error: could not pack the sprites within an area fifty times their size."); 63 | std::process::exit(exitcode::DATAERR); 64 | }; 65 | 66 | // Save the bitmapped spritesheet to a local PNG. 67 | let file_prefix = args.output; 68 | let spritesheet_path = format!("{file_prefix}.png"); 69 | if let Err(e) = spritesheet.save_spritesheet(&spritesheet_path) { 70 | eprintln!("Error: could not save spritesheet to {spritesheet_path} ({e})"); 71 | std::process::exit(exitcode::IOERR); 72 | }; 73 | 74 | // Save the index file to a local JSON file with the same name as the spritesheet. 75 | if let Err(e) = spritesheet.save_index(&file_prefix, args.minify_index_file) { 76 | eprintln!("Error: could not save sprite index to {file_prefix} ({e})"); 77 | std::process::exit(exitcode::IOERR); 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::PathBuf; 3 | 4 | use oxipng::PngError; 5 | use thiserror::Error; 6 | 7 | pub type SpreetResult = Result; 8 | 9 | /// Errors encountered during execution. 10 | #[derive(Debug, Error)] 11 | pub enum SpreetError { 12 | #[error("i/o error: {0}")] 13 | IoError(#[from] io::Error), 14 | #[error("Incorrect path {}", .0.display())] 15 | PathError(PathBuf), 16 | #[error("PNG encoding error: {0}")] 17 | PngError(#[from] png::EncodingError), 18 | #[error("Oxipng error: {0}")] 19 | OxiPngError(#[from] PngError), 20 | #[error("SVG error: {0}")] 21 | SvgError(#[from] resvg::usvg::Error), 22 | } 23 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{read, read_dir, DirEntry}; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use resvg::usvg::{Options, Tree}; 5 | 6 | use crate::error::SpreetResult; 7 | 8 | /// Returns `true` if `entry`'s file name starts with `.`, `false` otherwise. 9 | fn is_hidden(entry: &DirEntry) -> bool { 10 | entry 11 | .file_name() 12 | .to_str() 13 | .map_or(false, |s| s.starts_with('.')) 14 | } 15 | 16 | /// Returns `true` if `entry` is a file with the extension `.svg`, `false` otherwise. 17 | fn is_svg_file(entry: &DirEntry) -> bool { 18 | entry.path().is_file() && entry.path().extension().map_or(false, |s| s == "svg") 19 | } 20 | 21 | /// Returns `true` if `entry` is an SVG image and isn't hidden. 22 | fn is_useful_input(entry: &DirEntry) -> bool { 23 | !is_hidden(entry) && is_svg_file(entry) 24 | } 25 | 26 | /// Returns a vector of file paths matching all SVGs within the given directory. 27 | /// 28 | /// It ignores hidden files (files whose names begin with `.`) but it does follow symlinks. If 29 | /// `recursive` is `true` it will also return file paths in sub-directories. 30 | /// 31 | /// # Errors 32 | /// 33 | /// This function will return an error if Rust's underlying [`read_dir`] returns an error. 34 | pub fn get_svg_input_paths>(path: P, recursive: bool) -> SpreetResult> { 35 | Ok(read_dir(path)? 36 | .filter_map(|entry| { 37 | if let Ok(entry) = entry { 38 | let path_buf = entry.path(); 39 | if recursive && path_buf.is_dir() { 40 | get_svg_input_paths(path_buf, true).ok() 41 | } else if is_useful_input(&entry) { 42 | Some(vec![path_buf]) 43 | } else { 44 | None 45 | } 46 | } else { 47 | None 48 | } 49 | }) 50 | .flatten() 51 | .collect()) 52 | } 53 | 54 | /// Load an SVG image from a file path. 55 | pub fn load_svg>(path: P) -> SpreetResult { 56 | // The resources directory needs to be the same location as the SVG file itself, so that any 57 | // embedded resources (like PNGs in elements) that use relative URLs can be resolved 58 | // correctly. 59 | let resources_dir = std::fs::canonicalize(&path) 60 | .ok() 61 | .and_then(|p| p.parent().map(Path::to_path_buf)); 62 | let options = Options { 63 | resources_dir, 64 | ..Options::default() 65 | }; 66 | 67 | Ok(Tree::from_data(&read(path)?, &options)?) 68 | } 69 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Re-export resvg because its types are used in the spreet API. This removes the need for users of 2 | // spreet to import resvg separately and manage version compatibility. 3 | pub use resvg; 4 | 5 | mod error; 6 | pub use error::{SpreetError, SpreetResult}; 7 | 8 | mod fs; 9 | pub use fs::*; 10 | 11 | mod sprite; 12 | pub use sprite::*; 13 | -------------------------------------------------------------------------------- /src/sprite/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::btree_map::Entry; 2 | use std::collections::BTreeMap; 3 | use std::fs::File; 4 | use std::io::Write; 5 | use std::path::Path; 6 | 7 | use crunch::{Item, PackedItem, PackedItems, Rotation}; 8 | use multimap::MultiMap; 9 | use oxipng::optimize_from_memory; 10 | use resvg::tiny_skia::{Color, Pixmap, PixmapPaint, Transform}; 11 | use resvg::usvg::{Rect, Tree}; 12 | use sdf_glyph_renderer::{clamp_to_u8, BitmapGlyph}; 13 | use serde::Serialize; 14 | 15 | use self::serialize::{serialize_rect, serialize_stretch_x_area, serialize_stretch_y_area}; 16 | pub use crate::error::{SpreetError, SpreetResult}; 17 | 18 | mod serialize; 19 | 20 | /// A single icon within a spritesheet. 21 | /// 22 | /// A sprite is a rectangular icon stored as an SVG image and converted to a bitmap. The bitmap is 23 | /// saved to a spritesheet. 24 | #[derive(Clone)] 25 | pub struct Sprite { 26 | /// Parsed source SVG image. 27 | tree: Tree, 28 | /// Ratio determining the size the destination pixels compared to the source pixels. A ratio of 29 | /// 2 means the bitmap will be scaled to be twice the size of the SVG image. 30 | pixel_ratio: u8, 31 | /// Bitmap image generated from the SVG image. 32 | pixmap: Pixmap, 33 | } 34 | 35 | impl Sprite { 36 | pub fn new(tree: Tree, pixel_ratio: u8) -> Option { 37 | let pixel_ratio_f32 = pixel_ratio.into(); 38 | let pixmap_size = tree.size().to_int_size().scale_by(pixel_ratio_f32)?; 39 | let mut pixmap = Pixmap::new(pixmap_size.width(), pixmap_size.height())?; 40 | let render_ts = Transform::from_scale(pixel_ratio_f32, pixel_ratio_f32); 41 | resvg::render(&tree, render_ts, &mut pixmap.as_mut()); 42 | Some(Self { 43 | tree, 44 | pixel_ratio, 45 | pixmap, 46 | }) 47 | } 48 | 49 | /// Create a sprite by rasterising an SVG, generating its signed distance field, and storing 50 | /// that in the sprite's alpha channel. 51 | /// 52 | /// The method comes from Valve's original 2007 paper, [Improved alpha-tested magnification for 53 | /// vector textures and special effects][1] and its general implementation is available in the 54 | /// [sdf_glyph_renderer][2] crate. There are [further details in this blog post from 55 | /// demofox.org][3]. 56 | /// 57 | /// There are SDF value [cut-offs and ranges][4] specific to Mapbox and MapLibre icons: 58 | /// 59 | /// > To render images with signed distance fields, we create a glyph texture that stores the 60 | /// > distance to the next outline in every pixel. Inside of a glyph, the distance is negative; 61 | /// > outside, it is positive. As an additional optimization, to fit into a one-byte unsigned 62 | /// > integer, Mapbox shifts these ranges so that values between 192 and 255 represent “inside” 63 | /// > a glyph and values from 0 to 191 represent "outside". This gives the appearance of a range 64 | /// > of values from black (0) to white (255). 65 | /// 66 | /// JavaScript code for [handling the cut-off][5] is available in Elastic's fork of Fontnik. 67 | /// 68 | /// Note SDF icons are buffered by 3px on each side and so are 6px wider and 6px higher than the 69 | /// original SVG image.. 70 | /// 71 | /// # Panics 72 | /// 73 | /// This function can panic if: 74 | /// - The `Color::from_rgba` function fails to create a color. 75 | /// 76 | /// [1]: https://dl.acm.org/doi/10.1145/1281500.1281665 77 | /// [2]: https://crates.io/crates/sdf_glyph_renderer 78 | /// [3]: https://blog.demofox.org/2014/06/30/distance-field-textures/ 79 | /// [4]: https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/ 80 | /// [5]: https://github.com/elastic/fontnik/blob/fcaecc174d7561d9147499ba4f254dc7e1b0feea/lib/sdf.js#L225-L230 81 | pub fn new_sdf(tree: Tree, pixel_ratio: u8) -> Option { 82 | let pixel_ratio_f32 = pixel_ratio.into(); 83 | let unbuff_pixmap_size = tree.size().to_int_size().scale_by(pixel_ratio_f32)?; 84 | let mut unbuff_pixmap = 85 | Pixmap::new(unbuff_pixmap_size.width(), unbuff_pixmap_size.height())?; 86 | let render_ts = Transform::from_scale(pixel_ratio_f32, pixel_ratio_f32); 87 | resvg::render(&tree, render_ts, &mut unbuff_pixmap.as_mut()); 88 | 89 | // Buffer from https://github.com/elastic/spritezero/blob/3b89dc0fef2acbf9db1e77a753a68b02f74939a8/index.js#L144 90 | let buffer = 3_i32; 91 | let mut buff_pixmap = Pixmap::new( 92 | unbuff_pixmap_size.width() + 2 * buffer as u32, 93 | unbuff_pixmap_size.height() + 2 * buffer as u32, 94 | )?; 95 | buff_pixmap.draw_pixmap( 96 | buffer, 97 | buffer, 98 | unbuff_pixmap.as_ref(), 99 | &PixmapPaint::default(), 100 | Transform::default(), 101 | None, 102 | ); 103 | let alpha = buff_pixmap 104 | .pixels() 105 | .iter() 106 | .map(|pixel| pixel.alpha()) 107 | .collect::>(); 108 | let bitmap = BitmapGlyph::new( 109 | alpha, 110 | unbuff_pixmap_size.width() as usize, 111 | unbuff_pixmap_size.height() as usize, 112 | buffer as usize, 113 | ) 114 | .ok()?; 115 | // Radius and cutoff are recommended to be 8 and 0.25 respectively. Taken from 116 | // https://github.com/stadiamaps/sdf_font_tools/blob/97c5634b8e3515ac7761d0a4f67d12e7f688b042/pbf_font_tools/src/ft_generate.rs#L32-L34 117 | let colors = clamp_to_u8(&bitmap.render_sdf(8), 0.25) 118 | .ok()? 119 | .into_iter() 120 | .map(|alpha| { 121 | Color::from_rgba(0.0, 0.0, 0.0, alpha as f32 / 255.0) 122 | .unwrap() 123 | .premultiply() 124 | .to_color_u8() 125 | }) 126 | .collect::>(); 127 | for (i, pixel) in buff_pixmap.pixels_mut().iter_mut().enumerate() { 128 | *pixel = colors[i]; 129 | } 130 | 131 | Some(Self { 132 | tree, 133 | pixel_ratio, 134 | pixmap: buff_pixmap, 135 | }) 136 | } 137 | 138 | /// Get the sprite's SVG tree. 139 | pub fn tree(&self) -> &Tree { 140 | &self.tree 141 | } 142 | 143 | /// Get the sprite's pixel ratio. 144 | pub fn pixel_ratio(&self) -> u8 { 145 | self.pixel_ratio 146 | } 147 | 148 | /// Generate a bitmap image from the sprite's SVG tree. 149 | /// 150 | /// The bitmap is generated at the sprite's [pixel ratio](Self::pixel_ratio). 151 | pub fn pixmap(&self) -> &Pixmap { 152 | &self.pixmap 153 | } 154 | 155 | /// Metadata for a [stretchable icon]. 156 | /// 157 | /// Describes the content area of an icon as a [`Rect`]. The metadata comes from the bounding 158 | /// box of an element in the SVG image that has the id `mapbox-content`. 159 | /// 160 | /// Most icons do not specify a content area. But if it is present and the MapLibre/Mapbox map 161 | /// symbol uses [`icon-text-fit`], the symbol's text will be fitted inside this content box. 162 | /// 163 | /// [stretchable icon]: https://github.com/mapbox/mapbox-gl-js/issues/8917 164 | /// [`icon-text-fit`]: https://maplibre.org/maplibre-style-spec/layers/#icon-text-fit 165 | pub fn content_area(&self) -> Option { 166 | self.get_node_bbox("mapbox-content") 167 | } 168 | 169 | /// Metadata for a [stretchable icon]. 170 | /// 171 | /// Describes the horizontal position of areas that can be stretched. There may be multiple 172 | /// areas. The metadata comes from the bounding boxes of elements in the SVG image that have 173 | /// ids like `mapbox-stretch-x-1`. Although the entire bounding box is provided, only the left 174 | /// and right edges are stored in the index file and used by MapLibre/Mapbox to define the 175 | /// stretchable area. 176 | /// 177 | /// Most icons do not specify stretchable areas. See also [`Sprite::content_area`]. 178 | /// 179 | /// [stretchable icon]: https://github.com/mapbox/mapbox-gl-js/issues/8917 180 | pub fn stretch_x_areas(&self) -> Option> { 181 | let mut values = vec![]; 182 | // First look for an SVG element with the id `mapbox-stretch-x`. 183 | if let Some(rect) = self.get_node_bbox("mapbox-stretch-x") { 184 | values.push(rect); 185 | } 186 | // Next look for SVG elements with ids like `mapbox-stretch-x-1`. As soon as one is missing, 187 | // stop looking. 188 | for i in 1.. { 189 | if let Some(rect) = self.get_node_bbox(format!("mapbox-stretch-x-{i}").as_str()) { 190 | values.push(rect); 191 | } else { 192 | break; 193 | } 194 | } 195 | if values.is_empty() { 196 | // If there are no SVG elements with `mapbox-stretch-x` ids, check for an element with 197 | // the id `mapbox-stretch`. That's a shorthand for stretch-x and stretch-y. If that 198 | // exists, use its horizontal coordinates. 199 | self.get_node_bbox("mapbox-stretch").map(|rect| vec![rect]) 200 | } else { 201 | Some(values) 202 | } 203 | } 204 | 205 | /// Metadata for a [stretchable icon]. 206 | /// 207 | /// Describes the vertical position of areas that can be stretched. There may be multiple areas. 208 | /// The metadata comes from the bounding boxes of elements in the SVG image that have ids like 209 | /// `mapbox-stretch-y-1`. Although the entire bounding box is provided, only the top and bottom 210 | /// edges are stored in the index file and used by MapLibre/Mapbox to define the stretchable 211 | /// area. 212 | /// 213 | /// Most icons do not specify stretchable areas. See also [`Sprite::content_area`]. 214 | /// 215 | /// [stretchable icon]: https://github.com/mapbox/mapbox-gl-js/issues/8917 216 | pub fn stretch_y_areas(&self) -> Option> { 217 | let mut values = vec![]; 218 | // First look for an SVG element with the id `mapbox-stretch-y`. 219 | if let Some(rect) = self.get_node_bbox("mapbox-stretch-y") { 220 | values.push(rect); 221 | } 222 | // Next look for SVG elements with ids like `mapbox-stretch-y-1`. As soon as one is missing, 223 | // stop looking. 224 | for i in 1.. { 225 | if let Some(rect) = self.get_node_bbox(format!("mapbox-stretch-y-{i}").as_str()) { 226 | values.push(rect); 227 | } else { 228 | break; 229 | } 230 | } 231 | if values.is_empty() { 232 | // If there are no SVG elements with `mapbox-stretch-x` ids, check for an element with 233 | // the id `mapbox-stretch`. That's a shorthand for stretch-x and stretch-y. If that 234 | // exists, use its vertical coordinates. 235 | self.get_node_bbox("mapbox-stretch").map(|rect| vec![rect]) 236 | } else { 237 | Some(values) 238 | } 239 | } 240 | 241 | /// Find a node in the SVG tree with a given id, and return its bounding box with coordinates 242 | /// multiplied by the sprite's pixel ratio. 243 | fn get_node_bbox(&self, id: &str) -> Option { 244 | let bbox = self.tree.node_by_id(id)?.abs_bounding_box(); 245 | let ratio = self.pixel_ratio as f32; 246 | Rect::from_ltrb( 247 | bbox.left() * ratio, 248 | bbox.top() * ratio, 249 | bbox.right() * ratio, 250 | bbox.bottom() * ratio, 251 | ) 252 | } 253 | } 254 | 255 | /// A description of a sprite image within a spritesheet. Used for the JSON output required by a 256 | /// Mapbox Style Specification [index file]. 257 | /// 258 | /// [index file]: https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/#index-file 259 | #[derive(Clone, Serialize)] 260 | #[serde(rename_all = "camelCase")] 261 | pub struct SpriteDescription { 262 | pub height: u32, 263 | pub pixel_ratio: u8, 264 | pub width: u32, 265 | pub x: u32, 266 | pub y: u32, 267 | #[serde( 268 | skip_serializing_if = "Option::is_none", 269 | serialize_with = "serialize_rect" 270 | )] 271 | pub content: Option, 272 | #[serde( 273 | skip_serializing_if = "Option::is_none", 274 | serialize_with = "serialize_stretch_x_area" 275 | )] 276 | pub stretch_x: Option>, 277 | #[serde( 278 | skip_serializing_if = "Option::is_none", 279 | serialize_with = "serialize_stretch_y_area" 280 | )] 281 | pub stretch_y: Option>, 282 | #[serde(skip_serializing_if = "std::ops::Not::not")] 283 | pub sdf: bool, 284 | } 285 | 286 | impl SpriteDescription { 287 | pub(crate) fn new(rect: &crunch::Rect, sprite: &Sprite, sdf: bool) -> Self { 288 | Self { 289 | height: rect.h as u32, 290 | width: rect.w as u32, 291 | pixel_ratio: sprite.pixel_ratio, 292 | x: rect.x as u32, 293 | y: rect.y as u32, 294 | content: sprite.content_area(), 295 | stretch_x: sprite.stretch_x_areas(), 296 | stretch_y: sprite.stretch_y_areas(), 297 | sdf, 298 | } 299 | } 300 | } 301 | 302 | /// Builder pattern for `Spritesheet`: construct a `Spritesheet` object using calls to a builder 303 | /// helper. 304 | #[derive(Default, Clone)] 305 | pub struct SpritesheetBuilder { 306 | sprites: Option>, 307 | references: Option>, 308 | sdf: bool, 309 | } 310 | 311 | impl SpritesheetBuilder { 312 | pub fn new() -> Self { 313 | Self { 314 | sprites: None, 315 | references: None, 316 | sdf: false, 317 | } 318 | } 319 | 320 | pub fn sprites(&mut self, sprites: BTreeMap) -> &mut Self { 321 | self.sprites = Some(sprites); 322 | self 323 | } 324 | 325 | // Remove any duplicate sprites from the spritesheet's sprites. This is used to let spritesheets 326 | // include only unique sprites, with multiple references to the same sprite in the index file. 327 | pub fn make_unique(&mut self) -> &mut Self { 328 | match self.sprites.take() { 329 | Some(sprites) => { 330 | let mut unique_sprites = BTreeMap::new(); 331 | let mut references = MultiMap::new(); 332 | let mut names_for_sprites: BTreeMap, String> = BTreeMap::new(); 333 | for (name, sprite) in sprites { 334 | let sprite_data = sprite.pixmap().encode_png().unwrap(); 335 | match names_for_sprites.entry(sprite_data) { 336 | Entry::Occupied(existing_sprite_name) => { 337 | references.insert(existing_sprite_name.get().clone(), name); 338 | } 339 | Entry::Vacant(entry) => { 340 | entry.insert(name.clone()); 341 | unique_sprites.insert(name, sprite); 342 | } 343 | } 344 | } 345 | self.sprites = Some(unique_sprites); 346 | self.references = Some(references); 347 | } 348 | None => { 349 | self.references = None; 350 | } 351 | } 352 | self 353 | } 354 | 355 | /// Add metadata to indicate that all images are SDF sprites. 356 | /// 357 | /// You have to ensure that the sprites are created as an SDF file beforehand. See 358 | /// [`Sprite::new_sdf`] for further context. 359 | pub fn make_sdf(&mut self) -> &mut Self { 360 | self.sdf = true; 361 | self 362 | } 363 | 364 | pub fn generate(self) -> Option { 365 | Spritesheet::new( 366 | self.sprites.unwrap_or_default(), 367 | self.references.unwrap_or_default(), 368 | self.sdf, 369 | ) 370 | } 371 | } 372 | 373 | // A bitmapped spritesheet and its matching index. 374 | pub struct Spritesheet { 375 | sheet: Pixmap, 376 | index: BTreeMap, 377 | } 378 | 379 | struct PixmapItem { 380 | name: String, 381 | sprite: Sprite, 382 | } 383 | 384 | impl Spritesheet { 385 | pub fn new( 386 | sprites: BTreeMap, 387 | references: MultiMap, 388 | sdf: bool, 389 | ) -> Option { 390 | let mut data_items = Vec::new(); 391 | let mut min_area: usize = 0; 392 | 393 | // The items are the rectangles that we want to pack into the smallest space possible. We 394 | // don't need to pass the pixels themselves, just the unique name for each sprite. 395 | for (name, sprite) in sprites { 396 | // Minimum area required for the spritesheet (i.e. 100% coverage). 397 | min_area += (sprite.pixmap().width() * sprite.pixmap().height()) as usize; 398 | data_items.push(PixmapItem { name, sprite }); 399 | } 400 | 401 | let items = data_items 402 | .iter() 403 | .map(|data| { 404 | Item::new( 405 | data, 406 | data.sprite.pixmap.width() as usize, 407 | data.sprite.pixmap.height() as usize, 408 | Rotation::None, 409 | ) 410 | }) 411 | .collect::>(); 412 | 413 | let PackedItems { items, .. } = crunch::pack_into_po2(min_area * 10, items).ok()?; 414 | 415 | // There might be some unused space in the packed items --- not all the pixels on 416 | // the right/bottom edges may have been used. Count the pixels in use so we can 417 | // strip off any empty edges in the final spritesheet. The won't strip any 418 | // transparent pixels within a sprite, just unused pixels around the sprites. 419 | let bin_width = items 420 | .iter() 421 | .map(|PackedItem { rect, .. }| rect.right()) 422 | .max()? as u32; 423 | let bin_height = items 424 | .iter() 425 | .map(|PackedItem { rect, .. }| rect.bottom()) 426 | .max()? as u32; 427 | // This is the meat of Spreet. Here we pack the sprite bitmaps into the spritesheet, 428 | // using the rectangle locations from the previous step, and store those locations 429 | // in the vector that will be output as the sprite index file. 430 | let mut index = BTreeMap::new(); 431 | let mut sheet = Pixmap::new(bin_width, bin_height)?; 432 | let pixmap_paint = PixmapPaint::default(); 433 | let pixmap_transform = Transform::default(); 434 | for PackedItem { rect, data } in items { 435 | sheet.draw_pixmap( 436 | rect.x as i32, 437 | rect.y as i32, 438 | data.sprite.pixmap.as_ref(), 439 | &pixmap_paint, 440 | pixmap_transform, 441 | None, 442 | ); 443 | index.insert( 444 | data.name.to_string(), 445 | SpriteDescription::new(&rect, &data.sprite, sdf), 446 | ); 447 | // If multiple names are used for a unique sprite, insert an entry in the index 448 | // for each of the other names. This is to allow for multiple names to reference 449 | // the same SVG image without having to include it in the spritesheet multiple 450 | // times. The `--unique` // command-flag can be used to control this behaviour. 451 | if let Some(other_sprite_names) = references.get_vec(&data.name) { 452 | for other_sprite_name in other_sprite_names { 453 | index.insert( 454 | other_sprite_name.to_string(), 455 | SpriteDescription::new(&rect, &data.sprite, sdf), 456 | ); 457 | } 458 | } 459 | } 460 | 461 | Some(Spritesheet { sheet, index }) 462 | } 463 | 464 | pub fn build() -> SpritesheetBuilder { 465 | SpritesheetBuilder::new() 466 | } 467 | 468 | /// Encode the spritesheet to the in-memory PNG image. 469 | /// 470 | /// The `spritesheet` `Pixmap` is converted to an in-memory PNG, optimised using the [`oxipng`] 471 | /// library. 472 | /// 473 | /// The spritesheet will match an index that can be retrieved with [`Self::get_index`]. 474 | /// 475 | /// [`oxipng`]: https://github.com/shssoichiro/oxipng 476 | pub fn encode_png(&self) -> SpreetResult> { 477 | Ok(optimize_from_memory( 478 | self.sheet.encode_png()?.as_slice(), 479 | &oxipng::Options::default(), 480 | )?) 481 | } 482 | 483 | /// Saves the spritesheet to a local file named `path`. 484 | /// 485 | /// A spritesheet, called an [image file] in the Mapbox Style Specification, is a PNG image 486 | /// containing all the individual sprite images. The `spritesheet` `Pixmap` is converted to an 487 | /// in-memory PNG, optimised using the [`oxipng`] library, and saved to a local file. 488 | /// 489 | /// The spritesheet will match an index file that can be saved with [`Self::save_index`]. 490 | /// 491 | /// [image file]: https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/#image-file 492 | /// [`oxipng`]: https://github.com/shssoichiro/oxipng 493 | pub fn save_spritesheet>(&self, path: P) -> SpreetResult<()> { 494 | Ok(std::fs::write(path, self.encode_png()?)?) 495 | } 496 | 497 | /// Get the `sprite_index` that can be serialized to JSON. 498 | /// 499 | /// An [index file] is defined in the Mapbox Style Specification as a JSON document containing a 500 | /// description of each sprite within a spritesheet. It contains the width, height, x and y 501 | /// positions, and pixel ratio of the sprite. 502 | /// 503 | /// The index file will match a spritesheet that can be saved with [`Self::save_spritesheet`]. 504 | /// 505 | /// [index file]: https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/#index-file 506 | pub fn get_index(&self) -> &BTreeMap { 507 | &self.index 508 | } 509 | 510 | /// Saves the `sprite_index` to a local file named `file_name_prefix` + ".json". 511 | /// 512 | /// An [index file] is defined in the Mapbox Style Specification as a JSON document containing a 513 | /// description of each sprite within a spritesheet. It contains the width, height, x and y 514 | /// positions, and pixel ratio of the sprite. 515 | /// 516 | /// The index file will match a spritesheet that can be saved with [`Self::save_spritesheet`]. 517 | /// 518 | /// [index file]: https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/#index-file 519 | pub fn save_index(&self, file_name_prefix: &str, minify: bool) -> std::io::Result<()> { 520 | let mut file = File::create(format!("{file_name_prefix}.json"))?; 521 | let json_string = if minify { 522 | serde_json::to_string(&self.get_index())? 523 | } else { 524 | serde_json::to_string_pretty(&self.get_index())? 525 | }; 526 | write!(file, "{json_string}")?; 527 | Ok(()) 528 | } 529 | } 530 | 531 | /// Returns the name (unique id within a spritesheet) taken from a file. 532 | /// 533 | /// The unique sprite name is the relative path from `path` to `base_path` 534 | /// without the file extension. 535 | /// 536 | /// # Errors 537 | /// 538 | /// This function will return an error if: 539 | /// 540 | /// - `abs_path` is not an ancestor of `path` 541 | /// - `path` is empty 542 | /// - getting the current directory fails 543 | pub fn sprite_name, P2: AsRef>( 544 | path: P1, 545 | base_path: P2, 546 | ) -> SpreetResult { 547 | let abs_path = std::path::absolute(path.as_ref())?; 548 | let abs_base_path = std::path::absolute(base_path)?; 549 | let Ok(rel_path) = abs_path.strip_prefix(abs_base_path) else { 550 | return Err(SpreetError::PathError(path.as_ref().to_path_buf())); 551 | }; 552 | 553 | let Some(file_stem) = path.as_ref().file_stem() else { 554 | return Err(SpreetError::PathError(path.as_ref().to_path_buf())); 555 | }; 556 | if let Some(parent) = rel_path.parent() { 557 | Ok(format!("{}", parent.join(file_stem).to_string_lossy())) 558 | } else { 559 | Ok(format!("{}", file_stem.to_string_lossy())) 560 | } 561 | } 562 | -------------------------------------------------------------------------------- /src/sprite/serialize.rs: -------------------------------------------------------------------------------- 1 | use resvg::usvg::Rect; 2 | use serde::ser::SerializeSeq; 3 | use serde::{Serialize, Serializer}; 4 | 5 | /// Custom Serde field serialiser for [`Rect`]. 6 | /// 7 | /// Serialises an [`f32`] with a zero fractional part as a [`u32`], and otherwise as an `f32` 8 | /// unchanged. Allows JSON outputted by Spreet to match the JavaScript style of intermingling 9 | /// integers and floats (because there's only one number type in JavaScript). 10 | /// 11 | /// Used to serialise a stretchable icon's [content area](`super::Sprite::content_area`). The 12 | /// serialised data is almost always integers (exceptions being transformed or rotated elements) and 13 | /// so it's a waste to serialise the extra `.0` every time. 14 | pub fn serialize_rect(rect: &Option, serializer: S) -> Result 15 | where 16 | S: Serializer, 17 | { 18 | match rect { 19 | Some(r) => { 20 | let mut seq = serializer.serialize_seq(Some(4))?; 21 | for num in [r.left(), r.top(), r.right(), r.bottom()] { 22 | if num.fract() == 0.0 { 23 | seq.serialize_element(&(num.round() as i32))?; 24 | } else { 25 | seq.serialize_element(&((num * 1e3).round() / 1e3))?; 26 | } 27 | } 28 | seq.end() 29 | } 30 | None => serializer.serialize_none(), 31 | } 32 | } 33 | 34 | /// Custom Serde field serialiser for a vector of [`Rect`]s. 35 | /// 36 | /// Serialises the left and right edges of each `Rect` as a [`u32`] if the value has no fractional 37 | /// part, or an unchanged [`f32`] otherwise. Allows JSON outputted by Spreet to match the JavaScript 38 | /// style of intermingling integers and floats (because there's only one number type in JavaScript). 39 | /// 40 | /// Used to serialise a stretchable icon's [stretch-x areas](`super::Sprite::stretch_x_areas`). The 41 | /// serialised data is almost always integers (exceptions being transformed or rotated elements) and 42 | /// so it's a waste to serialise the extra `.0` every time. 43 | pub fn serialize_stretch_x_area( 44 | rects: &Option>, 45 | serializer: S, 46 | ) -> Result 47 | where 48 | S: Serializer, 49 | { 50 | match rects { 51 | Some(rects) => { 52 | let mut seq = serializer.serialize_seq(Some(rects.len()))?; 53 | for rect in rects { 54 | let line = [icon_number(rect.left()), icon_number(rect.right())]; 55 | seq.serialize_element(&line)?; 56 | } 57 | seq.end() 58 | } 59 | None => serializer.serialize_none(), 60 | } 61 | } 62 | 63 | /// Custom Serde field serialiser for a vector of [`Rect`]s. 64 | /// 65 | /// Serialises the top and bottom edges of each `Rect` as a [`u32`] if the value has no fractional 66 | /// part, or an unchanged [`f32`] otherwise. Allows JSON outputted by Spreet to match the JavaScript 67 | /// style of intermingling integers and floats (because there's only one number type in JavaScript). 68 | /// 69 | /// Used to serialise a stretchable icon's [stretch-y areas](`super::Sprite::stretch_y_areas`). The 70 | /// serialised data is almost always integers (exceptions being transformed or rotated elements) and 71 | /// so it's a waste to serialise the extra `.0` every time. 72 | pub fn serialize_stretch_y_area( 73 | rects: &Option>, 74 | serializer: S, 75 | ) -> Result 76 | where 77 | S: Serializer, 78 | { 79 | match rects { 80 | Some(rects) => { 81 | let mut seq = serializer.serialize_seq(Some(rects.len()))?; 82 | for rect in rects { 83 | let line = [icon_number(rect.top()), icon_number(rect.bottom())]; 84 | seq.serialize_element(&line)?; 85 | } 86 | seq.end() 87 | } 88 | None => serializer.serialize_none(), 89 | } 90 | } 91 | 92 | /// Represents a number, whether integer or floating point, that can be serialised to JSON. 93 | #[derive(Serialize)] 94 | #[serde(untagged)] 95 | enum Number { 96 | Int(u32), 97 | Float(f32), 98 | } 99 | 100 | /// Converts an [`f32`] to a [`Number`], so it can be serialised to JSON in its most minimal form. 101 | fn icon_number(num: f32) -> Number { 102 | if num.fract() == 0.0 { 103 | Number::Int(num.round() as u32) 104 | } else { 105 | Number::Float((num * 1e3).round() / 1e3) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "cli")] 2 | 3 | use std::path::Path; 4 | use std::process::Command; 5 | 6 | use assert_cmd::prelude::*; 7 | use predicates::prelude::*; 8 | 9 | #[test] 10 | fn spreet_can_run_successfully() -> Result<(), Box> { 11 | let temp = assert_fs::TempDir::new().unwrap(); 12 | 13 | let mut cmd = Command::cargo_bin("spreet")?; 14 | cmd.arg("tests/fixtures/svgs") 15 | .arg(temp.join("can_run")) 16 | .arg("--ratio") 17 | .arg("2") 18 | .assert() 19 | .success(); 20 | 21 | Ok(()) 22 | } 23 | 24 | #[test] 25 | fn spreet_can_output_spritesheet() -> Result<(), Box> { 26 | let temp = assert_fs::TempDir::new().unwrap(); 27 | 28 | let mut cmd = Command::cargo_bin("spreet")?; 29 | cmd.arg("tests/fixtures/svgs") 30 | .arg(temp.join("default")) 31 | .assert() 32 | .success(); 33 | 34 | let expected_spritesheet = Path::new("tests/fixtures/output/default@1x.png"); 35 | let actual_spritesheet = predicate::path::eq_file(temp.join("default.png")); 36 | let expected_index = Path::new("tests/fixtures/output/default@1x.json"); 37 | let actual_index = predicate::path::eq_file(temp.join("default.json")); 38 | 39 | assert!(actual_spritesheet.eval(expected_spritesheet)); 40 | assert!(actual_index.eval(expected_index)); 41 | 42 | Ok(()) 43 | } 44 | 45 | #[test] 46 | fn spreet_can_output_unique_spritesheet() -> Result<(), Box> { 47 | let temp = assert_fs::TempDir::new().unwrap(); 48 | 49 | let mut cmd = Command::cargo_bin("spreet")?; 50 | cmd.arg("tests/fixtures/svgs") 51 | .arg(temp.join("unique")) 52 | .arg("--unique") 53 | .assert() 54 | .success(); 55 | 56 | let expected_spritesheet = Path::new("tests/fixtures/output/unique@1x.png"); 57 | let actual_spritesheet = predicate::path::eq_file(temp.join("unique.png")); 58 | let expected_index = Path::new("tests/fixtures/output/unique@1x.json"); 59 | let actual_index = predicate::path::eq_file(temp.join("unique.json")); 60 | 61 | assert!(actual_spritesheet.eval(expected_spritesheet)); 62 | assert!(actual_index.eval(expected_index)); 63 | 64 | Ok(()) 65 | } 66 | 67 | #[test] 68 | fn spreet_can_output_retina_spritesheet() -> Result<(), Box> { 69 | let temp = assert_fs::TempDir::new().unwrap(); 70 | 71 | let mut cmd = Command::cargo_bin("spreet")?; 72 | cmd.arg("tests/fixtures/svgs") 73 | .arg(temp.join("default@2x")) 74 | .arg("--retina") 75 | .assert() 76 | .success(); 77 | 78 | let expected_spritesheet = Path::new("tests/fixtures/output/default@2x.png"); 79 | let actual_spritesheet = predicate::path::eq_file(temp.join("default@2x.png")); 80 | let expected_index = Path::new("tests/fixtures/output/default@2x.json"); 81 | let actual_index = predicate::path::eq_file(temp.join("default@2x.json")); 82 | 83 | assert!(actual_spritesheet.eval(expected_spritesheet)); 84 | assert!(actual_index.eval(expected_index)); 85 | 86 | Ok(()) 87 | } 88 | 89 | #[test] 90 | fn spreet_can_output_recursive_spritesheet() -> Result<(), Box> { 91 | let temp = assert_fs::TempDir::new().unwrap(); 92 | 93 | let mut cmd = Command::cargo_bin("spreet")?; 94 | cmd.arg("tests/fixtures/svgs") 95 | .arg(temp.join("recursive")) 96 | .arg("--recursive") 97 | .assert() 98 | .success(); 99 | 100 | let expected_spritesheet = Path::new("tests/fixtures/output/recursive@1x.png"); 101 | let actual_spritesheet = predicate::path::eq_file(temp.join("recursive.png")); 102 | let expected_index = Path::new("tests/fixtures/output/recursive@1x.json"); 103 | let actual_index = predicate::path::eq_file(temp.join("recursive.json")); 104 | 105 | assert!(actual_spritesheet.eval(expected_spritesheet)); 106 | assert!(actual_index.eval(expected_index)); 107 | 108 | Ok(()) 109 | } 110 | 111 | #[test] 112 | fn spreet_can_output_unique_retina_spritesheet() -> Result<(), Box> { 113 | let temp = assert_fs::TempDir::new().unwrap(); 114 | 115 | let mut cmd = Command::cargo_bin("spreet")?; 116 | cmd.arg("tests/fixtures/svgs") 117 | .arg(temp.join("unique@2x")) 118 | .arg("--retina") 119 | .arg("--unique") 120 | .assert() 121 | .success(); 122 | 123 | let expected_spritesheet = Path::new("tests/fixtures/output/unique@2x.png"); 124 | let actual_spritesheet = predicate::path::eq_file(temp.join("unique@2x.png")); 125 | let expected_index = Path::new("tests/fixtures/output/unique@2x.json"); 126 | let actual_index = predicate::path::eq_file(temp.join("unique@2x.json")); 127 | 128 | assert!(actual_spritesheet.eval(expected_spritesheet)); 129 | assert!(actual_index.eval(expected_index)); 130 | 131 | Ok(()) 132 | } 133 | 134 | #[test] 135 | fn spreet_can_output_minified_index_file() -> Result<(), Box> { 136 | let temp = assert_fs::TempDir::new().unwrap(); 137 | 138 | let mut cmd = Command::cargo_bin("spreet")?; 139 | cmd.arg("tests/fixtures/svgs") 140 | .arg(temp.join("minify")) 141 | .arg("--minify-index-file") 142 | .assert() 143 | .success(); 144 | 145 | let expected_spritesheet = Path::new("tests/fixtures/output/minify@1x.png"); 146 | let actual_spritesheet = predicate::path::eq_file(temp.join("minify.png")); 147 | let expected_index = Path::new("tests/fixtures/output/minify@1x.json"); 148 | let actual_index = predicate::path::eq_file(temp.join("minify.json")); 149 | 150 | assert!(actual_spritesheet.eval(expected_spritesheet)); 151 | assert!(actual_index.eval(expected_index)); 152 | 153 | Ok(()) 154 | } 155 | 156 | #[test] 157 | fn spreet_can_output_minified_index_file_and_unique_spritesheet( 158 | ) -> Result<(), Box> { 159 | let temp = assert_fs::TempDir::new().unwrap(); 160 | 161 | let mut cmd = Command::cargo_bin("spreet")?; 162 | cmd.arg("tests/fixtures/svgs") 163 | .arg(temp.join("minify_unique")) 164 | .arg("--minify-index-file") 165 | .arg("--unique") 166 | .assert() 167 | .success(); 168 | 169 | let expected_spritesheet = Path::new("tests/fixtures/output/minify_unique@1x.png"); 170 | let actual_spritesheet = predicate::path::eq_file(temp.join("minify_unique.png")); 171 | let expected_index = Path::new("tests/fixtures/output/minify_unique@1x.json"); 172 | let actual_index = predicate::path::eq_file(temp.join("minify_unique.json")); 173 | 174 | assert!(actual_spritesheet.eval(expected_spritesheet)); 175 | assert!(actual_index.eval(expected_index)); 176 | 177 | Ok(()) 178 | } 179 | 180 | #[test] 181 | fn spreet_can_output_stretchable_icons() -> Result<(), Box> { 182 | let temp = assert_fs::TempDir::new().unwrap(); 183 | 184 | let mut cmd = Command::cargo_bin("spreet")?; 185 | cmd.arg("tests/fixtures/stretchable") 186 | .arg(temp.join("stretchable@2x")) 187 | .arg("--retina") 188 | .assert() 189 | .success(); 190 | 191 | let expected_spritesheet = Path::new("tests/fixtures/output/stretchable@2x.png"); 192 | let actual_spritesheet = predicate::path::eq_file(temp.join("stretchable@2x.png")); 193 | let expected_index = Path::new("tests/fixtures/output/stretchable@2x.json"); 194 | let actual_index = predicate::path::eq_file(temp.join("stretchable@2x.json")); 195 | 196 | assert!(actual_spritesheet.eval(expected_spritesheet)); 197 | assert!(actual_index.eval(expected_index)); 198 | 199 | Ok(()) 200 | } 201 | 202 | #[test] 203 | fn spreet_can_output_sdf_icons() -> Result<(), Box> { 204 | let temp = assert_fs::TempDir::new().unwrap(); 205 | 206 | let mut cmd = Command::cargo_bin("spreet")?; 207 | cmd.arg("tests/fixtures/svgs") 208 | .arg(temp.join("sdf@2x")) 209 | .arg("--sdf") 210 | .arg("--retina") 211 | .arg("--recursive") 212 | .assert() 213 | .success(); 214 | 215 | let expected_spritesheet = Path::new("tests/fixtures/output/sdf@2x.png"); 216 | let actual_spritesheet = predicate::path::eq_file(temp.join("sdf@2x.png")); 217 | let expected_index = Path::new("tests/fixtures/output/sdf@2x.json"); 218 | let actual_index = predicate::path::eq_file(temp.join("sdf@2x.json")); 219 | 220 | assert!(actual_spritesheet.eval(expected_spritesheet)); 221 | assert!(actual_index.eval(expected_index)); 222 | 223 | Ok(()) 224 | } 225 | 226 | #[test] 227 | fn spreet_rejects_non_existent_input_directory() { 228 | let mut cmd = Command::cargo_bin("spreet").unwrap(); 229 | cmd.arg("does_not_exist") 230 | .arg("default") 231 | .assert() 232 | .failure() 233 | .code(2) 234 | .stderr("error: invalid value 'does_not_exist' for '': must be an existing directory\n\nFor more information, try '--help'.\n"); 235 | } 236 | 237 | #[test] 238 | fn spreet_rejects_zero_ratio() { 239 | let temp = assert_fs::TempDir::new().unwrap(); 240 | 241 | let mut cmd = Command::cargo_bin("spreet").unwrap(); 242 | cmd.arg("tests/fixtures/svgs") 243 | .arg(temp.join("default")) 244 | .arg("--ratio") 245 | .arg("0") 246 | .assert() 247 | .failure() 248 | .code(2) 249 | .stderr("error: invalid value '0' for '--ratio ': must be greater than one\n\nFor more information, try '--help'.\n"); 250 | } 251 | 252 | #[test] 253 | fn spreet_rejects_negative_ratio() { 254 | let temp = assert_fs::TempDir::new().unwrap(); 255 | 256 | let mut cmd = Command::cargo_bin("spreet").unwrap(); 257 | cmd.arg("tests/fixtures/svgs") 258 | .arg(temp.join("default")) 259 | .arg("--ratio") 260 | .arg(" -3") 261 | .assert() 262 | .failure() 263 | .code(2) 264 | .stderr("error: invalid value ' -3' for '--ratio ': invalid digit found in string\n\nFor more information, try '--help'.\n"); 265 | } 266 | 267 | #[test] 268 | fn spreet_accepts_pngs_wrapped_in_svgs() { 269 | let temp = assert_fs::TempDir::new().unwrap(); 270 | 271 | let mut cmd = Command::cargo_bin("spreet").unwrap(); 272 | cmd.arg("tests/fixtures/pngs") 273 | .arg(temp.join("pngs@2x")) 274 | .arg("--retina") 275 | .assert() 276 | .success(); 277 | 278 | let expected_spritesheet = Path::new("tests/fixtures/output/pngs@2x.png"); 279 | let actual_spritesheet = predicate::path::eq_file(temp.join("pngs@2x.png")); 280 | let expected_index = Path::new("tests/fixtures/output/pngs@2x.json"); 281 | let actual_index = predicate::path::eq_file(temp.join("pngs@2x.json")); 282 | 283 | assert!(actual_spritesheet.eval(expected_spritesheet)); 284 | assert!(actual_index.eval(expected_index)); 285 | } 286 | -------------------------------------------------------------------------------- /tests/fixtures/output/default@1x.json: -------------------------------------------------------------------------------- 1 | { 2 | "another_bicycle": { 3 | "height": 15, 4 | "pixelRatio": 1, 5 | "width": 15, 6 | "x": 20, 7 | "y": 0 8 | }, 9 | "bicycle": { 10 | "height": 15, 11 | "pixelRatio": 1, 12 | "width": 15, 13 | "x": 20, 14 | "y": 15 15 | }, 16 | "circle": { 17 | "height": 20, 18 | "pixelRatio": 1, 19 | "width": 20, 20 | "x": 0, 21 | "y": 0 22 | } 23 | } -------------------------------------------------------------------------------- /tests/fixtures/output/default@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/output/default@1x.png -------------------------------------------------------------------------------- /tests/fixtures/output/default@2x.json: -------------------------------------------------------------------------------- 1 | { 2 | "another_bicycle": { 3 | "height": 30, 4 | "pixelRatio": 2, 5 | "width": 30, 6 | "x": 40, 7 | "y": 0 8 | }, 9 | "bicycle": { 10 | "height": 30, 11 | "pixelRatio": 2, 12 | "width": 30, 13 | "x": 40, 14 | "y": 30 15 | }, 16 | "circle": { 17 | "height": 40, 18 | "pixelRatio": 2, 19 | "width": 40, 20 | "x": 0, 21 | "y": 0 22 | } 23 | } -------------------------------------------------------------------------------- /tests/fixtures/output/default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/output/default@2x.png -------------------------------------------------------------------------------- /tests/fixtures/output/minify@1x.json: -------------------------------------------------------------------------------- 1 | {"another_bicycle":{"height":15,"pixelRatio":1,"width":15,"x":20,"y":0},"bicycle":{"height":15,"pixelRatio":1,"width":15,"x":20,"y":15},"circle":{"height":20,"pixelRatio":1,"width":20,"x":0,"y":0}} -------------------------------------------------------------------------------- /tests/fixtures/output/minify@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/output/minify@1x.png -------------------------------------------------------------------------------- /tests/fixtures/output/minify_unique@1x.json: -------------------------------------------------------------------------------- 1 | {"another_bicycle":{"height":15,"pixelRatio":1,"width":15,"x":20,"y":0},"bicycle":{"height":15,"pixelRatio":1,"width":15,"x":20,"y":0},"circle":{"height":20,"pixelRatio":1,"width":20,"x":0,"y":0}} -------------------------------------------------------------------------------- /tests/fixtures/output/minify_unique@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/output/minify_unique@1x.png -------------------------------------------------------------------------------- /tests/fixtures/output/pngs@2x.json: -------------------------------------------------------------------------------- 1 | { 2 | "iceland_flag": { 3 | "height": 460, 4 | "pixelRatio": 2, 5 | "width": 640, 6 | "x": 0, 7 | "y": 0 8 | }, 9 | "sweden_flag": { 10 | "height": 200, 11 | "pixelRatio": 2, 12 | "width": 320, 13 | "x": 640, 14 | "y": 0 15 | } 16 | } -------------------------------------------------------------------------------- /tests/fixtures/output/pngs@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/output/pngs@2x.png -------------------------------------------------------------------------------- /tests/fixtures/output/recursive@1x.json: -------------------------------------------------------------------------------- 1 | { 2 | "another_bicycle": { 3 | "height": 15, 4 | "pixelRatio": 1, 5 | "width": 15, 6 | "x": 20, 7 | "y": 16 8 | }, 9 | "bicycle": { 10 | "height": 15, 11 | "pixelRatio": 1, 12 | "width": 15, 13 | "x": 35, 14 | "y": 16 15 | }, 16 | "circle": { 17 | "height": 20, 18 | "pixelRatio": 1, 19 | "width": 20, 20 | "x": 0, 21 | "y": 0 22 | }, 23 | "recursive/bear": { 24 | "height": 16, 25 | "pixelRatio": 1, 26 | "width": 16, 27 | "x": 20, 28 | "y": 0 29 | } 30 | } -------------------------------------------------------------------------------- /tests/fixtures/output/recursive@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/output/recursive@1x.png -------------------------------------------------------------------------------- /tests/fixtures/output/sdf@2x.json: -------------------------------------------------------------------------------- 1 | { 2 | "another_bicycle": { 3 | "height": 36, 4 | "pixelRatio": 2, 5 | "width": 36, 6 | "x": 84, 7 | "y": 0, 8 | "sdf": true 9 | }, 10 | "bicycle": { 11 | "height": 36, 12 | "pixelRatio": 2, 13 | "width": 36, 14 | "x": 84, 15 | "y": 36, 16 | "sdf": true 17 | }, 18 | "circle": { 19 | "height": 46, 20 | "pixelRatio": 2, 21 | "width": 46, 22 | "x": 0, 23 | "y": 0, 24 | "sdf": true 25 | }, 26 | "recursive/bear": { 27 | "height": 38, 28 | "pixelRatio": 2, 29 | "width": 38, 30 | "x": 46, 31 | "y": 0, 32 | "sdf": true 33 | } 34 | } -------------------------------------------------------------------------------- /tests/fixtures/output/sdf@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/output/sdf@2x.png -------------------------------------------------------------------------------- /tests/fixtures/output/stretchable@2x.json: -------------------------------------------------------------------------------- 1 | { 2 | "ae-national-3-affinity": { 3 | "height": 50, 4 | "pixelRatio": 2, 5 | "width": 52, 6 | "x": 0, 7 | "y": 0, 8 | "content": [ 9 | 6, 10 | 14, 11 | 46, 12 | 36 13 | ], 14 | "stretchX": [ 15 | [ 16 | 10, 17 | 14 18 | ], 19 | [ 20 | 40, 21 | 44 22 | ] 23 | ] 24 | }, 25 | "cn-nths-expy-2-affinity": { 26 | "height": 46, 27 | "pixelRatio": 2, 28 | "width": 40, 29 | "x": 52, 30 | "y": 0, 31 | "content": [ 32 | 4, 33 | 10, 34 | 36, 35 | 36 36 | ], 37 | "stretchX": [ 38 | [ 39 | 8, 40 | 32 41 | ] 42 | ], 43 | "stretchY": [ 44 | [ 45 | 10, 46 | 32 47 | ] 48 | ] 49 | }, 50 | "cn-nths-expy-2-inkscape-plain": { 51 | "height": 46, 52 | "pixelRatio": 2, 53 | "width": 40, 54 | "x": 52, 55 | "y": 46, 56 | "stretchX": [ 57 | [ 58 | 6, 59 | 34 60 | ] 61 | ], 62 | "stretchY": [ 63 | [ 64 | 10, 65 | 34 66 | ] 67 | ] 68 | }, 69 | "shield-illustrator": { 70 | "height": 36, 71 | "pixelRatio": 2, 72 | "width": 36, 73 | "x": 0, 74 | "y": 50, 75 | "content": [ 76 | 8, 77 | 16, 78 | 28, 79 | 28 80 | ], 81 | "stretchX": [ 82 | [ 83 | 8, 84 | 28 85 | ] 86 | ], 87 | "stretchY": [ 88 | [ 89 | 16, 90 | 28 91 | ] 92 | ] 93 | }, 94 | "shield-illustrator-rotated": { 95 | "height": 36, 96 | "pixelRatio": 2, 97 | "width": 36, 98 | "x": 0, 99 | "y": 86, 100 | "content": [ 101 | 7.407, 102 | 11.612, 103 | 24.377, 104 | 28.582 105 | ], 106 | "stretchX": [ 107 | [ 108 | 7.46, 109 | 19.056 110 | ] 111 | ], 112 | "stretchY": [ 113 | [ 114 | 21.162, 115 | 28.516 116 | ] 117 | ] 118 | }, 119 | "shield-illustrator-rotated-reversed": { 120 | "height": 36, 121 | "pixelRatio": 2, 122 | "width": 36, 123 | "x": 36, 124 | "y": 92, 125 | "content": [ 126 | 12.0, 127 | 16.0, 128 | 24.0, 129 | 24.0 130 | ], 131 | "stretchX": [ 132 | [ 133 | 12.0, 134 | 24.0 135 | ] 136 | ], 137 | "stretchY": [ 138 | [ 139 | 16.0, 140 | 24.0 141 | ] 142 | ] 143 | }, 144 | "shield-illustrator-rotated-translated": { 145 | "height": 36, 146 | "pixelRatio": 2, 147 | "width": 36, 148 | "x": 72, 149 | "y": 92, 150 | "content": [ 151 | 8.485, 152 | 14.142, 153 | 22.627, 154 | 28.284 155 | ], 156 | "stretchX": [ 157 | [ 158 | 8.485, 159 | 18.385 160 | ] 161 | ], 162 | "stretchY": [ 163 | [ 164 | 21.213, 165 | 28.284 166 | ] 167 | ] 168 | }, 169 | "shield-rotated": { 170 | "height": 36, 171 | "pixelRatio": 2, 172 | "width": 36, 173 | "x": 92, 174 | "y": 0, 175 | "content": [ 176 | 12, 177 | 20, 178 | 32, 179 | 32 180 | ], 181 | "stretchX": [ 182 | [ 183 | 12, 184 | 32 185 | ] 186 | ], 187 | "stretchY": [ 188 | [ 189 | 20, 190 | 32 191 | ] 192 | ] 193 | } 194 | } -------------------------------------------------------------------------------- /tests/fixtures/output/stretchable@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/output/stretchable@2x.png -------------------------------------------------------------------------------- /tests/fixtures/output/unique@1x.json: -------------------------------------------------------------------------------- 1 | { 2 | "another_bicycle": { 3 | "height": 15, 4 | "pixelRatio": 1, 5 | "width": 15, 6 | "x": 20, 7 | "y": 0 8 | }, 9 | "bicycle": { 10 | "height": 15, 11 | "pixelRatio": 1, 12 | "width": 15, 13 | "x": 20, 14 | "y": 0 15 | }, 16 | "circle": { 17 | "height": 20, 18 | "pixelRatio": 1, 19 | "width": 20, 20 | "x": 0, 21 | "y": 0 22 | } 23 | } -------------------------------------------------------------------------------- /tests/fixtures/output/unique@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/output/unique@1x.png -------------------------------------------------------------------------------- /tests/fixtures/output/unique@2x.json: -------------------------------------------------------------------------------- 1 | { 2 | "another_bicycle": { 3 | "height": 30, 4 | "pixelRatio": 2, 5 | "width": 30, 6 | "x": 40, 7 | "y": 0 8 | }, 9 | "bicycle": { 10 | "height": 30, 11 | "pixelRatio": 2, 12 | "width": 30, 13 | "x": 40, 14 | "y": 0 15 | }, 16 | "circle": { 17 | "height": 40, 18 | "pixelRatio": 2, 19 | "width": 40, 20 | "x": 0, 21 | "y": 0 22 | } 23 | } -------------------------------------------------------------------------------- /tests/fixtures/output/unique@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/output/unique@2x.png -------------------------------------------------------------------------------- /tests/fixtures/pngs/iceland_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/pngs/iceland_flag.png -------------------------------------------------------------------------------- /tests/fixtures/pngs/iceland_flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/fixtures/pngs/sweden_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flother/spreet/04ceaf1bb29e61ccddf279974aba2b8541c9775e/tests/fixtures/pngs/sweden_flag.png -------------------------------------------------------------------------------- /tests/fixtures/pngs/sweden_flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/fixtures/stretchable/README.md: -------------------------------------------------------------------------------- 1 | The SVG files in this directory are taken from the [Spritezero](https://github.com/mapbox/spritezero) test suite and are used under the ISC licence below. 2 | 3 | # Internet Systems Consortium licence 4 | 5 | Copyright (c) 2016, Mapbox 6 | 7 | Permission to use, copy, modify, and/or distribute this software for any purpose 8 | with or without fee is hereby granted, provided that the above copyright notice 9 | and this permission notice appear in all copies. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 12 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 13 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 14 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 15 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 16 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 17 | THIS SOFTWARE. 18 | -------------------------------------------------------------------------------- /tests/fixtures/stretchable/ae-national-3-affinity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/fixtures/stretchable/cn-nths-expy-2-affinity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/fixtures/stretchable/cn-nths-expy-2-inkscape-plain.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 16 | 18 | image/svg+xml 19 | 21 | 22 | cn-nths-expy-2 23 | 24 | 25 | 26 | 27 | 29 | 31 | cn-nths-expy-2 32 | 33 | 35 | 39 | 43 | 44 | 48 | 52 | 59 | 60 | -------------------------------------------------------------------------------- /tests/fixtures/stretchable/shield-illustrator-rotated-reversed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/fixtures/stretchable/shield-illustrator-rotated-translated.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/fixtures/stretchable/shield-illustrator-rotated.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/fixtures/stretchable/shield-illustrator.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/fixtures/stretchable/shield-rotated.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/fixtures/svgs/another_bicycle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/fixtures/svgs/bicycle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | -------------------------------------------------------------------------------- /tests/fixtures/svgs/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/fixtures/svgs/recursive/bear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/fs.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use assert_matches::assert_matches; 4 | use spreet::{get_svg_input_paths, SpreetError}; 5 | 6 | #[test] 7 | fn get_svg_input_paths_returns_non_recursive_results() { 8 | let mut input_paths = get_svg_input_paths(Path::new("tests/fixtures/svgs"), false).unwrap(); 9 | input_paths.sort(); 10 | assert_eq!( 11 | input_paths, 12 | vec![ 13 | Path::new("tests/fixtures/svgs/another_bicycle.svg"), 14 | Path::new("tests/fixtures/svgs/bicycle.svg"), 15 | Path::new("tests/fixtures/svgs/circle.svg"), 16 | ] 17 | ); 18 | } 19 | 20 | #[test] 21 | fn get_svg_input_paths_returns_recursive_results() { 22 | let mut input_paths = get_svg_input_paths(Path::new("tests/fixtures/svgs"), true).unwrap(); 23 | input_paths.sort(); 24 | assert_eq!( 25 | input_paths, 26 | vec![ 27 | Path::new("tests/fixtures/svgs/another_bicycle.svg"), 28 | Path::new("tests/fixtures/svgs/bicycle.svg"), 29 | Path::new("tests/fixtures/svgs/circle.svg"), 30 | Path::new("tests/fixtures/svgs/recursive/bear.svg"), 31 | ] 32 | ); 33 | } 34 | 35 | #[test] 36 | fn get_svg_input_paths_returns_error_when_path_does_not_exist() { 37 | assert_matches!( 38 | get_svg_input_paths(Path::new("fake"), false), 39 | Err(SpreetError::IoError(_)) 40 | ); 41 | } 42 | 43 | #[test] 44 | fn get_svg_input_paths_returns_error_when_path_is_file() { 45 | assert_matches!( 46 | get_svg_input_paths(Path::new("tests/fixtures/svgs/bicycle.svg"), false), 47 | Err(SpreetError::IoError(_)) 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /tests/sprite.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use assert_matches::assert_matches; 4 | use resvg::usvg::{Options, Rect, Tree}; 5 | use spreet::{load_svg, sprite_name, SpreetError, Sprite}; 6 | 7 | #[test] 8 | fn sprite_name_works_with_root_files() { 9 | assert_eq!( 10 | sprite_name( 11 | Path::new("./tests/fixtures/svgs/recursive/bear.svg"), 12 | Path::new("./tests/fixtures/svgs/recursive") 13 | ) 14 | .unwrap(), 15 | "bear" 16 | ); 17 | } 18 | 19 | #[test] 20 | fn sprite_name_works_with_nested_files() { 21 | assert_eq!( 22 | sprite_name( 23 | Path::new("./tests/fixtures/svgs/recursive/bear.svg"), 24 | Path::new("./tests/fixtures/svgs") 25 | ) 26 | .unwrap(), 27 | "recursive/bear" 28 | ); 29 | } 30 | 31 | #[test] 32 | fn sprite_name_works_with_deeply_nested_files() { 33 | assert_eq!( 34 | sprite_name( 35 | Path::new("./tests/fixtures/svgs/recursive/bear.svg"), 36 | Path::new("./tests") 37 | ) 38 | .unwrap(), 39 | "fixtures/svgs/recursive/bear" 40 | ); 41 | } 42 | 43 | #[test] 44 | fn sprite_name_returns_ok_for_non_existent_path() { 45 | assert_eq!( 46 | sprite_name(Path::new("./does_not_exist.svg"), Path::new("./")).unwrap(), 47 | "does_not_exist" 48 | ); 49 | } 50 | 51 | #[test] 52 | fn sprite_name_returns_error_when_path_is_empty() { 53 | assert_matches!( 54 | sprite_name(Path::new(""), Path::new("")), 55 | Err(SpreetError::IoError(_)) 56 | ); 57 | } 58 | 59 | #[test] 60 | fn sprite_name_returns_error_for_non_existent_base_path() { 61 | assert_matches!( 62 | sprite_name( 63 | Path::new("./tests/fixtures/svgs/bicycle.svg"), 64 | Path::new("./tests/fixtures/foo"), 65 | ), 66 | Err(SpreetError::PathError(_)) 67 | ); 68 | } 69 | 70 | #[test] 71 | fn sprite_name_returns_error_when_base_path_not_parent_of_path() { 72 | assert_matches!( 73 | sprite_name( 74 | Path::new("./tests/fixtures/svgs/bicycle.svg"), 75 | Path::new("./tests/fixtures/pngs/"), 76 | ), 77 | Err(SpreetError::PathError(_)) 78 | ); 79 | } 80 | 81 | #[test] 82 | fn unstretchable_icon_has_no_metadata() { 83 | let path = Path::new("./tests/fixtures/svgs/bicycle.svg"); 84 | let tree = load_svg(path).unwrap(); 85 | let sprite = Sprite::new(tree, 1).unwrap(); 86 | 87 | assert!(sprite.content_area().is_none()); 88 | assert!(sprite.stretch_x_areas().is_none()); 89 | assert!(sprite.stretch_y_areas().is_none()); 90 | } 91 | 92 | #[test] 93 | fn stretchable_icon_has_metadata() { 94 | let path = Path::new("./tests/fixtures/stretchable/cn-nths-expy-2-affinity.svg"); 95 | let tree = load_svg(path).unwrap(); 96 | let sprite = Sprite::new(tree, 1).unwrap(); 97 | 98 | assert_eq!( 99 | sprite.content_area().unwrap(), 100 | Rect::from_ltrb(2.0, 5.0, 18.0, 18.0).unwrap() 101 | ); 102 | assert_eq!( 103 | sprite.stretch_x_areas().unwrap(), 104 | [Rect::from_ltrb(4.0, 0.0, 16.0, 0.0).unwrap()] 105 | ); 106 | assert_eq!( 107 | sprite.stretch_y_areas().unwrap(), 108 | [Rect::from_ltrb(0.0, 5.0, 0.0, 16.0).unwrap()] 109 | ); 110 | } 111 | 112 | #[test] 113 | fn stretchable_icons_can_use_stretch_shorthand() { 114 | let path = Path::new("./tests/fixtures/stretchable/cn-nths-expy-2-inkscape-plain.svg"); 115 | let tree = load_svg(path).unwrap(); 116 | let sprite = Sprite::new(tree, 1).unwrap(); 117 | 118 | assert!(sprite.content_area().is_none()); 119 | assert_eq!( 120 | sprite.stretch_x_areas().unwrap(), 121 | [Rect::from_ltrb(3.0, 5.0, 17.0, 17.0).unwrap()], 122 | ); 123 | assert_eq!( 124 | sprite.stretch_y_areas().unwrap(), 125 | [Rect::from_ltrb(3.0, 5.0, 17.0, 17.0).unwrap()], 126 | ); 127 | } 128 | 129 | #[test] 130 | fn stretchable_icon_can_have_multiple_horizontal_stretch_zones() { 131 | let path = Path::new("./tests/fixtures/stretchable/ae-national-3-affinity.svg"); 132 | let tree = load_svg(path).unwrap(); 133 | let sprite = Sprite::new(tree, 1).unwrap(); 134 | 135 | assert_eq!( 136 | sprite.stretch_x_areas().unwrap(), 137 | [ 138 | Rect::from_ltrb(5.0, 5.0, 7.0, 5.0).unwrap(), 139 | Rect::from_ltrb(20.0, 5.0, 22.0, 5.0).unwrap(), 140 | ] 141 | ); 142 | } 143 | 144 | #[test] 145 | fn stretchable_icon_metadata_matches_pixel_ratio() { 146 | let path = Path::new("./tests/fixtures/stretchable/cn-nths-expy-2-affinity.svg"); 147 | let tree = load_svg(path).unwrap(); 148 | let sprite = Sprite::new(tree, 2).unwrap(); 149 | 150 | assert_eq!( 151 | sprite.content_area().unwrap(), 152 | Rect::from_ltrb(4.0, 10.0, 36.0, 36.0).unwrap() 153 | ); 154 | assert_eq!( 155 | sprite.stretch_x_areas().unwrap(), 156 | [Rect::from_ltrb(8.0, 0.0, 32.0, 0.0).unwrap()] 157 | ); 158 | assert_eq!( 159 | sprite.stretch_y_areas().unwrap(), 160 | [Rect::from_ltrb(0.0, 10.0, 0.0, 32.0).unwrap()] 161 | ); 162 | } 163 | 164 | #[test] 165 | fn stretchable_icon_with_empty_metadata_is_ignored() { 166 | let svg = ""; 167 | let tree = Tree::from_str(svg, &Options::default()).unwrap(); 168 | let sprite = Sprite::new(tree, 1).unwrap(); 169 | 170 | assert!(sprite.content_area().is_none()); 171 | } 172 | 173 | #[test] 174 | fn stretchable_icon_with_invalid_metadata_is_ignored() { 175 | let svg = ""; 176 | let tree = Tree::from_str(svg, &Options::default()).unwrap(); 177 | let sprite = Sprite::new(tree, 1).unwrap(); 178 | 179 | assert!(sprite.content_area().is_none()); 180 | } 181 | 182 | #[test] 183 | fn stretchable_icon_with_metadata_in_hidden_element_is_ignored() { 184 | let svg = " 185 | 186 | 187 | 188 | "; 189 | let tree = Tree::from_str(svg, &Options::default()).unwrap(); 190 | let sprite = Sprite::new(tree, 1).unwrap(); 191 | 192 | assert!(sprite.content_area().is_none()); 193 | } 194 | --------------------------------------------------------------------------------