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
- .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
.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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# 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? 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 # 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 from. 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 |
.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.
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/echoes/kitty_graphics_appkit.rb', line 142 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.
117 118 119 120 121 122 123 124 |
# File 'lib/echoes/kitty_graphics_appkit.rb', line 117 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.
129 130 131 132 133 134 135 136 |
# File 'lib/echoes/kitty_graphics_appkit.rb', line 129 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 |