diff options
| author | Gabriel Schneider <[email protected]> | 2026-06-30 13:00:23 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-06-30 15:32:46 -0300 |
| commit | a68564e9fb9fb8be6d997db5e6ad9148fb506d18 (patch) | |
| tree | 0230a359962c2e5e67bc4875050afe8411142c9e /image.zig | |
| parent | 651af1db938bcbaa039d14b65ff6bc184a3d91b4 (diff) | |
| download | pardes-a68564e9fb9fb8be6d997db5e6ad9148fb506d18.tar.gz pardes-a68564e9fb9fb8be6d997db5e6ad9148fb506d18.zip | |
images? lol
Diffstat (limited to 'image.zig')
| -rw-r--r-- | image.zig | 83 |
1 files changed, 83 insertions, 0 deletions
diff --git a/image.zig b/image.zig new file mode 100644 index 00000000..56085f5a --- /dev/null +++ b/image.zig @@ -0,0 +1,83 @@ +// Image-viewer panes — the kitty-graphics side quest, kept out of the main editor +// logic. A pane can carry a `View`; the render loop transmits it once (lazily, +// because transmitting needs the tty writer) and re-places it every frame. +// +// Decoding is done by zstbi (C stb_image) — NOT zigimg, whose pure-Zig decoder +// crashes the Zig 0.16 compiler when analyzed here. We decode to RGBA, downscale +// large images so the transmit stays cheap, then hand the raw pixels to the +// terminal via vaxis (kitty graphics). Call `start` once before any pane loads. +const std = @import("std"); +const vaxis = @import("vaxis"); +const zstbi = @import("zstbi"); + +// Cap the longest side before transmitting: a terminal pane is at most a screenful +// of cells, so multi-thousand-pixel photos just waste decode/transmit time. +const MAX_DIM: u32 = 1280; + +pub const View = struct { + path: []const u8, // gpa-owned absolute path; doubles as the tag label + tried: bool = false, // transmit attempted (success or failure) — try exactly once + handle: ?vaxis.Image = null, // kitty image handle once transmitted (null = pending/failed) + // ponytail: we never vx.freeImage on close, so the terminal-side pixel data + // leaks until app exit. Fine for a viewer; add freeImage in the close paths + // (needs vx + the tty writer there) if you open many large images. +}; + +// Initialize / tear down the stb_image allocator. Call once at startup / exit. +pub fn start(io: std.Io, gpa: std.mem.Allocator) void { + zstbi.init(io, gpa); +} +pub fn stop() void { + zstbi.deinit(); +} + +// Extensions stb_image can decode. Right-clicking one of these — when the terminal +// reports kitty graphics — opens an image pane instead of a text view. +pub fn isImagePath(path: []const u8) bool { + const exts = [_][]const u8{ + ".png", ".jpg", ".jpeg", ".jpe", ".bmp", ".tga", + ".gif", ".psd", ".hdr", ".pic", ".pnm", ".ppm", ".pgm", + }; + for (exts) |ext| { + if (path.len >= ext.len and std.ascii.eqlIgnoreCase(path[path.len - ext.len ..], ext)) return true; + } + return false; +} + +// Decode (RGBA), downscale to MAX_DIM, and transmit to the terminal exactly once. +// Runs from the render loop, which owns the tty writer. On any failure `handle` +// stays null and the pane shows just its tag. +pub fn ensureLoaded(view: *View, vx: *vaxis.Vaxis, gpa: std.mem.Allocator, writer: *std.Io.Writer) void { + if (view.tried) return; + view.tried = true; + var pathbuf: [4096]u8 = undefined; + const pz = std.fmt.bufPrintZ(&pathbuf, "{s}", .{view.path}) catch return; + var img = zstbi.Image.loadFromFile(pz, 4) catch return; // force 4 components = RGBA + defer img.deinit(); + + // downscale the longest side to MAX_DIM (keeping aspect) when it's bigger. + var scaled: ?zstbi.Image = null; + defer if (scaled) |*s| s.deinit(); + const longest = @max(img.width, img.height); + const src: *const zstbi.Image = if (longest > MAX_DIM) blk: { + const nw = @max(1, img.width * MAX_DIM / longest); + const nh = @max(1, img.height * MAX_DIM / longest); + scaled = img.resize(nw, nh); + break :blk &scaled.?; + } else &img; + + // base64-encode the raw RGBA and transmit it (terminal shows the pixels as-is, + // no terminal-side decode); vaxis chunks the payload over the kitty protocol. + const enc = std.base64.standard.Encoder; + const b64 = gpa.alloc(u8, enc.calcSize(src.data.len)) catch return; + defer gpa.free(b64); + _ = enc.encode(b64, src.data); + view.handle = vx.transmitPreEncodedImage(writer, b64, @intCast(src.width), @intCast(src.height), .rgba) catch null; +} + +// Re-place the image in `win`, scaled to fit while keeping aspect ratio. Kitty +// placements aren't persistent (render clears them), so this must run every frame. +pub fn draw(view: *const View, win: vaxis.Window) void { + const h = view.handle orelse return; + h.draw(win, .{ .scale = .contain }) catch {}; +} |
