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

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.message}"
    nil
  ensure
    @rendering = false
  end
end