Module: Echoes::SvgSniffer

Defined in:
lib/echoes/svg_sniffer.rb

Overview

Detect SVG payloads in arbitrary image bytes. Used by the Kitty graphics and iTerm2 inline-image decoders to fork off to the SvgRenderer instead of NSBitmapImageRep (which is raster-only).

Constant Summary collapse

SVG_RE =

Skip optional UTF-8 BOM, whitespace, an XML prologue, and a DOCTYPE before the ‘<svg` element. Case-insensitive so `<SVG` and `<Svg` both match. `n` flag forces ASCII-8BIT so we can match against the binary payloads the decoders hand us.

/\A(?:\xEF\xBB\xBF)?\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE\s+svg[^>]*>\s*)?<svg\b/imn
OPEN_SVG_RE =

Capture the leading ‘<svg …>` opening tag so intrinsic_size can work on just the attribute soup, ignoring the rest of the document.

/<svg\b([^>]*)>/imn
DIM_RE =

Numeric attribute on the SVG root, optionally suffixed with a CSS length unit. We extract the number; unit conversion is best-effort (px / pt / unitless treated as pixels; em / % yield nil because they need layout context we don’t have).

/\A\s*([0-9]*\.?[0-9]+)\s*([a-z%]*)\s*\z/i

Class Method Summary collapse

Class Method Details

.extract_attr(attrs, name) ⇒ Object



62
63
64
65
# File 'lib/echoes/svg_sniffer.rb', line 62

def extract_attr(attrs, name)
  m = attrs.match(/\s#{name}\s*=\s*["']([^"']*)["']/i)
  m && m[1]
end

.intrinsic_size(bytes) ⇒ Object

Returns [width_px, height_px] or nil. Parses the <svg> tag’s width/height attributes first; falls back to viewBox dimensions if either is missing or non-pixel.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/echoes/svg_sniffer.rb', line 35

def intrinsic_size(bytes)
  return nil if bytes.nil? || bytes.empty?
  head = bytes.byteslice(0, 4096).b
  m = OPEN_SVG_RE.match(head)
  return nil unless m
  attrs = m[1].to_s

  w = parse_dim(extract_attr(attrs, 'width'))
  h = parse_dim(extract_attr(attrs, 'height'))

  if w.nil? || h.nil?
    vb = extract_attr(attrs, 'viewBox')
    if vb
      parts = vb.strip.split(/[\s,]+/)
      if parts.size == 4
        vw = parts[2].to_f
        vh = parts[3].to_f
        w ||= vw if vw.positive?
        h ||= vh if vh.positive?
      end
    end
  end

  return nil unless w && h && w.positive? && h.positive?
  [w.round, h.round]
end

.parse_dim(str) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/echoes/svg_sniffer.rb', line 67

def parse_dim(str)
  return nil if str.nil? || str.empty?
  m = DIM_RE.match(str)
  return nil unless m
  unit = m[2].downcase
  # Unitless / px / pt are treated as pixels (pt is technically
  # 1.333px but the difference is invisible at typical SVG sizes
  # and we'd be guessing the renderer's DPI anyway). em / % need
  # layout context, so we punt.
  return nil if unit == 'em' || unit == '%'
  n = m[1].to_f
  n.positive? ? n : nil
end

.svg?(bytes) ⇒ Boolean

Returns:

  • (Boolean)


26
27
28
29
30
# File 'lib/echoes/svg_sniffer.rb', line 26

def svg?(bytes)
  return false if bytes.nil? || bytes.empty?
  head = bytes.byteslice(0, 4096).b
  SVG_RE.match?(head)
end