aboutsummaryrefslogtreecommitdiffstats
path: root/src/png.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-15 19:14:59 +0200
committeruser <user@clank>2026-08-15 19:14:59 +0200
commit2f0b3426d0f40d2cf53740cb597495af2c45bc2e (patch)
tree1969c511f2ec69e234e4eb8b2ba1aa2bf78c7904 /src/png.c
parentsl0ppty: measure chrome in columns, not characters (diff)
downloadslosh-2f0b3426d0f40d2cf53740cb597495af2c45bc2e.tar.gz
slosh-2f0b3426d0f40d2cf53740cb597495af2c45bc2e.tar.xz
slosh-2f0b3426d0f40d2cf53740cb597495af2c45bc2e.zip
sl0ppty: decode the PNGs programs actually send
Kitty graphics were green in the suite and a real program still drew nothing. libghostty-vt ships no image codec: built as a library it sets its `sys.decode_png` hook to null, and a null hook rejects every `f=100` transmission outright -- no image stored, so no placement exists, so the re-emission layer this project is mostly *about* is never reached. The program gets no error and no picture. Raw RGB worked throughout, which is why it looked done: every test here sent raw pixels, because that is what is easy to write in a test. It is the same lesson the last kitty graphics bug taught -- write the test the way a program actually behaves -- and a program with a picture on disk has a PNG. Found because ida-tui's startup logo never appeared, and its detection probe answers `OK` first, so it fails silently rather than falling back to its block art. PNG is the whole of the gap and cannot grow: the protocol has exactly three formats (`f=24`, `f=32`, `f=100`), the first two are raw pixels lib-vt already handles, and `decode_png` is the only codec hook it has. Decoding only, never encoding. The decoder is the stb_image lib-vt already vendors and we already commit -- upstream compiles that same header with the same `STBI_ONLY_PNG` for this same purpose, it is just not in the `-Demit-lib-vt` build. So nothing new is pulled in; a file we already ship is finally compiled. src/png.c is the hook, a header pre-check and a copy: dimensions are read before decoding because the buffer we return is allocated through lib-vt's limited allocator but stb's own working allocation is plain malloc and answers to nothing. Tests cover a PNG placed at the size it asked for (RGB and RGBA sources, both normalised to RGBA), the client being handed `f=32` rather than the png it was given, and a malformed png placing nothing without taking the pane with it. They fail on the previous binary.
Diffstat (limited to 'src/png.c')
-rw-r--r--src/png.c87
1 files changed, 87 insertions, 0 deletions
diff --git a/src/png.c b/src/png.c
new file mode 100644
index 0000000..daf0f2e
--- /dev/null
+++ b/src/png.c
@@ -0,0 +1,87 @@
+#define _GNU_SOURCE
+#include "png.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+/* stb_image is already in the tree: libghostty-vt vendors it, and upstream
+ * compiles that same header with exactly these defines (see
+ * vendor/libghostty-vt/src/stb/stb.c) to answer exactly this question —
+ * "the only use case we have right now is the Kitty Graphics protocol which
+ * only supports PNG as a format besides raw RGB/RGBA buffers". So this is not
+ * a new dependency; it is a file we already ship and pin, finally compiled.
+ *
+ * ONLY_PNG keeps the other decoders out of the binary, NO_STDIO the file
+ * paths: the bytes always arrive in memory, straight off a pane's pty, so a
+ * decoder that can open files is surface we have no use for. */
+#define STBI_ONLY_PNG
+#define STBI_NO_STDIO
+#define STB_IMAGE_IMPLEMENTATION
+#include "stb_image.h"
+
+#include <ghostty/vt.h>
+
+/* The library refuses anything past this (`max_size` in
+ * kitty/graphics_image.zig), so decoding past it can only ever be work done
+ * to have the result thrown away. Matched rather than tightened: being
+ * stricter here would silently reject images the library would have taken. */
+#define MAX_DECODED (400u * 1024u * 1024u)
+
+static bool decode(void *ud, const GhosttyAllocator *alloc, const uint8_t *data,
+ size_t data_len, GhosttySysImage *out) {
+ if (!alloc || !data || !data_len || !out) return false;
+ if (data_len > INT32_MAX) return false; /* stb counts in int */
+
+ /* Read the header before decoding it. The buffer we return is allocated
+ * through the library's allocator, which is wrapped in a limit — but
+ * stb_image's own working allocation is plain malloc and answers to nothing,
+ * so a header claiming 60000x60000 would have us try for 14GB before anyone
+ * got to refuse it. This costs an IHDR parse instead. */
+ int w = 0, h = 0, channels = 0;
+ if (!stbi_info_from_memory(data, (int)data_len, &w, &h, &channels))
+ return false;
+ if (w <= 0 || h <= 0) return false;
+ if ((uint64_t)w * (uint64_t)h * 4u > MAX_DECODED) return false;
+
+ /* 4 = "give me RGBA whatever the file is", so palette, greyscale and
+ * 16-bit all arrive in the one layout the protocol wants. */
+ int got_w = 0, got_h = 0;
+ stbi_uc *px =
+ stbi_load_from_memory(data, (int)data_len, &got_w, &got_h, &channels, 4);
+ if (!px) return false;
+ if (got_w <= 0 || got_h <= 0) {
+ stbi_image_free(px);
+ return false;
+ }
+
+ /* Sized from what came back, not from what the header promised. */
+ uint64_t len = (uint64_t)got_w * (uint64_t)got_h * 4u;
+ if (len > MAX_DECODED) {
+ stbi_image_free(px);
+ return false;
+ }
+
+ /* Copied into the library's allocator because that is what it frees the
+ * buffer with. Routing stb's own allocation there instead would mean
+ * smuggling a per-call allocator into a global, for one memcpy that is
+ * already far cheaper than the decode that produced it. */
+ uint8_t *buf = ghostty_alloc(alloc, (size_t)len);
+ if (!buf) {
+ stbi_image_free(px);
+ return false;
+ }
+ memcpy(buf, px, (size_t)len);
+ stbi_image_free(px);
+
+ *out = (GhosttySysImage){.width = (uint32_t)got_w,
+ .height = (uint32_t)got_h,
+ .data = buf,
+ .data_len = (size_t)len};
+ return true;
+}
+
+void png_init(void) {
+ ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, (const void *)(uintptr_t)decode);
+}