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
-
.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:.
- .decode(bytes) ⇒ Object
-
.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.
-
.from_rgb(bytes, width, height) ⇒ Object
Raw 24-bit RGB → RGBA8.
-
.from_rgba(bytes, width, height) ⇒ Object
Raw 32-bit RGBA → RGBA8 (premultiplied).
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 |