Module: Kward::PixelLogo

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

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



222
223
224
# File 'lib/kward/resources/pixel_logo.rb', line 222

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

.dominant_color(pixels, x_range, y_range) ⇒ Object



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

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



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

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



218
219
220
# File 'lib/kward/resources/pixel_logo.rb', line 218

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

.half_block_cell(top, bottom) ⇒ Object



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

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



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

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



23
24
25
26
27
# File 'lib/kward/resources/pixel_logo.rb', line 23

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



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
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/kward/resources/pixel_logo.rb', line 36

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



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

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



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

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



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

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



226
227
228
# File 'lib/kward/resources/pixel_logo.rb', line 226

def reset_sgr
  "\e[0m"
end

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



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

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



10
11
12
13
14
# File 'lib/kward/resources/pixel_logo.rb', line 10

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



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

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



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

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



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

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



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

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