diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f8d2671c..4f07faf2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -478,3 +478,63 @@ jobs: useDaemon: false # sometimes fails on short jobs - name: typos check run: nix develop -c typos + + test-pkg-linux: + strategy: + fail-fast: false + matrix: + pkg: ["wuffs"] + name: Run pkg/${{ matrix.pkg }} tests on Linux + runs-on: namespace-profile-ghostty-sm + needs: test + env: + ZIG_LOCAL_CACHE_DIR: /zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@v1.2.0 + with: + path: | + /nix + /zig + + # Install Nix and use that to run our tests so our environment matches exactly. + - uses: cachix/install-nix-action@v30 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v15 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Test ${{ matrix.pkg }} Build + run: | + nix develop -c sh -c "cd pkg/${{ matrix.pkg }} ; zig build test" + + test-pkg-macos: + strategy: + fail-fast: false + matrix: + pkg: ["wuffs"] + name: Run pkg/${{ matrix.pkg }} tests on macOS + runs-on: namespace-profile-ghostty-macos + needs: test + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # Install Nix and use that to run our tests so our environment matches exactly. + - uses: cachix/install-nix-action@v30 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v15 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Test ${{ matrix.pkg }} Build + run: | + nix develop -c sh -c "cd pkg/${{ matrix.pkg }} ; zig build test" diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index d4d451e03..60e9e58a4 100644 --- a/nix/zigCacheHash.nix +++ b/nix/zigCacheHash.nix @@ -1,3 +1,3 @@ # This file is auto-generated! check build-support/check-zig-cache-hash.sh for # more details. -"sha256-ot5onG1yq7EWQkNUgTNBuqvsnLuaoFs2UDS96IqgJmU=" +"sha256-njCce+r1DPTKLNrmrD2ObEoBS9nR7q03hqegQWe1UuY=" diff --git a/pkg/wuffs/build.zig b/pkg/wuffs/build.zig index 36bb5a07c..438f714d3 100644 --- a/pkg/wuffs/build.zig +++ b/pkg/wuffs/build.zig @@ -30,4 +30,36 @@ pub fn build(b: *std.Build) !void { .file = wuffs.path("release/c/wuffs-v0.4.c"), .flags = flags.items, }); + + const unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + unit_tests.linkLibC(); + unit_tests.addIncludePath(wuffs.path("release/c")); + unit_tests.addCSourceFile(.{ + .file = wuffs.path("release/c/wuffs-v0.4.c"), + .flags = flags.items, + }); + + const pixels = b.dependency("pixels", .{}); + + inline for (.{ "000000", "FFFFFF" }) |color| { + inline for (.{ "gif", "jpg", "png", "ppm" }) |extension| { + const filename = std.fmt.comptimePrint("1x1#{s}.{s}", .{ color, extension }); + unit_tests.root_module.addAnonymousImport( + filename, + .{ + .root_source_file = pixels.path(filename), + }, + ); + } + } + + const run_unit_tests = b.addRunArtifact(unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); } diff --git a/pkg/wuffs/build.zig.zon b/pkg/wuffs/build.zig.zon index 126e43aba..d84d6957e 100644 --- a/pkg/wuffs/build.zig.zon +++ b/pkg/wuffs/build.zig.zon @@ -3,8 +3,13 @@ .version = "0.0.0", .dependencies = .{ .wuffs = .{ - .url = "https://github.com/google/wuffs/archive/refs/tags/v0.4.0-alpha.8.tar.gz", - .hash = "12200984439edc817fbcbbaff564020e5104a0d04a2d0f53080700827052de700462", + .url = "https://github.com/google/wuffs/archive/refs/tags/v0.4.0-alpha.9.tar.gz", + .hash = "122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd", + }, + + .pixels = .{ + .url = "git+https://github.com/make-github-pseudonymous-again/pixels?ref=main#d843c2714d32e15b48b8d7eeb480295af537f877", + .hash = "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806", }, .apple_sdk = .{ .path = "../apple-sdk" }, diff --git a/pkg/wuffs/src/error.zig b/pkg/wuffs/src/error.zig index 609deec9c..c75188718 100644 --- a/pkg/wuffs/src/error.zig +++ b/pkg/wuffs/src/error.zig @@ -1,3 +1,13 @@ const std = @import("std"); +const c = @import("c.zig").c; + pub const Error = std.mem.Allocator.Error || error{WuffsError}; + +pub fn check(log: anytype, status: *const c.struct_wuffs_base__status__struct) error{WuffsError}!void { + if (!c.wuffs_base__status__is_ok(status)) { + const e = c.wuffs_base__status__message(status); + log.warn("decode err={s}", .{e}); + return error.WuffsError; + } +} diff --git a/pkg/wuffs/src/jpeg.zig b/pkg/wuffs/src/jpeg.zig new file mode 100644 index 000000000..63ca428d1 --- /dev/null +++ b/pkg/wuffs/src/jpeg.zig @@ -0,0 +1,146 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const c = @import("c.zig").c; +const Error = @import("error.zig").Error; +const check = @import("error.zig").check; + +const log = std.log.scoped(.wuffs_jpeg); + +/// Decode a JPEG image. +pub fn decode(alloc: Allocator, data: []const u8) Error!struct { + width: u32, + height: u32, + data: []const u8, +} { + // Work around some weirdness in WUFFS/Zig, there are some structs that + // are defined as "extern" by the Zig compiler which means that Zig won't + // allocate them on the stack at compile time. WUFFS has functions for + // dynamically allocating these structs but they use the C malloc/free. This + // gets around that by using the Zig allocator to allocate enough memory for + // the struct and then casts it to the appropriate pointer. + + const decoder_buf = try alloc.alloc(u8, c.sizeof__wuffs_jpeg__decoder()); + defer alloc.free(decoder_buf); + + const decoder: ?*c.wuffs_jpeg__decoder = @ptrCast(decoder_buf); + { + const status = c.wuffs_jpeg__decoder__initialize( + decoder, + c.sizeof__wuffs_jpeg__decoder(), + c.WUFFS_VERSION, + 0, + ); + try check(log, &status); + } + + var source_buffer: c.wuffs_base__io_buffer = .{ + .data = .{ .ptr = @constCast(@ptrCast(data.ptr)), .len = data.len }, + .meta = .{ + .wi = data.len, + .ri = 0, + .pos = 0, + .closed = true, + }, + }; + + var image_config: c.wuffs_base__image_config = undefined; + { + const status = c.wuffs_jpeg__decoder__decode_image_config( + decoder, + &image_config, + &source_buffer, + ); + try check(log, &status); + } + + const width = c.wuffs_base__pixel_config__width(&image_config.pixcfg); + const height = c.wuffs_base__pixel_config__height(&image_config.pixcfg); + + c.wuffs_base__pixel_config__set( + &image_config.pixcfg, + c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL, + c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, + width, + height, + ); + + const destination = try alloc.alloc( + u8, + width * height * @sizeOf(c.wuffs_base__color_u32_argb_premul), + ); + errdefer alloc.free(destination); + + // temporary buffer for intermediate processing of image + const work_buffer = try alloc.alloc( + u8, + + // The type of this is a u64 on all systems but our allocator + // uses a usize which is a u32 on 32-bit systems. + std.math.cast( + usize, + c.wuffs_jpeg__decoder__workbuf_len(decoder).max_incl, + ) orelse return error.OutOfMemory, + ); + defer alloc.free(work_buffer); + + const work_slice = c.wuffs_base__make_slice_u8( + work_buffer.ptr, + work_buffer.len, + ); + + var pixel_buffer: c.wuffs_base__pixel_buffer = undefined; + { + const status = c.wuffs_base__pixel_buffer__set_from_slice( + &pixel_buffer, + &image_config.pixcfg, + c.wuffs_base__make_slice_u8(destination.ptr, destination.len), + ); + try check(log, &status); + } + + var frame_config: c.wuffs_base__frame_config = undefined; + { + const status = c.wuffs_jpeg__decoder__decode_frame_config( + decoder, + &frame_config, + &source_buffer, + ); + try check(log, &status); + } + + { + const status = c.wuffs_jpeg__decoder__decode_frame( + decoder, + &pixel_buffer, + &source_buffer, + c.WUFFS_BASE__PIXEL_BLEND__SRC, + work_slice, + null, + ); + try check(log, &status); + } + + return .{ + .width = width, + .height = height, + .data = destination, + }; +} + +test "jpeg_decode_000000" { + const data = try decode(std.testing.allocator, @embedFile("1x1#000000.jpg")); + defer std.testing.allocator.free(data.data); + + try std.testing.expectEqual(1, data.width); + try std.testing.expectEqual(1, data.height); + try std.testing.expectEqualSlices(u8, &.{ 0, 0, 0, 255 }, data.data); +} + +test "jpeg_decode_FFFFFF" { + const data = try decode(std.testing.allocator, @embedFile("1x1#FFFFFF.jpg")); + defer std.testing.allocator.free(data.data); + + try std.testing.expectEqual(1, data.width); + try std.testing.expectEqual(1, data.height); + try std.testing.expectEqualSlices(u8, &.{ 255, 255, 255, 255 }, data.data); +} diff --git a/pkg/wuffs/src/main.zig b/pkg/wuffs/src/main.zig index 3f03a4158..f5fc01501 100644 --- a/pkg/wuffs/src/main.zig +++ b/pkg/wuffs/src/main.zig @@ -1,2 +1,9 @@ +const std = @import("std"); + pub const png = @import("png.zig"); +pub const jpeg = @import("jpeg.zig"); pub const swizzle = @import("swizzle.zig"); + +test { + std.testing.refAllDeclsRecursive(@This()); +} diff --git a/pkg/wuffs/src/png.zig b/pkg/wuffs/src/png.zig index 3a3ac9a35..4597c6ccb 100644 --- a/pkg/wuffs/src/png.zig +++ b/pkg/wuffs/src/png.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const c = @import("c.zig").c; const Error = @import("error.zig").Error; +const check = @import("error.zig").check; const log = std.log.scoped(.wuffs_png); @@ -29,11 +30,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct { c.WUFFS_VERSION, 0, ); - if (!c.wuffs_base__status__is_ok(&status)) { - const e = c.wuffs_base__status__message(&status); - log.warn("decode err={s}", .{e}); - return error.WuffsError; - } + try check(log, &status); } var source_buffer: c.wuffs_base__io_buffer = .{ @@ -53,11 +50,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct { &image_config, &source_buffer, ); - if (!c.wuffs_base__status__is_ok(&status)) { - const e = c.wuffs_base__status__message(&status); - log.warn("decode err={s}", .{e}); - return error.WuffsError; - } + try check(log, &status); } const width = c.wuffs_base__pixel_config__width(&image_config.pixcfg); @@ -102,11 +95,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct { &image_config.pixcfg, c.wuffs_base__make_slice_u8(destination.ptr, destination.len), ); - if (!c.wuffs_base__status__is_ok(&status)) { - const e = c.wuffs_base__status__message(&status); - log.warn("decode err={s}", .{e}); - return error.WuffsError; - } + try check(log, &status); } var frame_config: c.wuffs_base__frame_config = undefined; @@ -116,11 +105,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct { &frame_config, &source_buffer, ); - if (!c.wuffs_base__status__is_ok(&status)) { - const e = c.wuffs_base__status__message(&status); - log.warn("decode err={s}", .{e}); - return error.WuffsError; - } + try check(log, &status); } { @@ -132,11 +117,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct { work_slice, null, ); - if (!c.wuffs_base__status__is_ok(&status)) { - const e = c.wuffs_base__status__message(&status); - log.warn("decode err={s}", .{e}); - return error.WuffsError; - } + try check(log, &status); } return .{ @@ -145,3 +126,21 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!struct { .data = destination, }; } + +test "png_decode_000000" { + const data = try decode(std.testing.allocator, @embedFile("1x1#000000.png")); + defer std.testing.allocator.free(data.data); + + try std.testing.expectEqual(1, data.width); + try std.testing.expectEqual(1, data.height); + try std.testing.expectEqualSlices(u8, &.{ 0, 0, 0, 255 }, data.data); +} + +test "png_decode_FFFFFF" { + const data = try decode(std.testing.allocator, @embedFile("1x1#FFFFFF.png")); + defer std.testing.allocator.free(data.data); + + try std.testing.expectEqual(1, data.width); + try std.testing.expectEqual(1, data.height); + try std.testing.expectEqualSlices(u8, &.{ 255, 255, 255, 255 }, data.data); +}