From 401a22db51261350853aace568dd48533a09d9d6 Mon Sep 17 00:00:00 2001 From: pavanscales Date: Sun, 26 Jul 2026 10:55:10 +0530 Subject: [PATCH 1/2] clean state: kiyo jpeg encoder/decoder with cli and zig api --- README.md | 294 ++++++++++++++++---------------------- build.zig | 31 ++-- src/decoder.zig | 8 +- src/encoder.zig | 7 + src/integration_tests.zig | 33 ----- src/io.zig | 13 +- src/main.zig | 20 +-- src/root.zig | 6 + 8 files changed, 176 insertions(+), 236 deletions(-) diff --git a/README.md b/README.md index dc96bc0..85c4333 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,138 @@ # kiyo -Pure Zig JPEG encoder/decoder. Zero dependencies. +pure zig jpeg encoder/decoder. zero dependencies. + +## why + +jpeg is everywhere. this is a self-contained zig implementation you +can drop into any project without pulling in c libraries. + +## build ``` zig build -Doptimize=ReleaseFast ``` ---- +this builds the library. also available: -## Quick Start +``` +zig build cli build the cli tool +zig build test run unit tests +zig build bench run benchmarks +zig build verify end-to-end validation +zig build fmt format source files +``` -### CLI +## cli ``` -zig build run -- encode input.ppm output.jpg --quality 85 -zig build run -- encode input.ppm output.jpg --preset web -zig build run -- decode input.jpg output.ppm +kiyo encode input.ppm output.jpg --quality 85 +kiyo decode input.jpg output.ppm +kiyo help ``` -Input/output uses PPM (P6) format. Convert with ImageMagick or ffmpeg: +input/output is ppm (p6). convert with imagemagick or ffmpeg: ``` -convert photo.png photo.ppm # ImageMagick +convert photo.png photo.ppm # imagemagick ffmpeg -i photo.png photo.ppm # ffmpeg ``` -### Zig API +encode options: + +| flag | description | +|---|---| +| `--quality <1-100` | jpeg quality (default: 75) | +| `--fast` | fast mode (lower quality, higher speed) | +| `--preset YCbCr - block_processor.zig 8x8 block splitting - dct.zig DCT / IDCT / fast DCT - quantization.zig quantize / dequantize - zigzag.zig zigzag reorder + RLE - - encoding/ - bitstream.zig bit-level writer - huffman.zig Huffman tables + encode - jpeg_writer.zig JPEG marker assembly - - integration_tests.zig 227 tests - -test/ - benchmark.zig benchmarks - bench_pro.zig advanced benchmarks - verify.zig end-to-end validation - fuzz_encoder.zig encoder fuzz testing - fuzz_decoder.zig decoder fuzz testing -``` - ---- - -## Encode - -``` - RGBA pixels - | - v - +--------------+ - | RGB to YCbCr | 256-entry LUT, no f64 math - +--------------+ - | - v - +--------------+ - | Block Split | image into 8x8 blocks - +--------------+ - | - v - +--------------+ - | DCT | standard or fast (17x) - +--------------+ - | - v - +--------------+ - | Quantize | multiply by 1/q (no division) - +--------------+ - | - v - +--------------+ - | Zigzag + RLE | i16 path (no f64) - +--------------+ - | - v - +--------------+ - | Huffman | precomputed tables - +--------------+ - | - v - +--------------+ - | JPEG Write | markers + stuffed data - +--------------+ - | - v - .jpg file -``` -## Decode +// encode to file +try kiyo.encodeToFile(allocator, "out.jpg", pixels, width, height, 85); -``` - .jpg file - | - v - +--------------+ - | Parse Header | SOI, DQT, SOF0, DHT, SOS - +--------------+ - | - v - +--------------+ - | Huffman Dec | bit reader + lookup - +--------------+ - | - v - +--------------+ - | Dequantize | scale back - +--------------+ - | - v - +--------------+ - | IDCT | frequency to spatial - +--------------+ - | - v - +--------------+ - | YCbCr to RGB | color reconversion - +--------------+ - | - v - RGBA pixels +// decode from file +const image = try kiyo.decodeFromFile(allocator, "photo.jpg"); +defer image.deinit(allocator); ``` ---- - -## Chroma Subsampling +### full api + +| function | returns | description | +|---|---|---| +| `encode(alloc, pixels, w, h, quality)` | `[]u8` | encode rgba to jpeg bytes | +| `decode(alloc, jpeg_data)` | `ImageData` | decode jpeg bytes to rgba | +| `encodeToFile(alloc, path, pixels, w, h, q)` | `void` | encode rgba to jpeg file | +| `decodeFromFile(alloc, path)` | `ImageData` | decode jpeg file to rgba | +| `encodeToBuffer(alloc, image, options)` | `[]u8` | encode with full options | +| `decodeFromBuffer(alloc, data, options)` | `ImageData` | decode with full options | + +## encode pipeline + +``` +rgba pixels + | + v +rgb to ycbcr 256-entry lut, no f64 math + | + v +block split image into 8x8 blocks + | + v +dct standard or fast (17x) + | + v +quantize multiply by 1/q (no division) + | + v +zigzag + rle i16 path (no f64) + | + v +huffman precomputed tables + | + v +jpeg write markers + stuffed data + | + v +.jpg file +``` + +## decode pipeline + +``` +.jpg file + | + v +parse header soi, dqt, sof0, dht, sos + | + v +huffman dec bit reader + lookup + | + v +dequantize scale back + | + v +idct frequency to spatial + | + v +ycbcr to rgb color reconversion + | + v +rgba pixels +``` + +## chroma subsampling ``` 4:4:4 4:2:0 @@ -175,44 +149,26 @@ test/ Cr = 1 block per Y block Cr = 1 block per 4 Y blocks ``` ---- - -## Benchmarks - -``` -DCT standard 2.8 us/block -DCT fast 0.2 us/block 17.2x faster -Quantize 0.1 us/block -Color convert 1.5 ms / 256x256 -Full encode 19 ms / 256x256 3.3 Mpixels/s -``` - -## Presets - -``` -name quality fast ---------------------------- -thumbnail 30 yes -web 60 no -balanced 75 no -high 90 no -maximum 100 no -``` - ---- +## presets -## Testing +| name | quality | fast | +|---|---|---| +| thumbnail | 30 | yes | +| web | 60 | no | +| balanced | 75 | no | +| high | 90 | no | +| maximum | 100 | no | -``` -zig build test unit tests -zig build bench benchmarks -zig build verify end-to-end validation -zig build build CLI -zig build run -- help CLI usage -``` +## benchmarks ---- +| operation | time | speedup | +|---|---|---| +| dct standard | 2.8 us/block | - | +| dct fast | 0.2 us/block | 17.2x | +| quantize | 0.1 us/block | - | +| color convert | 1.5 ms / 256x256 | - | +| full encode | 19 ms / 256x256 | 3.3 mpixels/s | -## License +## license -MIT +mit diff --git a/build.zig b/build.zig index 26c0789..372345c 100644 --- a/build.zig +++ b/build.zig @@ -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", @@ -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, @@ -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"); @@ -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"); @@ -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); @@ -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); @@ -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); } diff --git a/src/decoder.zig b/src/decoder.zig index 9056da0..3c4d7ac 100644 --- a/src/decoder.zig +++ b/src/decoder.zig @@ -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; diff --git a/src/encoder.zig b/src/encoder.zig index 659ad33..8c3c84a 100644 --- a/src/encoder.zig +++ b/src/encoder.zig @@ -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, diff --git a/src/integration_tests.zig b/src/integration_tests.zig index 85cc7c2..92cfe51 100644 --- a/src/integration_tests.zig +++ b/src/integration_tests.zig @@ -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); diff --git a/src/io.zig b/src/io.zig index 1bb4c47..29b9c09 100644 --- a/src/io.zig +++ b/src/io.zig @@ -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 { @@ -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, diff --git a/src/main.zig b/src/main.zig index 49fd310..ce8295a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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}); @@ -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); }; @@ -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); }; @@ -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); }; @@ -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, diff --git a/src/root.zig b/src/root.zig index 2cb6177..574f3af 100644 --- a/src/root.zig +++ b/src/root.zig @@ -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 // ============================================================================ @@ -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, @@ -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, From d3f76095eca7d53171e66064a5d639492695f7d6 Mon Sep 17 00:00:00 2001 From: pavanscales Date: Sun, 26 Jul 2026 10:59:52 +0530 Subject: [PATCH 2/2] readme: explain what kiyo does, api, pipeline, benchmark --- README.md | 246 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 85c4333..52981a2 100644 --- a/README.md +++ b/README.md @@ -1,173 +1,237 @@ # kiyo -pure zig jpeg encoder/decoder. zero dependencies. +pure zig jpeg encoder + decoder. zero c dependencies. -## why - -jpeg is everywhere. this is a self-contained zig implementation you -can drop into any project without pulling in c libraries. +reads and writes baseline jpeg (sof0). no libjpeg, no c libraries, just zig. ## build ``` -zig build -Doptimize=ReleaseFast -``` - -this builds the library. also available: - -``` -zig build cli build the cli tool -zig build test run unit tests -zig build bench run benchmarks -zig build verify end-to-end validation -zig build fmt format source files +zig build lib # build library (default) +zig build cli # build cli tool +zig build test # run unit tests +zig build bench # run benchmarks +zig build verify # end-to-end roundtrip +zig build fmt # format source +zig build run -- ... # run cli directly ``` ## cli +input/output is ppm (p6). you can pipe through imagemagick or ffmpeg. + ``` -kiyo encode input.ppm output.jpg --quality 85 -kiyo decode input.jpg output.ppm -kiyo help +zig build run -- encode input.ppm output.jpg --quality 85 +zig build run -- decode input.jpg output.ppm +zig build run -- help ``` -input/output is ppm (p6). convert with imagemagick or ffmpeg: +options for encode: ``` -convert photo.png photo.ppm # imagemagick -ffmpeg -i photo.png photo.ppm # ffmpeg +--quality <1-100> jpeg quality (default 75) +--fast lower quality, faster encode +--preset thumbnail / web / balanced / high / maximum ``` -encode options: - -| flag | description | -|---|---| -| `--quality <1-100` | jpeg quality (default: 75) | -| `--fast` | fast mode (lower quality, higher speed) | -| `--preset jpeg bytes | +| `decode(a, jpeg)` | `ImageData` | jpeg bytes -> rgba pixels | +| `encodeToFile(a, path, px, w, h, q)` | `void` | rgba -> jpeg file | +| `decodeFromFile(a, path)` | `ImageData` | jpeg file -> rgba | +| `encodeToBuffer(a, image, opts)` | `[]u8` | with EncodeOptions | +| `decodeFromBuffer(a, data, opts)` | `ImageData` | with DecodeOptions | + +### types + +**ImageData** contains `.width`, `.height`, `.pixels` (`[]RGBAPixel`). +**RGBAPixel** has `.r`, `.g`, `.b`, `.a` (all u8, alpha defaults to 255). +**JPEGData** contains `.buffer`, `.width`, `.height`, `.quality`. call `.deinit(a)`. + +**EncodeOptions** fields: +- `quality: u8 = 75` +- `fast_mode: bool = false` — uses faster but lower quality dct +- `subsample: bool = false` — 4:2:0 chroma subsampling (only if >=16x16) +- `preset: ?[]const u8 = null` — named preset overrides quality/fast_mode +- `on_progress: ?*const fn (f32, []const u8) void = null` +- `thread_pool: ?*ThreadPool = null` — supply external thread pool +- `workspace: ?*EncoderWorkspace = null` — reusable scratch buffers + +**DecodeOptions** fields: +- `output_format: union { .rgba, .rgb } = .rgba` — rgb skips alpha write +- `workspace: ?*DecoderWorkspace = null` +- `thread_pool: ?*ThreadPool = null` + +### presets + +named presets you can pass to --preset or EncodeOptions.preset: + +| name | quality | fast | +|---|---|---| +| thumbnail | 30 | yes | +| web | 60 | no | +| balanced | 75 | no | +| high | 90 | no | +| maximum | 100 | no | + +## how it works -## encode pipeline +### encode ``` rgba pixels | v -rgb to ycbcr 256-entry lut, no f64 math +rgb -> ycbcr precomputed 256-entry lut for each channel. + no floating-point multiplies per pixel. + runs in parallel over the image. | v -block split image into 8x8 blocks +8x8 blocks image split into overlapping tiles. | v -dct standard or fast (17x) +dct standard (f32 simd) or fast (row-column llm). + fused with quantization and zigzag into i16. | v -quantize multiply by 1/q (no division) +quantize multiply dct coefficients by 1/q. no division. + quality controls the quantization matrix scale. | v -zigzag + rle i16 path (no f64) +zigzag + rle reorder 8x8 block into 64-element zigzag sequence. + run-length encode trailing zeros. | v -huffman precomputed tables +huffman precomputed dc/ac lookup tables. + encodes symbol + magnitude in one pass. | v -jpeg write markers + stuffed data +markers + bitstream + soi, app0, dqt (lum+chr), sof0, dht (dc+ac for + lum+chr), sos, then compressed entropy data, eoi. | v -.jpg file +.jpg ``` -## decode pipeline +optionally with chroma subsampling (4:2:0), the cb/cr planes are +averaged over 2x2 blocks before encoding. halves color resolution +with minimal visual difference. + +### decode ``` -.jpg file +.jpg | v -parse header soi, dqt, sof0, dht, sos +parse markers soi, dqt, sof0, dht, sos. builds huffman decode + tables with 9-bit fast lookup prefixes. | v -huffman dec bit reader + lookup +huffman decode bit reader handling 0xff byte stuffing. + lookup-table accelerated symbol decode. | v -dequantize scale back +dequantize multiply coefficients by quantization table values. | v -idct frequency to spatial +idct fixed-point i16 inverse dct -> u8 pixel values. | v -ycbcr to rgb color reconversion +ycbcr -> rgb upsample chroma planes if subsampled, + convert to rgba with standard bt.601 coefficients. | v rgba pixels ``` -## chroma subsampling +supports baseline sequential jpeg: 1 component (grayscale) or +3 component (ycbcr), 4:4:4 and 4:2:0 sampling, restart markers. -``` - 4:4:4 4:2:0 - - +------+------+------+------+ +------+------+------+------+ - | Y | Y | Y | Y | | Y | Y | Y | Y | - +------+------+------+------+ | Y | Y | Y | Y | - | Y | Y | Y | Y | +------+------+------+------+ - +------+------+------+------+ | Y | Y | Y | Y | - | Y | Y | Y | Y | | Y | Y | Y | Y | - +------+------+------+------+ +------+------+------+------+ - - Cb = 1 block per Y block Cb = 1 block per 4 Y blocks - Cr = 1 block per Y block Cr = 1 block per 4 Y blocks -``` +## benchmarks -## presets +numbers from a ~3ghz x86-64 system (zig 0.14, releasefast): -| name | quality | fast | +| operation | time | notes | |---|---|---| -| thumbnail | 30 | yes | -| web | 60 | no | -| balanced | 75 | no | -| high | 90 | no | -| maximum | 100 | no | +| color conversion | 1.5 ms | 256x256 rgba -> soa | +| dct (fast, fused) | 0.2 us/block | dct + quantize + zigzag | +| dct (standard) | 2.8 us/block | full precision | +| quantize | 0.1 us/block | multiply by 1/q | +| full encode | 19 ms | 256x256 -> ~30kb jpeg | +| full decode | ~12 ms | 256x256 jpeg -> rgba | -## benchmarks +## project layout -| operation | time | speedup | -|---|---|---| -| dct standard | 2.8 us/block | - | -| dct fast | 0.2 us/block | 17.2x | -| quantize | 0.1 us/block | - | -| color convert | 1.5 ms / 256x256 | - | -| full encode | 19 ms / 256x256 | 3.3 mpixels/s | +``` +build.zig zig build definitions +src/ + root.zig public api + main.zig cli entry point + encoder.zig jpeg encoder (fused pipeline) + decoder.zig jpeg decoder (marker parsing + entropy) + types.zig pixel/block/option types + presets.zig quality presets + io.zig ppm file i/o + atomic writes + integration_tests.zig round-trip tests (200+) + core/ + dct.zig dct, idct, fused dct+quantize+zigzag + quantization.zig quantization matrices, quality scaling + color_space.zig rgb<->ycbcr with lut + simd + block_processor.zig image -> 8x8 blocks + zigzag.zig zigzag order tables + encoding/ + bitstream.zig bit-level writer + huffman.zig huffman tables + encode/decode + jpeg_writer.zig jpeg marker assembly +test/ + benchmark.zig benchmarks + bench_pro.zig extended benchmarks + verify.zig disk round-trip verification + fuzz_encoder.zig encoder fuzzer + fuzz_decoder.zig decoder fuzzer +``` + +## limits + +- baseline sequential jpeg only (no progressive, no lossless) +- 8-bit precision only +- ppm (p6) for cli input/output +- max image dimensions: 65535 x 65535 +- 1 or 3 color components +- huffman tables are standard (not optimized per image) ## license