diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd592e7b..eaf8a24f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,7 @@ jobs: - name: Build and run tests if: matrix.run_tests == true run: | - zig build -Dtarget=${{matrix.target}} test-all --summary all + zig build -Dtarget=${{matrix.target}} test --summary all - name: Build without running tests if: matrix.run_tests != true @@ -195,7 +195,7 @@ jobs: - name: Run AVX tests run: | - zig build test-all --summary all + zig build test --summary all e2e-tests: name: End-to-end JavaScript tests diff --git a/.github/workflows/riscv-qemu.yml b/.github/workflows/riscv-qemu.yml index eaddb50e..e2aa6d4c 100644 --- a/.github/workflows/riscv-qemu.yml +++ b/.github/workflows/riscv-qemu.yml @@ -4,7 +4,7 @@ on: pull_request: # this is a VERY SLOW job try to run it only on the riscv C changes paths: - - "src/rvv.c" + - "src/lib/rvv.c" jobs: riscv: @@ -31,4 +31,4 @@ jobs: export PATH="$PWD/zig-riscv64-linux-0.15.1:$PATH" cd /opt/odiff - zig build test-all -Dtarget=riscv64-linux -Dcpu=generic_rv64+rva23u64 -Doptimize=ReleaseFast + zig build test -Dtarget=riscv64-linux -Dcpu=generic_rv64+rva23u64 -Doptimize=ReleaseFast diff --git a/build.zig b/build.zig index 088d3b3b..e8c2b394 100644 --- a/build.zig +++ b/build.zig @@ -7,21 +7,22 @@ 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 native_target = b.resolveTargetQuery(.{}); - const is_cross_compiling = target.result.cpu.arch != native_target.result.cpu.arch or - target.result.os.tag != native_target.result.os.tag; - - const build_options = b.addOptions(); - build_options.addOption([]const u8, "version", manifest.version); - const build_options_mod = build_options.createModule(); - - const lib_mod, const exe = buildOdiff(b, target, optimize, dynamic, build_options_mod); + const odiff_mod, const exe = makeOdiff(b, .{ + .target = target, + .optimize = optimize, + .dynamic = dynamic, + }); b.installArtifact(exe); - const run_cmd = b.addRunArtifact(exe); + // const lib = b.addLibrary(.{ + // .name = "odiff", + // .linkage = .static, + // .root_module = odiff_mod, + // }); + // b.installArtifact(lib); + const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| { run_cmd.addArgs(args); } @@ -29,143 +30,107 @@ pub fn build(b: *std.Build) !void { const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); - const lib_unit_tests = b.addTest(.{ - .root_module = lib_mod, - }); - - const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); - - const integration_tests_with_io = [_][]const u8{ + const test_files: []const []const u8 = &.{ + "src/test_color_delta.zig", + "src/test_io.zig", "src/test_core.zig", - "src/test_io_png.zig", - "src/test_io_bmp.zig", - "src/test_io_jpg.zig", - "src/test_io_tiff.zig", "src/test_avx.zig", - "src/test_io_webp.zig", - }; - - const integration_tests_pure_zig = [_][]const u8{ - "src/test_color_delta.zig", }; - - var integration_test_steps = std.array_list.Managed(*std.Build.Step.Run).init(b.allocator); - defer integration_test_steps.deinit(); - - if (!is_cross_compiling) { - const root_lib = b.addLibrary(.{ - .name = "odiff_lib", - .root_module = lib_mod, - .linkage = if (dynamic) .dynamic else .static, - }); - for (integration_tests_with_io) |test_path| { - const integration_test = b.addTest(.{ - .root_module = b.createModule(.{ - .root_source_file = b.path(test_path), - .target = target, - .optimize = optimize, - }), - }); - integration_test.root_module.addImport("build_options", build_options_mod); - integration_test.linkLibC(); - integration_test.linkLibrary(root_lib); - linkDeps(b, target, optimize, dynamic, integration_test.root_module); - - const run_integration_test = b.addRunArtifact(integration_test); - integration_test_steps.append(run_integration_test) catch @panic("OOM"); - } - - for (integration_tests_pure_zig) |test_path| { - const pure_test = b.addTest(.{ - .root_module = b.createModule(.{ - .root_source_file = b.path(test_path), - .target = target, - .optimize = optimize, - }), - }); - - pure_test.root_module.addImport("build_options", build_options_mod); - pure_test.addCSourceFiles(.{ - .files = &.{"src/rvv.c"}, - }); - - const run_pure_test = b.addRunArtifact(pure_test); - integration_test_steps.append(run_pure_test) catch @panic("OOM"); - } - } - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_lib_unit_tests.step); - - const integration_test_step = b.step("test-integration", "Run integration tests with test images"); - for (integration_test_steps.items) |test_run_step| { - integration_test_step.dependOn(&test_run_step.step); + for (test_files) |test_file_path| { + const test_exe = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path(test_file_path), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "odiff", .module = odiff_mod }, + }, + }), + }); + const run_test_exe = b.addRunArtifact(test_exe); + test_step.dependOn(&run_test_exe.step); } - const test_all_step = b.step("test-all", "Run both unit and integration tests"); - test_all_step.dependOn(test_step); - test_all_step.dependOn(integration_test_step); - const build_ci_step = b.step("ci", "Build the app for CI"); for (build_targets) |target_query| { const t = b.resolveTargetQuery(target_query); - _, const odiff_exe = buildOdiff(b, t, optimize, dynamic, build_options_mod); - odiff_exe.root_module.strip = true; var target_name = try target_query.zigTriple(b.allocator); if (target_query.cpu_arch == .riscv64 and !target_query.cpu_features_add.isEmpty()) target_name = try std.mem.join(b.allocator, "-", &[_][]const u8{ target_name, "rva23" }); - const odiff_output = b.addInstallArtifact(odiff_exe, .{ + + const mod, const odiff_exe = makeOdiff(b, .{ + .target = t, + .optimize = optimize, + .dynamic = dynamic, + }); + odiff_exe.root_module.strip = true; + + const odiff_bin_output = b.addInstallArtifact(odiff_exe, .{ .dest_dir = .{ .override = .{ .custom = target_name }, }, }); - build_ci_step.dependOn(&odiff_output.step); + build_ci_step.dependOn(&odiff_bin_output.step); + + _ = mod; + // const odiff_lib = b.addLibrary(.{ + // .name = "odiff", + // .linkage = .static, + // .root_module = mod, + // }); + // const odiff_lib_output = b.addInstallArtifact(odiff_lib, .{ + // .dest_dir = .{ + // .override = .{ .custom = target_name }, + // }, + // }); + // build_ci_step.dependOn(&odiff_lib_output.step); } } -fn buildOdiff( - b: *std.Build, +const OdiffBuildOptions = struct { target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, dynamic: bool, - build_options_mod: *std.Build.Module, -) struct { *std.Build.Module, *std.Build.Step.Compile } { - const lib_mod = b.createModule(.{ - .root_source_file = b.path("src/root.zig"), +}; +fn makeOdiff(b: *std.Build, options: OdiffBuildOptions) struct { *std.Build.Module, *std.Build.Step.Compile } { + const target = options.target; + const optimize = options.optimize; + const dynamic = options.dynamic; + + const image_mod = b.createModule(.{ + .root_source_file = b.path("src/image/image.zig"), .target = target, .optimize = optimize, - .link_libc = true, }); - linkDeps(b, target, optimize, dynamic, lib_mod); - - 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"); - - lib_mod.addCSourceFiles(.{ - .files = &.{ - "src/rvv.c", + + const io_mod = b.createModule(.{ + .root_source_file = b.path("src/io/io.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "image", .module = image_mod }, }, - .flags = c_flags.items, }); + linkImageDeps(b, target, optimize, dynamic, io_mod); - const exe_mod = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), + const module = b.createModule(.{ + .root_source_file = b.path("src/lib/root.zig"), .target = target, .optimize = optimize, + .imports = &.{ + .{ .name = "io", .module = io_mod }, + .{ .name = "image", .module = image_mod }, + }, }); - exe_mod.addImport("odiff_lib", lib_mod); - exe_mod.addImport("build_options", build_options_mod); - lib_mod.addImport("build_options", build_options_mod); + // riscv vector version + module.addCSourceFile(.{ + .file = b.path("src/lib/rvv.c"), + .flags = &.{"-std=c99"}, + }); + // avx version if (target.result.cpu.arch == .x86_64) { const os_tag = target.result.os.tag; const fmt: ?[]const u8 = switch (os_tag) { @@ -177,36 +142,46 @@ fn buildOdiff( 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); + nasm.addFileArg(b.path("src/lib/vxdiff.asm")); + module.addObjectFile(asm_obj); } } + const build_options = b.addOptions(); + build_options.addOption([]const u8, "version", manifest.version); const exe = b.addExecutable(.{ .name = "odiff", - .root_module = exe_mod, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "odiff", .module = module }, + .{ .name = "build_options", .module = build_options.createModule() }, + }, + }), }); - return .{ lib_mod, exe }; + return .{ module, exe }; } -pub fn linkDeps(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, dynamic: bool, module: *std.Build.Module) void { +pub fn linkImageDeps(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, dynamic: bool, module: *std.Build.Module) 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); - }, - else => { - module.linkSystemLibrary("spng", .{}); - module.linkSystemLibrary("jpeg", .{}); - module.linkSystemLibrary("tiff", .{}); - }, - } + const can_link_dynamically = switch (build_target.os.tag) { + .windows => false, + else => is_cross_compiling, + }; + + if (dynamic and !can_link_dynamically) { + std.log.warn("Dynamic linking is not supported for this target, falling back to static linking", .{}); + } else if (dynamic) { + module.linkSystemLibrary("spng", .{}); + module.linkSystemLibrary("jpeg", .{}); + module.linkSystemLibrary("tiff", .{}); + module.linkSystemLibrary("webp", .{}); } else { Imgz.addToModule(b, module, .{ .target = target, diff --git a/src/cli.zig b/src/cli.zig index fdbece71..86a6ebbe 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1,6 +1,7 @@ const std = @import("std"); -const diff = @import("diff.zig"); +const lib = @import("odiff"); const build_options = @import("build_options"); +const diff = lib.diff; const print = std.debug.print; @@ -76,9 +77,9 @@ fn parseFloatArg(args: [][:0]u8, index: *usize, option_name: []const u8) ?f32 { // --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..]; + arg[option_name.len] == '=') + { + const value_str = arg[option_name.len + 1 ..]; if (value_str.len == 0) return null; index.* += 1; diff --git a/src/image/image.zig b/src/image/image.zig new file mode 100644 index 00000000..27c3451e --- /dev/null +++ b/src/image/image.zig @@ -0,0 +1,102 @@ +const std = @import("std"); + +pub const Image = extern struct { + data: [*]u32, + len: usize, + width: u32, + height: u32, + + pub fn slice(self: Image) []u32 { + return self.data[0..self.len]; + } + + pub fn deinit(self: Image, allocator: std.mem.Allocator) void { + allocator.free(self.slice()); + } + + 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.len); + @memset(data, 0); + return Image{ + .width = self.width, + .height = self.height, + .data = data.ptr, + .len = data.len, + }; + } + + pub fn makeWithWhiteOverlay(self: *const Image, factor: f32, allocator: std.mem.Allocator) !Image { + const data = try allocator.alloc(u32, self.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.ptr, + .len = data.len, + }; + } +}; diff --git a/src/io.zig b/src/io.zig deleted file mode 100644 index d6000713..00000000 --- a/src/io.zig +++ /dev/null @@ -1,8 +0,0 @@ -const io = @import("io/io.zig"); - -pub const Image = io.Image; -pub const ImageFormat = io.ImageFormat; -pub const loadImage = io.loadImage; -pub const loadImageWithFormat = io.loadImageWithFormat; -pub const saveImage = io.saveImage; -pub const saveImageWithFormat = io.saveImageWithFormat; diff --git a/src/io/bmp.zig b/src/io/bmp.zig index 3900fd18..31792542 100644 --- a/src/io/bmp.zig +++ b/src/io/bmp.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const io = @import("io.zig"); +const Image = @import("image").Image; const BMP_SIGNATURE: u16 = 19778; // "BM" in little-endian const BYTES_PER_PIXEL_24: u8 = 3; @@ -191,7 +191,7 @@ fn loadImage32Data(data: []const u8, offset: usize, width: u32, height: u32, all return argb_data; } -pub fn load(allocator: std.mem.Allocator, file_data: []const u8) !io.Image { +pub fn load(allocator: std.mem.Allocator, file_data: []const u8) !Image { if (file_data.len < @sizeOf(BitmapFileHeader) + @sizeOf(BitmapInfoHeader)) { return BmpError.FileCorrupted; } @@ -251,7 +251,7 @@ pub fn load(allocator: std.mem.Allocator, file_data: []const u8) !io.Image { else => return BmpError.UnsupportedBitDepth, }; - return io.Image{ + return .{ .width = width, .height = height, .data = pixel_data.ptr, diff --git a/src/io/io.zig b/src/io/io.zig index c64e4f5b..468e61d4 100644 --- a/src/io/io.zig +++ b/src/io/io.zig @@ -1,112 +1,13 @@ const std = @import("std"); const MemoryMappedFile = @import("memory_mapped_file.zig"); +const Image = @import("image").Image; + const bmp = @import("bmp.zig"); const png = @import("png.zig"); const jpeg = @import("jpeg.zig"); const tiff = @import("tiff.zig"); const webp = @import("webp.zig"); -pub const Image = extern struct { - data: [*]u32, - len: usize, - width: u32, - height: u32, - - pub fn slice(self: Image) []u32 { - return self.data[0..self.len]; - } - - pub fn deinit(self: Image, allocator: std.mem.Allocator) void { - allocator.free(self.slice()); - } - - 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.len); - @memset(data, 0); - return Image{ - .width = self.width, - .height = self.height, - .data = data.ptr, - .len = data.len, - }; - } - - pub fn makeWithWhiteOverlay(self: *const Image, factor: f32, allocator: std.mem.Allocator) !Image { - const data = try allocator.alloc(u32, self.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.ptr, - .len = data.len, - }; - } -}; - pub const ImageFormat = enum(c_int) { png, jpg, @@ -127,7 +28,7 @@ pub const ImageFormat = enum(c_int) { /// Loads an image from a given file path. /// Automatically detects the image format based on the file extension. /// Image data is owned by the caller and must be freed using `allocator.free`. -/// Also checkout `loadImageEx` +/// Also checkout `loadImageWithFormat` pub fn loadImage(allocator: std.mem.Allocator, file_path: []const u8) !Image { const ext = std.fs.path.extension(file_path); const format = ImageFormat.fromExtension(ext) orelse return error.UnsupportedFormat; @@ -154,7 +55,7 @@ pub fn loadImageWithFormat(allocator: std.mem.Allocator, file_path: []const u8, /// Saves an image to a given file path. /// Does not take ownership of the image data. /// -/// Also checkout `saveImageEx` +/// Also checkout `saveImageWithFormat` pub fn saveImage(img: Image, file_path: []const u8) !void { const ext = std.fs.path.extension(file_path); const format = ImageFormat.fromExtension(ext) orelse return error.UnsupportedFormat; diff --git a/src/io/jpeg.zig b/src/io/jpeg.zig index a9002fa8..4436fea0 100644 --- a/src/io/jpeg.zig +++ b/src/io/jpeg.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const Image = @import("io.zig").Image; +const Image = @import("image").Image; const c = @cImport({ @cInclude("turbojpeg.h"); }); diff --git a/src/io/png.zig b/src/io/png.zig index 2cad0ca3..6f5c3b5e 100644 --- a/src/io/png.zig +++ b/src/io/png.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const Image = @import("io.zig").Image; +const Image = @import("image").Image; const c = @cImport({ @cInclude("spng.h"); }); diff --git a/src/io/tiff.zig b/src/io/tiff.zig index 92501fcc..e06acc5d 100644 --- a/src/io/tiff.zig +++ b/src/io/tiff.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const Image = @import("io.zig").Image; +const Image = @import("image").Image; const c = @cImport({ @cInclude("tiffio.h"); }); diff --git a/src/io/webp.zig b/src/io/webp.zig index 70ce4d66..4898ab84 100644 --- a/src/io/webp.zig +++ b/src/io/webp.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const Image = @import("io.zig").Image; +const Image = @import("image").Image; const c = @cImport({ @cInclude("webp/decode.h"); }); diff --git a/src/antialiasing.zig b/src/lib/antialiasing.zig similarity index 98% rename from src/antialiasing.zig rename to src/lib/antialiasing.zig index a232c6fb..c4665da9 100644 --- a/src/antialiasing.zig +++ b/src/lib/antialiasing.zig @@ -1,10 +1,8 @@ // Antialiasing detection - equivalent to Antialiasing.ml const std = @import("std"); -const io = @import("io.zig"); +const Image = @import("image").Image; const color_delta = @import("color_delta.zig"); -const 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); diff --git a/src/color_delta.zig b/src/lib/color_delta.zig similarity index 100% rename from src/color_delta.zig rename to src/lib/color_delta.zig diff --git a/src/diff.zig b/src/lib/diff.zig similarity index 99% rename from src/diff.zig rename to src/lib/diff.zig index 88fff286..ec9078fc 100644 --- a/src/diff.zig +++ b/src/lib/diff.zig @@ -1,11 +1,10 @@ // Core image diffing algorithm - equivalent to Diff.ml const std = @import("std"); const builtin = @import("builtin"); -const io = @import("io.zig"); +const Image = @import("image").Image; const color_delta = @import("color_delta.zig"); const antialiasing = @import("antialiasing.zig"); -const Image = io.Image; const ArrayList = std.ArrayList; const HAS_AVX512f = std.Target.x86.featureSetHas(builtin.cpu.features, .avx512f); diff --git a/src/root.zig b/src/lib/root.zig similarity index 56% rename from src/root.zig rename to src/lib/root.zig index d770e8b8..335a012d 100644 --- a/src/root.zig +++ b/src/lib/root.zig @@ -1,8 +1,11 @@ const std = @import("std"); -pub const cli = @import("cli.zig"); +const image = @import("image"); +pub const Image = image.Image; + +pub const io = @import("io"); +pub const ImageFormat = io.ImageFormat; + pub const diff = @import("diff.zig"); -pub const io = @import("io.zig"); pub const color_delta = @import("color_delta.zig"); pub const antialiasing = @import("antialiasing.zig"); -// pub const bmp_reader = @import("bmp_reader.zig"); diff --git a/src/rvv.c b/src/lib/rvv.c similarity index 100% rename from src/rvv.c rename to src/lib/rvv.c diff --git a/src/vxdiff.asm b/src/lib/vxdiff.asm similarity index 100% rename from src/vxdiff.asm rename to src/lib/vxdiff.asm diff --git a/src/main.zig b/src/main.zig index 137d952a..69b1fd8e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,9 +1,9 @@ const std = @import("std"); -const lib = @import("odiff_lib"); +const lib = @import("odiff"); const print = std.debug.print; -const cli = lib.cli; +const cli = @import("cli.zig"); const io = lib.io; const diff = lib.diff; diff --git a/src/test_avx.zig b/src/test_avx.zig index 73df488b..82863e08 100644 --- a/src/test_avx.zig +++ b/src/test_avx.zig @@ -4,12 +4,12 @@ const expect = testing.expect; const expectEqual = testing.expectEqual; const expectApproxEqRel = testing.expectApproxEqRel; -const odiff = @import("root.zig"); -const io = odiff.io; -const diff = odiff.diff; -const color_delta = odiff.color_delta; +const lib = @import("odiff"); +const io = lib.io; +const diff = lib.diff; +const color_delta = lib.color_delta; -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !io.Image { +fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !lib.Image { return io.loadImage(allocator, path) catch |err| { std.debug.print("Failed to load image: {s}\nError: {}\n", .{ path, err }); return err; @@ -17,9 +17,7 @@ fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !io.Image { } test "layoutDifference: diff images with different layouts without capture" { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); + const allocator = std.testing.allocator; var img1 = try loadTestImage("test/png/white4x4.png", allocator); defer img1.deinit(allocator); diff --git a/src/test_color_delta.zig b/src/test_color_delta.zig index 3d5f3cc0..040baa85 100644 --- a/src/test_color_delta.zig +++ b/src/test_color_delta.zig @@ -1,12 +1,18 @@ 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 { +const color_delta = @import("odiff").color_delta; + +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 { @@ -133,4 +139,3 @@ test "color delta: edge cases" { } } } - diff --git a/src/test_core.zig b/src/test_core.zig index 9677f4d5..2168f6e1 100644 --- a/src/test_core.zig +++ b/src/test_core.zig @@ -4,13 +4,13 @@ const expect = testing.expect; const expectEqual = testing.expectEqual; const expectApproxEqRel = testing.expectApproxEqRel; -const odiff = @import("root.zig"); -const image_io = odiff.io; -const diff = odiff.diff; -const color_delta = odiff.color_delta; +const lib = @import("odiff"); +const io = lib.io; +const diff = lib.diff; +const color_delta = lib.color_delta; -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !image_io.Image { - return image_io.loadImage(allocator, path) catch |err| { +fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !lib.Image { + return io.loadImage(allocator, path) catch |err| { std.debug.print("Failed to load image: {s}\nError: {}\n", .{ path, err }); return err; }; @@ -154,9 +154,9 @@ test "diff color: creates diff output image with custom green diff color" { // 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"); + try io.saveImage(diff_output_img, "test/png/diff-output-green.png"); if (nested_diff_output) |diff_mask| { - try image_io.saveImage(diff_mask, "test/png/diff-of-diff-green.png"); + try io.saveImage(diff_mask, "test/png/diff-of-diff-green.png"); } } diff --git a/src/test_io.zig b/src/test_io.zig new file mode 100644 index 00000000..6a62eed5 --- /dev/null +++ b/src/test_io.zig @@ -0,0 +1,277 @@ +const std = @import("std"); +const testing = std.testing; +const builtin = @import("builtin"); +const lib = @import("odiff"); +const diff = lib.diff; +const io = lib.io; + +const Image = lib.Image; +const alloc = std.testing.allocator; + +///////// PNG + +test "PNG: finds difference between 2 images" { + try expectDiff(.{ + .a = "test/png/orange.png", + .b = "test/png/orange_changed.png", + .options = .{}, + .expected_diff_count = 1366, + .expected_diff_percentage = 1.14, + .diff_percentage_tolerance = 0.1, + }); +} + +test "PNG: Diff of mask and no mask are equal" { + try expectEqualMask( + "test/png/orange.png", + "test/png/orange_changed.png", + ); +} + +test "PNG: Creates correct diff output image" { + try expectCorrectDiffOutput(.{ + .a = "test/png/orange.png", + .b = "test/png/orange_changed.png", + .diff_output_path = "test/png/orange_diff.png", + .debug_output_base = "test/png", + }); +} + +test "PNG: Correctly handles different encodings of transparency" { + try expectDiff(.{ + .a = "test/png/extreme-alpha.png", + .b = "test/png/extreme-alpha-1.png", + .options = .{}, + .expected_diff_count = 0, + .expected_diff_percentage = 0.0, + }); +} + +///////// JPG + +test "JPG: finds difference between 2 images" { + try expectDiff(.{ + .a = "test/jpg/tiger.jpg", + .b = "test/jpg/tiger-2.jpg", + .options = .{}, + .expected_diff_count = 7789, + .expected_diff_percentage = 1.1677, + }); +} + +test "JPG: Diff of mask and no mask are equal" { + try expectEqualMask( + "test/jpg/tiger.jpg", + "test/jpg/tiger-2.jpg", + ); +} + +test "JPG: Creates correct diff output image" { + try expectCorrectDiffOutput(.{ + .a = "test/jpg/tiger.jpg", + .b = "test/jpg/tiger-2.jpg", + .diff_output_path = "test/jpg/tiger-diff.png", + .debug_output_base = "test/jpg", + }); +} + +///////// TIFF + +test "TIFF: finds difference between 2 images" { + try expectDiff(.{ + .a = "test/tiff/laptops.tiff", + .b = "test/tiff/laptops-2.tiff", + .options = .{}, + .expected_diff_count = 8569, + .expected_diff_percentage = 3.79, + .diff_percentage_tolerance = 0.01, + }); +} + +test "TIFF: Diff of mask and no mask are equal" { + try expectEqualMask( + "test/tiff/laptops.tiff", + "test/tiff/laptops-2.tiff", + ); +} + +test "TIFF: Creates correct diff output image" { + try expectCorrectDiffOutput(.{ + .a = "test/tiff/laptops.tiff", + .b = "test/tiff/laptops-2.tiff", + .diff_output_path = "test/tiff/laptops-diff.png", + .debug_output_base = "test/tiff", + }); +} + +///////// WEBP + +test "WEBP: compares webp with png correctly" { + const images = try Images.load("images/donkey.webp", "images/donkey.png"); + defer images.deinit(); + + const res = try images.compare(.{}); + defer res.deinit(); + + try std.testing.expect(res.diff_count > 0); + try std.testing.expect(res.diff_percentage > 0.0); +} + +test "WEBP: Identical WebP images have no differences" { + try expectDiff(.{ + .a = "images/donkey.webp", + .b = "images/donkey.webp", + .options = .{}, + .expected_diff_count = 0, + .expected_diff_percentage = 0.0, + }); +} + +///////// BMP + +test "BMP: finds difference between 2 images" { + try expectDiff(.{ + .a = "test/bmp/clouds.bmp", + .b = "test/bmp/clouds-2.bmp", + .options = .{}, + .expected_diff_count = 192, + .expected_diff_percentage = 0.077, + .diff_percentage_tolerance = 0.01, + }); +} + +test "BMP: Diff of mask and no mask are equal" { + try expectEqualMask( + "test/bmp/clouds.bmp", + "test/bmp/clouds-2.bmp", + ); +} + +// 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" { + // try expectCorrectDiffOutput(.{ + // .a = "test/bmp/clouds.bmp", + // .b = "test/bmp/clouds-2.bmp", + // .diff_output_path = "test/bmp/clouds-diff.png", + // .debug_output_base = "test/bmp", + // }); + return error.SkipZigTest; +} + +const ExpectDiffOpts = struct { + a: []const u8, + b: []const u8, + options: diff.DiffOptions, + expected_diff_count: u32, + expected_diff_percentage: f64, + diff_percentage_tolerance: f64 = 0.001, +}; +fn expectDiff(opts: ExpectDiffOpts) !void { + const images = try Images.load(opts.a, opts.b); + defer images.deinit(); + + const res = try images.compare(opts.options); + defer res.deinit(); + + try std.testing.expectEqual(opts.expected_diff_count, res.diff_count); // diffPixels + try std.testing.expectApproxEqRel( + opts.expected_diff_percentage, + res.diff_percentage, + opts.diff_percentage_tolerance, + ); // diffPercentage +} + +fn expectEqualMask(a_path: []const u8, b_path: []const u8) !void { + const images = try Images.load(a_path, b_path); + defer images.deinit(); + + const with_mask = try images.compare(.{ .output_diff_mask = true }); + defer with_mask.deinit(); + const without_mask = try images.compare(.{ .output_diff_mask = false }); + defer without_mask.deinit(); + + try std.testing.expectEqual(with_mask.diff_count, without_mask.diff_count); + try std.testing.expectApproxEqRel(with_mask.diff_percentage, without_mask.diff_percentage, 0.001); +} + +const ExpectCorrectDiffOutputOpts = struct { + a: []const u8, + b: []const u8, + diff_output_path: []const u8, + debug_output_base: []const u8, +}; +fn expectCorrectDiffOutput(opts: ExpectCorrectDiffOutputOpts) !void { + const images = try Images.load(opts.a, opts.b); + defer images.deinit(); + + const diff_output = try images.compare(.{}); + defer diff_output.deinit(); + try std.testing.expect(diff_output.diff_output != null); + + const expected_diff = try io.loadImage(alloc, opts.diff_output_path); + defer expected_diff.deinit(alloc); + + const diff_result = try Images.compare(.{ + .a = expected_diff, + .b = diff_output.diff_output.?, + }, .{}); + defer diff_result.deinit(); + try std.testing.expect(diff_result.diff_output != null); + + // If there are differences, save debug images + if (diff_result.diff_count > 0) { + const diff_output_path = try std.fs.path.join(alloc, &.{ opts.debug_output_base, "_diff-output.png" }); + defer alloc.free(diff_output_path); + const diff_of_diff_path = try std.fs.path.join(alloc, &.{ opts.debug_output_base, "_diff-of-diff.png" }); + defer alloc.free(diff_of_diff_path); + + // Note: We can only save as PNG currently, but that's fine for debug output + try io.saveImage(diff_result.diff_output.?, diff_output_path); + if (diff_result.diff_output) |diff_mask| { + try io.saveImage(diff_mask, diff_of_diff_path); + } + } + + try std.testing.expectEqual(0, diff_result.diff_count); // diffOfDiffPixels + try std.testing.expectApproxEqRel(0.0, diff_result.diff_percentage, 0.001); // diffOfDiffPercentage +} + +const Images = struct { + a: Image, + b: Image, + + pub fn load(a_path: []const u8, b_path: []const u8) !Images { + const a = try io.loadImage(alloc, a_path); + errdefer a.deinit(alloc); + const b = try io.loadImage(alloc, b_path); + errdefer b.deinit(alloc); + return .{ .a = a, .b = b }; + } + + pub fn deinit(self: Images) void { + self.a.deinit(alloc); + self.b.deinit(alloc); + } + + pub const CompareResult = struct { + diff_output: ?Image, + diff_count: u32, + diff_percentage: f64, + + pub fn deinit(self: CompareResult) void { + if (self.diff_output) |img| img.deinit(alloc); + } + }; + + pub fn compare(self: Images, options: diff.DiffOptions) !CompareResult { + const diff_output, const diff_count, const diff_percentage, var diff_lines = + try diff.compare(&self.a, &self.b, options, alloc); + defer if (diff_lines) |*lines| lines.deinit(); + return .{ + .diff_output = diff_output, + .diff_count = diff_count, + .diff_percentage = diff_percentage, + }; + } +}; diff --git a/src/test_io_bmp.zig b/src/test_io_bmp.zig deleted file mode 100644 index 360a8468..00000000 --- a/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 io = odiff.io; -const diff = odiff.diff; - -const testing_allocator = testing.allocator; - -fn loadImage(path: []const u8) !io.Image { - return io.loadImage(testing_allocator, path) 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(testing_allocator); - var img2 = try loadImage("test/bmp/clouds-2.bmp"); - defer img2.deinit(testing_allocator); - - 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(testing_allocator); - 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(testing_allocator); - var img2 = try loadImage("test/bmp/clouds-2.bmp"); - defer img2.deinit(testing_allocator); - - // 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(testing_allocator); - 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(testing_allocator); - var img2_mask = try loadImage("test/bmp/clouds-2.bmp"); - defer img2_mask.deinit(testing_allocator); - - 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(testing_allocator); - 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/src/test_io_jpg.zig b/src/test_io_jpg.zig deleted file mode 100644 index d2d15f9a..00000000 --- a/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 io = odiff.io; -const diff = odiff.diff; - -// Helper function to load test images -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !io.Image { - return io.loadImage(allocator, path) 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(allocator); - - var img2 = try loadTestImage("test/jpg/tiger-2.jpg", allocator); - defer img2.deinit(allocator); - - 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(allocator); - 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(allocator); - - var img2 = try loadTestImage("test/jpg/tiger-2.jpg", allocator); - defer img2.deinit(allocator); - - // 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(allocator); - 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(allocator); - - var img2_copy = try loadTestImage("test/jpg/tiger-2.jpg", allocator); - defer img2_copy.deinit(allocator); - - 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(allocator); - 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(allocator); - - var img2 = try loadTestImage("test/jpg/tiger-2.jpg", allocator); - defer img2.deinit(allocator); - - 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(allocator); - 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(allocator); - - 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(allocator); - 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 io.saveImage(diff_output_img, "test/jpg/_diff-output.png"); - if (diff_result_output) |diff_mask| { - try io.saveImage(diff_mask, "test/jpg/_diff-of-diff.png"); - } - } - - try expectEqual(@as(u32, 0), diff_result_count); // diffOfDiffPixels - try expectApproxEqRel(@as(f64, 0.0), diff_result_percentage, 0.001); // diffOfDiffPercentage - } -} diff --git a/src/test_io_png.zig b/src/test_io_png.zig deleted file mode 100644 index 162f7ce5..00000000 --- a/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 io = odiff.io; -const diff = odiff.diff; - -// Helper function to load test images -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !io.Image { - return io.loadImage(allocator, path) 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(allocator); - - var img2 = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2.deinit(allocator); - - 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(allocator); - 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(allocator); - - var img2 = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2.deinit(allocator); - - // 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(allocator); - 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(allocator); - - var img2_copy = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2_copy.deinit(allocator); - - 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(allocator); - 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(allocator); - var img2 = try loadTestImage("test/png/orange_changed.png", allocator); - defer img2.deinit(allocator); - - 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(allocator); - }; - 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(allocator); - - 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(allocator); - 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 io.saveImage(diff_output_img, "test/png/diff-output.png"); - if (nested_diff_output) |diff_mask| { - try io.saveImage(diff_mask, "test/png/diff-of-diff.png"); - } - } - - 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(allocator); - - var img2 = try loadTestImage("test/png/extreme-alpha-1.png", allocator); - defer img2.deinit(allocator); - - 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(allocator); - }; - 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/src/test_io_tiff.zig b/src/test_io_tiff.zig deleted file mode 100644 index 661b7146..00000000 --- a/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 io = odiff.io; -const diff = odiff.diff; - -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !io.Image { - return io.loadImage(allocator, path) 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(allocator); - - var img2 = try loadTestImage("test/tiff/laptops-2.tiff", allocator); - defer img2.deinit(allocator); - - 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(allocator); - 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(allocator); - - var img2 = try loadTestImage("test/tiff/laptops-2.tiff", allocator); - defer img2.deinit(allocator); - - // 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(allocator); - 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(allocator); - - var img2_copy = try loadTestImage("test/tiff/laptops-2.tiff", allocator); - defer img2_copy.deinit(allocator); - - 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(allocator); - 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(allocator); - - var img2 = try loadTestImage("test/tiff/laptops-2.tiff", allocator); - defer img2.deinit(allocator); - - 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(allocator); - }; - 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(allocator); - - 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(allocator); - 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 io.saveImage(diff_output_img, "test/tiff/_diff-output.png"); - if (nested_diff_output) |diff_mask| { - try io.saveImage(diff_mask, "test/tiff/_diff-of-diff.png"); - } - } - - try expectEqual(@as(u32, 0), nested_diff_count); // diffOfDiffPixels - try expectApproxEqRel(@as(f64, 0.0), nested_diff_percentage, 0.001); // diffOfDiffPercentage - } -} diff --git a/src/test_io_webp.zig b/src/test_io_webp.zig deleted file mode 100644 index 655b1513..00000000 --- a/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 io = odiff.io; -const diff = odiff.diff; - -fn loadTestImage(path: []const u8, allocator: std.mem.Allocator) !io.Image { - return io.loadImage(allocator, path) 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(allocator); - - 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(allocator); - - var png_img = try loadTestImage("images/donkey.png", allocator); - defer png_img.deinit(allocator); - - const options = diff.DiffOptions{}; - const 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(allocator); - 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(allocator); - - var img2 = try loadTestImage("images/donkey.webp", allocator); - defer img2.deinit(allocator); - - const options = diff.DiffOptions{}; - const 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(allocator); - 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 -}