aboutsummaryrefslogtreecommitdiffstats
path: root/src/png.c
blob: daf0f2ebdd78038d5663c12db0aab90877724365 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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);
}