Module: Echoes::SvgRenderer
- Defined in:
- lib/echoes/svg_renderer.rb
Overview
SVG → RGBA8 rasterizer backed by WKWebView. The Kitty graphics and iTerm2 inline-image protocols both detect SVG payloads via SvgSniffer and route here instead of NSBitmapImageRep (which can’t decode vector formats).
WebKit’s snapshot API is async, but the decoders are synchronous. We bridge by pumping a nested NSRunLoop until the completion handler fires, with a hard per-call timeout so a runaway SVG can’t hang the GUI. Re-entrant calls during the pump are short-circuited by a recursion guard.
JavaScript is disabled and baseURL is nil so the SVG sandbox can’t execute scripts or fetch external resources.
Constant Summary collapse
- WEBKIT =
Fiddle.dlopen('/System/Library/Frameworks/WebKit.framework/WebKit')
- LIBSYSTEM =
Fiddle.dlopen('/usr/lib/libSystem.B.dylib')
- COREGRAPHICS =
Fiddle.dlopen('/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics')
- P =
ObjC::P
- D =
ObjC::D
- L =
ObjC::L
- I =
ObjC::I
- V =
ObjC::V
- MSG_PTR_RECT_1 =
‘initWithFrame:configuration:` — CGRect (4 doubles) + id.
ObjC.new_msg([P, P, D, D, D, D, P], P)
- CGImageGetWidth =
CG functions for snapshot pixel-dim lookup.
Fiddle::Function.new(COREGRAPHICS['CGImageGetWidth'], [P], L)
- CGImageGetHeight =
Fiddle::Function.new(COREGRAPHICS['CGImageGetHeight'], [P], L)
- NS_DEFAULT_RUN_LOOP_MODE =
Foundation exports NSDefaultRunLoopMode as an NSString* symbol. Same dereference-the-symbol trick as ObjC.appkit_const, just against Foundation instead of AppKit.
begin sym_ptr = Fiddle::Pointer.new(ObjC::FOUNDATION['NSDefaultRunLoopMode']) Fiddle::Pointer.new(sym_ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J')) end
- NS_CONCRETE_GLOBAL_BLOCK =
Objective-C global block plumbing. Snapshot’s completion handler is ‘void (^)(NSImage *, NSError *)`, which we fabricate via Fiddle by building a Block_layout struct that points at a Fiddle closure. Global blocks live forever and never copy/dispose, so the layout is minimal.
Fiddle::Pointer.new(LIBSYSTEM['_NSConcreteGlobalBlock'])
- BLOCK_HAS_STRET =
not set
1 << 29
- BLOCK_HAS_SIGNATURE =
not set; we omit the type signature
1 << 30
- BLOCK_IS_GLOBAL =
1 << 28
Class Method Summary collapse
-
.rasterize(svg_bytes, width:, height:) ⇒ Object
Returns width:, height: or nil.
Class Method Details
.rasterize(svg_bytes, width:, height:) ⇒ Object
Returns width:, height: or nil. ‘width` and `height` are the target pixel dimensions for the rasterized output; the renderer respects them via the WKWebView frame, but the final pixel count may exceed them on Retina displays (we use whatever the snapshot reports back).
Tries the native CoreGraphics renderer first (fast, synchronous, no WebContent XPC process); falls back to WKWebView when the SVG uses anything outside that subset (text, filters, gradients, etc.) — see SvgCgRenderer for the precise contract.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/echoes/svg_renderer.rb', line 105 def rasterize(svg_bytes, width:, height:) return nil if svg_bytes.nil? || svg_bytes.empty? return nil if width <= 0 || height <= 0 return nil if @rendering # recursion guard — nested runloop can re-enter @rendering = true begin require_relative 'svg_cg_renderer' fast = SvgCgRenderer.rasterize(svg_bytes, width: width, height: height) return fast if fast ensure_setup html = build_html(svg_bytes) ObjC::MSG_VOID_2D.call(@web_view, ObjC.sel('setFrameSize:'), width.to_f, height.to_f) @nav_done = false @nav_error = false @snap_done = false @snap_error = false @snap_image = nil ObjC::MSG_VOID_2.call(@web_view, ObjC.sel('loadHTMLString:baseURL:'), ObjC.nsstring(html), Fiddle::Pointer.new(0)) return nil unless pump_until(2.0) { @nav_done } return nil if @nav_error snap_config = build_snapshot_config(width, height) ObjC::MSG_VOID_2.call(@web_view, ObjC.sel('takeSnapshotWithConfiguration:completionHandler:'), snap_config, @snap_block) return nil unless pump_until(2.0) { @snap_done } return nil if @snap_error || @snap_image.nil? cgimage = ns_image_to_cgimage(@snap_image, width, height) return nil if cgimage.nil? || cgimage.null? w_px = CGImageGetWidth.call(cgimage) h_px = CGImageGetHeight.call(cgimage) return nil if w_px <= 0 || h_px <= 0 KittyGraphics::AppKitPng.cgimage_to_rgba(cgimage, w_px, h_px) rescue StandardError => e warn "echoes SvgRenderer: #{e.class}: #{e.}" nil ensure @rendering = false end end |