Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
388 changes: 204 additions & 184 deletions README.md

Large diffs are not rendered by default.

31 changes: 20 additions & 11 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ pub fn build(b: *std.Build) void {
});

const lib = b.addStaticLibrary(.{
.name = "jpeg-encoder",
.name = "kiyo",
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
lib.linkLibC();
b.installArtifact(lib);

// Default: build the library
const default_step = b.step("lib", "Build the kiyo library");
default_step.dependOn(&b.addInstallArtifact(lib, .{}).step);
b.getInstallStep().dependOn(default_step);

// CLI tool
const cli_exe = b.addExecutable(.{
.name = "kiyo",
Expand All @@ -27,16 +31,19 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
cli_exe.root_module.addImport("kiyo", mod);
b.installArtifact(cli_exe);
const install_cli = b.addInstallArtifact(cli_exe, .{});

const run_cli = b.addRunArtifact(cli_exe);
if (b.args) |args| {
run_cli.addArgs(args);
}
const cli_step = b.step("run", "Run the kiyo CLI");
cli_step.dependOn(&run_cli.step);
const cli_step = b.step("cli", "Build the kiyo CLI");
cli_step.dependOn(&install_cli.step);

const run_step = b.step("run", "Run the kiyo CLI");
run_step.dependOn(&run_cli.step);

// Default: run all tests
// Tests
const tests = b.addTest(.{
.root_source_file = b.path("src/root.zig"),
.target = target,
Expand All @@ -54,7 +61,6 @@ pub fn build(b: *std.Build) void {
.optimize = .ReleaseFast,
});
bench_exe.root_module.addImport("kiyo", mod);
b.installArtifact(bench_exe);

const run_bench = b.addRunArtifact(bench_exe);
const bench_step = b.step("bench", "Run benchmarks");
Expand All @@ -68,7 +74,6 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
verify_exe.root_module.addImport("kiyo", mod);
b.installArtifact(verify_exe);

const run_verify = b.addRunArtifact(verify_exe);
const verify_step = b.step("verify", "Encode & validate JPEG files on disk");
Expand All @@ -82,7 +87,6 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
fuzz_dec_exe.root_module.addImport("kiyo", mod);
b.installArtifact(fuzz_dec_exe);

const fuzz_dec_step = b.step("fuzz-decoder", "Run decoder fuzzer");
fuzz_dec_step.dependOn(&b.addRunArtifact(fuzz_dec_exe).step);
Expand All @@ -95,7 +99,6 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
fuzz_enc_exe.root_module.addImport("kiyo", mod);
b.installArtifact(fuzz_enc_exe);

const fuzz_enc_step = b.step("fuzz-encoder", "Run encoder fuzzer");
fuzz_enc_step.dependOn(&b.addRunArtifact(fuzz_enc_exe).step);
Expand All @@ -108,9 +111,15 @@ pub fn build(b: *std.Build) void {
.optimize = .ReleaseFast,
});
bench_pro_exe.root_module.addImport("kiyo", mod);
b.installArtifact(bench_pro_exe);

const run_bench_pro = b.addRunArtifact(bench_pro_exe);
const bench_pro_step = b.step("bench-pro", "Run professional benchmarks");
bench_pro_step.dependOn(&run_bench_pro.step);

// Format
const fmt_step = b.step("fmt", "Format source files");
const fmt = b.addFmt(.{
.paths = &.{ "src", "test", "build.zig" },
});
fmt_step.dependOn(&fmt.step);
}
8 changes: 4 additions & 4 deletions src/decoder.zig
Original file line number Diff line number Diff line change
Expand Up @@ -777,12 +777,12 @@ pub const JPEGDecoder = struct {

pub fn decodeJPEG(allocator: std.mem.Allocator, buffer: []const u8, options: types.DecodeOptions) !types.ImageData {
var decoder = JPEGDecoder.init(buffer);
var result = try decoder.decode(allocator, options);
const result = try decoder.decode(allocator, options);

if (options.output_format == .rgb) {
for (0..result.pixels.len) |i| {
result.pixels[i].a = 255;
}
// Decoder always produces RGBA internally (alpha=255). The .rgb format
// signals to callers that alpha is unused — no transformation needed
// since alpha is already 255 from the decode path.
}

return result;
Expand Down
7 changes: 7 additions & 0 deletions src/encoder.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const presets_mod = @import("presets.zig");

const Block8x8 = types.Block8x8;

pub const EncoderError = error{
InvalidQuality,
InvalidImageDimensions,
ImageTooLarge,
InvalidImageData,
};

const RawBitBuffer = struct {
buf: [*]u8,
len: usize,
Expand Down
33 changes: 0 additions & 33 deletions src/integration_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3167,39 +3167,6 @@ test "Subsampling: 4:2:0 non-square 64x32" {
try testing.expect(avg_err < 70.0);
}

test "DIAG: 64x64 encode+decode" {
const pixels = try makeRandomPixels(64, 64, 42);
defer allocator.free(pixels);

var img = types.ImageData{ .width = 64, .height = 64, .pixels = pixels };
var encoded = try encoder.encodeJPEG(allocator, &img, .{ .quality = 90, .subsample = true });
defer encoded.deinit(allocator);

std.debug.print("\n=== 64x64 encoded len={d}, first 16 bytes: ", .{encoded.buffer.len});
const end = @min(encoded.buffer.len, 16);
for (0..end) |i| {
std.debug.print("{X:0>2} ", .{encoded.buffer[i]});
}
std.debug.print("===\n", .{});

var decoded = decoder.decodeJPEG(allocator, encoded.buffer, .{}) catch |err| {
std.debug.print("DECODE FAILED: {s}\n", .{@errorName(err)});
return err;
};
defer decoded.deinit(allocator);

std.debug.print("DECODE OK: {d}x{d}\n", .{ decoded.width, decoded.height });

var total_err: u64 = 0;
for (0..4096) |i| {
total_err += @abs(@as(i32, pixels[i].r) - @as(i32, decoded.pixels[i].r));
total_err += @abs(@as(i32, pixels[i].g) - @as(i32, decoded.pixels[i].g));
total_err += @abs(@as(i32, pixels[i].b) - @as(i32, decoded.pixels[i].b));
}
const avg_err = @as(f64, @floatFromInt(total_err)) / (4096.0 * 3.0);
std.debug.print("avg_err={d:.2}\n", .{avg_err});
}

test "Subsampling: 4:2:0 non-multiple-of-8 33x33" {
const pixels = try makeRandomPixels(33, 33, 77);
defer allocator.free(pixels);
Expand Down
13 changes: 10 additions & 3 deletions src/io.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ pub fn readEntireFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
}

pub fn writeEntireFile(path: []const u8, data: []const u8) !void {
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
try file.writeAll(data);
const tmp_path = try std.fmt.allocPrint(std.heap.page_allocator, "{s}.tmp", .{path});
defer std.heap.page_allocator.free(tmp_path);

{
const file = try std.fs.cwd().createFile(tmp_path, .{});
defer file.close();
try file.writeAll(data);
}
try std.fs.cwd().rename(tmp_path, path);
}

pub fn decodeFromFile(allocator: std.mem.Allocator, path: []const u8) !types.ImageData {
Expand All @@ -34,6 +40,7 @@ pub fn encodeToFile(allocator: std.mem.Allocator, path: []const u8, image: *cons
}

pub fn encodeToPath(allocator: std.mem.Allocator, path: []const u8, pixels: []const types.RGBAPixel, width: u32, height: u32, quality: u8) !void {
// SAFETY: encoder only reads pixels, never writes.
var img = types.ImageData{
.width = width,
.height = height,
Expand Down
20 changes: 4 additions & 16 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub fn main() !void {
printUsage();
} else if (std.mem.eql(u8, command, "version") or std.mem.eql(u8, command, "--version")) {
const stdout = std.io.getStdOut().writer();
try stdout.print("kiyo v2.0.0\n", .{});
try stdout.print("kiyo v{s}\n", .{kiyo.version});
} else {
const stderr = std.io.getStdErr().writer();
try stderr.print("error: unknown command '{s}'\n\n", .{command});
Expand Down Expand Up @@ -108,7 +108,7 @@ fn encodeCmd(allocator: std.mem.Allocator, args: []const []const u8) !void {
}
}

const input_data = readEntireFile(allocator, input_path) catch |err| {
const input_data = kiyo.io.readEntireFile(allocator, input_path) catch |err| {
try stderr.print("error: cannot read '{s}': {}\n", .{ input_path, err });
std.process.exit(1);
};
Expand Down Expand Up @@ -136,7 +136,7 @@ fn encodeCmd(allocator: std.mem.Allocator, args: []const []const u8) !void {
};
defer result.deinit(allocator);

writeEntireFile(output_path, result.buffer) catch |err| {
kiyo.io.writeEntireFile(output_path, result.buffer) catch |err| {
try stderr.print("error: cannot write '{s}': {}\n", .{ output_path, err });
std.process.exit(1);
};
Expand All @@ -158,7 +158,7 @@ fn decodeCmd(allocator: std.mem.Allocator, args: []const []const u8) !void {
const input_path = args[0];
const output_path = args[1];

const jpeg_data = readEntireFile(allocator, input_path) catch |err| {
const jpeg_data = kiyo.io.readEntireFile(allocator, input_path) catch |err| {
try stderr.print("error: cannot read '{s}': {}\n", .{ input_path, err });
std.process.exit(1);
};
Expand All @@ -181,18 +181,6 @@ fn decodeCmd(allocator: std.mem.Allocator, args: []const []const u8) !void {
});
}

fn readEntireFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
return try file.readToEndAlloc(allocator, 100 * 1024 * 1024);
}

fn writeEntireFile(path: []const u8, data: []const u8) !void {
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
try file.writeAll(data);
}

const PixelData = struct {
pixels: []kiyo.types.RGBAPixel,
width: u32,
Expand Down
6 changes: 6 additions & 0 deletions src/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub const presets = @import("presets.zig");
pub const types = @import("types.zig");
pub const io = @import("io.zig");

pub const version = "2.0.0";

// ============================================================================
// Simple High-Level API
// ============================================================================
Expand All @@ -36,6 +38,9 @@ pub fn decodeFromBuffer(allocator: std.mem.Allocator, data: []const u8, options:
/// Encode RGBA pixels to JPEG with a single quality setting.
/// Returns JPEG bytes. Caller must free with allocator.
pub fn encode(allocator: std.mem.Allocator, pixels: []const types.RGBAPixel, width: u32, height: u32, quality: u8) ![]u8 {
// SAFETY: encoder only reads pixels, never writes. constCast is safe here
// because encodeJPEG takes *const ImageData and the pixel data is treated
// as immutable throughout the encode pipeline.
var img = types.ImageData{
.width = width,
.height = height,
Expand All @@ -53,6 +58,7 @@ pub fn decode(allocator: std.mem.Allocator, jpeg_data: []const u8) !types.ImageD

/// Encode RGBA pixels to a JPEG file on disk.
pub fn encodeToFile(allocator: std.mem.Allocator, path: []const u8, pixels: []const types.RGBAPixel, width: u32, height: u32, quality: u8) !void {
// SAFETY: same as encode() — encoder only reads pixels.
var img = types.ImageData{
.width = width,
.height = height,
Expand Down
Loading