Module: Echoes::KittyGraphics::AppKitPng

Defined in:
lib/echoes/kitty_graphics_appkit.rb

Overview

AppKit + CoreGraphics-backed PNG decoder, isolated in its own file so headless tests don’t pull CoreGraphics into the process. Decoding takes raw PNG bytes through NSBitmapImageRep → CGImage → CGBitmapContext (RGBA8 layout we control), then reads the bitmap context’s backing buffer as a Ruby string. Format conversion (palette PNG, 16-bit, etc.) is handled inside CoreGraphics so we always get RGBA8 out.

Constant Summary collapse

CG =
Fiddle.dlopen('/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics')
P =
ObjC::P
D =
ObjC::D
L =
ObjC::L
I =
ObjC::I
V =
ObjC::V
ColorSpaceCreateDeviceRGB =
Fiddle::Function.new(
  CG['CGColorSpaceCreateDeviceRGB'], [], P
)
ColorSpaceRelease =
Fiddle::Function.new(
  CG['CGColorSpaceRelease'], [P], V
)
BitmapContextCreate =

CGBitmapContextCreate(data, w, h, bitsPerComp, bytesPerRow, cs, bitmapInfo)

Fiddle::Function.new(
  CG['CGBitmapContextCreate'], [P, L, L, L, L, P, I], P
)
ContextDrawImage =

CGContextDrawImage(ctx, rect, image) — CGRect inlined as 4 doubles

Fiddle::Function.new(
  CG['CGContextDrawImage'], [P, D, D, D, D, P], V
)
ContextRelease =
Fiddle::Function.new(
  CG['CGContextRelease'], [P], V
)
DataProviderCreateWithData =

CGDataProviderCreateWithData(info, data, size, releaseCallback)

Fiddle::Function.new(
  CG['CGDataProviderCreateWithData'], [P, P, L, P], P
)
DataProviderRelease =
Fiddle::Function.new(
  CG['CGDataProviderRelease'], [P], V
)
ImageCreate =

CGImageCreate(w, h, bpc, bpp, bpr, cs, bitmapInfo, provider, decode, interp, intent)

Fiddle::Function.new(
  CG['CGImageCreate'], [L, L, L, L, L, P, I, P, P, I, I], P
)
ImageRelease =
Fiddle::Function.new(
  CG['CGImageRelease'], [P], V
)
ALPHA_NONE =

kCGImageAlphaNone — source RGB has no alpha channel (24bpp)

0
ALPHA_PREMULTIPLIED_LAST =

kCGImageAlphaPremultipliedLast (RGBA byte order, alpha last)

1

Class Method Summary collapse

Class Method Details

.cgimage_to_rgba(cgimage, width, height) ⇒ Object

Draw a CGImage into a fresh premultiplied-RGBA8 bitmap context and return the raw pixel buffer as width:, height:. Shared between the PNG decoder and SvgRenderer (which gets its CGImage from a WKWebView snapshot rather than NSBitmapImageRep). Returns nil if the bitmap context can’t be allocated.

CoreGraphics’ coordinate system has y up; PNGs decode top-down. The renderer expects pixel row 0 at the top, which matches CGContextDrawImage’s natural output here because the bitmap context we created has the same orientation we’ll later read.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/echoes/kitty_graphics_appkit.rb', line 99

def cgimage_to_rgba(cgimage, width, height)
  bytes_per_row = width * 4
  buf = Fiddle::Pointer.malloc(width * height * 4, Fiddle::RUBY_FREE)
  cs  = ColorSpaceCreateDeviceRGB.call
  begin
    ctx = BitmapContextCreate.call(
      buf, width, height, 8, bytes_per_row, cs,
      ALPHA_PREMULTIPLIED_LAST
    )
    return nil if ctx.null?
    begin
      ContextDrawImage.call(ctx, 0.0, 0.0, width.to_f, height.to_f, cgimage)
      rgba = buf.to_str(width * height * 4)
      {rgba: rgba, width: width, height: height}
    ensure
      ContextRelease.call(ctx)
    end
  ensure
    ColorSpaceRelease.call(cs)
  end
end

.decode(bytes) ⇒ Object



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
# File 'lib/echoes/kitty_graphics_appkit.rb', line 63

def decode(bytes)
  return nil if bytes.nil? || bytes.bytesize.zero?

  ns_data = ObjC::MSG_PTR_1L.call(
    ObjC.cls('NSData'), ObjC.sel('dataWithBytes:length:'),
    Fiddle::Pointer[bytes], bytes.bytesize
  )
  return nil if ns_data.null?

  rep = ObjC::MSG_PTR_1.call(
    ObjC.cls('NSBitmapImageRep'),
    ObjC.sel('imageRepWithData:'),
    ns_data
  )
  return nil if rep.null?

  width  = ObjC::MSG_RET_L.call(rep, ObjC.sel('pixelsWide'))
  height = ObjC::MSG_RET_L.call(rep, ObjC.sel('pixelsHigh'))
  return nil if width <= 0 || height <= 0

  cgimage = ObjC::MSG_PTR.call(rep, ObjC.sel('CGImage'))
  return nil if cgimage.null?

  cgimage_to_rgba(cgimage, width, height)
end

.draw_into_rgba(bytes, width, height, bits_per_pixel:, bytes_per_row:, source_bitmap_info:) ⇒ Object

Wrap raw pixel bytes in a CGImage, draw into a fresh premultiplied-RGBA8 CGBitmapContext. We keep a Ruby reference to ‘bytes` so the GC can’t collect it while CG still holds the data-provider pointer.



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/echoes/kitty_graphics_appkit.rb', line 151

def draw_into_rgba(bytes, width, height,
                    bits_per_pixel:, bytes_per_row:, source_bitmap_info:)
  src_ptr  = Fiddle::Pointer[bytes]
  provider = DataProviderCreateWithData.call(nil, src_ptr, bytes.bytesize, nil)
  return nil if provider.null?
  cs = ColorSpaceCreateDeviceRGB.call
  begin
    cgimage = ImageCreate.call(width, height, 8, bits_per_pixel, bytes_per_row,
                               cs, source_bitmap_info, provider, nil, 0, 0)
    return nil if cgimage.null?
    begin
      buf = Fiddle::Pointer.malloc(width * height * 4, Fiddle::RUBY_FREE)
      ctx = BitmapContextCreate.call(buf, width, height, 8, width * 4, cs,
                                     ALPHA_PREMULTIPLIED_LAST)
      return nil if ctx.null?
      begin
        ContextDrawImage.call(ctx, 0.0, 0.0, width.to_f, height.to_f, cgimage)
        rgba = buf.to_str(width * height * 4)
        {rgba: rgba, width: width, height: height}
      ensure
        ContextRelease.call(ctx)
      end
    ensure
      ImageRelease.call(cgimage)
    end
  ensure
    ColorSpaceRelease.call(cs)
    DataProviderRelease.call(provider)
  end
end

.from_rgb(bytes, width, height) ⇒ Object

Raw 24-bit RGB → RGBA8. The wire carries width*height*3 bytes; we wrap them in a CGImage (kCGImageAlphaNone, 24bpp) and draw into a fresh kCGImageAlphaPremultipliedLast context. CG fills the alpha channel with 0xFF for us, so the renderer sees the same RGBA8 shape PNGs produce.



126
127
128
129
130
131
132
133
# File 'lib/echoes/kitty_graphics_appkit.rb', line 126

def from_rgb(bytes, width, height)
  return nil if bytes.nil? || width <= 0 || height <= 0
  return nil if bytes.bytesize != width * height * 3
  draw_into_rgba(bytes, width, height,
                 bits_per_pixel: 24,
                 bytes_per_row:  width * 3,
                 source_bitmap_info: ALPHA_NONE)
end

.from_rgba(bytes, width, height) ⇒ Object

Raw 32-bit RGBA → RGBA8 (premultiplied). The wire carries width*height*4 bytes; treated as alpha-premultiplied to match the PNG path’s output exactly.



138
139
140
141
142
143
144
145
# File 'lib/echoes/kitty_graphics_appkit.rb', line 138

def from_rgba(bytes, width, height)
  return nil if bytes.nil? || width <= 0 || height <= 0
  return nil if bytes.bytesize != width * height * 4
  draw_into_rgba(bytes, width, height,
                 bits_per_pixel: 32,
                 bytes_per_row:  width * 4,
                 source_bitmap_info: ALPHA_PREMULTIPLIED_LAST)
end