├── .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 |
6 |
7 |
8 |
9 | The fastest* (one-thread) pixel-by-pixel image difference tool in the world.
10 |
11 |
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 | [](https://vshymanskyy.github.io/StandWithUkraine/)
23 |
24 | ## Demo
25 |
26 | | base | comparison | diff |
27 | | ------------------------------ | -------------------------------- | ------------------------------------- |
28 | |  |  |  |
29 | |  |  |  |
30 | |  |  |  |
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 | 
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 |
--------------------------------------------------------------------------------