Class: Clogs::Image

Inherits:
Drawable
  • Object
show all
Defined in:
lib/clogs/drawables/image.rb

Overview

Bitmaps.

This is the one place where libui really lets Shoes down. libui-ng has uiImage, but the only thing that can display one is a table cell -- the released library exports no uiDrawImage, so there is no way to blit a bitmap into an area. (Clogs checks for it at runtime and uses it if a future build provides one.)

The fallback is the same trick glimmer-dsl-libui uses: turn the image into filled rectangles. Clogs run-length encodes each row first, so flat art -- icons, logos, screenshots of text -- costs a handful of rectangles per row instead of one per pixel. Photographs are genuinely slow and are best avoided until libui grows a draw-image call.

Constant Summary collapse

RUN_BUDGET =

Every rectangle is a separate libui fill, so a photograph can cost tens of thousands of draw calls per frame and starve the event loop. Flat art stays well under this; anything that does not gets sampled at a coarser resolution until it fits, trading sharpness for a UI that still responds. None of this is needed once libui exports uiDrawImage.

40_000

Instance Attribute Summary

Attributes inherited from Drawable

#abs_x, #abs_y, #children, #height, #parent, #shoes_linkable_id, #styles, #width, #x, #y

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Drawable

#add_child, #app, #contains?, #destroy_self, #each_peer, #focus_gained, #focus_lost, #focusable?, for_shoes_class, #hidden?, #initialize, #margin, #needs_layout!, #notify, #on_click, #on_key, #on_mouse_move, #paint, #properties_changed, #redraw!, #remove_child, #requested_height, #requested_width, #set_parent, #style

Constructor Details

This class inherits a constructor from Clogs::Drawable

Class Method Details

.build_runs(url, width, height) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/clogs/drawables/image.rb', line 39

def build_runs(url, width, height)
  pixels, src_w, src_h = load_pixels(url)
  return nil unless pixels

  width = src_w if width.nil? || width <= 0
  height = src_h if height.nil? || height <= 0

  runs = encode(pixels, src_w, src_h, width, height, 1)
  step = 1
  while runs.size > RUN_BUDGET && step < 8
    step *= 2
    runs = encode(pixels, src_w, src_h, width, height, step)
  end
  [runs, width, height]
end

.cacheObject



21
22
23
# File 'lib/clogs/drawables/image.rb', line 21

def cache
  @cache ||= {}
end

.compact(runs, step = 1) ⇒ Object

Rows that repeat identically become one taller rectangle. Flat art collapses dramatically here.



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/clogs/drawables/image.rb', line 80

def compact(runs, step = 1)
  by_key = {}
  result = []
  runs.each do |x, y, w, color|
    key = [x, w, color]
    prev = by_key[key]
    if prev && prev[1] + prev[3] == y
      prev[3] += step
    else
      rect = [x, y, w, step, color]
      result << rect
      by_key[key] = rect
    end
  end
  result
end

.convert_to_png(path) ⇒ Object

chunky_png only reads PNG. Anything else goes through ImageMagick if it happens to be installed.



127
128
129
130
131
132
133
134
# File 'lib/clogs/drawables/image.rb', line 127

def convert_to_png(path)
  require "tmpdir"
  out = File.join(Dir.tmpdir, "clogs-#{File.basename(path)}.png")
  return out if File.exist?(out)
  return nil unless system("which convert > /dev/null 2>&1")

  system("convert", path, out) ? out : nil
end

.download(url) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/clogs/drawables/image.rb', line 145

def download(url)
  require "tmpdir"
  require "digest"
  require "open-uri"
  dest = File.join(Dir.tmpdir, "clogs-img-#{Digest::SHA256.hexdigest(url)[0, 16]}")
  return dest if File.exist?(dest)

  URI.parse(url).open { |io| File.binwrite(dest, io.read) }
  dest
rescue StandardError => e
  warn "Clogs: could not fetch #{url}: #{e.message}"
  nil
end

.encode(pixels, src_w, src_h, width, height, step) ⇒ Object

Walk the target rectangle at step pixels at a time, emitting one rectangle per run of equal colour.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/clogs/drawables/image.rb', line 57

def encode(pixels, src_w, src_h, width, height, step)
  runs = []
  0.step(height - 1, step) do |y|
    sy = src_h == height ? y : (y * src_h / height)
    row_start = sy * src_w
    run_color = nil
    run_x = 0
    0.step(width - 1, step) do |x|
      sx = src_w == width ? x : (x * src_w / width)
      color = pixels[row_start + sx]
      next if color == run_color

      runs << [run_x, y, x - run_x, run_color] if run_color && run_color[3].positive?
      run_color = color
      run_x = x
    end
    runs << [run_x, y, width - run_x, run_color] if run_color && run_color[3].positive?
  end
  compact(runs, step)
end

.load_pixels(url) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/clogs/drawables/image.rb', line 97

def load_pixels(url)
  path = local_path(url)
  return nil unless path && File.exist?(path)

  png = png_canvas(path)
  return nil unless png

  pixels = Array.new(png.width * png.height)
  png.height.times do |y|
    png.width.times do |x|
      v = png[x, y]
      pixels[y * png.width + x] = [(v >> 24) & 0xff, (v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff]
    end
  end
  [pixels, png.width, png.height]
rescue StandardError => e
  warn "Clogs: could not load image #{url.inspect}: #{e.message}"
  nil
end

.local_path(url) ⇒ Object



136
137
138
139
140
141
142
143
# File 'lib/clogs/drawables/image.rb', line 136

def local_path(url)
  return nil if url.nil?

  url = url.to_s
  return url unless url.start_with?("http://", "https://")

  download(url)
end

.png_canvas(path) ⇒ Object



117
118
119
120
121
122
123
# File 'lib/clogs/drawables/image.rb', line 117

def png_canvas(path)
  require "chunky_png"
  return ChunkyPNG::Canvas.from_file(path) if File.extname(path).downcase == ".png"

  converted = convert_to_png(path)
  converted ? ChunkyPNG::Canvas.from_file(converted) : nil
end

.runs_for(url, width, height) ⇒ Object

Load an image and reduce it to per-row colour runs at the requested size. Cached by [path, width, height].



27
28
29
30
# File 'lib/clogs/drawables/image.rb', line 27

def runs_for(url, width, height)
  key = [url, width, height]
  cache[key] ||= build_runs(url, width, height)
end

Instance Method Details

#clickable?Boolean

Returns:

  • (Boolean)


190
191
192
# File 'lib/clogs/drawables/image.rb', line 190

def clickable?
  !style(:click).nil?
end

#draw(painter, x, y) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/clogs/drawables/image.rb', line 177

def draw(painter, x, y)
  unless @runs
    # Nothing loaded: a light placeholder box beats an invisible hole.
    painter.stroke_rect(x + 0.5, y + 0.5, [@width - 1, 1].max, [@height - 1, 1].max,
      [200, 200, 200, 255], thickness: 1)
    return
  end

  @runs.each do |rx, ry, rw, rh, color|
    painter.fill_rect(x + rx, y + ry, rw, rh, color)
  end
end

#measure(available_width) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/clogs/drawables/image.rb', line 164

def measure(available_width)
  req_w = requested_width(available_width)
  req_h = requested_height(available_width)
  loaded = self.class.runs_for(style(:url), req_w, req_h)
  if loaded
    @runs, @width, @height = loaded
  else
    @runs = nil
    @width = req_w || 0
    @height = req_h || 0
  end
end

#on_release(x, y, _button) ⇒ Object



194
195
196
# File 'lib/clogs/drawables/image.rb', line 194

def on_release(x, y, _button)
  notify("click") if contains?(x, y) && style(:click)
end

#positioned?Boolean

Returns:

  • (Boolean)


160
161
162
# File 'lib/clogs/drawables/image.rb', line 160

def positioned?
  !style(:left).nil? && !style(:top).nil?
end