├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .ocamlformat ├── .spr.yml ├── .vscode ├── c_cpp_properties.json └── settings.json ├── LICENSE ├── README.md ├── ava.config.cjs ├── bin ├── Color.ml ├── Main.ml ├── ODiffBin.ml ├── Print.ml └── dune ├── dune-project ├── images ├── 2x2-ff0000ff.png ├── README.md ├── __debug.png ├── benchmarks.png ├── donkey-2.png ├── donkey-diff.png ├── donkey.png ├── extreme-alpha-1.png ├── extreme-alpha.png ├── out.png ├── test_map.png ├── test_map_1.png ├── tiger-2.jpg ├── tiger-diff.png ├── tiger.jpg ├── water-4k-2.png ├── water-4k.png ├── water-diff.png ├── www.cypress-diff.png ├── www.cypress.io-1.png └── www.cypress.io.png ├── io ├── ODiffIO.ml ├── bmp │ ├── Bmp.ml │ ├── Bmp.mli │ ├── ReadBmp.ml │ ├── ReadBmp.mli │ └── dune ├── config │ ├── discover.ml │ └── dune ├── dune ├── jpg │ ├── Jpg.ml │ ├── Jpg.mli │ ├── ReadJpg.c │ ├── ReadJpg.ml │ └── dune ├── png │ ├── Png.ml │ ├── ReadPng.c │ ├── ReadPng.ml │ └── dune ├── png_write │ ├── WritePng.c │ ├── WritePng.ml │ └── dune └── tiff │ ├── ReadTiff.c │ ├── ReadTiff.ml │ ├── Tiff.ml │ ├── Tiff.mli │ └── dune ├── npm_package ├── bin │ └── odiff.exe ├── odiff.d.ts ├── odiff.js ├── package.json ├── post_install.js └── raw_binaries │ └── .gitkeep ├── odiff-core.opam ├── odiff-io.opam ├── odiff-logo-dark.png ├── odiff-logo-light.png ├── odiff-tests.opam ├── odiff.opam ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── release.sh ├── scripts └── process-readme.js ├── src ├── Antialiasing.ml ├── ColorDelta.ml ├── Diff.ml ├── ImageIO.ml ├── PerfTest.ml └── dune ├── test ├── Test_Core.ml ├── Test_IO_BMP.ml ├── Test_IO_JPG.ml ├── Test_IO_PNG.ml ├── Test_IO_TIFF.ml ├── dune ├── node-binding.test.cjs ├── node-bindings.test.ts └── test-images │ ├── aa │ ├── antialiasing-off-small.png │ ├── antialiasing-off.png │ └── antialiasing-on.png │ ├── bmp │ ├── clouds-2.bmp │ ├── clouds-diff.png │ └── clouds.bmp │ ├── jpg │ ├── tiger-2.jpg │ ├── tiger-diff.png │ └── tiger.jpg │ ├── png │ ├── diff-output-green.png │ ├── extreme-alpha-1.png │ ├── extreme-alpha.png │ ├── orange.png │ ├── orange_changed.png │ ├── orange_diff.png │ ├── orange_diff_green.png │ ├── purple8x8.png │ └── white4x4.png │ └── tiff │ ├── laptops-2.tiff │ ├── laptops-diff.png │ └── laptops.tiff ├── typos.toml └── vcpkg.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.yaml linguist-detectable=false 2 | .ci/* linguist-vendored -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: dmtrKovalenko 4 | open_collective: odiff 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*.*.*" 9 | 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | continue-on-error: true 14 | strategy: 15 | matrix: 16 | include: 17 | - os: ubuntu-latest 18 | artifact: "linux-x64" 19 | ocaml-compiler: "ocaml-variants.5.2.0+options,ocaml-option-flambda" 20 | triplet: "x64-linux" 21 | - os: ubuntu-24.04-arm 22 | artifact: "linux-arm64" 23 | ocaml-compiler: "ocaml-variants.5.2.0+options,ocaml-option-flambda" 24 | triplet: "arm64-linux" 25 | - os: windows-latest 26 | artifact: "windows-x64" 27 | ocaml-compiler: "ocaml-variants.5.2.0+options,ocaml-option-flambda" 28 | triplet: "x64-mingw-static" 29 | - os: macos-latest 30 | artifact: "macos-arm64" 31 | ocaml-compiler: "ocaml-variants.5.2.0+options,ocaml-option-flambda" 32 | triplet: "arm64-osx" 33 | - os: macos-13 34 | artifact: "macos-x64" 35 | ocaml-compiler: "ocaml-variants.5.2.0+options,ocaml-option-flambda" 36 | triplet: "x64-osx" 37 | defaults: 38 | run: 39 | shell: bash 40 | steps: 41 | - uses: actions/checkout@v2.3.2 42 | 43 | - if: runner.os == 'Windows' 44 | run: | 45 | rm -rf $(which pkg-config) 46 | choco install pkgconfiglite 47 | 48 | - run: gcc --version 49 | 50 | - uses: lukka/get-cmake@latest 51 | - name: Setup anew (or from cache) vcpkg (and does not build any package) 52 | uses: lukka/run-vcpkg@v11 53 | env: 54 | VCPKG_DEFAULT_TRIPLET: ${{ matrix.triplet }} 55 | VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.triplet }} 56 | VCPKG_BUILD_TYPE: release 57 | with: 58 | runVcpkgInstall: true 59 | runVcpkgFormatString: '["install", "--clean-after-build"]' 60 | 61 | - name: Set pkg-config path on Unix 62 | if: runner.os != 'Windows' 63 | run: | 64 | ls "${GITHUB_WORKSPACE}/vcpkg_installed/${VCPKG_DEFAULT_TRIPLET}/lib/pkgconfig" 65 | echo "PKG_CONFIG_PATH=${GITHUB_WORKSPACE}/vcpkg_installed/${VCPKG_DEFAULT_TRIPLET}/lib/pkgconfig" >> $GITHUB_ENV 66 | 67 | - name: Set pkg-config path on Unix 68 | shell: bash 69 | if: runner.os == 'Windows' 70 | run: | 71 | echo "PKG_CONFIG_PATH=${GITHUB_WORKSPACE}\vcpkg_installed\\${VCPKG_DEFAULT_TRIPLET}\lib\pkgconfig" >> $GITHUB_ENV 72 | 73 | - shell: bash 74 | run: | 75 | echo "LIBPNG_CFLAGS=$(pkg-config --cflags libspng_static)" >> $GITHUB_ENV 76 | echo "LIBPNG_LIBS=$(pkg-config --libs libspng_static)" >> $GITHUB_ENV 77 | echo "LIBTIFF_LIBS=$(pkg-config --libs libtiff-4)" >> $GITHUB_ENV 78 | echo "LIBTIFF_CFLAGS=$(pkg-config --cflags libtiff-4)" >> $GITHUB_ENV 79 | echo "LIBJPEG_CFLAGS=$(pkg-config --cflags libturbojpeg)" >> $GITHUB_ENV 80 | echo "LIBJPEG_LIBS=$(pkg-config --libs libturbojpeg)" >> $GITHUB_ENV 81 | 82 | - uses: ocaml/setup-ocaml@v3 83 | with: 84 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 85 | opam-disable-sandboxing: true 86 | dune-cache: false 87 | 88 | - run: opam exec -- opam install . --deps-only --with-test 89 | - run: opam exec -- dune build --verbose 90 | 91 | - run: opam exec -- dune exec ODiffBin -- --version 92 | - run: opam exec -- dune runtest 93 | 94 | - if: failure() 95 | uses: actions/upload-artifact@v4 96 | with: 97 | name: test_images 98 | path: _build/default/test/test_images 99 | 100 | - name: Set up Node.js 101 | uses: actions/setup-node@v4 102 | with: 103 | node-version: '20' 104 | cache: 'npm' 105 | cache-dependency-path: 'package-lock.json' 106 | 107 | - name: Install node deps 108 | run: npm ci 109 | - name: e2e test 110 | run: npm test 111 | 112 | - name: Build release binary 113 | # this is needed because now ocaml's internal cygwin will be used on windows 114 | # which breaks normal line endings 115 | env: 116 | SHELLOPTS: igncr 117 | run: | 118 | opam exec -- dune clean && \ 119 | opam exec -- dune build --release && \ 120 | cp "_build/default/bin/ODiffBin.exe" "odiff-${{ matrix.artifact }}.exe" 121 | 122 | - if: always() 123 | uses: actions/upload-artifact@v4 124 | with: 125 | if-no-files-found: error 126 | name: odiff-${{ matrix.artifact }}.exe 127 | path: odiff-${{ matrix.artifact }}.exe 128 | retention-days: 14 129 | 130 | publish: 131 | name: Publish release 132 | needs: [build] 133 | if: startsWith(github.ref, 'refs/tags/') 134 | runs-on: ubuntu-latest 135 | steps: 136 | - name: Checkout 137 | uses: actions/checkout@v1 138 | 139 | - name: Download built binaries 140 | uses: actions/download-artifact@v4 141 | with: 142 | pattern: odiff-*.exe 143 | merge-multiple: true 144 | path: npm_package/raw_binaries 145 | 146 | - name: Set up Node.js 147 | uses: actions/setup-node@v4 148 | with: 149 | node-version: '20' 150 | cache: 'npm' 151 | always-auth: true 152 | registry-url: 'https://registry.npmjs.org' 153 | cache-dependency-path: 'package-lock.json' 154 | 155 | - name: Publish npm package 156 | working-directory: npm_package 157 | run: npm publish 158 | env: 159 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 160 | 161 | - name: Create github release 162 | uses: softprops/action-gh-release@v1 163 | with: 164 | files: "npm_package/raw_binaries/*" 165 | env: 166 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 167 | 168 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .merlin 2 | .DS_Store 3 | **/.DS_Store 4 | node_modules/ 5 | _build 6 | _esy 7 | _release 8 | *.byte 9 | *.native 10 | *.install 11 | images/diff.png 12 | test/test-images/_*.png 13 | vcpkg_installed/* 14 | out.png 15 | _opam/ 16 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/.ocamlformat -------------------------------------------------------------------------------- /.spr.yml: -------------------------------------------------------------------------------- 1 | githubRepoOwner: dmtrKovalenko 2 | githubRepoName: odiff 3 | githubHost: github.com 4 | githubRemote: origin/chore 5 | githubBranch: esy-nightly 6 | requireChecks: true 7 | requireApproval: true 8 | mergeMethod: rebase 9 | mergeQueue: false 10 | forceFetchTags: false 11 | showPrTitlesInStack: false 12 | branchPushIndividually: false 13 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Mac", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "macFrameworkPath": [ 10 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks" 11 | ], 12 | "compilerPath": "/usr/bin/clang", 13 | "cStandard": "c11", 14 | "cppStandard": "c++17", 15 | "intelliSenseMode": "clang-x64" 16 | } 17 | ], 18 | "version": 4 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Odiff" 4 | ], 5 | "files.associations": { 6 | "*.prisma": "graphql", 7 | "__bit_reference": "c", 8 | "__functional_base": "c", 9 | "__node_handle": "c", 10 | "algorithm": "c", 11 | "atomic": "c", 12 | "bitset": "c", 13 | "chrono": "c", 14 | "__memory": "c", 15 | "functional": "c", 16 | "iterator": "c", 17 | "limits": "c", 18 | "locale": "c", 19 | "memory": "c", 20 | "optional": "c", 21 | "ratio": "c", 22 | "system_error": "c", 23 | "tuple": "c", 24 | "type_traits": "c", 25 | "vector": "c", 26 | "ios": "c", 27 | "cstddef": "c" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dmitriy Kovalenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | pixeletad caml and odiff text with highlighted red pixels difference 6 | 7 |

8 | 9 |

The fastest* (one-thread) pixel-by-pixel image difference tool in the world.

10 | 11 |
12 | made with reason 13 | npm 14 | 15 |
16 | 17 | 18 | ## Why Odiff? 19 | 20 | ODiff is a blazing fast native image comparison tool. Check [benchmarks](#benchmarks) for the results, but it compares the visual difference between 2 images in **milliseconds**. It was originally designed to handle the "big" images. Thanks to [OCaml](https://ocaml.org/) and its speedy and predictable compiler we can significantly speed up your CI pipeline. 21 | 22 | [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine/) 23 | 24 | ## Demo 25 | 26 | | base | comparison | diff | 27 | | ------------------------------ | -------------------------------- | ------------------------------------- | 28 | | ![](images/tiger.jpg) | ![](images/tiger-2.jpg) | ![1diff](images/tiger-diff.png) | 29 | | ![](images/www.cypress.io.png) | ![](images/www.cypress.io-1.png) | ![1diff](images/www.cypress-diff.png) | 30 | | ![](images/donkey.png) | ![](images/donkey-2.png) | ![1diff](images/donkey-diff.png) | 31 | 32 | ## Features 33 | 34 | - ✅ Cross-format comparison - Yes .jpg vs .png comparison without any problems. 35 | - ✅ Support for `.png`, `.jpeg`, `.jpg`, and `.tiff` 36 | - ✅ Supports comparison of images with different layouts. 37 | - ✅ Anti-aliasing detection 38 | - ✅ Ignoring regions 39 | - ✅ Using [YIQ NTSC 40 | transmission algorithm](https://progmat.uaem.mx/progmat/index.php/progmat/article/view/2010-2-2-03/2010-2-2-03) to determine visual difference. 41 | 42 | ### Coming in the nearest future: 43 | 44 | - ⏹ Reading image from memory buffer 45 | - ⏹ Reading images from url 46 | 47 | ## Usage 48 | 49 | ### Basic comparison 50 | 51 | Run the simple comparison. Image paths can be one of supported formats, diff output is optional and can only be `.png`. 52 | 53 | ``` 54 | odiff [DIFF output path] 55 | ``` 56 | 57 | ### Node.js 58 | 59 | We also provides direct node.js binding for the `odiff`. Run the `odiff` from nodejs: 60 | 61 | ```js 62 | const { compare } = require("odiff-bin"); 63 | 64 | const { match, reason } = await compare( 65 | "path/to/first/image.png", 66 | "path/to/second/image.png", 67 | "path/to/diff.png" 68 | ); 69 | ``` 70 | 71 | ### Cypress 72 | Checkout [cypress-odiff](https://github.com/odai-alali/cypress-odiff), a cypress plugin to add visual regression tests using `odiff-bin`. 73 | 74 | ### Visual regression services 75 | 76 | [LostPixel](https://github.com/lost-pixel/lost-pixel) – Holistic visual testing for your Frontend allows very easy integration with storybook and uses odiff for comparison 77 | 78 | [Argos CI](https://argos-ci.com/) – Visual regression service powering projects like material-ui. ([It became 8x faster with odiff](https://twitter.com/argos_ci/status/1601873725019807744)) 79 | 80 | [Visual Regression Tracker](https://github.com/Visual-Regression-Tracker/Visual-Regression-Tracker) – Self hosted visual regression service that allows to use odiff as screenshot comparison engine 81 | 82 | [OSnap](https://github.com/eWert-Online/OSnap) – Snapshot testing tool written in OCaml that uses config based declaration to define test and was built by odiff collaborator. 83 | 84 | ## Api 85 | 86 | Here is an api reference: 87 | 88 | ### CLI 89 | 90 | The best way to get up-to-date cli interface is just to type the 91 | 92 | ``` 93 | odiff --help 94 | ``` 95 | 96 | ### Node.js 97 | 98 | NodeJS Api is pretty tiny as well. Here is a typescript interface we have: 99 | 100 | 101 | ```tsx 102 | export type ODiffOptions = Partial<{ 103 | /** Color used to highlight different pixels in the output (in hex format e.g. #cd2cc9). */ 104 | diffColor: string; 105 | /** Output full diff image. */ 106 | outputDiffMask: boolean; 107 | /** Do not compare images and produce output if images layout is different. */ 108 | failOnLayoutDiff: boolean; 109 | /** Return { match: false, reason: '...' } instead of throwing error if file is missing. */ 110 | noFailOnFsErrors: boolean; 111 | /** Color difference threshold (from 0 to 1). Less more precise. */ 112 | threshold: number; 113 | /** If this is true, antialiased pixels are not counted to the diff of an image */ 114 | antialiasing: boolean; 115 | /** If `true` reason: "pixel-diff" output will contain the set of line indexes containing different pixels */ 116 | captureDiffLines: boolean; 117 | /** If `true` odiff will use less memory but will be slower with larger images */ 118 | reduceRamUsage: boolean; 119 | /** An array of regions to ignore in the diff. */ 120 | ignoreRegions: Array<{ 121 | x1: number; 122 | y1: number; 123 | x2: number; 124 | y2: number; 125 | }>; 126 | }>; 127 | 128 | declare function compare( 129 | basePath: string, 130 | comparePath: string, 131 | diffPath: string, 132 | options?: ODiffOptions 133 | ): Promise< 134 | | { match: true } 135 | | { match: false; reason: "layout-diff" } 136 | | { 137 | match: false; 138 | reason: "pixel-diff"; 139 | /** Amount of different pixels */ 140 | diffCount: number; 141 | /** Percentage of different pixels in the whole image */ 142 | diffPercentage: number; 143 | /** Individual line indexes containing different pixels. Guaranteed to be ordered and distinct. */ 144 | diffLines?: number[]; 145 | } 146 | | { 147 | match: false; 148 | reason: "file-not-exists"; 149 | /** Errored file path */ 150 | file: string; 151 | } 152 | >; 153 | 154 | export { compare }; 155 | ``` 156 | " 157 | 158 | Compare option will return `{ match: true }` if images are identical. Otherwise return `{ match: false, reason: "*" }` with a reason why images were different. 159 | 160 | > Make sure that diff output file will be created only if images have pixel difference we can see 👀 161 | 162 | ## Installation 163 | 164 | We provide prebuilt binaries for most of the used platforms, there are a few ways to install them: 165 | 166 | ### Cross-platform 167 | 168 | The recommended and cross-platform way to install this lib is npm and node.js. Make sure that this package is compiled directly to the platform binary executable, so the npm package contains all binaries and `post-install` script will automatically link the right one for the current platform. 169 | 170 | > **Important**: package name is **odiff-bin**. But the binary itself is **odiff** 171 | 172 | ``` 173 | npm install odiff-bin 174 | ``` 175 | 176 | Then give it a try 👀 177 | 178 | ``` 179 | odiff --help 180 | ``` 181 | 182 | ### From binaries 183 | 184 | Download the binaries for your platform from [release](https://github.com/dmtrKovalenko/odiff/releases) page. 185 | 186 | ## Benchmarks 187 | 188 | > Run the benchmarks by yourself. Instructions of how to run the benchmark is [here](./images) 189 | 190 | ![benchmark](images/benchmarks.png) 191 | 192 | Performance matters. At least for sort of tasks like visual regression. For example, if you are running 25000 image snapshots per month you can save **20 hours** of CI time per month by speeding up comparison time in just **3 seconds** per snapshot. 193 | 194 | ``` 195 | 3s * 25000 / 3600 = 20,83333 hours 196 | ``` 197 | 198 | Here is `odiff` performance comparison with other popular visual difference solutions. We are going to compare some real-world use cases. 199 | 200 | Lets compare 2 screenshots of full-size [https::/cypress.io](cypress.io) page: 201 | 202 | | Command | Mean [s] | Min [s] | Max [s] | Relative | 203 | | :----------------------------------------------------------------------------------------- | ------------: | ------: | ------: | ----------: | 204 | | `pixelmatch www.cypress.io-1.png www.cypress.io.png www.cypress-diff.png` | 7.712 ± 0.069 | 7.664 | 7.896 | 6.67 ± 0.03 | 205 | | ImageMagick `compare www.cypress.io-1.png www.cypress.io.png -compose src diff-magick.png` | 8.881 ± 0.121 | 8.692 | 9.066 | 7.65 ± 0.04 | 206 | | `odiff www.cypress.io-1.png www.cypress.io.png www.cypress-diff.png` | 1.168 ± 0.008 | 1.157 | 1.185 | 1.00 | 207 | 208 | Wow. Odiff is mostly 6 times faster than imagemagick and pixelmatch. And this will be even clearer if image will become larger. Lets compare an [8k image](images/water-4k.png) to find a difference with [another 8k image](images/water-4k-2.png): 209 | 210 | | Command | Mean [s] | Min [s] | Max [s] | Relative | 211 | | :---------------------------------------------------------------------------- | -------------: | ------: | ------: | ----------: | 212 | | `pixelmatch water-4k.png water-4k-2.png water-diff.png` | 10.614 ± 0.162 | 10.398 | 10.910 | 5.50 ± 0.05 | 213 | | Imagemagick `compare water-4k.png water-4k-2.png -compose src water-diff.png` | 9.326 ± 0.436 | 8.819 | 10.394 | 5.24 ± 0.10 | 214 | | `odiff water-4k.png water-4k-2.png water-diff.png` | 1.951 ± 0.014 | 1.936 | 1.981 | 1.00 | 215 | 216 | Yes it is significant improvement. And the produced difference will be the same for all 3 commands. 217 | 218 | ## Changelog 219 | 220 | If you have recently updated, please read the [changelog](https://github.com/dmtrKovalenko/odiff/releases) for details of what has changed. 221 | 222 | ## License 223 | 224 | The project is licensed under the terms of [MIT license](./LICENSE) 225 | 226 | ## Thanks 227 | 228 | This project was highly inspired by [pixelmatch](https://github.com/mapbox/pixelmatch) and [imagemagick](https://github.com/ImageMagick/ImageMagick). 229 | 230 | ## Support the project 231 | 232 | ...one day a donation button will appear here. But for now you can follow [author's twitter](https://twitter.com/dmtrKovalenko) :) 233 | -------------------------------------------------------------------------------- /ava.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | files: ["test/*"], 3 | }; 4 | -------------------------------------------------------------------------------- /bin/Color.ml: -------------------------------------------------------------------------------- 1 | let ofHexString s = 2 | match String.length s with 3 | | (4 | 7) as len -> 4 | (let short = len = 4 in 5 | let r' = 6 | match short with true -> String.sub s 1 1 | false -> String.sub s 1 2 7 | in 8 | let g' = 9 | match short with true -> String.sub s 2 1 | false -> String.sub s 3 2 10 | in 11 | let b' = 12 | match short with true -> String.sub s 3 1 | false -> String.sub s 5 2 13 | in 14 | let r = int_of_string_opt ("0x" ^ r') in 15 | let g = int_of_string_opt ("0x" ^ g') in 16 | let b = int_of_string_opt ("0x" ^ b') in 17 | 18 | match (r, g, b) with 19 | | Some r, Some g, Some b when short -> 20 | Some ((16 * r) + r, (16 * g) + g, (16 * b) + b) 21 | | Some r, Some g, Some b -> Some (r, g, b) 22 | | _ -> None) 23 | |> Option.map (fun (r, g, b) -> 24 | (* Create rgba pixel value right after parsing *) 25 | let r = (r land 255) lsl 0 in 26 | let g = (g land 255) lsl 8 in 27 | let b = (b land 255) lsl 16 in 28 | let a = 255 lsl 24 in 29 | 30 | Int32.of_int (a lor b lor g lor r)) 31 | | _ -> None 32 | -------------------------------------------------------------------------------- /bin/Main.ml: -------------------------------------------------------------------------------- 1 | open Odiff.ImageIO 2 | open Odiff.Diff 3 | 4 | let getIOModule filename = 5 | match Filename.extension filename with 6 | | ".png" -> (module ODiffIO.Png.IO : ImageIO) 7 | | ".jpg" | ".jpeg" -> (module ODiffIO.Jpg.IO : ImageIO) 8 | | ".bmp" -> (module ODiffIO.Bmp.IO : ImageIO) 9 | | ".tiff" -> (module ODiffIO.Tiff.IO : ImageIO) 10 | | "" -> 11 | failwith 12 | ("Usage: " ^ Sys.argv.(0) 13 | ^ " ") 14 | | f -> failwith ("This format is not supported: " ^ f) 15 | 16 | type 'output diffResult = { exitCode : int; diff : 'output option } 17 | 18 | (* Arguments must remain positional for the cmd parser lib that we use *) 19 | let main img1Path img2Path diffPath threshold outputDiffMask failOnLayoutChange 20 | diffColorHex toEmitStdoutParsableString antialiasing ignoreRegions diffLines 21 | disableGcOptimizations = 22 | (* 23 | Increase amount of allowed overhead to reduce amount of GC work and cycles. 24 | we target 1-2 minor collections per run which is the best tradeoff between 25 | amount of memory allocated and time spend on GC. 26 | 27 | For sure it depends on the image size and architecture. Primary target x86_64 28 | *) 29 | if not disableGcOptimizations then 30 | Gc.set 31 | { 32 | (Gc.get ()) with 33 | (* 16MB is a reasonable value for minor heap size *) 34 | minor_heap_size = 2 * 1024 * 1024; 35 | (* Double the minor heap *) 36 | major_heap_increment = 2 * 1024 * 1024; 37 | (* Reasonable high value to reduce major GC frequency *) 38 | space_overhead = 500; 39 | (* Disable compaction *) 40 | max_overhead = 1_000_000; 41 | }; 42 | 43 | let module IO1 = (val getIOModule img1Path) in 44 | let module IO2 = (val getIOModule img2Path) in 45 | let module Diff = MakeDiff (IO1) (IO2) in 46 | let img1 = IO1.loadImage img1Path in 47 | let img2 = IO2.loadImage img2Path in 48 | let { diff; exitCode } = 49 | Diff.diff img1 img2 ~outputDiffMask ~threshold ~failOnLayoutChange 50 | ~antialiasing ~ignoreRegions ~diffLines 51 | ~diffPixel: 52 | (match Color.ofHexString diffColorHex with 53 | | Some c -> c 54 | | None -> redPixel) 55 | () 56 | |> Print.printDiffResult toEmitStdoutParsableString 57 | |> function 58 | | Layout -> { diff = None; exitCode = 21 } 59 | | Pixel (diffOutput, diffCount, stdoutParsableString, _) when diffCount = 0 60 | -> 61 | { exitCode = 0; diff = Some diffOutput } 62 | | Pixel (diffOutput, diffCount, diffPercentage, _) -> 63 | diffPath |> Option.iter (IO1.saveImage diffOutput); 64 | { exitCode = 22; diff = Some diffOutput } 65 | in 66 | IO1.freeImage img1; 67 | IO2.freeImage img2; 68 | (match diff with 69 | | Some output when outputDiffMask -> IO1.freeImage output 70 | | _ -> ()); 71 | 72 | (* Gc.print_stat stdout; *) 73 | exit exitCode 74 | -------------------------------------------------------------------------------- /bin/ODiffBin.ml: -------------------------------------------------------------------------------- 1 | open Cmdliner 2 | open Term 3 | open Arg 4 | 5 | let diffPath = 6 | value & pos 2 (some string) None 7 | & info [] ~docv:"DIFF" ~doc:"Optional Diff output path (.png only)" 8 | 9 | let base = 10 | value & pos 0 file "" & info [] ~docv:"BASE" ~doc:"Path to base image" 11 | 12 | let comp = 13 | value & pos 1 file "" 14 | & info [] ~docv:"COMPARING" ~doc:"Path to comparing image" 15 | 16 | let threshold = 17 | value & opt float 0.1 18 | & info [ "t"; "threshold" ] ~docv:"THRESHOLD" 19 | ~doc:"Color difference threshold (from 0 to 1). Less more precise." 20 | 21 | let diffMask = 22 | value & flag 23 | & info [ "dm"; "diff-mask" ] ~docv:"DIFF_IMAGE" 24 | ~doc:"Output only changed pixel over transparent background." 25 | 26 | let failOnLayout = 27 | value & flag 28 | & info [ "fail-on-layout" ] ~docv:"FAIL_ON_LAYOUT" 29 | ~doc: 30 | "Do not compare images and produce output if images layout is \ 31 | different." 32 | 33 | let parsableOutput = 34 | value & flag 35 | & info [ "parsable-stdout" ] ~docv:"PARSABLE_OUTPUT" 36 | ~doc:"Stdout parsable output" 37 | 38 | let diffColor = 39 | value & opt string "" 40 | & info [ "diff-color" ] 41 | ~doc: 42 | "Color used to highlight different pixels in the output (in hex format \ 43 | e.g. #cd2cc9)." 44 | 45 | let antialiasing = 46 | value & flag 47 | & info [ "aa"; "antialiasing" ] 48 | ~doc: 49 | "With this flag enabled, antialiased pixels are not counted to the \ 50 | diff of an image" 51 | 52 | let diffLines = 53 | value & flag 54 | & info [ "output-diff-lines" ] 55 | ~doc: 56 | "With this flag enabled, output result in case of different images \ 57 | will output lines for all the different pixels" 58 | 59 | let disableGcOptimizations = 60 | value & flag 61 | & info [ "reduce-ram-usage" ] 62 | ~doc: 63 | "With this flag enabled odiff will use less memory, but will be slower \ 64 | in some cases." 65 | 66 | let ignoreRegions = 67 | value 68 | & opt 69 | (list ~sep:',' (t2 ~sep:'-' (t2 ~sep:':' int int) (t2 ~sep:':' int int))) 70 | [] 71 | & info [ "i"; "ignore" ] 72 | ~doc: 73 | "An array of regions to ignore in the diff. One region looks like \ 74 | \"x1:y1-x2:y2\". Multiple regions are separated with a ','." 75 | 76 | let cmd = 77 | const Main.main $ base $ comp $ diffPath $ threshold $ diffMask $ failOnLayout 78 | $ diffColor $ parsableOutput $ antialiasing $ ignoreRegions $ diffLines 79 | $ disableGcOptimizations 80 | 81 | let version = 82 | match Build_info.V1.version () with 83 | | None -> "dev" 84 | | Some v -> Build_info.V1.Version.to_string v 85 | 86 | let info = 87 | let man = 88 | [ 89 | `S Manpage.s_description; 90 | `P "$(tname) is the fastest pixel-by-pixel image comparison tool."; 91 | `P "Supported image types: .png, .jpg, .jpeg, .tiff"; 92 | ] 93 | in 94 | Cmd.info "odiff" ~version ~doc:"Find difference between 2 images." 95 | ~exits: 96 | [ 97 | Cmd.Exit.info 0 ~doc:"on image match"; 98 | Cmd.Exit.info 21 ~doc:"on layout diff when --fail-on-layout"; 99 | Cmd.Exit.info 22 ~doc:"on image pixel difference"; 100 | ] 101 | ~man 102 | 103 | let cmd = Cmd.v info cmd 104 | let () = Cmd.eval cmd |> Stdlib.exit 105 | -------------------------------------------------------------------------------- /bin/Print.ml: -------------------------------------------------------------------------------- 1 | open Odiff.Diff 2 | 3 | let esc = "\027[" 4 | let red = esc ^ "31m" 5 | let green = esc ^ "32m" 6 | let bold = esc ^ "1m" 7 | let dim = esc ^ "2m" 8 | let reset = esc ^ "0m" 9 | 10 | let printDiffResult makeParsableOutput result = 11 | (match (result, makeParsableOutput) with 12 | | Layout, true -> () 13 | | Layout, false -> 14 | Format.printf "%s%sFailure!%s Images have different layout.\n" red bold 15 | reset 16 | | Pixel (_output, diffCount, diffPercentage, stack), true 17 | when not (Stack.is_empty stack) -> 18 | Int.to_string diffCount ^ ";" 19 | ^ Float.to_string diffPercentage 20 | ^ ";" 21 | ^ (stack 22 | |> Stack.fold (fun acc line -> (line |> Int.to_string) ^ "," ^ acc) "") 23 | |> print_endline 24 | | Pixel (_output, diffCount, diffPercentage, _), true -> 25 | Int.to_string diffCount ^ ";" ^ Float.to_string diffPercentage 26 | |> print_endline 27 | | Pixel (_output, diffCount, _percentage, _lines), false when diffCount == 0 28 | -> 29 | Format.printf 30 | "%s%sSuccess!%s Images are equal.\n%sNo diff output created.%s\n" green 31 | bold reset dim reset 32 | | Pixel (_output, diffCount, diffPercentage, _lines), false -> 33 | Format.printf 34 | "%s%sFailure!%s Images are different.\n\ 35 | Different pixels: %s%s%i (%f%%)%s\n" 36 | red bold reset red bold diffCount diffPercentage reset); 37 | 38 | result 39 | -------------------------------------------------------------------------------- /bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name ODiffBin) 3 | (public_name ODiffBin) 4 | (package odiff) 5 | (flags 6 | (:standard -w -27)) 7 | (libraries odiff-core odiff-io cmdliner dune-build-info)) 8 | 9 | (env 10 | (dev 11 | (flags (:standard -w +42)) 12 | (ocamlopt_flags (:standard -S))) 13 | (release 14 | (ocamlopt_flags (:standard -no-g -O3 -rounds 5 -unbox-closures -inline 200 -inline-max-depth 7 -unbox-closures-factor 50)))) 15 | 16 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.8) 2 | (name odiff) 3 | 4 | ; Warning: The flag set for these foreign sources overrides the `:standard` set 5 | ; of flags. However the flags in this standard set are still added to the 6 | ; compiler arguments by Dune. This might cause unexpected issues. You can 7 | ; disable this warning by defining the option `(use_standard_c_and_cxx_flags )` 8 | ; in your `dune-project` file. Setting this option to `true` will 9 | ; effectively prevent Dune from silently adding c-flags to the compiler 10 | ; arguments which is the new recommended behaviour. 11 | (use_standard_c_and_cxx_flags false) 12 | 13 | (generate_opam_files true) 14 | 15 | (version 3.2.1) 16 | (source (github dmtrKovalenko/odiff)) 17 | (license MIT) 18 | (authors "Dmitriy Kovalenko") 19 | (maintainers "https://dmtrkovalenko.dev" "dmtr.kovalenko@outlook.com") 20 | 21 | (package 22 | (name odiff) 23 | (synopsis "CLI for comparing images pixel-by-pixel") 24 | (depends 25 | odiff-core 26 | odiff-io 27 | (dune-build-info (>= 3.16.0)) 28 | (cmdliner (= 1.3.0)) 29 | ) 30 | ) 31 | 32 | (package 33 | (name odiff-core) 34 | (synopsis "Pixel-by-pixel image difference algorithm") 35 | (depends 36 | dune 37 | ocaml 38 | ) 39 | ) 40 | 41 | (package 42 | (name odiff-io) 43 | (synopsis "Ready to use io for odiff-core") 44 | (depends 45 | dune 46 | odiff-core 47 | ocaml 48 | (dune-configurator (>= 3.16.0)) 49 | ) 50 | ) 51 | 52 | (package 53 | (name odiff-tests) 54 | (synopsis "Internal package for integration tests of odiff") 55 | (depends 56 | (alcotest (= 1.8.0)) 57 | odiff-core 58 | ) 59 | ) 60 | -------------------------------------------------------------------------------- /images/2x2-ff0000ff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/2x2-ff0000ff.png -------------------------------------------------------------------------------- /images/README.md: -------------------------------------------------------------------------------- 1 | # Benchmark studio 2 | 3 | Run the benchmark with any difference tool you want. This guide shows how to run the benchmark via [hyperfine](https://github.com/sharkdp/hyperfine). Make sure it is installed. 4 | 5 | ## Install the tools to benchmarks 6 | 7 | Make sure you installed `odiff`, `pixelmatch` and `ImageMagick` (at least for this guide) 8 | 9 | ## Install the benchmark tool 10 | 11 | We are using [hyperfine](https://github.com/sharkdp/hyperfine) to run performance tests. Follow the installation instructions on their [github](https://github.com/sharkdp/hyperfine). On MacOS you can do: 12 | 13 | ``` 14 | brew install hyperfine 15 | ``` 16 | 17 | ## Run the benchmark 18 | 19 | > Make sure that provided benchmark results were achieved on MacBook Pro 16, MacOS 11 BigSur beta. 20 | 21 | Simple benchmark that compares [4k water image](./water-4k.png) with [corrupted one](./water-4k-2.png). 22 | 23 | ``` 24 | hyperfine -i 'odiff water-4k.png water-4k-2.png water-diff.png' 'pixelmatch water-4k.png water-4k-2.png water-diff.png' 'compare water-4k.png water-4k-2.png -compose src water-diff.png' 25 | 26 | ``` 27 | 28 | ## Generate markdown results 29 | 30 | This generates markdown output that is displayed in README. 31 | 32 | ``` 33 | hyperfine -i --export-markdown 'pixelmatch www.cypress.io-1.png www.cypress.io.png www.cypress-diff.png' 'compare www.cypress.io-1.png www.cypress.io.png -compose src diff-magick.png' 'ODiffBin www.cypress.io-1.png www.cypress.io.png www.cypress-diff.png' 34 | ``` 35 | -------------------------------------------------------------------------------- /images/__debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/__debug.png -------------------------------------------------------------------------------- /images/benchmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/benchmarks.png -------------------------------------------------------------------------------- /images/donkey-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/donkey-2.png -------------------------------------------------------------------------------- /images/donkey-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/donkey-diff.png -------------------------------------------------------------------------------- /images/donkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/donkey.png -------------------------------------------------------------------------------- /images/extreme-alpha-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/extreme-alpha-1.png -------------------------------------------------------------------------------- /images/extreme-alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/extreme-alpha.png -------------------------------------------------------------------------------- /images/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/out.png -------------------------------------------------------------------------------- /images/test_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/test_map.png -------------------------------------------------------------------------------- /images/test_map_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/test_map_1.png -------------------------------------------------------------------------------- /images/tiger-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/tiger-2.jpg -------------------------------------------------------------------------------- /images/tiger-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/tiger-diff.png -------------------------------------------------------------------------------- /images/tiger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/tiger.jpg -------------------------------------------------------------------------------- /images/water-4k-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/water-4k-2.png -------------------------------------------------------------------------------- /images/water-4k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/water-4k.png -------------------------------------------------------------------------------- /images/water-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/water-diff.png -------------------------------------------------------------------------------- /images/www.cypress-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/www.cypress-diff.png -------------------------------------------------------------------------------- /images/www.cypress.io-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/www.cypress.io-1.png -------------------------------------------------------------------------------- /images/www.cypress.io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/images/www.cypress.io.png -------------------------------------------------------------------------------- /io/ODiffIO.ml: -------------------------------------------------------------------------------- 1 | module Bmp = Bmp 2 | module Png = Png 3 | module Jpg = Jpg 4 | module Tiff = Tiff 5 | -------------------------------------------------------------------------------- /io/bmp/Bmp.ml: -------------------------------------------------------------------------------- 1 | open Bigarray 2 | 3 | type data = (int32, int32_elt, c_layout) Array1.t 4 | 5 | module IO : Odiff.ImageIO.ImageIO = struct 6 | type t = data 7 | 8 | let loadImage filename : t Odiff.ImageIO.img = 9 | let width, height, data = ReadBmp.load filename in 10 | { width; height; image = data } 11 | 12 | let readRawPixel ~(x : int) ~(y : int) (img : t Odiff.ImageIO.img) = 13 | let image : data = img.image in 14 | Array1.unsafe_get image ((y * img.width) + x) 15 | [@@inline] 16 | 17 | let readRawPixelAtOffset offset (img : t Odiff.ImageIO.img) = 18 | Array1.unsafe_get img.image offset 19 | [@@inline] 20 | 21 | let setImgColor ~x ~y color (img : t Odiff.ImageIO.img) = 22 | let image : data = img.image in 23 | Array1.unsafe_set image ((y * img.width) + x) color 24 | [@@inline] 25 | 26 | let saveImage (img : t Odiff.ImageIO.img) filename = 27 | WritePng.write_png_bigarray filename img.image img.width img.height 28 | 29 | let freeImage (img : t Odiff.ImageIO.img) = () 30 | 31 | let makeSameAsLayout (img : t Odiff.ImageIO.img) = 32 | let image = Array1.create int32 c_layout (Array1.dim img.image) in 33 | { img with image } 34 | end 35 | -------------------------------------------------------------------------------- /io/bmp/Bmp.mli: -------------------------------------------------------------------------------- 1 | type data = (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t 2 | 3 | module IO : Odiff.ImageIO.ImageIO 4 | -------------------------------------------------------------------------------- /io/bmp/ReadBmp.ml: -------------------------------------------------------------------------------- 1 | open Bigarray 2 | 3 | type bicompression = BI_RGB | BI_RLE8 | BI_RLE4 | BI_BITFIELDS 4 | type bibitcount = Monochrome | Color16 | Color256 | ColorRGB | ColorRGBA 5 | 6 | type bitmapfileheader = { 7 | bfType : int; 8 | bfSize : int; 9 | bfReserved1 : int; 10 | bfReserved2 : int; 11 | bfOffBits : int; 12 | } 13 | 14 | type bitmapinfoheader = { 15 | biSize : int; 16 | biWidth : int; 17 | biHeight : int; 18 | biPlanes : int; 19 | biBitCount : bibitcount; 20 | biCompression : bicompression; 21 | biSizeImage : int; 22 | biXPelsPerMeter : int; 23 | biYPelsPerMeter : int; 24 | biClrUsed : int; 25 | biClrImportant : int; 26 | } 27 | 28 | type bmp = { 29 | bmpFileHeader : bitmapfileheader; 30 | bmpInfoHeader : bitmapinfoheader; 31 | bmpBytes : (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t; 32 | } 33 | 34 | let bytes_read = ref 0 35 | 36 | let read_byte ic = 37 | incr bytes_read; 38 | input_byte ic 39 | 40 | let skip_byte ic = 41 | incr bytes_read; 42 | ignore (input_byte ic) 43 | 44 | let read_16bit ic = 45 | let b0 = read_byte ic in 46 | let b1 = read_byte ic in 47 | (b1 lsl 8) + b0 48 | 49 | let read_32bit ic = 50 | let b0 = read_byte ic in 51 | let b1 = read_byte ic in 52 | let b2 = read_byte ic in 53 | let b3 = read_byte ic in 54 | (b3 lsl 24) + (b2 lsl 16) + (b1 lsl 8) + b0 55 | 56 | let read_bit_count ic = 57 | match read_16bit ic with 58 | | 1 -> Monochrome 59 | | 4 -> Color16 60 | | 8 -> Color256 61 | | 24 -> ColorRGB 62 | | 32 -> ColorRGBA 63 | | n -> failwith ("invalid number of colors in bitmap: " ^ string_of_int n) 64 | 65 | let read_compression ic = 66 | match read_32bit ic with 67 | | 0 -> BI_RGB 68 | | 1 -> BI_RLE8 69 | | 2 -> BI_RLE4 70 | | 3 -> BI_BITFIELDS 71 | | n -> failwith ("invalid compression: " ^ string_of_int n) 72 | 73 | let load_bitmapfileheader ic = 74 | let bfType = read_16bit ic in 75 | if bfType <> 19778 then failwith "Invalid bitmap file"; 76 | let bfSize = read_32bit ic in 77 | let bfReserved1 = read_16bit ic in 78 | let bfReserved2 = read_16bit ic in 79 | let bfOffBits = read_32bit ic in 80 | { bfType; bfSize; bfReserved1; bfReserved2; bfOffBits } 81 | 82 | let load_bitmapinfoheader ic = 83 | try 84 | let biSize = read_32bit ic in 85 | let biWidth = read_32bit ic in 86 | let biHeight = read_32bit ic in 87 | let biPlanes = read_16bit ic in 88 | let biBitCount = read_bit_count ic in 89 | let biCompression = read_compression ic in 90 | let biSizeImage = read_32bit ic in 91 | let biXPelsPerMeter = read_32bit ic in 92 | let biYPelsPerMeter = read_32bit ic in 93 | let biClrUsed = read_32bit ic in 94 | let biClrImportant = read_32bit ic in 95 | { 96 | biSize; 97 | biWidth; 98 | biHeight; 99 | biPlanes; 100 | biBitCount; 101 | biCompression; 102 | biSizeImage; 103 | biXPelsPerMeter; 104 | biYPelsPerMeter; 105 | biClrUsed; 106 | biClrImportant; 107 | } 108 | with Failure s as e -> 109 | prerr_endline s; 110 | raise e 111 | 112 | let load_image24data bih ic = 113 | let data = Array1.create int32 c_layout (bih.biWidth * bih.biHeight) in 114 | let pad = (4 - (bih.biWidth * 3 mod 4)) land 3 in 115 | for y = bih.biHeight - 1 downto 0 do 116 | for x = 0 to bih.biWidth - 1 do 117 | let b = (read_byte ic land 255) lsl 16 in 118 | let g = (read_byte ic land 255) lsl 8 in 119 | let r = (read_byte ic land 255) lsl 0 in 120 | let a = 255 lsl 24 in 121 | Array1.set data 122 | ((y * bih.biWidth) + x) 123 | (Int32.of_int (a lor b lor g lor r)) 124 | done; 125 | for _j = 0 to pad - 1 do 126 | skip_byte ic 127 | done 128 | done; 129 | data 130 | 131 | let load_image32data bih ic = 132 | let data = Array1.create int32 c_layout (bih.biWidth * bih.biHeight) in 133 | for y = bih.biHeight - 1 downto 0 do 134 | for x = 0 to bih.biWidth - 1 do 135 | let b = (read_byte ic land 255) lsl 16 in 136 | let g = (read_byte ic land 255) lsl 8 in 137 | let r = (read_byte ic land 255) lsl 0 in 138 | let a = (read_byte ic land 255) lsl 24 in 139 | Array1.set data 140 | ((y * bih.biWidth) + x) 141 | (Int32.of_int (a lor b lor g lor r)) 142 | done 143 | done; 144 | data 145 | 146 | let load_imagedata bih ic = 147 | match bih.biBitCount with 148 | | ColorRGBA -> load_image32data bih ic 149 | | ColorRGB -> load_image24data bih ic 150 | | _ -> failwith "BMP has to be 32 or 24 bit" 151 | 152 | let skip_to ic n = 153 | while !bytes_read <> n do 154 | skip_byte ic 155 | done 156 | 157 | let read_bmp ic = 158 | bytes_read := 0; 159 | let bmpFileHeader = load_bitmapfileheader ic in 160 | let bmpInfoHeader = load_bitmapinfoheader ic in 161 | skip_to ic bmpFileHeader.bfOffBits; 162 | let bmpBytes = load_imagedata bmpInfoHeader ic in 163 | { bmpFileHeader; bmpInfoHeader; bmpBytes } 164 | 165 | let load filename = 166 | let ic = open_in_bin filename in 167 | let bmp = read_bmp ic in 168 | close_in ic; 169 | (bmp.bmpInfoHeader.biWidth, bmp.bmpInfoHeader.biHeight, bmp.bmpBytes) 170 | -------------------------------------------------------------------------------- /io/bmp/ReadBmp.mli: -------------------------------------------------------------------------------- 1 | val load : 2 | string -> 3 | int * int * (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t 4 | -------------------------------------------------------------------------------- /io/bmp/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name Bmp) 3 | (public_name odiff-io.bmp) 4 | (flags 5 | (-w -40 -w +26)) 6 | (libraries odiff-core WritePng)) 7 | -------------------------------------------------------------------------------- /io/config/discover.ml: -------------------------------------------------------------------------------- 1 | module C = Configurator.V1 2 | 3 | exception Pkg_Config_Resolution_Failed of string 4 | 5 | type pkg_config_result = { cflags : string list; libs : string list } 6 | type process_result = { exit_code : int; stdout : string; stderr : string } 7 | 8 | let run_process ~env prog args = 9 | let stdout_fn = Filename.temp_file "stdout" ".tmp" in 10 | let stderr_fn = Filename.temp_file "stderr" ".tmp" in 11 | let openfile f = 12 | Unix.openfile f [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o666 13 | in 14 | let stdout = openfile stdout_fn in 15 | let stderr = openfile stderr_fn in 16 | let stdin, stdin_w = Unix.pipe () in 17 | Unix.close stdin_w; 18 | 19 | let pid = 20 | match env with 21 | | [] -> 22 | Unix.create_process prog 23 | (Array.of_list (prog :: args)) 24 | stdin stdout stderr 25 | | _ -> 26 | let env_array = Array.of_list env in 27 | Unix.create_process_env prog 28 | (Array.of_list (prog :: args)) 29 | env_array stdin stdout stderr 30 | in 31 | 32 | Unix.close stdin; 33 | Unix.close stdout; 34 | Unix.close stderr; 35 | 36 | let _, status = Unix.waitpid [] pid in 37 | 38 | let read_file filename = 39 | try 40 | let ic = open_in filename in 41 | let n = in_channel_length ic in 42 | let s = really_input_string ic n in 43 | close_in ic; 44 | s 45 | with 46 | | Sys_error msg -> Printf.sprintf "Error reading file %s: %s" filename msg 47 | | End_of_file -> 48 | Printf.sprintf "Unexpected end of file while reading %s" filename 49 | in 50 | 51 | let stdout_content = read_file stdout_fn in 52 | let stderr_content = read_file stderr_fn in 53 | 54 | Sys.remove stdout_fn; 55 | Sys.remove stderr_fn; 56 | 57 | let exit_code = 58 | match status with 59 | | Unix.WEXITED code -> code 60 | | Unix.WSIGNALED signal -> 61 | raise 62 | (Pkg_Config_Resolution_Failed 63 | (Printf.sprintf "Process killed by signal %d" signal)) 64 | | Unix.WSTOPPED signal -> 65 | raise 66 | (Pkg_Config_Resolution_Failed 67 | (Printf.sprintf "Process stopped by signal %d" signal)) 68 | in 69 | 70 | { exit_code; stdout = stdout_content; stderr = stderr_content } 71 | 72 | let run_pkg_config _c lib = 73 | let pkg_config_path = Sys.getenv "PKG_CONFIG_PATH" in 74 | Printf.printf "Use PKG_CONFIG_PATH: %s\n" pkg_config_path; 75 | 76 | let env = [ "PKG_CONFIG_PATH=" ^ pkg_config_path ] in 77 | let c_flags_result = run_process ~env "pkg-config" [ "--cflags"; lib ] in 78 | let libs_result = run_process ~env "pkg-config" [ "--libs"; lib ] in 79 | 80 | if c_flags_result.exit_code = 0 && libs_result.exit_code == 0 then 81 | { 82 | cflags = c_flags_result.stdout |> C.Flags.extract_blank_separated_words; 83 | libs = libs_result.stdout |> C.Flags.extract_blank_separated_words; 84 | } 85 | else 86 | let std_errors = 87 | String.concat "\n" [ c_flags_result.stderr; libs_result.stderr ] 88 | in 89 | 90 | raise (Pkg_Config_Resolution_Failed std_errors) 91 | 92 | let get_flags_from_env_or_run_pkg_conifg c ~env ~lib = 93 | match (Sys.getenv_opt (env ^ "_CFLAGS"), Sys.getenv_opt (env ^ "_LIBS")) with 94 | | Some cflags, Some lib -> 95 | { 96 | cflags = String.trim cflags |> C.Flags.extract_blank_separated_words; 97 | libs = lib |> C.Flags.extract_blank_separated_words; 98 | } 99 | | None, None -> run_pkg_config c lib 100 | | _ -> 101 | let err = "Missing CFLAGS or LIB env vars for " ^ env in 102 | raise (Pkg_Config_Resolution_Failed err) 103 | 104 | let c_flags_to_ocaml_opt_flags flags = 105 | flags 106 | |> List.filter_map (function 107 | | opt when String.starts_with opt ~prefix:"-l" -> Some [ "-cclib"; opt ] 108 | | _ -> None) 109 | |> List.flatten 110 | 111 | let () = 112 | C.main ~name:"odiff-c-lib-packae-resolver" (fun c -> 113 | let png_config = 114 | get_flags_from_env_or_run_pkg_conifg c ~env:"LIBPNG" 115 | ~lib:"libspng_static" 116 | in 117 | let tiff_config = 118 | get_flags_from_env_or_run_pkg_conifg c ~lib:"libtiff-4" ~env:"LIBTIFF" 119 | in 120 | let jpeg_config = 121 | get_flags_from_env_or_run_pkg_conifg c ~lib:"libturbojpeg" 122 | ~env:"LIBJPEG" 123 | in 124 | 125 | C.Flags.write_sexp "png_c_flags.sexp" png_config.cflags; 126 | C.Flags.write_sexp "png_c_library_flags.sexp" png_config.libs; 127 | C.Flags.write_sexp "png_write_c_flags.sexp" png_config.cflags; 128 | C.Flags.write_sexp "png_write_c_library_flags.sexp" png_config.libs; 129 | C.Flags.write_sexp "png_c_flags.sexp" png_config.cflags; 130 | C.Flags.write_sexp "jpg_c_flags.sexp" jpeg_config.cflags; 131 | C.Flags.write_sexp "jpg_c_library_flags.sexp" jpeg_config.libs; 132 | C.Flags.write_sexp "tiff_c_flags.sexp" tiff_config.cflags; 133 | C.Flags.write_sexp "tiff_c_library_flags.sexp" tiff_config.libs; 134 | 135 | (* this are ocamlopt flags that need to link c libs to ocaml compiler *) 136 | let png_ocamlopt_flags = png_config.libs |> c_flags_to_ocaml_opt_flags in 137 | C.Flags.write_sexp "png_write_flags.sexp" png_ocamlopt_flags; 138 | C.Flags.write_sexp "png_flags.sexp" png_ocamlopt_flags; 139 | 140 | jpeg_config.libs |> c_flags_to_ocaml_opt_flags 141 | |> C.Flags.write_sexp "jpg_flags.sexp"; 142 | tiff_config.libs |> c_flags_to_ocaml_opt_flags 143 | |> C.Flags.write_sexp "tiff_flags.sexp") 144 | -------------------------------------------------------------------------------- /io/config/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name discover) 3 | (libraries dune-configurator)) 4 | -------------------------------------------------------------------------------- /io/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name ODiffIO) 3 | (public_name odiff-io) 4 | (flags 5 | (-w -40 -w +26)) 6 | (libraries odiff-io.png odiff-io.jpg odiff-io.bmp odiff-io.tiff)) 7 | -------------------------------------------------------------------------------- /io/jpg/Jpg.ml: -------------------------------------------------------------------------------- 1 | open Bigarray 2 | 3 | type data = (int32, int32_elt, c_layout) Array1.t 4 | 5 | module IO = struct 6 | type t = { data : data } 7 | 8 | let loadImage filename : t Odiff.ImageIO.img = 9 | let width, height, data = ReadJpg.read_jpeg_image filename in 10 | { width; height; image = { data } } 11 | 12 | let readRawPixel ~x ~y (img : t Odiff.ImageIO.img) = 13 | (Array1.unsafe_get img.image.data ((y * img.width) + x) [@inline.always]) 14 | [@@inline] 15 | 16 | let readRawPixelAtOffset offset (img : t Odiff.ImageIO.img) = 17 | Array1.unsafe_get img.image.data offset 18 | [@@inline] 19 | 20 | let setImgColor ~x ~y color (img : t Odiff.ImageIO.img) = 21 | Array1.unsafe_set img.image.data ((y * img.width) + x) color 22 | 23 | let saveImage (img : t Odiff.ImageIO.img) filename = 24 | WritePng.write_png_bigarray filename img.image.data img.width img.height 25 | 26 | let freeImage (img : t Odiff.ImageIO.img) = () 27 | 28 | let makeSameAsLayout (img : t Odiff.ImageIO.img) = 29 | let data = Array1.create int32 c_layout (Array1.dim img.image.data) in 30 | { img with image = { data } } 31 | end 32 | -------------------------------------------------------------------------------- /io/jpg/Jpg.mli: -------------------------------------------------------------------------------- 1 | type data = (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t 2 | 3 | module IO : Odiff.ImageIO.ImageIO 4 | -------------------------------------------------------------------------------- /io/jpg/ReadJpg.c: -------------------------------------------------------------------------------- 1 | #define CAML_NAME_SPACE 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | CAMLprim value 14 | read_jpeg_file_to_tuple(value file) 15 | { 16 | CAMLparam1(file); 17 | CAMLlocal2(res, ba); 18 | 19 | size_t size; 20 | struct jpeg_error_mgr jerr; 21 | struct jpeg_decompress_struct cinfo; 22 | 23 | jpeg_create_decompress(&cinfo); 24 | cinfo.err = jpeg_std_error(&jerr); 25 | 26 | const char *filename = String_val(file); 27 | FILE *fp = fopen(filename, "rb"); 28 | if (!fp) { 29 | caml_failwith("opening input file failed!"); 30 | } 31 | if (fseek(fp, 0, SEEK_END) < 0 || ((size = ftell(fp)) < 0) || fseek(fp, 0, SEEK_SET) < 0) { 32 | fclose(fp); 33 | caml_failwith("determining input file size failed"); 34 | } 35 | if (size == 0) { 36 | fclose(fp); 37 | caml_failwith("Input file contains no data"); 38 | } 39 | 40 | jpeg_stdio_src(&cinfo, fp); 41 | jpeg_read_header(&cinfo, TRUE); 42 | jpeg_start_decompress(&cinfo); 43 | 44 | uint32_t width = cinfo.output_width; 45 | uint32_t height = cinfo.output_height; 46 | uint32_t channels = cinfo.output_components; 47 | 48 | JDIMENSION stride = width * channels; 49 | JSAMPARRAY temp_buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, stride, 1); 50 | 51 | int buffer_size = width * height * 4; 52 | intnat dims[1] = {buffer_size}; 53 | ba = caml_ba_alloc(CAML_BA_UINT8 | CAML_BA_C_LAYOUT | CAML_BA_MANAGED, 1, NULL, dims); 54 | uint8_t *image_buffer = (uint8_t *)Caml_ba_data_val(ba); 55 | 56 | while (cinfo.output_scanline < height) { 57 | jpeg_read_scanlines(&cinfo, temp_buffer, 1); 58 | 59 | unsigned int k = (cinfo.output_scanline - 1) * 4 * width; 60 | unsigned int j = 0; 61 | for(unsigned int i = 0; i < 4 * width; i += 4) { 62 | image_buffer[k + i] = temp_buffer[0][j]; 63 | image_buffer[k + i + 1] = temp_buffer[0][j + 1]; 64 | image_buffer[k + i + 2] = temp_buffer[0][j + 2]; 65 | image_buffer[k + i + 3] = 255; 66 | 67 | j += 3; 68 | } 69 | } 70 | 71 | jpeg_finish_decompress(&cinfo); 72 | jpeg_destroy_decompress(&cinfo); 73 | fclose(fp); 74 | 75 | res = caml_alloc_tuple(3); 76 | Store_field(res, 0, Val_int(width)); 77 | Store_field(res, 1, Val_int(height)); 78 | Store_field(res, 2, ba); 79 | 80 | CAMLreturn(res); 81 | } 82 | -------------------------------------------------------------------------------- /io/jpg/ReadJpg.ml: -------------------------------------------------------------------------------- 1 | external read_jpeg_image : 2 | string -> 3 | int * int * (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t 4 | = "read_jpeg_file_to_tuple" 5 | -------------------------------------------------------------------------------- /io/jpg/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name Jpg) 3 | (public_name odiff-io.jpg) 4 | (flags 5 | (-w -40 -w +26) 6 | (:include jpg_flags.sexp)) 7 | (foreign_stubs 8 | (language c) 9 | (names ReadJpg) 10 | (flags 11 | (:include jpg_c_flags.sexp) -O3)) 12 | (c_library_flags 13 | (:include jpg_c_library_flags.sexp)) 14 | (libraries odiff-core WritePng)) 15 | 16 | (rule 17 | (targets jpg_flags.sexp jpg_c_flags.sexp jpg_c_library_flags.sexp) 18 | (action 19 | (run ../config/discover.exe))) 20 | -------------------------------------------------------------------------------- /io/png/Png.ml: -------------------------------------------------------------------------------- 1 | open Bigarray 2 | open Odiff.ImageIO 3 | 4 | type data = (int32, int32_elt, c_layout) Array1.t 5 | 6 | module IO : Odiff.ImageIO.ImageIO = struct 7 | type t = data 8 | 9 | let readRawPixelAtOffset offset (img : t Odiff.ImageIO.img) = 10 | Array1.unsafe_get img.image offset 11 | [@@inline always] 12 | 13 | let readRawPixel ~(x : int) ~(y : int) (img : t Odiff.ImageIO.img) = 14 | let image : data = img.image in 15 | Array1.unsafe_get image ((y * img.width) + x) 16 | [@@inline always] 17 | 18 | let setImgColor ~x ~y color (img : t Odiff.ImageIO.img) = 19 | let image : data = img.image in 20 | Array1.unsafe_set image ((y * img.width) + x) color 21 | 22 | let loadImage filename : t Odiff.ImageIO.img = 23 | let width, height, data = ReadPng.read_png_image filename in 24 | { width; height; image = data } 25 | 26 | let saveImage (img : t Odiff.ImageIO.img) filename = 27 | WritePng.write_png_bigarray filename img.image img.width img.height 28 | 29 | let freeImage (img : t Odiff.ImageIO.img) = () 30 | 31 | let makeSameAsLayout (img : t Odiff.ImageIO.img) = 32 | let image = Array1.create int32 c_layout (Array1.dim img.image) in 33 | { img with image } 34 | end 35 | -------------------------------------------------------------------------------- /io/png/ReadPng.c: -------------------------------------------------------------------------------- 1 | #define CAML_NAME_SPACE 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | CAMLprim value read_png_file(value file) { 12 | CAMLparam1(file); 13 | CAMLlocal2(res, ba); 14 | 15 | int result = 0; 16 | FILE *png; 17 | spng_ctx *ctx = NULL; 18 | const char *filename = String_val(file); 19 | 20 | png = fopen(filename, "rb"); 21 | if (png == NULL) { 22 | caml_failwith("error opening input file"); 23 | } 24 | 25 | ctx = spng_ctx_new(0); 26 | if (ctx == NULL) { 27 | fclose(png); 28 | caml_failwith("spng_ctx_new() failed"); 29 | } 30 | 31 | /* Ignore and don't calculate chunk CRC's */ 32 | spng_set_crc_action(ctx, SPNG_CRC_USE, SPNG_CRC_USE); 33 | 34 | /* Set memory usage limits for storing standard and unknown chunks, 35 | this is important when reading untrusted files! */ 36 | size_t limit = 1024 * 1024 * 64; 37 | spng_set_chunk_limits(ctx, limit, limit); 38 | 39 | /* Set source PNG */ 40 | spng_set_png_file(ctx, png); 41 | 42 | struct spng_ihdr ihdr; 43 | result = spng_get_ihdr(ctx, &ihdr); 44 | 45 | if (result) { 46 | spng_ctx_free(ctx); 47 | fclose(png); 48 | caml_failwith("spng_get_ihdr() error!"); 49 | } 50 | 51 | size_t out_size; 52 | result = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &out_size); 53 | if (result) { 54 | spng_ctx_free(ctx); 55 | fclose(png); 56 | caml_failwith(spng_strerror(result)); 57 | }; 58 | 59 | ba = caml_ba_alloc(CAML_BA_UINT8 | CAML_BA_C_LAYOUT | CAML_BA_MANAGED, 1, 60 | NULL, &out_size); 61 | unsigned char *out = (unsigned char *)Caml_ba_data_val(ba); 62 | 63 | result = 64 | spng_decode_image(ctx, out, out_size, SPNG_FMT_RGBA8, SPNG_DECODE_TRNS); 65 | if (result) { 66 | spng_ctx_free(ctx); 67 | fclose(png); 68 | caml_failwith(spng_strerror(result)); 69 | } 70 | 71 | spng_ctx_free(ctx); 72 | fclose(png); 73 | 74 | res = caml_alloc_tuple(3); 75 | Store_field(res, 0, Val_int(ihdr.width)); 76 | Store_field(res, 1, Val_int(ihdr.height)); 77 | Store_field(res, 2, ba); 78 | 79 | CAMLreturn(res); 80 | } 81 | -------------------------------------------------------------------------------- /io/png/ReadPng.ml: -------------------------------------------------------------------------------- 1 | external read_png_image : 2 | string -> 3 | int * int * (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t 4 | = "read_png_file" 5 | -------------------------------------------------------------------------------- /io/png/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name Png) 3 | (public_name odiff-io.png) 4 | (flags 5 | (-w -40 -w +26) 6 | (:include png_flags.sexp)) 7 | (foreign_stubs 8 | (language c) 9 | (names ReadPng) 10 | (flags 11 | (:include png_c_flags.sexp) -O3)) 12 | (c_library_flags 13 | (:include png_c_library_flags.sexp)) 14 | (libraries odiff-core WritePng)) 15 | 16 | (rule 17 | (targets png_flags.sexp png_c_flags.sexp png_c_library_flags.sexp) 18 | (action 19 | (run ../config/discover.exe))) 20 | -------------------------------------------------------------------------------- /io/png_write/WritePng.c: -------------------------------------------------------------------------------- 1 | #define CAML_NAME_SPACE 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | char *concat(const char *s1, const char *s2) 15 | { 16 | char *result = malloc(strlen(s1) + strlen(s2) + 1); // +1 for the null-terminator 17 | 18 | if (result == NULL) 19 | { 20 | caml_failwith("Can not concat strings"); 21 | } 22 | 23 | strcpy(result, s1); 24 | strcat(result, s2); 25 | 26 | return result; 27 | } 28 | 29 | value write_png_bigarray(value filename_val, value bigarray, value width_val, value height_val) 30 | { 31 | CAMLparam4(filename_val, bigarray, width_val, height_val); 32 | 33 | int width = Int_val(width_val); 34 | int height = Int_val(height_val); 35 | const char *data = Caml_ba_data_val(bigarray); 36 | const char *filename = String_val(filename_val); 37 | 38 | FILE *fp; 39 | if ((fp = fopen(filename, "wb")) == NULL) 40 | { 41 | char *err = strerror(errno); 42 | char *message = concat("Can not write diff output. fopen error: ", err); 43 | 44 | caml_failwith(message); 45 | 46 | free(err); 47 | free(message); 48 | } 49 | 50 | int result = 0; 51 | 52 | uint8_t bit_depth = 8; 53 | uint8_t color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; 54 | uint8_t compression_method = 0; 55 | uint8_t filter_method = SPNG_FILTER_NONE; 56 | uint8_t interlace_method = SPNG_INTERLACE_NONE; 57 | 58 | size_t out_size = width * height * 4; 59 | size_t out_width = out_size / height; 60 | 61 | spng_ctx *ctx = spng_ctx_new(SPNG_CTX_ENCODER); 62 | struct spng_ihdr ihdr = { 63 | width, 64 | height, 65 | bit_depth, 66 | color_type, 67 | compression_method, 68 | filter_method, 69 | interlace_method, 70 | }; 71 | 72 | result = spng_set_ihdr(ctx, &ihdr); 73 | if (result) 74 | { 75 | spng_ctx_free(ctx); 76 | fclose(fp); 77 | caml_failwith(spng_strerror(result)); 78 | } 79 | 80 | result = spng_set_option(ctx, SPNG_FILTER_CHOICE, SPNG_DISABLE_FILTERING); 81 | if (result) 82 | { 83 | spng_ctx_free(ctx); 84 | fclose(fp); 85 | caml_failwith(spng_strerror(result)); 86 | } 87 | 88 | result = spng_set_png_file(ctx, fp); 89 | if (result) 90 | { 91 | fclose(fp); 92 | spng_ctx_free(ctx); 93 | caml_failwith(spng_strerror(result)); 94 | } 95 | 96 | result = spng_encode_image(ctx, 0, 0, SPNG_FMT_PNG, SPNG_ENCODE_PROGRESSIVE); 97 | 98 | if (result) 99 | { 100 | fclose(fp); 101 | spng_ctx_free(ctx); 102 | caml_failwith(spng_strerror(result)); 103 | } 104 | 105 | for (int i = 0; i < ihdr.height; i++) 106 | { 107 | const char *row = data + out_width * i; 108 | result = spng_encode_scanline(ctx, row, out_width); 109 | if (result) 110 | break; 111 | } 112 | 113 | if (result != SPNG_EOI) 114 | { 115 | spng_ctx_free(ctx); 116 | fclose(fp); 117 | caml_failwith(spng_strerror(result)); 118 | } 119 | 120 | spng_ctx_free(ctx); 121 | fclose(fp); 122 | 123 | CAMLreturn(Val_unit); 124 | } 125 | -------------------------------------------------------------------------------- /io/png_write/WritePng.ml: -------------------------------------------------------------------------------- 1 | open Bigarray 2 | 3 | external write_png_bigarray : 4 | string -> (int32, int32_elt, c_layout) Array1.t -> int -> int -> unit 5 | = "write_png_bigarray" 6 | [@@noalloc] 7 | -------------------------------------------------------------------------------- /io/png_write/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name WritePng) 3 | (public_name odiff-io.png_write) 4 | (flags 5 | (-w -40 -w +26) 6 | (:include png_write_flags.sexp)) 7 | (foreign_stubs 8 | (language c) 9 | (names WritePng) 10 | (flags 11 | (:include png_write_c_flags.sexp) -O3)) 12 | (c_library_flags 13 | (:include png_write_c_library_flags.sexp))) 14 | 15 | (rule 16 | (targets 17 | png_write_flags.sexp 18 | png_write_c_flags.sexp 19 | png_write_c_library_flags.sexp) 20 | (action 21 | (run ../config/discover.exe))) 22 | -------------------------------------------------------------------------------- /io/tiff/ReadTiff.c: -------------------------------------------------------------------------------- 1 | #define CAML_NAME_SPACE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifndef _WIN32 10 | #include 11 | 12 | CAMLprim value read_tiff_file_to_tuple(value file) { 13 | CAMLparam1(file); 14 | CAMLlocal2(res, ba); 15 | 16 | const char *filename = String_val(file); 17 | int width; 18 | int height; 19 | 20 | TIFF *image; 21 | 22 | if (!(image = TIFFOpen(filename, "r"))) { 23 | caml_failwith("opening input file failed!"); 24 | } 25 | 26 | TIFFGetField(image, TIFFTAG_IMAGEWIDTH, &width); 27 | TIFFGetField(image, TIFFTAG_IMAGELENGTH, &height); 28 | 29 | int buffer_size = width * height; 30 | 31 | intnat dims[1] = {buffer_size}; 32 | ba = caml_ba_alloc(CAML_BA_INT32 | CAML_BA_C_LAYOUT | CAML_BA_MANAGED, 1, 33 | NULL, dims); 34 | 35 | uint32_t *buffer = (uint32_t *)Caml_ba_data_val(ba); 36 | 37 | if (!(TIFFReadRGBAImageOriented(image, width, height, buffer, 38 | ORIENTATION_TOPLEFT, 0))) { 39 | TIFFClose(image); 40 | caml_failwith("reading input file failed"); 41 | } 42 | 43 | TIFFClose(image); 44 | 45 | res = caml_alloc_tuple(3); 46 | Store_field(res, 0, Val_int(width)); 47 | Store_field(res, 1, Val_int(height)); 48 | Store_field(res, 2, ba); 49 | 50 | CAMLreturn(res); 51 | } 52 | #else 53 | CAMLprim value read_tiff_file_to_tuple(value file) { 54 | caml_failwith("Tiff files are not supported on Windows platform"); 55 | } 56 | #endif 57 | -------------------------------------------------------------------------------- /io/tiff/ReadTiff.ml: -------------------------------------------------------------------------------- 1 | external load : 2 | string -> 3 | int * int * (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t 4 | = "read_tiff_file_to_tuple" 5 | -------------------------------------------------------------------------------- /io/tiff/Tiff.ml: -------------------------------------------------------------------------------- 1 | open Bigarray 2 | open Odiff.ImageIO 3 | 4 | type data = (int32, int32_elt, c_layout) Array1.t 5 | 6 | module IO : ImageIO = struct 7 | type buffer 8 | type t = { data : data } 9 | 10 | let loadImage filename : t Odiff.ImageIO.img = 11 | let width, height, data = ReadTiff.load filename in 12 | { width; height; image = { data } } 13 | 14 | let readRawPixel ~x ~y img = 15 | (Array1.unsafe_get img.image.data ((y * img.width) + x) [@inline.always]) 16 | 17 | let readRawPixelAtOffset offset img = Array1.unsafe_get img.image.data offset 18 | [@@inline.always] 19 | 20 | let setImgColor ~x ~y color (img : t Odiff.ImageIO.img) = 21 | Array1.unsafe_set img.image.data ((y * img.width) + x) color 22 | 23 | let saveImage (img : t Odiff.ImageIO.img) filename = 24 | WritePng.write_png_bigarray filename img.image.data img.width img.height 25 | 26 | let freeImage (img : t Odiff.ImageIO.img) = () 27 | 28 | let makeSameAsLayout (img : t Odiff.ImageIO.img) = 29 | let data = Array1.create int32 c_layout (Array1.dim img.image.data) in 30 | { img with image = { data } } 31 | end 32 | -------------------------------------------------------------------------------- /io/tiff/Tiff.mli: -------------------------------------------------------------------------------- 1 | type data = (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t 2 | 3 | module IO : Odiff.ImageIO.ImageIO 4 | -------------------------------------------------------------------------------- /io/tiff/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name Tiff) 3 | (public_name odiff-io.tiff) 4 | (flags 5 | (-w -40 -w +26) 6 | (:include tiff_flags.sexp)) 7 | (foreign_stubs 8 | (language c) 9 | (names ReadTiff) 10 | (flags 11 | (:include tiff_c_flags.sexp) -O3)) 12 | (c_library_flags 13 | (:include tiff_c_library_flags.sexp)) 14 | (libraries odiff-core WritePng)) 15 | 16 | (rule 17 | (targets tiff_flags.sexp tiff_c_flags.sexp tiff_c_library_flags.sexp) 18 | (action 19 | (run ../config/discover.exe))) 20 | -------------------------------------------------------------------------------- /npm_package/bin/odiff.exe: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.error("odiff: seems like a binary executable for your OS wasn't linked. Please verify that postinstsall script run successfully") 4 | process.exit(1); 5 | -------------------------------------------------------------------------------- /npm_package/odiff.d.ts: -------------------------------------------------------------------------------- 1 | export type ODiffOptions = Partial<{ 2 | /** Color used to highlight different pixels in the output (in hex format e.g. #cd2cc9). */ 3 | diffColor: string; 4 | /** Output full diff image. */ 5 | outputDiffMask: boolean; 6 | /** Do not compare images and produce output if images layout is different. */ 7 | failOnLayoutDiff: boolean; 8 | /** Return { match: false, reason: '...' } instead of throwing error if file is missing. */ 9 | noFailOnFsErrors: boolean; 10 | /** Color difference threshold (from 0 to 1). Less more precise. */ 11 | threshold: number; 12 | /** If this is true, antialiased pixels are not counted to the diff of an image */ 13 | antialiasing: boolean; 14 | /** If `true` reason: "pixel-diff" output will contain the set of line indexes containing different pixels */ 15 | captureDiffLines: boolean; 16 | /** If `true` odiff will use less memory but will be slower with larger images */ 17 | reduceRamUsage: boolean; 18 | /** An array of regions to ignore in the diff. */ 19 | ignoreRegions: Array<{ 20 | x1: number; 21 | y1: number; 22 | x2: number; 23 | y2: number; 24 | }>; 25 | }>; 26 | 27 | declare function compare( 28 | basePath: string, 29 | comparePath: string, 30 | diffPath: string, 31 | options?: ODiffOptions 32 | ): Promise< 33 | | { match: true } 34 | | { match: false; reason: "layout-diff" } 35 | | { 36 | match: false; 37 | reason: "pixel-diff"; 38 | /** Amount of different pixels */ 39 | diffCount: number; 40 | /** Percentage of different pixels in the whole image */ 41 | diffPercentage: number; 42 | /** Individual line indexes containing different pixels. Guaranteed to be ordered and distinct. */ 43 | diffLines?: number[]; 44 | } 45 | | { 46 | match: false; 47 | reason: "file-not-exists"; 48 | /** Errored file path */ 49 | file: string; 50 | } 51 | >; 52 | 53 | export { compare }; 54 | -------------------------------------------------------------------------------- /npm_package/odiff.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const path = require("path"); 3 | const { execFile } = require("child_process"); 4 | 5 | function optionsToArgs(options) { 6 | let argArray = ["--parsable-stdout"]; 7 | 8 | if (!options) { 9 | return argArray; 10 | } 11 | 12 | const setArgWithValue = (name, value) => { 13 | argArray.push(`--${name}=${value.toString()}`); 14 | }; 15 | 16 | const setFlag = (name, value) => { 17 | if (value) { 18 | argArray.push(`--${name}`); 19 | } 20 | }; 21 | 22 | Object.entries(options).forEach((optionEntry) => { 23 | /** 24 | * @type {[keyof import('./odiff').ODiffOptions, unknown]} 25 | * @ts-expect-error */ 26 | const [option, value] = optionEntry; 27 | 28 | switch (option) { 29 | case "failOnLayoutDiff": 30 | setFlag("fail-on-layout", value); 31 | break; 32 | 33 | case "outputDiffMask": 34 | setFlag("diff-mask", value); 35 | break; 36 | 37 | case "threshold": 38 | setArgWithValue("threshold", value); 39 | break; 40 | 41 | case "diffColor": 42 | setArgWithValue("diff-color", value); 43 | break; 44 | 45 | case "antialiasing": 46 | setFlag("antialiasing", value); 47 | break; 48 | 49 | case "captureDiffLines": 50 | setFlag("output-diff-lines", value); 51 | break; 52 | 53 | case "reduceRamUsage": 54 | setFlag("reduce-ram-usage", value); 55 | break; 56 | 57 | case "ignoreRegions": { 58 | const regions = value 59 | .map( 60 | (region) => `${region.x1}:${region.y1}-${region.x2}:${region.y2}` 61 | ) 62 | .join(","); 63 | 64 | setArgWithValue("ignore", regions); 65 | break; 66 | } 67 | } 68 | }); 69 | 70 | return argArray; 71 | } 72 | 73 | /** @type {(stdout: string) => Partial<{ diffCount: number, diffPercentage: number, diffLines: number[] }>} */ 74 | function parsePixelDiffStdout(stdout) { 75 | try { 76 | const parts = stdout.split(";"); 77 | 78 | if (parts.length === 2) { 79 | const [diffCount, diffPercentage] = parts; 80 | 81 | return { 82 | diffCount: parseInt(diffCount), 83 | diffPercentage: parseFloat(diffPercentage), 84 | }; 85 | } else if (parts.length === 3) { 86 | const [diffCount, diffPercentage, linesPart] = parts; 87 | 88 | return { 89 | diffCount: parseInt(diffCount), 90 | diffPercentage: parseFloat(diffPercentage), 91 | diffLines: linesPart.split(",").flatMap((line) => { 92 | let parsedInt = parseInt(line); 93 | 94 | return isNaN(parsedInt) ? [] : parsedInt; 95 | }), 96 | }; 97 | } else { 98 | throw new Error(`Weird pixel diff stdout: ${stdout}`); 99 | } 100 | } catch (e) { 101 | console.warn( 102 | "Can't parse output from internal process. Please submit an issue at https://github.com/dmtrKovalenko/odiff/issues/new with the following stacktrace:", 103 | e 104 | ); 105 | } 106 | 107 | return {}; 108 | } 109 | 110 | const CMD_BIN_HELPER_MSG = 111 | "Usage: odiff [OPTION]... [BASE] [COMPARING] [DIFF]\nTry `odiff --help' for more information.\n"; 112 | 113 | const NO_FILE_ODIFF_ERROR_REGEX = /no\s+'([^']+)'\s+file\s+or\s+directory/; 114 | 115 | async function compare(basePath, comparePath, diffOutput, options = {}) { 116 | return new Promise((resolve, reject) => { 117 | let producedStdout, producedStdError; 118 | 119 | const binaryPath = 120 | options && options.__binaryPath 121 | ? options.__binaryPath 122 | : path.join(__dirname, "bin", "odiff.exe"); 123 | 124 | execFile( 125 | binaryPath, 126 | [basePath, comparePath, diffOutput, ...optionsToArgs(options)], 127 | (_, stdout, stderr) => { 128 | producedStdout = stdout; 129 | producedStdError = stderr; 130 | } 131 | ).on("close", (code) => { 132 | switch (code) { 133 | case 0: 134 | resolve({ match: true }); 135 | break; 136 | case 21: 137 | resolve({ match: false, reason: "layout-diff" }); 138 | break; 139 | case 22: 140 | resolve({ 141 | match: false, 142 | reason: "pixel-diff", 143 | ...parsePixelDiffStdout(producedStdout), 144 | }); 145 | break; 146 | case 124: 147 | /** @type string */ 148 | const originalErrorMessage = ( 149 | producedStdError || "Invalid Argument Exception" 150 | ).replace(CMD_BIN_HELPER_MSG, ""); 151 | 152 | const noFileOrDirectoryMatches = originalErrorMessage.match( 153 | NO_FILE_ODIFF_ERROR_REGEX 154 | ); 155 | 156 | if (options.noFailOnFsErrors && noFileOrDirectoryMatches?.[1]) { 157 | resolve({ 158 | match: false, 159 | reason: "file-not-exists", 160 | file: noFileOrDirectoryMatches[1], 161 | }); 162 | } else { 163 | reject(new TypeError(originalErrorMessage)); 164 | } 165 | break; 166 | 167 | default: 168 | reject( 169 | new Error( 170 | (producedStdError || producedStdout).replace( 171 | CMD_BIN_HELPER_MSG, 172 | "" 173 | ) 174 | ) 175 | ); 176 | break; 177 | } 178 | }); 179 | }); 180 | } 181 | 182 | module.exports = { 183 | compare, 184 | }; 185 | -------------------------------------------------------------------------------- /npm_package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odiff-bin", 3 | "version": "3.2.1", 4 | "author": "Dmitriy Kovalenko ", 5 | "license": "MIT", 6 | "description": "The fastest image difference tool in the world", 7 | "scripts": { 8 | "postinstall": "node ./post_install.js" 9 | }, 10 | "bin": { 11 | "odiff": "bin/odiff.exe" 12 | }, 13 | "types": "odiff.d.ts", 14 | "main": "odiff.js", 15 | "keywords": [ 16 | "visual-regression", 17 | "pixelmatch", 18 | "image", 19 | "comparison", 20 | "diff" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /npm_package/post_install.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | 5 | const binaries = { 6 | 'linux-x64': 'odiff-linux-x64.exe', 7 | 'linux-arm64': 'odiff-linux-arm64.exe', 8 | 'darwin-arm64': 'odiff-macos-arm64.exe', 9 | 'darwin-x64': 'odiff-macos-x64.exe', 10 | 'win32-x64': 'odiff-windows-x64.exe', 11 | }; 12 | 13 | const platform = os.platform(); 14 | const arch = os.arch(); 15 | 16 | let binaryKey = `${platform}-${arch}`; 17 | if (platform === 'win32' && arch === 'x64') { 18 | binaryKey = 'win32-x64'; 19 | } 20 | 21 | const binaryFile = binaries[binaryKey]; 22 | 23 | if (!binaryFile) { 24 | console.error(`odiff: Sorry your platform or architecture is not supported. Here is a list of supported binaries: ${Object.keys(binaries).join(', ')}`); 25 | process.exit(1); 26 | } 27 | 28 | const sourcePath = path.join(__dirname, 'raw_binaries', binaryFile); 29 | const destPath = path.join(__dirname, 'bin', 'odiff.exe'); 30 | 31 | try { 32 | fs.copyFileSync(sourcePath, destPath); 33 | fs.chmodSync(destPath, 0o755); 34 | } catch (err) { 35 | console.error(`odiff: failed to copy and link the binary file: ${err}`); 36 | process.exit(1); 37 | } 38 | -------------------------------------------------------------------------------- /npm_package/raw_binaries/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/npm_package/raw_binaries/.gitkeep -------------------------------------------------------------------------------- /odiff-core.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | version: "3.2.1" 4 | synopsis: "Pixel-by-pixel image difference algorithm" 5 | maintainer: ["https://dmtrkovalenko.dev" "dmtr.kovalenko@outlook.com"] 6 | authors: ["Dmitriy Kovalenko"] 7 | license: "MIT" 8 | homepage: "https://github.com/dmtrKovalenko/odiff" 9 | bug-reports: "https://github.com/dmtrKovalenko/odiff/issues" 10 | depends: [ 11 | "dune" {>= "2.8"} 12 | "ocaml" 13 | "odoc" {with-doc} 14 | ] 15 | build: [ 16 | ["dune" "subst"] {dev} 17 | [ 18 | "dune" 19 | "build" 20 | "-p" 21 | name 22 | "-j" 23 | jobs 24 | "@install" 25 | "@runtest" {with-test} 26 | "@doc" {with-doc} 27 | ] 28 | ] 29 | dev-repo: "git+https://github.com/dmtrKovalenko/odiff.git" 30 | -------------------------------------------------------------------------------- /odiff-io.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | version: "3.2.1" 4 | synopsis: "Ready to use io for odiff-core" 5 | maintainer: ["https://dmtrkovalenko.dev" "dmtr.kovalenko@outlook.com"] 6 | authors: ["Dmitriy Kovalenko"] 7 | license: "MIT" 8 | homepage: "https://github.com/dmtrKovalenko/odiff" 9 | bug-reports: "https://github.com/dmtrKovalenko/odiff/issues" 10 | depends: [ 11 | "dune" {>= "2.8"} 12 | "odiff-core" 13 | "ocaml" 14 | "dune-configurator" {>= "3.16.0"} 15 | "odoc" {with-doc} 16 | ] 17 | build: [ 18 | ["dune" "subst"] {dev} 19 | [ 20 | "dune" 21 | "build" 22 | "-p" 23 | name 24 | "-j" 25 | jobs 26 | "@install" 27 | "@runtest" {with-test} 28 | "@doc" {with-doc} 29 | ] 30 | ] 31 | dev-repo: "git+https://github.com/dmtrKovalenko/odiff.git" 32 | -------------------------------------------------------------------------------- /odiff-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/odiff-logo-dark.png -------------------------------------------------------------------------------- /odiff-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/odiff-logo-light.png -------------------------------------------------------------------------------- /odiff-tests.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | version: "3.2.1" 4 | synopsis: "Internal package for integration tests of odiff" 5 | maintainer: ["https://dmtrkovalenko.dev" "dmtr.kovalenko@outlook.com"] 6 | authors: ["Dmitriy Kovalenko"] 7 | license: "MIT" 8 | homepage: "https://github.com/dmtrKovalenko/odiff" 9 | bug-reports: "https://github.com/dmtrKovalenko/odiff/issues" 10 | depends: [ 11 | "dune" {>= "2.8"} 12 | "alcotest" {= "1.8.0"} 13 | "odiff-core" 14 | "odoc" {with-doc} 15 | ] 16 | build: [ 17 | ["dune" "subst"] {dev} 18 | [ 19 | "dune" 20 | "build" 21 | "-p" 22 | name 23 | "-j" 24 | jobs 25 | "@install" 26 | "@runtest" {with-test} 27 | "@doc" {with-doc} 28 | ] 29 | ] 30 | dev-repo: "git+https://github.com/dmtrKovalenko/odiff.git" 31 | -------------------------------------------------------------------------------- /odiff.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | version: "3.2.1" 4 | synopsis: "CLI for comparing images pixel-by-pixel" 5 | maintainer: ["https://dmtrkovalenko.dev" "dmtr.kovalenko@outlook.com"] 6 | authors: ["Dmitriy Kovalenko"] 7 | license: "MIT" 8 | homepage: "https://github.com/dmtrKovalenko/odiff" 9 | bug-reports: "https://github.com/dmtrKovalenko/odiff/issues" 10 | depends: [ 11 | "dune" {>= "2.8"} 12 | "odiff-core" 13 | "odiff-io" 14 | "dune-build-info" {>= "3.16.0"} 15 | "cmdliner" {= "1.3.0"} 16 | "odoc" {with-doc} 17 | ] 18 | build: [ 19 | ["dune" "subst"] {dev} 20 | [ 21 | "dune" 22 | "build" 23 | "-p" 24 | name 25 | "-j" 26 | jobs 27 | "@install" 28 | "@runtest" {with-test} 29 | "@doc" {with-doc} 30 | ] 31 | ] 32 | dev-repo: "git+https://github.com/dmtrKovalenko/odiff.git" 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odiff-bin", 3 | "version": "3.2.1", 4 | "author": "Dmitriy Kovalenko ", 5 | "license": "MIT", 6 | "description": "The fastest image difference tool in the world", 7 | "scripts": { 8 | "test": "ava" 9 | }, 10 | "keywords": [ 11 | "visual-regression", 12 | "pixelmatch", 13 | "image", 14 | "comparison", 15 | "diff" 16 | ], 17 | "devDependencies": { 18 | "ava": "^6.1.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | ava: 12 | specifier: ^6.1.3 13 | version: 6.1.3 14 | 15 | packages: 16 | 17 | '@mapbox/node-pre-gyp@1.0.11': 18 | resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} 19 | hasBin: true 20 | 21 | '@nodelib/fs.scandir@2.1.5': 22 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 23 | engines: {node: '>= 8'} 24 | 25 | '@nodelib/fs.stat@2.0.5': 26 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 27 | engines: {node: '>= 8'} 28 | 29 | '@nodelib/fs.walk@1.2.8': 30 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 31 | engines: {node: '>= 8'} 32 | 33 | '@rollup/pluginutils@4.2.1': 34 | resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} 35 | engines: {node: '>= 8.0.0'} 36 | 37 | '@sindresorhus/merge-streams@2.3.0': 38 | resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} 39 | engines: {node: '>=18'} 40 | 41 | '@vercel/nft@0.26.5': 42 | resolution: {integrity: sha512-NHxohEqad6Ra/r4lGknO52uc/GrWILXAMs1BB4401GTqww0fw1bAqzpG1XHuDO+dprg4GvsD9ZLLSsdo78p9hQ==} 43 | engines: {node: '>=16'} 44 | hasBin: true 45 | 46 | abbrev@1.1.1: 47 | resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} 48 | 49 | acorn-import-attributes@1.9.5: 50 | resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} 51 | peerDependencies: 52 | acorn: ^8 53 | 54 | acorn-walk@8.3.3: 55 | resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} 56 | engines: {node: '>=0.4.0'} 57 | 58 | acorn@8.12.1: 59 | resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} 60 | engines: {node: '>=0.4.0'} 61 | hasBin: true 62 | 63 | agent-base@6.0.2: 64 | resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} 65 | engines: {node: '>= 6.0.0'} 66 | 67 | ansi-regex@5.0.1: 68 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 69 | engines: {node: '>=8'} 70 | 71 | ansi-regex@6.0.1: 72 | resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} 73 | engines: {node: '>=12'} 74 | 75 | ansi-styles@4.3.0: 76 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 77 | engines: {node: '>=8'} 78 | 79 | ansi-styles@6.2.1: 80 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 81 | engines: {node: '>=12'} 82 | 83 | aproba@2.0.0: 84 | resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} 85 | 86 | are-we-there-yet@2.0.0: 87 | resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} 88 | engines: {node: '>=10'} 89 | deprecated: This package is no longer supported. 90 | 91 | argparse@1.0.10: 92 | resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} 93 | 94 | array-find-index@1.0.2: 95 | resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} 96 | engines: {node: '>=0.10.0'} 97 | 98 | arrgv@1.0.2: 99 | resolution: {integrity: sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==} 100 | engines: {node: '>=8.0.0'} 101 | 102 | arrify@3.0.0: 103 | resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} 104 | engines: {node: '>=12'} 105 | 106 | async-sema@3.1.1: 107 | resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} 108 | 109 | ava@6.1.3: 110 | resolution: {integrity: sha512-tkKbpF1pIiC+q09wNU9OfyTDYZa8yuWvU2up3+lFJ3lr1RmnYh2GBpPwzYUEB0wvTPIUysGjcZLNZr7STDviRA==} 111 | engines: {node: ^18.18 || ^20.8 || ^21 || ^22} 112 | hasBin: true 113 | peerDependencies: 114 | '@ava/typescript': '*' 115 | peerDependenciesMeta: 116 | '@ava/typescript': 117 | optional: true 118 | 119 | balanced-match@1.0.2: 120 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 121 | 122 | bindings@1.5.0: 123 | resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} 124 | 125 | blueimp-md5@2.19.0: 126 | resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} 127 | 128 | brace-expansion@1.1.11: 129 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 130 | 131 | braces@3.0.3: 132 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 133 | engines: {node: '>=8'} 134 | 135 | callsites@4.2.0: 136 | resolution: {integrity: sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==} 137 | engines: {node: '>=12.20'} 138 | 139 | cbor@9.0.2: 140 | resolution: {integrity: sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==} 141 | engines: {node: '>=16'} 142 | 143 | chalk@5.3.0: 144 | resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} 145 | engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 146 | 147 | chownr@2.0.0: 148 | resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} 149 | engines: {node: '>=10'} 150 | 151 | chunkd@2.0.1: 152 | resolution: {integrity: sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==} 153 | 154 | ci-info@4.0.0: 155 | resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} 156 | engines: {node: '>=8'} 157 | 158 | ci-parallel-vars@1.0.1: 159 | resolution: {integrity: sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==} 160 | 161 | cli-truncate@4.0.0: 162 | resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} 163 | engines: {node: '>=18'} 164 | 165 | cliui@8.0.1: 166 | resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} 167 | engines: {node: '>=12'} 168 | 169 | code-excerpt@4.0.0: 170 | resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} 171 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 172 | 173 | color-convert@2.0.1: 174 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 175 | engines: {node: '>=7.0.0'} 176 | 177 | color-name@1.1.4: 178 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 179 | 180 | color-support@1.1.3: 181 | resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} 182 | hasBin: true 183 | 184 | common-path-prefix@3.0.0: 185 | resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} 186 | 187 | concat-map@0.0.1: 188 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 189 | 190 | concordance@5.0.4: 191 | resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==} 192 | engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'} 193 | 194 | console-control-strings@1.1.0: 195 | resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} 196 | 197 | convert-to-spaces@2.0.1: 198 | resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} 199 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 200 | 201 | currently-unhandled@0.4.1: 202 | resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} 203 | engines: {node: '>=0.10.0'} 204 | 205 | date-time@3.1.0: 206 | resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} 207 | engines: {node: '>=6'} 208 | 209 | debug@4.3.6: 210 | resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} 211 | engines: {node: '>=6.0'} 212 | peerDependencies: 213 | supports-color: '*' 214 | peerDependenciesMeta: 215 | supports-color: 216 | optional: true 217 | 218 | delegates@1.0.0: 219 | resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} 220 | 221 | detect-libc@2.0.3: 222 | resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} 223 | engines: {node: '>=8'} 224 | 225 | emittery@1.0.3: 226 | resolution: {integrity: sha512-tJdCJitoy2lrC2ldJcqN4vkqJ00lT+tOWNT1hBJjO/3FDMJa5TTIiYGCKGkn/WfCyOzUMObeohbVTj00fhiLiA==} 227 | engines: {node: '>=14.16'} 228 | 229 | emoji-regex@10.3.0: 230 | resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} 231 | 232 | emoji-regex@8.0.0: 233 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 234 | 235 | escalade@3.1.2: 236 | resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} 237 | engines: {node: '>=6'} 238 | 239 | escape-string-regexp@2.0.0: 240 | resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} 241 | engines: {node: '>=8'} 242 | 243 | escape-string-regexp@5.0.0: 244 | resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} 245 | engines: {node: '>=12'} 246 | 247 | esprima@4.0.1: 248 | resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} 249 | engines: {node: '>=4'} 250 | hasBin: true 251 | 252 | estree-walker@2.0.2: 253 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 254 | 255 | esutils@2.0.3: 256 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 257 | engines: {node: '>=0.10.0'} 258 | 259 | fast-diff@1.3.0: 260 | resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} 261 | 262 | fast-glob@3.3.2: 263 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 264 | engines: {node: '>=8.6.0'} 265 | 266 | fastq@1.17.1: 267 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 268 | 269 | figures@6.1.0: 270 | resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} 271 | engines: {node: '>=18'} 272 | 273 | file-uri-to-path@1.0.0: 274 | resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} 275 | 276 | fill-range@7.1.1: 277 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 278 | engines: {node: '>=8'} 279 | 280 | find-up-simple@1.0.0: 281 | resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} 282 | engines: {node: '>=18'} 283 | 284 | fs-minipass@2.1.0: 285 | resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} 286 | engines: {node: '>= 8'} 287 | 288 | fs.realpath@1.0.0: 289 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 290 | 291 | gauge@3.0.2: 292 | resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} 293 | engines: {node: '>=10'} 294 | deprecated: This package is no longer supported. 295 | 296 | get-caller-file@2.0.5: 297 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 298 | engines: {node: 6.* || 8.* || >= 10.*} 299 | 300 | get-east-asian-width@1.2.0: 301 | resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} 302 | engines: {node: '>=18'} 303 | 304 | glob-parent@5.1.2: 305 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 306 | engines: {node: '>= 6'} 307 | 308 | glob@7.2.3: 309 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 310 | deprecated: Glob versions prior to v9 are no longer supported 311 | 312 | globby@14.0.2: 313 | resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} 314 | engines: {node: '>=18'} 315 | 316 | graceful-fs@4.2.11: 317 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 318 | 319 | has-unicode@2.0.1: 320 | resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} 321 | 322 | https-proxy-agent@5.0.1: 323 | resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} 324 | engines: {node: '>= 6'} 325 | 326 | ignore-by-default@2.1.0: 327 | resolution: {integrity: sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==} 328 | engines: {node: '>=10 <11 || >=12 <13 || >=14'} 329 | 330 | ignore@5.3.2: 331 | resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 332 | engines: {node: '>= 4'} 333 | 334 | imurmurhash@0.1.4: 335 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 336 | engines: {node: '>=0.8.19'} 337 | 338 | indent-string@5.0.0: 339 | resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} 340 | engines: {node: '>=12'} 341 | 342 | inflight@1.0.6: 343 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 344 | deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 345 | 346 | inherits@2.0.4: 347 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 348 | 349 | irregular-plurals@3.5.0: 350 | resolution: {integrity: sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==} 351 | engines: {node: '>=8'} 352 | 353 | is-extglob@2.1.1: 354 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 355 | engines: {node: '>=0.10.0'} 356 | 357 | is-fullwidth-code-point@3.0.0: 358 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 359 | engines: {node: '>=8'} 360 | 361 | is-fullwidth-code-point@4.0.0: 362 | resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} 363 | engines: {node: '>=12'} 364 | 365 | is-glob@4.0.3: 366 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 367 | engines: {node: '>=0.10.0'} 368 | 369 | is-number@7.0.0: 370 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 371 | engines: {node: '>=0.12.0'} 372 | 373 | is-plain-object@5.0.0: 374 | resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} 375 | engines: {node: '>=0.10.0'} 376 | 377 | is-promise@4.0.0: 378 | resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} 379 | 380 | is-unicode-supported@2.0.0: 381 | resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} 382 | engines: {node: '>=18'} 383 | 384 | js-string-escape@1.0.1: 385 | resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} 386 | engines: {node: '>= 0.8'} 387 | 388 | js-yaml@3.14.1: 389 | resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} 390 | hasBin: true 391 | 392 | load-json-file@7.0.1: 393 | resolution: {integrity: sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==} 394 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 395 | 396 | lodash@4.17.21: 397 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 398 | 399 | make-dir@3.1.0: 400 | resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} 401 | engines: {node: '>=8'} 402 | 403 | matcher@5.0.0: 404 | resolution: {integrity: sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==} 405 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 406 | 407 | md5-hex@3.0.1: 408 | resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} 409 | engines: {node: '>=8'} 410 | 411 | memoize@10.0.0: 412 | resolution: {integrity: sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==} 413 | engines: {node: '>=18'} 414 | 415 | merge2@1.4.1: 416 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 417 | engines: {node: '>= 8'} 418 | 419 | micromatch@4.0.8: 420 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 421 | engines: {node: '>=8.6'} 422 | 423 | mimic-function@5.0.1: 424 | resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} 425 | engines: {node: '>=18'} 426 | 427 | minimatch@3.1.2: 428 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 429 | 430 | minipass@3.3.6: 431 | resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} 432 | engines: {node: '>=8'} 433 | 434 | minipass@5.0.0: 435 | resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} 436 | engines: {node: '>=8'} 437 | 438 | minizlib@2.1.2: 439 | resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} 440 | engines: {node: '>= 8'} 441 | 442 | mkdirp@1.0.4: 443 | resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} 444 | engines: {node: '>=10'} 445 | hasBin: true 446 | 447 | ms@2.1.2: 448 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 449 | 450 | ms@2.1.3: 451 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 452 | 453 | node-fetch@2.7.0: 454 | resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} 455 | engines: {node: 4.x || >=6.0.0} 456 | peerDependencies: 457 | encoding: ^0.1.0 458 | peerDependenciesMeta: 459 | encoding: 460 | optional: true 461 | 462 | node-gyp-build@4.8.1: 463 | resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} 464 | hasBin: true 465 | 466 | nofilter@3.1.0: 467 | resolution: {integrity: sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==} 468 | engines: {node: '>=12.19'} 469 | 470 | nopt@5.0.0: 471 | resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} 472 | engines: {node: '>=6'} 473 | hasBin: true 474 | 475 | npmlog@5.0.1: 476 | resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} 477 | deprecated: This package is no longer supported. 478 | 479 | object-assign@4.1.1: 480 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 481 | engines: {node: '>=0.10.0'} 482 | 483 | once@1.4.0: 484 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 485 | 486 | p-map@7.0.2: 487 | resolution: {integrity: sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==} 488 | engines: {node: '>=18'} 489 | 490 | package-config@5.0.0: 491 | resolution: {integrity: sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==} 492 | engines: {node: '>=18'} 493 | 494 | parse-ms@4.0.0: 495 | resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} 496 | engines: {node: '>=18'} 497 | 498 | path-is-absolute@1.0.1: 499 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 500 | engines: {node: '>=0.10.0'} 501 | 502 | path-type@5.0.0: 503 | resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} 504 | engines: {node: '>=12'} 505 | 506 | picomatch@2.3.1: 507 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 508 | engines: {node: '>=8.6'} 509 | 510 | picomatch@3.0.1: 511 | resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==} 512 | engines: {node: '>=10'} 513 | 514 | plur@5.1.0: 515 | resolution: {integrity: sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==} 516 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 517 | 518 | pretty-ms@9.1.0: 519 | resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} 520 | engines: {node: '>=18'} 521 | 522 | queue-microtask@1.2.3: 523 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 524 | 525 | readable-stream@3.6.2: 526 | resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} 527 | engines: {node: '>= 6'} 528 | 529 | require-directory@2.1.1: 530 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 531 | engines: {node: '>=0.10.0'} 532 | 533 | resolve-cwd@3.0.0: 534 | resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} 535 | engines: {node: '>=8'} 536 | 537 | resolve-from@5.0.0: 538 | resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} 539 | engines: {node: '>=8'} 540 | 541 | reusify@1.0.4: 542 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 543 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 544 | 545 | rimraf@3.0.2: 546 | resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} 547 | deprecated: Rimraf versions prior to v4 are no longer supported 548 | hasBin: true 549 | 550 | run-parallel@1.2.0: 551 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 552 | 553 | safe-buffer@5.2.1: 554 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 555 | 556 | semver@6.3.1: 557 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 558 | hasBin: true 559 | 560 | semver@7.6.3: 561 | resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} 562 | engines: {node: '>=10'} 563 | hasBin: true 564 | 565 | serialize-error@7.0.1: 566 | resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} 567 | engines: {node: '>=10'} 568 | 569 | set-blocking@2.0.0: 570 | resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} 571 | 572 | signal-exit@3.0.7: 573 | resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 574 | 575 | signal-exit@4.1.0: 576 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 577 | engines: {node: '>=14'} 578 | 579 | slash@5.1.0: 580 | resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} 581 | engines: {node: '>=14.16'} 582 | 583 | slice-ansi@5.0.0: 584 | resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} 585 | engines: {node: '>=12'} 586 | 587 | sprintf-js@1.0.3: 588 | resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} 589 | 590 | stack-utils@2.0.6: 591 | resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} 592 | engines: {node: '>=10'} 593 | 594 | string-width@4.2.3: 595 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 596 | engines: {node: '>=8'} 597 | 598 | string-width@7.2.0: 599 | resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} 600 | engines: {node: '>=18'} 601 | 602 | string_decoder@1.3.0: 603 | resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 604 | 605 | strip-ansi@6.0.1: 606 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 607 | engines: {node: '>=8'} 608 | 609 | strip-ansi@7.1.0: 610 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 611 | engines: {node: '>=12'} 612 | 613 | supertap@3.0.1: 614 | resolution: {integrity: sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==} 615 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 616 | 617 | tar@6.2.1: 618 | resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} 619 | engines: {node: '>=10'} 620 | 621 | temp-dir@3.0.0: 622 | resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} 623 | engines: {node: '>=14.16'} 624 | 625 | time-zone@1.0.0: 626 | resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} 627 | engines: {node: '>=4'} 628 | 629 | to-regex-range@5.0.1: 630 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 631 | engines: {node: '>=8.0'} 632 | 633 | tr46@0.0.3: 634 | resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} 635 | 636 | type-fest@0.13.1: 637 | resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} 638 | engines: {node: '>=10'} 639 | 640 | unicorn-magic@0.1.0: 641 | resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} 642 | engines: {node: '>=18'} 643 | 644 | util-deprecate@1.0.2: 645 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 646 | 647 | webidl-conversions@3.0.1: 648 | resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} 649 | 650 | well-known-symbols@2.0.0: 651 | resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} 652 | engines: {node: '>=6'} 653 | 654 | whatwg-url@5.0.0: 655 | resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} 656 | 657 | wide-align@1.1.5: 658 | resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} 659 | 660 | wrap-ansi@7.0.0: 661 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 662 | engines: {node: '>=10'} 663 | 664 | wrappy@1.0.2: 665 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 666 | 667 | write-file-atomic@5.0.1: 668 | resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} 669 | engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} 670 | 671 | y18n@5.0.8: 672 | resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 673 | engines: {node: '>=10'} 674 | 675 | yallist@4.0.0: 676 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 677 | 678 | yargs-parser@21.1.1: 679 | resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} 680 | engines: {node: '>=12'} 681 | 682 | yargs@17.7.2: 683 | resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} 684 | engines: {node: '>=12'} 685 | 686 | snapshots: 687 | 688 | '@mapbox/node-pre-gyp@1.0.11': 689 | dependencies: 690 | detect-libc: 2.0.3 691 | https-proxy-agent: 5.0.1 692 | make-dir: 3.1.0 693 | node-fetch: 2.7.0 694 | nopt: 5.0.0 695 | npmlog: 5.0.1 696 | rimraf: 3.0.2 697 | semver: 7.6.3 698 | tar: 6.2.1 699 | transitivePeerDependencies: 700 | - encoding 701 | - supports-color 702 | 703 | '@nodelib/fs.scandir@2.1.5': 704 | dependencies: 705 | '@nodelib/fs.stat': 2.0.5 706 | run-parallel: 1.2.0 707 | 708 | '@nodelib/fs.stat@2.0.5': {} 709 | 710 | '@nodelib/fs.walk@1.2.8': 711 | dependencies: 712 | '@nodelib/fs.scandir': 2.1.5 713 | fastq: 1.17.1 714 | 715 | '@rollup/pluginutils@4.2.1': 716 | dependencies: 717 | estree-walker: 2.0.2 718 | picomatch: 2.3.1 719 | 720 | '@sindresorhus/merge-streams@2.3.0': {} 721 | 722 | '@vercel/nft@0.26.5': 723 | dependencies: 724 | '@mapbox/node-pre-gyp': 1.0.11 725 | '@rollup/pluginutils': 4.2.1 726 | acorn: 8.12.1 727 | acorn-import-attributes: 1.9.5(acorn@8.12.1) 728 | async-sema: 3.1.1 729 | bindings: 1.5.0 730 | estree-walker: 2.0.2 731 | glob: 7.2.3 732 | graceful-fs: 4.2.11 733 | micromatch: 4.0.8 734 | node-gyp-build: 4.8.1 735 | resolve-from: 5.0.0 736 | transitivePeerDependencies: 737 | - encoding 738 | - supports-color 739 | 740 | abbrev@1.1.1: {} 741 | 742 | acorn-import-attributes@1.9.5(acorn@8.12.1): 743 | dependencies: 744 | acorn: 8.12.1 745 | 746 | acorn-walk@8.3.3: 747 | dependencies: 748 | acorn: 8.12.1 749 | 750 | acorn@8.12.1: {} 751 | 752 | agent-base@6.0.2: 753 | dependencies: 754 | debug: 4.3.6 755 | transitivePeerDependencies: 756 | - supports-color 757 | 758 | ansi-regex@5.0.1: {} 759 | 760 | ansi-regex@6.0.1: {} 761 | 762 | ansi-styles@4.3.0: 763 | dependencies: 764 | color-convert: 2.0.1 765 | 766 | ansi-styles@6.2.1: {} 767 | 768 | aproba@2.0.0: {} 769 | 770 | are-we-there-yet@2.0.0: 771 | dependencies: 772 | delegates: 1.0.0 773 | readable-stream: 3.6.2 774 | 775 | argparse@1.0.10: 776 | dependencies: 777 | sprintf-js: 1.0.3 778 | 779 | array-find-index@1.0.2: {} 780 | 781 | arrgv@1.0.2: {} 782 | 783 | arrify@3.0.0: {} 784 | 785 | async-sema@3.1.1: {} 786 | 787 | ava@6.1.3: 788 | dependencies: 789 | '@vercel/nft': 0.26.5 790 | acorn: 8.12.1 791 | acorn-walk: 8.3.3 792 | ansi-styles: 6.2.1 793 | arrgv: 1.0.2 794 | arrify: 3.0.0 795 | callsites: 4.2.0 796 | cbor: 9.0.2 797 | chalk: 5.3.0 798 | chunkd: 2.0.1 799 | ci-info: 4.0.0 800 | ci-parallel-vars: 1.0.1 801 | cli-truncate: 4.0.0 802 | code-excerpt: 4.0.0 803 | common-path-prefix: 3.0.0 804 | concordance: 5.0.4 805 | currently-unhandled: 0.4.1 806 | debug: 4.3.6 807 | emittery: 1.0.3 808 | figures: 6.1.0 809 | globby: 14.0.2 810 | ignore-by-default: 2.1.0 811 | indent-string: 5.0.0 812 | is-plain-object: 5.0.0 813 | is-promise: 4.0.0 814 | matcher: 5.0.0 815 | memoize: 10.0.0 816 | ms: 2.1.3 817 | p-map: 7.0.2 818 | package-config: 5.0.0 819 | picomatch: 3.0.1 820 | plur: 5.1.0 821 | pretty-ms: 9.1.0 822 | resolve-cwd: 3.0.0 823 | stack-utils: 2.0.6 824 | strip-ansi: 7.1.0 825 | supertap: 3.0.1 826 | temp-dir: 3.0.0 827 | write-file-atomic: 5.0.1 828 | yargs: 17.7.2 829 | transitivePeerDependencies: 830 | - encoding 831 | - supports-color 832 | 833 | balanced-match@1.0.2: {} 834 | 835 | bindings@1.5.0: 836 | dependencies: 837 | file-uri-to-path: 1.0.0 838 | 839 | blueimp-md5@2.19.0: {} 840 | 841 | brace-expansion@1.1.11: 842 | dependencies: 843 | balanced-match: 1.0.2 844 | concat-map: 0.0.1 845 | 846 | braces@3.0.3: 847 | dependencies: 848 | fill-range: 7.1.1 849 | 850 | callsites@4.2.0: {} 851 | 852 | cbor@9.0.2: 853 | dependencies: 854 | nofilter: 3.1.0 855 | 856 | chalk@5.3.0: {} 857 | 858 | chownr@2.0.0: {} 859 | 860 | chunkd@2.0.1: {} 861 | 862 | ci-info@4.0.0: {} 863 | 864 | ci-parallel-vars@1.0.1: {} 865 | 866 | cli-truncate@4.0.0: 867 | dependencies: 868 | slice-ansi: 5.0.0 869 | string-width: 7.2.0 870 | 871 | cliui@8.0.1: 872 | dependencies: 873 | string-width: 4.2.3 874 | strip-ansi: 6.0.1 875 | wrap-ansi: 7.0.0 876 | 877 | code-excerpt@4.0.0: 878 | dependencies: 879 | convert-to-spaces: 2.0.1 880 | 881 | color-convert@2.0.1: 882 | dependencies: 883 | color-name: 1.1.4 884 | 885 | color-name@1.1.4: {} 886 | 887 | color-support@1.1.3: {} 888 | 889 | common-path-prefix@3.0.0: {} 890 | 891 | concat-map@0.0.1: {} 892 | 893 | concordance@5.0.4: 894 | dependencies: 895 | date-time: 3.1.0 896 | esutils: 2.0.3 897 | fast-diff: 1.3.0 898 | js-string-escape: 1.0.1 899 | lodash: 4.17.21 900 | md5-hex: 3.0.1 901 | semver: 7.6.3 902 | well-known-symbols: 2.0.0 903 | 904 | console-control-strings@1.1.0: {} 905 | 906 | convert-to-spaces@2.0.1: {} 907 | 908 | currently-unhandled@0.4.1: 909 | dependencies: 910 | array-find-index: 1.0.2 911 | 912 | date-time@3.1.0: 913 | dependencies: 914 | time-zone: 1.0.0 915 | 916 | debug@4.3.6: 917 | dependencies: 918 | ms: 2.1.2 919 | 920 | delegates@1.0.0: {} 921 | 922 | detect-libc@2.0.3: {} 923 | 924 | emittery@1.0.3: {} 925 | 926 | emoji-regex@10.3.0: {} 927 | 928 | emoji-regex@8.0.0: {} 929 | 930 | escalade@3.1.2: {} 931 | 932 | escape-string-regexp@2.0.0: {} 933 | 934 | escape-string-regexp@5.0.0: {} 935 | 936 | esprima@4.0.1: {} 937 | 938 | estree-walker@2.0.2: {} 939 | 940 | esutils@2.0.3: {} 941 | 942 | fast-diff@1.3.0: {} 943 | 944 | fast-glob@3.3.2: 945 | dependencies: 946 | '@nodelib/fs.stat': 2.0.5 947 | '@nodelib/fs.walk': 1.2.8 948 | glob-parent: 5.1.2 949 | merge2: 1.4.1 950 | micromatch: 4.0.8 951 | 952 | fastq@1.17.1: 953 | dependencies: 954 | reusify: 1.0.4 955 | 956 | figures@6.1.0: 957 | dependencies: 958 | is-unicode-supported: 2.0.0 959 | 960 | file-uri-to-path@1.0.0: {} 961 | 962 | fill-range@7.1.1: 963 | dependencies: 964 | to-regex-range: 5.0.1 965 | 966 | find-up-simple@1.0.0: {} 967 | 968 | fs-minipass@2.1.0: 969 | dependencies: 970 | minipass: 3.3.6 971 | 972 | fs.realpath@1.0.0: {} 973 | 974 | gauge@3.0.2: 975 | dependencies: 976 | aproba: 2.0.0 977 | color-support: 1.1.3 978 | console-control-strings: 1.1.0 979 | has-unicode: 2.0.1 980 | object-assign: 4.1.1 981 | signal-exit: 3.0.7 982 | string-width: 4.2.3 983 | strip-ansi: 6.0.1 984 | wide-align: 1.1.5 985 | 986 | get-caller-file@2.0.5: {} 987 | 988 | get-east-asian-width@1.2.0: {} 989 | 990 | glob-parent@5.1.2: 991 | dependencies: 992 | is-glob: 4.0.3 993 | 994 | glob@7.2.3: 995 | dependencies: 996 | fs.realpath: 1.0.0 997 | inflight: 1.0.6 998 | inherits: 2.0.4 999 | minimatch: 3.1.2 1000 | once: 1.4.0 1001 | path-is-absolute: 1.0.1 1002 | 1003 | globby@14.0.2: 1004 | dependencies: 1005 | '@sindresorhus/merge-streams': 2.3.0 1006 | fast-glob: 3.3.2 1007 | ignore: 5.3.2 1008 | path-type: 5.0.0 1009 | slash: 5.1.0 1010 | unicorn-magic: 0.1.0 1011 | 1012 | graceful-fs@4.2.11: {} 1013 | 1014 | has-unicode@2.0.1: {} 1015 | 1016 | https-proxy-agent@5.0.1: 1017 | dependencies: 1018 | agent-base: 6.0.2 1019 | debug: 4.3.6 1020 | transitivePeerDependencies: 1021 | - supports-color 1022 | 1023 | ignore-by-default@2.1.0: {} 1024 | 1025 | ignore@5.3.2: {} 1026 | 1027 | imurmurhash@0.1.4: {} 1028 | 1029 | indent-string@5.0.0: {} 1030 | 1031 | inflight@1.0.6: 1032 | dependencies: 1033 | once: 1.4.0 1034 | wrappy: 1.0.2 1035 | 1036 | inherits@2.0.4: {} 1037 | 1038 | irregular-plurals@3.5.0: {} 1039 | 1040 | is-extglob@2.1.1: {} 1041 | 1042 | is-fullwidth-code-point@3.0.0: {} 1043 | 1044 | is-fullwidth-code-point@4.0.0: {} 1045 | 1046 | is-glob@4.0.3: 1047 | dependencies: 1048 | is-extglob: 2.1.1 1049 | 1050 | is-number@7.0.0: {} 1051 | 1052 | is-plain-object@5.0.0: {} 1053 | 1054 | is-promise@4.0.0: {} 1055 | 1056 | is-unicode-supported@2.0.0: {} 1057 | 1058 | js-string-escape@1.0.1: {} 1059 | 1060 | js-yaml@3.14.1: 1061 | dependencies: 1062 | argparse: 1.0.10 1063 | esprima: 4.0.1 1064 | 1065 | load-json-file@7.0.1: {} 1066 | 1067 | lodash@4.17.21: {} 1068 | 1069 | make-dir@3.1.0: 1070 | dependencies: 1071 | semver: 6.3.1 1072 | 1073 | matcher@5.0.0: 1074 | dependencies: 1075 | escape-string-regexp: 5.0.0 1076 | 1077 | md5-hex@3.0.1: 1078 | dependencies: 1079 | blueimp-md5: 2.19.0 1080 | 1081 | memoize@10.0.0: 1082 | dependencies: 1083 | mimic-function: 5.0.1 1084 | 1085 | merge2@1.4.1: {} 1086 | 1087 | micromatch@4.0.8: 1088 | dependencies: 1089 | braces: 3.0.3 1090 | picomatch: 2.3.1 1091 | 1092 | mimic-function@5.0.1: {} 1093 | 1094 | minimatch@3.1.2: 1095 | dependencies: 1096 | brace-expansion: 1.1.11 1097 | 1098 | minipass@3.3.6: 1099 | dependencies: 1100 | yallist: 4.0.0 1101 | 1102 | minipass@5.0.0: {} 1103 | 1104 | minizlib@2.1.2: 1105 | dependencies: 1106 | minipass: 3.3.6 1107 | yallist: 4.0.0 1108 | 1109 | mkdirp@1.0.4: {} 1110 | 1111 | ms@2.1.2: {} 1112 | 1113 | ms@2.1.3: {} 1114 | 1115 | node-fetch@2.7.0: 1116 | dependencies: 1117 | whatwg-url: 5.0.0 1118 | 1119 | node-gyp-build@4.8.1: {} 1120 | 1121 | nofilter@3.1.0: {} 1122 | 1123 | nopt@5.0.0: 1124 | dependencies: 1125 | abbrev: 1.1.1 1126 | 1127 | npmlog@5.0.1: 1128 | dependencies: 1129 | are-we-there-yet: 2.0.0 1130 | console-control-strings: 1.1.0 1131 | gauge: 3.0.2 1132 | set-blocking: 2.0.0 1133 | 1134 | object-assign@4.1.1: {} 1135 | 1136 | once@1.4.0: 1137 | dependencies: 1138 | wrappy: 1.0.2 1139 | 1140 | p-map@7.0.2: {} 1141 | 1142 | package-config@5.0.0: 1143 | dependencies: 1144 | find-up-simple: 1.0.0 1145 | load-json-file: 7.0.1 1146 | 1147 | parse-ms@4.0.0: {} 1148 | 1149 | path-is-absolute@1.0.1: {} 1150 | 1151 | path-type@5.0.0: {} 1152 | 1153 | picomatch@2.3.1: {} 1154 | 1155 | picomatch@3.0.1: {} 1156 | 1157 | plur@5.1.0: 1158 | dependencies: 1159 | irregular-plurals: 3.5.0 1160 | 1161 | pretty-ms@9.1.0: 1162 | dependencies: 1163 | parse-ms: 4.0.0 1164 | 1165 | queue-microtask@1.2.3: {} 1166 | 1167 | readable-stream@3.6.2: 1168 | dependencies: 1169 | inherits: 2.0.4 1170 | string_decoder: 1.3.0 1171 | util-deprecate: 1.0.2 1172 | 1173 | require-directory@2.1.1: {} 1174 | 1175 | resolve-cwd@3.0.0: 1176 | dependencies: 1177 | resolve-from: 5.0.0 1178 | 1179 | resolve-from@5.0.0: {} 1180 | 1181 | reusify@1.0.4: {} 1182 | 1183 | rimraf@3.0.2: 1184 | dependencies: 1185 | glob: 7.2.3 1186 | 1187 | run-parallel@1.2.0: 1188 | dependencies: 1189 | queue-microtask: 1.2.3 1190 | 1191 | safe-buffer@5.2.1: {} 1192 | 1193 | semver@6.3.1: {} 1194 | 1195 | semver@7.6.3: {} 1196 | 1197 | serialize-error@7.0.1: 1198 | dependencies: 1199 | type-fest: 0.13.1 1200 | 1201 | set-blocking@2.0.0: {} 1202 | 1203 | signal-exit@3.0.7: {} 1204 | 1205 | signal-exit@4.1.0: {} 1206 | 1207 | slash@5.1.0: {} 1208 | 1209 | slice-ansi@5.0.0: 1210 | dependencies: 1211 | ansi-styles: 6.2.1 1212 | is-fullwidth-code-point: 4.0.0 1213 | 1214 | sprintf-js@1.0.3: {} 1215 | 1216 | stack-utils@2.0.6: 1217 | dependencies: 1218 | escape-string-regexp: 2.0.0 1219 | 1220 | string-width@4.2.3: 1221 | dependencies: 1222 | emoji-regex: 8.0.0 1223 | is-fullwidth-code-point: 3.0.0 1224 | strip-ansi: 6.0.1 1225 | 1226 | string-width@7.2.0: 1227 | dependencies: 1228 | emoji-regex: 10.3.0 1229 | get-east-asian-width: 1.2.0 1230 | strip-ansi: 7.1.0 1231 | 1232 | string_decoder@1.3.0: 1233 | dependencies: 1234 | safe-buffer: 5.2.1 1235 | 1236 | strip-ansi@6.0.1: 1237 | dependencies: 1238 | ansi-regex: 5.0.1 1239 | 1240 | strip-ansi@7.1.0: 1241 | dependencies: 1242 | ansi-regex: 6.0.1 1243 | 1244 | supertap@3.0.1: 1245 | dependencies: 1246 | indent-string: 5.0.0 1247 | js-yaml: 3.14.1 1248 | serialize-error: 7.0.1 1249 | strip-ansi: 7.1.0 1250 | 1251 | tar@6.2.1: 1252 | dependencies: 1253 | chownr: 2.0.0 1254 | fs-minipass: 2.1.0 1255 | minipass: 5.0.0 1256 | minizlib: 2.1.2 1257 | mkdirp: 1.0.4 1258 | yallist: 4.0.0 1259 | 1260 | temp-dir@3.0.0: {} 1261 | 1262 | time-zone@1.0.0: {} 1263 | 1264 | to-regex-range@5.0.1: 1265 | dependencies: 1266 | is-number: 7.0.0 1267 | 1268 | tr46@0.0.3: {} 1269 | 1270 | type-fest@0.13.1: {} 1271 | 1272 | unicorn-magic@0.1.0: {} 1273 | 1274 | util-deprecate@1.0.2: {} 1275 | 1276 | webidl-conversions@3.0.1: {} 1277 | 1278 | well-known-symbols@2.0.0: {} 1279 | 1280 | whatwg-url@5.0.0: 1281 | dependencies: 1282 | tr46: 0.0.3 1283 | webidl-conversions: 3.0.1 1284 | 1285 | wide-align@1.1.5: 1286 | dependencies: 1287 | string-width: 4.2.3 1288 | 1289 | wrap-ansi@7.0.0: 1290 | dependencies: 1291 | ansi-styles: 4.3.0 1292 | string-width: 4.2.3 1293 | strip-ansi: 6.0.1 1294 | 1295 | wrappy@1.0.2: {} 1296 | 1297 | write-file-atomic@5.0.1: 1298 | dependencies: 1299 | imurmurhash: 0.1.4 1300 | signal-exit: 4.1.0 1301 | 1302 | y18n@5.0.8: {} 1303 | 1304 | yallist@4.0.0: {} 1305 | 1306 | yargs-parser@21.1.1: {} 1307 | 1308 | yargs@17.7.2: 1309 | dependencies: 1310 | cliui: 8.0.1 1311 | escalade: 3.1.2 1312 | get-caller-file: 2.0.5 1313 | require-directory: 2.1.1 1314 | string-width: 4.2.3 1315 | y18n: 5.0.8 1316 | yargs-parser: 21.1.1 1317 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eou pipefail 4 | 5 | VERSION="$1" 6 | DRY_RUN=false 7 | 8 | for arg in "$@" 9 | do 10 | if [ "$arg" = "--dry-run" ]; then 11 | DRY_RUN=true 12 | break 13 | fi 14 | done 15 | 16 | if ! git diff --quiet; then 17 | echo "Error: There are unstaged changes in the repository." 18 | exit 1 19 | fi 20 | 21 | sed -i '' "s/(version [^)]*)/(version $VERSION)/g" dune-project 22 | dune build 23 | 24 | sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/g" package.json 25 | npm install 26 | 27 | sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/g" npm_package/package.json 28 | 29 | if [ "$DRY_RUN" == true ]; then 30 | echo "Dry run, not committing or tagging" 31 | else 32 | git add --all 33 | git commit -m "chore(release): v$VERSION" 34 | git tag "v$VERSION" 35 | git push origin "v$VERSION" 36 | git push 37 | fi 38 | -------------------------------------------------------------------------------- /scripts/process-readme.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | 5 | const readmePath = path.resolve(__dirname, "..", "README.md"); 6 | const currentReadmeContent = fs.readFileSync(readmePath, { 7 | encoding: "utf-8", 8 | }); 9 | 10 | const START_COMMENT = ""; 11 | const END_COMMENT = ""; 12 | 13 | const [startOfFile, afterStartMark] = currentReadmeContent.split(START_COMMENT); 14 | const [_, endOfFile] = afterStartMark.split(END_COMMENT); 15 | 16 | const tsInterface = fs.readFileSync( 17 | path.resolve(__dirname, "..", "bin", "node-bindings", "odiff.d.ts"), 18 | { 19 | encoding: "utf-8", 20 | } 21 | ); 22 | 23 | const updatedReadme = [ 24 | startOfFile, 25 | START_COMMENT, 26 | "\n```tsx\n", 27 | tsInterface, 28 | "```\n", 29 | END_COMMENT, 30 | endOfFile, 31 | ].join("") 32 | 33 | console.log(process.argv[2]) 34 | if (process.argv[2] === 'verify') { 35 | if (updatedReadme !== currentReadmeContent) { 36 | throw new Error("❌ Outdated README detected. Run `esy process:readme` and repush your branch") 37 | } else { 38 | console.log("✅ README is up-to-date") 39 | } 40 | } else { 41 | fs.writeFileSync( 42 | readmePath, 43 | updatedReadme 44 | ); 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/Antialiasing.ml: -------------------------------------------------------------------------------- 1 | open ImageIO 2 | 3 | module MakeAntialiasing (IO1 : ImageIO.ImageIO) (IO2 : ImageIO.ImageIO) = struct 4 | let hasManySiblingsWithSameColor ~x ~y ~width ~height ~readColor = 5 | if x <= width - 1 && y <= height - 1 then ( 6 | let x0 = max (x - 1) 0 in 7 | let y0 = max (y - 1) 0 in 8 | let x1 = min (x + 1) (width - 1) in 9 | let y1 = min (y + 1) (height - 1) in 10 | let zeroes = 11 | match x = x0 || x = x1 || y = y0 || y = y1 with 12 | | true -> ref 1 13 | | false -> ref 0 14 | in 15 | let baseColor = readColor ~x ~y in 16 | for adj_y = y0 to y1 do 17 | for adj_x = x0 to x1 do 18 | if !zeroes < 3 && (x <> adj_x || y <> adj_y) then 19 | let adjacentColor = readColor ~x:adj_x ~y:adj_y in 20 | if baseColor = adjacentColor then incr zeroes 21 | done 22 | done; 23 | !zeroes >= 3) 24 | else false 25 | 26 | let detect ~x ~y ~baseImg ~compImg = 27 | let x0 = max (x - 1) 0 in 28 | let y0 = max (y - 1) 0 in 29 | let x1 = min (x + 1) (baseImg.width - 1) in 30 | let y1 = min (y + 1) (baseImg.height - 1) in 31 | let minSiblingDelta = ref 0.0 in 32 | let maxSiblingDelta = ref 0.0 in 33 | let minSiblingDeltaCoord = ref (0, 0) in 34 | let maxSiblingDeltaCoord = ref (0, 0) in 35 | let zeroes = 36 | ref 37 | (match x = x0 || x = x1 || y = y0 || y = y1 with 38 | | true -> 1 39 | | false -> 0) 40 | in 41 | 42 | let baseColor = baseImg |> IO1.readRawPixel ~x ~y in 43 | for adj_y = y0 to y1 do 44 | for adj_x = x0 to x1 do 45 | if !zeroes < 3 && (x <> adj_x || y <> adj_y) then 46 | let adjacentColor = baseImg |> IO1.readRawPixel ~x:adj_x ~y:adj_y in 47 | if baseColor = adjacentColor then incr zeroes 48 | else 49 | let delta = 50 | ColorDelta.calculatePixelBrightnessDelta baseColor adjacentColor 51 | in 52 | if delta < !minSiblingDelta then ( 53 | minSiblingDelta := delta; 54 | minSiblingDeltaCoord := (adj_x, adj_y)) 55 | else if delta > !maxSiblingDelta then ( 56 | maxSiblingDelta := delta; 57 | maxSiblingDeltaCoord := (adj_x, adj_y)) 58 | done 59 | done; 60 | 61 | if !zeroes >= 3 || !minSiblingDelta = 0.0 || !maxSiblingDelta = 0.0 then 62 | (* 63 | If we found more than 2 equal siblings or there are 64 | no darker pixels among other siblings or 65 | there are not brighter pixels among the siblings 66 | *) 67 | false 68 | else 69 | (* 70 | If either the darkest or the brightest pixel has 3+ equal siblings in both images 71 | (definitely not anti-aliased), this pixel is anti-aliased 72 | *) 73 | let minX, minY = !minSiblingDeltaCoord in 74 | let maxX, maxY = !maxSiblingDeltaCoord in 75 | (hasManySiblingsWithSameColor ~x:minX ~y:minY ~width:baseImg.width 76 | ~height:baseImg.height ~readColor:(IO1.readRawPixel baseImg) 77 | || hasManySiblingsWithSameColor ~x:maxX ~y:maxY ~width:baseImg.width 78 | ~height:baseImg.height ~readColor:(IO1.readRawPixel baseImg)) 79 | && (hasManySiblingsWithSameColor ~x:minX ~y:minY ~width:compImg.width 80 | ~height:compImg.height ~readColor:(IO2.readRawPixel compImg) 81 | || hasManySiblingsWithSameColor ~x:maxX ~y:maxY ~width:compImg.width 82 | ~height:compImg.height ~readColor:(IO2.readRawPixel compImg)) 83 | end 84 | -------------------------------------------------------------------------------- /src/ColorDelta.ml: -------------------------------------------------------------------------------- 1 | open Int32 2 | 3 | type pixel = { r : float; g : float; b : float; a : float } 4 | 5 | let white_pixel : pixel = { r = 255.; g = 255.; b = 255.; a = 0. } 6 | let blend_channel_white color alpha = 255. +. ((color -. 255.) *. alpha) 7 | 8 | let blendSemiTransparentPixel = function 9 | | { r; g; b; a } when a = 0. -> white_pixel 10 | | { r; g; b; a } when a = 255. -> { r; g; b; a = 1. } 11 | | { r; g; b; a } when a < 255. -> 12 | let normalizedAlpha = a /. 255. in 13 | let r, g, b, a = 14 | ( blend_channel_white r normalizedAlpha, 15 | blend_channel_white g normalizedAlpha, 16 | blend_channel_white b normalizedAlpha, 17 | normalizedAlpha ) 18 | in 19 | 20 | { r; g; b; a } 21 | | _ -> 22 | failwith 23 | "Found pixel with alpha value greater than uint8 max value. Aborting." 24 | 25 | let decodeRawPixel pixel = 26 | let a = logand (shift_right_logical pixel 24) 255l in 27 | let b = logand (shift_right_logical pixel 16) 255l in 28 | let g = logand (shift_right_logical pixel 8) 255l in 29 | let r = logand pixel 255l in 30 | 31 | { 32 | r = Int32.to_float r; 33 | g = Int32.to_float g; 34 | b = Int32.to_float b; 35 | a = Int32.to_float a; 36 | } 37 | [@@inline] 38 | 39 | let rgb2y { r; g; b; a } = 40 | (r *. 0.29889531) +. (g *. 0.58662247) +. (b *. 0.11448223) 41 | 42 | let rgb2i { r; g; b; a } = 43 | (r *. 0.59597799) -. (g *. 0.27417610) -. (b *. 0.32180189) 44 | 45 | let rgb2q { r; g; b; a } = 46 | (r *. 0.21147017) -. (g *. 0.52261711) +. (b *. 0.31114694) 47 | 48 | let calculatePixelColorDelta pixelA pixelB = 49 | let pixelA = pixelA |> decodeRawPixel |> blendSemiTransparentPixel in 50 | let pixelB = pixelB |> decodeRawPixel |> blendSemiTransparentPixel in 51 | 52 | let y = rgb2y pixelA -. rgb2y pixelB in 53 | let i = rgb2i pixelA -. rgb2i pixelB in 54 | let q = rgb2q pixelA -. rgb2q pixelB in 55 | 56 | let delta = (0.5053 *. y *. y) +. (0.299 *. i *. i) +. (0.1957 *. q *. q) in 57 | delta 58 | 59 | let calculatePixelBrightnessDelta pixelA pixelB = 60 | let pixelA = pixelA |> decodeRawPixel |> blendSemiTransparentPixel in 61 | let pixelB = pixelB |> decodeRawPixel |> blendSemiTransparentPixel in 62 | rgb2y pixelA -. rgb2y pixelB 63 | -------------------------------------------------------------------------------- /src/Diff.ml: -------------------------------------------------------------------------------- 1 | open Int32 2 | 3 | (* Decimal representation of the RGBA in32 pixel red pixel *) 4 | let redPixel = Int32.of_int 4278190335 5 | 6 | (* Decimal representation of the RGBA in32 pixel green pixel *) 7 | let maxYIQPossibleDelta = 35215. 8 | 9 | type 'a diffVariant = Layout | Pixel of ('a * int * float * int Stack.t) 10 | 11 | let unrollIgnoreRegions width list = 12 | list 13 | |> Option.map 14 | (List.map (fun ((x1, y1), (x2, y2)) -> 15 | let p1 = (y1 * width) + x1 in 16 | let p2 = (y2 * width) + x2 in 17 | (p1, p2))) 18 | 19 | let isInIgnoreRegion offset list = 20 | list 21 | |> Option.map 22 | (List.exists (fun ((p1 : int), (p2 : int)) -> 23 | offset >= p1 && offset <= p2)) 24 | |> Option.value ~default:false 25 | 26 | module MakeDiff (IO1 : ImageIO.ImageIO) (IO2 : ImageIO.ImageIO) = struct 27 | module BaseAA = Antialiasing.MakeAntialiasing (IO1) (IO2) 28 | module CompAA = Antialiasing.MakeAntialiasing (IO2) (IO1) 29 | 30 | let compare (base : IO1.t ImageIO.img) (comp : IO2.t ImageIO.img) 31 | ?(antialiasing = false) ?(outputDiffMask = false) ?(diffLines = false) 32 | ?diffPixel ?(threshold = 0.1) ?ignoreRegions ?(captureDiff = true) () = 33 | let maxDelta = maxYIQPossibleDelta *. (threshold ** 2.) in 34 | let diffPixel = match diffPixel with Some x -> x | None -> redPixel in 35 | let diffOutput = 36 | match captureDiff with 37 | | true -> 38 | Some 39 | (match outputDiffMask with 40 | | true -> IO1.makeSameAsLayout base 41 | | false -> base) 42 | | false -> None 43 | in 44 | 45 | let diffCount = ref 0 in 46 | let diffLinesStack = Stack.create () in 47 | let countDifference x y = 48 | incr diffCount; 49 | diffOutput |> Option.iter (IO1.setImgColor ~x ~y diffPixel); 50 | 51 | if 52 | diffLines 53 | && (diffLinesStack |> Stack.is_empty || diffLinesStack |> Stack.top < y) 54 | then diffLinesStack |> Stack.push y 55 | in 56 | 57 | let ignoreRegions = unrollIgnoreRegions base.width ignoreRegions in 58 | let hasIgnoreRegions = ignoreRegions |> Option.is_some in 59 | 60 | let size = (base.height * base.width) - 1 in 61 | let x = ref 0 in 62 | let y = ref 0 in 63 | 64 | let layoutDifference = 65 | base.width <> comp.width || base.height <> comp.height 66 | in 67 | 68 | for offset = 0 to size do 69 | (* if images are different we can't use offset *) 70 | let baseColor = 71 | if layoutDifference then IO1.readRawPixel ~x:!x ~y:!y base 72 | else IO1.readRawPixelAtOffset offset base 73 | in 74 | 75 | (if !x >= comp.width || !y >= comp.height then ( 76 | let alpha = logand (shift_right_logical baseColor 24) 255l in 77 | if alpha <> Int32.zero then countDifference !x !y) 78 | else 79 | let compColor = 80 | if layoutDifference then IO2.readRawPixel ~x:!x ~y:!y comp 81 | else IO2.readRawPixelAtOffset offset comp 82 | in 83 | 84 | if baseColor <> compColor then 85 | let isIgnored = 86 | hasIgnoreRegions && isInIgnoreRegion offset ignoreRegions 87 | in 88 | 89 | if not isIgnored then 90 | let delta = 91 | ColorDelta.calculatePixelColorDelta baseColor compColor 92 | in 93 | if delta > maxDelta then 94 | let isAntialiased = 95 | if not antialiasing then false 96 | else 97 | BaseAA.detect ~x:!x ~y:!y ~baseImg:base ~compImg:comp 98 | || CompAA.detect ~x:!x ~y:!y ~baseImg:comp ~compImg:base 99 | in 100 | if not isAntialiased then countDifference !x !y); 101 | 102 | if !x = base.width - 1 then ( 103 | x := 0; 104 | incr y) 105 | else incr x 106 | done; 107 | 108 | let diffPercentage = 109 | 100.0 *. Float.of_int !diffCount 110 | /. (Float.of_int base.width *. Float.of_int base.height) 111 | in 112 | (diffOutput, !diffCount, diffPercentage, diffLinesStack) 113 | 114 | let diff (base : IO1.t ImageIO.img) (comp : IO2.t ImageIO.img) ~outputDiffMask 115 | ?(threshold = 0.1) ~diffPixel ?(failOnLayoutChange = true) 116 | ?(antialiasing = false) ?(diffLines = false) ?ignoreRegions () = 117 | if 118 | failOnLayoutChange = true 119 | && (base.width <> comp.width || base.height <> comp.height) 120 | then Layout 121 | else 122 | let diffOutput, diffCount, diffPercentage, diffLinesStack = 123 | compare base comp ~threshold ~diffPixel ~outputDiffMask ~antialiasing 124 | ~diffLines ?ignoreRegions ~captureDiff:true () 125 | in 126 | Pixel (Option.get diffOutput, diffCount, diffPercentage, diffLinesStack) 127 | 128 | let diffWithoutOutput (base : IO1.t ImageIO.img) (comp : IO2.t ImageIO.img) 129 | ?(threshold = 0.1) ?(failOnLayoutChange = true) ?(antialiasing = false) 130 | ?(diffLines = false) ?ignoreRegions () = 131 | if 132 | failOnLayoutChange = true 133 | && (base.width <> comp.width || base.height <> comp.height) 134 | then Layout 135 | else 136 | let diffResult = 137 | compare base comp ~threshold ~outputDiffMask:false ~antialiasing 138 | ~diffLines ?ignoreRegions ~captureDiff:false () 139 | in 140 | Pixel diffResult 141 | end 142 | -------------------------------------------------------------------------------- /src/ImageIO.ml: -------------------------------------------------------------------------------- 1 | type 'a img = { width : int; height : int; image : 'a } 2 | 3 | exception ImageNotLoaded 4 | 5 | module type ImageIO = sig 6 | type t 7 | 8 | val loadImage : string -> t img 9 | val makeSameAsLayout : t img -> t img 10 | val readRawPixelAtOffset : int -> t img -> Int32.t [@@inline.always] 11 | val readRawPixel : x:int -> y:int -> t img -> Int32.t [@@inline.always] 12 | val setImgColor : x:int -> y:int -> Int32.t -> t img -> unit 13 | val saveImage : t img -> string -> unit 14 | val freeImage : t img -> unit 15 | end 16 | -------------------------------------------------------------------------------- /src/PerfTest.ml: -------------------------------------------------------------------------------- 1 | let now (name : string) = (name, ref (Sys.time ())) 2 | 3 | let cycle (name, timepoint) ?(cycleName = "") () = 4 | Printf.printf "'%s %s' executed for: %f ms \n" name cycleName 5 | ((Sys.time () -. !timepoint) *. 1000.); 6 | timepoint := Sys.time () 7 | 8 | let ifTimeMore amount (name, timepoint) = 9 | (Sys.time () -. timepoint) *. 1000. > amount 10 | 11 | let cycleIf point predicate = if predicate point then cycle point () 12 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name odiff) 3 | (public_name odiff-core) 4 | (flags 5 | (-w -40 -w +26))) 6 | 7 | (env 8 | (dev 9 | (flags (:standard -w +42)) 10 | (ocamlopt_flags (:standard -unsafe))) 11 | (release 12 | (ocamlopt_flags (:standard -unsafe -O3 -rounds 5 -unboxed-types -unbox-closures -inline 200 -inline-max-depth 7 -unbox-closures-factor 50)))) 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/Test_Core.ml: -------------------------------------------------------------------------------- 1 | open Alcotest 2 | module PNG_Diff = Odiff.Diff.MakeDiff (Png.IO) (Png.IO) 3 | 4 | let test_antialiasing () = 5 | Sys.getcwd () |> print_endline; 6 | let img1 = Png.IO.loadImage "test-images/aa/antialiasing-on.png" in 7 | let img2 = Png.IO.loadImage "test-images/aa/antialiasing-off.png" in 8 | let _, diffPixels, diffPercentage, _ = 9 | PNG_Diff.compare img1 img2 ~outputDiffMask:false ~antialiasing:true () 10 | in 11 | check int "diffPixels" 46 diffPixels; 12 | check (float 0.001) "diffPercentage" 0.115 diffPercentage 13 | 14 | let test_different_sized_aa_images () = 15 | let img1 = Png.IO.loadImage "test-images/aa/antialiasing-on.png" in 16 | let img2 = Png.IO.loadImage "test-images/aa/antialiasing-off-small.png" in 17 | let _, diffPixels, diffPercentage, _ = 18 | PNG_Diff.compare img1 img2 ~outputDiffMask:true ~antialiasing:true () 19 | in 20 | check int "diffPixels" 417 diffPixels; 21 | check (float 0.01) "diffPercentage" 1.0425 diffPercentage 22 | 23 | let test_threshold () = 24 | let img1 = Png.IO.loadImage "test-images/png/orange.png" in 25 | let img2 = Png.IO.loadImage "test-images/png/orange_changed.png" in 26 | let _, diffPixels, diffPercentage, _ = 27 | PNG_Diff.compare img1 img2 ~threshold:0.5 () 28 | in 29 | check int "diffPixels" 25 diffPixels; 30 | check (float 0.001) "diffPercentage" 0.02 diffPercentage 31 | 32 | let test_ignore_regions () = 33 | let img1 = Png.IO.loadImage "test-images/png/orange.png" in 34 | let img2 = Png.IO.loadImage "test-images/png/orange_changed.png" in 35 | let _diffOutput, diffPixels, diffPercentage, _ = 36 | PNG_Diff.compare img1 img2 37 | ~ignoreRegions:[ ((150, 30), (310, 105)); ((20, 175), (105, 200)) ] 38 | () 39 | in 40 | check int "diffPixels" 0 diffPixels; 41 | check (float 0.001) "diffPercentage" 0.0 diffPercentage 42 | 43 | let test_diff_color () = 44 | let img1 = Png.IO.loadImage "test-images/png/orange.png" in 45 | let img2 = Png.IO.loadImage "test-images/png/orange_changed.png" in 46 | let diffOutput, _, _, _ = 47 | PNG_Diff.compare img1 img2 48 | ~diffPixel:(Int32.of_int 4278255360 (*int32 representation of #00ff00*)) 49 | () 50 | in 51 | check bool "diffOutput" (Option.is_some diffOutput) true; 52 | let diffOutput = Option.get diffOutput in 53 | let originalDiff = Png.IO.loadImage "test-images/png/orange_diff_green.png" in 54 | let diffMaskOfDiff, diffOfDiffPixels, diffOfDiffPercentage, _ = 55 | PNG_Diff.compare originalDiff diffOutput () 56 | in 57 | check bool "diffMaskOfDiff" (Option.is_some diffMaskOfDiff) true; 58 | let diffMaskOfDiff = Option.get diffMaskOfDiff in 59 | if diffOfDiffPixels > 0 then ( 60 | Png.IO.saveImage diffOutput "test-images/png/diff-output-green.png"; 61 | Png.IO.saveImage diffMaskOfDiff "test-images/png/diff-of-diff-green.png"); 62 | check int "diffOfDiffPixels" 0 diffOfDiffPixels; 63 | check (float 0.001) "diffOfDiffPercentage" 0.0 diffOfDiffPercentage 64 | 65 | let test_blend_semi_transparent_color () = 66 | let open Odiff.ColorDelta in 67 | let test_blend r g b a expected_r expected_g expected_b expected_a = 68 | let { r; g; b; a } = blendSemiTransparentPixel { r; g; b; a } in 69 | check (float 0.01) "r" expected_r r; 70 | check (float 0.01) "g" expected_g g; 71 | check (float 0.01) "b" expected_b b; 72 | check (float 0.01) "a" expected_a a 73 | in 74 | test_blend 0. 128. 255. 255. 0. 128. 255. 1.; 75 | test_blend 0. 128. 255. 0. 255. 255. 255. 0.; 76 | test_blend 0. 128. 255. 5. 250. 252.51 255. 0.02; 77 | test_blend 0. 128. 255. 51. 204. 229.6 255. 0.2; 78 | test_blend 0. 128. 255. 128. 127. 191.25 255. 0.5 79 | 80 | let test_different_layouts () = 81 | Sys.getcwd () |> print_endline; 82 | let img1 = Png.IO.loadImage "test-images/png/white4x4.png" in 83 | let img2 = Png.IO.loadImage "test-images/png/purple8x8.png" in 84 | let _, diffPixels, diffPercentage, _ = 85 | PNG_Diff.compare img1 img2 ~outputDiffMask:false ~antialiasing:false () 86 | in 87 | check int "diffPixels" 16 diffPixels; 88 | check (float 0.001) "diffPercentage" 100.0 diffPercentage 89 | 90 | let () = 91 | run "CORE" 92 | [ 93 | ( "Antialiasing", 94 | [ 95 | test_case "does not count anti-aliased pixels as different" `Quick 96 | test_antialiasing; 97 | test_case "tests different sized AA images" `Quick 98 | test_different_sized_aa_images; 99 | ] ); 100 | ( "Threshold", 101 | [ test_case "uses provided threshold" `Quick test_threshold ] ); 102 | ( "Ignore Regions", 103 | [ test_case "uses provided ignore regions" `Quick test_ignore_regions ] 104 | ); 105 | ( "Diff Color", 106 | [ 107 | test_case "creates diff output image with custom green diff color" 108 | `Quick test_diff_color; 109 | ] ); 110 | ( "blendSemiTransparentColor", 111 | [ 112 | test_case "blend semi-transparent colors" `Quick 113 | test_blend_semi_transparent_color; 114 | ] ); 115 | ( "layoutDifference", 116 | [ 117 | test_case "diff images with different layouts" `Quick 118 | test_different_layouts; 119 | ] ); 120 | ] 121 | -------------------------------------------------------------------------------- /test/Test_IO_BMP.ml: -------------------------------------------------------------------------------- 1 | open Alcotest 2 | 3 | module Diff = Odiff.Diff.MakeDiff (Bmp.IO) (Bmp.IO) 4 | module Output_Diff = Odiff.Diff.MakeDiff (Png.IO) (Bmp.IO) 5 | 6 | let load_image path = 7 | match Bmp.IO.loadImage path with 8 | | exception ex -> fail (Printf.sprintf "Failed to load image: %s\nError: %s" path (Printexc.to_string ex)) 9 | | img -> img 10 | 11 | let load_png_image path = 12 | match Png.IO.loadImage path with 13 | | exception ex -> fail (Printf.sprintf "Failed to load image: %s\nError: %s" path (Printexc.to_string ex)) 14 | | img -> img 15 | 16 | let test_finds_difference_between_images () = 17 | let img1 = load_image "test-images/bmp/clouds.bmp" in 18 | let img2 = load_image "test-images/bmp/clouds-2.bmp" in 19 | let _, diffPixels, diffPercentage, _ = Diff.compare img1 img2 () in 20 | check int "diffPixels" 191 diffPixels; 21 | check (float 0.001) "diffPercentage" 0.076 diffPercentage 22 | 23 | let test_diff_mask_no_mask_equal () = 24 | let img1 = load_image "test-images/bmp/clouds.bmp" in 25 | let img2 = load_image "test-images/bmp/clouds-2.bmp" in 26 | let _, diffPixels, diffPercentage, _ = Diff.compare img1 img2 ~outputDiffMask:false () in 27 | let img1 = load_image "test-images/bmp/clouds.bmp" in 28 | let img2 = load_image "test-images/bmp/clouds-2.bmp" in 29 | let _, diffPixelsMask, diffPercentageMask, _ = Diff.compare img1 img2 ~outputDiffMask:true () in 30 | check int "diffPixels" diffPixels diffPixelsMask; 31 | check (float 0.001) "diffPercentage" diffPercentage diffPercentageMask 32 | 33 | let test_creates_correct_diff_output_image () = 34 | let img1 = load_image "test-images/bmp/clouds.bmp" in 35 | let img2 = load_image "test-images/bmp/clouds-2.bmp" in 36 | let diffOutput, _, _, _ = Diff.compare img1 img2 () in 37 | check bool "diffOutput" (Option.is_some diffOutput) true; 38 | let diffOutput = Option.get diffOutput in 39 | let originalDiff = load_png_image "test-images/bmp/clouds-diff.png" in 40 | let diffMaskOfDiff, diffOfDiffPixels, diffOfDiffPercentage, _ = Output_Diff.compare originalDiff diffOutput () in 41 | check bool "diffMaskOfDiff" (Option.is_some diffMaskOfDiff) true; 42 | let diffMaskOfDiff = Option.get diffMaskOfDiff in 43 | if diffOfDiffPixels > 0 then ( 44 | Bmp.IO.saveImage diffOutput "test-images/bmp/_diff-output.png"; 45 | Png.IO.saveImage diffMaskOfDiff "test-images/bmp/_diff-of-diff.png" 46 | ); 47 | check int "diffOfDiffPixels" 0 diffOfDiffPixels; 48 | check (float 0.001) "diffOfDiffPercentage" 0.0 diffOfDiffPercentage 49 | 50 | let () = 51 | run "IO" [ 52 | "BMP", [ 53 | test_case "finds difference between 2 images" `Quick test_finds_difference_between_images; 54 | test_case "Diff of mask and no mask are equal" `Quick test_diff_mask_no_mask_equal; 55 | test_case "Creates correct diff output image" `Quick test_creates_correct_diff_output_image; 56 | ]; 57 | ] 58 | 59 | -------------------------------------------------------------------------------- /test/Test_IO_JPG.ml: -------------------------------------------------------------------------------- 1 | open Alcotest 2 | module Diff = Odiff.Diff.MakeDiff (Jpg.IO) (Jpg.IO) 3 | module Output_Diff = Odiff.Diff.MakeDiff (Png.IO) (Jpg.IO) 4 | 5 | let load_image path = 6 | match Jpg.IO.loadImage path with 7 | | exception ex -> 8 | fail 9 | (Printf.sprintf "Failed to load image: %s\nError: %s" path 10 | (Printexc.to_string ex)) 11 | | img -> img 12 | 13 | let load_png_image path = 14 | match Png.IO.loadImage path with 15 | | exception ex -> 16 | fail 17 | (Printf.sprintf "Failed to load image: %s\nError: %s" path 18 | (Printexc.to_string ex)) 19 | | img -> img 20 | 21 | let test_finds_difference_between_images () = 22 | let img1 = load_image "test-images/jpg/tiger.jpg" in 23 | let img2 = load_image "test-images/jpg/tiger-2.jpg" in 24 | let _, diffPixels, diffPercentage, _ = Diff.compare img1 img2 () in 25 | check int "diffPixels" 7789 diffPixels; 26 | check (float 0.001) "diffPercentage" 1.1677 diffPercentage 27 | 28 | let test_diff_mask_no_mask_equal () = 29 | let img1 = load_image "test-images/jpg/tiger.jpg" in 30 | let img2 = load_image "test-images/jpg/tiger-2.jpg" in 31 | let _, diffPixels, diffPercentage, _ = 32 | Diff.compare img1 img2 ~outputDiffMask:false () 33 | in 34 | let img1 = load_image "test-images/jpg/tiger.jpg" in 35 | let img2 = load_image "test-images/jpg/tiger-2.jpg" in 36 | let _, diffPixelsMask, diffPercentageMask, _ = 37 | Diff.compare img1 img2 ~outputDiffMask:true () 38 | in 39 | check int "diffPixels" diffPixels diffPixelsMask; 40 | check (float 0.001) "diffPercentage" diffPercentage diffPercentageMask 41 | 42 | let test_creates_correct_diff_output_image () = 43 | let img1 = load_image "test-images/jpg/tiger.jpg" in 44 | let img2 = load_image "test-images/jpg/tiger-2.jpg" in 45 | let diffOutput, _, _, _ = Diff.compare img1 img2 () in 46 | check bool "diffOutput" (Option.is_some diffOutput) true; 47 | let diffOutput = Option.get diffOutput in 48 | let originalDiff = load_png_image "test-images/jpg/tiger-diff.png" in 49 | let diffMaskOfDiff, diffOfDiffPixels, diffOfDiffPercentage, _ = 50 | Output_Diff.compare originalDiff diffOutput () 51 | in 52 | check bool "diffMaskOfDiff" (Option.is_some diffMaskOfDiff) true; 53 | let diffMaskOfDiff = Option.get diffMaskOfDiff in 54 | if diffOfDiffPixels > 0 then ( 55 | Jpg.IO.saveImage diffOutput "test-images/jpg/_diff-output.png"; 56 | Png.IO.saveImage diffMaskOfDiff "test-images/jpg/_diff-of-diff.png"); 57 | check int "diffOfDiffPixels" 0 diffOfDiffPixels; 58 | check (float 0.001) "diffOfDiffPercentage" 0.0 diffOfDiffPercentage 59 | 60 | let () = 61 | run "IO" 62 | [ 63 | ( "JPG / JPEG", 64 | [ 65 | test_case "finds difference between 2 images" `Quick 66 | test_finds_difference_between_images; 67 | test_case "Diff of mask and no mask are equal" `Quick 68 | test_diff_mask_no_mask_equal; 69 | test_case "Creates correct diff output image" `Quick 70 | test_creates_correct_diff_output_image; 71 | ] ); 72 | ] 73 | -------------------------------------------------------------------------------- /test/Test_IO_PNG.ml: -------------------------------------------------------------------------------- 1 | open Alcotest 2 | module Diff = Odiff.Diff.MakeDiff (Png.IO) (Png.IO) 3 | 4 | let load_image path = 5 | match Png.IO.loadImage path with 6 | | exception ex -> 7 | fail 8 | (Printf.sprintf "Failed to load image: %s\nError: %s" path 9 | (Printexc.to_string ex)) 10 | | img -> img 11 | 12 | let () = 13 | run "IO" 14 | [ 15 | ( "PNG", 16 | [ 17 | test_case "finds difference between 2 images" `Quick (fun () -> 18 | let img1 = load_image "test-images/png/orange.png" in 19 | let img2 = load_image "test-images/png/orange_changed.png" in 20 | let _, diffPixels, diffPercentage, _ = 21 | Diff.compare img1 img2 () 22 | in 23 | check int "diffPixels" 1366 diffPixels; 24 | check (float 0.1) "diffPercentage" 1.14 diffPercentage); 25 | test_case "Diff of mask and no mask are equal" `Quick (fun () -> 26 | let img1 = load_image "test-images/png/orange.png" in 27 | let img2 = load_image "test-images/png/orange_changed.png" in 28 | let _, diffPixels, diffPercentage, _ = 29 | Diff.compare img1 img2 ~outputDiffMask:false () 30 | in 31 | let img1 = load_image "test-images/png/orange.png" in 32 | let img2 = load_image "test-images/png/orange_changed.png" in 33 | let _, diffPixelsMask, diffPercentageMask, _ = 34 | Diff.compare img1 img2 ~outputDiffMask:true () 35 | in 36 | check int "diffPixels" diffPixels diffPixelsMask; 37 | check (float 0.001) "diffPercentage" diffPercentage 38 | diffPercentageMask); 39 | test_case "Creates correct diff output image" `Quick (fun () -> 40 | let img1 = load_image "test-images/png/orange.png" in 41 | let img2 = load_image "test-images/png/orange_changed.png" in 42 | let diffOutput, _, _, _ = Diff.compare img1 img2 () in 43 | check bool "diffOutput" (Option.is_some diffOutput) true; 44 | let diffOutput = Option.get diffOutput in 45 | let originalDiff = load_image "test-images/png/orange_diff.png" in 46 | let diffMaskOfDiff, diffOfDiffPixels, diffOfDiffPercentage, _ = 47 | Diff.compare originalDiff diffOutput () 48 | in 49 | check bool "diffMaskOfDiff" (Option.is_some diffMaskOfDiff) true; 50 | let diffMaskOfDiff = Option.get diffMaskOfDiff in 51 | if diffOfDiffPixels > 0 then ( 52 | Png.IO.saveImage diffOutput "test-images/png/diff-output.png"; 53 | Png.IO.saveImage diffMaskOfDiff 54 | "test-images/png/diff-of-diff.png"); 55 | check int "diffOfDiffPixels" 0 diffOfDiffPixels; 56 | check (float 0.001) "diffOfDiffPercentage" 0.0 57 | diffOfDiffPercentage); 58 | test_case "Correctly handles different encodings of transparency" 59 | `Quick (fun () -> 60 | let img1 = load_image "test-images/png/extreme-alpha.png" in 61 | let img2 = load_image "test-images/png/extreme-alpha-1.png" in 62 | let _, diffPixels, _, _ = Diff.compare img1 img2 () in 63 | check int "diffPixels" 0 diffPixels); 64 | ] ); 65 | ] 66 | -------------------------------------------------------------------------------- /test/Test_IO_TIFF.ml: -------------------------------------------------------------------------------- 1 | open Alcotest 2 | module Diff = Odiff.Diff.MakeDiff (Tiff.IO) (Tiff.IO) 3 | module Output_Diff = Odiff.Diff.MakeDiff (Png.IO) (Tiff.IO) 4 | 5 | let load_tiff_image path = 6 | match Tiff.IO.loadImage path with 7 | | exception ex -> 8 | fail 9 | (Printf.sprintf "Failed to load image: %s\nError: %s" path 10 | (Printexc.to_string ex)) 11 | | img -> img 12 | 13 | let load_png_image path = 14 | match Png.IO.loadImage path with 15 | | exception ex -> 16 | fail 17 | (Printf.sprintf "Failed to load image: %s\nError: %s" path 18 | (Printexc.to_string ex)) 19 | | img -> img 20 | 21 | let run_tiff_tests () = 22 | run "IO" 23 | [ 24 | ( "TIFF", 25 | [ 26 | test_case "finds difference between 2 images" `Quick (fun () -> 27 | let img1 = load_tiff_image "test-images/tiff/laptops.tiff" in 28 | let img2 = load_tiff_image "test-images/tiff/laptops-2.tiff" in 29 | let _, diffPixels, diffPercentage, _ = 30 | Diff.compare img1 img2 () 31 | in 32 | check int "diffPixels" 8569 diffPixels; 33 | check (float 0.01) "diffPercentage" 3.79 diffPercentage); 34 | test_case "Diff of mask and no mask are equal" `Quick (fun () -> 35 | let img1 = load_tiff_image "test-images/tiff/laptops.tiff" in 36 | let img2 = load_tiff_image "test-images/tiff/laptops-2.tiff" in 37 | let _, diffPixels, diffPercentage, _ = 38 | Diff.compare img1 img2 ~outputDiffMask:false () 39 | in 40 | let img1 = load_tiff_image "test-images/tiff/laptops.tiff" in 41 | let img2 = load_tiff_image "test-images/tiff/laptops-2.tiff" in 42 | let _, diffPixelsMask, diffPercentageMask, _ = 43 | Diff.compare img1 img2 ~outputDiffMask:true () 44 | in 45 | check int "diffPixels" diffPixels diffPixelsMask; 46 | check (float 0.001) "diffPercentage" diffPercentage 47 | diffPercentageMask); 48 | test_case "Creates correct diff output image" `Quick (fun () -> 49 | let img1 = load_tiff_image "test-images/tiff/laptops.tiff" in 50 | let img2 = load_tiff_image "test-images/tiff/laptops-2.tiff" in 51 | let diffOutput, _, _, _ = Diff.compare img1 img2 () in 52 | check bool "diffOutput" (Option.is_some diffOutput) true; 53 | let diffOutput = Option.get diffOutput in 54 | let originalDiff = 55 | load_png_image "test-images/tiff/laptops-diff.png" 56 | in 57 | let diffMaskOfDiff, diffOfDiffPixels, diffOfDiffPercentage, _ = 58 | Output_Diff.compare originalDiff diffOutput () 59 | in 60 | check bool "diffMaskOfDiff" (Option.is_some diffMaskOfDiff) true; 61 | let diffMaskOfDiff = Option.get diffMaskOfDiff in 62 | if diffOfDiffPixels > 0 then ( 63 | Tiff.IO.saveImage diffOutput "test-images/tiff/_diff-output.png"; 64 | Png.IO.saveImage diffMaskOfDiff 65 | "test-images/tiff/_diff-of-diff.png"); 66 | check int "diffOfDiffPixels" 0 diffOfDiffPixels; 67 | check (float 0.001) "diffOfDiffPercentage" 0.0 68 | diffOfDiffPercentage); 69 | ] ); 70 | ] 71 | 72 | let () = 73 | if Sys.os_type = "Unix" then run_tiff_tests () 74 | else print_endline "Skipping TIFF tests on Windows systems" 75 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (tests 2 | (names Test_Core Test_IO_BMP Test_IO_JPG Test_IO_PNG Test_IO_TIFF) 3 | (libraries alcotest odiff odiff-io) 4 | (deps (glob_files test_images/*)) 5 | ) 6 | -------------------------------------------------------------------------------- /test/node-binding.test.cjs: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const test = require("ava"); 3 | const { compare } = require("../npm_package/odiff"); 4 | 5 | const IMAGES_PATH = path.resolve(__dirname, "..", "images"); 6 | const BINARY_PATH = path.resolve( 7 | __dirname, 8 | "..", 9 | "_build", 10 | "default", 11 | "bin", 12 | "ODiffBin.exe" 13 | ); 14 | 15 | console.log(`Testing binary ${BINARY_PATH}`); 16 | 17 | const options = { 18 | __binaryPath: BINARY_PATH, 19 | } 20 | 21 | test("Outputs correct parsed result when images different", async (t) => { 22 | const { reason, diffCount, diffPercentage } = await compare( 23 | path.join(IMAGES_PATH, "donkey.png"), 24 | path.join(IMAGES_PATH, "donkey-2.png"), 25 | path.join(IMAGES_PATH, "diff.png"), 26 | options 27 | ); 28 | 29 | t.is(reason, "pixel-diff"); 30 | t.is(diffCount, 101841); 31 | t.is(diffPercentage, 2.65077570347); 32 | }) 33 | 34 | test("Correctly works with reduceRamUsage", async (t) => { 35 | const { reason, diffCount, diffPercentage } = await compare( 36 | path.join(IMAGES_PATH, "donkey.png"), 37 | path.join(IMAGES_PATH, "donkey-2.png"), 38 | path.join(IMAGES_PATH, "diff.png"), 39 | { 40 | ...options, 41 | reduceRamUsage: true, 42 | } 43 | ); 44 | 45 | t.is(reason, "pixel-diff"); 46 | t.is(diffCount, 101841); 47 | t.is(diffPercentage, 2.65077570347); 48 | }); 49 | 50 | test("Correctly parses threshold", async (t) => { 51 | const { reason, diffCount, diffPercentage } = await compare( 52 | path.join(IMAGES_PATH, "donkey.png"), 53 | path.join(IMAGES_PATH, "donkey-2.png"), 54 | path.join(IMAGES_PATH, "diff.png"), 55 | { 56 | ...options, 57 | threshold: 0.5, 58 | } 59 | ); 60 | 61 | t.is(reason, "pixel-diff"); 62 | t.is(diffCount, 65357); 63 | t.is(diffPercentage, 1.70114931758); 64 | }); 65 | 66 | test("Correctly parses antialiasing", async (t) => { 67 | const { reason, diffCount, diffPercentage } = await compare( 68 | path.join(IMAGES_PATH, "donkey.png"), 69 | path.join(IMAGES_PATH, "donkey-2.png"), 70 | path.join(IMAGES_PATH, "diff.png"), 71 | { 72 | ...options, 73 | antialiasing: true, 74 | } 75 | ); 76 | 77 | t.is(reason, "pixel-diff"); 78 | t.is(diffCount, 101499); 79 | t.is(diffPercentage, 2.64187393218); 80 | }); 81 | 82 | test("Correctly parses ignore regions", async (t) => { 83 | const { match } = await compare( 84 | path.join(IMAGES_PATH, "donkey.png"), 85 | path.join(IMAGES_PATH, "donkey-2.png"), 86 | path.join(IMAGES_PATH, "diff.png"), 87 | { 88 | ...options, 89 | ignoreRegions: [ 90 | { 91 | x1: 749, 92 | y1: 1155, 93 | x2: 1170, 94 | y2: 1603, 95 | }, 96 | { 97 | x1: 657, 98 | y1: 1278, 99 | x2: 742, 100 | y2: 1334, 101 | }, 102 | ], 103 | } 104 | ); 105 | 106 | t.is(match, true); 107 | }); 108 | 109 | test("Outputs correct parsed result when images different for cypress image", async (t) => { 110 | const { reason, diffCount, diffPercentage } = await compare( 111 | path.join(IMAGES_PATH, "www.cypress.io.png"), 112 | path.join(IMAGES_PATH, "www.cypress.io-1.png"), 113 | path.join(IMAGES_PATH, "diff.png"), 114 | options 115 | ); 116 | 117 | t.is(reason, "pixel-diff"); 118 | t.is(diffCount, 1091034); 119 | t.is(diffPercentage, 2.95123808559); 120 | }); 121 | 122 | test("Correctly handles same images", async (t) => { 123 | const { match } = await compare( 124 | path.join(IMAGES_PATH, "donkey.png"), 125 | path.join(IMAGES_PATH, "donkey.png"), 126 | path.join(IMAGES_PATH, "diff.png"), 127 | options 128 | ); 129 | 130 | t.is(match, true); 131 | }); 132 | 133 | test("Correctly outputs diff lines", async (t) => { 134 | const { match, diffLines } = await compare( 135 | path.join(IMAGES_PATH, "donkey.png"), 136 | path.join(IMAGES_PATH, "donkey-2.png"), 137 | path.join(IMAGES_PATH, "diff.png"), 138 | { 139 | captureDiffLines: true, 140 | ...options 141 | } 142 | ); 143 | 144 | t.is(match, false); 145 | t.is(diffLines.length, 402); 146 | }); 147 | 148 | test("Returns meaningful error if file does not exist and noFailOnFsErrors", async (t) => { 149 | const { match, reason, file } = await compare( 150 | path.join(IMAGES_PATH, "not-existing.png"), 151 | path.join(IMAGES_PATH, "not-existing.png"), 152 | path.join(IMAGES_PATH, "diff.png"), 153 | { 154 | ...options, 155 | noFailOnFsErrors: true, 156 | } 157 | ); 158 | 159 | t.is(match, false); 160 | t.is(reason, "file-not-exists"); 161 | t.is(file, path.join(IMAGES_PATH, "not-existing.png")); 162 | }); 163 | -------------------------------------------------------------------------------- /test/node-bindings.test.ts: -------------------------------------------------------------------------------- 1 | import { compare } from "../npm_package/odiff"; 2 | 3 | // allow no options 4 | compare("path1", "path2", "path3") 5 | 6 | // @ts-expect-error options can be only object 7 | compare("path1", "path2", "path3", "") 8 | 9 | // allow partial options 10 | compare("path1", "path2", "path3", { 11 | antialiasing: true, 12 | threshold: 2, 13 | }); 14 | 15 | compare("path1", "path2", "path3", { 16 | antialiasing: true, 17 | threshold: 2, 18 | // @ts-expect-error invalid field 19 | ab: true 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /test/test-images/aa/antialiasing-off-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/aa/antialiasing-off-small.png -------------------------------------------------------------------------------- /test/test-images/aa/antialiasing-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/aa/antialiasing-off.png -------------------------------------------------------------------------------- /test/test-images/aa/antialiasing-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/aa/antialiasing-on.png -------------------------------------------------------------------------------- /test/test-images/bmp/clouds-2.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/bmp/clouds-2.bmp -------------------------------------------------------------------------------- /test/test-images/bmp/clouds-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/bmp/clouds-diff.png -------------------------------------------------------------------------------- /test/test-images/bmp/clouds.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/bmp/clouds.bmp -------------------------------------------------------------------------------- /test/test-images/jpg/tiger-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/jpg/tiger-2.jpg -------------------------------------------------------------------------------- /test/test-images/jpg/tiger-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/jpg/tiger-diff.png -------------------------------------------------------------------------------- /test/test-images/jpg/tiger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/jpg/tiger.jpg -------------------------------------------------------------------------------- /test/test-images/png/diff-output-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/png/diff-output-green.png -------------------------------------------------------------------------------- /test/test-images/png/extreme-alpha-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/png/extreme-alpha-1.png -------------------------------------------------------------------------------- /test/test-images/png/extreme-alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/png/extreme-alpha.png -------------------------------------------------------------------------------- /test/test-images/png/orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/png/orange.png -------------------------------------------------------------------------------- /test/test-images/png/orange_changed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/png/orange_changed.png -------------------------------------------------------------------------------- /test/test-images/png/orange_diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/png/orange_diff.png -------------------------------------------------------------------------------- /test/test-images/png/orange_diff_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/png/orange_diff_green.png -------------------------------------------------------------------------------- /test/test-images/png/purple8x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/png/purple8x8.png -------------------------------------------------------------------------------- /test/test-images/png/white4x4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/png/white4x4.png -------------------------------------------------------------------------------- /test/test-images/tiff/laptops-2.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/tiff/laptops-2.tiff -------------------------------------------------------------------------------- /test/test-images/tiff/laptops-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/tiff/laptops-diff.png -------------------------------------------------------------------------------- /test/test-images/tiff/laptops.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmtrKovalenko/odiff/df03e4a16e05c504342225454c2a318679dd1fe5/test/test-images/tiff/laptops.tiff -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = [ 3 | "package.json" 4 | ] 5 | [default.extend-words] 6 | esy = "esy" 7 | 8 | 9 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", 3 | "builtin-baseline": "fe1cde61e971d53c9687cf9a46308f8f55da19fa", 4 | "dependencies": ["libspng", "tiff", "libjpeg-turbo"] 5 | } 6 | --------------------------------------------------------------------------------