Module: Kward::PixelLogo

Defined in:
lib/kward/resources/pixel_logo.rb

Overview

Pixel-art logo data and rendering helpers.

Constant Summary collapse

PNG_SIGNATURE =
"\x89PNG\r\n\x1a\n".b.freeze
TRANSPARENT_ALPHA =
128

Class Method Summary collapse

Class Method Details

.background_sgr(color) ⇒ Object



224
225
226
# File 'lib/kward/resources/pixel_logo.rb', line 224

def background_sgr(color)
  color ? "\e[48;2;#{color.join(";")}m" : "\e[49m"
end

.dominant_color(pixels, x_range, y_range) ⇒ Object



149
150
151
152
153
154
155
156
157
# File 'lib/kward/resources/pixel_logo.rb', line 149

def dominant_color(pixels, x_range, y_range)
  counts = Hash.new(0)
  y_range.each do |y|
    x_range.each do |x|
      counts[visible_color(pixels[y][x])] += 1
    end
  end
  counts.max_by { |_color, count| count }&.first
end

.each_chunk(data) ⇒ Object



76
77
78
79
80
81
82
83
84
85
# File 'lib/kward/resources/pixel_logo.rb', line 76

def each_chunk(data)
  offset = PNG_SIGNATURE.bytesize
  while offset < data.bytesize
    length = data[offset, 4].unpack1("N")
    type = data[offset + 4, 4]
    chunk = data[offset + 8, length]
    yield type, chunk
    offset += length + 12
  end
end

.foreground_sgr(color) ⇒ Object



220
221
222
# File 'lib/kward/resources/pixel_logo.rb', line 220

def foreground_sgr(color)
  color ? "\e[38;2;#{color.join(";")}m" : "\e[39m"
end

.half_block_cell(top, bottom) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
# File 'lib/kward/resources/pixel_logo.rb', line 208

def half_block_cell(top, bottom)
  if top && bottom
    [top, bottom, ""]
  elsif top
    [top, nil, ""]
  elsif bottom
    [bottom, nil, ""]
  else
    [nil, nil, " "]
  end
end

.half_block_rows_from_pixels(pixels, width:, pixel_height:) ⇒ Object



31
32
33
34
35
36
# File 'lib/kward/resources/pixel_logo.rb', line 31

def half_block_rows_from_pixels(pixels, width:, pixel_height:)
  scaled = scale_pixels(pixels, width: width, height: pixel_height)
  render_half_block_rows(scaled)
rescue StandardError
  []
end

.half_block_rows_from_png(path, width:, pixel_height:) ⇒ Object



25
26
27
28
29
# File 'lib/kward/resources/pixel_logo.rb', line 25

def half_block_rows_from_png(path, width:, pixel_height:)
  half_block_rows_from_pixels(indexed_png_pixels(path), width: width, pixel_height: pixel_height)
rescue StandardError
  []
end

.indexed_png_pixels(path) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/kward/resources/pixel_logo.rb', line 38

def indexed_png_pixels(path)
  data = File.binread(path)
  raise "Invalid PNG" unless data.start_with?(PNG_SIGNATURE)

  png_width = nil
  png_height = nil
  bit_depth = nil
  color_type = nil
  interlace = nil
  palette = nil
  transparency = []
  idat = +"".b

  each_chunk(data) do |type, chunk|
    case type
    when "IHDR"
      png_width, png_height, bit_depth, color_type, _compression, _filter, interlace = chunk.unpack("NNCCCCC")
    when "PLTE"
      palette = chunk.bytes.each_slice(3).map { |red, green, blue| [red, green, blue, 255] }
    when "tRNS"
      transparency = chunk.bytes
    when "IDAT"
      idat << chunk
    end
  end

  raise "Unsupported PNG" unless png_width && png_height && palette
  raise "Unsupported PNG" unless bit_depth == 8 && color_type == 3 && interlace == 0

  transparency.each_with_index do |alpha, index|
    palette[index] = palette[index][0, 3] + [alpha] if palette[index]
  end

  unfilter_indexed_rows(Zlib::Inflate.inflate(idat), png_width, png_height).map do |row|
    row.map { |index| palette[index] || [0, 0, 0, 0] }
  end
end

.paeth(left, up, up_left) ⇒ Object



123
124
125
126
127
128
129
130
131
132
# File 'lib/kward/resources/pixel_logo.rb', line 123

def paeth(left, up, up_left)
  estimate = left + up - up_left
  left_distance = (estimate - left).abs
  up_distance = (estimate - up).abs
  up_left_distance = (estimate - up_left).abs
  return left if left_distance <= up_distance && left_distance <= up_left_distance
  return up if up_distance <= up_left_distance

  up_left
end

.render_half_block_rows(rows) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/kward/resources/pixel_logo.rb', line 184

def render_half_block_rows(rows)
  rows.each_slice(2).map do |top_row, bottom_row|
    bottom_row ||= Array.new(top_row.length)
    current_foreground = nil
    current_background = nil
    rendered = +""
    top_row.each_with_index do |top, index|
      bottom = bottom_row[index]
      foreground, background, glyph = half_block_cell(top, bottom)
      if background != current_background
        rendered << background_sgr(background)
        current_background = background
      end
      if foreground != current_foreground
        rendered << foreground_sgr(foreground)
        current_foreground = foreground
      end
      rendered << glyph
    end
    rendered << reset_sgr if current_foreground || current_background
    rendered
  end
end

.render_rows(rows) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/kward/resources/pixel_logo.rb', line 168

def render_rows(rows)
  rows.map do |row|
    current = nil
    rendered = +""
    row.each do |color|
      if color != current
        rendered << background_sgr(color)
        current = color
      end
      rendered << " "
    end
    rendered << reset_sgr if current
    rendered
  end
end

.reset_sgrObject



228
229
230
# File 'lib/kward/resources/pixel_logo.rb', line 228

def reset_sgr
  "\e[0m"
end

.rows_from_pixels(pixels, width:, height:) ⇒ Object



18
19
20
21
22
23
# File 'lib/kward/resources/pixel_logo.rb', line 18

def rows_from_pixels(pixels, width:, height:)
  scaled = scale_pixels(pixels, width: width, height: height)
  render_rows(scaled)
rescue StandardError
  []
end

.rows_from_png(path, width:, height:) ⇒ Object



12
13
14
15
16
# File 'lib/kward/resources/pixel_logo.rb', line 12

def rows_from_png(path, width:, height:)
  rows_from_pixels(indexed_png_pixels(path), width: width, height: height)
rescue StandardError
  []
end

.scale_pixels(pixels, width:, height:) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/kward/resources/pixel_logo.rb', line 134

def scale_pixels(pixels, width:, height:)
  source_height = pixels.length
  source_width = pixels.first.length

  height.times.map do |target_y|
    source_y_start = target_y * source_height / height
    source_y_end = [(target_y + 1) * source_height / height, source_y_start + 1].max
    width.times.map do |target_x|
      source_x_start = target_x * source_width / width
      source_x_end = [(target_x + 1) * source_width / width, source_x_start + 1].max
      dominant_color(pixels, source_x_start...source_x_end, source_y_start...source_y_end)
    end
  end
end

.unfilter_indexed_rows(raw, width, height) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/kward/resources/pixel_logo.rb', line 87

def unfilter_indexed_rows(raw, width, height)
  rows = []
  previous = Array.new(width, 0)
  offset = 0

  height.times do
    filter = raw.getbyte(offset)
    offset += 1
    scanline = raw.byteslice(offset, width).bytes
    offset += width
    row = unfilter_scanline(scanline, previous, filter)
    rows << row
    previous = row
  end

  rows
end

.unfilter_scanline(scanline, previous, filter) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/kward/resources/pixel_logo.rb', line 105

def unfilter_scanline(scanline, previous, filter)
  row = []
  scanline.each_with_index do |byte, index|
    left = index.zero? ? 0 : row[index - 1]
    up = previous[index] || 0
    up_left = index.zero? ? 0 : previous[index - 1]
    row << case filter
           when 0 then byte
           when 1 then (byte + left) & 0xff
           when 2 then (byte + up) & 0xff
           when 3 then (byte + ((left + up) / 2)) & 0xff
           when 4 then (byte + paeth(left, up, up_left)) & 0xff
           else byte
           end
  end
  row
end

.visible_color(color) ⇒ Object



159
160
161
162
163
164
165
166
# File 'lib/kward/resources/pixel_logo.rb', line 159

def visible_color(color)
  return nil unless color

  red, green, blue, alpha = color
  return nil if !alpha.nil? && alpha.to_i < TRANSPARENT_ALPHA

  [red, green, blue]
end