├── .github ├── copyright.sh ├── debug_assertions.sh └── workflows │ └── ci.yml ├── .gitignore ├── .typos.toml ├── AUTHORS ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── docs ├── logo.png ├── logo_small.png └── screenshot.png ├── example ├── .cargo │ └── config.toml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── demolib │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── test_utils.rs ├── tests │ ├── current │ │ └── .gitignore │ └── snapshots │ │ └── create_rectangle.png └── xtask_kompari │ ├── Cargo.toml │ └── src │ └── main.rs ├── kompari-cli ├── Cargo.toml ├── README.md ├── src │ └── main.rs └── tests │ └── tests.rs ├── kompari-html ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── pageconsts.rs │ ├── report.rs │ └── review.rs ├── kompari-tasks ├── Cargo.toml ├── README.md └── src │ ├── args.rs │ ├── lib.rs │ ├── optimizations.rs │ └── task.rs ├── kompari ├── Cargo.toml ├── README.md ├── src │ ├── dirdiff.rs │ ├── fsutils.rs │ ├── imageutils.rs │ ├── imgdiff.rs │ └── lib.rs └── tests │ └── compare.rs └── tests ├── left ├── bright.png ├── changetext.png ├── right_missing.png ├── same.png ├── shift.png └── size_error.png └── right ├── bright.png ├── changetext.png ├── left_missing.png ├── same.png ├── shift.png └── size_error.png /.github/copyright.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # If there are new files with headers that can't match the conditions here, 4 | # then the files can be ignored by an additional glob argument via the -g flag. 5 | # For example: 6 | # -g "!src/special_file.rs" 7 | # -g "!src/special_directory" 8 | 9 | # Check all the standard Rust source files 10 | output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Kompari Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n" --files-without-match --multiline -g "*.rs" .) 11 | 12 | if [ -n "$output" ]; then 13 | echo -e "The following files lack the correct copyright header:\n" 14 | echo $output 15 | echo -e "\n\nPlease add the following header:\n" 16 | echo "// Copyright $(date +%Y) the Kompari Authors" 17 | echo "// SPDX-License-Identifier: Apache-2.0 OR MIT" 18 | echo -e "\n... rest of the file ...\n" 19 | exit 1 20 | fi 21 | 22 | echo "All files have correct copyright headers." 23 | exit 0 24 | -------------------------------------------------------------------------------- /.github/debug_assertions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check all the standard Rust source files 4 | output=$(rg "debug_assertions" -g "*.rs" .) 5 | 6 | if [ -z "$output" ]; then 7 | if [ "$USING_DEBUG_ASSERTIONS" = "true" ]; then 8 | echo "Could not find any debug_assertions usage in Rust code." 9 | echo "The CI script must be modified to not expect usage." 10 | echo "Set USING_DEBUG_ASSERTIONS to false in .github/workflows/ci.yml." 11 | exit 1 12 | else 13 | echo "Expected no debug_assertions usage in Rust code and found none." 14 | exit 0 15 | fi 16 | else 17 | if [ "$USING_DEBUG_ASSERTIONS" = "true" ]; then 18 | echo "Expected debug_assertions to be used in Rust code and found it." 19 | exit 0 20 | else 21 | echo "Found debug_assertions usage in Rust code." 22 | echo "" 23 | echo $output 24 | echo "" 25 | echo "The CI script must be modified to expect this usage." 26 | echo "Set USING_DEBUG_ASSERTIONS to true in .github/workflows/ci.yml." 27 | exit 1 28 | fi 29 | fi 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | env: 2 | # We aim to always test with the latest stable Rust toolchain, however we pin to a specific 3 | # version like 1.70. Note that we only specify MAJOR.MINOR and not PATCH so that bugfixes still 4 | # come automatically. If the version specified here is no longer the latest stable version, 5 | # then please feel free to submit a PR that adjusts it along with the potential clippy fixes. 6 | RUST_STABLE_VER: "1.83" # In quotes because otherwise (e.g.) 1.70 would be interpreted as 1.7 7 | # The purpose of checking with the minimum supported Rust toolchain is to detect its staleness. 8 | # If the compilation fails, then the version specified here needs to be bumped up to reality. 9 | # Be sure to also update the rust-version property in the workspace Cargo.toml file, 10 | # the Unreleased section of CHANGELOG.md, plus all the README.md files of the affected packages. 11 | RUST_MIN_VER: "1.78" 12 | # List of packages that can not target Wasm. 13 | NO_WASM_PKGS: "--exclude kompari-cli --exclude kompari-html --exclude kompari-tasks" 14 | # List of packages that will be checked with the minimum supported Rust version. 15 | # This should be limited to packages that are intended for publishing. 16 | RUST_MIN_VER_PKGS: "-p kompari -p kompari-html -p kompari-tasks" 17 | # Whether the workspace contains Rust code using the debug_assertions configuration option. 18 | USING_DEBUG_ASSERTIONS: "false" 19 | 20 | 21 | # Rationale 22 | # 23 | # We don't run clippy with --all-targets because then even --lib and --bins are compiled with 24 | # dev dependencies enabled, which does not match how they would be compiled by users. 25 | # A dev dependency might enable a feature that we need for a regular dependency, 26 | # and checking with --all-targets would not find our feature requirements lacking. 27 | # This problem still applies to cargo resolver version 2. 28 | # Thus we split all the targets into two steps, one with --lib --bins 29 | # and another with --tests --benches --examples. 30 | # Also, we can't give --lib --bins explicitly because then cargo will error on binary-only packages. 31 | # Luckily the default behavior of cargo with no explicit targets is the same but without the error. 32 | # 33 | # We use cargo-hack for a similar reason. Cargo's --workspace will do feature unification across 34 | # the whole workspace. While cargo-hack will instead check each workspace package separately. 35 | # 36 | # Using cargo-hack also allows us to more easily test the feature matrix of our packages. 37 | # We use --each-feature & --optional-deps which will run a separate check for every feature. 38 | # 39 | # We use cargo-nextest, which has a faster concurrency model for running tests. 40 | # However cargo-nextest does not support running doc tests, so we also have a cargo test --doc step. 41 | # For more information see https://github.com/nextest-rs/nextest/issues/16 42 | # 43 | # The MSRV jobs run only cargo check because different clippy versions can disagree on goals and 44 | # running tests introduces dev dependencies which may require a higher MSRV than the bare package. 45 | # 46 | # If the workspace uses debug_assertions then we verify code twice, with it set to true or false. 47 | # We always keep it true for external dependencies so that we can reuse the cache for faster builds. 48 | # 49 | # We don't save caches in the merge-group cases, because those caches will never be re-used (apart 50 | # from the very rare cases where there are multiple PRs in the merge queue). 51 | # This is because GitHub doesn't share caches between merge queues and the main branch. 52 | 53 | name: CI 54 | 55 | on: 56 | pull_request: 57 | merge_group: 58 | # We run on push, even though the commit is the same as when we ran in merge_group. 59 | # This allows the cache to be primed. 60 | # See https://github.com/orgs/community/discussions/66430 61 | push: 62 | branches: 63 | - main 64 | 65 | jobs: 66 | fmt: 67 | name: formatting 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v4 71 | 72 | - name: install stable toolchain 73 | uses: dtolnay/rust-toolchain@master 74 | with: 75 | toolchain: ${{ env.RUST_STABLE_VER }} 76 | components: rustfmt 77 | 78 | - name: cargo fmt 79 | run: cargo fmt --all --check 80 | 81 | - name: install ripgrep 82 | run: | 83 | sudo apt update 84 | sudo apt install ripgrep 85 | 86 | - name: check copyright headers 87 | run: bash .github/copyright.sh 88 | 89 | - name: check debug_assertions presence 90 | run: bash .github/debug_assertions.sh 91 | 92 | clippy-stable: 93 | name: cargo clippy 94 | runs-on: ${{ matrix.os }} 95 | strategy: 96 | matrix: 97 | os: [ windows-latest, macos-latest, ubuntu-latest ] 98 | steps: 99 | - uses: actions/checkout@v4 100 | 101 | - name: install stable toolchain 102 | uses: dtolnay/rust-toolchain@master 103 | with: 104 | toolchain: ${{ env.RUST_STABLE_VER }} 105 | components: clippy 106 | 107 | - name: install cargo-hack 108 | uses: taiki-e/install-action@v2 109 | with: 110 | tool: cargo-hack 111 | 112 | - name: restore cache 113 | uses: Swatinem/rust-cache@v2 114 | with: 115 | save-if: ${{ github.event_name != 'merge_group' }} 116 | 117 | - name: cargo clippy 118 | run: cargo hack clippy --workspace --locked --profile ci --optional-deps --each-feature -- -D warnings 119 | 120 | - name: cargo clippy (auxiliary) 121 | run: cargo hack clippy --workspace --locked --profile ci --optional-deps --each-feature --tests --benches --examples -- -D warnings 122 | 123 | - name: cargo clippy (no debug_assertions) 124 | if: env.USING_DEBUG_ASSERTIONS == 'true' 125 | run: cargo hack clippy --workspace --locked --profile ci --optional-deps --each-feature -- -D warnings 126 | env: 127 | CARGO_PROFILE_CI_DEBUG_ASSERTIONS: "false" 128 | 129 | - name: cargo clippy (auxiliary) (no debug_assertions) 130 | if: env.USING_DEBUG_ASSERTIONS == 'true' 131 | run: cargo hack clippy --workspace --locked --profile ci --optional-deps --each-feature --tests --benches --examples -- -D warnings 132 | env: 133 | CARGO_PROFILE_CI_DEBUG_ASSERTIONS: "false" 134 | 135 | clippy-stable-wasm: 136 | name: cargo clippy (wasm32) 137 | runs-on: ubuntu-latest 138 | steps: 139 | - uses: actions/checkout@v4 140 | 141 | - name: install stable toolchain 142 | uses: dtolnay/rust-toolchain@master 143 | with: 144 | toolchain: ${{ env.RUST_STABLE_VER }} 145 | targets: wasm32-unknown-unknown 146 | components: clippy 147 | 148 | - name: install cargo-hack 149 | uses: taiki-e/install-action@v2 150 | with: 151 | tool: cargo-hack 152 | 153 | - name: restore cache 154 | uses: Swatinem/rust-cache@v2 155 | with: 156 | save-if: ${{ github.event_name != 'merge_group' }} 157 | 158 | - name: cargo clippy 159 | run: cargo hack clippy --workspace ${{ env.NO_WASM_PKGS }} --locked --profile ci --target wasm32-unknown-unknown --optional-deps --each-feature --skip review,oxipng,default -- -D warnings 160 | 161 | - name: cargo clippy (auxiliary) 162 | run: cargo hack clippy --workspace ${{ env.NO_WASM_PKGS }} --locked --profile ci --target wasm32-unknown-unknown --optional-deps --each-feature --skip review,oxipng,default --tests --benches --examples -- -D warnings 163 | 164 | - name: cargo clippy (no debug_assertions) 165 | if: env.USING_DEBUG_ASSERTIONS == 'true' 166 | run: cargo hack clippy --workspace ${{ env.NO_WASM_PKGS }} --locked --profile ci --target wasm32-unknown-unknown --optional-deps --each-feature --skip review,oxipng,default -- -D warnings 167 | env: 168 | CARGO_PROFILE_CI_DEBUG_ASSERTIONS: "false" 169 | 170 | - name: cargo clippy (auxiliary) (no debug_assertions) 171 | if: env.USING_DEBUG_ASSERTIONS == 'true' 172 | run: cargo hack clippy --workspace ${{ env.NO_WASM_PKGS }} --locked --profile ci --target wasm32-unknown-unknown --optional-deps --each-feature --skip review,oxipng,default --tests --benches --examples -- -D warnings 173 | env: 174 | CARGO_PROFILE_CI_DEBUG_ASSERTIONS: "false" 175 | 176 | test-stable: 177 | name: cargo test 178 | runs-on: ${{ matrix.os }} 179 | strategy: 180 | matrix: 181 | os: [ windows-latest, macos-latest, ubuntu-latest ] 182 | steps: 183 | - uses: actions/checkout@v4 184 | 185 | - name: install stable toolchain 186 | uses: dtolnay/rust-toolchain@master 187 | with: 188 | toolchain: ${{ env.RUST_STABLE_VER }} 189 | 190 | - name: install cargo-nextest 191 | uses: taiki-e/install-action@v2 192 | with: 193 | tool: cargo-nextest 194 | 195 | - name: restore cache 196 | uses: Swatinem/rust-cache@v2 197 | with: 198 | save-if: ${{ github.event_name != 'merge_group' }} 199 | 200 | - name: cargo nextest 201 | run: cargo nextest run --workspace --locked --all-features --no-fail-fast 202 | 203 | - name: cargo test --doc 204 | run: cargo test --doc --workspace --locked --all-features --no-fail-fast 205 | 206 | test-stable-wasm: 207 | name: cargo test (wasm32) 208 | runs-on: ubuntu-latest 209 | steps: 210 | - uses: actions/checkout@v4 211 | 212 | - name: install stable toolchain 213 | uses: dtolnay/rust-toolchain@master 214 | with: 215 | toolchain: ${{ env.RUST_STABLE_VER }} 216 | targets: wasm32-unknown-unknown 217 | 218 | - name: restore cache 219 | uses: Swatinem/rust-cache@v2 220 | with: 221 | save-if: ${{ github.event_name != 'merge_group' }} 222 | 223 | # TODO: Find a way to make tests work. Until then the tests are merely compiled. 224 | - name: cargo test compile 225 | run: cargo test --workspace ${{ env.NO_WASM_PKGS }} --locked --target wasm32-unknown-unknown --no-default-features --no-run 226 | 227 | check-msrv: 228 | name: cargo check (msrv) 229 | runs-on: ${{ matrix.os }} 230 | strategy: 231 | matrix: 232 | os: [ windows-latest, macos-latest, ubuntu-latest ] 233 | steps: 234 | - uses: actions/checkout@v4 235 | 236 | - name: install msrv toolchain 237 | uses: dtolnay/rust-toolchain@master 238 | with: 239 | toolchain: ${{ env.RUST_MIN_VER }} 240 | 241 | - name: install cargo-hack 242 | uses: taiki-e/install-action@v2 243 | with: 244 | tool: cargo-hack 245 | 246 | - name: restore cache 247 | uses: Swatinem/rust-cache@v2 248 | with: 249 | save-if: ${{ github.event_name != 'merge_group' }} 250 | 251 | - name: cargo check 252 | run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --profile ci --optional-deps --each-feature 253 | 254 | - name: cargo check (no debug_assertions) 255 | if: env.USING_DEBUG_ASSERTIONS == 'true' 256 | run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --profile ci --optional-deps --each-feature 257 | env: 258 | CARGO_PROFILE_CI_DEBUG_ASSERTIONS: "false" 259 | 260 | check-msrv-wasm: 261 | name: cargo check (msrv) (wasm32) 262 | runs-on: ubuntu-latest 263 | steps: 264 | - uses: actions/checkout@v4 265 | 266 | - name: install msrv toolchain 267 | uses: dtolnay/rust-toolchain@master 268 | with: 269 | toolchain: ${{ env.RUST_MIN_VER }} 270 | targets: wasm32-unknown-unknown 271 | 272 | - name: install cargo-hack 273 | uses: taiki-e/install-action@v2 274 | with: 275 | tool: cargo-hack 276 | 277 | - name: restore cache 278 | uses: Swatinem/rust-cache@v2 279 | with: 280 | save-if: ${{ github.event_name != 'merge_group' }} 281 | 282 | - name: cargo check 283 | run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} ${{ env.NO_WASM_PKGS }} --locked --profile ci --target wasm32-unknown-unknown --optional-deps --each-feature --skip review,oxipng,default 284 | 285 | - name: cargo check (no debug_assertions) 286 | if: env.USING_DEBUG_ASSERTIONS == 'true' 287 | run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} ${{ env.NO_WASM_PKGS }} --locked --profile ci --target wasm32-unknown-unknown --optional-deps --each-feature --skip review,oxipng,default 288 | env: 289 | CARGO_PROFILE_CI_DEBUG_ASSERTIONS: "false" 290 | 291 | doc: 292 | name: cargo doc 293 | # NOTE: We don't have any platform specific docs in this workspace, so we only run on Ubuntu. 294 | # If we get per-platform docs (win/macos/linux/wasm32/..) then doc jobs should match that. 295 | runs-on: ubuntu-latest 296 | steps: 297 | - uses: actions/checkout@v4 298 | 299 | - name: install nightly toolchain 300 | uses: dtolnay/rust-toolchain@nightly 301 | 302 | - name: restore cache 303 | uses: Swatinem/rust-cache@v2 304 | with: 305 | save-if: ${{ github.event_name != 'merge_group' }} 306 | 307 | # We test documentation using nightly to match docs.rs. 308 | - name: cargo doc 309 | run: cargo doc --workspace --locked --all-features --no-deps --document-private-items 310 | env: 311 | RUSTDOCFLAGS: '--cfg docsrs -D warnings' 312 | 313 | # If this fails, consider changing your text or adding something to .typos.toml. 314 | typos: 315 | runs-on: ubuntu-latest 316 | steps: 317 | - uses: actions/checkout@v4 318 | 319 | - name: check typos 320 | uses: crate-ci/typos@v1.27.0 321 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | report.html 3 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # See the configuration reference at 2 | # https://github.com/crate-ci/typos/blob/master/docs/reference.md 3 | 4 | # Corrections take the form of a key/value pair. The key is the incorrect word 5 | # and the value is the correct word. If the key and value are the same, the 6 | # word is treated as always correct. If the value is an empty string, the word 7 | # is treated as always incorrect. 8 | 9 | # Match Identifier - Case Sensitive 10 | [default.extend-identifiers] 11 | 12 | # Match Inside a Word - Case Insensitive 13 | [default.extend-words] 14 | 15 | [files] 16 | # Include .github, .cargo, etc. 17 | ignore-hidden = false 18 | extend-exclude = [ 19 | # /.git isn't in .gitignore, because git never tracks it. 20 | # Typos doesn't know that, though. 21 | "/.git", 22 | ] 23 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of Kompari's significant contributors. 2 | # 3 | # This does not necessarily list everyone who has contributed code, 4 | # especially since many employees of one corporation may be contributing. 5 | # To see the full list of contributors, see the revision history in 6 | # source control. 7 | Ada Böhm 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | # Changelog 10 | 11 | 15 | 16 | ## [Unreleased] 17 | 18 | This release has an [MSRV][] of 1.78. 19 | 20 | - Initial release. 21 | 22 | [@spirali]: https://github.com/spirali 23 | 24 | 27 | 28 | 32 | [Unreleased]: https://github.com/linebender/kompari 33 | 34 | [MSRV]: README.md#minimum-supported-rust-version-msrv 35 | -------------------------------------------------------------------------------- /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 = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.18" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "is_terminal_polyfill", 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle" 52 | version = "1.0.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 70 | dependencies = [ 71 | "windows-sys 0.59.0", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.7" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 79 | dependencies = [ 80 | "anstyle", 81 | "once_cell", 82 | "windows-sys 0.59.0", 83 | ] 84 | 85 | [[package]] 86 | name = "assert_cmd" 87 | version = "2.0.16" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 90 | dependencies = [ 91 | "anstyle", 92 | "bstr", 93 | "doc-comment", 94 | "libc", 95 | "predicates", 96 | "predicates-core", 97 | "predicates-tree", 98 | "wait-timeout", 99 | ] 100 | 101 | [[package]] 102 | name = "autocfg" 103 | version = "1.4.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 106 | 107 | [[package]] 108 | name = "axum" 109 | version = "0.8.1" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" 112 | dependencies = [ 113 | "axum-core", 114 | "bytes", 115 | "form_urlencoded", 116 | "futures-util", 117 | "http", 118 | "http-body", 119 | "http-body-util", 120 | "hyper", 121 | "hyper-util", 122 | "itoa", 123 | "matchit", 124 | "memchr", 125 | "mime", 126 | "percent-encoding", 127 | "pin-project-lite", 128 | "rustversion", 129 | "serde", 130 | "serde_json", 131 | "serde_path_to_error", 132 | "serde_urlencoded", 133 | "sync_wrapper", 134 | "tokio", 135 | "tower", 136 | "tower-layer", 137 | "tower-service", 138 | "tracing", 139 | ] 140 | 141 | [[package]] 142 | name = "axum-core" 143 | version = "0.5.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" 146 | dependencies = [ 147 | "bytes", 148 | "futures-util", 149 | "http", 150 | "http-body", 151 | "http-body-util", 152 | "mime", 153 | "pin-project-lite", 154 | "rustversion", 155 | "sync_wrapper", 156 | "tower-layer", 157 | "tower-service", 158 | "tracing", 159 | ] 160 | 161 | [[package]] 162 | name = "backtrace" 163 | version = "0.3.74" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 166 | dependencies = [ 167 | "addr2line", 168 | "cfg-if", 169 | "libc", 170 | "miniz_oxide", 171 | "object", 172 | "rustc-demangle", 173 | "windows-targets", 174 | ] 175 | 176 | [[package]] 177 | name = "base64" 178 | version = "0.22.1" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 181 | 182 | [[package]] 183 | name = "bitflags" 184 | version = "1.3.2" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 187 | 188 | [[package]] 189 | name = "bitflags" 190 | version = "2.8.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 193 | 194 | [[package]] 195 | name = "bitvec" 196 | version = "1.0.1" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 199 | dependencies = [ 200 | "funty", 201 | "radium", 202 | "tap", 203 | "wyz", 204 | ] 205 | 206 | [[package]] 207 | name = "bstr" 208 | version = "1.11.3" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" 211 | dependencies = [ 212 | "memchr", 213 | "regex-automata", 214 | "serde", 215 | ] 216 | 217 | [[package]] 218 | name = "bumpalo" 219 | version = "3.16.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 222 | 223 | [[package]] 224 | name = "bytemuck" 225 | version = "1.21.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" 228 | 229 | [[package]] 230 | name = "byteorder-lite" 231 | version = "0.1.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 234 | 235 | [[package]] 236 | name = "bytes" 237 | version = "1.9.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 240 | 241 | [[package]] 242 | name = "cc" 243 | version = "1.2.10" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" 246 | dependencies = [ 247 | "shlex", 248 | ] 249 | 250 | [[package]] 251 | name = "cfg-if" 252 | version = "1.0.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 255 | 256 | [[package]] 257 | name = "chrono" 258 | version = "0.4.39" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 261 | dependencies = [ 262 | "android-tzdata", 263 | "iana-time-zone", 264 | "js-sys", 265 | "num-traits", 266 | "wasm-bindgen", 267 | "windows-targets", 268 | ] 269 | 270 | [[package]] 271 | name = "clap" 272 | version = "4.5.27" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" 275 | dependencies = [ 276 | "clap_builder", 277 | "clap_derive", 278 | ] 279 | 280 | [[package]] 281 | name = "clap_builder" 282 | version = "4.5.27" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" 285 | dependencies = [ 286 | "anstream", 287 | "anstyle", 288 | "clap_lex", 289 | "strsim", 290 | ] 291 | 292 | [[package]] 293 | name = "clap_derive" 294 | version = "4.5.24" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" 297 | dependencies = [ 298 | "heck", 299 | "proc-macro2", 300 | "quote", 301 | "syn", 302 | ] 303 | 304 | [[package]] 305 | name = "clap_lex" 306 | version = "0.7.4" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 309 | 310 | [[package]] 311 | name = "colorchoice" 312 | version = "1.0.3" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 315 | 316 | [[package]] 317 | name = "console" 318 | version = "0.15.11" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 321 | dependencies = [ 322 | "encode_unicode", 323 | "libc", 324 | "once_cell", 325 | "unicode-width", 326 | "windows-sys 0.59.0", 327 | ] 328 | 329 | [[package]] 330 | name = "core-foundation-sys" 331 | version = "0.8.7" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 334 | 335 | [[package]] 336 | name = "crc32fast" 337 | version = "1.4.2" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 340 | dependencies = [ 341 | "cfg-if", 342 | ] 343 | 344 | [[package]] 345 | name = "crossbeam-channel" 346 | version = "0.5.14" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" 349 | dependencies = [ 350 | "crossbeam-utils", 351 | ] 352 | 353 | [[package]] 354 | name = "crossbeam-deque" 355 | version = "0.8.6" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 358 | dependencies = [ 359 | "crossbeam-epoch", 360 | "crossbeam-utils", 361 | ] 362 | 363 | [[package]] 364 | name = "crossbeam-epoch" 365 | version = "0.9.18" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 368 | dependencies = [ 369 | "crossbeam-utils", 370 | ] 371 | 372 | [[package]] 373 | name = "crossbeam-utils" 374 | version = "0.8.21" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 377 | 378 | [[package]] 379 | name = "difflib" 380 | version = "0.4.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 383 | 384 | [[package]] 385 | name = "doc-comment" 386 | version = "0.3.3" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 389 | 390 | [[package]] 391 | name = "either" 392 | version = "1.14.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" 395 | 396 | [[package]] 397 | name = "encode_unicode" 398 | version = "1.0.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 401 | 402 | [[package]] 403 | name = "equivalent" 404 | version = "1.0.2" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 407 | 408 | [[package]] 409 | name = "errno" 410 | version = "0.3.10" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 413 | dependencies = [ 414 | "libc", 415 | "windows-sys 0.59.0", 416 | ] 417 | 418 | [[package]] 419 | name = "fastrand" 420 | version = "2.3.0" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 423 | 424 | [[package]] 425 | name = "fdeflate" 426 | version = "0.3.7" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 429 | dependencies = [ 430 | "simd-adler32", 431 | ] 432 | 433 | [[package]] 434 | name = "filetime" 435 | version = "0.2.25" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 438 | dependencies = [ 439 | "cfg-if", 440 | "libc", 441 | "libredox", 442 | "windows-sys 0.59.0", 443 | ] 444 | 445 | [[package]] 446 | name = "flate2" 447 | version = "1.0.35" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" 450 | dependencies = [ 451 | "crc32fast", 452 | "miniz_oxide", 453 | ] 454 | 455 | [[package]] 456 | name = "fnv" 457 | version = "1.0.7" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 460 | 461 | [[package]] 462 | name = "form_urlencoded" 463 | version = "1.2.1" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 466 | dependencies = [ 467 | "percent-encoding", 468 | ] 469 | 470 | [[package]] 471 | name = "funty" 472 | version = "2.0.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 475 | 476 | [[package]] 477 | name = "futures-channel" 478 | version = "0.3.31" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 481 | dependencies = [ 482 | "futures-core", 483 | ] 484 | 485 | [[package]] 486 | name = "futures-core" 487 | version = "0.3.31" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 490 | 491 | [[package]] 492 | name = "futures-task" 493 | version = "0.3.31" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 496 | 497 | [[package]] 498 | name = "futures-util" 499 | version = "0.3.31" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 502 | dependencies = [ 503 | "futures-core", 504 | "futures-task", 505 | "pin-project-lite", 506 | "pin-utils", 507 | ] 508 | 509 | [[package]] 510 | name = "getrandom" 511 | version = "0.2.15" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 514 | dependencies = [ 515 | "cfg-if", 516 | "libc", 517 | "wasi", 518 | ] 519 | 520 | [[package]] 521 | name = "gimli" 522 | version = "0.31.1" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 525 | 526 | [[package]] 527 | name = "hashbrown" 528 | version = "0.15.2" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 531 | 532 | [[package]] 533 | name = "heck" 534 | version = "0.5.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 537 | 538 | [[package]] 539 | name = "http" 540 | version = "1.2.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" 543 | dependencies = [ 544 | "bytes", 545 | "fnv", 546 | "itoa", 547 | ] 548 | 549 | [[package]] 550 | name = "http-body" 551 | version = "1.0.1" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 554 | dependencies = [ 555 | "bytes", 556 | "http", 557 | ] 558 | 559 | [[package]] 560 | name = "http-body-util" 561 | version = "0.1.2" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 564 | dependencies = [ 565 | "bytes", 566 | "futures-util", 567 | "http", 568 | "http-body", 569 | "pin-project-lite", 570 | ] 571 | 572 | [[package]] 573 | name = "httparse" 574 | version = "1.9.5" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 577 | 578 | [[package]] 579 | name = "httpdate" 580 | version = "1.0.3" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 583 | 584 | [[package]] 585 | name = "humansize" 586 | version = "2.1.3" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" 589 | dependencies = [ 590 | "libm", 591 | ] 592 | 593 | [[package]] 594 | name = "hyper" 595 | version = "1.5.2" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" 598 | dependencies = [ 599 | "bytes", 600 | "futures-channel", 601 | "futures-util", 602 | "http", 603 | "http-body", 604 | "httparse", 605 | "httpdate", 606 | "itoa", 607 | "pin-project-lite", 608 | "smallvec", 609 | "tokio", 610 | ] 611 | 612 | [[package]] 613 | name = "hyper-util" 614 | version = "0.1.10" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 617 | dependencies = [ 618 | "bytes", 619 | "futures-util", 620 | "http", 621 | "http-body", 622 | "hyper", 623 | "pin-project-lite", 624 | "tokio", 625 | "tower-service", 626 | ] 627 | 628 | [[package]] 629 | name = "iana-time-zone" 630 | version = "0.1.61" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 633 | dependencies = [ 634 | "android_system_properties", 635 | "core-foundation-sys", 636 | "iana-time-zone-haiku", 637 | "js-sys", 638 | "wasm-bindgen", 639 | "windows-core", 640 | ] 641 | 642 | [[package]] 643 | name = "iana-time-zone-haiku" 644 | version = "0.1.2" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 647 | dependencies = [ 648 | "cc", 649 | ] 650 | 651 | [[package]] 652 | name = "image" 653 | version = "0.25.5" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" 656 | dependencies = [ 657 | "bytemuck", 658 | "byteorder-lite", 659 | "num-traits", 660 | "png", 661 | ] 662 | 663 | [[package]] 664 | name = "imagesize" 665 | version = "0.13.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" 668 | 669 | [[package]] 670 | name = "indexmap" 671 | version = "2.7.1" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 674 | dependencies = [ 675 | "equivalent", 676 | "hashbrown", 677 | "rayon", 678 | ] 679 | 680 | [[package]] 681 | name = "indicatif" 682 | version = "0.17.11" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 685 | dependencies = [ 686 | "console", 687 | "number_prefix", 688 | "portable-atomic", 689 | "unicode-width", 690 | "web-time", 691 | ] 692 | 693 | [[package]] 694 | name = "is_terminal_polyfill" 695 | version = "1.70.1" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 698 | 699 | [[package]] 700 | name = "itoa" 701 | version = "1.0.14" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 704 | 705 | [[package]] 706 | name = "js-sys" 707 | version = "0.3.77" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 710 | dependencies = [ 711 | "once_cell", 712 | "wasm-bindgen", 713 | ] 714 | 715 | [[package]] 716 | name = "kompari" 717 | version = "0.1.0" 718 | dependencies = [ 719 | "image", 720 | "log", 721 | "oxipng", 722 | "rayon", 723 | "thiserror", 724 | "walkdir", 725 | ] 726 | 727 | [[package]] 728 | name = "kompari-cli" 729 | version = "0.1.0" 730 | dependencies = [ 731 | "assert_cmd", 732 | "clap", 733 | "kompari", 734 | "kompari-html", 735 | "kompari-tasks", 736 | "tempfile", 737 | ] 738 | 739 | [[package]] 740 | name = "kompari-html" 741 | version = "0.1.0" 742 | dependencies = [ 743 | "axum", 744 | "base64", 745 | "chrono", 746 | "imagesize", 747 | "kompari", 748 | "maud", 749 | "rayon", 750 | "serde", 751 | "tokio", 752 | ] 753 | 754 | [[package]] 755 | name = "kompari-tasks" 756 | version = "0.1.0" 757 | dependencies = [ 758 | "clap", 759 | "humansize", 760 | "indicatif", 761 | "kompari", 762 | "kompari-html", 763 | "rayon", 764 | "termcolor", 765 | ] 766 | 767 | [[package]] 768 | name = "libc" 769 | version = "0.2.169" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 772 | 773 | [[package]] 774 | name = "libdeflate-sys" 775 | version = "1.23.1" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "38b72ad3fbf5ac78f2df7b36075e48adf2459b57c150b9e63937d0204d0f9cd7" 778 | dependencies = [ 779 | "cc", 780 | ] 781 | 782 | [[package]] 783 | name = "libdeflater" 784 | version = "1.23.1" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "013344b17f9dceddff4872559ae19378bd8ee0479eccdd266d2dd2e894b4792f" 787 | dependencies = [ 788 | "libdeflate-sys", 789 | ] 790 | 791 | [[package]] 792 | name = "libm" 793 | version = "0.2.11" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" 796 | 797 | [[package]] 798 | name = "libredox" 799 | version = "0.1.3" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 802 | dependencies = [ 803 | "bitflags 2.8.0", 804 | "libc", 805 | "redox_syscall", 806 | ] 807 | 808 | [[package]] 809 | name = "linux-raw-sys" 810 | version = "0.4.15" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 813 | 814 | [[package]] 815 | name = "lockfree-object-pool" 816 | version = "0.1.6" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" 819 | 820 | [[package]] 821 | name = "log" 822 | version = "0.4.25" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 825 | 826 | [[package]] 827 | name = "matchit" 828 | version = "0.8.4" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 831 | 832 | [[package]] 833 | name = "maud" 834 | version = "0.27.0" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e" 837 | dependencies = [ 838 | "itoa", 839 | "maud_macros", 840 | ] 841 | 842 | [[package]] 843 | name = "maud_macros" 844 | version = "0.27.0" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca" 847 | dependencies = [ 848 | "proc-macro2", 849 | "proc-macro2-diagnostics", 850 | "quote", 851 | "syn", 852 | ] 853 | 854 | [[package]] 855 | name = "memchr" 856 | version = "2.7.4" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 859 | 860 | [[package]] 861 | name = "mime" 862 | version = "0.3.17" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 865 | 866 | [[package]] 867 | name = "miniz_oxide" 868 | version = "0.8.3" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" 871 | dependencies = [ 872 | "adler2", 873 | "simd-adler32", 874 | ] 875 | 876 | [[package]] 877 | name = "mio" 878 | version = "1.0.3" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 881 | dependencies = [ 882 | "libc", 883 | "wasi", 884 | "windows-sys 0.52.0", 885 | ] 886 | 887 | [[package]] 888 | name = "num-traits" 889 | version = "0.2.19" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 892 | dependencies = [ 893 | "autocfg", 894 | ] 895 | 896 | [[package]] 897 | name = "number_prefix" 898 | version = "0.4.0" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 901 | 902 | [[package]] 903 | name = "object" 904 | version = "0.36.7" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 907 | dependencies = [ 908 | "memchr", 909 | ] 910 | 911 | [[package]] 912 | name = "once_cell" 913 | version = "1.20.2" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 916 | 917 | [[package]] 918 | name = "oxipng" 919 | version = "9.1.4" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "3bce05680d3f2ec3f0510f19608d56712fa7ea681b4ba293c3b74a04c2e55279" 922 | dependencies = [ 923 | "bitvec", 924 | "crossbeam-channel", 925 | "filetime", 926 | "indexmap", 927 | "libdeflater", 928 | "log", 929 | "rayon", 930 | "rgb", 931 | "rustc-hash", 932 | "zopfli", 933 | ] 934 | 935 | [[package]] 936 | name = "percent-encoding" 937 | version = "2.3.1" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 940 | 941 | [[package]] 942 | name = "pin-project-lite" 943 | version = "0.2.16" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 946 | 947 | [[package]] 948 | name = "pin-utils" 949 | version = "0.1.0" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 952 | 953 | [[package]] 954 | name = "png" 955 | version = "0.17.16" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 958 | dependencies = [ 959 | "bitflags 1.3.2", 960 | "crc32fast", 961 | "fdeflate", 962 | "flate2", 963 | "miniz_oxide", 964 | ] 965 | 966 | [[package]] 967 | name = "portable-atomic" 968 | version = "1.11.0" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 971 | 972 | [[package]] 973 | name = "predicates" 974 | version = "3.1.3" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" 977 | dependencies = [ 978 | "anstyle", 979 | "difflib", 980 | "predicates-core", 981 | ] 982 | 983 | [[package]] 984 | name = "predicates-core" 985 | version = "1.0.9" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" 988 | 989 | [[package]] 990 | name = "predicates-tree" 991 | version = "1.0.12" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" 994 | dependencies = [ 995 | "predicates-core", 996 | "termtree", 997 | ] 998 | 999 | [[package]] 1000 | name = "proc-macro2" 1001 | version = "1.0.93" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 1004 | dependencies = [ 1005 | "unicode-ident", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "proc-macro2-diagnostics" 1010 | version = "0.10.1" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" 1013 | dependencies = [ 1014 | "proc-macro2", 1015 | "quote", 1016 | "syn", 1017 | "version_check", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "quote" 1022 | version = "1.0.38" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 1025 | dependencies = [ 1026 | "proc-macro2", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "radium" 1031 | version = "0.7.0" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 1034 | 1035 | [[package]] 1036 | name = "rayon" 1037 | version = "1.10.0" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 1040 | dependencies = [ 1041 | "either", 1042 | "rayon-core", 1043 | ] 1044 | 1045 | [[package]] 1046 | name = "rayon-core" 1047 | version = "1.12.1" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 1050 | dependencies = [ 1051 | "crossbeam-deque", 1052 | "crossbeam-utils", 1053 | ] 1054 | 1055 | [[package]] 1056 | name = "redox_syscall" 1057 | version = "0.5.10" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" 1060 | dependencies = [ 1061 | "bitflags 2.8.0", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "regex-automata" 1066 | version = "0.4.9" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1069 | 1070 | [[package]] 1071 | name = "rgb" 1072 | version = "0.8.50" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" 1075 | dependencies = [ 1076 | "bytemuck", 1077 | ] 1078 | 1079 | [[package]] 1080 | name = "rustc-demangle" 1081 | version = "0.1.24" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1084 | 1085 | [[package]] 1086 | name = "rustc-hash" 1087 | version = "2.1.1" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 1090 | 1091 | [[package]] 1092 | name = "rustix" 1093 | version = "0.38.44" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 1096 | dependencies = [ 1097 | "bitflags 2.8.0", 1098 | "errno", 1099 | "libc", 1100 | "linux-raw-sys", 1101 | "windows-sys 0.59.0", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "rustversion" 1106 | version = "1.0.19" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 1109 | 1110 | [[package]] 1111 | name = "ryu" 1112 | version = "1.0.18" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1115 | 1116 | [[package]] 1117 | name = "same-file" 1118 | version = "1.0.6" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1121 | dependencies = [ 1122 | "winapi-util", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "serde" 1127 | version = "1.0.217" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 1130 | dependencies = [ 1131 | "serde_derive", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "serde_derive" 1136 | version = "1.0.217" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 1139 | dependencies = [ 1140 | "proc-macro2", 1141 | "quote", 1142 | "syn", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "serde_json" 1147 | version = "1.0.137" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" 1150 | dependencies = [ 1151 | "itoa", 1152 | "memchr", 1153 | "ryu", 1154 | "serde", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "serde_path_to_error" 1159 | version = "0.1.16" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" 1162 | dependencies = [ 1163 | "itoa", 1164 | "serde", 1165 | ] 1166 | 1167 | [[package]] 1168 | name = "serde_urlencoded" 1169 | version = "0.7.1" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1172 | dependencies = [ 1173 | "form_urlencoded", 1174 | "itoa", 1175 | "ryu", 1176 | "serde", 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "shlex" 1181 | version = "1.3.0" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1184 | 1185 | [[package]] 1186 | name = "simd-adler32" 1187 | version = "0.3.7" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1190 | 1191 | [[package]] 1192 | name = "smallvec" 1193 | version = "1.13.2" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1196 | 1197 | [[package]] 1198 | name = "socket2" 1199 | version = "0.5.8" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1202 | dependencies = [ 1203 | "libc", 1204 | "windows-sys 0.52.0", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "strsim" 1209 | version = "0.11.1" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1212 | 1213 | [[package]] 1214 | name = "syn" 1215 | version = "2.0.96" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 1218 | dependencies = [ 1219 | "proc-macro2", 1220 | "quote", 1221 | "unicode-ident", 1222 | ] 1223 | 1224 | [[package]] 1225 | name = "sync_wrapper" 1226 | version = "1.0.2" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1229 | 1230 | [[package]] 1231 | name = "tap" 1232 | version = "1.0.1" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 1235 | 1236 | [[package]] 1237 | name = "tempfile" 1238 | version = "3.15.0" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" 1241 | dependencies = [ 1242 | "cfg-if", 1243 | "fastrand", 1244 | "getrandom", 1245 | "once_cell", 1246 | "rustix", 1247 | "windows-sys 0.59.0", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "termcolor" 1252 | version = "1.4.1" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1255 | dependencies = [ 1256 | "winapi-util", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "termtree" 1261 | version = "0.5.1" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" 1264 | 1265 | [[package]] 1266 | name = "thiserror" 1267 | version = "2.0.11" 1268 | source = "registry+https://github.com/rust-lang/crates.io-index" 1269 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 1270 | dependencies = [ 1271 | "thiserror-impl", 1272 | ] 1273 | 1274 | [[package]] 1275 | name = "thiserror-impl" 1276 | version = "2.0.11" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 1279 | dependencies = [ 1280 | "proc-macro2", 1281 | "quote", 1282 | "syn", 1283 | ] 1284 | 1285 | [[package]] 1286 | name = "tokio" 1287 | version = "1.43.0" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 1290 | dependencies = [ 1291 | "backtrace", 1292 | "libc", 1293 | "mio", 1294 | "pin-project-lite", 1295 | "socket2", 1296 | "tokio-macros", 1297 | "windows-sys 0.52.0", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "tokio-macros" 1302 | version = "2.5.0" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1305 | dependencies = [ 1306 | "proc-macro2", 1307 | "quote", 1308 | "syn", 1309 | ] 1310 | 1311 | [[package]] 1312 | name = "tower" 1313 | version = "0.5.2" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1316 | dependencies = [ 1317 | "futures-core", 1318 | "futures-util", 1319 | "pin-project-lite", 1320 | "sync_wrapper", 1321 | "tokio", 1322 | "tower-layer", 1323 | "tower-service", 1324 | "tracing", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "tower-layer" 1329 | version = "0.3.3" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1332 | 1333 | [[package]] 1334 | name = "tower-service" 1335 | version = "0.3.3" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1338 | 1339 | [[package]] 1340 | name = "tracing" 1341 | version = "0.1.41" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1344 | dependencies = [ 1345 | "log", 1346 | "pin-project-lite", 1347 | "tracing-core", 1348 | ] 1349 | 1350 | [[package]] 1351 | name = "tracing-core" 1352 | version = "0.1.33" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1355 | dependencies = [ 1356 | "once_cell", 1357 | ] 1358 | 1359 | [[package]] 1360 | name = "unicode-ident" 1361 | version = "1.0.15" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" 1364 | 1365 | [[package]] 1366 | name = "unicode-width" 1367 | version = "0.2.0" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1370 | 1371 | [[package]] 1372 | name = "utf8parse" 1373 | version = "0.2.2" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1376 | 1377 | [[package]] 1378 | name = "version_check" 1379 | version = "0.9.5" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1382 | 1383 | [[package]] 1384 | name = "wait-timeout" 1385 | version = "0.2.0" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1388 | dependencies = [ 1389 | "libc", 1390 | ] 1391 | 1392 | [[package]] 1393 | name = "walkdir" 1394 | version = "2.5.0" 1395 | source = "registry+https://github.com/rust-lang/crates.io-index" 1396 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1397 | dependencies = [ 1398 | "same-file", 1399 | "winapi-util", 1400 | ] 1401 | 1402 | [[package]] 1403 | name = "wasi" 1404 | version = "0.11.0+wasi-snapshot-preview1" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1407 | 1408 | [[package]] 1409 | name = "wasm-bindgen" 1410 | version = "0.2.100" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1413 | dependencies = [ 1414 | "cfg-if", 1415 | "once_cell", 1416 | "rustversion", 1417 | "wasm-bindgen-macro", 1418 | ] 1419 | 1420 | [[package]] 1421 | name = "wasm-bindgen-backend" 1422 | version = "0.2.100" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1425 | dependencies = [ 1426 | "bumpalo", 1427 | "log", 1428 | "proc-macro2", 1429 | "quote", 1430 | "syn", 1431 | "wasm-bindgen-shared", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "wasm-bindgen-macro" 1436 | version = "0.2.100" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1439 | dependencies = [ 1440 | "quote", 1441 | "wasm-bindgen-macro-support", 1442 | ] 1443 | 1444 | [[package]] 1445 | name = "wasm-bindgen-macro-support" 1446 | version = "0.2.100" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1449 | dependencies = [ 1450 | "proc-macro2", 1451 | "quote", 1452 | "syn", 1453 | "wasm-bindgen-backend", 1454 | "wasm-bindgen-shared", 1455 | ] 1456 | 1457 | [[package]] 1458 | name = "wasm-bindgen-shared" 1459 | version = "0.2.100" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1462 | dependencies = [ 1463 | "unicode-ident", 1464 | ] 1465 | 1466 | [[package]] 1467 | name = "web-time" 1468 | version = "1.1.0" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1471 | dependencies = [ 1472 | "js-sys", 1473 | "wasm-bindgen", 1474 | ] 1475 | 1476 | [[package]] 1477 | name = "winapi-util" 1478 | version = "0.1.9" 1479 | source = "registry+https://github.com/rust-lang/crates.io-index" 1480 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1481 | dependencies = [ 1482 | "windows-sys 0.59.0", 1483 | ] 1484 | 1485 | [[package]] 1486 | name = "windows-core" 1487 | version = "0.52.0" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1490 | dependencies = [ 1491 | "windows-targets", 1492 | ] 1493 | 1494 | [[package]] 1495 | name = "windows-sys" 1496 | version = "0.52.0" 1497 | source = "registry+https://github.com/rust-lang/crates.io-index" 1498 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1499 | dependencies = [ 1500 | "windows-targets", 1501 | ] 1502 | 1503 | [[package]] 1504 | name = "windows-sys" 1505 | version = "0.59.0" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1508 | dependencies = [ 1509 | "windows-targets", 1510 | ] 1511 | 1512 | [[package]] 1513 | name = "windows-targets" 1514 | version = "0.52.6" 1515 | source = "registry+https://github.com/rust-lang/crates.io-index" 1516 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1517 | dependencies = [ 1518 | "windows_aarch64_gnullvm", 1519 | "windows_aarch64_msvc", 1520 | "windows_i686_gnu", 1521 | "windows_i686_gnullvm", 1522 | "windows_i686_msvc", 1523 | "windows_x86_64_gnu", 1524 | "windows_x86_64_gnullvm", 1525 | "windows_x86_64_msvc", 1526 | ] 1527 | 1528 | [[package]] 1529 | name = "windows_aarch64_gnullvm" 1530 | version = "0.52.6" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1533 | 1534 | [[package]] 1535 | name = "windows_aarch64_msvc" 1536 | version = "0.52.6" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1539 | 1540 | [[package]] 1541 | name = "windows_i686_gnu" 1542 | version = "0.52.6" 1543 | source = "registry+https://github.com/rust-lang/crates.io-index" 1544 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1545 | 1546 | [[package]] 1547 | name = "windows_i686_gnullvm" 1548 | version = "0.52.6" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1551 | 1552 | [[package]] 1553 | name = "windows_i686_msvc" 1554 | version = "0.52.6" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1557 | 1558 | [[package]] 1559 | name = "windows_x86_64_gnu" 1560 | version = "0.52.6" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1563 | 1564 | [[package]] 1565 | name = "windows_x86_64_gnullvm" 1566 | version = "0.52.6" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1569 | 1570 | [[package]] 1571 | name = "windows_x86_64_msvc" 1572 | version = "0.52.6" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1575 | 1576 | [[package]] 1577 | name = "wyz" 1578 | version = "0.5.1" 1579 | source = "registry+https://github.com/rust-lang/crates.io-index" 1580 | checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 1581 | dependencies = [ 1582 | "tap", 1583 | ] 1584 | 1585 | [[package]] 1586 | name = "zopfli" 1587 | version = "0.8.1" 1588 | source = "registry+https://github.com/rust-lang/crates.io-index" 1589 | checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" 1590 | dependencies = [ 1591 | "bumpalo", 1592 | "crc32fast", 1593 | "lockfree-object-pool", 1594 | "log", 1595 | "once_cell", 1596 | "simd-adler32", 1597 | ] 1598 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["kompari", "kompari-cli", "kompari-html", "kompari-tasks"] 4 | 5 | [workspace.package] 6 | version = "0.1.0" 7 | edition = "2021" 8 | # Keep in sync with RUST_MIN_VER in .github/workflows/ci.yml, with the relevant README.md files, 9 | # and with the MSRV in the Unreleased section of CHANGELOG.md. 10 | rust-version = "1.78" 11 | license = "Apache-2.0 OR MIT" 12 | repository = "https://github.com/linebender/kompari" 13 | 14 | [workspace.dependencies] 15 | image = { version = "0.25", default-features = false, features = ["png"] } 16 | thiserror = { version = "2" } 17 | clap = { version = "4.5", features = ["derive"] } 18 | oxipng = { version = "9.1", features = [ 19 | "parallel", 20 | "zopfli", 21 | "filetime", 22 | ], default-features = false } 23 | rayon = "1.10" # Make sure that we are using the version as in oxipng 24 | log = "0.4" 25 | 26 | [workspace.lints] 27 | # This one may vary depending on the project. 28 | rust.unsafe_code = "forbid" 29 | 30 | # LINEBENDER LINT SET - Cargo.toml - v6 31 | # See https://linebender.org/wiki/canonical-lints/ 32 | rust.keyword_idents_2024 = "forbid" 33 | rust.non_ascii_idents = "forbid" 34 | rust.non_local_definitions = "forbid" 35 | rust.unsafe_op_in_unsafe_fn = "forbid" 36 | 37 | rust.elided_lifetimes_in_paths = "warn" 38 | rust.missing_debug_implementations = "warn" 39 | # rust.missing_docs = "warn" 40 | # TODO: We should document things 41 | rust.trivial_numeric_casts = "warn" 42 | rust.unexpected_cfgs = "warn" 43 | rust.unnameable_types = "warn" 44 | rust.unreachable_pub = "warn" 45 | rust.unused_import_braces = "warn" 46 | rust.unused_lifetimes = "warn" 47 | rust.unused_macro_rules = "warn" 48 | 49 | clippy.too_many_arguments = "allow" 50 | 51 | clippy.allow_attributes_without_reason = "warn" 52 | clippy.cast_possible_truncation = "warn" 53 | clippy.collection_is_never_read = "warn" 54 | clippy.dbg_macro = "warn" 55 | clippy.debug_assert_with_mut_call = "warn" 56 | clippy.doc_markdown = "warn" 57 | clippy.fn_to_numeric_cast_any = "warn" 58 | clippy.infinite_loop = "warn" 59 | clippy.large_stack_arrays = "warn" 60 | clippy.mismatching_type_param_order = "warn" 61 | clippy.missing_assert_message = "warn" 62 | clippy.missing_fields_in_debug = "warn" 63 | clippy.same_functions_in_if_condition = "warn" 64 | clippy.semicolon_if_nothing_returned = "warn" 65 | clippy.should_panic_without_expect = "warn" 66 | clippy.todo = "warn" 67 | clippy.unseparated_literal_suffix = "warn" 68 | clippy.use_self = "warn" 69 | 70 | clippy.cargo_common_metadata = "warn" 71 | clippy.negative_feature_names = "warn" 72 | clippy.redundant_feature_names = "warn" 73 | clippy.wildcard_dependencies = "warn" 74 | # END LINEBENDER LINT SET 75 | 76 | [profile.ci] 77 | inherits = "dev" 78 | [profile.ci.package."*"] 79 | debug-assertions = true # Keep always on for dependencies for cache reuse. 80 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2020 the Kompari Authors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Kompari 6 | 7 | *Kompari* is a tool for reporting image differences. It is intended for use in snapshot testing. 8 | It can be used as a stand-alone CLI tool or as a Rust crate. 9 | 10 | 11 | 12 | 13 | ## CLI 14 | 15 | ### Local build 16 | 17 | ```commandline 18 | $ cargo build --release 19 | ``` 20 | 21 | ### Usage 22 | 23 | Create static HTML report: 24 | 25 | ```commandline 26 | $ cargo run --release report 27 | ``` 28 | 29 | Start HTTP server for interactive test blessing: 30 | 31 | ```commandline 32 | $ cargo run --release review 33 | ``` 34 | 35 | 36 | ## Minimum supported Rust Version (MSRV) 37 | 38 | This version of Kompari has been verified to compile with **Rust 1.78** and later. 39 | 40 | Future versions of Kompari might increase the Rust version requirement. 41 | It will not be treated as a breaking change and as such can even happen with small patch releases. 42 | 43 |
44 | Click here if compiling fails. 45 | 46 | As time has passed, some of Kompari's dependencies could have released versions with a higher Rust requirement. 47 | If you encounter a compilation issue due to a dependency and don't want to upgrade your Rust toolchain, then you could downgrade the dependency. 48 | 49 | ```sh 50 | # Use the problematic dependency's name and version 51 | cargo update -p package_name --precise 0.1.1 52 | ``` 53 | 54 |
55 | 56 | ## License 57 | 58 | Licensed under either of 59 | 60 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) 61 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 62 | 63 | at your option. 64 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/docs/logo.png -------------------------------------------------------------------------------- /docs/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/docs/logo_small.png -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/docs/screenshot.png -------------------------------------------------------------------------------- /example/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask_kompari --" 3 | -------------------------------------------------------------------------------- /example/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.18" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "is_terminal_polyfill", 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle" 52 | version = "1.0.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 70 | dependencies = [ 71 | "windows-sys 0.59.0", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.6" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 79 | dependencies = [ 80 | "anstyle", 81 | "windows-sys 0.59.0", 82 | ] 83 | 84 | [[package]] 85 | name = "autocfg" 86 | version = "1.4.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 89 | 90 | [[package]] 91 | name = "axum" 92 | version = "0.8.1" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" 95 | dependencies = [ 96 | "axum-core", 97 | "bytes", 98 | "form_urlencoded", 99 | "futures-util", 100 | "http", 101 | "http-body", 102 | "http-body-util", 103 | "hyper", 104 | "hyper-util", 105 | "itoa", 106 | "matchit", 107 | "memchr", 108 | "mime", 109 | "percent-encoding", 110 | "pin-project-lite", 111 | "rustversion", 112 | "serde", 113 | "serde_json", 114 | "serde_path_to_error", 115 | "serde_urlencoded", 116 | "sync_wrapper", 117 | "tokio", 118 | "tower", 119 | "tower-layer", 120 | "tower-service", 121 | "tracing", 122 | ] 123 | 124 | [[package]] 125 | name = "axum-core" 126 | version = "0.5.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" 129 | dependencies = [ 130 | "bytes", 131 | "futures-util", 132 | "http", 133 | "http-body", 134 | "http-body-util", 135 | "mime", 136 | "pin-project-lite", 137 | "rustversion", 138 | "sync_wrapper", 139 | "tower-layer", 140 | "tower-service", 141 | "tracing", 142 | ] 143 | 144 | [[package]] 145 | name = "backtrace" 146 | version = "0.3.74" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 149 | dependencies = [ 150 | "addr2line", 151 | "cfg-if", 152 | "libc", 153 | "miniz_oxide", 154 | "object", 155 | "rustc-demangle", 156 | "windows-targets", 157 | ] 158 | 159 | [[package]] 160 | name = "base64" 161 | version = "0.22.1" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 164 | 165 | [[package]] 166 | name = "bitflags" 167 | version = "1.3.2" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 170 | 171 | [[package]] 172 | name = "bitflags" 173 | version = "2.9.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 176 | 177 | [[package]] 178 | name = "bitvec" 179 | version = "1.0.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 182 | dependencies = [ 183 | "funty", 184 | "radium", 185 | "tap", 186 | "wyz", 187 | ] 188 | 189 | [[package]] 190 | name = "bumpalo" 191 | version = "3.16.0" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 194 | 195 | [[package]] 196 | name = "bytemuck" 197 | version = "1.20.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" 200 | 201 | [[package]] 202 | name = "byteorder-lite" 203 | version = "0.1.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 206 | 207 | [[package]] 208 | name = "bytes" 209 | version = "1.9.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 212 | 213 | [[package]] 214 | name = "cc" 215 | version = "1.2.3" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" 218 | dependencies = [ 219 | "shlex", 220 | ] 221 | 222 | [[package]] 223 | name = "cfg-if" 224 | version = "1.0.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 227 | 228 | [[package]] 229 | name = "chrono" 230 | version = "0.4.39" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 233 | dependencies = [ 234 | "android-tzdata", 235 | "iana-time-zone", 236 | "js-sys", 237 | "num-traits", 238 | "wasm-bindgen", 239 | "windows-targets", 240 | ] 241 | 242 | [[package]] 243 | name = "clap" 244 | version = "4.5.23" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 247 | dependencies = [ 248 | "clap_builder", 249 | "clap_derive", 250 | ] 251 | 252 | [[package]] 253 | name = "clap_builder" 254 | version = "4.5.23" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 257 | dependencies = [ 258 | "anstream", 259 | "anstyle", 260 | "clap_lex", 261 | "strsim", 262 | ] 263 | 264 | [[package]] 265 | name = "clap_derive" 266 | version = "4.5.18" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 269 | dependencies = [ 270 | "heck", 271 | "proc-macro2", 272 | "quote", 273 | "syn", 274 | ] 275 | 276 | [[package]] 277 | name = "clap_lex" 278 | version = "0.7.4" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 281 | 282 | [[package]] 283 | name = "colorchoice" 284 | version = "1.0.3" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 287 | 288 | [[package]] 289 | name = "console" 290 | version = "0.15.11" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 293 | dependencies = [ 294 | "encode_unicode", 295 | "libc", 296 | "once_cell", 297 | "unicode-width", 298 | "windows-sys 0.59.0", 299 | ] 300 | 301 | [[package]] 302 | name = "core-foundation-sys" 303 | version = "0.8.7" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 306 | 307 | [[package]] 308 | name = "crc32fast" 309 | version = "1.4.2" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 312 | dependencies = [ 313 | "cfg-if", 314 | ] 315 | 316 | [[package]] 317 | name = "crossbeam-channel" 318 | version = "0.5.14" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" 321 | dependencies = [ 322 | "crossbeam-utils", 323 | ] 324 | 325 | [[package]] 326 | name = "crossbeam-deque" 327 | version = "0.8.6" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 330 | dependencies = [ 331 | "crossbeam-epoch", 332 | "crossbeam-utils", 333 | ] 334 | 335 | [[package]] 336 | name = "crossbeam-epoch" 337 | version = "0.9.18" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 340 | dependencies = [ 341 | "crossbeam-utils", 342 | ] 343 | 344 | [[package]] 345 | name = "crossbeam-utils" 346 | version = "0.8.21" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 349 | 350 | [[package]] 351 | name = "demolib" 352 | version = "0.1.0" 353 | dependencies = [ 354 | "image", 355 | ] 356 | 357 | [[package]] 358 | name = "either" 359 | version = "1.14.0" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" 362 | 363 | [[package]] 364 | name = "encode_unicode" 365 | version = "1.0.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 368 | 369 | [[package]] 370 | name = "equivalent" 371 | version = "1.0.2" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 374 | 375 | [[package]] 376 | name = "fdeflate" 377 | version = "0.3.7" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 380 | dependencies = [ 381 | "simd-adler32", 382 | ] 383 | 384 | [[package]] 385 | name = "filetime" 386 | version = "0.2.25" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 389 | dependencies = [ 390 | "cfg-if", 391 | "libc", 392 | "libredox", 393 | "windows-sys 0.59.0", 394 | ] 395 | 396 | [[package]] 397 | name = "flate2" 398 | version = "1.0.35" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" 401 | dependencies = [ 402 | "crc32fast", 403 | "miniz_oxide", 404 | ] 405 | 406 | [[package]] 407 | name = "fnv" 408 | version = "1.0.7" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 411 | 412 | [[package]] 413 | name = "form_urlencoded" 414 | version = "1.2.1" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 417 | dependencies = [ 418 | "percent-encoding", 419 | ] 420 | 421 | [[package]] 422 | name = "funty" 423 | version = "2.0.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 426 | 427 | [[package]] 428 | name = "futures-channel" 429 | version = "0.3.31" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 432 | dependencies = [ 433 | "futures-core", 434 | ] 435 | 436 | [[package]] 437 | name = "futures-core" 438 | version = "0.3.31" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 441 | 442 | [[package]] 443 | name = "futures-task" 444 | version = "0.3.31" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 447 | 448 | [[package]] 449 | name = "futures-util" 450 | version = "0.3.31" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 453 | dependencies = [ 454 | "futures-core", 455 | "futures-task", 456 | "pin-project-lite", 457 | "pin-utils", 458 | ] 459 | 460 | [[package]] 461 | name = "gimli" 462 | version = "0.31.1" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 465 | 466 | [[package]] 467 | name = "hashbrown" 468 | version = "0.15.2" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 471 | 472 | [[package]] 473 | name = "heck" 474 | version = "0.5.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 477 | 478 | [[package]] 479 | name = "http" 480 | version = "1.2.0" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" 483 | dependencies = [ 484 | "bytes", 485 | "fnv", 486 | "itoa", 487 | ] 488 | 489 | [[package]] 490 | name = "http-body" 491 | version = "1.0.1" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 494 | dependencies = [ 495 | "bytes", 496 | "http", 497 | ] 498 | 499 | [[package]] 500 | name = "http-body-util" 501 | version = "0.1.2" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 504 | dependencies = [ 505 | "bytes", 506 | "futures-util", 507 | "http", 508 | "http-body", 509 | "pin-project-lite", 510 | ] 511 | 512 | [[package]] 513 | name = "httparse" 514 | version = "1.9.5" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 517 | 518 | [[package]] 519 | name = "httpdate" 520 | version = "1.0.3" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 523 | 524 | [[package]] 525 | name = "humansize" 526 | version = "2.1.3" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" 529 | dependencies = [ 530 | "libm", 531 | ] 532 | 533 | [[package]] 534 | name = "hyper" 535 | version = "1.5.2" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" 538 | dependencies = [ 539 | "bytes", 540 | "futures-channel", 541 | "futures-util", 542 | "http", 543 | "http-body", 544 | "httparse", 545 | "httpdate", 546 | "itoa", 547 | "pin-project-lite", 548 | "smallvec", 549 | "tokio", 550 | ] 551 | 552 | [[package]] 553 | name = "hyper-util" 554 | version = "0.1.10" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 557 | dependencies = [ 558 | "bytes", 559 | "futures-util", 560 | "http", 561 | "http-body", 562 | "hyper", 563 | "pin-project-lite", 564 | "tokio", 565 | "tower-service", 566 | ] 567 | 568 | [[package]] 569 | name = "iana-time-zone" 570 | version = "0.1.61" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 573 | dependencies = [ 574 | "android_system_properties", 575 | "core-foundation-sys", 576 | "iana-time-zone-haiku", 577 | "js-sys", 578 | "wasm-bindgen", 579 | "windows-core", 580 | ] 581 | 582 | [[package]] 583 | name = "iana-time-zone-haiku" 584 | version = "0.1.2" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 587 | dependencies = [ 588 | "cc", 589 | ] 590 | 591 | [[package]] 592 | name = "image" 593 | version = "0.25.5" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" 596 | dependencies = [ 597 | "bytemuck", 598 | "byteorder-lite", 599 | "num-traits", 600 | "png", 601 | ] 602 | 603 | [[package]] 604 | name = "imagesize" 605 | version = "0.13.0" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" 608 | 609 | [[package]] 610 | name = "indexmap" 611 | version = "2.7.1" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 614 | dependencies = [ 615 | "equivalent", 616 | "hashbrown", 617 | "rayon", 618 | ] 619 | 620 | [[package]] 621 | name = "indicatif" 622 | version = "0.17.11" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 625 | dependencies = [ 626 | "console", 627 | "number_prefix", 628 | "portable-atomic", 629 | "unicode-width", 630 | "web-time", 631 | ] 632 | 633 | [[package]] 634 | name = "is_terminal_polyfill" 635 | version = "1.70.1" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 638 | 639 | [[package]] 640 | name = "itoa" 641 | version = "1.0.14" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 644 | 645 | [[package]] 646 | name = "js-sys" 647 | version = "0.3.76" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" 650 | dependencies = [ 651 | "once_cell", 652 | "wasm-bindgen", 653 | ] 654 | 655 | [[package]] 656 | name = "kompari" 657 | version = "0.1.0" 658 | dependencies = [ 659 | "image", 660 | "log", 661 | "oxipng", 662 | "rayon", 663 | "thiserror", 664 | "walkdir", 665 | ] 666 | 667 | [[package]] 668 | name = "kompari-html" 669 | version = "0.1.0" 670 | dependencies = [ 671 | "axum", 672 | "base64", 673 | "chrono", 674 | "imagesize", 675 | "kompari", 676 | "maud", 677 | "rayon", 678 | "serde", 679 | "tokio", 680 | ] 681 | 682 | [[package]] 683 | name = "kompari-tasks" 684 | version = "0.1.0" 685 | dependencies = [ 686 | "clap", 687 | "humansize", 688 | "indicatif", 689 | "kompari", 690 | "kompari-html", 691 | "rayon", 692 | "termcolor", 693 | ] 694 | 695 | [[package]] 696 | name = "libc" 697 | version = "0.2.168" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" 700 | 701 | [[package]] 702 | name = "libdeflate-sys" 703 | version = "1.23.1" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "38b72ad3fbf5ac78f2df7b36075e48adf2459b57c150b9e63937d0204d0f9cd7" 706 | dependencies = [ 707 | "cc", 708 | ] 709 | 710 | [[package]] 711 | name = "libdeflater" 712 | version = "1.23.1" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "013344b17f9dceddff4872559ae19378bd8ee0479eccdd266d2dd2e894b4792f" 715 | dependencies = [ 716 | "libdeflate-sys", 717 | ] 718 | 719 | [[package]] 720 | name = "libm" 721 | version = "0.2.11" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" 724 | 725 | [[package]] 726 | name = "libredox" 727 | version = "0.1.3" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 730 | dependencies = [ 731 | "bitflags 2.9.0", 732 | "libc", 733 | "redox_syscall", 734 | ] 735 | 736 | [[package]] 737 | name = "lockfree-object-pool" 738 | version = "0.1.6" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" 741 | 742 | [[package]] 743 | name = "log" 744 | version = "0.4.22" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 747 | 748 | [[package]] 749 | name = "matchit" 750 | version = "0.8.4" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 753 | 754 | [[package]] 755 | name = "maud" 756 | version = "0.27.0" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e" 759 | dependencies = [ 760 | "itoa", 761 | "maud_macros", 762 | ] 763 | 764 | [[package]] 765 | name = "maud_macros" 766 | version = "0.27.0" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca" 769 | dependencies = [ 770 | "proc-macro2", 771 | "proc-macro2-diagnostics", 772 | "quote", 773 | "syn", 774 | ] 775 | 776 | [[package]] 777 | name = "memchr" 778 | version = "2.7.4" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 781 | 782 | [[package]] 783 | name = "mime" 784 | version = "0.3.17" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 787 | 788 | [[package]] 789 | name = "miniz_oxide" 790 | version = "0.8.0" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 793 | dependencies = [ 794 | "adler2", 795 | "simd-adler32", 796 | ] 797 | 798 | [[package]] 799 | name = "mio" 800 | version = "1.0.3" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 803 | dependencies = [ 804 | "libc", 805 | "wasi", 806 | "windows-sys 0.52.0", 807 | ] 808 | 809 | [[package]] 810 | name = "num-traits" 811 | version = "0.2.19" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 814 | dependencies = [ 815 | "autocfg", 816 | ] 817 | 818 | [[package]] 819 | name = "number_prefix" 820 | version = "0.4.0" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 823 | 824 | [[package]] 825 | name = "object" 826 | version = "0.36.7" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 829 | dependencies = [ 830 | "memchr", 831 | ] 832 | 833 | [[package]] 834 | name = "once_cell" 835 | version = "1.20.2" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 838 | 839 | [[package]] 840 | name = "oxipng" 841 | version = "9.1.4" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "3bce05680d3f2ec3f0510f19608d56712fa7ea681b4ba293c3b74a04c2e55279" 844 | dependencies = [ 845 | "bitvec", 846 | "crossbeam-channel", 847 | "filetime", 848 | "indexmap", 849 | "libdeflater", 850 | "log", 851 | "rayon", 852 | "rgb", 853 | "rustc-hash", 854 | "zopfli", 855 | ] 856 | 857 | [[package]] 858 | name = "percent-encoding" 859 | version = "2.3.1" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 862 | 863 | [[package]] 864 | name = "pin-project-lite" 865 | version = "0.2.16" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 868 | 869 | [[package]] 870 | name = "pin-utils" 871 | version = "0.1.0" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 874 | 875 | [[package]] 876 | name = "png" 877 | version = "0.17.15" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "b67582bd5b65bdff614270e2ea89a1cf15bef71245cc1e5f7ea126977144211d" 880 | dependencies = [ 881 | "bitflags 1.3.2", 882 | "crc32fast", 883 | "fdeflate", 884 | "flate2", 885 | "miniz_oxide", 886 | ] 887 | 888 | [[package]] 889 | name = "portable-atomic" 890 | version = "1.11.0" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 893 | 894 | [[package]] 895 | name = "proc-macro2" 896 | version = "1.0.92" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 899 | dependencies = [ 900 | "unicode-ident", 901 | ] 902 | 903 | [[package]] 904 | name = "proc-macro2-diagnostics" 905 | version = "0.10.1" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" 908 | dependencies = [ 909 | "proc-macro2", 910 | "quote", 911 | "syn", 912 | "version_check", 913 | ] 914 | 915 | [[package]] 916 | name = "quote" 917 | version = "1.0.37" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 920 | dependencies = [ 921 | "proc-macro2", 922 | ] 923 | 924 | [[package]] 925 | name = "radium" 926 | version = "0.7.0" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 929 | 930 | [[package]] 931 | name = "rayon" 932 | version = "1.10.0" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 935 | dependencies = [ 936 | "either", 937 | "rayon-core", 938 | ] 939 | 940 | [[package]] 941 | name = "rayon-core" 942 | version = "1.12.1" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 945 | dependencies = [ 946 | "crossbeam-deque", 947 | "crossbeam-utils", 948 | ] 949 | 950 | [[package]] 951 | name = "redox_syscall" 952 | version = "0.5.10" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" 955 | dependencies = [ 956 | "bitflags 2.9.0", 957 | ] 958 | 959 | [[package]] 960 | name = "rgb" 961 | version = "0.8.50" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" 964 | dependencies = [ 965 | "bytemuck", 966 | ] 967 | 968 | [[package]] 969 | name = "rustc-demangle" 970 | version = "0.1.24" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 973 | 974 | [[package]] 975 | name = "rustc-hash" 976 | version = "2.1.1" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 979 | 980 | [[package]] 981 | name = "rustversion" 982 | version = "1.0.19" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 985 | 986 | [[package]] 987 | name = "ryu" 988 | version = "1.0.18" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 991 | 992 | [[package]] 993 | name = "same-file" 994 | version = "1.0.6" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 997 | dependencies = [ 998 | "winapi-util", 999 | ] 1000 | 1001 | [[package]] 1002 | name = "serde" 1003 | version = "1.0.217" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 1006 | dependencies = [ 1007 | "serde_derive", 1008 | ] 1009 | 1010 | [[package]] 1011 | name = "serde_derive" 1012 | version = "1.0.217" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 1015 | dependencies = [ 1016 | "proc-macro2", 1017 | "quote", 1018 | "syn", 1019 | ] 1020 | 1021 | [[package]] 1022 | name = "serde_json" 1023 | version = "1.0.137" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" 1026 | dependencies = [ 1027 | "itoa", 1028 | "memchr", 1029 | "ryu", 1030 | "serde", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "serde_path_to_error" 1035 | version = "0.1.16" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" 1038 | dependencies = [ 1039 | "itoa", 1040 | "serde", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "serde_urlencoded" 1045 | version = "0.7.1" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1048 | dependencies = [ 1049 | "form_urlencoded", 1050 | "itoa", 1051 | "ryu", 1052 | "serde", 1053 | ] 1054 | 1055 | [[package]] 1056 | name = "shlex" 1057 | version = "1.3.0" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1060 | 1061 | [[package]] 1062 | name = "simd-adler32" 1063 | version = "0.3.7" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1066 | 1067 | [[package]] 1068 | name = "smallvec" 1069 | version = "1.13.2" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1072 | 1073 | [[package]] 1074 | name = "socket2" 1075 | version = "0.5.8" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1078 | dependencies = [ 1079 | "libc", 1080 | "windows-sys 0.52.0", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "strsim" 1085 | version = "0.11.1" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1088 | 1089 | [[package]] 1090 | name = "syn" 1091 | version = "2.0.90" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 1094 | dependencies = [ 1095 | "proc-macro2", 1096 | "quote", 1097 | "unicode-ident", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "sync_wrapper" 1102 | version = "1.0.2" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1105 | 1106 | [[package]] 1107 | name = "tap" 1108 | version = "1.0.1" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 1111 | 1112 | [[package]] 1113 | name = "termcolor" 1114 | version = "1.4.1" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1117 | dependencies = [ 1118 | "winapi-util", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "thiserror" 1123 | version = "2.0.6" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" 1126 | dependencies = [ 1127 | "thiserror-impl", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "thiserror-impl" 1132 | version = "2.0.6" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" 1135 | dependencies = [ 1136 | "proc-macro2", 1137 | "quote", 1138 | "syn", 1139 | ] 1140 | 1141 | [[package]] 1142 | name = "tokio" 1143 | version = "1.43.0" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 1146 | dependencies = [ 1147 | "backtrace", 1148 | "libc", 1149 | "mio", 1150 | "pin-project-lite", 1151 | "socket2", 1152 | "tokio-macros", 1153 | "windows-sys 0.52.0", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "tokio-macros" 1158 | version = "2.5.0" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1161 | dependencies = [ 1162 | "proc-macro2", 1163 | "quote", 1164 | "syn", 1165 | ] 1166 | 1167 | [[package]] 1168 | name = "tower" 1169 | version = "0.5.2" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1172 | dependencies = [ 1173 | "futures-core", 1174 | "futures-util", 1175 | "pin-project-lite", 1176 | "sync_wrapper", 1177 | "tokio", 1178 | "tower-layer", 1179 | "tower-service", 1180 | "tracing", 1181 | ] 1182 | 1183 | [[package]] 1184 | name = "tower-layer" 1185 | version = "0.3.3" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1188 | 1189 | [[package]] 1190 | name = "tower-service" 1191 | version = "0.3.3" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1194 | 1195 | [[package]] 1196 | name = "tracing" 1197 | version = "0.1.41" 1198 | source = "registry+https://github.com/rust-lang/crates.io-index" 1199 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1200 | dependencies = [ 1201 | "log", 1202 | "pin-project-lite", 1203 | "tracing-core", 1204 | ] 1205 | 1206 | [[package]] 1207 | name = "tracing-core" 1208 | version = "0.1.33" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1211 | dependencies = [ 1212 | "once_cell", 1213 | ] 1214 | 1215 | [[package]] 1216 | name = "unicode-ident" 1217 | version = "1.0.14" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 1220 | 1221 | [[package]] 1222 | name = "unicode-width" 1223 | version = "0.2.0" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1226 | 1227 | [[package]] 1228 | name = "utf8parse" 1229 | version = "0.2.2" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1232 | 1233 | [[package]] 1234 | name = "version_check" 1235 | version = "0.9.5" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1238 | 1239 | [[package]] 1240 | name = "walkdir" 1241 | version = "2.5.0" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1244 | dependencies = [ 1245 | "same-file", 1246 | "winapi-util", 1247 | ] 1248 | 1249 | [[package]] 1250 | name = "wasi" 1251 | version = "0.11.0+wasi-snapshot-preview1" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1254 | 1255 | [[package]] 1256 | name = "wasm-bindgen" 1257 | version = "0.2.99" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" 1260 | dependencies = [ 1261 | "cfg-if", 1262 | "once_cell", 1263 | "wasm-bindgen-macro", 1264 | ] 1265 | 1266 | [[package]] 1267 | name = "wasm-bindgen-backend" 1268 | version = "0.2.99" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" 1271 | dependencies = [ 1272 | "bumpalo", 1273 | "log", 1274 | "proc-macro2", 1275 | "quote", 1276 | "syn", 1277 | "wasm-bindgen-shared", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "wasm-bindgen-macro" 1282 | version = "0.2.99" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" 1285 | dependencies = [ 1286 | "quote", 1287 | "wasm-bindgen-macro-support", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "wasm-bindgen-macro-support" 1292 | version = "0.2.99" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" 1295 | dependencies = [ 1296 | "proc-macro2", 1297 | "quote", 1298 | "syn", 1299 | "wasm-bindgen-backend", 1300 | "wasm-bindgen-shared", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "wasm-bindgen-shared" 1305 | version = "0.2.99" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" 1308 | 1309 | [[package]] 1310 | name = "web-time" 1311 | version = "1.1.0" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1314 | dependencies = [ 1315 | "js-sys", 1316 | "wasm-bindgen", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "winapi-util" 1321 | version = "0.1.9" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1324 | dependencies = [ 1325 | "windows-sys 0.59.0", 1326 | ] 1327 | 1328 | [[package]] 1329 | name = "windows-core" 1330 | version = "0.52.0" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1333 | dependencies = [ 1334 | "windows-targets", 1335 | ] 1336 | 1337 | [[package]] 1338 | name = "windows-sys" 1339 | version = "0.52.0" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1342 | dependencies = [ 1343 | "windows-targets", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "windows-sys" 1348 | version = "0.59.0" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1351 | dependencies = [ 1352 | "windows-targets", 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "windows-targets" 1357 | version = "0.52.6" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1360 | dependencies = [ 1361 | "windows_aarch64_gnullvm", 1362 | "windows_aarch64_msvc", 1363 | "windows_i686_gnu", 1364 | "windows_i686_gnullvm", 1365 | "windows_i686_msvc", 1366 | "windows_x86_64_gnu", 1367 | "windows_x86_64_gnullvm", 1368 | "windows_x86_64_msvc", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "windows_aarch64_gnullvm" 1373 | version = "0.52.6" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1376 | 1377 | [[package]] 1378 | name = "windows_aarch64_msvc" 1379 | version = "0.52.6" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1382 | 1383 | [[package]] 1384 | name = "windows_i686_gnu" 1385 | version = "0.52.6" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1388 | 1389 | [[package]] 1390 | name = "windows_i686_gnullvm" 1391 | version = "0.52.6" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1394 | 1395 | [[package]] 1396 | name = "windows_i686_msvc" 1397 | version = "0.52.6" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1400 | 1401 | [[package]] 1402 | name = "windows_x86_64_gnu" 1403 | version = "0.52.6" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1406 | 1407 | [[package]] 1408 | name = "windows_x86_64_gnullvm" 1409 | version = "0.52.6" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1412 | 1413 | [[package]] 1414 | name = "windows_x86_64_msvc" 1415 | version = "0.52.6" 1416 | source = "registry+https://github.com/rust-lang/crates.io-index" 1417 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1418 | 1419 | [[package]] 1420 | name = "wyz" 1421 | version = "0.5.1" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 1424 | dependencies = [ 1425 | "tap", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "xtask_kompari" 1430 | version = "0.1.0" 1431 | dependencies = [ 1432 | "clap", 1433 | "demolib", 1434 | "kompari", 1435 | "kompari-tasks", 1436 | ] 1437 | 1438 | [[package]] 1439 | name = "zopfli" 1440 | version = "0.8.1" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" 1443 | dependencies = [ 1444 | "bumpalo", 1445 | "crc32fast", 1446 | "lockfree-object-pool", 1447 | "log", 1448 | "once_cell", 1449 | "simd-adler32", 1450 | ] 1451 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["demolib", "xtask_kompari"] 4 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Kompari Xtask example 2 | 3 | This is an example that demonstrates how to integrate `kompari` as xtask into a test. 4 | 5 | * `demolib` is an example library that we are testing with snapshot tests 6 | * `xtask-kompari` is xtask that integrates kompari into the example project 7 | * `tests` is a directory with snapshots and image for the current tests 8 | 9 | ## Demo 10 | 11 | Try to modify the test in `demolib/src/lib.rs` to make it fail, 12 | for example try to modify a parameter of the tested function as it 13 | is shown in the comment of the test. 14 | 15 | Run tests via `cargo test`. The test fails because snapshot is different. 16 | 17 | Run `cargo xtask-kompari report` to generate a report 18 | with differences. It will create a `report.html`. 19 | -------------------------------------------------------------------------------- /example/demolib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "demolib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | image = { version = "0.25", default-features = false, features = ["png"] } 8 | -------------------------------------------------------------------------------- /example/demolib/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use image::{Rgb, RgbImage}; 5 | 6 | /// Create an image with rectangle 7 | pub fn create_rectangle(x1: u32, y1: u32, x2: u32, y2: u32, color: Rgb) -> RgbImage { 8 | RgbImage::from_fn(100, 100, |x, y| { 9 | if x1 <= x && x < x2 && y1 <= y && y < y2 { 10 | color 11 | } else { 12 | Rgb([255, 255, 255]) 13 | } 14 | }) 15 | } 16 | 17 | #[cfg(test)] 18 | mod test_utils; 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use super::*; 23 | use crate::test_utils::check_snapshot; 24 | 25 | #[test] 26 | fn test_create_rectangle() { 27 | // If you want to make this test fails, change something 28 | // 29 | // For example: 30 | // Change the value ----\ 31 | // here to e.g. 25 | 32 | // v 33 | let image = create_rectangle(10, 5, 50, 70, Rgb([255, 0, 0])); 34 | check_snapshot(image, "create_rectangle.png"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/demolib/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use image::RgbImage; 5 | use std::path::{Path, PathBuf}; 6 | 7 | /// Directory where current tests creates images 8 | fn current_dir() -> PathBuf { 9 | Path::new(env!("CARGO_MANIFEST_DIR")) 10 | .parent() 11 | .unwrap() 12 | .join("tests") 13 | .join("current") 14 | } 15 | 16 | /// Directory with blessed snapshots 17 | fn snapshot_dir() -> PathBuf { 18 | Path::new(env!("CARGO_MANIFEST_DIR")) 19 | .parent() 20 | .unwrap() 21 | .join("tests") 22 | .join("snapshots") 23 | } 24 | 25 | fn is_generate_all_mode() -> bool { 26 | std::env::var("DEMOLIB_TEST") 27 | .map(|x| x.to_ascii_lowercase() == "generate-all") 28 | .unwrap_or(false) 29 | } 30 | 31 | /// Check an image against snapshot 32 | pub(crate) fn check_snapshot(image: RgbImage, image_name: &str) { 33 | let snapshot_dir = snapshot_dir(); 34 | let snapshot = image::ImageReader::open(snapshot_dir.join(image_name)) 35 | .map_err(|e| e.to_string()) 36 | .and_then(|x| x.decode().map_err(|e| e.to_string())) 37 | .map(|x| x.to_rgb8()); 38 | if let Ok(snapshot) = snapshot { 39 | if snapshot != image { 40 | image.save(current_dir().join(image_name)).unwrap(); 41 | panic!("Snapshot is different; run 'cargo xtask-test report' for report") 42 | } 43 | } else { 44 | println!("{}", current_dir().join(image_name).display()); 45 | image.save(current_dir().join(image_name)).unwrap(); 46 | snapshot.unwrap(); 47 | } 48 | if is_generate_all_mode() { 49 | image.save(current_dir().join(image_name)).unwrap(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/tests/current/.gitignore: -------------------------------------------------------------------------------- 1 | *.png -------------------------------------------------------------------------------- /example/tests/snapshots/create_rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/example/tests/snapshots/create_rectangle.png -------------------------------------------------------------------------------- /example/xtask_kompari/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask_kompari" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | demolib = { path = "../demolib" } 8 | clap = { version = "4.5", features = ["derive"] } 9 | kompari = { path = "../../kompari" } 10 | kompari-tasks = { path = "../../kompari-tasks" } 11 | -------------------------------------------------------------------------------- /example/xtask_kompari/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use clap::Parser; 5 | use kompari::DirDiffConfig; 6 | use kompari_tasks::{Actions, Args, Task}; 7 | use std::path::Path; 8 | use std::process::Command; 9 | 10 | struct ActionsImpl(); 11 | 12 | impl Actions for ActionsImpl { 13 | fn generate_all_tests(&self) -> kompari::Result<()> { 14 | let cargo = std::env::var("CARGO").unwrap(); 15 | Command::new(&cargo) 16 | .arg("test") 17 | .env("DEMOLIB_TEST", "generate-all") 18 | .status()?; 19 | Ok(()) 20 | } 21 | } 22 | 23 | fn main() -> kompari::Result<()> { 24 | let tests_path = Path::new(env!("CARGO_MANIFEST_DIR")) 25 | .parent() 26 | .unwrap() 27 | .join("tests"); 28 | 29 | let snapshots_path = tests_path.join("snapshots"); 30 | let current_path = tests_path.join("current"); 31 | 32 | let args = Args::parse(); 33 | let diff_config = DirDiffConfig::new(snapshots_path, current_path); 34 | let actions = ActionsImpl(); 35 | let mut task = Task::new(diff_config, Box::new(actions)); 36 | task.run(&args)?; 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /kompari-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kompari-cli" 3 | description = "CLI for reporting tool of image differences for snapshot testing." 4 | keywords = ["image", "report", "diff", "tests"] 5 | categories = ["graphics", "multimedia::images", "development-tools::testing"] 6 | 7 | 8 | version.workspace = true 9 | edition.workspace = true 10 | rust-version.workspace = true 11 | license.workspace = true 12 | repository.workspace = true 13 | 14 | [dependencies] 15 | kompari = { path = "../kompari" } 16 | kompari-html = { path = "../kompari-html" } 17 | clap = { workspace = true } 18 | kompari-tasks = { path = "../kompari-tasks" } 19 | 20 | [dev-dependencies] 21 | assert_cmd = "2.0" 22 | tempfile = "3.15" 23 | -------------------------------------------------------------------------------- /kompari-cli/README.md: -------------------------------------------------------------------------------- 1 | # Kompari CLI 2 | 3 | Command line interface for Kompari 4 | -------------------------------------------------------------------------------- /kompari-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | // LINEBENDER LINT SET - lib.rs - v3 5 | // See https://linebender.org/wiki/canonical-lints/ 6 | // These lints shouldn't apply to examples or tests. 7 | #![cfg_attr(not(test), warn(unused_crate_dependencies))] 8 | // These lints shouldn't apply to examples. 9 | // #![warn(clippy::print_stdout, clippy::print_stderr)] 10 | // Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit. 11 | #![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))] 12 | // END LINEBENDER LINT SET 13 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 14 | 15 | use clap::Parser; 16 | use kompari::DirDiffConfig; 17 | use kompari_html::{render_html_report, start_review_server, ReportConfig}; 18 | use kompari_tasks::check_size_optimizations; 19 | use std::path::PathBuf; 20 | 21 | #[derive(Parser, Debug)] 22 | pub struct CliReportArgs { 23 | #[clap(flatten)] 24 | diff_args: DiffArgs, 25 | #[clap(flatten)] 26 | args: kompari_tasks::args::ReportArgs, 27 | } 28 | 29 | #[derive(Parser, Debug)] 30 | pub struct CliReviewArgs { 31 | #[clap(flatten)] 32 | diff_args: DiffArgs, 33 | #[clap(flatten)] 34 | args: kompari_tasks::args::ReviewArgs, 35 | } 36 | 37 | #[derive(Parser, Debug, Clone)] 38 | struct DiffArgs { 39 | /// Path to "left" images 40 | left_path: PathBuf, 41 | 42 | /// Path to "right" images 43 | right_path: PathBuf, 44 | 45 | /// Left title 46 | #[arg(long, default_value = "Left image")] 47 | left_title: String, 48 | 49 | /// Right title 50 | #[arg(long, default_value = "Right image")] 51 | right_title: String, 52 | 53 | /// Ignore left missing files 54 | #[arg(long, default_value_t = false)] 55 | ignore_left_missing: bool, 56 | 57 | /// Ignore right missing files 58 | #[arg(long, default_value_t = false)] 59 | ignore_right_missing: bool, 60 | 61 | /// Ignore match 62 | #[arg(long, default_value_t = false)] 63 | ignore_match: bool, 64 | 65 | /// Filter filenames by name 66 | #[arg(long)] 67 | filter: Option, 68 | } 69 | 70 | #[derive(Parser, Debug)] 71 | pub struct CliSizeCheckArgs { 72 | path: PathBuf, 73 | 74 | #[clap(flatten)] 75 | args: kompari_tasks::args::SizeCheckArgs, 76 | } 77 | 78 | #[derive(Parser, Debug)] 79 | #[command(version, about, long_about = None)] 80 | pub enum Args { 81 | Report(CliReportArgs), 82 | Review(CliReviewArgs), 83 | SizeCheck(CliSizeCheckArgs), 84 | } 85 | 86 | fn make_diff_config(args: DiffArgs) -> (DirDiffConfig, ReportConfig) { 87 | let mut diff_config = DirDiffConfig::new(args.left_path, args.right_path); 88 | diff_config.set_ignore_left_missing(args.ignore_left_missing); 89 | diff_config.set_ignore_right_missing(args.ignore_right_missing); 90 | diff_config.set_filter_name(args.filter); 91 | 92 | let mut report_config = ReportConfig::default(); 93 | report_config.set_left_title(args.left_title); 94 | report_config.set_right_title(args.right_title); 95 | 96 | (diff_config, report_config) 97 | } 98 | 99 | fn main() -> kompari::Result<()> { 100 | let args = Args::parse(); 101 | 102 | match args { 103 | Args::Report(args) => { 104 | let (diff_config, mut report_config) = make_diff_config(args.diff_args); 105 | let diff = diff_config.create_diff()?; 106 | report_config.set_embed_images(args.args.embed_images); 107 | report_config.set_size_optimization(args.args.optimize_size.to_level()); 108 | let report = render_html_report(&report_config, diff.results())?; 109 | let output = args.args.output.unwrap_or("report.html".into()); 110 | std::fs::write(&output, report)?; 111 | println!("Report written into '{}'", output.display()); 112 | } 113 | Args::Review(args) => { 114 | let (diff_config, mut report_config) = make_diff_config(args.diff_args); 115 | report_config.set_size_optimization(args.args.optimize_size.to_level()); 116 | start_review_server(&diff_config, &report_config, args.args.port)? 117 | } 118 | Args::SizeCheck(args) => { 119 | check_size_optimizations(&args.path, &args.args)?; 120 | } 121 | } 122 | Ok(()) 123 | } 124 | -------------------------------------------------------------------------------- /kompari-cli/tests/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use assert_cmd::Command; 5 | use std::path::{Path, PathBuf}; 6 | use tempfile::TempDir; 7 | 8 | fn test_assets_dir() -> PathBuf { 9 | Path::new(env!("CARGO_MANIFEST_DIR")) 10 | .parent() 11 | .unwrap() 12 | .join("tests") 13 | } 14 | 15 | #[test] 16 | fn test_create_report() { 17 | let workdir = TempDir::new().unwrap(); 18 | std::env::set_current_dir(workdir.path()).unwrap(); 19 | 20 | let test_dir = test_assets_dir(); 21 | let left = test_dir.join("left"); 22 | let right = test_dir.join("right"); 23 | let mut cmd = Command::cargo_bin("kompari-cli").unwrap(); 24 | cmd.arg("report").arg(&left).arg(&right); 25 | cmd.current_dir(&workdir); 26 | cmd.assert().success(); 27 | let result: Vec<_> = std::fs::read_dir(&workdir) 28 | .unwrap() 29 | .map(|e| e.unwrap().file_name().to_str().unwrap().to_owned()) 30 | .collect(); 31 | assert_eq!(result, vec!["report.html"]); 32 | let report = std::fs::read_to_string(workdir.path().join("report.html")).unwrap(); 33 | for name in [ 34 | "bright", 35 | "changetext", 36 | "right_missing", 37 | "left_missing", 38 | "shift", 39 | "size_error", 40 | ] { 41 | assert!(report.contains(&format!("{}.png", name))); 42 | } 43 | assert!(!report.contains("same.png")); 44 | } 45 | 46 | #[test] 47 | fn test_filter_filenames() { 48 | let workdir = TempDir::new().unwrap(); 49 | std::env::set_current_dir(workdir.path()).unwrap(); 50 | 51 | let test_dir = test_assets_dir(); 52 | let left = test_dir.join("left"); 53 | let right = test_dir.join("right"); 54 | let mut cmd = Command::cargo_bin("kompari-cli").unwrap(); 55 | cmd.arg("report").arg("--filter").arg("change"); 56 | cmd.arg(&left).arg(&right); 57 | cmd.current_dir(&workdir); 58 | cmd.assert().success(); 59 | let report = std::fs::read_to_string(workdir.path().join("report.html")).unwrap(); 60 | assert!(report.contains("changetext.png")); 61 | for name in [ 62 | "bright", 63 | "right_missing", 64 | "left_missing", 65 | "shift", 66 | "size_error", 67 | ] { 68 | assert!(!report.contains(&format!("{}.png", name))); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /kompari-html/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kompari-html" 3 | 4 | description = "HTML reports for image differences for snapshot testing." 5 | keywords = ["image", "report", "diff", "tests"] 6 | categories = ["graphics", "multimedia::images", "development-tools::testing"] 7 | 8 | 9 | version.workspace = true 10 | edition.workspace = true 11 | rust-version.workspace = true 12 | license.workspace = true 13 | repository.workspace = true 14 | 15 | [dependencies] 16 | kompari = { path = "../kompari", features = ["oxipng"] } 17 | rayon = { workspace = true } 18 | base64 = "0.22" 19 | chrono = "0.4" 20 | maud = "0.27" 21 | imagesize = "0.13" 22 | serde = { version = "1.0.217", features = ["derive"] } 23 | tokio = "1.43" 24 | axum = "0.8" 25 | -------------------------------------------------------------------------------- /kompari-html/README.md: -------------------------------------------------------------------------------- 1 | # Kompari HTML 2 | 3 | Generates a HTML report from a failing set of snapshot tests. 4 | -------------------------------------------------------------------------------- /kompari-html/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | // LINEBENDER LINT SET - lib.rs - v3 5 | // See https://linebender.org/wiki/canonical-lints/ 6 | // These lints shouldn't apply to examples or tests. 7 | #![cfg_attr(not(test), warn(unused_crate_dependencies))] 8 | // These lints shouldn't apply to examples. 9 | // #![warn(clippy::print_stdout, clippy::print_stderr)] 10 | // Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit. 11 | #![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))] 12 | // END LINEBENDER LINT SET 13 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 14 | 15 | mod pageconsts; 16 | mod report; 17 | mod review; 18 | 19 | #[derive(Debug, Clone)] 20 | pub struct ReportConfig { 21 | left_title: String, 22 | right_title: String, 23 | embed_images: bool, 24 | is_review: bool, 25 | size_optimization: SizeOptimizationLevel, 26 | } 27 | 28 | impl Default for ReportConfig { 29 | fn default() -> Self { 30 | ReportConfig { 31 | left_title: "Left image".to_string(), 32 | right_title: "Right image".to_string(), 33 | embed_images: false, 34 | is_review: false, 35 | size_optimization: SizeOptimizationLevel::None, 36 | } 37 | } 38 | } 39 | 40 | impl ReportConfig { 41 | pub fn set_left_title(&mut self, value: impl ToString) { 42 | self.left_title = value.to_string() 43 | } 44 | pub fn set_right_title(&mut self, value: impl ToString) { 45 | self.right_title = value.to_string() 46 | } 47 | pub fn set_embed_images(&mut self, value: bool) { 48 | self.embed_images = value 49 | } 50 | pub fn set_size_optimization(&mut self, value: SizeOptimizationLevel) { 51 | self.size_optimization = value 52 | } 53 | pub fn set_review(&mut self, value: bool) { 54 | self.is_review = value 55 | } 56 | } 57 | 58 | use kompari::SizeOptimizationLevel; 59 | pub use report::render_html_report; 60 | pub use review::start_review_server; 61 | -------------------------------------------------------------------------------- /kompari-html/src/pageconsts.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | pub(crate) const ICON: &[u8] = include_bytes!("../../docs/logo_small.png"); 5 | 6 | pub(crate) const CSS_STYLE: &str = " 7 | body { 8 | font-family: Roboto, sans-serif; 9 | margin: 0; 10 | padding: 20px; 11 | background: #f5f5f5; 12 | color: #333; 13 | } 14 | 15 | .header { 16 | background: #fff; 17 | padding: 20px; 18 | border-radius: 8px; 19 | margin-bottom: 20px; 20 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 21 | } 22 | 23 | .logo { 24 | vertical-align: -10%; 25 | } 26 | 27 | .header h1 { 28 | margin: 0; 29 | color: #2d3748; 30 | } 31 | 32 | .summary { 33 | margin-bottom: 20px; 34 | padding: 15px; 35 | background: #fff; 36 | border-radius: 8px; 37 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 38 | } 39 | 40 | .diff-entry { 41 | background: #fff; 42 | margin-bottom: 30px; 43 | padding: 20px; 44 | border-radius: 8px; 45 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 46 | } 47 | 48 | .diff-entry h2 { 49 | margin-top: 0; 50 | color: #2d3748; 51 | border-bottom: 2px solid #edf2f7; 52 | padding-bottom: 10px; 53 | } 54 | 55 | .comparison-container { 56 | display: flex; 57 | gap: 20px; 58 | margin-top: 15px; 59 | } 60 | 61 | .image-container { 62 | display: flex; 63 | gap: 20px; 64 | flex-wrap: wrap; 65 | flex: 1; 66 | } 67 | 68 | .image-box { 69 | flex: 1; 70 | min-width: 250px; 71 | max-width: 400px; 72 | } 73 | 74 | .image-box h3 { 75 | margin: 0 0 10px 0; 76 | color: #4a5568; 77 | font-size: 1rem; 78 | } 79 | 80 | .image-box img { 81 | max-width: 100%; 82 | border: 1px solid #e2e8f0; 83 | border-radius: 4px; 84 | } 85 | 86 | .stats-container { 87 | width: 200px; 88 | flex-shrink: 0; 89 | background: #f8fafc; 90 | padding: 15px; 91 | border-radius: 6px; 92 | border: 1px solid #e2e8f0; 93 | } 94 | 95 | .stat-item { 96 | margin-bottom: 15px; 97 | } 98 | 99 | .stat-label { 100 | font-size: 0.875rem; 101 | color: #64748b; 102 | margin-bottom: 4px; 103 | } 104 | 105 | .stat-value { 106 | font-size: 1.25rem; 107 | font-weight: 600; 108 | color: #2d3748; 109 | } 110 | 111 | .stat-value.ok { 112 | color: #77d906; 113 | } 114 | 115 | .stat-value.warning { 116 | color: #d97706; 117 | } 118 | 119 | .stat-value.error { 120 | color: #dc2626; 121 | } 122 | 123 | @media (max-width: 1200px) { 124 | .comparison-container { 125 | flex-direction: column-reverse; 126 | } 127 | 128 | .stats-container { 129 | width: auto; 130 | display: flex; 131 | flex-wrap: wrap; 132 | gap: 20px; 133 | } 134 | 135 | .stat-item { 136 | flex: 1; 137 | min-width: 150px; 138 | margin-bottom: 0; 139 | } 140 | } 141 | 142 | @media (max-width: 768px) { 143 | .image-box { 144 | min-width: 100%; 145 | } 146 | } 147 | 148 | img.zoom:hover { 149 | cursor: pointer; 150 | transform: scale(1.05); 151 | } 152 | 153 | dialog { 154 | width: 80%; 155 | height: 80%; 156 | max-width: 800px; 157 | max-height: 820px; 158 | padding: 0; 159 | border: none; 160 | border-radius: 10px; 161 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.3); 162 | } 163 | 164 | .zoomed-image { 165 | object-fit: contain; 166 | } 167 | 168 | .zoomed-image-small { 169 | image-rendering: pixelated; 170 | } 171 | 172 | .toggle-switch { 173 | position: relative; 174 | display: inline-block; 175 | width: 60px; 176 | height: 30px; 177 | margin-right: 1em; 178 | } 179 | .toggle-switch input { 180 | opacity: 0; 181 | width: 0; 182 | height: 0; 183 | } 184 | .slider { 185 | position: absolute; 186 | cursor: pointer; 187 | top: 0; 188 | left: 0; 189 | right: 0; 190 | bottom: 0; 191 | background-color: #ccc; 192 | transition: .1s; 193 | border-radius: 34px; 194 | } 195 | .slider:before { 196 | position: absolute; 197 | content: \"\"; 198 | height: 22px; 199 | width: 22px; 200 | left: 4px; 201 | bottom: 4px; 202 | background-color: white; 203 | transition: .1s; 204 | border-radius: 50%; 205 | } 206 | input:checked + .slider { 207 | background-color: #3c3; 208 | } 209 | input:checked + .slider:before { 210 | transform: translateX(30px); 211 | } 212 | 213 | .accept-button { 214 | padding: 12px 24px; 215 | margin-bottom: 1em; 216 | font-size: 16px; 217 | font-weight: 500; 218 | color: white; 219 | background-color: #4CAF50; 220 | border: none; 221 | border-radius: 6px; 222 | cursor: pointer; 223 | transition: all 0.3s ease; 224 | display: flex; 225 | align-items: center; 226 | gap: 8px; 227 | } 228 | .accept-button:hover { 229 | background-color: #45A049; 230 | transform: translateY(-1px); 231 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 232 | } 233 | .accept-button:active { 234 | transform: translateY(0px); 235 | box-shadow: none; 236 | } 237 | .accept-button:disabled { 238 | background-color: #CCCCCC; 239 | cursor: not-allowed; 240 | transform: none; 241 | } 242 | #errorMsg { 243 | background-color: #fef2f2; 244 | border: 1px solid #f87171; 245 | border-radius: 6px; 246 | padding: 16px; 247 | margin: 12px 0; 248 | display: none; 249 | align-items: flex-start; 250 | gap: 12px; 251 | max-width: 600px; 252 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 253 | } 254 | .tabs { 255 | margin-top: 8px; 256 | display: flex; 257 | } 258 | .tab { 259 | margin-left: 10px; 260 | margin-right: 10px; 261 | background: none; 262 | cursor: pointer; 263 | font-size: 16px; 264 | color: #666; 265 | } 266 | .tab.active { 267 | color: #444; 268 | border-bottom: 2px solid #444; 269 | } 270 | 271 | .hint { 272 | color: #666; 273 | font-size: 80%; 274 | } 275 | "; 276 | 277 | pub(crate) const JS_CODE: &str = " 278 | function openImageDialog(img, pixelize) { 279 | const dialog = document.getElementById('imageDialog'); 280 | const zoomedImg = document.getElementById('zoomedImage'); 281 | 282 | if (pixelize) { 283 | zoomedImg.classList.add(\"zoomed-image-small\"); 284 | } else { 285 | zoomedImg.classList.remove(\"zoomed-image-small\"); 286 | } 287 | 288 | zoomedImg.src = img.src; 289 | if (img.width < img.height) { 290 | zoomedImg.style.width = \"100%\"; 291 | zoomedImg.style.height = \"auto\"; 292 | } else { 293 | zoomedImg.style.width = \"auto\"; 294 | zoomedImg.style.height = \"100%\"; 295 | } 296 | dialog.showModal(); 297 | } 298 | 299 | function closeImageDialog() { 300 | const dialog = document.getElementById('imageDialog'); 301 | dialog.close(); 302 | } 303 | 304 | document.getElementById('imageDialog').addEventListener('click', function(event) { 305 | closeImageDialog(); 306 | }); 307 | 308 | var selected = new Set(); 309 | function toggle(event) { 310 | let node = event.target.parentNode.parentNode; 311 | let name = node.childNodes[2].textContent; 312 | if (event.target.checked) { 313 | selected.add(name); 314 | node.style.color = \"#3a3\"; 315 | } else { 316 | selected.delete(name); 317 | node.style.color = \"#333\"; 318 | } 319 | updateAcceptButton() 320 | } 321 | 322 | function updateAcceptButton() { 323 | let text = document.getElementById('acceptText'); 324 | text.textContent = \"Accept selected cases (\" + selected.size + \" / \" + nTests + \")\"; 325 | let button = document.getElementById('acceptButton'); 326 | button.disabled = (selected.size === 0); 327 | } 328 | 329 | function switchDiffTab(id, selected, n) { 330 | for (let idx = 0; idx < n; idx++) { 331 | document.getElementById(`tab-diff-${id}-${idx}`).classList.remove('active'); 332 | document.getElementById(`img-diff-${id}-${idx}`).style.display = 'none'; 333 | } 334 | document.getElementById(`tab-diff-${id}-${selected}`).classList.add('active'); 335 | document.getElementById(`img-diff-${id}-${selected}`).style.display = 'inline'; 336 | } 337 | 338 | async function acceptTests() { 339 | let text = document.getElementById('acceptText'); 340 | text.textContent = \"Updating \" + selected.size + \" cases ...\"; 341 | let button = document.getElementById('acceptButton'); 342 | button.disabled = true; 343 | 344 | try { 345 | const url = '/update'; 346 | const response = await fetch(url, { 347 | method: 'POST', 348 | headers: { 349 | \"Content-Type\": \"application/json\", 350 | }, 351 | body: JSON.stringify({ accepted_names: Array.from(selected) }) 352 | }); 353 | if (!response.ok) { 354 | throw new Error(`Response status: ${response.status}`); 355 | } else { 356 | for (i = 0; i < nTests; i++) { 357 | document.getElementById(\"t\" + i).checked = false; 358 | } 359 | location.reload(); 360 | } 361 | } catch (e) { 362 | let error = document.getElementById('errorMsg'); 363 | error.textContent = e.message; 364 | error.style.display = \"flex\"; 365 | text.textContent = \"Try update again\"; 366 | button.disabled = false; 367 | } 368 | } 369 | "; 370 | -------------------------------------------------------------------------------- /kompari-html/src/report.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::pageconsts::{CSS_STYLE, ICON, JS_CODE}; 5 | use crate::ReportConfig; 6 | use base64::prelude::*; 7 | use chrono::SubsecRound; 8 | use kompari::{ImageDifference, LeftRightError, PairResult}; 9 | use maud::{html, Markup, PreEscaped, DOCTYPE}; 10 | use rayon::iter::IndexedParallelIterator; 11 | use rayon::iter::IntoParallelRefIterator; 12 | use rayon::iter::ParallelIterator; 13 | use std::cmp::min; 14 | use std::path::Path; 15 | 16 | const IMAGE_SIZE_LIMIT: u32 = 400; 17 | const IMAGE_PIXELIZE_LIMIT: u32 = 400; 18 | 19 | fn embed_png_url(data: &[u8]) -> String { 20 | let mut url = "data:image/png;base64,".to_string(); 21 | url.push_str(&base64::engine::general_purpose::STANDARD.encode(data)); 22 | url 23 | } 24 | 25 | fn render_image( 26 | config: &ReportConfig, 27 | path: &Path, 28 | error: Option<&kompari::Error>, 29 | ) -> kompari::Result { 30 | Ok(match error { 31 | None => { 32 | let (path, size) = if config.embed_images { 33 | let image_data = 34 | kompari::optimize_png(std::fs::read(path)?, config.size_optimization); 35 | ( 36 | embed_png_url(&image_data), 37 | imagesize::blob_size(&image_data) 38 | .map_err(|e| kompari::Error::GenericError(e.to_string()))?, 39 | ) 40 | } else { 41 | ( 42 | path.display().to_string(), 43 | imagesize::size(path) 44 | .map_err(|e| kompari::Error::GenericError(e.to_string()))?, 45 | ) 46 | }; 47 | let (w, h) = html_size(size.width as u32, size.height as u32, IMAGE_SIZE_LIMIT); 48 | html! { 49 | img class="zoom" src=(path) 50 | width=[w] height=[h] 51 | onclick=(open_image_dialog(size.width as u32, size.height as u32)); 52 | } 53 | } 54 | Some(kompari::Error::FileNotFound(_)) => { 55 | html! { "File is missing" } 56 | } 57 | Some(err) => { 58 | html! { "Error: " (err) } 59 | } 60 | }) 61 | } 62 | 63 | pub fn html_size(width: u32, height: u32, size_limit: u32) -> (Option, Option) { 64 | if width > height { 65 | (Some(width.min(size_limit)), None) 66 | } else { 67 | (None, Some(height.min(size_limit))) 68 | } 69 | } 70 | 71 | fn open_image_dialog(width: u32, height: u32) -> &'static str { 72 | if min(width, height) < IMAGE_PIXELIZE_LIMIT { 73 | "openImageDialog(this, true)" 74 | } else { 75 | "openImageDialog(this, false)" 76 | } 77 | } 78 | 79 | fn render_difference_image( 80 | config: &ReportConfig, 81 | id: usize, 82 | difference: &Result, 83 | ) -> Markup { 84 | match difference { 85 | Ok(ImageDifference::Content { diff_images, .. }) => { 86 | html! { 87 | @for (idx, di) in diff_images.iter().enumerate() { 88 | @let (w, h, data) = { 89 | let (w, h) = html_size( 90 | di.image.width(), 91 | di.image.height(), 92 | IMAGE_SIZE_LIMIT, 93 | ); 94 | let data = kompari::image_to_png(&di.image, config.size_optimization); 95 | (w, h, data) 96 | }; 97 | @let style = if idx == 0 { None } else { Some("display: none") }; 98 | img id=(format!("img-diff-{id}-{idx}")) 99 | style=[style] 100 | class="zoom" 101 | src=(embed_png_url(&data)) 102 | width=[w] height=[h] 103 | onclick=(open_image_dialog(di.image.width(), di.image.height())); 104 | } 105 | div class="tabs" { 106 | @for (idx, img) in diff_images.iter().enumerate() { 107 | @let class = if idx == 0 { "tab active" } else { "tab" }; 108 | div id=(format!("tab-diff-{id}-{idx}")) class=(class) {(img.method.to_string())}; 109 | } 110 | } 111 | script { 112 | @for idx in 0..diff_images.len() { 113 | (PreEscaped(format!("document.getElementById('tab-diff-{id}-{idx}').addEventListener('click', () => switchDiffTab({id}, {idx}, {}));", diff_images.len()))) 114 | } 115 | } 116 | } 117 | } 118 | _ => html!("N/A"), 119 | } 120 | } 121 | 122 | fn render_stat_item(label: &str, value_type: &str, value: &str) -> Markup { 123 | html! { 124 | div .stat-item { 125 | div .stat-label { 126 | (label) 127 | } 128 | @let value_class = format!("stat-value {}", value_type); 129 | div class=(value_class) { 130 | (value) 131 | } 132 | } 133 | } 134 | } 135 | 136 | fn render_difference_info( 137 | config: &ReportConfig, 138 | difference: &Result, 139 | ) -> Markup { 140 | match difference { 141 | Ok(ImageDifference::None) => render_stat_item("Status", "ok", "Match"), 142 | Ok(ImageDifference::SizeMismatch { 143 | left_size, 144 | right_size, 145 | }) => html! { 146 | (render_stat_item("Status", "error", "Size mismatch")) 147 | (render_stat_item(&format!("{} size", config.left_title), "", &format!("{}x{}", left_size.0, left_size.1))) 148 | (render_stat_item(&format!("{} size", config.right_title), "", &format!("{}x{}", right_size.0, right_size.1))) 149 | }, 150 | Ok(ImageDifference::Content { 151 | n_pixels, 152 | n_different_pixels, 153 | distance_sum, 154 | .. 155 | }) => { 156 | let n_pixels = (*n_pixels) as f32; 157 | let pct = *n_different_pixels as f32 / n_pixels * 100.0; 158 | let distance_sum = *distance_sum as f32 / 255.0; // Normalize 159 | let avg_color_distance = distance_sum / n_pixels; 160 | html! { 161 | (render_stat_item("Different pixels", "warning", &format!("{n_different_pixels} ({pct:.1}%)"))) 162 | (render_stat_item("Color distance", "", &format!("{distance_sum:.3}"))) 163 | (render_stat_item("Avg. color distance", "", &format!("{avg_color_distance:.4}"))) 164 | } 165 | } 166 | Err(e) if e.is_missing_file_error() => render_stat_item("Status", "error", "Missing file"), 167 | Err(_) => render_stat_item("Status", "error", "Loading error"), 168 | } 169 | } 170 | 171 | fn render_pair_diff( 172 | config: &ReportConfig, 173 | id: usize, 174 | pair_diff: &PairResult, 175 | ) -> kompari::Result { 176 | Ok(html! { 177 | div class="diff-entry" { 178 | h2 { 179 | @if config.is_review { 180 | label class="toggle-switch" { 181 | input type="checkbox" id=(format!("t{id}")); 182 | span class="slider"; 183 | } 184 | script { 185 | (format!("document.getElementById('t{id}').addEventListener('change', toggle)")) 186 | } 187 | } 188 | (pair_diff.title)}; 189 | div class="comparison-container" { 190 | div class="image-container" { 191 | div class="stats-container" { 192 | (render_difference_info(config, &pair_diff.image_diff)) 193 | } 194 | div class="image-box" { 195 | h3 { (config.left_title) } 196 | (render_image(config, &pair_diff.left, if let Err(e) = &pair_diff.image_diff { e.left() } else { None })?) 197 | } 198 | div class="image-box" { 199 | h3 { (config.right_title) } 200 | (render_image(config, &pair_diff.right, if let Err(e) = &pair_diff.image_diff { e.right() } else { None })?) 201 | } 202 | div class="image-box" { 203 | h3 { "Difference"} 204 | (render_difference_image(config, id, &pair_diff.image_diff)) 205 | } 206 | } 207 | } 208 | } 209 | }) 210 | } 211 | 212 | pub fn render_html_report(config: &ReportConfig, diffs: &[PairResult]) -> kompari::Result { 213 | let now = chrono::Local::now().round_subsecs(0); 214 | let rendered_diffs: Vec = diffs 215 | .par_iter() 216 | .enumerate() 217 | .map(|(id, pair_diff)| render_pair_diff(config, id, pair_diff)) 218 | .collect::>>()?; 219 | let title = PreEscaped(if config.is_review { 220 | "Kompari review" 221 | } else { 222 | "Kompari report" 223 | }); 224 | let report = html! { 225 | (DOCTYPE) 226 | html { 227 | head { 228 | meta charset="utf-8"; 229 | meta name="viewport" content="width=device-width, initial-scale=1.0"; 230 | meta name="generator" content=(format!("Kompari {}", env!("CARGO_PKG_VERSION"))); 231 | title { (title) } 232 | style { (PreEscaped(CSS_STYLE)) } 233 | link rel="icon" type="image/png" href=(embed_png_url(&ICON)); 234 | } 235 | body { 236 | div class="header" { 237 | h1 { img class="logo" src=(embed_png_url(ICON)) width="32" height="32"; (title) } 238 | p { "Generated on " (now) } 239 | } 240 | dialog id="imageDialog" { 241 | img id="zoomedImage" class="zoomed-image" src="" alt="Zoomed Image"; 242 | } 243 | @if config.is_review { 244 | script { (format!("const nTests = {};", diffs.len())) } 245 | button class="accept-button" id="acceptButton" disabled onClick="acceptTests()" { 246 | span class="button-text" id="acceptText" { (format!("Accept selected cases (0 / {})", diffs.len())) } 247 | } 248 | span class="hint" { "Accepting a case copies '" (config.right_title) "' to '" (config.left_title) "'" } 249 | span id="errorMsg" {}; 250 | } 251 | script { (PreEscaped(JS_CODE)) } 252 | @for chunk in rendered_diffs { 253 | (chunk) 254 | } 255 | } 256 | } 257 | }; 258 | Ok(report.into_string()) 259 | } 260 | -------------------------------------------------------------------------------- /kompari-html/src/review.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::{render_html_report, ReportConfig}; 5 | use axum::extract::State; 6 | use axum::http::StatusCode; 7 | use axum::response::{Html, IntoResponse}; 8 | use axum::routing::post; 9 | use axum::{routing::get, Json, Router}; 10 | use kompari::{bless_image, DirDiffConfig}; 11 | use serde::Deserialize; 12 | use std::path::PathBuf; 13 | use std::sync::Arc; 14 | 15 | struct AppState { 16 | report_config: ReportConfig, 17 | diff_builder: DirDiffConfig, 18 | } 19 | 20 | pub fn start_review_server( 21 | diff_builder: &DirDiffConfig, 22 | report_config: &ReportConfig, 23 | port: u16, 24 | ) -> kompari::Result<()> { 25 | let mut report_config = report_config.clone(); 26 | report_config.set_review(true); 27 | report_config.set_embed_images(true); 28 | let shared_state = Arc::new(AppState { 29 | report_config, 30 | diff_builder: diff_builder.clone(), 31 | }); 32 | println!("Running at http://localhost:{port}"); 33 | tokio::runtime::Builder::new_current_thread() 34 | .enable_all() 35 | .build()? 36 | .block_on(async { 37 | let app = Router::new() 38 | .route("/", get(index)) 39 | .route("/update", post(update)) 40 | .with_state(shared_state); 41 | let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")) 42 | .await 43 | .unwrap(); 44 | axum::serve(listener, app).await.unwrap(); 45 | }); 46 | Ok(()) 47 | } 48 | 49 | fn result_to_response(result: kompari::Result) -> (StatusCode, Html) { 50 | match result { 51 | Ok(s) => (StatusCode::OK, Html::from(s)), 52 | Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())), 53 | } 54 | } 55 | 56 | async fn index(State(state): State>) -> impl IntoResponse { 57 | result_to_response((|| { 58 | let diff = state.diff_builder.create_diff()?; 59 | render_html_report(&state.report_config, diff.results()) 60 | })()) 61 | } 62 | 63 | #[derive(Deserialize, Debug)] 64 | struct UpdateParams { 65 | accepted_names: Vec, 66 | } 67 | 68 | async fn update( 69 | State(state): State>, 70 | Json(params): Json, 71 | ) -> StatusCode { 72 | let paths: Vec<_> = params 73 | .accepted_names 74 | .into_iter() 75 | .map(PathBuf::from) 76 | .collect(); 77 | if paths.iter().any(|p| !p.is_relative()) { 78 | return StatusCode::BAD_REQUEST; 79 | } 80 | for path in paths { 81 | let left = state.diff_builder.left_path().join(&path); 82 | let right = state.diff_builder.right_path().join(&path); 83 | println!("Updating {} -> {}", right.display(), left.display()); 84 | if let Err(e) = bless_image(&right, &left) { 85 | eprintln!("Failed to rename {}: {}", right.display(), e); 86 | return StatusCode::INTERNAL_SERVER_ERROR; 87 | } 88 | } 89 | StatusCode::OK 90 | } 91 | -------------------------------------------------------------------------------- /kompari-tasks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kompari-tasks" 3 | 4 | description = "Supportive code for Xtasks based on Kompari" 5 | keywords = ["image", "report", "diff", "tests"] 6 | categories = ["graphics", "multimedia::images", "development-tools::testing"] 7 | 8 | 9 | version.workspace = true 10 | edition.workspace = true 11 | rust-version.workspace = true 12 | license.workspace = true 13 | repository.workspace = true 14 | 15 | [dependencies] 16 | kompari = { path = "../kompari" } 17 | kompari-html = { path = "../kompari-html" } 18 | clap = { workspace = true } 19 | rayon = { workspace = true } 20 | termcolor = "1.4" 21 | humansize = "2.1" 22 | indicatif = "0.17" 23 | -------------------------------------------------------------------------------- /kompari-tasks/README.md: -------------------------------------------------------------------------------- 1 | # Kompari Tasks 2 | 3 | Supportive code for Xtasks based on Kompari 4 | -------------------------------------------------------------------------------- /kompari-tasks/src/args.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use clap::{Parser, ValueEnum}; 5 | use kompari::SizeOptimizationLevel; 6 | use std::path::PathBuf; 7 | 8 | #[derive(Parser, Debug)] 9 | #[command(version, about, long_about = None)] 10 | pub struct Args { 11 | #[clap(subcommand)] 12 | pub command: Command, 13 | } 14 | 15 | #[derive(Parser, Debug)] 16 | pub enum Command { 17 | Report(ReportArgs), 18 | Review(ReviewArgs), 19 | Clean, 20 | DeadSnapshots(DeadSnapshotArgs), 21 | SizeCheck(SizeCheckArgs), 22 | } 23 | 24 | #[derive(ValueEnum, Debug, Clone, Copy)] 25 | #[clap(rename_all = "lowercase")] 26 | pub enum SizeOptimization { 27 | None, 28 | Fast, 29 | High, 30 | } 31 | 32 | impl SizeOptimization { 33 | pub fn to_level(&self) -> SizeOptimizationLevel { 34 | match self { 35 | SizeOptimization::None => SizeOptimizationLevel::None, 36 | SizeOptimization::Fast => SizeOptimizationLevel::Fast, 37 | SizeOptimization::High => SizeOptimizationLevel::High, 38 | } 39 | } 40 | } 41 | 42 | #[derive(Parser, Debug)] 43 | pub struct ReportArgs { 44 | /// Output filename 45 | #[arg(long)] 46 | pub output: Option, 47 | 48 | /// Embed images into the report 49 | #[arg(long, default_value_t = false)] 50 | pub embed_images: bool, 51 | 52 | /// Optimize image sizes in HTML report 53 | #[arg(long, default_value = "none")] 54 | pub optimize_size: SizeOptimization, 55 | } 56 | 57 | #[derive(Parser, Debug)] 58 | pub struct ReviewArgs { 59 | /// Port for web server 60 | #[arg(long, default_value_t = 7200)] 61 | pub port: u16, 62 | 63 | /// Optimize image sizes in generated HTML 64 | #[arg(long, default_value = "none")] 65 | pub optimize_size: SizeOptimization, 66 | } 67 | 68 | #[derive(Parser, Debug)] 69 | pub struct DeadSnapshotArgs { 70 | #[arg(long, default_value_t = false)] 71 | pub remove_files: bool, 72 | } 73 | 74 | #[derive(Parser, Debug)] 75 | pub struct SizeCheckArgs { 76 | /// If enabled, images on file system are replaced with optimized version 77 | #[arg(long, default_value_t = false)] 78 | pub optimize: bool, 79 | 80 | /// Command will fail if at least one image can be optimized by more than given ratio. 81 | /// E.g. --improvement-limit=0.8 means that error is signaled when an image can be optimized 82 | /// more than 80% of its original size 83 | /// (that is, the optimized image's size is 20% or less of the original). 84 | #[arg(long)] 85 | pub improvement_limit: Option, 86 | 87 | /// Command will fail if at least one image has a size larger than the given limit (in KiB). 88 | /// If --optimize is used then limit is computed from target size, otherwise the limit is applied 89 | /// on the original size 90 | #[arg(long)] 91 | pub size_limit: Option, 92 | } 93 | -------------------------------------------------------------------------------- /kompari-tasks/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | // LINEBENDER LINT SET - lib.rs - v3 5 | // See https://linebender.org/wiki/canonical-lints/ 6 | // These lints shouldn't apply to examples or tests. 7 | #![cfg_attr(not(test), warn(unused_crate_dependencies))] 8 | // These lints shouldn't apply to examples. 9 | // #![warn(clippy::print_stdout, clippy::print_stderr)] 10 | // Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit. 11 | #![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))] 12 | // END LINEBENDER LINT SET 13 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 14 | 15 | pub mod args; 16 | mod optimizations; 17 | mod task; 18 | 19 | pub use args::Args; 20 | pub use optimizations::{check_size_optimizations, OptimizationResult}; 21 | pub use task::{Actions, Task}; 22 | -------------------------------------------------------------------------------- /kompari-tasks/src/optimizations.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::args::SizeCheckArgs; 5 | use humansize::{format_size, DECIMAL}; 6 | use kompari::{list_image_dir, optimize_png, SizeOptimizationLevel}; 7 | use rayon::iter::{IntoParallelIterator, ParallelIterator}; 8 | use std::cmp::min; 9 | use std::io::Write; 10 | use std::path::{Path, PathBuf}; 11 | use termcolor::{Color, ColorSpec, WriteColor}; 12 | 13 | #[derive(Debug)] 14 | pub struct OptimizationResult { 15 | pub path: PathBuf, 16 | pub old_size: usize, 17 | pub new_size: usize, 18 | pub improvement: f32, 19 | pub size_limit_breached: bool, 20 | pub improvement_limit_breached: bool, 21 | } 22 | 23 | pub fn check_file_optimizations( 24 | path: PathBuf, 25 | args: &SizeCheckArgs, 26 | ) -> kompari::Result> { 27 | let old_data = std::fs::read(&path)?; 28 | let old_size = old_data.len(); 29 | let new_data = optimize_png(old_data, SizeOptimizationLevel::High); 30 | let new_size = min(new_data.len(), old_size); 31 | 32 | let is_big = args.size_limit.is_some_and(|limit| { 33 | if args.optimize { 34 | new_size > limit * 1024 // Limit is given in KiB 35 | } else { 36 | old_size > limit * 1024 37 | } 38 | }); 39 | 40 | Ok(if old_size > new_size || is_big { 41 | if args.optimize { 42 | std::fs::write(&path, new_data)?; 43 | } 44 | let improvement = (old_size as f32 - new_size as f32) / old_size as f32; 45 | Some(OptimizationResult { 46 | path, 47 | old_size, 48 | new_size, 49 | improvement, 50 | size_limit_breached: is_big, 51 | improvement_limit_breached: args 52 | .improvement_limit 53 | .is_some_and(|limit| limit < improvement), 54 | }) 55 | } else { 56 | None 57 | }) 58 | } 59 | 60 | pub fn check_size_optimizations(dir_path: &Path, args: &SizeCheckArgs) -> kompari::Result<()> { 61 | let paths: Vec<_> = list_image_dir(dir_path)?.collect(); 62 | let progressbar = indicatif::ProgressBar::new(paths.len() as u64); 63 | let results = paths 64 | .into_par_iter() 65 | .map(|path| { 66 | let result = check_file_optimizations(path, args); 67 | progressbar.inc(1); 68 | result 69 | }) 70 | .collect::>>()?; 71 | progressbar.finish_with_message(""); 72 | let mut results: Vec<_> = results.into_iter().flatten().collect(); 73 | results.sort_unstable_by(|a, b| a.path.cmp(&b.path)); 74 | if print_size_optimization_results(&results, args.optimize)? && !args.optimize { 75 | std::process::exit(1); 76 | } 77 | Ok(()) 78 | } 79 | 80 | pub fn print_size_optimization_results( 81 | results: &[OptimizationResult], 82 | optimize: bool, 83 | ) -> kompari::Result { 84 | if results.is_empty() { 85 | println!("Nothing to optimize"); 86 | return Ok(false); 87 | } 88 | let stdout = termcolor::StandardStream::stdout(termcolor::ColorChoice::Auto); 89 | let mut stdout = stdout.lock(); 90 | let mut total_size = 0; 91 | let mut total_diff = 0; 92 | let mut has_error = false; 93 | for result in results { 94 | let diff = result.old_size - result.new_size; 95 | stdout.set_color(ColorSpec::new().set_fg(None))?; 96 | write!(stdout, "{}: ", result.path.display(),)?; 97 | stdout.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?; 98 | write!(stdout, "{} ", format_size(result.new_size, DECIMAL))?; 99 | stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?; 100 | writeln!(stdout, "(-{})", format_size(diff, DECIMAL))?; 101 | total_size += result.new_size; 102 | total_diff += diff; 103 | if result.size_limit_breached { 104 | stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?; 105 | writeln!(stdout, "Size limit breached")?; 106 | has_error = true; 107 | } 108 | if result.improvement_limit_breached { 109 | stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?; 110 | writeln!(stdout, "Improvement limit breached")?; 111 | has_error = true; 112 | } 113 | } 114 | stdout.set_color(ColorSpec::new().set_fg(None))?; 115 | write!(stdout, "----------------------------\nTotal size: ",)?; 116 | stdout.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?; 117 | write!(stdout, "{} ", format_size(total_size, DECIMAL))?; 118 | stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?; 119 | writeln!(stdout, "(-{})", format_size(total_diff, DECIMAL))?; 120 | stdout.set_color(ColorSpec::new().set_fg(None))?; 121 | if !optimize { 122 | write!(stdout, "Run with ")?; 123 | stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?; 124 | write!(stdout, "--optimize ")?; 125 | stdout.set_color(ColorSpec::new().set_fg(None))?; 126 | writeln!(stdout, "to apply the optimizations")?; 127 | } 128 | Ok(has_error) 129 | } 130 | -------------------------------------------------------------------------------- /kompari-tasks/src/task.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::args::{Args, Command, DeadSnapshotArgs}; 5 | use crate::check_size_optimizations; 6 | use kompari::{list_image_dir, list_image_dir_names, DirDiffConfig}; 7 | use kompari_html::{render_html_report, start_review_server, ReportConfig}; 8 | use std::collections::BTreeSet; 9 | use std::ops::Deref; 10 | use std::path::{Path, PathBuf}; 11 | 12 | pub trait Actions { 13 | fn generate_all_tests(&self) -> kompari::Result<()>; 14 | } 15 | 16 | pub struct Task { 17 | diff_config: DirDiffConfig, 18 | report_config: ReportConfig, 19 | report_output_path: PathBuf, 20 | actions: Box, 21 | } 22 | 23 | impl Task { 24 | pub fn new(diff_config: DirDiffConfig, actions: Box) -> Self { 25 | let mut report_config = ReportConfig::default(); 26 | report_config.set_left_title("Reference"); 27 | report_config.set_right_title("Current"); 28 | Task { 29 | diff_config, 30 | report_config, 31 | report_output_path: "report.html".into(), 32 | actions, 33 | } 34 | } 35 | 36 | pub fn report_config(&mut self) -> &mut ReportConfig { 37 | &mut self.report_config 38 | } 39 | 40 | pub fn set_report_output_path(&mut self, path: PathBuf) { 41 | self.report_output_path = path; 42 | } 43 | 44 | pub fn run(&mut self, args: &Args) -> kompari::Result<()> { 45 | match &args.command { 46 | Command::Report(report_args) => { 47 | if report_args.embed_images { 48 | self.report_config.set_embed_images(true); 49 | } 50 | self.report_config 51 | .set_size_optimization(report_args.optimize_size.to_level()); 52 | let output: &Path = report_args 53 | .output 54 | .as_deref() 55 | .unwrap_or(self.report_output_path.as_path()); 56 | let diff = self.diff_config.create_diff()?; 57 | let report = render_html_report(&self.report_config, diff.results())?; 58 | std::fs::write(output, report)?; 59 | println!("Report written into '{}'", output.display()); 60 | } 61 | Command::Review(args) => { 62 | self.report_config 63 | .set_size_optimization(args.optimize_size.to_level()); 64 | start_review_server(&self.diff_config, &self.report_config, args.port)? 65 | } 66 | Command::Clean => { 67 | clean_image_dir(self.diff_config.right_path())?; 68 | } 69 | Command::DeadSnapshots(ds_args) => { 70 | process_dead_snapshots( 71 | self.diff_config.left_path(), 72 | self.diff_config.right_path(), 73 | self.actions.deref(), 74 | ds_args, 75 | )?; 76 | } 77 | Command::SizeCheck(sc_args) => { 78 | check_size_optimizations(self.diff_config.left_path(), sc_args)?; 79 | } 80 | } 81 | Ok(()) 82 | } 83 | } 84 | 85 | fn clean_image_dir(path: &Path) -> kompari::Result<()> { 86 | for path in list_image_dir(path)? { 87 | std::fs::remove_file(path)?; 88 | } 89 | Ok(()) 90 | } 91 | 92 | fn find_dead_snapshots( 93 | snapshot_path: &Path, 94 | current_path: &Path, 95 | actions: &dyn Actions, 96 | ) -> kompari::Result> { 97 | clean_image_dir(current_path)?; 98 | actions.generate_all_tests()?; 99 | let snapshot_images: BTreeSet<_> = list_image_dir_names(snapshot_path)?.collect(); 100 | let current_images: BTreeSet<_> = list_image_dir_names(current_path)?.collect(); 101 | Ok(snapshot_images 102 | .difference(¤t_images) 103 | .map(|name| current_path.join(name)) 104 | .collect()) 105 | } 106 | 107 | fn process_dead_snapshots( 108 | snapshot_path: &Path, 109 | current_path: &Path, 110 | actions: &dyn Actions, 111 | args: &DeadSnapshotArgs, 112 | ) -> kompari::Result<()> { 113 | let dead_snapshots = find_dead_snapshots(snapshot_path, current_path, actions)?; 114 | if dead_snapshots.is_empty() { 115 | println!("No dead snapshots detected"); 116 | } else { 117 | println!("========== DEAD SNAPSHOTS =========="); 118 | for path in &dead_snapshots { 119 | println!("{}", path.display()); 120 | } 121 | println!("===================================="); 122 | if args.remove_files { 123 | for path in &dead_snapshots { 124 | std::fs::remove_file(path)?; 125 | } 126 | println!("Dead snapshots removed") 127 | } else { 128 | println!("Run the command with '--remove' to remove the files") 129 | } 130 | } 131 | clean_image_dir(current_path)?; 132 | Ok(()) 133 | } 134 | -------------------------------------------------------------------------------- /kompari/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kompari" 3 | description = "Snapshot test reporting CLI.." 4 | keywords = ["image", "report", "diff", "tests"] 5 | categories = ["graphics", "multimedia::images", "development-tools::testing"] 6 | 7 | version.workspace = true 8 | edition.workspace = true 9 | rust-version.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [package.metadata.docs.rs] 17 | all-features = true 18 | # There are no platform specific docs. 19 | default-target = "x86_64-unknown-linux-gnu" 20 | targets = [] 21 | 22 | [dependencies] 23 | # For the bulk of Kompari's functionality 24 | image = { workspace = true } 25 | thiserror = { workspace = true } 26 | walkdir = "2.5" 27 | log = { workspace = true } 28 | oxipng = { workspace = true, optional = true } 29 | rayon = { workspace = true } 30 | 31 | [features] 32 | default = ["oxipng"] 33 | oxipng = ["dep:oxipng"] 34 | -------------------------------------------------------------------------------- /kompari/README.md: -------------------------------------------------------------------------------- 1 | # Kompari 2 | 3 | Core functionality for image diffing and snapshot testing in Rust. 4 | 5 | Your test infrastructure should internally depend on this crate. 6 | -------------------------------------------------------------------------------- /kompari/src/dirdiff.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::imgdiff::{compare_images, ImageDifference}; 5 | use crate::{list_image_dir_names, load_image}; 6 | use rayon::iter::IntoParallelIterator; 7 | use rayon::iter::ParallelIterator; 8 | use std::path::{Path, PathBuf}; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct DirDiffConfig { 12 | left_path: PathBuf, 13 | right_path: PathBuf, 14 | ignore_left_missing: bool, 15 | ignore_right_missing: bool, 16 | filter_name: Option, 17 | } 18 | 19 | impl DirDiffConfig { 20 | pub fn new(left_path: PathBuf, right_path: PathBuf) -> Self { 21 | Self { 22 | left_path, 23 | right_path, 24 | ignore_left_missing: false, 25 | ignore_right_missing: false, 26 | filter_name: None, 27 | } 28 | } 29 | 30 | pub fn left_path(&self) -> &Path { 31 | &self.left_path 32 | } 33 | 34 | pub fn right_path(&self) -> &Path { 35 | &self.right_path 36 | } 37 | 38 | pub fn create_diff(&self) -> crate::Result { 39 | let pairs = pairs_from_paths( 40 | &self.left_path, 41 | &self.right_path, 42 | self.filter_name.as_deref(), 43 | )?; 44 | let diffs: Vec<_> = pairs 45 | .into_par_iter() 46 | .filter_map(|pair| { 47 | let image_diff = compute_pair_diff(&pair); 48 | if matches!(image_diff, Ok(ImageDifference::None)) { 49 | return None; 50 | } 51 | if self.ignore_left_missing 52 | && matches!(image_diff, Err(ref e) if e.is_left_missing()) 53 | { 54 | return None; 55 | } 56 | if self.ignore_right_missing 57 | && matches!(image_diff, Err(ref e) if e.is_right_missing()) 58 | { 59 | return None; 60 | } 61 | Some(PairResult { 62 | title: pair.title, 63 | left: pair.left, 64 | right: pair.right, 65 | image_diff, 66 | }) 67 | }) 68 | .collect(); 69 | Ok(DirDiff { diffs }) 70 | } 71 | 72 | pub fn set_ignore_left_missing(&mut self, value: bool) { 73 | self.ignore_left_missing = value; 74 | } 75 | 76 | pub fn set_ignore_right_missing(&mut self, value: bool) { 77 | self.ignore_right_missing = value; 78 | } 79 | 80 | pub fn set_filter_name(&mut self, value: Option) { 81 | self.filter_name = value; 82 | } 83 | } 84 | 85 | #[derive(Debug)] 86 | pub enum LeftRightError { 87 | Left(crate::Error), 88 | Right(crate::Error), 89 | 90 | // This case is boxed to silence warning of too big error structure, 91 | // Moreover, this case should be rare 92 | Both(Box<(crate::Error, crate::Error)>), 93 | } 94 | 95 | impl LeftRightError { 96 | pub fn left(&self) -> Option<&crate::Error> { 97 | match self { 98 | Self::Left(e) => Some(e), 99 | Self::Both(pair) => Some(&pair.0), 100 | Self::Right(_) => None, 101 | } 102 | } 103 | pub fn right(&self) -> Option<&crate::Error> { 104 | match self { 105 | Self::Right(e) => Some(e), 106 | Self::Both(pair) => Some(&pair.1), 107 | Self::Left(_) => None, 108 | } 109 | } 110 | 111 | pub fn is_left_missing(&self) -> bool { 112 | self.left() 113 | .map(|e| matches!(e, crate::Error::FileNotFound(_))) 114 | .unwrap_or(false) 115 | } 116 | 117 | pub fn is_right_missing(&self) -> bool { 118 | self.right() 119 | .map(|e| matches!(e, crate::Error::FileNotFound(_))) 120 | .unwrap_or(false) 121 | } 122 | 123 | pub fn is_missing_file_error(&self) -> bool { 124 | matches!( 125 | self, 126 | Self::Left(crate::Error::FileNotFound(_)) | Self::Right(crate::Error::FileNotFound(_)) 127 | ) 128 | } 129 | } 130 | 131 | #[derive(Debug)] 132 | pub struct PairResult { 133 | pub title: String, 134 | pub left: PathBuf, 135 | pub right: PathBuf, 136 | pub image_diff: Result, 137 | } 138 | 139 | #[derive(Default, Debug)] 140 | pub struct DirDiff { 141 | diffs: Vec, 142 | } 143 | 144 | impl DirDiff { 145 | pub fn results(&self) -> &[PairResult] { 146 | &self.diffs 147 | } 148 | } 149 | 150 | pub(crate) struct Pair { 151 | pub title: String, 152 | pub left: PathBuf, 153 | pub right: PathBuf, 154 | } 155 | 156 | pub(crate) fn pairs_from_paths( 157 | left_path: &Path, 158 | right_path: &Path, 159 | filter_name: Option<&str>, 160 | ) -> crate::Result> { 161 | if !left_path.is_dir() { 162 | return Err(crate::Error::NotDirectory(left_path.to_path_buf())); 163 | } 164 | if !right_path.is_dir() { 165 | return Err(crate::Error::NotDirectory(right_path.to_path_buf())); 166 | } 167 | let mut names: Vec<_> = list_image_dir_names(left_path)?.collect(); 168 | names.extend(list_image_dir_names(right_path)?); 169 | names.sort_unstable(); 170 | names.dedup(); 171 | names.retain(|filename| { 172 | filter_name 173 | .as_ref() 174 | .map(|f| filename.to_string_lossy().contains(f)) 175 | .unwrap_or(true) 176 | }); 177 | Ok(names 178 | .into_iter() 179 | .map(|name| { 180 | let left = left_path.join(&name); 181 | let right = right_path.join(&name); 182 | Pair { 183 | title: name.to_string_lossy().to_string(), 184 | left, 185 | right, 186 | } 187 | }) 188 | .collect()) 189 | } 190 | 191 | fn compute_pair_diff(pair: &Pair) -> Result { 192 | let left = load_image(&pair.left); 193 | let right = load_image(&pair.right); 194 | let (left_image, right_image) = match (left, right) { 195 | (Ok(left_image), Ok(right_image)) => (left_image, right_image), 196 | (Err(e), Ok(_)) => return Err(LeftRightError::Left(e)), 197 | (Ok(_), Err(e)) => return Err(LeftRightError::Right(e)), 198 | (Err(e1), Err(e2)) => return Err(LeftRightError::Both(Box::new((e1, e2)))), 199 | }; 200 | Ok(compare_images(&left_image, &right_image)) 201 | } 202 | -------------------------------------------------------------------------------- /kompari/src/fsutils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use std::ffi::OsStr; 5 | use std::path::{Path, PathBuf}; 6 | use walkdir::WalkDir; 7 | 8 | pub fn list_image_dir(dir_path: &Path) -> Result, std::io::Error> { 9 | Ok(WalkDir::new(dir_path).into_iter().filter_map(|entry| { 10 | if let Ok(entry) = entry { 11 | let path = entry.path(); 12 | if path 13 | .extension() 14 | .and_then(OsStr::to_str) 15 | .map(|ext| ext.eq_ignore_ascii_case("png")) 16 | .unwrap_or(false) 17 | { 18 | Some(path.to_path_buf()) 19 | } else { 20 | None 21 | } 22 | } else { 23 | None 24 | } 25 | })) 26 | } 27 | 28 | pub fn list_image_dir_names( 29 | dir_path: &Path, 30 | ) -> Result + '_, std::io::Error> { 31 | Ok(list_image_dir(dir_path)?.map(move |p| p.strip_prefix(dir_path).unwrap().to_path_buf())) 32 | } 33 | -------------------------------------------------------------------------------- /kompari/src/imageutils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::Image; 5 | use std::io::Cursor; 6 | use std::path::Path; 7 | 8 | #[derive(Debug, Clone, Copy)] 9 | pub enum SizeOptimizationLevel { 10 | None, 11 | Fast, 12 | High, 13 | } 14 | 15 | pub fn load_image(path: &Path) -> crate::Result { 16 | log::debug!("Loading image {}", path.display()); 17 | if !path.is_file() { 18 | return Err(crate::Error::FileNotFound(path.to_path_buf())); 19 | } 20 | Ok(image::ImageReader::open(path)?.decode()?.into_rgba8()) 21 | } 22 | 23 | #[cfg(feature = "oxipng")] 24 | pub fn optimize_png(data: Vec, opt_level: SizeOptimizationLevel) -> Vec { 25 | let preset = match opt_level { 26 | SizeOptimizationLevel::None => return data, 27 | SizeOptimizationLevel::Fast => 2, 28 | SizeOptimizationLevel::High => 5, 29 | }; 30 | oxipng::optimize_from_memory(&data[..], &oxipng::Options::from_preset(preset)) 31 | .inspect_err(|e| log::warn!("PNG optimization failed: {}", e)) 32 | .unwrap_or(data) 33 | } 34 | 35 | #[cfg(not(feature = "oxipng"))] 36 | pub fn optimize_png(data: Vec, _opt_level: SizeOptimizationLevel) -> Vec { 37 | /* Do nothing */ 38 | data 39 | } 40 | 41 | pub fn image_to_png(image: &Image, opt_level: SizeOptimizationLevel) -> Vec { 42 | let mut data = Vec::new(); 43 | image 44 | .write_to(&mut Cursor::new(&mut data), image::ImageFormat::Png) 45 | .unwrap(); 46 | optimize_png(data, opt_level) 47 | } 48 | 49 | #[cfg(feature = "oxipng")] 50 | pub fn bless_image(source: &Path, target: &Path) -> crate::Result<()> { 51 | let image = load_image(source)?; 52 | let data = image_to_png(&image, SizeOptimizationLevel::High); 53 | std::fs::write(target, data)?; 54 | Ok(()) 55 | } 56 | 57 | #[cfg(not(feature = "oxipng"))] 58 | pub fn bless_image(source: &Path, target: &Path) -> crate::Result<()> { 59 | std::fs::copy(source, target)?; 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /kompari/src/imgdiff.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use image::{Pixel, Rgba}; 5 | use std::fmt::{Debug, Display, Formatter}; 6 | 7 | use crate::Image; 8 | 9 | #[derive(Debug)] 10 | pub enum DiffImageMethod { 11 | RedGreen, 12 | Overlay, 13 | } 14 | 15 | impl Display for DiffImageMethod { 16 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 17 | write!( 18 | f, 19 | "{}", 20 | match self { 21 | Self::RedGreen => "RedGreen", 22 | Self::Overlay => "Overlay", 23 | } 24 | ) 25 | } 26 | } 27 | 28 | #[derive(Debug)] 29 | pub struct DiffImage { 30 | pub method: DiffImageMethod, 31 | pub image: Image, 32 | } 33 | 34 | pub enum ImageDifference { 35 | None, 36 | SizeMismatch { 37 | left_size: (u32, u32), 38 | right_size: (u32, u32), 39 | }, 40 | Content { 41 | n_pixels: u64, 42 | n_different_pixels: u64, 43 | distance_sum: u64, 44 | diff_images: Vec, 45 | }, 46 | } 47 | 48 | impl Debug for ImageDifference { 49 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 50 | match self { 51 | Self::None => write!(f, "Difference::None"), 52 | Self::SizeMismatch { 53 | left_size, 54 | right_size, 55 | } => write!( 56 | f, 57 | "Difference::SizeMismatch({:?}, {:?})", 58 | left_size, right_size 59 | ), 60 | Self::Content { 61 | n_different_pixels, .. 62 | } => f 63 | .debug_struct("Difference::Content") 64 | .field("n_different_pixels", n_different_pixels) 65 | .finish(), 66 | } 67 | } 68 | } 69 | 70 | fn compute_rg_diff_image(left: &Image, right: &Image) -> (Image, u64) { 71 | let mut distance_sum = 0; 72 | let diff_image_data: Vec = left 73 | .pixels() 74 | .zip(right.pixels()) 75 | .flat_map(|(&p1, &p2)| { 76 | let (diff_min, diff_max) = pixel_min_max_distance(p1, p2); 77 | distance_sum += diff_max.max(diff_min) as u64; 78 | if diff_min > diff_max { 79 | [diff_min, 0, 0, u8::MAX] 80 | } else { 81 | [0, diff_max, 0, u8::MAX] 82 | } 83 | }) 84 | .collect(); 85 | let image = Image::from_vec(left.width(), left.height(), diff_image_data) 86 | .expect("Same number of pixels as left and right, which have the same dimensions"); 87 | (image, distance_sum) 88 | } 89 | 90 | fn compute_overlay_diff_image(left: &Image, right: &Image) -> Image { 91 | let diff_image_data: Vec = left 92 | .pixels() 93 | .zip(right.pixels()) 94 | .flat_map(|(&p1, &p2)| { 95 | let distance = pixel_distance(p1, p2); 96 | if distance > 0 { 97 | p2.0 98 | } else { 99 | let [r, g, b, a] = p1.0; 100 | if a > 128 { 101 | [r, g, b, a / 3] 102 | } else { 103 | [r, g, b, 0] 104 | } 105 | } 106 | }) 107 | .collect(); 108 | Image::from_vec(left.width(), left.height(), diff_image_data) 109 | .expect("Same number of pixels as left and right, which have the same dimensions") 110 | } 111 | 112 | /// Find differences between two images 113 | pub fn compare_images(left: &Image, right: &Image) -> ImageDifference { 114 | if left.width() != right.width() || left.height() != right.height() { 115 | return ImageDifference::SizeMismatch { 116 | left_size: (left.width(), left.height()), 117 | right_size: (right.width(), right.height()), 118 | }; 119 | } 120 | 121 | let n_different_pixels: u64 = left 122 | .pixels() 123 | .zip(right.pixels()) 124 | .map(|(pl, pr)| if pl == pr { 0 } else { 1 }) 125 | .sum(); 126 | 127 | if n_different_pixels == 0 { 128 | return ImageDifference::None; 129 | } 130 | 131 | let (rg_diff_image, distance_sum) = compute_rg_diff_image(left, right); 132 | let overlay_diff_image = compute_overlay_diff_image(left, right); 133 | ImageDifference::Content { 134 | n_pixels: left.width() as u64 * right.height() as u64, 135 | n_different_pixels, 136 | distance_sum, 137 | diff_images: vec![ 138 | DiffImage { 139 | method: DiffImageMethod::RedGreen, 140 | image: rg_diff_image, 141 | }, 142 | DiffImage { 143 | method: DiffImageMethod::Overlay, 144 | image: overlay_diff_image, 145 | }, 146 | ], 147 | } 148 | } 149 | 150 | fn pixel_distance(left: Rgba, right: Rgba) -> u64 { 151 | left.channels() 152 | .iter() 153 | .zip(right.channels()) 154 | .map(|(c_left, c_right)| c_left.abs_diff(*c_right).into()) 155 | .max() 156 | .unwrap_or_default() 157 | } 158 | 159 | fn pixel_min_max_distance(left: Rgba, right: Rgba) -> (u8, u8) { 160 | left.channels() 161 | .iter() 162 | .zip(right.channels()) 163 | .fold((0, 0), |(min, max), (c1, c2)| { 164 | if c2 > c1 { 165 | (min, max.max(c2 - c1)) 166 | } else { 167 | (min.max(c1 - c2), max) 168 | } 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /kompari/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! A shared image diffing implementation, to be used in testing and developer tools. 5 | //! 6 | //! This crate also includes utilities for creating image snapshot test suites. 7 | 8 | // LINEBENDER LINT SET - lib.rs - v3 9 | // See https://linebender.org/wiki/canonical-lints/ 10 | // These lints shouldn't apply to examples or tests. 11 | #![cfg_attr(not(test), warn(unused_crate_dependencies))] 12 | // These lints shouldn't apply to examples. 13 | #![warn(clippy::print_stdout, clippy::print_stderr)] 14 | // Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit. 15 | #![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))] 16 | // END LINEBENDER LINT SET 17 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 18 | 19 | pub use image; 20 | use std::path::PathBuf; 21 | use thiserror::Error; 22 | 23 | mod dirdiff; 24 | mod fsutils; 25 | mod imageutils; 26 | mod imgdiff; 27 | 28 | /// The image type used throughout Kompari. 29 | pub type Image = image::RgbaImage; 30 | 31 | #[derive(Error, Debug)] 32 | pub enum Error { 33 | #[error("IO error")] 34 | IoError(#[from] std::io::Error), 35 | 36 | #[error("Path is a directory: `{0}`")] 37 | NotDirectory(PathBuf), 38 | 39 | #[error("File not found: `{0}`")] 40 | FileNotFound(PathBuf), 41 | 42 | #[error("Image error")] 43 | ImageError(#[from] image::ImageError), 44 | 45 | #[error("Error `{0}`")] 46 | GenericError(String), 47 | } 48 | 49 | pub type Result = std::result::Result; 50 | 51 | pub use dirdiff::{DirDiff, DirDiffConfig, LeftRightError, PairResult}; 52 | pub use fsutils::{list_image_dir, list_image_dir_names}; 53 | pub use imageutils::{bless_image, image_to_png, load_image, optimize_png, SizeOptimizationLevel}; 54 | pub use imgdiff::{compare_images, DiffImage, DiffImageMethod, ImageDifference}; 55 | -------------------------------------------------------------------------------- /kompari/tests/compare.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Kompari Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use kompari::{DirDiffConfig, ImageDifference, LeftRightError}; 5 | use std::path::Path; 6 | 7 | fn create_test_diff_config() -> DirDiffConfig { 8 | let test_dir = Path::new(env!("CARGO_MANIFEST_DIR")) 9 | .parent() 10 | .unwrap() 11 | .join("tests"); 12 | let left = test_dir.join("left"); 13 | let right = test_dir.join("right"); 14 | DirDiffConfig::new(left, right) 15 | } 16 | 17 | #[test] 18 | pub(crate) fn test_compare_dir() { 19 | let diff = create_test_diff_config().create_diff().unwrap(); 20 | let res = diff.results(); 21 | let titles: Vec<_> = res.iter().map(|r| r.title.as_str()).collect(); 22 | assert_eq!( 23 | titles, 24 | [ 25 | "bright.png", 26 | "changetext.png", 27 | "left_missing.png", 28 | "right_missing.png", 29 | "shift.png", 30 | "size_error.png" 31 | ] 32 | ); 33 | assert!(matches!( 34 | res[0].image_diff, 35 | Ok(ImageDifference::Content { 36 | n_different_pixels: 18623, 37 | .. 38 | }) 39 | )); 40 | assert!(matches!( 41 | res[1].image_diff, 42 | Ok(ImageDifference::Content { 43 | n_different_pixels: 275, 44 | .. 45 | }) 46 | )); 47 | assert!(matches!( 48 | res[2].image_diff, 49 | Err(LeftRightError::Left(kompari::Error::FileNotFound(_))) 50 | )); 51 | assert!(matches!( 52 | res[3].image_diff, 53 | Err(LeftRightError::Right(kompari::Error::FileNotFound(_))) 54 | )); 55 | assert!(matches!( 56 | res[4].image_diff, 57 | Ok(ImageDifference::Content { 58 | n_different_pixels: 3858, 59 | .. 60 | }) 61 | )); 62 | assert!(matches!( 63 | res[5].image_diff, 64 | Ok(ImageDifference::SizeMismatch { 65 | left_size: (850, 88), 66 | right_size: (147, 881) 67 | }) 68 | )); 69 | } 70 | 71 | #[test] 72 | pub(crate) fn test_ignore_left_missing() { 73 | let mut config = create_test_diff_config(); 74 | config.set_ignore_left_missing(true); 75 | let diff = config.create_diff().unwrap(); 76 | let res = diff.results(); 77 | let titles: Vec<_> = res.iter().map(|r| r.title.as_str()).collect(); 78 | assert_eq!( 79 | titles, 80 | [ 81 | "bright.png", 82 | "changetext.png", 83 | "right_missing.png", 84 | "shift.png", 85 | "size_error.png" 86 | ] 87 | ); 88 | } 89 | 90 | #[test] 91 | pub(crate) fn test_ignore_right_missing() { 92 | let mut config = create_test_diff_config(); 93 | config.set_ignore_right_missing(true); 94 | let diff = config.create_diff().unwrap(); 95 | let res = diff.results(); 96 | let titles: Vec<_> = res.iter().map(|r| r.title.as_str()).collect(); 97 | assert_eq!( 98 | titles, 99 | [ 100 | "bright.png", 101 | "changetext.png", 102 | "left_missing.png", 103 | "shift.png", 104 | "size_error.png" 105 | ] 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /tests/left/bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/left/bright.png -------------------------------------------------------------------------------- /tests/left/changetext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/left/changetext.png -------------------------------------------------------------------------------- /tests/left/right_missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/left/right_missing.png -------------------------------------------------------------------------------- /tests/left/same.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/left/same.png -------------------------------------------------------------------------------- /tests/left/shift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/left/shift.png -------------------------------------------------------------------------------- /tests/left/size_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/left/size_error.png -------------------------------------------------------------------------------- /tests/right/bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/right/bright.png -------------------------------------------------------------------------------- /tests/right/changetext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/right/changetext.png -------------------------------------------------------------------------------- /tests/right/left_missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/right/left_missing.png -------------------------------------------------------------------------------- /tests/right/same.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/right/same.png -------------------------------------------------------------------------------- /tests/right/shift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/right/shift.png -------------------------------------------------------------------------------- /tests/right/size_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/kompari/4b851413e1b17307064aa48c50e59d7e29656543/tests/right/size_error.png --------------------------------------------------------------------------------