diff --git a/odiff/.gitignore b/odiff-lib/.gitignore similarity index 60% rename from odiff/.gitignore rename to odiff-lib/.gitignore index 363496b..8f8c270 100644 --- a/odiff/.gitignore +++ b/odiff-lib/.gitignore @@ -1,3 +1,3 @@ + .zig-cache/ -node_modules/ zig-out/ diff --git a/odiff-lib/build.zig b/odiff-lib/build.zig new file mode 100644 index 0000000..6e178df --- /dev/null +++ b/odiff-lib/build.zig @@ -0,0 +1,95 @@ +const std = @import("std"); +const manifest = @import("build.zig.zon"); + +const ndk = @import("ndk.zig"); +const getAndroidTriple = ndk.getAndroidTriple; +const getOutputDir = ndk.getOutputDir; +const createLibC = ndk.createLibC; +const ndk_path = ndk.DEFAULT_NDK_PATH; +const ndk_version = ndk.DEFAULT_NDK_VERSION; +const android_api_version = ndk.DEFAULT_ANDROID_API_VERSION; + +const build_targets: []const std.Target.Query = &.{ + .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .android, .android_api_level = 24 }, + .{ .cpu_arch = .arm, .os_tag = .linux, .abi = .androideabi, .android_api_level = 24 }, + .{ .cpu_arch = .x86, .os_tag = .linux, .abi = .android, .android_api_level = 24 }, + .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .android, .android_api_level = 24 }, +}; + +pub fn build(b: *std.Build) !void { + const optimize = b.standardOptimizeOption(.{}); + + const build_options = b.addOptions(); + build_options.addOption([]const u8, "version", manifest.version); + + for (build_targets) |target_query| { + const target = b.resolveTargetQuery(target_query); + const odiff = b.dependency("odiff", .{ + .target = target, + .optimize = optimize, + }); + + const odiff_lib = odiff.artifact("odiff-lib"); + + const mod = b.addModule("odiff-android-lib", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + .imports = &.{ + .{ .name = "odiff-lib", .module = odiff_lib.root_module }, + }, + }); + + const root_lib = b.addLibrary(.{ + .name = "odiff-lib", + .root_module = mod, + .linkage = .static, + }); + + const abi_output_dir = getOutputDir(target.result) catch |err| { + std.log.err("Unsupported target architecture: {}", .{target.result.cpu.arch}); + return err; + }; + + const header_output_dir = try std.fs.path.join(b.allocator, &.{ abi_output_dir, "include" }); + + const android_triple = try getAndroidTriple(target.result); + const libc_config = createLibC(b, android_triple, android_api_version, ndk_path, ndk_version); + + odiff_lib.setLibCFile(libc_config); + odiff_lib.linkLibC(); + b.installArtifact(odiff_lib); + + const c_test_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }); + c_test_mod.addCSourceFiles(.{ + .files = &.{"src/test-odiff.c"}, + .flags = &.{}, + }); + + const c_test_exe = b.addExecutable(.{ + .name = "test_odiff", + .root_module = c_test_mod, + }); + c_test_exe.linkLibrary(root_lib); + + const install_artifact = b.addInstallArtifact(odiff_lib, .{ + .h_dir = .{ + .override = .{ + .custom = header_output_dir, + }, + }, + .dest_dir = .{ + .override = .{ + .custom = abi_output_dir, + }, + }, + }); + + b.getInstallStep().dependOn(&install_artifact.step); + } +} diff --git a/odiff-lib/build.zig.zon b/odiff-lib/build.zig.zon new file mode 100644 index 0000000..4eca0ba --- /dev/null +++ b/odiff-lib/build.zig.zon @@ -0,0 +1,18 @@ +.{ + .name = .odiff_android_lib, + .version = "0.0.0", + .fingerprint = 0x7b4b77cc05107eb1, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + .url = "git+https://github.com/shreyassanthu77/odiff?ref=refactor-tests#1f53db8c9b882ba84dd9750e0908df4a99476a5d", + .hash = "odiff-4.1.1-bDbIYd0cAgB8D9q44E3OtsAnzXSWNkp1SxTYdF2vask9", + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/odiff/ndk.zig b/odiff-lib/ndk.zig similarity index 100% rename from odiff/ndk.zig rename to odiff-lib/ndk.zig diff --git a/odiff-lib/src/c_odiff_api.zig b/odiff-lib/src/c_odiff_api.zig new file mode 100644 index 0000000..5fc55b7 --- /dev/null +++ b/odiff-lib/src/c_odiff_api.zig @@ -0,0 +1,136 @@ +const std = @import("std"); +const odiff = @import("odiff-lib").diff; +const image_io = @import("odiff-lib").io; + +// C-compatible structs +pub const CDiffOptions = extern struct { + antialiasing: bool, + output_diff_mask: bool, + diff_overlay_factor: f32, // 0.0 if not set + diff_lines: bool, + diff_pixel: u32, + threshold: f64, + fail_on_layout_change: bool, + enable_asm: bool, + ignore_region_count: usize, + ignore_regions: ?[*]const odiff.IgnoreRegion, +}; + +pub const CDiffResult = extern struct { + result_type: c_int, // 0 = layout, 1 = pixel + diff_count: u32, + diff_percentage: f64, + diff_line_count: usize, + diff_lines: ?[*]u32, + diff_output_path: ?[*:0]const u8, // if saved +}; + +pub const COdiffError = enum(c_int) { + success = 0, + image_not_loaded = 1, + unsupported_format = 2, + failed_to_diff = 3, + out_of_memory = 4, + invalid_hex_color = 5, +}; + +fn parseHexColor(hex: []const u8) !u32 { + if (hex.len != 7 or hex[0] != '#') return error.InvalidHexColor; + return std.fmt.parseInt(u32, hex[1..], 16) catch error.InvalidHexColor; +} + +pub export fn odiff_diff( + base_image_path: [*:0]const u8, + comp_image_path: [*:0]const u8, + diff_output_path: ?[*:0]const u8, + c_options: CDiffOptions, +) COdiffError { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var base_img = image_io.loadImage(allocator, std.mem.span(base_image_path)) catch |err| switch (err) { + error.ImageNotLoaded => return .image_not_loaded, + error.UnsupportedFormat => return .unsupported_format, + else => return .failed_to_diff, + }; + defer base_img.deinit(allocator); + + var comp_img = image_io.loadImage(allocator, std.mem.span(comp_image_path)) catch |err| switch (err) { + error.ImageNotLoaded => return .image_not_loaded, + error.UnsupportedFormat => return .unsupported_format, + else => return .failed_to_diff, + }; + defer comp_img.deinit(allocator); + + var ignore_regions: ?[]const odiff.IgnoreRegion = null; + if (c_options.ignore_regions) |regions| { + ignore_regions = regions[0..c_options.ignore_region_count]; + } + + const diff_options = odiff.DiffOptions{ + .antialiasing = c_options.antialiasing, + .output_diff_mask = c_options.output_diff_mask, + .diff_overlay_factor = if (c_options.diff_overlay_factor > 0.0) c_options.diff_overlay_factor else null, + .diff_lines = c_options.diff_lines, + .diff_pixel = c_options.diff_pixel, + .threshold = c_options.threshold, + .ignore_regions = ignore_regions, + .capture_diff = diff_output_path != null, + .fail_on_layout_change = c_options.fail_on_layout_change, + .enable_asm = c_options.enable_asm, + }; + + const result = odiff.diff(&base_img, &comp_img, diff_options, allocator) catch return .failed_to_diff; + + switch (result) { + .layout => { + return .success; // Or define a way to indicate layout diff + }, + .pixel => |pixel_result| { + defer { + if (pixel_result.diff_output) |*output| { + var img = output.*; + img.deinit(allocator); + } + if (pixel_result.diff_lines) |lines| { + var mutable_lines = lines; + mutable_lines.deinit(); + } + } + + if (diff_output_path) |output_path| { + if (pixel_result.diff_output) |output_img| { + image_io.saveImage(output_img, std.mem.span(output_path)) catch return .failed_to_diff; + } + } + + return .success; + }, + } +} + +pub export fn odiff_free_diff_lines(diff_lines: [*]u32, count: usize) void { + const slice = diff_lines[0..count]; + std.heap.page_allocator.free(slice); +} + +pub export fn parse_hex_color(hex: [*:0]const u8) u32 { + const hex_str = std.mem.span(hex); + if (hex_str.len == 0) return 0xFF0000FF; + + var color_str = hex_str; + if (hex_str[0] == '#') { + color_str = hex_str[1..]; + } + + if (color_str.len != 6) { + @panic("Invalid Hex Color"); + } + + const r = std.fmt.parseInt(u8, color_str[0..2], 16) catch @panic("R is missing"); + const g = std.fmt.parseInt(u8, color_str[2..4], 16) catch @panic("G is missing"); + const b = std.fmt.parseInt(u8, color_str[4..6], 16) catch @panic("B is missing"); + + return (@as(u32, 255) << 24) | (@as(u32, b) << 16) | (@as(u32, g) << 8) | @as(u32, r); +} diff --git a/odiff/src/root.zig b/odiff-lib/src/root.zig similarity index 61% rename from odiff/src/root.zig rename to odiff-lib/src/root.zig index d8222cb..dd25058 100644 --- a/odiff/src/root.zig +++ b/odiff-lib/src/root.zig @@ -1,18 +1,10 @@ const std = @import("std"); -pub const cli = @import("cli.zig"); -pub const image_io = @import("image_io.zig"); -pub const diff = @import("diff.zig"); -pub const color_delta = @import("color_delta.zig"); -pub const antialiasing = @import("antialiasing.zig"); -pub const bmp_reader = @import("bmp_reader.zig"); -pub const c_bindings = @import("c_bindings.zig"); -pub const c_api = @import("c_api.zig"); +pub const c_api = @import("c_odiff_api.zig"); // Ensure C API functions are included comptime { _ = c_api.odiff_diff; - _ = c_api.odiff_diff_with_results; _ = c_api.odiff_free_diff_lines; } diff --git a/odiff-lib/src/test-odiff.c b/odiff-lib/src/test-odiff.c new file mode 100644 index 0000000..e63317b --- /dev/null +++ b/odiff-lib/src/test-odiff.c @@ -0,0 +1,75 @@ +#include +#include +#include + +typedef struct { + int antialiasing; + int output_diff_mask; + float diff_overlay_factor; + int diff_lines; + unsigned int diff_pixel; + double threshold; + int fail_on_layout_change; + int enable_asm; + size_t ignore_region_count; + void* ignore_regions; // array of IgnoreRegion +} CDiffOptions; + +typedef struct { + int result_type; // 0 = layout, 1 = pixel + unsigned int diff_count; + double diff_percentage; + size_t diff_line_count; + unsigned int* diff_lines; + const char* diff_output_path; +} CDiffResult; + +typedef enum { + SUCCESS = 0, + IMAGE_NOT_LOADED = 1, + UNSUPPORTED_FORMAT = 2, + FAILED_TO_DIFF = 3, + OUT_OF_MEMORY = 4, + INVALID_HEX_COLOR = 5, +} COdiffError; + +// Declare the exported functions +COdiffError odiff_diff(const char* base_image_path, const char* comp_image_path, const char* diff_output_path, CDiffOptions options); +COdiffError odiff_diff_with_results(const char* base_image_path, const char* comp_image_path, const char* diff_output_path, CDiffOptions options, CDiffResult* out_result); +void odiff_free_diff_lines(unsigned int* diff_lines, size_t count); +int parse_hex_color(const char* hex_str); + +// No custom allocator needed + +int main(int argc, char* argv[]) { + if (argc < 3) { + printf("Usage: %s [diff_output]\n", argv[0]); + return 1; + } + + const char* base_path = argv[1]; + const char* comp_path = argv[2]; + const char* diff_output = (argc >= 4) ? argv[3] : NULL; + + int diff_pixel = parse_hex_color(""); + + // Set up options + CDiffOptions options = { + .antialiasing = 0, + .output_diff_mask = 0, + .diff_overlay_factor = 0.0f, + .diff_lines = 0, + .diff_pixel = diff_pixel, + .threshold = 0.1, + .fail_on_layout_change = 0, + .enable_asm = 0, + .ignore_region_count = 0, + .ignore_regions = NULL, + }; + + CDiffResult result; + COdiffError err = odiff_diff(base_path, comp_path, diff_output, options); + + return 0; +} + diff --git a/odiff/LICENSE.txt b/odiff/LICENSE.txt deleted file mode 100644 index f61c36e..0000000 --- a/odiff/LICENSE.txt +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2021 Dmitriy Kovalenko - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/odiff/README.md b/odiff/README.md deleted file mode 100644 index 38234b2..0000000 --- a/odiff/README.md +++ /dev/null @@ -1,224 +0,0 @@ -

- - - - pixeletad caml and odiff text with highlighted red pixels difference - -

- -

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

- -
- made with reason - npm - -
- -## Why Odiff? - -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**. ODiff is designed speicifcally to handle significantly similar images like screenshots, photos, AI-generated images and many more. ODiff is designed to be portable, fast, and memory efficient. - -Originally written in OCaml, currently in Zig with SIMD optimizations for SSE2, AVX2, AVX512, and NEON. - -## Demo - -| base | comparison | diff | -| ------------------------------ | -------------------------------- | ------------------------------------- | -| ![](images/tiger.jpg) | ![](images/tiger-2.jpg) | ![1diff](images/tiger-diff.png) | -| ![](images/www.cypress.io.png) | ![](images/www.cypress.io-1.png) | ![1diff](images/www.cypress-diff.png) | -| ![](images/donkey.png) | ![](images/donkey-2.png) | ![1diff](images/donkey-diff.png) | - -## Features - -- ✅ Cross-format comparison - Yes .jpg vs .png comparison without any problems. -- ✅ Support for `.png`, `.jpeg`, `.jpg`, `.webp`, and `.tiff` -- ✅ Supports comparison of images with different layouts. -- ✅ Anti-aliasing detection -- ✅ Ignoring regions -- ✅ Using [YIQ NTSC - transmission algorithm](https://progmat.uaem.mx/progmat/index.php/progmat/article/view/2010-2-2-03/2010-2-2-03) to determine visual difference. -- ✅ SIMD optimized working for SSE2, AVX2, AVX512, and NEON -- ✅ Controlled memory footprint -- ✅ 100% test coverage and backward compatibility - -## Usage - -### Basic comparison - -Run the simple comparison. Image paths can be one of supported formats, diff output is optional and can only be `.png`. - -``` -odiff [DIFF output path] -``` - -### Node.js - -We also provides direct node.js binding for the `odiff`. Run the `odiff` from nodejs: - -```js -const { compare } = require("odiff-bin"); - -const { match, reason } = await compare( - "path/to/first/image.png", - "path/to/second/image.png", - "path/to/diff.png", -); -``` - -### Cypress - -Checkout [cypress-odiff](https://github.com/odai-alali/cypress-odiff), a cypress plugin to add visual regression tests using `odiff-bin`. - -### Visual regression services - -[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 - -[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)) - -[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 - -[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. - -## Api - -Here is an api reference: - -### CLI - -The best way to get up-to-date cli interface is just to type the - -``` -odiff --help -``` - -### Node.js - -NodeJS Api is pretty tiny as well. Here is a typescript interface we have: - - -```tsx -export type ODiffOptions = Partial<{ - /** Color used to highlight different pixels in the output (in hex format e.g. #cd2cc9). */ - diffColor: string; - /** Output full diff image. */ - outputDiffMask: boolean; - /** Outputs diff images with a white shaded overlay for easier diff reading */ - diffOverlay: boolean | number; - /** Do not compare images and produce output if images layout is different. */ - failOnLayoutDiff: boolean; - /** Return { match: false, reason: '...' } instead of throwing error if file is missing. */ - noFailOnFsErrors: boolean; - /** Color difference threshold (from 0 to 1). Less more precise. */ - threshold: number; - /** If this is true, antialiased pixels are not counted to the diff of an image */ - antialiasing: boolean; - /** If `true` reason: "pixel-diff" output will contain the set of line indexes containing different pixels */ - captureDiffLines: boolean; - /** If `true` odiff will use less memory but will be slower with larger images */ - reduceRamUsage: boolean; - /** An array of regions to ignore in the diff. */ - ignoreRegions: Array<{ - x1: number; - y1: number; - x2: number; - y2: number; - }>; -}>; - -declare function compare( - basePath: string, - comparePath: string, - diffPath: string, - options?: ODiffOptions -): Promise< - | { match: true } - | { match: false; reason: "layout-diff" } - | { - match: false; - reason: "pixel-diff"; - /** Amount of different pixels */ - diffCount: number; - /** Percentage of different pixels in the whole image */ - diffPercentage: number; - /** Individual line indexes containing different pixels. Guaranteed to be ordered and distinct. */ - diffLines?: number[]; - } - | { - match: false; - reason: "file-not-exists"; - /** Errored file path */ - file: string; - } ->; - -export { compare }; -``` -" - -Compare option will return `{ match: true }` if images are identical. Otherwise return `{ match: false, reason: "*" }` with a reason why images were different. - -> Make sure that diff output file will be created only if images have pixel difference we can see 👀 - -## Installation - -We provide prebuilt binaries for most of the used platforms, there are a few ways to install them: - -### Cross-platform - -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. - -> **Important**: package name is **odiff-bin**. But the binary itself is **odiff** - -``` -npm install odiff-bin -``` - -Then give it a try 👀 - -``` -odiff --help -``` - -### From binaries - -Download the binaries for your platform from [release](https://github.com/dmtrKovalenko/odiff/releases) page. - -## Benchmarks - -> Run the benchmarks by yourself. Instructions of how to run the benchmark is [here](./images) - -![benchmark](images/benchmarks.png) - -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. - -``` -3s * 25000 / 3600 = 20,83333 hours -``` - -Here is `odiff` performance comparison with other popular visual difference solutions. We are going to compare some real-world use cases. - -Lets compare 2 screenshots of full-size [https::/cypress.io](cypress.io) page: - -| Command | Mean [s] | Min [s] | Max [s] | Relative | -| :----------------------------------------------------------------------------------------- | ------------: | ------: | ------: | ----------: | -| `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 | -| 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 | -| `odiff www.cypress.io-1.png www.cypress.io.png www.cypress-diff.png` | 1.168 ± 0.008 | 1.157 | 1.185 | 1.00 | - -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): - -| Command | Mean [s] | Min [s] | Max [s] | Relative | -| :---------------------------------------------------------------------------- | -------------: | ------: | ------: | ----------: | -| `pixelmatch water-4k.png water-4k-2.png water-diff.png` | 10.614 ± 0.162 | 10.398 | 10.910 | 5.50 ± 0.05 | -| 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 | -| `odiff water-4k.png water-4k-2.png water-diff.png` | 1.951 ± 0.014 | 1.936 | 1.981 | 1.00 | - -Yes it is significant improvement. And the produced difference will be the same for all 3 commands. - -## Changelog - -If you have recently updated, please read the [changelog](https://github.com/dmtrKovalenko/odiff/releases) for details of what has changed. - -## License - -The project is licensed under the terms of [MIT license](./LICENSE) diff --git a/odiff/ava.config.cjs b/odiff/ava.config.cjs deleted file mode 100644 index d1d2aa8..0000000 --- a/odiff/ava.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - files: ["test/*"], -}; diff --git a/odiff/build.zig b/odiff/build.zig deleted file mode 100644 index 6239fd7..0000000 --- a/odiff/build.zig +++ /dev/null @@ -1,186 +0,0 @@ -const std = @import("std"); -const manifest = @import("build.zig.zon"); -const Imgz = @import("imgz"); - -const ndk = @import("ndk.zig"); -const getAndroidTriple = ndk.getAndroidTriple; -const getOutputDir = ndk.getOutputDir; -const createLibC = ndk.createLibC; -const ndk_path = ndk.DEFAULT_NDK_PATH; -const ndk_version = ndk.DEFAULT_NDK_VERSION; -const android_api_version = ndk.DEFAULT_ANDROID_API_VERSION; - -const build_targets: []const std.Target.Query = &.{ - .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .android, .android_api_level = 24 }, - .{ .cpu_arch = .arm, .os_tag = .linux, .abi = .androideabi, .android_api_level = 24 }, - .{ .cpu_arch = .x86, .os_tag = .linux, .abi = .android, .android_api_level = 24 }, - .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .android, .android_api_level = 24 }, -}; - -pub fn build(b: *std.Build) !void { - const optimize = b.standardOptimizeOption(.{}); - const dynamic = b.option(bool, "dynamic", "Link against libspng, libjpeg and libtiff dynamically") orelse false; - - const build_options = b.addOptions(); - build_options.addOption([]const u8, "version", manifest.version); - - for (build_targets) |target_query| { - const target = b.resolveTargetQuery(target_query); - const abi_output_dir = getOutputDir(target.result) catch |err| { - std.log.err("Unsupported target architecture: {}", .{target.result.cpu.arch}); - return err; - }; - - const header_output_dir = try std.fs.path.join(b.allocator, &.{ abi_output_dir, "include" }); - const build_options_mod = build_options.createModule(); - - const android_triple = try getAndroidTriple(target.result); - const libc_config = createLibC(b, android_triple, android_api_version, ndk_path, ndk_version); - - const lib_mod, _ = buildOdiff(b, target, optimize, dynamic, build_options_mod, libc_config); - const root_lib = b.addLibrary(.{ - .name = "odiff", - .root_module = lib_mod, - .linkage = .static, - }); - root_lib.setLibCFile(libc_config); - - root_lib.linkLibC(); - - b.installArtifact(root_lib); - - const install_artifact = b.addInstallArtifact(root_lib, .{ - .h_dir = .{ - .override = .{ - .custom = header_output_dir, - }, - }, - .dest_dir = .{ - .override = .{ - .custom = abi_output_dir, - }, - }, - }); - - b.getInstallStep().dependOn(&install_artifact.step); - } -} - -fn addInstallArtifact(b: *std.Build, lib: *std.Build.Step.Compile, step: *std.Build.Step, header_output_dir: []const u8, abi_output_dir: []const u8) void { - const zlib_install_artifact = b.addInstallArtifact(lib, .{ - .h_dir = .{ - .override = .{ - .custom = header_output_dir, - }, - }, - .dest_dir = .{ - .override = .{ - .custom = abi_output_dir, - }, - }, - }); - - step.dependOn(&zlib_install_artifact.step); -} - -fn buildOdiff( - b: *std.Build, - target: std.Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, - dynamic: bool, - build_options_mod: *std.Build.Module, - libc_config: std.Build.LazyPath, -) struct { *std.Build.Module, *std.Build.Step.Compile } { - const lib_mod = b.createModule(.{ - .root_source_file = b.path("src/root.zig"), - .target = target, - .optimize = optimize, - .link_libc = true, - }); - linkDeps(b, target, optimize, dynamic, lib_mod, libc_config); - - var c_flags = std.array_list.Managed([]const u8).init(b.allocator); - defer c_flags.deinit(); - c_flags.append("-std=c99") catch @panic("OOM"); - c_flags.append("-Wno-nullability-completeness") catch @panic("OOM"); - c_flags.append("-DHAVE_SPNG") catch @panic("OOM"); - c_flags.append("-DSPNG_STATIC") catch @panic("OOM"); - c_flags.append("-DSPNG_SSE=3") catch @panic("OOM"); - c_flags.append("-DHAVE_JPEG") catch @panic("OOM"); - c_flags.append("-DHAVE_TIFF") catch @panic("OOM"); - c_flags.append("-DHAVE_WEBP") catch @panic("OOM"); - c_flags.append("-fno-sanitize=undefined") catch @panic("OOM"); - - lib_mod.addCSourceFiles(.{ - .files = &.{ - "c_bindings/odiff_io.c", - "src/rvv.c", - }, - .flags = c_flags.items, - }); - - const exe_mod = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - - exe_mod.addImport("odiff_lib", lib_mod); - exe_mod.addImport("build_options", build_options_mod); - lib_mod.addImport("build_options", build_options_mod); - - if (target.result.cpu.arch == .x86_64) { - const os_tag = target.result.os.tag; - const fmt: ?[]const u8 = switch (os_tag) { - .linux => "elf64", - .macos => "macho64", - else => null, - }; - - if (fmt) |nasm_fmt| { - const nasm = b.addSystemCommand(&.{ "nasm", "-f", nasm_fmt, "-o" }); - const asm_obj = nasm.addOutputFileArg("vxdiff.o"); - nasm.addFileArg(b.path("src/vxdiff.asm")); - lib_mod.addObjectFile(asm_obj); - } - } - - const exe = b.addExecutable(.{ - .name = "odiff", - .root_module = exe_mod, - }); - - return .{ lib_mod, exe }; -} - -pub fn linkDeps(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, dynamic: bool, module: *std.Build.Module, libc_config: std.Build.LazyPath) void { - const host_target = b.graph.host.result; - const build_target = target.result; - const is_cross_compiling = host_target.cpu.arch != build_target.cpu.arch or - host_target.os.tag != build_target.os.tag; - if (dynamic and !is_cross_compiling) { - switch (build_target.os.tag) { - .windows => { - std.log.warn("Dynamic linking is not supported on Windows, falling back to static linking", .{}); - return linkDeps(b, target, optimize, false, module, libc_config); - }, - else => { - module.linkSystemLibrary("spng", .{}); - module.linkSystemLibrary("jpeg", .{}); - module.linkSystemLibrary("tiff", .{}); - }, - } - } else { - Imgz.addToModule(b, module, .{ - .target = target, - .optimize = optimize, - .jpeg_turbo = .{ - .simd = target.result.cpu.arch != .arm, - }, - .spng = .{}, - .tiff = .{}, - .webp = .{}, - .libc_file = libc_config, - }) catch @panic("Failed to link required dependencies, please create an issue on the repo :)"); - } -} diff --git a/odiff/build.zig.zon b/odiff/build.zig.zon deleted file mode 100644 index 8da3492..0000000 --- a/odiff/build.zig.zon +++ /dev/null @@ -1,20 +0,0 @@ -.{ - .name = .odiff, - .version = "4.1.1", - .fingerprint = 0xb9748f3661c8366c, - .minimum_zig_version = "0.15.1", - .dependencies = .{ - .imgz = .{ - .url = "git+https://github.com/shreyassanthu77/imgz.git?ref=custom-libc#14f7b7e1c234fcff7ea044a561999df3ca75b01d", - .hash = "imgz-0.2.0-BqHzkEtuCwDMZt6Bz-_hHFAF8625FY10LQGL4J2o49E0", - }, - }, - - .paths = .{ - "build.zig", - "build.zig.zon", - "src", - "LICENSE", - "README.md", - }, -} diff --git a/odiff/c_bindings/ReadJpg.c b/odiff/c_bindings/ReadJpg.c deleted file mode 100644 index 63140f3..0000000 --- a/odiff/c_bindings/ReadJpg.c +++ /dev/null @@ -1,81 +0,0 @@ -#define CAML_NAME_SPACE - -#include - -#include - -#include -#include -#include -#include -#include - -CAMLprim value -read_jpeg_file_to_tuple(value file) -{ - CAMLparam1(file); - CAMLlocal2(res, ba); - - size_t size; - struct jpeg_error_mgr jerr; - struct jpeg_decompress_struct cinfo; - - jpeg_create_decompress(&cinfo); - cinfo.err = jpeg_std_error(&jerr); - - const char *filename = String_val(file); - FILE *fp = fopen(filename, "rb"); - if (!fp) { - caml_failwith("opening input file failed!"); - } - if (fseek(fp, 0, SEEK_END) < 0 || ((size = ftell(fp)) < 0) || fseek(fp, 0, SEEK_SET) < 0) { - fclose(fp); - caml_failwith("determining input file size failed"); - } - if (size == 0) { - fclose(fp); - caml_failwith("Input file contains no data"); - } - - jpeg_stdio_src(&cinfo, fp); - jpeg_read_header(&cinfo, TRUE); - jpeg_start_decompress(&cinfo); - - uint32_t width = cinfo.output_width; - uint32_t height = cinfo.output_height; - uint32_t channels = cinfo.output_components; - - JDIMENSION stride = width * channels; - JSAMPARRAY temp_buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, stride, 1); - - int buffer_size = width * height * 4; - intnat dims[1] = {buffer_size}; - ba = caml_ba_alloc(CAML_BA_UINT8 | CAML_BA_C_LAYOUT | CAML_BA_MANAGED, 1, NULL, dims); - uint8_t *image_buffer = (uint8_t *)Caml_ba_data_val(ba); - - while (cinfo.output_scanline < height) { - jpeg_read_scanlines(&cinfo, temp_buffer, 1); - - unsigned int k = (cinfo.output_scanline - 1) * 4 * width; - unsigned int j = 0; - for(unsigned int i = 0; i < 4 * width; i += 4) { - image_buffer[k + i] = temp_buffer[0][j]; - image_buffer[k + i + 1] = temp_buffer[0][j + 1]; - image_buffer[k + i + 2] = temp_buffer[0][j + 2]; - image_buffer[k + i + 3] = 255; - - j += 3; - } - } - - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); - fclose(fp); - - res = caml_alloc_tuple(3); - Store_field(res, 0, Val_int(width)); - Store_field(res, 1, Val_int(height)); - Store_field(res, 2, ba); - - CAMLreturn(res); -} diff --git a/odiff/c_bindings/ReadPng.c b/odiff/c_bindings/ReadPng.c deleted file mode 100644 index 6526232..0000000 --- a/odiff/c_bindings/ReadPng.c +++ /dev/null @@ -1,80 +0,0 @@ -#define CAML_NAME_SPACE - -#include - -#include -#include -#include -#include -#include - -CAMLprim value read_png_file(value file) { - CAMLparam1(file); - CAMLlocal2(res, ba); - - int result = 0; - FILE *png; - spng_ctx *ctx = NULL; - const char *filename = String_val(file); - - png = fopen(filename, "rb"); - if (png == NULL) { - caml_failwith("error opening input file"); - } - - ctx = spng_ctx_new(0); - if (ctx == NULL) { - fclose(png); - caml_failwith("spng_ctx_new() failed"); - } - - /* Ignore and don't calculate chunk CRC's */ - spng_set_crc_action(ctx, SPNG_CRC_USE, SPNG_CRC_USE); - - /* Set memory usage limits for storing standard and unknown chunks, - this is important when reading untrusted files! */ - size_t limit = 1024 * 1024 * 64; - spng_set_chunk_limits(ctx, limit, limit); - - /* Set source PNG */ - spng_set_png_file(ctx, png); - - struct spng_ihdr ihdr; - result = spng_get_ihdr(ctx, &ihdr); - - if (result) { - spng_ctx_free(ctx); - fclose(png); - caml_failwith("spng_get_ihdr() error!"); - } - - size_t out_size; - result = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &out_size); - if (result) { - spng_ctx_free(ctx); - fclose(png); - caml_failwith(spng_strerror(result)); - }; - - ba = caml_ba_alloc(CAML_BA_UINT8 | CAML_BA_C_LAYOUT | CAML_BA_MANAGED, 1, - NULL, &out_size); - unsigned char *out = (unsigned char *)Caml_ba_data_val(ba); - - result = - spng_decode_image(ctx, out, out_size, SPNG_FMT_RGBA8, SPNG_DECODE_TRNS); - if (result) { - spng_ctx_free(ctx); - fclose(png); - caml_failwith(spng_strerror(result)); - } - - spng_ctx_free(ctx); - fclose(png); - - res = caml_alloc_tuple(3); - Store_field(res, 0, Val_int(ihdr.width)); - Store_field(res, 1, Val_int(ihdr.height)); - Store_field(res, 2, ba); - - CAMLreturn(res); -} diff --git a/odiff/c_bindings/ReadTiff.c b/odiff/c_bindings/ReadTiff.c deleted file mode 100644 index 58b60b5..0000000 --- a/odiff/c_bindings/ReadTiff.c +++ /dev/null @@ -1,56 +0,0 @@ -#define CAML_NAME_SPACE -#include -#include -#include -#include -#include -#include - -#ifndef _WIN32 -#include - -CAMLprim value read_tiff_file_to_tuple(value file) { - CAMLparam1(file); - CAMLlocal2(res, ba); - - const char *filename = String_val(file); - int width; - int height; - - TIFF *image; - - if (!(image = TIFFOpen(filename, "r"))) { - caml_failwith("opening input file failed!"); - } - - TIFFGetField(image, TIFFTAG_IMAGEWIDTH, &width); - TIFFGetField(image, TIFFTAG_IMAGELENGTH, &height); - - int buffer_size = width * height; - - intnat dims[1] = {buffer_size}; - ba = caml_ba_alloc(CAML_BA_INT32 | CAML_BA_C_LAYOUT | CAML_BA_MANAGED, 1, - NULL, dims); - - uint32_t *buffer = (uint32_t *)Caml_ba_data_val(ba); - - if (!(TIFFReadRGBAImageOriented(image, width, height, buffer, - ORIENTATION_TOPLEFT, 0))) { - TIFFClose(image); - caml_failwith("reading input file failed"); - } - - TIFFClose(image); - - res = caml_alloc_tuple(3); - Store_field(res, 0, Val_int(width)); - Store_field(res, 1, Val_int(height)); - Store_field(res, 2, ba); - - CAMLreturn(res); -} -#else -CAMLprim value read_tiff_file_to_tuple(value file) { - caml_failwith("Tiff files are not supported on Windows platform"); -} -#endif diff --git a/odiff/c_bindings/WritePng.c b/odiff/c_bindings/WritePng.c deleted file mode 100644 index 492d58c..0000000 --- a/odiff/c_bindings/WritePng.c +++ /dev/null @@ -1,117 +0,0 @@ -#define CAML_NAME_SPACE - -#include -#include -#include -#include - -#include -#include -#include - -#include - -char *concat(const char *s1, const char *s2) { - char *result = - malloc(strlen(s1) + strlen(s2) + 1); // +1 for the null-terminator - - if (result == NULL) { - caml_failwith("Can not concat strings"); - } - - strcpy(result, s1); - strcat(result, s2); - - return result; -} - -value write_png_bigarray(value filename_val, value bigarray, value width_val, - value height_val) { - CAMLparam4(filename_val, bigarray, width_val, height_val); - - int width = Int_val(width_val); - int height = Int_val(height_val); - const char *data = Caml_ba_data_val(bigarray); - const char *filename = String_val(filename_val); - - FILE *fp; - if ((fp = fopen(filename, "wb")) == NULL) { - char *err = strerror(errno); - char *message = concat("Can not write diff output. fopen error: ", err); - - caml_failwith(message); - - free(err); - free(message); - } - - int result = 0; - - uint8_t bit_depth = 8; - uint8_t color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; - uint8_t compression_method = 0; - uint8_t filter_method = SPNG_FILTER_NONE; - uint8_t interlace_method = SPNG_INTERLACE_NONE; - - size_t out_size = width * height * 4; - size_t out_width = out_size / height; - - spng_ctx *ctx = spng_ctx_new(SPNG_CTX_ENCODER); - struct spng_ihdr ihdr = { - width, - height, - bit_depth, - color_type, - compression_method, - filter_method, - interlace_method, - }; - - result = spng_set_ihdr(ctx, &ihdr); - if (result) { - spng_ctx_free(ctx); - fclose(fp); - caml_failwith(spng_strerror(result)); - } - - result = spng_set_option(ctx, SPNG_FILTER_CHOICE, SPNG_DISABLE_FILTERING); - if (result) { - spng_ctx_free(ctx); - fclose(fp); - caml_failwith(spng_strerror(result)); - } - - result = spng_set_png_file(ctx, fp); - if (result) { - fclose(fp); - spng_ctx_free(ctx); - caml_failwith(spng_strerror(result)); - } - - result = spng_encode_image(ctx, 0, 0, SPNG_FMT_PNG, SPNG_ENCODE_PROGRESSIVE); - - if (result) { - fclose(fp); - spng_ctx_free(ctx); - caml_failwith(spng_strerror(result)); - } - - for (int i = 0; i < ihdr.height; i++) { - const char *row = data + out_width * i; - result = spng_encode_scanline(ctx, row, out_width); - if (result) - break; - } - - if (result != SPNG_EOI) { - spng_ctx_free(ctx); - fclose(fp); - caml_failwith(spng_strerror(result)); - } - - spng_encode_chunks(ctx); - spng_ctx_free(ctx); - fclose(fp); - - CAMLreturn(Val_unit); -} diff --git a/odiff/c_bindings/odiff_io.c b/odiff/c_bindings/odiff_io.c deleted file mode 100644 index 89ba87f..0000000 --- a/odiff/c_bindings/odiff_io.c +++ /dev/null @@ -1,472 +0,0 @@ -#ifdef __linux__ -#define _GNU_SOURCE -#endif - -#include "odiff_io.h" -#include -#include -#include -#include -#if defined(HAVE_JPEG) -#include -#endif -#if defined(HAVE_SPNG) -#include -#endif -#if defined(HAVE_TIFF) -#include -#endif -#if defined(HAVE_WEBP) -#include -#endif - -#ifdef _WIN32 -#include -#include -#else -#include -#include -#include -#include -#endif - -// Cross-platform file buffer for memory mapping -typedef struct { - void *data; - size_t size; -#ifdef _WIN32 - HANDLE hFile; - HANDLE hMapping; -#else - int fd; -#endif -} FileBuffer; - -// Initialize file buffer with memory mapping -static FileBuffer open_file_buffer(const char *filename) { - FileBuffer buffer = {0}; - -#ifdef _WIN32 - buffer.hFile = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL, - OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (buffer.hFile == INVALID_HANDLE_VALUE) - return buffer; - - LARGE_INTEGER file_size; - if (!GetFileSizeEx(buffer.hFile, &file_size) || file_size.QuadPart == 0) { - CloseHandle(buffer.hFile); - buffer.hFile = INVALID_HANDLE_VALUE; - return buffer; - } - - buffer.hMapping = - CreateFileMappingA(buffer.hFile, NULL, PAGE_READONLY, 0, 0, NULL); - if (!buffer.hMapping) { - CloseHandle(buffer.hFile); - buffer.hFile = INVALID_HANDLE_VALUE; - return buffer; - } - - buffer.data = MapViewOfFile(buffer.hMapping, FILE_MAP_READ, 0, 0, 0); - if (!buffer.data) { - CloseHandle(buffer.hMapping); - CloseHandle(buffer.hFile); - buffer.hFile = INVALID_HANDLE_VALUE; - return buffer; - } - - buffer.size = (size_t)file_size.QuadPart; - -#else - buffer.fd = open(filename, O_RDONLY); - if (buffer.fd == -1) - return buffer; - - struct stat st; - if (fstat(buffer.fd, &st) == -1 || st.st_size <= 0) { - close(buffer.fd); - buffer.fd = -1; - return buffer; - } - - buffer.data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, buffer.fd, 0); - if (buffer.data == MAP_FAILED) { - close(buffer.fd); - buffer.fd = -1; - buffer.data = NULL; - return buffer; - } - -#ifdef MADV_SEQUENTIAL - madvise(buffer.data, st.st_size, MADV_SEQUENTIAL); -#endif - buffer.size = st.st_size; -#endif - - return buffer; -} - -// Cleanup file buffer -static void close_file_buffer(FileBuffer *buffer) { - if (!buffer) - return; - -#ifdef _WIN32 - if (buffer->data) { - UnmapViewOfFile(buffer->data); - buffer->data = NULL; - } - if (buffer->hMapping) { - CloseHandle(buffer->hMapping); - buffer->hMapping = NULL; - } - if (buffer->hFile != INVALID_HANDLE_VALUE) { - CloseHandle(buffer->hFile); - buffer->hFile = INVALID_HANDLE_VALUE; - } -#else - if (buffer->data && buffer->data != MAP_FAILED) { - munmap(buffer->data, buffer->size); - buffer->data = NULL; - } - if (buffer->fd != -1) { - close(buffer->fd); - buffer->fd = -1; - } -#endif - buffer->size = 0; -} - -ImageData read_png_file(const char *filename, void *allocator) { -#if defined(HAVE_SPNG) - ImageData result = {0, 0, NULL}; - - // Memory-map the PNG file (cross-platform) - FileBuffer file_buf = open_file_buffer(filename); - if (!file_buf.data) - return result; - - spng_ctx *ctx = spng_ctx_new(0); - if (ctx == NULL) { - close_file_buffer(&file_buf); - return result; - } - - // Ignore and don't calculate chunk CRC's for better performance - spng_set_crc_action(ctx, SPNG_CRC_USE, SPNG_CRC_USE); - - // Set memory usage limits - size_t limit = 1024 * 1024 * 64; - spng_set_chunk_limits(ctx, limit, limit); - - // Set source PNG from memory-mapped data - int spng_result = spng_set_png_buffer(ctx, file_buf.data, file_buf.size); - if (spng_result) { - spng_ctx_free(ctx); - close_file_buffer(&file_buf); - return result; - } - - struct spng_ihdr ihdr; - spng_result = spng_get_ihdr(ctx, &ihdr); - if (spng_result) { - spng_ctx_free(ctx); - close_file_buffer(&file_buf); - return result; - } - - size_t out_size; - spng_result = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &out_size); - if (spng_result) { - spng_ctx_free(ctx); - close_file_buffer(&file_buf); - return result; - } - - result.width = ihdr.width; - result.height = ihdr.height; - result.data = (uint32_t *)zig_alloc(allocator, ihdr.width * ihdr.height * - sizeof(uint32_t)); - if (!result.data) { - spng_ctx_free(ctx); - close_file_buffer(&file_buf); - return result; - } - - spng_result = spng_decode_image(ctx, result.data, out_size, SPNG_FMT_RGBA8, - SPNG_DECODE_TRNS); - if (spng_result) { - zig_free(allocator, result.data, - ihdr.width * ihdr.height * sizeof(uint32_t)); - result.data = NULL; - spng_ctx_free(ctx); - close_file_buffer(&file_buf); - return result; - } - - spng_ctx_free(ctx); - close_file_buffer(&file_buf); - return result; -#else - fprintf(stderr, "SPNG support not enabled\n"); - abort(); -#endif -} - -int write_png_file(const char *filename, int width, int height, - const uint32_t *data) { -#if defined(HAVE_SPNG) - FILE *fp = fopen(filename, "wb"); - if (fp == NULL) { - fprintf(stderr, "Failed to open file for writing: %s\n", filename); - return -1; - } - - spng_ctx *ctx = spng_ctx_new(SPNG_CTX_ENCODER); - if (ctx == NULL) { - fclose(fp); - return -1; - } - - struct spng_ihdr ihdr = { - .width = width, - .height = height, - .bit_depth = 8, - .color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA, - .compression_method = 0, - .filter_method = SPNG_FILTER_NONE, - .interlace_method = SPNG_INTERLACE_NONE, - }; - - int result = spng_set_ihdr(ctx, &ihdr); - if (result) { - fprintf(stderr, "spng_set_ihdr failed: %s\n", spng_strerror(result)); - spng_ctx_free(ctx); - fclose(fp); - return -1; - } - - result = spng_set_option(ctx, SPNG_FILTER_CHOICE, SPNG_DISABLE_FILTERING); - if (result) { - spng_ctx_free(ctx); - fclose(fp); - return -1; - } - - result = spng_set_png_file(ctx, fp); - if (result) { - spng_ctx_free(ctx); - fclose(fp); - return -1; - } - - // Start progressive encoding - use SPNG_FMT_PNG like the original odiff - result = spng_encode_image(ctx, 0, 0, SPNG_FMT_PNG, SPNG_ENCODE_PROGRESSIVE); - if (result) { - fprintf(stderr, "spng_encode_image (start) failed: %s\n", - spng_strerror(result)); - spng_ctx_free(ctx); - fclose(fp); - return -1; - } - - // Encode scanlines - pass data directly like original odiff - size_t bytes_per_row = width * 4; // 4 bytes per pixel (RGBA) - const char *byte_data = (const char *)data; - - for (int i = 0; i < height; i++) { - const char *row = byte_data + bytes_per_row * i; - result = spng_encode_scanline(ctx, row, bytes_per_row); - if (result) - break; - } - - // Check if encoding completed successfully (SPNG_EOI means end of image - - // success) - if (result != SPNG_EOI) { - fprintf(stderr, "PNG encoding failed: %s\n", spng_strerror(result)); - spng_ctx_free(ctx); - fclose(fp); - return -1; - } - - // Finalize encoding by writing remaining chunks - spng_encode_chunks(ctx); - - spng_ctx_free(ctx); - fclose(fp); - return 0; -#else - fprintf(stderr, "SPNG support not enabled\n"); - abort(); -#endif -} - -ImageData read_jpg_file(const char *filename, void *allocator) { -#if defined(HAVE_JPEG) - ImageData result = {0, 0, NULL}; - struct jpeg_error_mgr jerr; - struct jpeg_decompress_struct cinfo; - - jpeg_create_decompress(&cinfo); - cinfo.err = jpeg_std_error(&jerr); - - FILE *fp = fopen(filename, "rb"); - if (!fp) - return result; - - size_t size; - if (fseek(fp, 0, SEEK_END) < 0 || ((size = ftell(fp)) < 0) || - fseek(fp, 0, SEEK_SET) < 0) { - fclose(fp); - return result; - } - if (size == 0) { - fclose(fp); - return result; - } - - jpeg_stdio_src(&cinfo, fp); - jpeg_read_header(&cinfo, TRUE); - jpeg_start_decompress(&cinfo); - - uint32_t width = cinfo.output_width; - uint32_t height = cinfo.output_height; - uint32_t channels = cinfo.output_components; - - JDIMENSION stride = width * channels; - JSAMPARRAY temp_buffer = - (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, stride, 1); - - result.width = width; - result.height = height; - result.data = - (uint32_t *)zig_alloc(allocator, width * height * 4 * sizeof(uint8_t)); - if (!result.data) { - jpeg_destroy_decompress(&cinfo); - fclose(fp); - return result; - } - - uint8_t *image_buffer = (uint8_t *)result.data; - - while (cinfo.output_scanline < height) { - jpeg_read_scanlines(&cinfo, temp_buffer, 1); - - unsigned int k = (cinfo.output_scanline - 1) * 4 * width; - unsigned int j = 0; - for (unsigned int i = 0; i < 4 * width; i += 4) { - image_buffer[k + i] = temp_buffer[0][j]; // R - image_buffer[k + i + 1] = temp_buffer[0][j + 1]; // G - image_buffer[k + i + 2] = temp_buffer[0][j + 2]; // B - image_buffer[k + i + 3] = 255; // A - j += 3; - } - } - - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); - fclose(fp); - - return result; -#else - fprintf(stderr, "JPEG support not enabled\n"); - abort(); -#endif -} - -ImageData read_tiff_file(const char *filename, void *allocator) { -#if defined(HAVE_TIFF) - ImageData result = {0, 0, NULL}; - TIFF *image = TIFFOpen(filename, "r"); - if (!image) - return result; - - int width, height; - TIFFGetField(image, TIFFTAG_IMAGEWIDTH, &width); - TIFFGetField(image, TIFFTAG_IMAGELENGTH, &height); - - result.width = width; - result.height = height; - result.data = - (uint32_t *)zig_alloc(allocator, width * height * sizeof(uint32_t)); - if (!result.data) { - TIFFClose(image); - return result; - } - - if (!TIFFReadRGBAImageOriented(image, width, height, result.data, - ORIENTATION_TOPLEFT, 0)) { - zig_free(allocator, result.data, width * height * sizeof(uint32_t)); - result.data = NULL; - TIFFClose(image); - return result; - } - - TIFFClose(image); - return result; -#else - fprintf(stderr, "TIFF support not enabled\n"); - abort(); -#endif -} - -void free_image_data(ImageData *img, void *allocator) { - if (img && allocator) { - if (img->data) { - size_t data_size = img->width * img->height * sizeof(uint32_t); - zig_free(allocator, img->data, data_size); - } - zig_free(allocator, img, sizeof(ImageData)); - } -} - -void free_image_data_ptr(uint32_t *data, void *allocator, size_t size) { - if (data && allocator) { - zig_free(allocator, data, size); - } -} - -ImageData read_webp_file(const char *filename, void *allocator) { -#if defined(HAVE_WEBP) - ImageData result = {0, 0, NULL}; - - FileBuffer file_buf = open_file_buffer(filename); - if (!file_buf.data) - return result; - - int width, height; - - // Get WebP image dimensions without decoding - if (!WebPGetInfo((const uint8_t*)file_buf.data, file_buf.size, &width, &height)) { - close_file_buffer(&file_buf); - return result; - } - - result.width = width; - result.height = height; - result.data = (uint32_t *)zig_alloc(allocator, width * height * sizeof(uint32_t)); - - if (!result.data) { - close_file_buffer(&file_buf); - return result; - } - - uint8_t* decoded_data = WebPDecodeRGBAInto((const uint8_t*)file_buf.data, file_buf.size, - (uint8_t*)result.data, width * height * 4, width * 4); - - close_file_buffer(&file_buf); - - if (!decoded_data) { - zig_free(allocator, result.data, width * height * sizeof(uint32_t)); - result.data = NULL; - return result; - } - - return result; -#else - fprintf(stderr, "WebP support not enabled\n"); - abort(); -#endif -} - diff --git a/odiff/c_bindings/odiff_io.h b/odiff/c_bindings/odiff_io.h deleted file mode 100644 index 9d822ea..0000000 --- a/odiff/c_bindings/odiff_io.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef ODIFF_IO_H -#define ODIFF_IO_H - -#include -#include - -typedef struct { - int width; - int height; - uint32_t *data; -} ImageData; - -ImageData read_png_file(const char *filename, - /* std.mem.Allocator */ void *allocator); -ImageData read_jpg_file(const char *filename, - /* std.mem.Allocator */ void *allocator); -ImageData read_tiff_file(const char *filename, - /* std.mem.Allocator */ void *allocator); -ImageData read_bmp_file(const char *filename, void *allocator); - -int write_png_file(const char *filename, int width, int height, - const uint32_t *data); - -// Hook to use zig's allocator for image data -// probably either to just rewrite all the functions in zig -void free_image_data_ptr(uint32_t *data, - /* std.mem.Allocator */ void *allocator, size_t size); -void *zig_alloc(/* std.mem.Allocator */ void *allocator, size_t size); -void zig_free(/* std.mem.Allocator */ void *allocator, void *ptr, size_t size); - -#endif diff --git a/odiff/diff_mask.png b/odiff/diff_mask.png deleted file mode 100644 index 127caaa..0000000 --- a/odiff/diff_mask.png +++ /dev/null @@ -1,18 +0,0 @@ -# Image Conversion Failed: - -## identify - -identify: Expected 8 bytes; found 0 bytes `/Users/neogoose/dev/odiff/diff_mask.png' @ warning/png.c/MagickPNGWarningHandler/1531. -identify: unexpected end-of-file `/Users/neogoose/dev/odiff/diff_mask.png' @ error/png.c/MagickPNGError/1305. - -- **cwd**: ~/dev/odiff -```sh -magick identify -format \ - '%m %[fx:w]x%[fx:h] %xx%y' \ - /Users/neogoose/dev/odiff/diff_mask.png[0] -``` - -# Output -identify: Expected 8 bytes; found 0 bytes `/Users/neogoose/dev/odiff/diff_mask.png' @ warning/png.c/MagickPNGWarningHandler/1531. -identify: unexpected end-of-file `/Users/neogoose/dev/odiff/diff_mask.png' @ error/png.c/MagickPNGError/1305. - diff --git a/odiff/diff_output.png b/odiff/diff_output.png deleted file mode 100644 index 636d46e..0000000 Binary files a/odiff/diff_output.png and /dev/null differ diff --git a/odiff/images/2x2-ff0000ff.png b/odiff/images/2x2-ff0000ff.png deleted file mode 100644 index e5c84a4..0000000 Binary files a/odiff/images/2x2-ff0000ff.png and /dev/null differ diff --git a/odiff/images/README.md b/odiff/images/README.md deleted file mode 100644 index a34e7a0..0000000 --- a/odiff/images/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Benchmark studio - -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. - -## Install the tools to benchmarks - -Make sure you installed `odiff`, `pixelmatch` and `ImageMagick` (at least for this guide) - -## Install the benchmark tool - -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: - -``` -brew install hyperfine -``` - -## Run the benchmark - -> Make sure that provided benchmark results were achieved on MacBook Pro 16, MacOS 11 BigSure beta. - -Simple benchmark that compares [4k water image](./water-4k.png) with [corrupted one](./water-4k-2.png). - -``` -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' - -``` - -## Generate markdown results - -This generates markdown output that is displayed in README. - -``` -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' -``` diff --git a/odiff/images/__debug.png b/odiff/images/__debug.png deleted file mode 100644 index feb6d61..0000000 Binary files a/odiff/images/__debug.png and /dev/null differ diff --git a/odiff/images/benchmarks.png b/odiff/images/benchmarks.png deleted file mode 100644 index dfe836e..0000000 Binary files a/odiff/images/benchmarks.png and /dev/null differ diff --git a/odiff/images/diff_white_mask.png b/odiff/images/diff_white_mask.png deleted file mode 100644 index 980e1cb..0000000 Binary files a/odiff/images/diff_white_mask.png and /dev/null differ diff --git a/odiff/images/donkey-2.png b/odiff/images/donkey-2.png deleted file mode 100644 index 29afe19..0000000 Binary files a/odiff/images/donkey-2.png and /dev/null differ diff --git a/odiff/images/donkey-diff.png b/odiff/images/donkey-diff.png deleted file mode 100644 index cd7fd8b..0000000 Binary files a/odiff/images/donkey-diff.png and /dev/null differ diff --git a/odiff/images/donkey.png b/odiff/images/donkey.png deleted file mode 100644 index 0072bd7..0000000 Binary files a/odiff/images/donkey.png and /dev/null differ diff --git a/odiff/images/donkey.webp b/odiff/images/donkey.webp deleted file mode 100644 index 06bfc87..0000000 Binary files a/odiff/images/donkey.webp and /dev/null differ diff --git a/odiff/images/extreme-alpha-1.png b/odiff/images/extreme-alpha-1.png deleted file mode 100644 index 7994e89..0000000 Binary files a/odiff/images/extreme-alpha-1.png and /dev/null differ diff --git a/odiff/images/extreme-alpha.png b/odiff/images/extreme-alpha.png deleted file mode 100644 index ec12bdd..0000000 Binary files a/odiff/images/extreme-alpha.png and /dev/null differ diff --git a/odiff/images/out.png b/odiff/images/out.png deleted file mode 100644 index ce4857a..0000000 Binary files a/odiff/images/out.png and /dev/null differ diff --git a/odiff/images/test_map.png b/odiff/images/test_map.png deleted file mode 100644 index 7ed33d1..0000000 Binary files a/odiff/images/test_map.png and /dev/null differ diff --git a/odiff/images/test_map_1.png b/odiff/images/test_map_1.png deleted file mode 100644 index 357790a..0000000 Binary files a/odiff/images/test_map_1.png and /dev/null differ diff --git a/odiff/images/tiger-2.jpg b/odiff/images/tiger-2.jpg deleted file mode 100644 index 2d6b982..0000000 Binary files a/odiff/images/tiger-2.jpg and /dev/null differ diff --git a/odiff/images/tiger-diff.png b/odiff/images/tiger-diff.png deleted file mode 100644 index 7d8b893..0000000 Binary files a/odiff/images/tiger-diff.png and /dev/null differ diff --git a/odiff/images/tiger.jpg b/odiff/images/tiger.jpg deleted file mode 100644 index 11d4c5c..0000000 Binary files a/odiff/images/tiger.jpg and /dev/null differ diff --git a/odiff/images/water-4k-2.png b/odiff/images/water-4k-2.png deleted file mode 100644 index 84d4201..0000000 Binary files a/odiff/images/water-4k-2.png and /dev/null differ diff --git a/odiff/images/water-4k.png b/odiff/images/water-4k.png deleted file mode 100644 index a3a408e..0000000 Binary files a/odiff/images/water-4k.png and /dev/null differ diff --git a/odiff/images/water-diff.png b/odiff/images/water-diff.png deleted file mode 100644 index 20bd470..0000000 Binary files a/odiff/images/water-diff.png and /dev/null differ diff --git a/odiff/images/www.cypress-diff-odiff.png b/odiff/images/www.cypress-diff-odiff.png deleted file mode 100644 index 5ec2116..0000000 Binary files a/odiff/images/www.cypress-diff-odiff.png and /dev/null differ diff --git a/odiff/images/www.cypress-diff.png b/odiff/images/www.cypress-diff.png deleted file mode 100644 index c88ce9a..0000000 Binary files a/odiff/images/www.cypress-diff.png and /dev/null differ diff --git a/odiff/images/www.cypress.io-1.png b/odiff/images/www.cypress.io-1.png deleted file mode 100644 index 9402f2c..0000000 Binary files a/odiff/images/www.cypress.io-1.png and /dev/null differ diff --git a/odiff/images/www.cypress.io.png b/odiff/images/www.cypress.io.png deleted file mode 100644 index 8a5a616..0000000 Binary files a/odiff/images/www.cypress.io.png and /dev/null differ diff --git a/odiff/npm_package/bin/odiff.exe b/odiff/npm_package/bin/odiff.exe deleted file mode 100644 index 2706196..0000000 Binary files a/odiff/npm_package/bin/odiff.exe and /dev/null differ diff --git a/odiff/npm_package/odiff.d.ts b/odiff/npm_package/odiff.d.ts deleted file mode 100644 index 60a3b31..0000000 --- a/odiff/npm_package/odiff.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -export type ODiffOptions = Partial<{ - /** Color used to highlight different pixels in the output (in hex format e.g. #cd2cc9). */ - diffColor: string; - /** Output full diff image. */ - outputDiffMask: boolean; - /** Outputs diff images with a white shaded overlay for easier diff reading */ - diffOverlay: boolean | number; - /** Do not compare images and produce output if images layout is different. */ - failOnLayoutDiff: boolean; - /** Return { match: false, reason: '...' } instead of throwing error if file is missing. */ - noFailOnFsErrors: boolean; - /** Color difference threshold (from 0 to 1). Less more precise. */ - threshold: number; - /** If this is true, antialiased pixels are not counted to the diff of an image */ - antialiasing: boolean; - /** If `true` reason: "pixel-diff" output will contain the set of line indexes containing different pixels */ - captureDiffLines: boolean; - /** If `true` odiff will use less memory but will be slower with larger images */ - reduceRamUsage: boolean; - /** An array of regions to ignore in the diff. */ - ignoreRegions: Array<{ - x1: number; - y1: number; - x2: number; - y2: number; - }>; -}>; - -declare function compare( - basePath: string, - comparePath: string, - diffPath: string, - options?: ODiffOptions -): Promise< - | { match: true } - | { match: false; reason: "layout-diff" } - | { - match: false; - reason: "pixel-diff"; - /** Amount of different pixels */ - diffCount: number; - /** Percentage of different pixels in the whole image */ - diffPercentage: number; - /** Individual line indexes containing different pixels. Guaranteed to be ordered and distinct. */ - diffLines?: number[]; - } - | { - match: false; - reason: "file-not-exists"; - /** Errored file path */ - file: string; - } ->; - -export { compare }; diff --git a/odiff/npm_package/odiff.js b/odiff/npm_package/odiff.js deleted file mode 100644 index 1180980..0000000 --- a/odiff/npm_package/odiff.js +++ /dev/null @@ -1,193 +0,0 @@ -// @ts-check -const path = require("path"); -const { execFile } = require("child_process"); - -function optionsToArgs(options) { - let argArray = ["--parsable-stdout"]; - - if (!options) { - return argArray; - } - - const setArgWithValue = (name, value) => { - argArray.push(`--${name}=${value.toString()}`); - }; - - const setFlag = (name, value) => { - if (value) { - argArray.push(`--${name}`); - } - }; - - Object.entries(options).forEach((optionEntry) => { - /** - * @type {[keyof import('./odiff').ODiffOptions, unknown]} - * @ts-expect-error */ - const [option, value] = optionEntry; - - switch (option) { - case "failOnLayoutDiff": - setFlag("fail-on-layout", value); - break; - - case "outputDiffMask": - setFlag("diff-mask", value); - break; - - case "diffOverlay": - if (typeof value === "number") { - setArgWithValue("diff-overlay", value); - } else { - setFlag("diff-overlay", value); - } - - break; - - case "threshold": - setArgWithValue("threshold", value); - break; - - case "diffColor": - setArgWithValue("diff-color", value); - break; - - case "antialiasing": - setFlag("antialiasing", value); - break; - - case "captureDiffLines": - setFlag("output-diff-lines", value); - break; - - case "reduceRamUsage": - setFlag("reduce-ram-usage", value); - break; - - case "ignoreRegions": { - const regions = value - .map( - (region) => `${region.x1}:${region.y1}-${region.x2}:${region.y2}`, - ) - .join(","); - - setArgWithValue("ignore", regions); - break; - } - } - }); - - return argArray; -} - -/** @type {(stdout: string) => Partial<{ diffCount: number, diffPercentage: number, diffLines: number[] }>} */ -function parsePixelDiffStdout(stdout) { - try { - const parts = stdout.trim().split(";"); - - if (parts.length === 2) { - const [diffCount, diffPercentage] = parts; - - return { - diffCount: parseInt(diffCount), - diffPercentage: parseFloat(diffPercentage), - }; - } else if (parts.length === 3) { - const [diffCount, diffPercentage, linesPart] = parts; - - return { - diffCount: parseInt(diffCount), - diffPercentage: parseFloat(diffPercentage), - diffLines: linesPart.split(",").flatMap((line) => { - let parsedInt = parseInt(line); - - return isNaN(parsedInt) ? [] : parsedInt; - }), - }; - } else { - throw new Error(`Unparsable stdout from odiff binary: ${stdout}`); - } - } catch (e) { - console.warn( - "Can't parse output from internal process. Please submit an issue at https://github.com/dmtrKovalenko/odiff/issues/new with the following stacktrace:", - e, - ); - } - - return {}; -} - -const CMD_BIN_HELPER_MSG = - "Usage: odiff [OPTION]... [BASE] [COMPARING] [DIFF]\nTry `odiff --help' for more information.\n"; - -const NO_FILE_ODIFF_ERROR_REGEX = /Could not load.*image:\s*(.+)/; - -async function compare(basePath, comparePath, diffOutput, options = {}) { - return new Promise((resolve, reject) => { - let producedStdout, producedStdError; - - const binaryPath = - options && options.__binaryPath - ? options.__binaryPath - : path.join(__dirname, "bin", "odiff.exe"); - - execFile( - binaryPath, - [basePath, comparePath, diffOutput, ...optionsToArgs(options)], - (_, stdout, stderr) => { - producedStdout = stdout; - producedStdError = stderr; - }, - ).on("close", (code) => { - switch (code) { - case 0: - resolve({ match: true }); - break; - case 21: - resolve({ match: false, reason: "layout-diff" }); - break; - case 22: - resolve({ - match: false, - reason: "pixel-diff", - ...parsePixelDiffStdout(producedStdout), - }); - break; - case 1: - /** @type string */ - const originalErrorMessage = ( - producedStdError || "Invalid Argument Exception" - ).replace(CMD_BIN_HELPER_MSG, ""); - - const noFileOrDirectoryMatches = originalErrorMessage.match( - NO_FILE_ODIFF_ERROR_REGEX, - ); - - if (options.noFailOnFsErrors && noFileOrDirectoryMatches?.[1]) { - resolve({ - match: false, - reason: "file-not-exists", - file: noFileOrDirectoryMatches[1], - }); - } else { - reject(new TypeError(originalErrorMessage)); - } - break; - - default: - reject( - new Error( - (producedStdError || producedStdout).replace( - CMD_BIN_HELPER_MSG, - "", - ), - ), - ); - break; - } - }); - }); -} - -module.exports = { - compare, -}; diff --git a/odiff/npm_package/package.json b/odiff/npm_package/package.json deleted file mode 100644 index 7371909..0000000 --- a/odiff/npm_package/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "odiff-bin", - "version": "4.1.1", - "author": "odiff contributors", - "license": "MIT", - "description": "The fastest image difference tool in the world (Zig port of odiff)", - "scripts": { - "postinstall": "node ./post_install.js" - }, - "bin": { - "odiff": "bin/odiff.exe" - }, - "types": "odiff.d.ts", - "main": "odiff.js", - "keywords": [ - "visual-regression", - "pixelmatch", - "image", - "comparison", - "diff", - "zig" - ] -} diff --git a/odiff/npm_package/post_install.js b/odiff/npm_package/post_install.js deleted file mode 100644 index e2275d9..0000000 --- a/odiff/npm_package/post_install.js +++ /dev/null @@ -1,36 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const os = require("os"); - -const binaries = { - "linux-x64": "odiff-linux-x64", - "linux-arm64": "odiff-linux-arm64", - "darwin-arm64": "odiff-macos-arm64", - "darwin-x64": "odiff-macos-x64", - "win32-x64": "odiff-windows-x64.exe", - "win32-arm64": "odiff-windows-arm64.exe", -}; - -const platform = os.platform(); -const arch = os.arch(); - -let binaryKey = `${platform}-${arch}`; -const binaryFile = binaries[binaryKey]; - -if (!binaryFile) { - console.error( - `odiff: Sorry your platform or architecture is not supported. Here is a list of supported binaries: ${Object.keys(binaries).join(", ")}`, - ); - process.exit(1); -} - -const sourcePath = path.join(__dirname, "raw_binaries", binaryFile); -const destPath = path.join(__dirname, "bin", "odiff.exe"); - -try { - fs.copyFileSync(sourcePath, destPath); - fs.chmodSync(destPath, 0o755); -} catch (err) { - console.error(`odiff: failed to copy and link the binary file: ${err}`); - process.exit(1); -} diff --git a/odiff/odiff-logo-dark.png b/odiff/odiff-logo-dark.png deleted file mode 100644 index f88844c..0000000 Binary files a/odiff/odiff-logo-dark.png and /dev/null differ diff --git a/odiff/odiff-logo-light.png b/odiff/odiff-logo-light.png deleted file mode 100644 index ecc242e..0000000 Binary files a/odiff/odiff-logo-light.png and /dev/null differ diff --git a/odiff/package-lock.json b/odiff/package-lock.json deleted file mode 100644 index 348dd72..0000000 --- a/odiff/package-lock.json +++ /dev/null @@ -1,2361 +0,0 @@ -{ - "name": "odiff", - "version": "4.1.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "odiff", - "version": "4.1.1", - "license": "MIT", - "devDependencies": { - "ava": "^6.1.3" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.0.tgz", - "integrity": "sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "consola": "^3.2.3", - "detect-libc": "^2.0.0", - "https-proxy-agent": "^7.0.5", - "node-fetch": "^2.6.7", - "nopt": "^8.0.0", - "semver": "^7.5.3", - "tar": "^7.4.0" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", - "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vercel/nft": { - "version": "0.29.4", - "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.4.tgz", - "integrity": "sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mapbox/node-pre-gyp": "^2.0.0", - "@rollup/pluginutils": "^5.1.3", - "acorn": "^8.6.0", - "acorn-import-attributes": "^1.9.5", - "async-sema": "^3.1.1", - "bindings": "^1.4.0", - "estree-walker": "2.0.2", - "glob": "^10.4.5", - "graceful-fs": "^4.2.9", - "node-gyp-build": "^4.2.2", - "picomatch": "^4.0.2", - "resolve-from": "^5.0.0" - }, - "bin": { - "nft": "out/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arrgv": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arrgv/-/arrgv-1.0.2.tgz", - "integrity": "sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/arrify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-3.0.0.tgz", - "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/async-sema": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", - "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ava": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/ava/-/ava-6.4.1.tgz", - "integrity": "sha512-vxmPbi1gZx9zhAjHBgw81w/iEDKcrokeRk/fqDTyA2DQygZ0o+dUGRHFOtX8RA5N0heGJTTsIk7+xYxitDb61Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vercel/nft": "^0.29.4", - "acorn": "^8.15.0", - "acorn-walk": "^8.3.4", - "ansi-styles": "^6.2.1", - "arrgv": "^1.0.2", - "arrify": "^3.0.0", - "callsites": "^4.2.0", - "cbor": "^10.0.9", - "chalk": "^5.4.1", - "chunkd": "^2.0.1", - "ci-info": "^4.3.0", - "ci-parallel-vars": "^1.0.1", - "cli-truncate": "^4.0.0", - "code-excerpt": "^4.0.0", - "common-path-prefix": "^3.0.0", - "concordance": "^5.0.4", - "currently-unhandled": "^0.4.1", - "debug": "^4.4.1", - "emittery": "^1.2.0", - "figures": "^6.1.0", - "globby": "^14.1.0", - "ignore-by-default": "^2.1.0", - "indent-string": "^5.0.0", - "is-plain-object": "^5.0.0", - "is-promise": "^4.0.0", - "matcher": "^5.0.0", - "memoize": "^10.1.0", - "ms": "^2.1.3", - "p-map": "^7.0.3", - "package-config": "^5.0.0", - "picomatch": "^4.0.2", - "plur": "^5.1.0", - "pretty-ms": "^9.2.0", - "resolve-cwd": "^3.0.0", - "stack-utils": "^2.0.6", - "strip-ansi": "^7.1.0", - "supertap": "^3.0.1", - "temp-dir": "^3.0.0", - "write-file-atomic": "^6.0.0", - "yargs": "^17.7.2" - }, - "bin": { - "ava": "entrypoints/cli.mjs" - }, - "engines": { - "node": "^18.18 || ^20.8 || ^22 || ^23 || >=24" - }, - "peerDependencies": { - "@ava/typescript": "*" - }, - "peerDependenciesMeta": { - "@ava/typescript": { - "optional": true - } - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/blueimp-md5": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz", - "integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cbor": { - "version": "10.0.10", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.10.tgz", - "integrity": "sha512-EirvzAg0G4okCsdTfTjLWHU+tToQ2V2ptO3577Vyy2GOTeVJad99uCIuDqdK7ppFRRcEuigyJY6TJ59wv5JpSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "nofilter": "^3.0.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/chalk": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", - "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/chunkd": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-2.0.1.tgz", - "integrity": "sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ci-parallel-vars": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ci-parallel-vars/-/ci-parallel-vars-1.0.1.tgz", - "integrity": "sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/code-excerpt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", - "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", - "dev": true, - "license": "MIT", - "dependencies": { - "convert-to-spaces": "^2.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true, - "license": "ISC" - }, - "node_modules/concordance": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", - "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "date-time": "^3.1.0", - "esutils": "^2.0.3", - "fast-diff": "^1.2.0", - "js-string-escape": "^1.0.1", - "lodash": "^4.17.15", - "md5-hex": "^3.0.1", - "semver": "^7.3.2", - "well-known-symbols": "^2.0.0" - }, - "engines": { - "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" - } - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/convert-to-spaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", - "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-find-index": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/date-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", - "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "time-zone": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emittery": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-1.2.0.tgz", - "integrity": "sha512-KxdRyyFcS85pH3dnU8Y5yFUm2YJdaHwcBZWrfG8o89ZY9a13/f9itbN+YG3ELbBo9Pg5zvIozstmuV8bX13q6g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/figures": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up-simple": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globby": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", - "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.3", - "path-type": "^6.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-by-default": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-2.1.0.tgz", - "integrity": "sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10 <11 || >=12 <13 || >=14" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/irregular-plurals": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", - "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/js-string-escape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/load-json-file": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-7.0.1.tgz", - "integrity": "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/matcher": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-5.0.0.tgz", - "integrity": "sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/md5-hex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", - "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", - "dev": true, - "license": "MIT", - "dependencies": { - "blueimp-md5": "^2.10.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/memoize": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.1.0.tgz", - "integrity": "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/memoize?sponsor=1" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "dev": true, - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.19" - } - }, - "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/p-map": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", - "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-config": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/package-config/-/package-config-5.0.0.tgz", - "integrity": "sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "load-json-file": "^7.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-type": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", - "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plur": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz", - "integrity": "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "irregular-plurals": "^3.3.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-ms": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", - "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/supertap": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/supertap/-/supertap-3.0.1.tgz", - "integrity": "sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^5.0.0", - "js-yaml": "^3.14.1", - "serialize-error": "^7.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/temp-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", - "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/time-zone": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", - "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/well-known-symbols": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", - "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=6" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/write-file-atomic": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz", - "integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - } - } -} diff --git a/odiff/package.json b/odiff/package.json deleted file mode 100644 index a27c99e..0000000 --- a/odiff/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "odiff", - "version": "4.1.1", - "author": "odiff contributors", - "license": "MIT", - "description": "The fastest image difference tool in the world (Zig port of odiff)", - "scripts": { - "test": "ava", - "update-readme" : "node scripts/process-readme.js" - - }, - "keywords": [ - "visual-regression", - "pixelmatch", - "image", - "comparison", - "diff", - "zig" - ], - "devDependencies": { - "ava": "^6.1.3" - } -} diff --git a/odiff/release.sh b/odiff/release.sh deleted file mode 100644 index 0c78375..0000000 --- a/odiff/release.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -set -eou pipefail - -VERSION="$1" -DRY_RUN=false - -for arg in "$@" -do - if [ "$arg" = "--dry-run" ]; then - DRY_RUN=true - break - fi -done - -if ! git diff --quiet; then - echo "Error: There are unstaged changes in the repository." - exit 1 -fi - -sed -i '' "s/\.version = \"[^\"]*\"/\.version = \"$VERSION\"/g" build.zig.zon -zig build - -sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/g" package.json -npm install - -sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/g" npm_package/package.json - -if [ "$DRY_RUN" == true ]; then - echo "Dry run, not committing or tagging" -else - git add --all - git commit -m "chore(release): v$VERSION" - git tag "v$VERSION" - git push origin "v$VERSION" - git push -fi - diff --git a/odiff/scripts/process-readme.js b/odiff/scripts/process-readme.js deleted file mode 100644 index 885414d..0000000 --- a/odiff/scripts/process-readme.js +++ /dev/null @@ -1,43 +0,0 @@ -const fs = require("fs"); -const path = require("path"); - -const readmePath = path.resolve(__dirname, "..", "README.md"); -const currentReadmeContent = fs.readFileSync(readmePath, { - encoding: "utf-8", -}); - -const START_COMMENT = ""; -const END_COMMENT = ""; - -const [startOfFile, afterStartMark] = currentReadmeContent.split(START_COMMENT); -const [_, endOfFile] = afterStartMark.split(END_COMMENT); - -const tsInterface = fs.readFileSync( - path.resolve(__dirname, "..", "npm_package", "odiff.d.ts"), - { - encoding: "utf-8", - }, -); - -const updatedReadme = [ - startOfFile, - START_COMMENT, - "\n```tsx\n", - tsInterface, - "```\n", - END_COMMENT, - endOfFile, -].join(""); - -console.log(process.argv[2]); -if (process.argv[2] === "verify") { - if (updatedReadme !== currentReadmeContent) { - throw new Error( - "❌ Outdated README detected. Run `node scripts/process-readme.js` and repush your branch", - ); - } else { - console.log("✅ README is up-to-date"); - } -} else { - fs.writeFileSync(readmePath, updatedReadme); -} diff --git a/odiff/src/antialiasing.zig b/odiff/src/antialiasing.zig deleted file mode 100644 index e8576b7..0000000 --- a/odiff/src/antialiasing.zig +++ /dev/null @@ -1,92 +0,0 @@ -// Antialiasing detection - equivalent to Antialiasing.ml -const std = @import("std"); -const image_io = @import("image_io.zig"); -const color_delta = @import("color_delta.zig"); - -const Image = image_io.Image; - -fn hasManySiblingsWithSameColor(x: u32, y: u32, width: u32, height: u32, image: *const Image) bool { - if (x <= width - 1 and y <= height - 1) { - const x0 = @max(if (x > 0) x - 1 else 0, 0); - const y0 = @max(if (y > 0) y - 1 else 0, 0); - const x1 = @min(x + 1, width - 1); - const y1 = @min(y + 1, height - 1); - - var zeroes: u32 = if (x == x0 or x == x1 or y == y0 or y == y1) 1 else 0; - - const base_color = image.readRawPixel(x, y); - - var adj_y = y0; - while (adj_y <= y1) : (adj_y += 1) { - var adj_x = x0; - while (adj_x <= x1) : (adj_x += 1) { - if (zeroes < 3 and (x != adj_x or y != adj_y)) { - const adjacent_color = image.readRawPixel(adj_x, adj_y); - if (base_color == adjacent_color) { - zeroes += 1; - } - } - } - } - - return zeroes >= 3; - } else { - return false; - } -} - -pub fn detect(x: u32, y: u32, base_img: *const Image, comp_img: *const Image) bool { - const x0 = @max(if (x > 0) x - 1 else 0, 0); - const y0 = @max(if (y > 0) y - 1 else 0, 0); - const x1 = @min(x + 1, base_img.width - 1); - const y1 = @min(y + 1, base_img.height - 1); - - var min_sibling_delta: f64 = 0.0; - var max_sibling_delta: f64 = 0.0; - var min_sibling_coord = struct { x: u32, y: u32 }{ .x = 0, .y = 0 }; - var max_sibling_coord = struct { x: u32, y: u32 }{ .x = 0, .y = 0 }; - - var zeroes: u32 = if (x == x0 or x == x1 or y == y0 or y == y1) 1 else 0; - - const base_color = base_img.readRawPixel(x, y); - - var adj_y = y0; - while (adj_y <= y1) : (adj_y += 1) { - var adj_x = x0; - while (adj_x <= x1) : (adj_x += 1) { - if (zeroes < 3 and (x != adj_x or y != adj_y)) { - const adjacent_color = base_img.readRawPixel(adj_x, adj_y); - if (base_color == adjacent_color) { - zeroes += 1; - } else { - const delta = color_delta.calculatePixelBrightnessDelta(base_color, adjacent_color); - if (delta < min_sibling_delta) { - min_sibling_delta = delta; - min_sibling_coord = .{ .x = adj_x, .y = adj_y }; - } else if (delta > max_sibling_delta) { - max_sibling_delta = delta; - max_sibling_coord = .{ .x = adj_x, .y = adj_y }; - } - } - } - } - } - - if (zeroes >= 3 or min_sibling_delta == 0.0 or max_sibling_delta == 0.0) { - // If we found more than 2 equal siblings or there are - // no darker pixels among other siblings or - // there are not brighter pixels among the siblings - return false; - } else { - // If either the darkest or the brightest pixel has 3+ equal siblings in both images - // (definitely not anti-aliased), this pixel is anti-aliased - const min_has_siblings_base = hasManySiblingsWithSameColor(min_sibling_coord.x, min_sibling_coord.y, base_img.width, base_img.height, base_img); - const max_has_siblings_base = hasManySiblingsWithSameColor(max_sibling_coord.x, max_sibling_coord.y, base_img.width, base_img.height, base_img); - - const min_has_siblings_comp = hasManySiblingsWithSameColor(min_sibling_coord.x, min_sibling_coord.y, comp_img.width, comp_img.height, comp_img); - const max_has_siblings_comp = hasManySiblingsWithSameColor(max_sibling_coord.x, max_sibling_coord.y, comp_img.width, comp_img.height, comp_img); - - return (min_has_siblings_base or max_has_siblings_base) and - (min_has_siblings_comp or max_has_siblings_comp); - } -} diff --git a/odiff/src/bmp_reader.zig b/odiff/src/bmp_reader.zig deleted file mode 100644 index 0794c46..0000000 --- a/odiff/src/bmp_reader.zig +++ /dev/null @@ -1,266 +0,0 @@ -const std = @import("std"); -const image_io = @import("image_io.zig"); - -const BMP_SIGNATURE: u16 = 19778; // "BM" in little-endian -const BYTES_PER_PIXEL_24: u8 = 3; -const BYTES_PER_PIXEL_32: u8 = 4; - -const BiCompression = enum(u32) { - BI_RGB = 0, - BI_RLE8 = 1, - BI_RLE4 = 2, - BI_BITFIELDS = 3, -}; - -const BiBitCount = enum(u16) { - Monochrome = 1, - Color16 = 4, - Color256 = 8, - ColorRGB = 24, - ColorRGBA = 32, -}; - -// BMP file header (14 bytes) -const BitmapFileHeader = packed struct { - bf_type: u16, // File signature - must be 19778 ("BM") - bf_size: u32, // File size in bytes - bf_reserved1: u16, // Reserved field 1 - bf_reserved2: u16, // Reserved field 2 - bf_off_bits: u32, // Offset to pixel data -}; - -// BMP info header (40 bytes) -const BitmapInfoHeader = packed struct { - bi_size: u32, // Info header size - bi_width: i32, // Image width (can be negative) - bi_height: i32, // Image height (can be negative) - bi_planes: u16, // Number of color planes - bi_bit_count: u16, // Bits per pixel - bi_compression: u32, // Compression type - bi_size_image: u32, // Image size - bi_x_pels_per_meter: u32, // Horizontal resolution - bi_y_pels_per_meter: u32, // Vertical resolution - bi_clr_used: u32, // Number of colors used - bi_clr_important: u32, // Number of important colors -}; - -pub const BmpError = error{ - InvalidSignature, - UnsupportedBitDepth, - UnsupportedCompression, - InvalidDimensions, - FileCorrupted, - OutOfMemory, -}; - -// Read little-endian values with bounds checking -inline fn readU16LE(data: []const u8, offset: usize) !u16 { - if (offset + 2 > data.len) return BmpError.FileCorrupted; - return std.mem.readInt(u16, data[offset .. offset + 2], .little); -} - -inline fn readU32LE(data: []const u8, offset: usize) !u32 { - if (offset + 4 > data.len) return BmpError.FileCorrupted; - return std.mem.readInt(u32, data[offset .. offset + 4], .little); -} - -inline fn readI32LE(data: []const u8, offset: usize) !i32 { - if (offset + 4 > data.len) return BmpError.FileCorrupted; - return std.mem.readInt(i32, data[offset .. offset + 4], .little); -} - -// Vectorized BGR to ARGB conversion for 24-bit BMP -inline fn convertBGR24ToARGB_Vec4(bgr_data: []const u8, argb_data: []u32, start_idx: usize) void { - if (start_idx + 4 > argb_data.len) return; - - // Load 4 BGR pixels (12 bytes) - not perfectly aligned but we'll handle it - const b0 = bgr_data[start_idx * 3 + 0]; - const g0 = bgr_data[start_idx * 3 + 1]; - const r0 = bgr_data[start_idx * 3 + 2]; - - const b1 = bgr_data[start_idx * 3 + 3]; - const g1 = bgr_data[start_idx * 3 + 4]; - const r1 = bgr_data[start_idx * 3 + 5]; - - const b2 = bgr_data[start_idx * 3 + 6]; - const g2 = bgr_data[start_idx * 3 + 7]; - const r2 = bgr_data[start_idx * 3 + 8]; - - const b3 = bgr_data[start_idx * 3 + 9]; - const g3 = bgr_data[start_idx * 3 + 10]; - const r3 = bgr_data[start_idx * 3 + 11]; - - // Create RGBA values (A=255, R, G, B) - fixed color order - argb_data[start_idx + 0] = (255 << 24) | (@as(u32, r0) << 16) | (@as(u32, g0) << 8) | @as(u32, b0); - argb_data[start_idx + 1] = (255 << 24) | (@as(u32, r1) << 16) | (@as(u32, g1) << 8) | @as(u32, b1); - argb_data[start_idx + 2] = (255 << 24) | (@as(u32, r2) << 16) | (@as(u32, g2) << 8) | @as(u32, b2); - argb_data[start_idx + 3] = (255 << 24) | (@as(u32, r3) << 16) | (@as(u32, g3) << 8) | @as(u32, b3); -} - -// Vectorized BGRA to ARGB conversion for 32-bit BMP -inline fn convertBGRA32ToARGB_Vec4(bgra_data: []const u8, argb_data: []u32, start_idx: usize) void { - if (start_idx + 4 > argb_data.len or start_idx * 4 + 16 > bgra_data.len) return; - - // Load 4 BGRA pixels (16 bytes) as vector - const bgra_bytes = bgra_data[start_idx * 4 .. start_idx * 4 + 16]; - - // Process 4 pixels at once - var i: usize = 0; - while (i < 4) : (i += 1) { - const b = bgra_bytes[i * 4 + 0]; - const g = bgra_bytes[i * 4 + 1]; - const r = bgra_bytes[i * 4 + 2]; - const a = bgra_bytes[i * 4 + 3]; - - // Convert BGRA to RGBA - argb_data[start_idx + i] = (@as(u32, a) << 24) | (@as(u32, r) << 16) | (@as(u32, g) << 8) | @as(u32, b); - } -} - -inline fn calculateRowPadding(width: u32, bytes_per_pixel: u8) u32 { - const row_size = width * bytes_per_pixel; - return (4 - (row_size % 4)) % 4; -} - -fn loadImage24Data(data: []const u8, offset: usize, width: u32, height: u32, allocator: std.mem.Allocator) ![]u32 { - const pixel_count = width * height; - const argb_data = try allocator.alloc(u32, pixel_count); - errdefer allocator.free(argb_data); - - const row_padding = (4 - (width * 3 % 4)) & 3; - var data_offset = offset; - - // BMP stores pixels bottom-up - var y: i32 = @as(i32, @intCast(height)) - 1; - while (y >= 0) : (y -= 1) { - var x: u32 = 0; - while (x < width) : (x += 1) { - if (data_offset + 3 > data.len) return BmpError.FileCorrupted; - - const b_byte = data[data_offset + 0]; - const g_byte = data[data_offset + 1]; - const r_byte = data[data_offset + 2]; - - const r = (@as(u32, r_byte) & 255) << 16; // Red to bits 16-23 - const g = (@as(u32, g_byte) & 255) << 8; // Green to bits 8-15 - const b = (@as(u32, b_byte) & 255) << 0; // Blue to bits 0-7 - const a = comptime @as(u32, 255) << 24; - - const pixel_index = (@as(u32, @intCast(y)) * width) + x; - argb_data[pixel_index] = a | b | g | r; - - data_offset += 3; - } - - data_offset += row_padding; - } - - return argb_data; -} - -fn loadImage32Data(data: []const u8, offset: usize, width: u32, height: u32, allocator: std.mem.Allocator) ![]u32 { - const pixel_count = width * height; - const argb_data = try allocator.alloc(u32, pixel_count); - errdefer allocator.free(argb_data); - - var data_offset = offset; - - var y: i32 = @as(i32, @intCast(height)) - 1; - while (y >= 0) : (y -= 1) { - var x: u32 = 0; - while (x < width) : (x += 1) { - if (data_offset + 4 > data.len) return BmpError.FileCorrupted; - - const b_byte = data[data_offset + 0]; - const g_byte = data[data_offset + 1]; - const r_byte = data[data_offset + 2]; - const a_byte = data[data_offset + 3]; - - const r = (@as(u32, r_byte) & 255) << 16; // Red to bits 16-23 - const g = (@as(u32, g_byte) & 255) << 8; // Green to bits 8-15 - const b = (@as(u32, b_byte) & 255) << 0; // Blue to bits 0-7 - const a = (@as(u32, a_byte) & 255) << 24; // Alpha to bits 24-31 - - const pixel_index = (@as(u32, @intCast(y)) * width) + x; - argb_data[pixel_index] = a | b | g | r; - - data_offset += 4; - } - } - - return argb_data; -} - -pub fn loadBmp(file_path: []const u8, allocator: std.mem.Allocator) !image_io.Image { - const file_data = std.fs.cwd().readFileAlloc(allocator, file_path, std.math.maxInt(usize)) catch |err| switch (err) { - error.FileNotFound => return BmpError.FileCorrupted, - else => return err, - }; - defer allocator.free(file_data); - - if (file_data.len < @sizeOf(BitmapFileHeader) + @sizeOf(BitmapInfoHeader)) { - return BmpError.FileCorrupted; - } - - // Read file header - const file_header = BitmapFileHeader{ - .bf_type = try readU16LE(file_data, 0), - .bf_size = try readU32LE(file_data, 2), - .bf_reserved1 = try readU16LE(file_data, 6), - .bf_reserved2 = try readU16LE(file_data, 8), - .bf_off_bits = try readU32LE(file_data, 10), - }; - - // Validate BMP signature - if (file_header.bf_type != BMP_SIGNATURE) { - return BmpError.InvalidSignature; - } - - // Read info header - const info_header = BitmapInfoHeader{ - .bi_size = try readU32LE(file_data, 14), - .bi_width = try readI32LE(file_data, 18), - .bi_height = try readI32LE(file_data, 22), - .bi_planes = try readU16LE(file_data, 26), - .bi_bit_count = try readU16LE(file_data, 28), - .bi_compression = try readU32LE(file_data, 30), - .bi_size_image = try readU32LE(file_data, 34), - .bi_x_pels_per_meter = try readU32LE(file_data, 38), - .bi_y_pels_per_meter = try readU32LE(file_data, 42), - .bi_clr_used = try readU32LE(file_data, 46), - .bi_clr_important = try readU32LE(file_data, 50), - }; - - // Validate dimensions - if (info_header.bi_width <= 0 or info_header.bi_height == 0) { - return BmpError.InvalidDimensions; - } - - // Support uncompressed BMPs and BITFIELDS (for 32-bit BMPs) - if (info_header.bi_compression != @intFromEnum(BiCompression.BI_RGB) and - info_header.bi_compression != @intFromEnum(BiCompression.BI_BITFIELDS)) - { - return BmpError.UnsupportedCompression; - } - - // Get absolute dimensions (handle negative height) - const width = @as(u32, @intCast(@abs(info_header.bi_width))); - const height = @as(u32, @intCast(@abs(info_header.bi_height))); - - // Use the pixel data offset from the file header - const pixel_offset = file_header.bf_off_bits; - - // Load pixel data based on bit depth - const pixel_data = switch (info_header.bi_bit_count) { - 24 => try loadImage24Data(file_data, pixel_offset, width, height, allocator), - 32 => try loadImage32Data(file_data, pixel_offset, width, height, allocator), - else => return BmpError.UnsupportedBitDepth, - }; - - return image_io.Image{ - .width = width, - .height = height, - .data = pixel_data, - .allocator = allocator, - }; -} diff --git a/odiff/src/c_api.zig b/odiff/src/c_api.zig deleted file mode 100644 index a9fdc03..0000000 --- a/odiff/src/c_api.zig +++ /dev/null @@ -1,222 +0,0 @@ -const std = @import("std"); -const image_io = @import("image_io.zig"); -const diff = @import("diff.zig"); - -// C-compatible structs -pub const CDiffOptions = extern struct { - antialiasing: bool, - output_diff_mask: bool, - diff_overlay_factor: f32, // 0.0 if not set - diff_lines: bool, - diff_pixel: u32, - threshold: f64, - fail_on_layout_change: bool, - enable_asm: bool, - ignore_region_count: usize, - ignore_regions: ?[*]const diff.IgnoreRegion, -}; - -pub const CDiffResult = extern struct { - result_type: c_int, // 0 = layout, 1 = pixel - diff_count: u32, - diff_percentage: f64, - diff_line_count: usize, - diff_lines: ?[*]u32, - diff_output_path: ?[*:0]const u8, // if saved -}; - -pub const COdiffError = enum(c_int) { - success = 0, - image_not_loaded = 1, - unsupported_format = 2, - failed_to_diff = 3, - out_of_memory = 4, - invalid_hex_color = 5, -}; - -fn parseHexColor(hex: []const u8) !u32 { - if (hex.len != 7 or hex[0] != '#') return error.InvalidHexColor; - return std.fmt.parseInt(u32, hex[1..], 16) catch error.InvalidHexColor; -} - -pub export fn odiff_diff( - base_image_path: [*:0]const u8, - comp_image_path: [*:0]const u8, - diff_output_path: ?[*:0]const u8, - c_options: CDiffOptions, -) COdiffError { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); - - // Load images - std.debug.print("LOADING_IMAGE {s}", .{base_image_path}); - std.debug.print("LOADING_IMAGE {s}", .{std.mem.span(base_image_path)}); - var base_img = image_io.loadImage(std.mem.span(base_image_path), allocator) catch |err| switch (err) { - error.ImageNotLoaded => return .image_not_loaded, - error.UnsupportedFormat => return .unsupported_format, - else => return .failed_to_diff, - }; - defer base_img.deinit(); - - var comp_img = image_io.loadImage(std.mem.span(comp_image_path), allocator) catch |err| switch (err) { - error.ImageNotLoaded => return .image_not_loaded, - error.UnsupportedFormat => return .unsupported_format, - else => return .failed_to_diff, - }; - defer comp_img.deinit(); - - // Convert options - var ignore_regions: ?[]const diff.IgnoreRegion = null; - if (c_options.ignore_regions) |regions| { - ignore_regions = regions[0..c_options.ignore_region_count]; - } - - const diff_options = diff.DiffOptions{ - .antialiasing = c_options.antialiasing, - .output_diff_mask = c_options.output_diff_mask, - .diff_overlay_factor = if (c_options.diff_overlay_factor > 0.0) c_options.diff_overlay_factor else null, - .diff_lines = c_options.diff_lines, - .diff_pixel = c_options.diff_pixel, - .threshold = c_options.threshold, - .ignore_regions = ignore_regions, - .capture_diff = diff_output_path != null, - .fail_on_layout_change = c_options.fail_on_layout_change, - .enable_asm = c_options.enable_asm, - }; - - const result = diff.diff(&base_img, &comp_img, diff_options, allocator) catch return .failed_to_diff; - - switch (result) { - .layout => { - // For layout differences, we can't return detailed results in this simple API - // Perhaps extend later - return .success; // Or define a way to indicate layout diff - }, - .pixel => |pixel_result| { - defer { - if (pixel_result.diff_output) |*output| { - var img = output.*; - img.deinit(); - } - if (pixel_result.diff_lines) |lines| { - var mutable_lines = lines; - mutable_lines.deinit(); - } - } - - if (diff_output_path) |output_path| { - if (pixel_result.diff_output) |output_img| { - image_io.saveImage(&output_img, std.mem.span(output_path), allocator) catch return .failed_to_diff; - } - } - - // In this simple API, we don't return the detailed results back to C - // Just perform the diff and save if needed - return .success; - }, - } -} - -// Simplified version that returns results -pub export fn odiff_diff_with_results( - base_image_path: [*:0]const u8, - comp_image_path: [*:0]const u8, - diff_output_path: ?[*:0]const u8, - c_options: CDiffOptions, - out_result: *CDiffResult, -) COdiffError { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); - - // Load images - var base_img = image_io.loadImage(std.mem.span(base_image_path), allocator) catch |err| switch (err) { - error.ImageNotLoaded => return .image_not_loaded, - error.UnsupportedFormat => return .unsupported_format, - else => return .failed_to_diff, - }; - defer base_img.deinit(); - - var comp_img = image_io.loadImage(std.mem.span(comp_image_path), allocator) catch |err| switch (err) { - error.ImageNotLoaded => return .image_not_loaded, - error.UnsupportedFormat => return .unsupported_format, - else => return .failed_to_diff, - }; - defer comp_img.deinit(); - std.debug.print("Base Image: {s}\n", .{base_image_path}); - std.debug.print("Base Image: {s}\n", .{comp_image_path}); - - // Convert options - var ignore_regions: ?[]const diff.IgnoreRegion = null; - if (c_options.ignore_regions) |regions| { - ignore_regions = regions[0..c_options.ignore_region_count]; - } - - const diff_options = diff.DiffOptions{ - .antialiasing = c_options.antialiasing, - .output_diff_mask = c_options.output_diff_mask, - .diff_overlay_factor = if (c_options.diff_overlay_factor > 0.0) c_options.diff_overlay_factor else null, - .diff_lines = c_options.diff_lines, - .diff_pixel = c_options.diff_pixel, - .threshold = c_options.threshold, - .ignore_regions = ignore_regions, - .capture_diff = diff_output_path != null, - .fail_on_layout_change = c_options.fail_on_layout_change, - .enable_asm = c_options.enable_asm, - }; - - const result = diff.diff(&base_img, &comp_img, diff_options, allocator) catch return .failed_to_diff; - - switch (result) { - .layout => { - out_result.result_type = 0; - out_result.diff_count = 0; - out_result.diff_percentage = 0.0; - out_result.diff_line_count = 0; - out_result.diff_lines = null; - out_result.diff_output_path = null; - return .success; - }, - .pixel => |pixel_result| { - defer { - if (pixel_result.diff_output) |*output| { - var img = output.*; - img.deinit(); - } - } - - out_result.result_type = 1; - out_result.diff_count = pixel_result.diff_count; - out_result.diff_percentage = pixel_result.diff_percentage; - out_result.diff_line_count = 0; - out_result.diff_lines = null; - out_result.diff_output_path = diff_output_path; - - if (pixel_result.diff_lines) |*lines| { - defer @constCast(lines).deinit(); - if (lines.count > 0) { - // Copy diff lines to C-allocated memory - const lines_copy = allocator.alloc(u32, lines.count) catch return .out_of_memory; - @memcpy(lines_copy, lines.getItems()); - out_result.diff_lines = lines_copy.ptr; - out_result.diff_line_count = lines.count; - } - } - - if (diff_output_path) |output_path| { - if (pixel_result.diff_output) |output_img| { - image_io.saveImage(&output_img, std.mem.span(output_path), allocator) catch return .failed_to_diff; - } - } - - return .success; - }, - } -} - -// Function to free memory allocated for diff_lines -pub export fn odiff_free_diff_lines(diff_lines: [*]u32, count: usize) void { - const slice = diff_lines[0..count]; - std.heap.page_allocator.free(slice); -} diff --git a/odiff/src/c_bindings.zig b/odiff/src/c_bindings.zig deleted file mode 100644 index 03dae7a..0000000 --- a/odiff/src/c_bindings.zig +++ /dev/null @@ -1,138 +0,0 @@ -// C FFI bindings for image I/O -const std = @import("std"); - -// C structure definitions matching our C code -pub const CImageData = extern struct { - width: c_int, - height: c_int, - data: [*]u32, -}; - -// Common return type for all image readers -pub const ImageResult = struct { width: u32, height: u32, data: []u32, is_c_allocated: bool = false }; - -// External C functions - updated to return by value -extern fn read_png_file(filename: [*:0]const u8, allocator: *anyopaque) CImageData; -extern fn write_png_file(filename: [*:0]const u8, width: c_int, height: c_int, data: [*]const u32) c_int; -extern fn read_jpg_file(filename: [*:0]const u8, allocator: *anyopaque) CImageData; -extern fn read_tiff_file(filename: [*:0]const u8, allocator: *anyopaque) CImageData; -extern fn read_webp_file(filename: [*:0]const u8, allocator: *anyopaque) CImageData; - -extern fn free_image_data(data: *CImageData, allocator: *anyopaque) void; - -pub extern fn free_image_data_ptr(data: [*]u32, allocator: *anyopaque, size: usize) void; - -pub fn readPngFile(filename: []const u8, allocator: std.mem.Allocator) !?ImageResult { - const c_filename = try allocator.dupeZ(u8, filename); - defer allocator.free(c_filename); - - var alloc = allocator; - const c_data = read_png_file(c_filename.ptr, &alloc); - - // Check if the C function failed (returned null data) - if (@intFromPtr(c_data.data) == 0) return null; - - // Use C-allocated memory directly - no copying! - const width: u32 = @intCast(c_data.width); - const height: u32 = @intCast(c_data.height); - const data_len = width * height; - - // Create a slice that points to the C-allocated memory - const data = c_data.data[0..data_len]; - - return ImageResult{ - .width = width, - .height = height, - .data = data, - .is_c_allocated = true, // Pixel data is C-allocated - }; -} - -pub fn writePngFile(filename: []const u8, width: u32, height: u32, data: []const u32, allocator: std.mem.Allocator) !void { - const c_filename = try allocator.dupeZ(u8, filename); - defer allocator.free(c_filename); - - const result = write_png_file(c_filename.ptr, @intCast(width), @intCast(height), data.ptr); - if (result != 0) { - std.debug.print("PNG write failed with code: {d}\n", .{result}); - return error.WriteFailed; - } -} - -pub fn readJpgFile(filename: []const u8, allocator: std.mem.Allocator) !?ImageResult { - const c_filename = try allocator.dupeZ(u8, filename); - defer allocator.free(c_filename); - - var alloc = allocator; - const c_data = read_jpg_file(c_filename.ptr, &alloc); - - // Check if the C function failed (returned null data) - if (@intFromPtr(c_data.data) == 0) return null; - - const width: u32 = @intCast(c_data.width); - const height: u32 = @intCast(c_data.height); - const data_len = width * height; - - const data = c_data.data[0..data_len]; - - return ImageResult{ - .width = width, - .height = height, - .data = data, - .is_c_allocated = true, // Pixel data is C-allocated - }; -} - -pub fn readTiffFile(filename: []const u8, allocator: std.mem.Allocator) !?ImageResult { - const c_filename = try allocator.dupeZ(u8, filename); - defer allocator.free(c_filename); - - var alloc = allocator; - const c_data = read_tiff_file(c_filename.ptr, &alloc); - - // Check if the C function failed (returned null data) - if (@intFromPtr(c_data.data) == 0) return null; - - const width: u32 = @intCast(c_data.width); - const height: u32 = @intCast(c_data.height); - const data_len = width * height; - const data = c_data.data[0..data_len]; - - return ImageResult{ - .width = width, - .height = height, - .data = data, - .is_c_allocated = true, // Pixel data is C-allocated - }; -} - -const bmp_reader = @import("bmp_reader.zig"); -pub fn readBmpFile(filename: []const u8, allocator: std.mem.Allocator) !?ImageResult { - const image = try bmp_reader.loadBmp(filename, allocator); - - return ImageResult{ .width = image.width, .height = image.height, .data = image.data }; -} - -pub fn readWebpFile(filename: []const u8, allocator: std.mem.Allocator) !?ImageResult { - const c_filename = try allocator.dupeZ(u8, filename); - defer allocator.free(c_filename); - - var alloc = allocator; - const c_data = read_webp_file(c_filename.ptr, &alloc); - - // Check if the C function failed (returned null data) - if (@intFromPtr(c_data.data) == 0) return null; - - const width: u32 = @intCast(c_data.width); - const height: u32 = @intCast(c_data.height); - const data_len = width * height; - const data = c_data.data[0..data_len]; - - return ImageResult{ - .width = width, - .height = height, - .data = data, - .is_c_allocated = true, // Pixel data is C-allocated - }; -} - diff --git a/odiff/src/cli.zig b/odiff/src/cli.zig deleted file mode 100644 index fdbece7..0000000 --- a/odiff/src/cli.zig +++ /dev/null @@ -1,247 +0,0 @@ -const std = @import("std"); -const diff = @import("diff.zig"); -const build_options = @import("build_options"); - -const print = std.debug.print; - -pub const CliArgs = struct { - base_image: []const u8, - comp_image: []const u8, - diff_output: ?[]const u8 = null, - threshold: f32 = 0.1, - diff_mask: bool = false, - diff_overlay_factor: ?f32 = null, - fail_on_layout: bool = false, - parsable_stdout: bool = false, - diff_color: []const u8 = "", - antialiasing: bool = false, - diff_lines: bool = false, - reduce_ram_usage: bool = false, - enable_asm: bool = false, - ignore_regions: std.array_list.Managed(diff.IgnoreRegion), - allocator: std.mem.Allocator, - - pub fn init(allocator: std.mem.Allocator) CliArgs { - return CliArgs{ - .base_image = "", - .comp_image = "", - .ignore_regions = std.array_list.Managed(diff.IgnoreRegion).init(allocator), - .allocator = allocator, - }; - } - - pub fn deinit(self: *CliArgs) void { - self.ignore_regions.deinit(); - if (self.base_image.len > 0) self.allocator.free(self.base_image); - if (self.comp_image.len > 0) self.allocator.free(self.comp_image); - if (self.diff_output) |output| { - if (output.len > 0) self.allocator.free(output); - } - if (self.diff_color.len > 0) self.allocator.free(self.diff_color); - } -}; - -fn printUsage(program_name: []const u8) void { - print("Usage: {s} [diff_output] [options]\n", .{program_name}); - print("\nOptions:\n", .{}); - print(" -t, --threshold Color difference threshold (0.0-1.0, default: 0.1)\n", .{}); - print(" --diff-mask Output only changed pixels over transparent background\n", .{}); - print(" --diff-overlay Render diff output on the white background\n", .{}); - print(" --fail-on-layout Fail if image dimensions differ\n", .{}); - print(" --parsable-stdout Machine-readable output format\n", .{}); - print(" --diff-color Color for highlighting differences (e.g., #cd2cc9)\n", .{}); - print(" --aa, --antialiasing Ignore antialiased pixels in diff\n", .{}); - print(" --output-diff-lines Output line numbers with differences\n", .{}); - print(" --reduce-ram-usage Use less memory (slower)\n", .{}); - print(" --enable-asm Enable AVX-512 optimized asm path when supported (x86_64 only)\n", .{}); - print(" -i, --ignore Ignore regions (format: x1:y1-x2:y2,x3:y3-x4:y4)\n", .{}); - print(" -h, --help Show this help message\n", .{}); - print(" --version Show version\n", .{}); - print("\nExit codes:\n", .{}); - print(" 0 - Images match\n", .{}); - print(" 21 - Layout difference (when --fail-on-layout is used)\n", .{}); - print(" 22 - Pixel differences found\n", .{}); -} - -fn printVersion() void { - print("odiff {s} - SIMD first pixel-by-pixel image comparison tool\n", .{build_options.version}); -} - -/// Parse float argument that supports both --option=value and --option value formats -/// Updates the index pointer and returns the parsed f32 value or null if not matched -fn parseFloatArg(args: [][:0]u8, index: *usize, option_name: []const u8) ?f32 { - if (index.* >= args.len) return null; - const arg = args[index.*]; - - // --option=value format - if (std.mem.startsWith(u8, arg, option_name) and - arg.len > option_name.len and - arg[option_name.len] == '=') { - - const value_str = arg[option_name.len + 1..]; - if (value_str.len == 0) return null; - - index.* += 1; - return std.fmt.parseFloat(f32, value_str) catch null; - } - - // --option {value} format - if (std.mem.eql(u8, arg, option_name)) { - if (index.* + 1 >= args.len) return null; - - const next_arg = args[index.* + 1]; - - // Check if next argument is another option (starts with '-') - if (std.mem.startsWith(u8, next_arg, "-")) return null; - - index.* += 2; - return std.fmt.parseFloat(f32, next_arg) catch null; - } - - return null; -} - -fn parseIgnoreRegions(input: []const u8, list: *std.array_list.Managed(diff.IgnoreRegion)) !void { - var regions_iter = std.mem.splitSequence(u8, input, ","); - while (regions_iter.next()) |region_str| { - // Parse format: x1:y1-x2:y2 - var coords_iter = std.mem.splitSequence(u8, region_str, "-"); - const start_coords = coords_iter.next() orelse return error.InvalidFormat; - const end_coords = coords_iter.next() orelse return error.InvalidFormat; - - // Parse start coordinates - var start_iter = std.mem.splitSequence(u8, start_coords, ":"); - const x1_str = start_iter.next() orelse return error.InvalidFormat; - const y1_str = start_iter.next() orelse return error.InvalidFormat; - - // Parse end coordinates - var end_iter = std.mem.splitSequence(u8, end_coords, ":"); - const x2_str = end_iter.next() orelse return error.InvalidFormat; - const y2_str = end_iter.next() orelse return error.InvalidFormat; - - const x1 = try std.fmt.parseInt(u32, x1_str, 10); - const y1 = try std.fmt.parseInt(u32, y1_str, 10); - const x2 = try std.fmt.parseInt(u32, x2_str, 10); - const y2 = try std.fmt.parseInt(u32, y2_str, 10); - - try list.append(.{ .x1 = x1, .y1 = y1, .x2 = x2, .y2 = y2 }); - } -} - -pub fn parseHexColor(hex_str: []const u8) !u32 { - if (hex_str.len == 0) return 0xFF0000FF; // Default red pixel - - var color_str = hex_str; - if (hex_str[0] == '#') { - color_str = hex_str[1..]; - } - - if (color_str.len != 6) return error.InvalidHexColor; - - const r = try std.fmt.parseInt(u8, color_str[0..2], 16); - const g = try std.fmt.parseInt(u8, color_str[2..4], 16); - const b = try std.fmt.parseInt(u8, color_str[4..6], 16); - - return (@as(u32, 255) << 24) | (@as(u32, b) << 16) | (@as(u32, g) << 8) | @as(u32, r); -} - -pub fn parseArgs(allocator: std.mem.Allocator) !CliArgs { - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); - - if (args.len < 2) { - printUsage(args[0]); - std.process.exit(1); - } - - var parsed_args = CliArgs.init(allocator); - var i: usize = 1; - var positional_count: u32 = 0; - - while (i < args.len) { - const arg = args[i]; - - if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { - printUsage(args[0]); - std.process.exit(0); - } else if (std.mem.eql(u8, arg, "--version")) { - printVersion(); - std.process.exit(0); - } else if (parseFloatArg(args, &i, "--threshold") orelse parseFloatArg(args, &i, "-t")) |value| { - parsed_args.threshold = value; - continue; - } else if (std.mem.eql(u8, arg, "--diff-mask")) { - parsed_args.diff_mask = true; - } else if (parseFloatArg(args, &i, "--diff-overlay")) |value| { - parsed_args.diff_overlay_factor = value; - continue; - } else if (std.mem.eql(u8, arg, "--diff-overlay")) { - parsed_args.diff_overlay_factor = 0.5; - } else if (std.mem.eql(u8, arg, "--fail-on-layout")) { - parsed_args.fail_on_layout = true; - } else if (std.mem.eql(u8, arg, "--parsable-stdout")) { - parsed_args.parsable_stdout = true; - } else if (std.mem.eql(u8, arg, "--diff-color")) { - i += 1; - if (i >= args.len) { - print("Error: --diff-color requires a value\n", .{}); - std.process.exit(1); - } - parsed_args.diff_color = try allocator.dupe(u8, args[i]); - } else if (std.mem.eql(u8, arg, "--aa") or std.mem.eql(u8, arg, "--antialiasing")) { - parsed_args.antialiasing = true; - } else if (std.mem.eql(u8, arg, "--output-diff-lines")) { - parsed_args.diff_lines = true; - } else if (std.mem.eql(u8, arg, "--reduce-ram-usage")) { - parsed_args.reduce_ram_usage = true; - } else if (std.mem.eql(u8, arg, "--enable-asm")) { - parsed_args.enable_asm = true; - } else if (std.mem.eql(u8, arg, "-i") or std.mem.eql(u8, arg, "--ignore")) { - i += 1; - if (i >= args.len) { - print("Error: --ignore requires a value\n", .{}); - std.process.exit(1); - } - parseIgnoreRegions(args[i], &parsed_args.ignore_regions) catch { - print("Error: Invalid ignore regions format\n", .{}); - std.process.exit(1); - }; - } else if (std.mem.startsWith(u8, arg, "--ignore=")) { - const value_str = arg["--ignore=".len..]; - parseIgnoreRegions(value_str, &parsed_args.ignore_regions) catch { - print("Error: Invalid ignore regions format\n", .{}); - std.process.exit(1); - }; - } else if (std.mem.startsWith(u8, arg, "--diff-color=")) { - const value_str = arg["--diff-color=".len..]; - parsed_args.diff_color = try allocator.dupe(u8, value_str); - } else if (!std.mem.startsWith(u8, arg, "-")) { - // Positional argument - switch (positional_count) { - 0 => parsed_args.base_image = try allocator.dupe(u8, arg), - 1 => parsed_args.comp_image = try allocator.dupe(u8, arg), - 2 => parsed_args.diff_output = try allocator.dupe(u8, arg), - else => { - print("Error: Too many positional arguments\n", .{}); - std.process.exit(1); - }, - } - positional_count += 1; - } else { - print("Error: Unknown option {s}\n", .{arg}); - printUsage(args[0]); - - std.process.exit(1); - } - - i += 1; - } - - if (positional_count < 2) { - print("Error: Missing required arguments\n", .{}); - printUsage(args[0]); - std.process.exit(1); - } - - return parsed_args; -} diff --git a/odiff/src/color_delta.zig b/odiff/src/color_delta.zig deleted file mode 100644 index 1b1f607..0000000 --- a/odiff/src/color_delta.zig +++ /dev/null @@ -1,153 +0,0 @@ -const std = @import("std"); -const math = std.math; - -pub const Pixel = struct { - r: f64, - g: f64, - b: f64, - a: f64, -}; - -const WHITE_PIXEL = Pixel{ .r = 255.0, .g = 255.0, .b = 255.0, .a = 0.0 }; - -const YIQ_Y_R_COEFF = 0.29889531; -const YIQ_Y_G_COEFF = 0.58662247; -const YIQ_Y_B_COEFF = 0.11448223; - -const YIQ_I_R_COEFF = 0.59597799; -const YIQ_I_G_COEFF = -0.27417610; -const YIQ_I_B_COEFF = -0.32180189; - -const YIQ_Q_R_COEFF = 0.21147017; -const YIQ_Q_G_COEFF = -0.52261711; -const YIQ_Q_B_COEFF = 0.31114694; - -const YIQ_Y_WEIGHT = 0.5053; -const YIQ_I_WEIGHT = 0.299; -const YIQ_Q_WEIGHT = 0.1957; - -inline fn blendChannelWhite(color: f64, alpha: f64) f64 { - return 255.0 + ((color - 255.0) * alpha); -} - -pub inline fn blendSemiTransparentPixel(pixel: Pixel) Pixel { - if (pixel.a == 0.0) return WHITE_PIXEL; - if (pixel.a == 255.0) return Pixel{ .r = pixel.r, .g = pixel.g, .b = pixel.b, .a = 1.0 }; - if (pixel.a < 255.0) { - const normalized_alpha = pixel.a / 255.0; - return Pixel{ - .r = blendChannelWhite(pixel.r, normalized_alpha), - .g = blendChannelWhite(pixel.g, normalized_alpha), - .b = blendChannelWhite(pixel.b, normalized_alpha), - .a = normalized_alpha, - }; - } - unreachable; // Alpha > 255 -} - -pub inline fn decodeRawPixel(raw_pixel: u32) Pixel { - const a: f64 = @floatFromInt((raw_pixel >> 24) & 0xFF); - const b: f64 = @floatFromInt((raw_pixel >> 16) & 0xFF); - const g: f64 = @floatFromInt((raw_pixel >> 8) & 0xFF); - const r: f64 = @floatFromInt(raw_pixel & 0xFF); - - return Pixel{ .r = r, .g = g, .b = b, .a = a }; -} - -inline fn rgb2y(pixel: Pixel) f64 { - return (pixel.r * YIQ_Y_R_COEFF) + (pixel.g * YIQ_Y_G_COEFF) + (pixel.b * YIQ_Y_B_COEFF); -} - -inline fn rgb2i(pixel: Pixel) f64 { - return (pixel.r * YIQ_I_R_COEFF) + (pixel.g * YIQ_I_G_COEFF) + (pixel.b * YIQ_I_B_COEFF); -} - -inline fn rgb2q(pixel: Pixel) f64 { - return (pixel.r * YIQ_Q_R_COEFF) + (pixel.g * YIQ_Q_G_COEFF) + (pixel.b * YIQ_Q_B_COEFF); -} - -pub fn calculatePixelColorDelta(pixel_a: u32, pixel_b: u32) f64 { - const decoded_a = blendSemiTransparentPixel(decodeRawPixel(pixel_a)); - const decoded_b = blendSemiTransparentPixel(decodeRawPixel(pixel_b)); - - const y = rgb2y(decoded_a) - rgb2y(decoded_b); - const i = rgb2i(decoded_a) - rgb2i(decoded_b); - const q = rgb2q(decoded_a) - rgb2q(decoded_b); - - return (YIQ_Y_WEIGHT * y * y) + (YIQ_I_WEIGHT * i * i) + (YIQ_Q_WEIGHT * q * q); -} - -pub fn calculatePixelBrightnessDelta(pixel_a: u32, pixel_b: u32) f64 { - const decoded_a = blendSemiTransparentPixel(decodeRawPixel(pixel_a)); - const decoded_b = blendSemiTransparentPixel(decodeRawPixel(pixel_b)); - - return rgb2y(decoded_a) - rgb2y(decoded_b); -} - -// SIMD version of the same algorithm based on the shifted integer arithmetic -pub const COLOR_DELTA_SIMD_SHIFT = 12; -const SHIFTED_1 = 1 << COLOR_DELTA_SIMD_SHIFT; - -const VEC_2X_SHIFT: @Vector(2, i64) = @splat(COLOR_DELTA_SIMD_SHIFT); -const VEC_2X_SHIFTED_ONE: @Vector(2, i64) = @splat(SHIFTED_1); - -const VEC_YIQ_Y_R_COEFF: @Vector(2, i64) = @splat(@as(i64, @intFromFloat(YIQ_Y_R_COEFF * SHIFTED_1))); -const VEC_YIQ_Y_G_COEFF: @Vector(2, i64) = @splat(@as(i64, @intFromFloat(YIQ_Y_G_COEFF * SHIFTED_1))); -const VEC_YIQ_Y_B_COEFF: @Vector(2, i64) = @splat(@as(i64, @intFromFloat(YIQ_Y_B_COEFF * SHIFTED_1))); - -const VEC_YIQ_I_R_COEFF: @Vector(2, i64) = @splat(@as(i64, @intFromFloat(YIQ_I_R_COEFF * SHIFTED_1))); -const VEC_YIQ_I_G_COEFF: @Vector(2, i64) = @splat(@as(i64, @intFromFloat(YIQ_I_G_COEFF * SHIFTED_1))); -const VEC_YIQ_I_B_COEFF: @Vector(2, i64) = @splat(@as(i64, @intFromFloat(YIQ_I_B_COEFF * SHIFTED_1))); - -const VEC_YIQ_Q_R_COEFF: @Vector(2, i64) = @splat(@as(i64, @intFromFloat(YIQ_Q_R_COEFF * SHIFTED_1))); -const VEC_YIQ_Q_G_COEFF: @Vector(2, i64) = @splat(@as(i64, @intFromFloat(YIQ_Q_G_COEFF * SHIFTED_1))); -const VEC_YIQ_Q_B_COEFF: @Vector(2, i64) = @splat(@as(i64, @intFromFloat(YIQ_Q_B_COEFF * SHIFTED_1))); - -inline fn blendChannelSimd( - channel_vec: @Vector(2, i64), - vec_alpha: @Vector(2, i64), -) @Vector(2, i64) { - const VEC_2X_255: @Vector(2, i64) = @splat(255); - const VEC_2X_0: @Vector(2, i64) = @splat(0); - const VEC_2X_WHITE_SHIFTED = comptime VEC_2X_255 << VEC_2X_SHIFT; - - const alpha_zero = vec_alpha == VEC_2X_0; - const alpha_full = vec_alpha == VEC_2X_255; - - const opaque_value = channel_vec << VEC_2X_SHIFT; - const blended = VEC_2X_WHITE_SHIFTED + @divTrunc((channel_vec - VEC_2X_255) * vec_alpha * VEC_2X_SHIFTED_ONE, VEC_2X_255); - - return @select(i64, alpha_zero, VEC_2X_WHITE_SHIFTED, @select(i64, alpha_full, opaque_value, blended)); -} - -pub fn calculatePixelColorDeltaSimd(pixel_a: u32, pixel_b: u32) i64 { - const pixels: @Vector(2, u32) = .{ pixel_a, pixel_b }; - const mask_ff: @Vector(2, u32) = comptime @splat(0xFF); - - const vec_r_u32 = pixels & mask_ff; - const vec_g_u32 = (pixels >> @as(@Vector(2, u5), @splat(8))) & mask_ff; - const vec_b_u32 = (pixels >> @as(@Vector(2, u5), @splat(16))) & mask_ff; const vec_alpha_u32 = (pixels >> @as(@Vector(2, u5), @splat(24))) & mask_ff; - - const vec_r: @Vector(2, i64) = @intCast(vec_r_u32); - const vec_g: @Vector(2, i64) = @intCast(vec_g_u32); - const vec_b: @Vector(2, i64) = @intCast(vec_b_u32); - const vec_alpha: @Vector(2, i64) = @intCast(vec_alpha_u32); - - const blended_r = blendChannelSimd(vec_r, vec_alpha); - const blended_g = blendChannelSimd(vec_g, vec_alpha); - const blended_b = blendChannelSimd(vec_b, vec_alpha); - - const vec_y = (blended_r * VEC_YIQ_Y_R_COEFF + blended_g * VEC_YIQ_Y_G_COEFF + blended_b * VEC_YIQ_Y_B_COEFF) >> VEC_2X_SHIFT; - const vec_i = (blended_r * VEC_YIQ_I_R_COEFF + blended_g * VEC_YIQ_I_G_COEFF + blended_b * VEC_YIQ_I_B_COEFF) >> VEC_2X_SHIFT; - const vec_q = (blended_r * VEC_YIQ_Q_R_COEFF + blended_g * VEC_YIQ_Q_G_COEFF + blended_b * VEC_YIQ_Q_B_COEFF) >> VEC_2X_SHIFT; - - const y_diff = vec_y[0] - vec_y[1]; - const i_diff = vec_i[0] - vec_i[1]; - const q_diff = vec_q[0] - vec_q[1]; - - const Y_WEIGHT = comptime @as(i64, @intFromFloat(YIQ_Y_WEIGHT * SHIFTED_1)); - const I_WEIGHT = comptime @as(i64, @intFromFloat(YIQ_I_WEIGHT * SHIFTED_1)); - const Q_WEIGHT = comptime @as(i64, @intFromFloat(YIQ_Q_WEIGHT * SHIFTED_1)); - - return (y_diff * y_diff * Y_WEIGHT + i_diff * i_diff * I_WEIGHT + q_diff * q_diff * Q_WEIGHT) >> (2 * COLOR_DELTA_SIMD_SHIFT); -} diff --git a/odiff/src/diff.zig b/odiff/src/diff.zig deleted file mode 100644 index 9216428..0000000 --- a/odiff/src/diff.zig +++ /dev/null @@ -1,445 +0,0 @@ -// Core image diffing algorithm - equivalent to Diff.ml -const std = @import("std"); -const builtin = @import("builtin"); -const image_io = @import("image_io.zig"); -const color_delta = @import("color_delta.zig"); -const antialiasing = @import("antialiasing.zig"); - -const Image = image_io.Image; -const ArrayList = std.ArrayList; - -const HAS_AVX512f = std.Target.x86.featureSetHas(builtin.cpu.features, .avx512f); -const HAS_AVX512bwvl = - HAS_AVX512f and - std.Target.x86.featureSetHas(builtin.cpu.features, .avx512bw) and - std.Target.x86.featureSetHas(builtin.cpu.features, .avx512vl); -const HAS_NEON = builtin.cpu.arch == .aarch64 and std.Target.aarch64.featureSetHas(builtin.cpu.features, .neon); -const HAS_RVV = builtin.cpu.arch == .riscv64 and std.Target.riscv.featureSetHas(builtin.cpu.features, .v); - -const RED_PIXEL: u32 = 0xFF0000FF; -const WHITE_PIXEL: u32 = 0xFFFFFFFF; -const MAX_YIQ_POSSIBLE_DELTA: f64 = 35215.0; - -pub const DiffLines = struct { - lines: []u32, - count: u32, - allocator: std.mem.Allocator, - - pub fn init(allocator: std.mem.Allocator, max_height: u32) !DiffLines { - const lines = try allocator.alloc(u32, max_height); - return DiffLines{ - .lines = lines, - .count = 0, - .allocator = allocator, - }; - } - - pub fn deinit(self: *DiffLines) void { - self.allocator.free(self.lines); - } - - pub fn addLine(self: *DiffLines, line: u32) void { - // Exactly match original logic: if (lines.items.len == 0 or lines.items[lines.items.len - 1] < y) - if (self.count == 0 or (self.count > 0 and self.lines[self.count - 1] < line)) { - if (self.count < self.lines.len) { - self.lines[self.count] = line; - self.count += 1; - } - } - } - - pub fn getItems(self: *const DiffLines) []const u32 { - return self.lines[0..self.count]; - } -}; - -pub const DiffVariant = union(enum) { - layout, - pixel: struct { - diff_output: ?Image, - diff_count: u32, - diff_percentage: f64, - diff_lines: ?DiffLines, - }, -}; - -pub const IgnoreRegion = struct { - x1: u32, - y1: u32, - x2: u32, - y2: u32, -}; - -pub const DiffOptions = struct { - antialiasing: bool = false, - output_diff_mask: bool = false, - diff_overlay_factor: ?f32 = null, - diff_lines: bool = false, - diff_pixel: u32 = RED_PIXEL, - threshold: f64 = 0.1, - ignore_regions: ?[]const IgnoreRegion = null, - capture_diff: bool = true, - fail_on_layout_change: bool = true, - enable_asm: bool = false, -}; - -fn unrollIgnoreRegions(width: u32, regions: ?[]const IgnoreRegion, allocator: std.mem.Allocator) !?[]struct { u32, u32 } { - if (regions == null) return null; - var unrolled = try allocator.alloc(struct { u32, u32 }, regions.?.len); - for (regions.?, 0..) |region, i| { - const p1 = (region.y1 * width) + region.x1; - const p2 = (region.y2 * width) + region.x2; - unrolled[i] = .{ p1, p2 }; - } - return unrolled; -} - -fn isInIgnoreRegion(offset: u32, regions: ?[]const struct { u32, u32 }) bool { - if (regions == null) return false; - - for (regions.?) |region| { - if (offset >= region[0] and offset <= region[1]) { - return true; - } - } - return false; -} - -pub noinline fn compare( - base: *const Image, - comp: *const Image, - options: DiffOptions, - allocator: std.mem.Allocator, -) !struct { ?Image, u32, f64, ?DiffLines } { - const max_delta_f64 = MAX_YIQ_POSSIBLE_DELTA * (options.threshold * options.threshold); - const max_delta_i64: i64 = @intFromFloat(max_delta_f64 * @as(f64, @floatFromInt(1 << color_delta.COLOR_DELTA_SIMD_SHIFT))); - - var diff_output: ?Image = null; - if (options.capture_diff) { - if (options.diff_overlay_factor) |factor| { - diff_output = try base.makeWithWhiteOverlay(factor, allocator); - } else if (options.output_diff_mask) { - diff_output = try base.makeSameAsLayout(allocator); - } else { - const data = try allocator.dupe(u32, base.data); - diff_output = Image{ - .width = base.width, - .height = base.height, - .data = data, - .allocator = allocator, - }; - } - } - - var diff_count: u32 = 0; - var diff_lines: ?DiffLines = null; - if (options.diff_lines) { - const max_height = @max(base.height, comp.height); - diff_lines = try DiffLines.init(allocator, max_height); - } - - const ignore_regions = try unrollIgnoreRegions(base.width, options.ignore_regions, allocator); - defer if (ignore_regions) |regions| allocator.free(regions); - - const layout_difference = base.width != comp.width or base.height != comp.height; - - // AVX diff only supports default options - const threshold_ok = @abs(options.threshold - 0.1) < 0.0000001; - const no_ignore_regions = options.ignore_regions == null or options.ignore_regions.?.len == 0; - const avx_compatible = !options.antialiasing and no_ignore_regions and !options.capture_diff and !options.diff_lines and threshold_ok; - - if (options.enable_asm and HAS_AVX512bwvl and avx_compatible) { - try compareAVX(base, comp, &diff_count); - } else if (HAS_RVV and !options.antialiasing and (options.ignore_regions == null or options.ignore_regions.?.len == 0)) { - try compareRVV(base, comp, &diff_output, &diff_count, if (diff_lines != null) &diff_lines.? else null, ignore_regions, max_delta_f64, options); - } else if (layout_difference) { - // slow path for different layout or weird widths - try compareDifferentLayouts(base, comp, &diff_output, &diff_count, if (diff_lines != null) &diff_lines.? else null, ignore_regions, max_delta_i64, options); - } else { - try compareSameLayouts(base, comp, &diff_output, &diff_count, if (diff_lines != null) &diff_lines.? else null, ignore_regions, max_delta_i64, options); - } - - const diff_percentage = 100.0 * @as(f64, @floatFromInt(diff_count)) / - (@as(f64, @floatFromInt(base.width)) * @as(f64, @floatFromInt(base.height))); - - return .{ diff_output, diff_count, diff_percentage, diff_lines }; -} - -inline fn processPixelDifference( - pixel_offset: usize, - base_color: u32, - comp_color: u32, - x: u32, - y: u32, - base: *const Image, - comp: *const Image, - diff_output: *?Image, - diff_count: *u32, - diff_lines: ?*DiffLines, - ignore_regions: ?[]struct { u32, u32 }, - max_delta: i64, - options: DiffOptions, -) !void { - const is_ignored = isInIgnoreRegion(@intCast(pixel_offset), ignore_regions); - if (!is_ignored) { - const delta = @call(.never_inline, color_delta.calculatePixelColorDeltaSimd, .{ base_color, comp_color }); - if (delta > max_delta) { - var is_antialiased = false; - - if (options.antialiasing) { - is_antialiased = antialiasing.detect(x, y, base, comp) or - antialiasing.detect(x, y, comp, base); - } - - if (!is_antialiased) { - diff_count.* += 1; - if (diff_output.*) |*output| { - output.setImgColor(x, y, options.diff_pixel); - } - - if (diff_lines) |lines| { - lines.addLine(y); - } - } - } - } -} - -inline fn increment_coords(x: *u32, y: *u32, width: u32) void { - x.* += 1; - if (x.* >= width) { - x.* = 0; - y.* += 1; - } -} - -inline fn increment_coords_by(x: *u32, y: *u32, step: u32, width: u32) void { - var remaining = step; - while (remaining > 0) { - const pixels_to_end_of_row = width - x.*; - if (remaining >= pixels_to_end_of_row) { - remaining -= pixels_to_end_of_row; - x.* = 0; - y.* += 1; - } else { - x.* += remaining; - remaining = 0; - } - } -} - -pub noinline fn compareSameLayouts(base: *const Image, comp: *const Image, diff_output: *?Image, diff_count: *u32, diff_lines: ?*DiffLines, ignore_regions: ?[]struct { u32, u32 }, max_delta: i64, options: DiffOptions) !void { - var x: u32 = 0; - var y: u32 = 0; - - const size = (base.height * base.width); - const base_data = base.data; - const comp_data = comp.data; - - const SIMD_SIZE = std.simd.suggestVectorLength(u32) orelse if (HAS_AVX512f) 16 else if (HAS_NEON) 8 else 4; - const simd_end = (size / SIMD_SIZE) * SIMD_SIZE; - - var offset: usize = 0; - while (offset < simd_end) : (offset += SIMD_SIZE) { - const base_vec: @Vector(SIMD_SIZE, u32) = base_data[offset .. offset + SIMD_SIZE][0..SIMD_SIZE].*; - const comp_vec: @Vector(SIMD_SIZE, u32) = comp_data[offset .. offset + SIMD_SIZE][0..SIMD_SIZE].*; - - const diff_mask = base_vec != comp_vec; - if (!@reduce(.Or, diff_mask)) { - increment_coords_by(&x, &y, SIMD_SIZE, base.width); - continue; - } - - for (0..SIMD_SIZE) |i| { - if (diff_mask[i]) { - const pixel_offset = offset + i; - const base_color = base_vec[i]; - const comp_color = comp_vec[i]; - - try processPixelDifference( - pixel_offset, - base_color, - comp_color, - x, - y, - base, - comp, - diff_output, - diff_count, - diff_lines, - ignore_regions, - max_delta, - options, - ); - } - increment_coords(&x, &y, base.width); - } - } - - // Handle remaining pixels - while (offset < size) : (offset += 1) { - const base_color = base_data[offset]; - const comp_color = comp_data[offset]; - - if (base_color != comp_color) { - try processPixelDifference( - offset, - base_color, - comp_color, - x, - y, - base, - comp, - diff_output, - diff_count, - diff_lines, - ignore_regions, - max_delta, - options, - ); - } - increment_coords(&x, &y, base.width); - } -} - -pub fn compareDifferentLayouts(base: *const Image, comp: *const Image, maybe_diff_output: *?Image, diff_count: *u32, diff_lines: ?*DiffLines, ignore_regions: ?[]struct { u32, u32 }, max_delta: i64, options: DiffOptions) !void { - var x: u32 = 0; - var y: u32 = 0; - var offset: u32 = 0; - - const size = (base.height * base.width); - while (offset < size) : (offset += 1) { - const base_color = base.readRawPixel(x, y); - - if (x >= comp.width or y >= comp.height) { - const alpha = (base_color >> 24) & 0xFF; - if (alpha != 0) { - diff_count.* += 1; - if (maybe_diff_output.*) |*output| { - output.setImgColor(x, y, options.diff_pixel); - } - - if (diff_lines) |lines| { - lines.addLine(y); - } - } - } else { - const comp_color = comp.readRawPixel(x, y); - - try processPixelDifference( - offset, - base_color, - comp_color, - x, - y, - base, - comp, - maybe_diff_output, - diff_count, - diff_lines, - ignore_regions, - max_delta, - options, - ); - } - - increment_coords(&x, &y, base.width); - } -} - -pub fn compareAVX(base: *const Image, comp: *const Image, diff_count: *u32) !void { - if (!HAS_AVX512bwvl) return error.Invalid; - - const base_ptr: [*]const u8 = @ptrCast(@alignCast(base.data.ptr)); - const comp_ptr: [*]const u8 = @ptrCast(@alignCast(comp.data.ptr)); - - const base_w: usize = base.width; - const base_h: usize = base.height; - const comp_w: usize = comp.width; - const comp_h: usize = comp.height; - - diff_count.* = vxdiff(base_ptr, comp_ptr, base_w, comp_w, base_h, comp_h); -} - -extern fn vxdiff( - base_rgba: [*]const u8, - comp_rgba: [*]const u8, - base_width: usize, - comp_width: usize, - base_height: usize, - comp_height: usize, -) u32; - -extern fn odiffRVV( - basePtr: [*]const u32, - compPtr: [*]const u32, - size: usize, - max_delta: f32, - diff: ?[*]u32, - diffcol: u32, -) u32; - -pub noinline fn compareRVV(base: *const Image, comp: *const Image, diff_output: *?Image, diff_count: *u32, diff_lines: ?*DiffLines, ignore_regions: ?[]struct { u32, u32 }, max_delta: f64, options: DiffOptions) !void { - _ = ignore_regions; - const basePtr: [*]const u32 = @ptrCast(@alignCast(base.data.ptr)); - const compPtr: [*]const u32 = @ptrCast(@alignCast(comp.data.ptr)); - var diffPtr: ?[*]u32 = null; - if (diff_output.*) |*out| { - diffPtr = @ptrCast(@alignCast(out.data.ptr)); - } - - const line_by_line = base.width != comp.width or base.height != comp.height or diff_lines != null; - if (line_by_line) { - var y: u32 = 0; - const minHeight = @min(base.height, comp.height); - const minWidth = @min(base.width, comp.width); - while (y < base.height) : (y += 1) { - var cnt: u32 = 0; - var x: u32 = 0; - if (y < minHeight) { - if (diffPtr) |ptr| { - cnt = odiffRVV(basePtr + y * base.width, compPtr + y * comp.width, minWidth, @floatCast(max_delta), ptr + y * base.width, options.diff_pixel); - } else { - cnt = odiffRVV(basePtr + y * base.width, compPtr + y * comp.width, minWidth, @floatCast(max_delta), null, options.diff_pixel); - } - x = minWidth; - } - while (x < base.width) : (x += 1) { - const idx = y * base.width + x; - const alpha = (basePtr[idx] >> 24) & 0xFF; - cnt += if (alpha != 0) 1 else 0; - if (diffPtr) |ptr| { - const old = ptr[idx]; // always read/write for better autovec - ptr[idx] = if (alpha != 0) options.diff_pixel else old; - } - } - if (diff_lines) |lines| { - if (cnt > 0) { - lines.addLine(y); - } - } - diff_count.* += cnt; - } - } else { - diff_count.* += odiffRVV(basePtr, compPtr, base.height * base.width, @floatCast(max_delta), diffPtr, options.diff_pixel); - } -} - -pub fn diff( - base: *const Image, - comp: *const Image, - options: DiffOptions, - allocator: std.mem.Allocator, -) !DiffVariant { - if (options.fail_on_layout_change and (base.width != comp.width or base.height != comp.height)) { - return DiffVariant.layout; - } - - const diff_output, const diff_count, const diff_percentage, const diff_lines = try compare(base, comp, options, allocator); - return DiffVariant{ .pixel = .{ - .diff_output = diff_output, - .diff_count = diff_count, - .diff_percentage = diff_percentage, - .diff_lines = diff_lines, - } }; -} diff --git a/odiff/src/image_io.zig b/odiff/src/image_io.zig deleted file mode 100644 index 1e6f767..0000000 --- a/odiff/src/image_io.zig +++ /dev/null @@ -1,169 +0,0 @@ -const std = @import("std"); - -const c_bindings = @import("c_bindings.zig"); -const CImageData = c_bindings.CImageData; - -pub const ImageFormat = enum { - png, - jpg, - bmp, - tiff, - webp, -}; - -pub const Image = struct { - width: u32, - height: u32, - /// RGBA pixels as 32-bit integers - data: []u32, - is_c_allocated: bool = false, - allocator: std.mem.Allocator, - - pub fn deinit(self: *Image) void { - if (self.is_c_allocated) { - var alloc = self.allocator; - c_bindings.free_image_data_ptr(self.data.ptr, &alloc, self.data.len * @sizeOf(u32)); - } else { - self.allocator.free(self.data); - } - } - - pub inline fn readRawPixelAtOffset(self: *const Image, offset: usize) u32 { - return self.data[offset]; - } - - pub fn readRawPixel(self: *const Image, x: u32, y: u32) u32 { - const offset = y * self.width + x; - return self.data[offset]; - } - - pub fn setImgColor(self: *Image, x: u32, y: u32, color: u32) void { - const offset = y * self.width + x; - self.data[offset] = color; - } - - pub fn makeSameAsLayout(self: *const Image, allocator: std.mem.Allocator) !Image { - const data = try allocator.alloc(u32, self.data.len); - @memset(data, 0); - return Image{ - .width = self.width, - .height = self.height, - .data = data, - .allocator = allocator, - }; - } - - pub fn makeWithWhiteOverlay(self: *const Image, factor: f32, allocator: std.mem.Allocator) !Image { - const data = try allocator.alloc(u32, self.data.len); - - const R_COEFF: u32 = 19595; // 0.29889531 * 65536 - const G_COEFF: u32 = 38469; // 0.58662247 * 65536 - const B_COEFF: u32 = 7504; // 0.11448223 * 65536 - const WHITE_SHADE_FACTOR: u32 = @intFromFloat(factor * 255); // by default 128 - const INV_SHADE_FACTOR: u32 = 255 - WHITE_SHADE_FACTOR; - const WHITE_CONTRIBUTION: u32 = WHITE_SHADE_FACTOR * 255; - const FULL_ALPHA: u32 = 0xFF000000; - - const SIMD_SIZE = std.simd.suggestVectorLength(u32) orelse 4; - const simd_end = (data.len / SIMD_SIZE) * SIMD_SIZE; - - const R_COEFF_VEC: @Vector(SIMD_SIZE, u32) = @splat(R_COEFF); - const G_COEFF_VEC: @Vector(SIMD_SIZE, u32) = @splat(G_COEFF); - const B_COEFF_VEC: @Vector(SIMD_SIZE, u32) = @splat(B_COEFF); - const INV_SHADE_VEC: @Vector(SIMD_SIZE, u32) = @splat(INV_SHADE_FACTOR); - const WHITE_CONTRIB_VEC: @Vector(SIMD_SIZE, u32) = @splat(WHITE_CONTRIBUTION); - const DIV255_VEC: @Vector(SIMD_SIZE, u32) = @splat(255); - const MASK_VEC: @Vector(SIMD_SIZE, u32) = @splat(0xFF); - const ALPHA_VEC: @Vector(SIMD_SIZE, u32) = @splat(FULL_ALPHA); - - var i: usize = 0; - while (i < simd_end) : (i += SIMD_SIZE) { - const pixels: @Vector(SIMD_SIZE, u32) = self.data[i .. i + SIMD_SIZE][0..SIMD_SIZE].*; - const r_vec = (pixels >> @splat(16)) & MASK_VEC; - const g_vec = (pixels >> @splat(8)) & MASK_VEC; - const b_vec = pixels & MASK_VEC; - - const luminance_scaled = r_vec * R_COEFF_VEC + g_vec * G_COEFF_VEC + b_vec * B_COEFF_VEC; - const luminance_vec = luminance_scaled >> @as(@Vector(SIMD_SIZE, u5), @splat(16)); - const blended_vec = (INV_SHADE_VEC * luminance_vec + WHITE_CONTRIB_VEC) / DIV255_VEC; - - const gray_masked = blended_vec & MASK_VEC; - const result_vec = ALPHA_VEC | (gray_masked << @splat(16)) | (gray_masked << @splat(8)) | gray_masked; - - @memcpy(data[i .. i + SIMD_SIZE], @as(*const [SIMD_SIZE]u32, @ptrCast(&result_vec))); - } - - // handle remaining pixels - while (i < data.len) : (i += 1) { - const pixel = self.data[i]; - - const red = (pixel >> 16) & 0xFF; - const green = (pixel >> 8) & 0xFF; - const blue = pixel & 0xFF; - - const luminance = (red * R_COEFF + green * G_COEFF + blue * B_COEFF) >> 16; - const gray_val = (INV_SHADE_FACTOR * luminance + WHITE_CONTRIBUTION) / 255; - data[i] = FULL_ALPHA | (gray_val << 16) | (gray_val << 8) | gray_val; - } - - return Image{ - .width = self.width, - .height = self.height, - .data = data, - .allocator = allocator, - }; - } -}; - -pub const ImageError = error{ - ImageNotLoaded, - UnsupportedFormat, - UnsupportedWriteFormat, - InvalidData, - OutOfMemory, - WriteFailed, -}; - -pub fn getImageFormat(filename: []const u8) !ImageFormat { - if (std.mem.endsWith(u8, filename, ".png")) return .png; - - if (std.mem.endsWith(u8, filename, ".jpg") or std.mem.endsWith(u8, filename, ".jpeg")) return .jpg; - if (std.mem.endsWith(u8, filename, ".bmp")) return .bmp; - if (std.mem.endsWith(u8, filename, ".tiff")) return .tiff; - if (std.mem.endsWith(u8, filename, ".webp")) return .webp; - - return error.UnsupportedFormat; -} - -pub fn loadImage(filename: []const u8, allocator: std.mem.Allocator) !Image { - const format = try getImageFormat(filename); - - const result = switch (format) { - .png => try c_bindings.readPngFile(filename, allocator), - .jpg => try c_bindings.readJpgFile(filename, allocator), - .tiff => try c_bindings.readTiffFile(filename, allocator), - .bmp => try c_bindings.readBmpFile(filename, allocator), - .webp => try c_bindings.readWebpFile(filename, allocator), - }; - - const unwrapped_result = result orelse return ImageError.ImageNotLoaded; - - return Image{ - .width = unwrapped_result.width, - .height = unwrapped_result.height, - .data = unwrapped_result.data, - .allocator = allocator, - .is_c_allocated = unwrapped_result.is_c_allocated, - }; -} - -pub fn saveImage(image: *const Image, filename: []const u8, allocator: std.mem.Allocator) !void { - const format = try getImageFormat(filename); - - switch (format) { - .png => { - try c_bindings.writePngFile(filename, image.width, image.height, image.data, allocator); - }, - else => return ImageError.UnsupportedWriteFormat, - } -} diff --git a/odiff/src/main.zig b/odiff/src/main.zig deleted file mode 100644 index 03c4bd7..0000000 --- a/odiff/src/main.zig +++ /dev/null @@ -1,171 +0,0 @@ -const std = @import("std"); -const lib = @import("odiff_lib"); - -const print = std.debug.print; - -const cli = lib.cli; -const image_io = lib.image_io; -const diff = lib.diff; - -// we need a large stdout for the lines parsable output -var stdout_buffer: [4096]u8 = undefined; - -pub fn main() !void { - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); - const stdout = &stdout_writer.interface; - - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - - const allocator = arena.allocator(); - - var args = cli.parseArgs(allocator) catch |err| switch (err) { - error.OutOfMemory => { - print("Error: Out of memory\n", .{}); - std.process.exit(1); - }, - else => { - print("Error: Failed to parse arguments\n", .{}); - std.process.exit(1); - }, - }; - defer args.deinit(); - - // Load images - var base_img = image_io.loadImage(args.base_image, allocator) catch |err| switch (err) { - error.ImageNotLoaded => { - print("Error: Could not load base image: {s}\n", .{args.base_image}); - std.process.exit(1); - }, - error.UnsupportedFormat => { - print("Error: Unsupported image format: {s}\n", .{args.base_image}); - std.process.exit(1); - }, - else => { - print("Error: Failed to load base image\n", .{}); - std.process.exit(1); - }, - }; - defer base_img.deinit(); - - var comp_img = image_io.loadImage(args.comp_image, allocator) catch |err| switch (err) { - error.ImageNotLoaded => { - print("Error: Could not load comparison image: {s}\n", .{args.comp_image}); - std.process.exit(1); - }, - error.UnsupportedFormat => { - print("Error: Unsupported image format: {s}\n", .{args.comp_image}); - std.process.exit(1); - }, - else => { - print("Error: Failed to load comparison image\n", .{}); - std.process.exit(1); - }, - }; - defer comp_img.deinit(); - - const diff_pixel = cli.parseHexColor(args.diff_color) catch { - print("Error: Invalid hex color format\n", .{}); - std.process.exit(1); - }; - - const diff_options = diff.DiffOptions{ - .output_diff_mask = args.diff_mask, - .diff_overlay_factor = args.diff_overlay_factor, - .threshold = args.threshold, - .diff_pixel = diff_pixel, - .fail_on_layout_change = args.fail_on_layout, - .antialiasing = args.antialiasing, - .diff_lines = args.diff_lines, - .ignore_regions = args.ignore_regions.items, - .capture_diff = args.diff_output != null, - .enable_asm = args.enable_asm, - }; - - const result = diff.diff(&base_img, &comp_img, diff_options, allocator) catch |err| { - print("Error: Failed to perform diff: {}\n", .{err}); - std.process.exit(1); - }; - - std.debug.print("Base Image: {s}\n", .{args.base_image}); - std.debug.print("Base Image: {s}\n", .{args.comp_image}); - - switch (result) { - .layout => { - if (args.parsable_stdout) { - stdout.print("layout\n", .{}) catch {}; - } else { - print("Images have different dimensions\n", .{}); - } - - try stdout.flush(); - std.process.exit(21); - }, - .pixel => |pixel_result| { - defer { - if (pixel_result.diff_output) |*output| { - var img = output.*; - img.deinit(); - } - if (pixel_result.diff_lines) |lines| { - var mutable_lines = lines; - mutable_lines.deinit(); - } - } - - if (pixel_result.diff_count == 0) { - if (args.parsable_stdout) { - stdout.print("0\n", .{}) catch {}; - } else { - print("Images are identical\n", .{}); - } - - try stdout.flush(); - std.process.exit(0); - } else { - // Save diff output if requested - if (args.diff_output) |output_path| { - if (pixel_result.diff_output) |output_img| { - image_io.saveImage(&output_img, output_path, allocator) catch { - print("Error: Failed to save diff output\n", .{}); - try stdout.flush(); - - std.process.exit(1); - }; - } - } - - if (args.parsable_stdout) { - stdout.print("{d};{:.2}", .{ pixel_result.diff_count, pixel_result.diff_percentage }) catch {}; - if (pixel_result.diff_lines) |diff_lines| { - if (diff_lines.count > 0) { - stdout.print(";", .{}) catch {}; - for (diff_lines.getItems(), 0..) |line, i| { - if (i > 0) stdout.print(",", .{}) catch {}; - stdout.print("{d}", .{line}) catch {}; - } - } - } - stdout.print("\n", .{}) catch {}; - } else { - print("Found {d} different pixels ({:.2}%)\n", .{ pixel_result.diff_count, pixel_result.diff_percentage }); - if (args.diff_lines) { - if (pixel_result.diff_lines) |diff_lines| { - if (diff_lines.count > 0) { - print("Different lines: ", .{}); - for (diff_lines.getItems(), 0..) |line, i| { - if (i > 0) print(", ", .{}); - print("{d}", .{line}); - } - print("\n", .{}); - } - } - } - } - - try stdout.flush(); - std.process.exit(22); - } - }, - } -} diff --git a/odiff/src/rvv.c b/odiff/src/rvv.c deleted file mode 100644 index 63128bf..0000000 --- a/odiff/src/rvv.c +++ /dev/null @@ -1,167 +0,0 @@ -#include -#include - -#if !__riscv_vector - -/* unused stubs */ -uint32_t odiffRVV(uint32_t *src1, uint32_t *src2, size_t n, float max_delta, uint32_t *diff, uint32_t diffcol) { return 0; } -double calculatePixelColorDeltaRVVForTest(uint32_t pixel_a, uint32_t pixel_b) { return 0; } - -#else - -/* See also: "Measuring perceived color difference using YIQ NTSC - * transmission color space in mobile applications" */ -#define YIQ_Y_R_COEFF 0.29889531 -#define YIQ_Y_G_COEFF 0.58662247 -#define YIQ_Y_B_COEFF 0.11448223 - -#define YIQ_I_R_COEFF 0.59597799 -#define YIQ_I_G_COEFF -0.27417610 -#define YIQ_I_B_COEFF -0.32180189 - -#define YIQ_Q_R_COEFF 0.21147017 -#define YIQ_Q_G_COEFF -0.52261711 -#define YIQ_Q_B_COEFF 0.31114694 - -#define YIQ_Y_WEIGHT 0.5053 -#define YIQ_I_WEIGHT 0.299 -#define YIQ_Q_WEIGHT 0.1957 - -#if 0 -/* simplifying the original equation */ -y = rgb2y(a) - rgb2y(b) = rgb2y(a - b); -i = rgb2i(a) - rgb2i(b) = rgb2i(a - b); -q = rgb2q(a) - rgb2q(b) = rgb2q(a - b); -return (YIQ_Y_WEIGHT * y * y) + (YIQ_I_WEIGHT * i * i) + (YIQ_Q_WEIGHT * q * q); - -[r,g,b] = a-b; -y = ((r * YIQ_Y_R_COEFF) + (g * YIQ_Y_G_COEFF) + (b * YIQ_Y_B_COEFF)) * YIQ_Y_WEIGHT_SQRT; -i = ((r * YIQ_I_R_COEFF) + (g * YIQ_I_G_COEFF) + (b * YIQ_I_B_COEFF)) * YIQ_I_WEIGHT_SQRT; -q = ((r * YIQ_Q_R_COEFF) + (g * YIQ_Q_G_COEFF) + (b * YIQ_Q_B_COEFF)) * YIQ_Q_WEIGHT_SQRT; -return y*y + i*i + q*q; - -return (r*C1 + g*C2 + b*C3)**2 - +(r*C4 + g*C5 + b*C6)**2 - +(r*C7 + g*C8 + b*C9)**2; - -return r*C1*(r*C1 + g*2*C2 + b*2*C3) + (g*C2)**2 + b*C3*(g*2*C2 + b*C3) - + r*C4*(r*C4 + g*2*C5 + b*2*C6) + (g*C5)**2 + b*C6*(g*2*C5 + b*C6) - + r*C7*(r*C7 + g*2*C8 + b*2*C9) + (g*C8)**2 + b*C9*(g*2*C8 + b*C9); - -return r*r*C1*C1 + r*g*2*C2*C1 + r*b*2*C3*C1 + (g*C2)**2 + b*g*2*C2*C3 + b*b*C3*C3 - + r*r*C4*C4 + r*g*2*C5*C4 + r*b*2*C6*C4 + (g*C5)**2 + b*g*2*C5*C6 + b*b*C6*C6 - + r*r*C7*C7 + r*g*2*C8*C7 + r*b*2*C9*C7 + (g*C8)**2 + b*g*2*C8*C9 + b*b*C9*C9; - -return r*r * (C1*C1+C4*C4+C7*C7) - + r*g*2*(C2*C1+C5*C4+C8*C7) - + r*b*2*(C3*C1+C6*C4+C9*C7) - + g*g * (C2*C2+C5*C5+C8*C8) - + b*g*2*(C2*C3+C5*C6+C8*C9) - + b*b * (C3*C3+C6*C6+C9*C9); - -// 24 -> 18 -> 15 -> 12 instructions -return r*(r*Y1 + g*Y2 + b*Y3) + g*(g*Y4 + b*Y5) + b*b*Y6; -#endif - -#define YIQ_Y_WEIGHT_SQRT 0.7108445681019163 -#define YIQ_I_WEIGHT_SQRT 0.5468089245796927 -#define YIQ_Q_WEIGHT_SQRT 0.4423799272118933 - -#define C1 (YIQ_Y_R_COEFF*YIQ_Y_WEIGHT_SQRT) -#define C2 (YIQ_Y_G_COEFF*YIQ_Y_WEIGHT_SQRT) -#define C3 (YIQ_Y_B_COEFF*YIQ_Y_WEIGHT_SQRT) -#define C4 (YIQ_I_R_COEFF*YIQ_I_WEIGHT_SQRT) -#define C5 (YIQ_I_G_COEFF*YIQ_I_WEIGHT_SQRT) -#define C6 (YIQ_I_B_COEFF*YIQ_I_WEIGHT_SQRT) -#define C7 (YIQ_Q_R_COEFF*YIQ_Q_WEIGHT_SQRT) -#define C8 (YIQ_Q_G_COEFF*YIQ_Q_WEIGHT_SQRT) -#define C9 (YIQ_Q_B_COEFF*YIQ_Q_WEIGHT_SQRT) - -#define Y1 ( (C1*C1+C4*C4+C7*C7)) /* 0.160096 */ -#define Y2 (2*(C2*C1+C5*C4+C8*C7)) /* 0.036226 */ -#define Y3 (2*(C3*C1+C6*C4+C9*C7)) /* -0.054354 */ -#define Y4 ( (C2*C2+C5*C5+C8*C8)) /* 0.249815 */ -#define Y5 (2*(C2*C3+C5*C6+C8*C9)) /* 0.056986 */ -#define Y6 ( (C3*C3+C6*C6+C9*C9)) /* 0.056532 */ - -#include - -static inline vfloat32m4_t -rvv_yiq_diff(vuint8m1x4_t v4, vuint8m1x4_t y4, size_t vl) -{ - vfloat32m4_t vr = __riscv_vfwcvt_f(__riscv_vzext_vf2(__riscv_vget_u8m1(v4, 0), vl), vl); - vfloat32m4_t vg = __riscv_vfwcvt_f(__riscv_vzext_vf2(__riscv_vget_u8m1(v4, 1), vl), vl); - vfloat32m4_t vb = __riscv_vfwcvt_f(__riscv_vzext_vf2(__riscv_vget_u8m1(v4, 2), vl), vl); - vfloat32m4_t va = __riscv_vfwcvt_f(__riscv_vzext_vf2(__riscv_vget_u8m1(v4, 3), vl), vl); - vfloat32m4_t yr = __riscv_vfwcvt_f(__riscv_vzext_vf2(__riscv_vget_u8m1(y4, 0), vl), vl); - vfloat32m4_t yg = __riscv_vfwcvt_f(__riscv_vzext_vf2(__riscv_vget_u8m1(y4, 1), vl), vl); - vfloat32m4_t yb = __riscv_vfwcvt_f(__riscv_vzext_vf2(__riscv_vget_u8m1(y4, 2), vl), vl); - vfloat32m4_t ya = __riscv_vfwcvt_f(__riscv_vzext_vf2(__riscv_vget_u8m1(y4, 3), vl), vl); - vfloat32m4_t vy, dr, dg, db, v; - - /* ((v-255)*(va/255)+255) - ((y-255)*(ya/255)+255) - * = (v-255)*(va/255)+255 - (y-255)*(ya/255)-255 - * = (v-255)*(va/255) - (y-255)*(ya/255) - * = (v-255)*(va/255) - (y-255)*(ya/255) - * = (v*va/255-255*va/255) - (y*ya/255-255*ya/255) - * = (v*va/255- va ) - (y*ya/255- ya ) - * = v*va/255- va - y*ya/255+ ya - * = v*va/255- y*ya/255 - va + ya - * = v*va/255- (y*ya/255 + (va - ya)) */ - vy = __riscv_vfsub(va, ya, vl); - va = __riscv_vfmul(va, 1/255.0f, vl); - ya = __riscv_vfmul(ya, 1/255.0f, vl); - dr = __riscv_vfmsub(vr, va, __riscv_vfmadd(yr, ya, vy, vl), vl); - dg = __riscv_vfmsub(vg, va, __riscv_vfmadd(yg, ya, vy, vl), vl); - db = __riscv_vfmsub(vb, va, __riscv_vfmadd(yb, ya, vy, vl), vl); - - /* r*(r*Y1 + g*Y2 + b*Y3) + g*(g*Y4 + b*Y5) + b*b*Y6 (see top of file) */ - v = __riscv_vfmul(__riscv_vfmul(db, Y6, vl), db, vl); - v = __riscv_vfmacc(v, dg, __riscv_vfmacc(__riscv_vfmul(dg, Y4, vl), Y5, db, vl), vl); - v = __riscv_vfmacc(v, dr, __riscv_vfmacc(__riscv_vfmacc(__riscv_vfmul(dr, Y1, vl), Y2, dg, vl), Y3, db, vl), vl); - return v; -} - -uint32_t -odiffRVV(uint32_t *src1, uint32_t *src2, size_t n, float max_delta, uint32_t *diff, uint32_t diffcol) -{ - size_t count = 0; - for (size_t i = 0, vl; n > 0; n -= vl, i += vl) { - vl = __riscv_vsetvl_e32m2(n); - vuint32m2_t v1 = __riscv_vle32_v_u32m2(src1+i, vl); - vuint32m2_t v2 = __riscv_vle32_v_u32m2(src2+i, vl); - long idx = __riscv_vfirst(__riscv_vmsne(v1, v2, vl), vl); - if (idx < 0) continue; - n -= idx; - i += idx; - - vl = __riscv_vsetvl_e8m1(n); - vuint8m1x4_t v4 = __riscv_vlseg4e8_v_u8m1x4((uint8_t*)(src1+i), vl); - vuint8m1x4_t y4 = __riscv_vlseg4e8_v_u8m1x4((uint8_t*)(src2+i), vl); - - vbool8_t m = __riscv_vmfgt(rvv_yiq_diff(v4, y4, vl), max_delta, vl); - count += __riscv_vcpop(m, vl); - - if (diff) { - __riscv_vse32(m, diff+i, __riscv_vmv_v_x_u32m4(diffcol, vl), vl); - } - } - return count; -} - -double /* Wrapper for testing */ -calculatePixelColorDeltaRVVForTest(uint32_t pixel_a, uint32_t pixel_b) -{ - vuint8m1x4_t v4 = __riscv_vcreate_v_u8m1x4( - __riscv_vmv_s_x_u8m1((uint8_t)(pixel_a>>(0*8)), 1), - __riscv_vmv_s_x_u8m1((uint8_t)(pixel_a>>(1*8)), 1), - __riscv_vmv_s_x_u8m1((uint8_t)(pixel_a>>(2*8)), 1), - __riscv_vmv_s_x_u8m1((uint8_t)(pixel_a>>(3*8)), 1)); - vuint8m1x4_t y4 = __riscv_vcreate_v_u8m1x4( - __riscv_vmv_s_x_u8m1((uint8_t)(pixel_b>>(0*8)), 1), - __riscv_vmv_s_x_u8m1((uint8_t)(pixel_b>>(1*8)), 1), - __riscv_vmv_s_x_u8m1((uint8_t)(pixel_b>>(2*8)), 1), - __riscv_vmv_s_x_u8m1((uint8_t)(pixel_b>>(3*8)), 1)); - return __riscv_vfmv_f(rvv_yiq_diff(v4, y4, 1)); -} -#endif diff --git a/odiff/src/test_avx.zig b/odiff/src/test_avx.zig deleted file mode 100644 index 8f146b5..0000000 --- a/odiff/src/test_avx.zig +++ /dev/null @@ -1,66 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const expect = testing.expect; -const expectEqual = testing.expectEqual; -const expectApproxEqRel = testing.expectApproxEqRel; - -const odiff = @import("root.zig"); -const image_io = odiff.image_io; -const diff = odiff.diff; -const color_delta = odiff.color_delta; - -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !image_io.Image { - return image_io.loadImage(path, allocator) catch |err| { - std.debug.print("Failed to load image: {s}\nError: {}\n", .{ path, err }); - return err; - }; -} - -test "layoutDifference: diff images with different layouts without capture" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/png/white4x4.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/png/purple8x8.png", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{ - .antialiasing = false, - .output_diff_mask = false, - .capture_diff = false, - .enable_asm = true, - }; - - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 16), diff_count); // diffPixels - try expectApproxEqRel(@as(f64, 100.0), diff_percentage, 0.001); // diffPercentage -} - -test "PNG: finds difference between 2 images without capture" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/png/orange.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{ - .capture_diff = false, - .enable_asm = true, - }; - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 1366), diff_count); // diffPixels - try expectApproxEqRel(@as(f64, 1.14), diff_percentage, 0.1); // diffPercentage -} diff --git a/odiff/src/test_color_delta.zig b/odiff/src/test_color_delta.zig deleted file mode 100644 index 3d5f3cc..0000000 --- a/odiff/src/test_color_delta.zig +++ /dev/null @@ -1,136 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const testing = std.testing; -const color_delta = @import("color_delta.zig"); - -const HAS_RVV = builtin.cpu.arch == .riscv64 and std.Target.riscv.featureSetHas(builtin.cpu.features, .v); -extern fn calculatePixelColorDeltaRVVForTest(pixel_a: u32, pixel_b: u32,) f64; - -fn calculatePixelColorDeltaUnderTest(pixel_a: u32, pixel_b: u32,) f64 { - if (HAS_RVV) { - return calculatePixelColorDeltaRVVForTest(pixel_a, pixel_b); - } else { - const fixed_i64 = color_delta.calculatePixelColorDeltaSimd(pixel_a, pixel_b); - return @as(f64, @floatFromInt(fixed_i64)) / 4096.0; - } -} - -test "color delta: compare fixed-point vs floating-point precision" { - const test_cases = [_]struct { u32, u32, []const u8 }{ - .{ 0xFF000000, 0xFF010101, "slight RGB difference" }, - .{ 0xFF000000, 0xFFFFFFFF, "black vs white" }, - .{ 0xFFFF0000, 0xFF00FF00, "red vs green" }, - .{ 0xFF0000FF, 0xFFFFFF00, "blue vs yellow" }, - .{ 0x80808080, 0x80818181, "semi-transparent slight difference" }, - .{ 0x00000000, 0xFF000000, "transparent vs opaque black" }, - .{ 0xFF808080, 0xFF828282, "gray slight difference" }, - .{ 0xFFFF8080, 0xFFFF8282, "pinkish slight difference" }, - .{ 0xFF123456, 0xFF123457, "minimal blue difference" }, - .{ 0xFF800000, 0xFF008000, "dark red vs dark green" }, - }; - - std.debug.print("\n=== Color Delta Comparison Test ===\n", .{}); - std.debug.print("Format: Original(float) | SIMD(float) | Diff | Error%\n", .{}); - std.debug.print("-----------------------------------------------------------\n", .{}); - - var max_error_percent: f64 = 0.0; - var total_error: f64 = 0.0; - - for (test_cases) |case| { - const original_result = color_delta.calculatePixelColorDelta(case[0], case[1]); - const fixed_result_f64 = calculatePixelColorDeltaUnderTest(case[0], case[1]); - - const diff = @abs(original_result - fixed_result_f64); - const error_percent = if (original_result != 0.0) (diff / original_result) * 100.0 else 0.0; - - max_error_percent = @max(max_error_percent, error_percent); - total_error += error_percent; - - std.debug.print("{s:<25}: {d:10.6} | {d:10.6} | {d:8.6} | {d:6.3}%\n", .{ - case[2], - original_result, - fixed_result_f64, - diff, - error_percent, - }); - } - - const avg_error_percent = total_error / @as(f64, @floatFromInt(test_cases.len)); - - std.debug.print("-----------------------------------------------------------\n", .{}); - std.debug.print("Max Error: {d:.3}% | Average Error: {d:.3}%\n", .{ max_error_percent, avg_error_percent }); - - // verify that the conversion errors are within < 0.5% difference - try testing.expect(avg_error_percent < 0.2); - try testing.expect(max_error_percent < 0.5); -} - -test "color delta: specific pixel comparison" { - // Test some specific cases that might be sensitive - const pixel_a: u32 = 0xFF808080; // Gray - const pixel_b: u32 = 0xFF818181; // Slightly different gray - - const original = color_delta.calculatePixelColorDelta(pixel_a, pixel_b); - const fixed_f64 = calculatePixelColorDeltaUnderTest(pixel_a, pixel_b); - - std.debug.print("\nSpecific test - Gray pixels:\n", .{}); - std.debug.print("Original: {d}\n", .{original}); - std.debug.print("SIMD: {d}\n", .{fixed_f64}); - std.debug.print("Difference: {d}\n", .{@abs(original - fixed_f64)}); - - // Should be very close for this simple case - const diff = @abs(original - fixed_f64); - try testing.expect(diff < 0.01); // Less than 1% difference -} - -test "color delta: vectorized vs scalar comparison" { - const test_cases = [_]struct { u32, u32 }{ - .{ 0xFF000000, 0xFF010101 }, - .{ 0xFF000000, 0xFFFFFFFF }, - .{ 0xFFFF0000, 0xFF00FF00 }, - .{ 0x80808080, 0x80818181 }, - .{ 0xFF123456, 0xFF123457 }, - }; - - for (test_cases) |case| { - const scalar_result = color_delta.calculatePixelColorDelta(case[0], case[1]); - const vectorized_result = color_delta.calculatePixelColorDeltaSimd(case[0], case[1]); - - // Convert both to same type for comparison - const vectorized_f64 = @as(f64, @floatFromInt(vectorized_result)) / 4096.0; - const diff = @abs(scalar_result - vectorized_f64); - - // They should be very close (less than 1% difference) - const error_percent = if (scalar_result != 0.0) (diff / scalar_result) * 100.0 else 0.0; - try testing.expect(error_percent < 1.0); - } -} - -test "color delta: edge cases" { - const edge_cases = [_]struct { u32, u32, []const u8 }{ - .{ 0x00000000, 0x00000000, "identical transparent" }, - .{ 0xFF000000, 0xFF000000, "identical opaque black" }, - .{ 0xFFFFFFFF, 0xFFFFFFFF, "identical white" }, - .{ 0x01010101, 0x02020202, "very small difference" }, - .{ 0xFEFEFEFE, 0xFDFDFDFD, "very small difference near white" }, - }; - - for (edge_cases) |case| { - const original = color_delta.calculatePixelColorDelta(case[0], case[1]); - const fixed_f64 = calculatePixelColorDeltaUnderTest(case[0], case[1]); - - std.debug.print("{s}: orig={d:.6}, fixed={d:.6}, diff={d:.6}\n", .{ - case[2], - original, - fixed_f64, - @abs(original - fixed_f64), - }); - - // For identical pixels, both should return 0 - if (case[0] == case[1]) { - try testing.expectEqual(@as(f64, 0.0), fixed_f64); - try testing.expectEqual(@as(f64, 0.0), original); - } - } -} - diff --git a/odiff/src/test_core.zig b/odiff/src/test_core.zig deleted file mode 100644 index 610699e..0000000 --- a/odiff/src/test_core.zig +++ /dev/null @@ -1,210 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const expect = testing.expect; -const expectEqual = testing.expectEqual; -const expectApproxEqRel = testing.expectApproxEqRel; - -const odiff = @import("root.zig"); -const image_io = odiff.image_io; -const diff = odiff.diff; -const color_delta = odiff.color_delta; - -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !image_io.Image { - return image_io.loadImage(path, allocator) catch |err| { - std.debug.print("Failed to load image: {s}\nError: {}\n", .{ path, err }); - return err; - }; -} - -test "antialiasing: does not count anti-aliased pixels as different" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/aa/antialiasing-on.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/aa/antialiasing-off.png", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{ - .antialiasing = true, - .output_diff_mask = false, - }; - - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 38), diff_count); - try expectApproxEqRel(@as(f64, 0.095), diff_percentage, 0.001); -} - -test "antialiasing: tests different sized AA images" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/aa/antialiasing-on.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/aa/antialiasing-off-small.png", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{ - .antialiasing = true, - .output_diff_mask = true, - }; - - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 417), diff_count); // diffPixels - try expectApproxEqRel(@as(f64, 1.0425), diff_percentage, 0.01); // diffPercentage -} - -test "threshold: uses provided threshold" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/png/orange.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{ - .threshold = 0.5, - }; - - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 25), diff_count); // diffPixels - try expectApproxEqRel(@as(f64, 0.020948550360315066), diff_percentage, 0.001); // diffPercentage - Zig implementation value -} - -test "ignore regions: uses provided ignore regions" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/png/orange.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2.deinit(); - - const ignore_regions = [_]diff.IgnoreRegion{ - .{ .x1 = 150, .y1 = 30, .x2 = 310, .y2 = 105 }, - .{ .x1 = 20, .y1 = 175, .x2 = 105, .y2 = 200 }, - }; - - const options = diff.DiffOptions{ - .ignore_regions = &ignore_regions, - }; - - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 0), diff_count); // diffPixels - try expectApproxEqRel(@as(f64, 0.0), diff_percentage, 0.001); // diffPercentage -} - -test "diff color: creates diff output image with custom green diff color" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/png/orange.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2.deinit(); - - const green_pixel: u32 = 4278255360; // #00ff00 in int32 representation - - const options = diff.DiffOptions{ - .diff_pixel = green_pixel, - }; - - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - _ = diff_count; - _ = diff_percentage; - - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expect(diff_output != null); // diffOutput should exist - - if (diff_output) |*diff_output_img| { - var original_diff = try loadTestImage("test/png/orange_diff_green.png", allocator); - defer original_diff.deinit(); - - const compare_options = diff.DiffOptions{}; - var nested_diff_output, const nested_diff_count, const nested_diff_percentage, var nested_diff_lines = try diff.compare(&original_diff, diff_output_img, compare_options, allocator); - defer if (nested_diff_output) |*img| img.deinit(); - defer if (nested_diff_lines) |*lines| lines.deinit(); - - try expect(nested_diff_output != null); // diffMaskOfDiff should exist - - // If there are differences, save debug images - if (nested_diff_count > 0) { - try image_io.saveImage(diff_output_img, "test/png/diff-output-green.png", allocator); - if (nested_diff_output) |*diff_mask| { - try image_io.saveImage(diff_mask, "test/png/diff-of-diff-green.png", allocator); - } - } - - try expectEqual(@as(u32, 0), nested_diff_count); // diffOfDiffPixels - try expectApproxEqRel(@as(f64, 0.0), nested_diff_percentage, 0.001); // diffOfDiffPercentage - } -} - -test "blendSemiTransparentColor: blend semi-transparent colors" { - const testBlend = struct { - fn call(r: f64, g: f64, b: f64, a: f64, expected_r: f64, expected_g: f64, expected_b: f64, expected_a: f64) !void { - const pixel = color_delta.Pixel{ .r = r, .g = g, .b = b, .a = a }; - const blended = color_delta.blendSemiTransparentPixel(pixel); - - try expectApproxEqRel(expected_r, blended.r, 0.01); - try expectApproxEqRel(expected_g, blended.g, 0.01); - try expectApproxEqRel(expected_b, blended.b, 0.01); - try expectApproxEqRel(expected_a, blended.a, 0.01); - } - }.call; - - try testBlend(0.0, 128.0, 255.0, 255.0, 0.0, 128.0, 255.0, 1.0); - try testBlend(0.0, 128.0, 255.0, 0.0, 255.0, 255.0, 255.0, 0.0); - try testBlend(0.0, 128.0, 255.0, 5.0, 250.0, 252.51, 255.0, 0.0196078431372549); // Mathematically correct value - try testBlend(0.0, 128.0, 255.0, 51.0, 204.0, 229.6, 255.0, 0.2); - try testBlend(0.0, 128.0, 255.0, 128.0, 127.0, 191.25, 255.0, 0.5); -} - -test "layoutDifference: diff images with different layouts" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/png/white4x4.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/png/purple8x8.png", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{ - .antialiasing = false, - .output_diff_mask = false, - }; - - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 16), diff_count); // diffPixels - try expectApproxEqRel(@as(f64, 100.0), diff_percentage, 0.001); // diffPercentage -} diff --git a/odiff/src/test_io_bmp.zig b/odiff/src/test_io_bmp.zig deleted file mode 100644 index 19b02da..0000000 --- a/odiff/src/test_io_bmp.zig +++ /dev/null @@ -1,60 +0,0 @@ -// BMP I/O tests - converted from Test_IO_BMP.ml -const std = @import("std"); -const testing = std.testing; -const odiff = @import("root.zig"); -const image_io = odiff.image_io; -const diff = odiff.diff; - -const testing_allocator = testing.allocator; - -fn loadImage(path: []const u8) !image_io.Image { - return image_io.loadImage(path, testing_allocator) catch |err| { - std.debug.print("Failed to load image: {s}\nError: {}\n", .{ path, err }); - return err; - }; -} - -test "BMP: finds difference between 2 images" { - var img1 = try loadImage("test/bmp/clouds.bmp"); - defer img1.deinit(); - var img2 = try loadImage("test/bmp/clouds-2.bmp"); - defer img2.deinit(); - - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, .{}, testing_allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try testing.expectEqual(@as(u32, 192), diff_count); - try testing.expectApproxEqRel(@as(f64, 0.077), diff_percentage, 0.01); -} - -test "BMP: diff of mask and no mask are equal" { - var img1 = try loadImage("test/bmp/clouds.bmp"); - defer img1.deinit(); - var img2 = try loadImage("test/bmp/clouds-2.bmp"); - defer img2.deinit(); - - // Compare without diff mask - var no_mask_diff_output, const no_mask_diff_count, const no_mask_diff_percentage, var no_mask_diff_lines = try diff.compare(&img1, &img2, .{ .output_diff_mask = false }, testing_allocator); - defer if (no_mask_diff_output) |*img| img.deinit(); - defer if (no_mask_diff_lines) |*lines| lines.deinit(); - - // Compare with diff mask - var img1_mask = try loadImage("test/bmp/clouds.bmp"); - defer img1_mask.deinit(); - var img2_mask = try loadImage("test/bmp/clouds-2.bmp"); - defer img2_mask.deinit(); - - var with_mask_diff_output, const with_mask_diff_count, const with_mask_diff_percentage, var with_mask_diff_lines = try diff.compare(&img1_mask, &img2_mask, .{ .output_diff_mask = true }, testing_allocator); - defer if (with_mask_diff_output) |*img| img.deinit(); - defer if (with_mask_diff_lines) |*lines| lines.deinit(); - - try testing.expectEqual(no_mask_diff_count, with_mask_diff_count); - try testing.expectApproxEqRel(no_mask_diff_percentage, with_mask_diff_percentage, 0.001); -} - -// Skip this test for now - there may be differences in pixel format between BMP and PNG -// The basic BMP reading functionality works correctly -test "BMP: creates correct diff output image" { - return error.SkipZigTest; -} diff --git a/odiff/src/test_io_jpg.zig b/odiff/src/test_io_jpg.zig deleted file mode 100644 index 5246c71..0000000 --- a/odiff/src/test_io_jpg.zig +++ /dev/null @@ -1,124 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const expect = testing.expect; -const expectEqual = testing.expectEqual; -const expectApproxEqRel = testing.expectApproxEqRel; - -const odiff = @import("root.zig"); -const image_io = odiff.image_io; -const diff = odiff.diff; - -// Helper function to load test images -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !image_io.Image { - return image_io.loadImage(path, allocator) catch |err| { - std.debug.print("Failed to load image: {s}\nError: {}\n", .{ path, err }); - return err; - }; -} - -test "JPG: finds difference between 2 images" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/jpg/tiger.jpg", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/jpg/tiger-2.jpg", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{}; - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 7789), diff_count); // diffPixels - try expectApproxEqRel(@as(f64, 1.1677), diff_percentage, 0.001); // diffPercentage -} - -test "JPG: Diff of mask and no mask are equal" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/jpg/tiger.jpg", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/jpg/tiger-2.jpg", allocator); - defer img2.deinit(); - - // Test without mask - const options_no_mask = diff.DiffOptions{ - .output_diff_mask = false, - }; - var diff_output_no_mask, const diff_count_no_mask, const diff_percentage_no_mask, var diff_lines_no_mask = - try diff.compare(&img1, &img2, options_no_mask, allocator); - defer if (diff_output_no_mask) |*img| img.deinit(); - defer if (diff_lines_no_mask) |*lines| lines.deinit(); - - // Test with mask - var img1_copy = try loadTestImage("test/jpg/tiger.jpg", allocator); - defer img1_copy.deinit(); - - var img2_copy = try loadTestImage("test/jpg/tiger-2.jpg", allocator); - defer img2_copy.deinit(); - - const options_with_mask = diff.DiffOptions{ - .output_diff_mask = true, - }; - var diff_output_with_mask, const diff_count_with_mask, const diff_percentage_with_mask, var diff_lines_with_mask = - try diff.compare(&img1_copy, &img2_copy, options_with_mask, allocator); - defer if (diff_output_with_mask) |*img| img.deinit(); - defer if (diff_lines_with_mask) |*lines| lines.deinit(); - - try expectEqual(diff_count_no_mask, diff_count_with_mask); // diffPixels should be equal - try expectApproxEqRel(diff_percentage_no_mask, diff_percentage_with_mask, 0.001); // diffPercentage should be equal -} - -test "JPG: Creates correct diff output image" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/jpg/tiger.jpg", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/jpg/tiger-2.jpg", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{}; - var diff_output, const diff_count, const diff_percentage, var diff_lines = - try diff.compare(&img1, &img2, options, allocator); - - _ = diff_count; - _ = diff_percentage; - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expect(diff_output != null); // diffOutput should exist - - if (diff_output) |*diff_output_img| { - var original_diff = try loadTestImage("test/jpg/tiger-diff.png", allocator); - defer original_diff.deinit(); - - const compare_options = diff.DiffOptions{}; - var diff_result_output, const diff_result_count, const diff_result_percentage, var diff_result_lines = - try diff.compare(&original_diff, diff_output_img, compare_options, allocator); - defer if (diff_result_output) |*img| img.deinit(); - defer if (diff_result_lines) |*lines| lines.deinit(); - - try expect(diff_result_output != null); // diffMaskOfDiff should exist - - // If there are differences, save debug images - if (diff_result_count > 0) { - // Note: We can only save as PNG currently, but that's fine for debug output - try image_io.saveImage(diff_output_img, "test/jpg/_diff-output.png", allocator); - if (diff_result_output) |*diff_mask| { - try image_io.saveImage(diff_mask, "test/jpg/_diff-of-diff.png", allocator); - } - } - - try expectEqual(@as(u32, 0), diff_result_count); // diffOfDiffPixels - try expectApproxEqRel(@as(f64, 0.0), diff_result_percentage, 0.001); // diffOfDiffPercentage - } -} diff --git a/odiff/src/test_io_png.zig b/odiff/src/test_io_png.zig deleted file mode 100644 index 810f85a..0000000 --- a/odiff/src/test_io_png.zig +++ /dev/null @@ -1,149 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const expect = testing.expect; -const expectEqual = testing.expectEqual; -const expectApproxEqRel = testing.expectApproxEqRel; - -const odiff = @import("root.zig"); -const image_io = odiff.image_io; -const diff = odiff.diff; - -// Helper function to load test images -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !image_io.Image { - return image_io.loadImage(path, allocator) catch |err| { - std.debug.print("Failed to load image: {s}\nError: {}\n", .{ path, err }); - return err; - }; -} - -test "PNG: finds difference between 2 images" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/png/orange.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{}; - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 1366), diff_count); // diffPixels - try expectApproxEqRel(@as(f64, 1.14), diff_percentage, 0.1); // diffPercentage -} - -test "PNG: Diff of mask and no mask are equal" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/png/orange.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2.deinit(); - - // Test without mask - const options_no_mask = diff.DiffOptions{ - .output_diff_mask = false, - }; - var diff_output_no_mask, const diff_count_no_mask, const diff_percentage_no_mask, var diff_lines_no_mask = try diff.compare(&img1, &img2, options_no_mask, allocator); - defer if (diff_output_no_mask) |*img| img.deinit(); - defer if (diff_lines_no_mask) |*lines| lines.deinit(); - - // Test with mask - var img1_copy = try loadTestImage("test/png/orange.png", allocator); - defer img1_copy.deinit(); - - var img2_copy = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2_copy.deinit(); - - const options_with_mask = diff.DiffOptions{ - .output_diff_mask = true, - }; - var diff_output_with_mask, const diff_count_with_mask, const diff_percentage_with_mask, var diff_lines_with_mask = try diff.compare(&img1_copy, &img2_copy, options_with_mask, allocator); - defer if (diff_output_with_mask) |*img| img.deinit(); - defer if (diff_lines_with_mask) |*lines| lines.deinit(); - - try expectEqual(diff_count_no_mask, diff_count_with_mask); // diffPixels should be equal - try expectApproxEqRel(diff_percentage_no_mask, diff_percentage_with_mask, 0.001); // diffPercentage should be equal -} - -test "PNG: Creates correct diff output image" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/png/orange.png", allocator); - defer img1.deinit(); - var img2 = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{}; - const diff_output, const diff_count, const diff_percentage, const diff_lines = try diff.compare(&img1, &img2, options, allocator); - _ = diff_count; - _ = diff_percentage; - defer if (diff_output) |img| { - var mut_img = img; - mut_img.deinit(); - }; - defer if (diff_lines) |lines| { - var mut_lines = lines; - mut_lines.deinit(); - }; - - try expect(diff_output != null); // diffOutput should exist - - if (diff_output) |*diff_output_img| { - var original_diff = try loadTestImage("test/png/orange_diff.png", allocator); - defer original_diff.deinit(); - - const compare_options = diff.DiffOptions{}; - var nested_diff_output, const nested_diff_count, const nested_diff_percentage, var nested_diff_lines = try diff.compare(&original_diff, diff_output_img, compare_options, allocator); - defer if (nested_diff_output) |*img| img.deinit(); - defer if (nested_diff_lines) |*lines| lines.deinit(); - - try expect(nested_diff_output != null); // diffMaskOfDiff should exist - - // If there are differences, save debug images - if (nested_diff_count > 0) { - try image_io.saveImage(diff_output_img, "test/png/diff-output.png", allocator); - if (nested_diff_output) |*diff_mask| { - try image_io.saveImage(diff_mask, "test/png/diff-of-diff.png", allocator); - } - } - - try expectEqual(@as(u32, 0), nested_diff_count); // diffOfDiffPixels - try expectApproxEqRel(@as(f64, 0.0), nested_diff_percentage, 0.001); // diffOfDiffPercentage - } -} - -test "PNG: Correctly handles different encodings of transparency" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/png/extreme-alpha.png", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/png/extreme-alpha-1.png", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{}; - const diff_output, const diff_count, const diff_percentage, const diff_lines = try diff.compare(&img1, &img2, options, allocator); - _ = diff_percentage; - defer if (diff_output) |img| { - var mut_img = img; - mut_img.deinit(); - }; - defer if (diff_lines) |lines| { - var mut_lines = lines; - mut_lines.deinit(); - }; - - try expectEqual(@as(u32, 0), diff_count); // diffPixels should be 0 -} diff --git a/odiff/src/test_io_tiff.zig b/odiff/src/test_io_tiff.zig deleted file mode 100644 index 9f1ffaa..0000000 --- a/odiff/src/test_io_tiff.zig +++ /dev/null @@ -1,142 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const expect = testing.expect; -const expectEqual = testing.expectEqual; -const expectApproxEqRel = testing.expectApproxEqRel; - -const odiff = @import("root.zig"); -const image_io = odiff.image_io; -const diff = odiff.diff; - -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !image_io.Image { - return image_io.loadImage(path, allocator) catch |err| { - std.debug.print("Failed to load image: {s}\nError: {}\n", .{ path, err }); - return err; - }; -} - -const builtin = @import("builtin"); -const skip_tiff_on_windows = builtin.os.tag == .windows; - -test "TIFF: finds difference between 2 images" { - if (skip_tiff_on_windows) { - std.debug.print("Skipping TIFF tests on Windows systems\n", .{}); - return; - } - - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/tiff/laptops.tiff", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/tiff/laptops-2.tiff", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{}; - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 8569), diff_count); // diffPixels - try expectApproxEqRel(@as(f64, 3.79), diff_percentage, 0.01); // diffPercentage -} - -test "TIFF: Diff of mask and no mask are equal" { - if (skip_tiff_on_windows) { - std.debug.print("Skipping TIFF tests on Windows systems\n", .{}); - return; - } - - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/tiff/laptops.tiff", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/tiff/laptops-2.tiff", allocator); - defer img2.deinit(); - - // Test without mask - const options_no_mask = diff.DiffOptions{ - .output_diff_mask = false, - }; - var no_mask_diff_output, const no_mask_diff_count, const no_mask_diff_percentage, var no_mask_diff_lines = try diff.compare(&img1, &img2, options_no_mask, allocator); - defer if (no_mask_diff_output) |*img| img.deinit(); - defer if (no_mask_diff_lines) |*lines| lines.deinit(); - - // Test with mask - var img1_copy = try loadTestImage("test/tiff/laptops.tiff", allocator); - defer img1_copy.deinit(); - - var img2_copy = try loadTestImage("test/tiff/laptops-2.tiff", allocator); - defer img2_copy.deinit(); - - const options_with_mask = diff.DiffOptions{ - .output_diff_mask = true, - }; - var mask_diff_output, const mask_diff_count, const mask_diff_percentage, var mask_diff_lines = try diff.compare(&img1_copy, &img2_copy, options_with_mask, allocator); - defer if (mask_diff_output) |*img| img.deinit(); - defer if (mask_diff_lines) |*lines| lines.deinit(); - - try expectEqual(no_mask_diff_count, mask_diff_count); // diffPixels should be equal - try expectApproxEqRel(no_mask_diff_percentage, mask_diff_percentage, 0.001); // diffPercentage should be equal -} - -test "TIFF: Creates correct diff output image" { - if (skip_tiff_on_windows) { - std.debug.print("Skipping TIFF tests on Windows systems\n", .{}); - return; - } - - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("test/tiff/laptops.tiff", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("test/tiff/laptops-2.tiff", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{}; - const diff_output, const diff_count, const diff_percentage, const diff_lines = try diff.compare(&img1, &img2, options, allocator); - _ = diff_count; - _ = diff_percentage; - defer if (diff_output) |img| { - var mut_img = img; - mut_img.deinit(); - }; - defer if (diff_lines) |lines| { - var mut_lines = lines; - mut_lines.deinit(); - }; - - try expect(diff_output != null); // diffOutput should exist - - if (diff_output) |*diff_output_img| { - var original_diff = try loadTestImage("test/tiff/laptops-diff.png", allocator); - defer original_diff.deinit(); - - const compare_options = diff.DiffOptions{}; - var nested_diff_output, const nested_diff_count, const nested_diff_percentage, var nested_diff_lines = try diff.compare(&original_diff, diff_output_img, compare_options, allocator); - defer if (nested_diff_output) |*img| img.deinit(); - defer if (nested_diff_lines) |*lines| lines.deinit(); - - try expect(nested_diff_output != null); // diffMaskOfDiff should exist - - // If there are differences, save debug images - if (nested_diff_count > 0) { - // Note: We can only save as PNG currently, but that's fine for debug output - try image_io.saveImage(diff_output_img, "test/tiff/_diff-output.png", allocator); - if (nested_diff_output) |*diff_mask| { - try image_io.saveImage(diff_mask, "test/tiff/_diff-of-diff.png", allocator); - } - } - - try expectEqual(@as(u32, 0), nested_diff_count); // diffOfDiffPixels - try expectApproxEqRel(@as(f64, 0.0), nested_diff_percentage, 0.001); // diffOfDiffPercentage - } -} diff --git a/odiff/src/test_io_webp.zig b/odiff/src/test_io_webp.zig deleted file mode 100644 index 673a2e5..0000000 --- a/odiff/src/test_io_webp.zig +++ /dev/null @@ -1,69 +0,0 @@ -const std = @import("std"); -const testing = std.testing; -const expect = testing.expect; -const expectEqual = testing.expectEqual; -const expectApproxEqRel = testing.expectApproxEqRel; - -const odiff = @import("root.zig"); -const image_io = odiff.image_io; -const diff = odiff.diff; - -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !image_io.Image { - return image_io.loadImage(path, allocator) catch |err| { - std.debug.print("Failed to load image: {s}\nError: {}\n", .{ path, err }); - return err; - }; -} - -test "webp: loads webp image correctly" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img = try loadTestImage("images/donkey.webp", allocator); - defer img.deinit(); - - try expectEqual(img.width, 1258); - try expectEqual(img.height, 3054); -} - -test "webp: compares webp with png correctly" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var webp_img = try loadTestImage("images/donkey.webp", allocator); - defer webp_img.deinit(); - - var png_img = try loadTestImage("images/donkey.png", allocator); - defer png_img.deinit(); - - const options = diff.DiffOptions{}; - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&webp_img, &png_img, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - // These are actually different images, so diff_count should be > 0 - try expect(diff_count > 0); - try expect(diff_percentage > 0.0); -} - -test "webp: identical WebP images have no differences" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var img1 = try loadTestImage("images/donkey.webp", allocator); - defer img1.deinit(); - - var img2 = try loadTestImage("images/donkey.webp", allocator); - defer img2.deinit(); - - const options = diff.DiffOptions{}; - var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator); - defer if (diff_output) |*img| img.deinit(); - defer if (diff_lines) |*lines| lines.deinit(); - - try expectEqual(@as(u32, 0), diff_count); // diffPixels should be 0 - try expectApproxEqRel(@as(f64, 0.0), diff_percentage, 0.001); // diffPercentage should be 0 -} diff --git a/odiff/src/vxdiff.asm b/odiff/src/vxdiff.asm deleted file mode 100644 index 06b68fe..0000000 --- a/odiff/src/vxdiff.asm +++ /dev/null @@ -1,276 +0,0 @@ -; vim: set ft=nasm: -DEFAULT REL - -section .data - zero: dq 0 - -align 64 - rgb2y: times 4 dd 0.29889531, 0.58662247, 0.11448223, 0.0 - rgb2i: times 4 dd 0.59597799, -0.27417610, -0.32180189, 0.0 - rgb2q: times 4 dd 0.21147017, -0.52261711, 0.31114694, 0.0 - - delta_coef: times 4 dd 0.5053, 0.299, 0.1957, 0.0 - - max_delta: dd 352.15 ; 35215.0 * 0.1^2 - - pixel1: dq 0b1111 - pixel2: dq 0b11111111 - pixel3: dq 0b111111111111 - -section .text -global vxdiff - -vxdiff: - ; RDI base image pixels encoded in RGBA8 format - ; RSI second image pixels encoded in RGBA8 format - ; RDX base image width in pixels - ; RCX second image width in pixels - ; R8 base image height in pixels - ; R9 second image height in pixels - push rbp - mov rbp, rsp - push rbx - push r12 - push r13 - push r14 - push r15 - sub rsp, 24 ; locals + stack realignment to 16-byte boundary - - ; local vars: - %define OVERFLOWED_Y QWORD[rsp+0] ; number of columns in the base image that overflow the second image - - mov rax, 0b0001000100010001000100010001000100010001000100010001000100010001 - kmovq k1, rax ; Rs - kshiftlq k2, k1, 1 ; Gs - kshiftlq k3, k2, 1 ; Bs - kshiftlq k4, k3, 1 ; As - knotq k5, k4 ; RGBs - - mov al, 1 - vpbroadcastb zmm3, al - vpxorq zmm0, zmm0, zmm0 - vpsubb zmm31, zmm0, zmm3 ; 255 - vpmovzxbd zmm30, xmm31 - vcvtudq2ps zmm30, zmm30 ; 255.0f - vpmovzxbd zmm3, xmm3 - vcvtudq2ps zmm3, zmm3 - vdivps zmm3, zmm3, zmm30 ; 1/255.0f - - vmovups zmm7, [rgb2y] - vmovups zmm8, [rgb2i] - vmovups zmm9, [rgb2q] - - vbroadcastss xmm28, [max_delta] - - vmovups zmm29, [delta_coef] - - mov r15, rdx ; base image width - - mov rax, rcx - cmp rdx, rcx - cmovl rcx, rdx - - mov r13, rdx - sub r13, rcx ; base image pointer increment after x loop end - shl r13, 2 - mov r14, rax - sub r14, rcx ; second image pointer increment after x loop end - shl r14, 2 - - mov r12, rcx ; number of x loop iterations - - mov rax, r8 - sub rax, r9 - cmovs rax, [zero] - mov OVERFLOWED_Y, rax - mov rax, r8 - cmp r9, r8 - cmovl rax, r9 - mov r15, rax - - xor rbx, rbx ; number of differences found - mov rax, rdx - mul OVERFLOWED_Y - add rbx, rax ; include overflowed rows - - mov rdx, r15 ; number of y iterations - - jmp .y_loop - -.x_leftovers: - ; handles pixels in a row that is not divisible by 4 - mov r15, rcx - shl r15, 2 - test rcx, rcx - jz .next_row - dec rcx - cmovz rax, [pixel1] - dec rcx - cmovz rax, [pixel2] - dec rcx - cmovz rax, [pixel3] - kmov k6, rax - vxorps xmm1, xmm1, xmm1 - vxorps xmm2, xmm2, xmm2 - vmovdqu8 xmm1 {k6}, [rdi] - vmovdqu8 xmm2 {k6}, [rsi] - add rsi, r15 - add rdi, r15 - xor rcx, rcx - jmp .x_loop_body - -.next_row: - add rdi, r13 - add rsi, r14 - mov r15, r13 - shr r15, 2 - add rbx, r15 ; include overflowed pixels in current row - -.y_loop: - ; loops over columns - cmp rdx, 0 - jle .done - - dec rdx - mov rcx, r12 - -.x_loop: - ; loops over pixels in a row - cmp rcx, 4 - jl .x_leftovers - - vmovdqu8 xmm1, [rdi] - vmovdqu8 xmm2, [rsi] - - add rdi, 16 - add rsi, 16 - sub rcx, 4 - -.x_loop_body: - ; replace pixels having alpha=0 with white - kxor k6, k6, k6 - kxor k7, k7, k7 - vpcmpequb k6 {k4}, xmm1, xmm0 - kshiftlb k6, k6, 1 - kor k7, k7, k6 - kshiftlb k6, k6, 1 - kor k7, k7, k6 - kshiftlb k6, k6, 1 - kor k7, k7, k6 - vmovdqu8 xmm1 {k7}, xmm31 - ; - kxor k6, k6, k6 - kxor k7, k7, k7 - vpcmpequb k6 {k4}, xmm2, xmm0 - kshiftlb k6, k6, 1 - kor k7, k7, k6 - kshiftlb k6, k6, 1 - kor k7, k7, k6 - kshiftlb k6, k6, 1 - kor k7, k7, k6 - vmovdqu8 xmm2 {k7}, xmm31 - - ; convert bytes to floats - vpmovzxbd zmm1, xmm1 - vcvtudq2ps zmm1, zmm1 - vpmovzxbd zmm2, xmm2 - vcvtudq2ps zmm2, zmm2 - - ; normalise alpha - vmulps zmm1 {k4}, zmm1, zmm3 - vmulps zmm2 {k4}, zmm2, zmm3 - - ; blend rgb with white pixel using alpha - vsubps zmm1 {k5}, zmm1, zmm30 - vshufps zmm10, zmm1, zmm1, 0xff - vmulps zmm1 {k5}, zmm1, zmm10 - vaddps zmm1 {k5}, zmm1, zmm30 - ; - vsubps zmm2 {k5}, zmm2, zmm30 - vshufps zmm20, zmm2, zmm2, 0xff - vmulps zmm2 {k5}, zmm2, zmm20 - vaddps zmm2 {k5}, zmm2, zmm30 - - ; rgb to yiq - vmulps zmm10, zmm1, zmm7 ; y - vmulps zmm11, zmm1, zmm8 ; i - vmulps zmm12, zmm1, zmm9 ; q - vmulps zmm20, zmm2, zmm7 ; y - vmulps zmm21, zmm2, zmm8 ; i - vmulps zmm22, zmm2, zmm9 ; q - - ; yiq(R) - vxorps zmm13, zmm13, zmm13 - vshufps zmm13 {k1}, zmm10, zmm10, 0b00000000 - vshufps zmm13 {k2}, zmm11, zmm11, 0b00000000 - vshufps zmm13 {k3}, zmm12, zmm12, 0b00000000 - ; yiq(G) - vxorps zmm14, zmm14, zmm14 - vshufps zmm14 {k1}, zmm10, zmm10, 0b00000001 - vshufps zmm14 {k2}, zmm11, zmm11, 0b00000100 - vshufps zmm14 {k3}, zmm12, zmm12, 0b00010000 - ; yiq(B) - vxorps zmm15, zmm15, zmm15 - vshufps zmm15 {k1}, zmm10, zmm10, 0b00000010 - vshufps zmm15 {k2}, zmm11, zmm11, 0b00001000 - vshufps zmm15 {k3}, zmm12, zmm12, 0b00100000 - - ; yiq(R) - vxorps zmm23, zmm23, zmm23 - vshufps zmm23 {k1}, zmm20, zmm20, 0b00000000 - vshufps zmm23 {k2}, zmm21, zmm21, 0b00000000 - vshufps zmm23 {k3}, zmm22, zmm22, 0b00000000 - ; yiq(G) - vxorps zmm24, zmm24, zmm24 - vshufps zmm24 {k1}, zmm20, zmm20, 0b00000001 - vshufps zmm24 {k2}, zmm21, zmm21, 0b00000100 - vshufps zmm24 {k3}, zmm22, zmm22, 0b00010000 - ; yiq(B) - vxorps zmm25, zmm25, zmm25 - vshufps zmm25 {k1}, zmm20, zmm20, 0b00000010 - vshufps zmm25 {k2}, zmm21, zmm21, 0b00001000 - vshufps zmm25 {k3}, zmm22, zmm22, 0b00100000 - - ; yiq - vaddps zmm16, zmm13, zmm14 - vaddps zmm16, zmm16, zmm15 - vaddps zmm26, zmm23, zmm24 - vaddps zmm26, zmm26, zmm25 - - ; YIQ diff - vsubps zmm16, zmm16, zmm26 - - ; YIQ*YIQ - vmulps zmm16, zmm16, zmm16 - ; YIQ*YIQ * delta coef - vmulps zmm16, zmm16, zmm29 - - vxorps zmm17, zmm17, zmm17 - vxorps zmm18, zmm18, zmm18 - vxorps zmm19, zmm19, zmm19 - vshufps zmm17 {k1}, zmm16, zmm16, 0b10101010 - vshufps zmm18 {k1}, zmm16, zmm16, 0b01010101 - vshufps zmm19 {k1}, zmm16, zmm16, 0b00000000 - - ; delta - vaddps zmm16, zmm19, zmm18 - vaddps zmm16, zmm16, zmm17 - - vcompressps zmm16 {k1}, zmm16 - vcmpgtps k6, xmm16, xmm28 - kmov eax, k6 - popcnt eax, eax - - add rbx, rax - jmp .x_loop - -.done: - mov eax, ebx - add rsp, 24 - pop r15 - pop r14 - pop r13 - pop r12 - pop rbx - pop rbp - ret diff --git a/odiff/test/aa/antialiasing-off-small.png b/odiff/test/aa/antialiasing-off-small.png deleted file mode 100644 index fc1c9a4..0000000 Binary files a/odiff/test/aa/antialiasing-off-small.png and /dev/null differ diff --git a/odiff/test/aa/antialiasing-off.png b/odiff/test/aa/antialiasing-off.png deleted file mode 100644 index 91fbdd9..0000000 Binary files a/odiff/test/aa/antialiasing-off.png and /dev/null differ diff --git a/odiff/test/aa/antialiasing-on.png b/odiff/test/aa/antialiasing-on.png deleted file mode 100644 index cdbb7a4..0000000 Binary files a/odiff/test/aa/antialiasing-on.png and /dev/null differ diff --git a/odiff/test/bmp/_diff-of-diff.png b/odiff/test/bmp/_diff-of-diff.png deleted file mode 100644 index 01516c8..0000000 Binary files a/odiff/test/bmp/_diff-of-diff.png and /dev/null differ diff --git a/odiff/test/bmp/_diff-output.png b/odiff/test/bmp/_diff-output.png deleted file mode 100644 index acf7b4b..0000000 Binary files a/odiff/test/bmp/_diff-output.png and /dev/null differ diff --git a/odiff/test/bmp/clouds-2.bmp b/odiff/test/bmp/clouds-2.bmp deleted file mode 100644 index 421b9a1..0000000 Binary files a/odiff/test/bmp/clouds-2.bmp and /dev/null differ diff --git a/odiff/test/bmp/clouds-diff.png b/odiff/test/bmp/clouds-diff.png deleted file mode 100644 index c3d5560..0000000 Binary files a/odiff/test/bmp/clouds-diff.png and /dev/null differ diff --git a/odiff/test/bmp/clouds.bmp b/odiff/test/bmp/clouds.bmp deleted file mode 100644 index b31b58e..0000000 Binary files a/odiff/test/bmp/clouds.bmp and /dev/null differ diff --git a/odiff/test/jpg/tiger-2.jpg b/odiff/test/jpg/tiger-2.jpg deleted file mode 100644 index 2d6b982..0000000 Binary files a/odiff/test/jpg/tiger-2.jpg and /dev/null differ diff --git a/odiff/test/jpg/tiger-diff.png b/odiff/test/jpg/tiger-diff.png deleted file mode 100644 index 47c026e..0000000 Binary files a/odiff/test/jpg/tiger-diff.png and /dev/null differ diff --git a/odiff/test/jpg/tiger.jpg b/odiff/test/jpg/tiger.jpg deleted file mode 100644 index 11d4c5c..0000000 Binary files a/odiff/test/jpg/tiger.jpg and /dev/null differ diff --git a/odiff/test/node-binding.test.cjs b/odiff/test/node-binding.test.cjs deleted file mode 100644 index 02035e1..0000000 --- a/odiff/test/node-binding.test.cjs +++ /dev/null @@ -1,227 +0,0 @@ -const path = require("path"); -const test = require("ava"); -const { compare } = require("../npm_package/odiff"); - -const IMAGES_PATH = path.resolve(__dirname, "..", "images"); -const BINARY_PATH = path.resolve(__dirname, "..", "zig-out", "bin", "odiff"); - -console.log(`Testing binary ${BINARY_PATH}`); - -const options = { - __binaryPath: BINARY_PATH, -}; - -test("Outputs correct parsed result when images different", async (t) => { - const result = await compare( - path.join(IMAGES_PATH, "donkey.png"), - path.join(IMAGES_PATH, "donkey-2.png"), - path.join(IMAGES_PATH, "diff.png"), - options, - ); - - t.is(result.reason, "pixel-diff"); - t.true(typeof result.diffCount === "number"); - t.true(result.diffCount > 0); - console.log(`Found ${result.diffCount} different pixels`); -}); - -test("Correctly works with reduceRamUsage", async (t) => { - const result = await compare( - path.join(IMAGES_PATH, "donkey.png"), - path.join(IMAGES_PATH, "donkey-2.png"), - path.join(IMAGES_PATH, "diff.png"), - { - ...options, - reduceRamUsage: true, - }, - ); - - t.is(result.reason, "pixel-diff"); - t.true(typeof result.diffCount === "number"); - t.true(result.diffCount > 0); -}); - -test("Correctly parses threshold", async (t) => { - const result = await compare( - path.join(IMAGES_PATH, "donkey.png"), - path.join(IMAGES_PATH, "donkey-2.png"), - path.join(IMAGES_PATH, "diff.png"), - { - ...options, - threshold: 0.5, - }, - ); - - t.is(result.reason, "pixel-diff"); - t.true(typeof result.diffCount === "number"); - t.true(result.diffCount > 0); -}); - -test("Correctly parses antialiasing", async (t) => { - const result = await compare( - path.join(IMAGES_PATH, "donkey.png"), - path.join(IMAGES_PATH, "donkey-2.png"), - path.join(IMAGES_PATH, "diff.png"), - { - ...options, - antialiasing: true, - }, - ); - - t.is(result.reason, "pixel-diff"); - t.true(typeof result.diffCount === "number"); - t.true(result.diffCount > 0); -}); - -test("Correctly parses ignore regions", async (t) => { - const result = await compare( - path.join(IMAGES_PATH, "donkey.png"), - path.join(IMAGES_PATH, "donkey-2.png"), - path.join(IMAGES_PATH, "diff.png"), - { - ...options, - ignoreRegions: [ - { - x1: 749, - y1: 1155, - x2: 1170, - y2: 1603, - }, - { - x1: 657, - y1: 1278, - x2: 742, - y2: 1334, - }, - ], - }, - ); - - // With our placeholder images, this might still show differences - // but the test should at least run without errors - t.true(typeof result.match === "boolean"); -}); - -test("Outputs correct parsed result when images different for cypress image", async (t) => { - const result = await compare( - path.join(IMAGES_PATH, "www.cypress.io.png"), - path.join(IMAGES_PATH, "www.cypress.io-1.png"), - path.join(IMAGES_PATH, "diff.png"), - options, - ); - - // Our placeholder implementation returns synthetic data, so we just check structure - t.true(typeof result.match === "boolean"); -}); - -test("Correctly handles same images", async (t) => { - const result = await compare( - path.join(IMAGES_PATH, "donkey.png"), - path.join(IMAGES_PATH, "donkey.png"), - path.join(IMAGES_PATH, "diff.png"), - options, - ); - - // With placeholder C implementation, identical images should match - t.is(result.match, true); -}); - -test("Correctly outputs diff lines", async (t) => { - const result = await compare( - path.join(IMAGES_PATH, "donkey.png"), - path.join(IMAGES_PATH, "donkey-2.png"), - path.join(IMAGES_PATH, "diff.png"), - { - captureDiffLines: true, - ...options, - }, - ); - - t.is(result.match, false); - // With our current implementation, we may or may not get diff lines - if (result.diffLines) { - t.true(Array.isArray(result.diffLines)); - } -}); - -test("Returns meaningful error if file does not exist and noFailOnFsErrors", async (t) => { - const result = await compare( - path.join(IMAGES_PATH, "not-existing.png"), - path.join(IMAGES_PATH, "not-existing.png"), - path.join(IMAGES_PATH, "diff.png"), - { - ...options, - noFailOnFsErrors: true, - }, - ); - - t.is(result.match, false); - // Our error handling might be different, but it should handle the case gracefully - t.true(["file-not-exists", "pixel-diff"].includes(result.reason)); -}); - -test("Correctly calculates and outputs diff percentage", async (t) => { - const result = await compare( - path.join(__dirname, "png", "orange.png"), - path.join(__dirname, "png", "orange_diff.png"), - path.join(IMAGES_PATH, "diff.png"), - options, - ); - - t.is(result.match, false); - t.is(result.reason, "pixel-diff"); - - t.true(typeof result.diffPercentage === "number"); - t.true(result.diffPercentage > 0); - t.true(typeof result.diffCount === "number"); - t.true(result.diffCount > 0); - - const expectedPercentage = (result.diffCount / (510 * 234)) * 100; - t.true(Math.abs(result.diffPercentage - expectedPercentage) < 0.01); - - console.log( - `Percentage test: ${result.diffCount} pixels (${result.diffPercentage}%)`, - ); -}); - -test("Correctly works with diff-overlay", async (t) => { - const result = await compare( - path.join(__dirname, "png", "orange.png"), - path.join(__dirname, "png", "orange_diff.png"), - path.join(IMAGES_PATH, "diff_white_mask.png"), - { - ...options, - diffOverlay: true, - }, - ); - - t.is(result.match, false); - t.is(result.reason, "pixel-diff"); - t.true(typeof result.diffCount === "number"); - t.true(result.diffCount > 0); - - console.log( - `White shade mask test: ${result.diffCount} different pixels found`, - ); -}); - -test("Works with numeric option to diffOverlay", async (t) => { - const result = await compare( - path.join(__dirname, "png", "orange.png"), - path.join(__dirname, "png", "orange_diff.png"), - path.join(IMAGES_PATH, "diff_white_mask.png"), - { - ...options, - diffOverlay: 0.6, - }, - ); - - t.is(result.match, false); - t.is(result.reason, "pixel-diff"); - t.true(typeof result.diffCount === "number"); - t.true(result.diffCount > 0); - - console.log( - `White shade mask test: ${result.diffCount} different pixels found`, - ); -}); diff --git a/odiff/test/node-bindings.test.ts b/odiff/test/node-bindings.test.ts deleted file mode 100644 index 1812f37..0000000 --- a/odiff/test/node-bindings.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { compare } from "../npm_package/odiff"; - -// allow no options -compare("path1", "path2", "path3") - -// @ts-expect-error options can be only object -compare("path1", "path2", "path3", "") - -// allow partial options -compare("path1", "path2", "path3", { - antialiasing: true, - threshold: 2, -}); - -compare("path1", "path2", "path3", { - antialiasing: true, - threshold: 2, - // @ts-expect-error invalid field - ab: true -}); \ No newline at end of file diff --git a/odiff/test/png/diff-output-green.png b/odiff/test/png/diff-output-green.png deleted file mode 100644 index 744a46d..0000000 Binary files a/odiff/test/png/diff-output-green.png and /dev/null differ diff --git a/odiff/test/png/extreme-alpha-1.png b/odiff/test/png/extreme-alpha-1.png deleted file mode 100644 index 7994e89..0000000 Binary files a/odiff/test/png/extreme-alpha-1.png and /dev/null differ diff --git a/odiff/test/png/extreme-alpha.png b/odiff/test/png/extreme-alpha.png deleted file mode 100644 index ec12bdd..0000000 Binary files a/odiff/test/png/extreme-alpha.png and /dev/null differ diff --git a/odiff/test/png/orange.png b/odiff/test/png/orange.png deleted file mode 100644 index f3b080f..0000000 Binary files a/odiff/test/png/orange.png and /dev/null differ diff --git a/odiff/test/png/orange_changed.png b/odiff/test/png/orange_changed.png deleted file mode 100644 index a6389be..0000000 Binary files a/odiff/test/png/orange_changed.png and /dev/null differ diff --git a/odiff/test/png/orange_diff.png b/odiff/test/png/orange_diff.png deleted file mode 100644 index 636d46e..0000000 Binary files a/odiff/test/png/orange_diff.png and /dev/null differ diff --git a/odiff/test/png/orange_diff_green.png b/odiff/test/png/orange_diff_green.png deleted file mode 100644 index fefba2b..0000000 Binary files a/odiff/test/png/orange_diff_green.png and /dev/null differ diff --git a/odiff/test/png/purple8x8.png b/odiff/test/png/purple8x8.png deleted file mode 100644 index 9e582cb..0000000 Binary files a/odiff/test/png/purple8x8.png and /dev/null differ diff --git a/odiff/test/png/white4x4.png b/odiff/test/png/white4x4.png deleted file mode 100644 index ffe1aa1..0000000 Binary files a/odiff/test/png/white4x4.png and /dev/null differ diff --git a/odiff/test/tiff/laptops-2.tiff b/odiff/test/tiff/laptops-2.tiff deleted file mode 100644 index 3faaacc..0000000 Binary files a/odiff/test/tiff/laptops-2.tiff and /dev/null differ diff --git a/odiff/test/tiff/laptops-diff.png b/odiff/test/tiff/laptops-diff.png deleted file mode 100644 index d505b91..0000000 Binary files a/odiff/test/tiff/laptops-diff.png and /dev/null differ diff --git a/odiff/test/tiff/laptops.tiff b/odiff/test/tiff/laptops.tiff deleted file mode 100644 index a9a24a5..0000000 Binary files a/odiff/test/tiff/laptops.tiff and /dev/null differ diff --git a/odiff/test_bmp_diff_output.png b/odiff/test_bmp_diff_output.png deleted file mode 100644 index c3d5560..0000000 Binary files a/odiff/test_bmp_diff_output.png and /dev/null differ diff --git a/odiff/test_output.png b/odiff/test_output.png deleted file mode 100644 index e69de29..0000000 diff --git a/odiff/typos.toml b/odiff/typos.toml deleted file mode 100644 index c0ecc64..0000000 --- a/odiff/typos.toml +++ /dev/null @@ -1,6 +0,0 @@ -[default.extend-words] -"ba" = "ba" -"odiff" = "odiff" - -[files] -extend-exclude = ["build.zig.zon"]