initial extract from ghostty+jpeg

This commit is contained in:
Jeffrey C. Ollie 2024-12-21 15:21:42 -06:00
commit 8271e7e4a1
Signed by: jeff
GPG key ID: 6F86035A6D97044E
12 changed files with 684 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/.zig-cache
/zig-out

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright © 2004 Jeffrey C. Ollie
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.

65
build.zig Normal file
View file

@ -0,0 +1,65 @@
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const wuffs = b.dependency("wuffs", .{});
const module = b.addModule("wuffs", .{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
// if (target.result.isDarwin()) {
// const apple_sdk = @import("apple_sdk");
// try apple_sdk.addPaths(b, module);
// }
var flags = std.ArrayList([]const u8).init(b.allocator);
defer flags.deinit();
try flags.append("-DWUFFS_IMPLEMENTATION");
inline for (@import("src/c.zig").defines) |key| {
try flags.append("-D" ++ key);
}
module.addIncludePath(wuffs.path("release/c"));
module.addCSourceFile(.{
.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);
}

21
build.zig.zon Normal file
View file

@ -0,0 +1,21 @@
.{
.name = "wuffs",
.version = "0.0.0",
.dependencies = .{
.wuffs = .{
.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=d843c2714d32e15b48b8d7eeb480295af537f877",
.hash = "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806",
}
// .apple_sdk = .{ .path = "../apple-sdk" },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

101
flake.lock Normal file
View file

@ -0,0 +1,101 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1734424634,
"narHash": "sha256-cHar1vqHOOyC7f1+tVycPoWTfKIaqkoe1Q6TnKzuti4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d3c42f187194c26d9f0309a8ecc469d6c878ce33",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"zig": "zig"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"zig": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1734740962,
"narHash": "sha256-5YsE/uxeHJSa1S86553fgjjYDRJl7iLs41cSSXW/K04=",
"owner": "mitchellh",
"repo": "zig-overlay",
"rev": "956fa2f98490537625f582122b6f7f8adabf9686",
"type": "github"
},
"original": {
"owner": "mitchellh",
"repo": "zig-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

39
flake.nix Normal file
View file

@ -0,0 +1,39 @@
{
description = "zig-wuffs";
inputs = {
nixpkgs = {
url = "nixpkgs/nixos-unstable";
};
flake-utils = {
url = "github:numtide/flake-utils";
};
zig = {
url = "github:mitchellh/zig-overlay";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
};
};
outputs = {
nixpkgs,
flake-utils,
zig,
...
}: let
in
flake-utils.lib.eachDefaultSystem (
system: let
pkgs = import nixpkgs {
inherit system;
};
in {
devShells.default = pkgs.mkShell {
name = "zig-wuffs";
nativeBuildInputs = [
zig.packages.${system}.master
];
};
}
);
}

18
src/c.zig Normal file
View file

@ -0,0 +1,18 @@
pub const c = @cImport({
for (defines) |d| @cDefine(d, "1");
@cInclude("wuffs-v0.4.c");
});
/// All the C macros defined so that the header matches the build.
pub const defines: []const []const u8 = &[_][]const u8{
"WUFFS_CONFIG__MODULES",
"WUFFS_CONFIG__MODULE__AUX__BASE",
"WUFFS_CONFIG__MODULE__AUX__IMAGE",
"WUFFS_CONFIG__MODULE__BASE",
"WUFFS_CONFIG__MODULE__ADLER32",
"WUFFS_CONFIG__MODULE__CRC32",
"WUFFS_CONFIG__MODULE__DEFLATE",
"WUFFS_CONFIG__MODULE__JPEG",
"WUFFS_CONFIG__MODULE__PNG",
"WUFFS_CONFIG__MODULE__ZLIB",
};

13
src/error.zig Normal file
View file

@ -0,0 +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
src/jpeg.zig Normal file
View file

@ -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);
}

9
src/main.zig Normal file
View file

@ -0,0 +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());
}

146
src/png.zig Normal file
View file

@ -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_png);
/// Decode a PNG 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_png__decoder());
defer alloc.free(decoder_buf);
const decoder: ?*c.wuffs_png__decoder = @ptrCast(decoder_buf);
{
const status = c.wuffs_png__decoder__initialize(
decoder,
c.sizeof__wuffs_png__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_png__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_png__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_png__decoder__decode_frame_config(
decoder,
&frame_config,
&source_buffer,
);
try check(log, &status);
}
{
const status = c.wuffs_png__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 "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);
}

103
src/swizzle.zig Normal file
View file

@ -0,0 +1,103 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Error = @import("error.zig").Error;
const log = std.log.scoped(.wuffs_swizzler);
pub fn gToRgba(alloc: Allocator, src: []const u8) Error![]u8 {
return swizzle(
alloc,
src,
c.WUFFS_BASE__PIXEL_FORMAT__Y,
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
);
}
pub fn gaToRgba(alloc: Allocator, src: []const u8) Error![]u8 {
return swizzle(
alloc,
src,
c.WUFFS_BASE__PIXEL_FORMAT__YA_PREMUL,
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
);
}
pub fn rgbToRgba(alloc: Allocator, src: []const u8) Error![]u8 {
return swizzle(
alloc,
src,
c.WUFFS_BASE__PIXEL_FORMAT__RGB,
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
);
}
fn swizzle(
alloc: Allocator,
src: []const u8,
comptime src_pixel_format: u32,
comptime dst_pixel_format: u32,
) Error![]u8 {
const src_slice = c.wuffs_base__make_slice_u8(
@constCast(src.ptr),
src.len,
);
const dst_fmt = c.wuffs_base__make_pixel_format(
dst_pixel_format,
);
assert(c.wuffs_base__pixel_format__is_direct(&dst_fmt));
assert(c.wuffs_base__pixel_format__is_interleaved(&dst_fmt));
assert(c.wuffs_base__pixel_format__bits_per_pixel(&dst_fmt) % 8 == 0);
const dst_size = c.wuffs_base__pixel_format__bits_per_pixel(&dst_fmt) / 8;
const src_fmt = c.wuffs_base__make_pixel_format(
src_pixel_format,
);
assert(c.wuffs_base__pixel_format__is_direct(&src_fmt));
assert(c.wuffs_base__pixel_format__is_interleaved(&src_fmt));
assert(c.wuffs_base__pixel_format__bits_per_pixel(&src_fmt) % 8 == 0);
const src_size = c.wuffs_base__pixel_format__bits_per_pixel(&src_fmt) / 8;
assert(src.len % src_size == 0);
const dst = try alloc.alloc(u8, src.len * dst_size / src_size);
errdefer alloc.free(dst);
const dst_slice = c.wuffs_base__make_slice_u8(
dst.ptr,
dst.len,
);
var swizzler: c.wuffs_base__pixel_swizzler = undefined;
{
const status = c.wuffs_base__pixel_swizzler__prepare(
&swizzler,
dst_fmt,
c.wuffs_base__empty_slice_u8(),
src_fmt,
c.wuffs_base__empty_slice_u8(),
c.WUFFS_BASE__PIXEL_BLEND__SRC,
);
if (!c.wuffs_base__status__is_ok(&status)) {
const e = c.wuffs_base__status__message(&status);
log.warn("{s}", .{e});
return error.WuffsError;
}
}
{
_ = c.wuffs_base__pixel_swizzler__swizzle_interleaved_from_slice(
&swizzler,
dst_slice,
c.wuffs_base__empty_slice_u8(),
src_slice,
);
}
return dst;
}