Skip to content

Commit

Permalink
wuffs: update, add jpeg decoding, add simple tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jcollie committed Jan 1, 2025
1 parent 60611b8 commit e990019
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 28 deletions.
60 changes: 60 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 Sentry 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 Sentry Build
run: |
nix develop -c sh -c "cd pkg/${{ matrix.pkg }} ; zig build test"
2 changes: 1 addition & 1 deletion nix/zigCacheHash.nix
Original file line number Diff line number Diff line change
@@ -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="
32 changes: 32 additions & 0 deletions pkg/wuffs/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
9 changes: 7 additions & 2 deletions pkg/wuffs/build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
10 changes: 10 additions & 0 deletions pkg/wuffs/src/error.zig
Original file line number Diff line number Diff line change
@@ -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;
}
}
146 changes: 146 additions & 0 deletions pkg/wuffs/src/jpeg.zig
Original file line number Diff line number Diff line change
@@ -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);
}
7 changes: 7 additions & 0 deletions pkg/wuffs/src/main.zig
Original file line number Diff line number Diff line change
@@ -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());
}
49 changes: 24 additions & 25 deletions pkg/wuffs/src/png.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 = .{
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}

{
Expand All @@ -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 .{
Expand All @@ -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);
}

0 comments on commit e990019

Please sign in to comment.