class PngCanvas
Constants
- CHARSET
- VERSION
Attributes
canvas[RW]
color[RW]
Public Class Methods
new(width, height, bgcolor: [0xff, 0xff, 0xff, 0xff], color: [0, 0, 0, 0xff])
click to toggle source
# File lib/png_canvas.rb, line 82 def initialize(width, height, bgcolor: [0xff, 0xff, 0xff, 0xff], color: [0, 0, 0, 0xff]) @canvas = [] @width = width @height = height @color = color height.times { @canvas << [bgcolor] * width } end
Public Instance Methods
blend_rectangle(x0, y0, x1, y1, dx, dy, destination_png, alpha = 0xff)
click to toggle source
# File lib/png_canvas.rb, line 138 def blend_rectangle(x0, y0, x1, y1, dx, dy, destination_png, alpha = 0xff) x0, y0, x1, y1 = rectangle_helper(x0, y0, x1, y1) (x0..x1).each do |x| (y0..y1).each do |y| rgba = @canvas[y][x] + [alpha] destination_png.point(dx + x - x0, dy + y - y0, rgba) end end end
copy_rectangle(x0, y0, x1, y1, dx, dy, destination_png)
click to toggle source
# File lib/png_canvas.rb, line 129 def copy_rectangle(x0, y0, x1, y1, dx, dy, destination_png) x0, y0, x1, y1 = rectangle_helper(x0, y0, x1, y1) (x0..x1).each do |x| (y0..y1).each do |y| destination_png.canvas[dy + y - y0][dx + x - x0] = @canvas[y][x] end end end
dump()
click to toggle source
# File lib/png_canvas.rb, line 240 def dump raw_list = [] @height.times do |y| raw_list << 0.chr # filter type 0 (nil) @width.times do |x| raw_list << @canvas[y][x].pack('C3') end end raw_data = raw_list.join # 8-bit image represented as RGB tuples # simple transparency, alpha is pure white [137, 80, 78, 71, 13, 10, 26, 10].pack('C8') + pack_chunk('IHDR', [@width, @height, 8, 2, 0, 0, 0].pack('N2C5')) + pack_chunk('tRNS', [0xff, 0xff, 0xff, 0xff, 0xff, 0xff].pack('C6')) + pack_chunk('IDAT', Zlib::Deflate.deflate(raw_data, 9)) + pack_chunk('IEND', '') end
filled_rectangle(x0, y0, x1, y1)
click to toggle source
# File lib/png_canvas.rb, line 120 def filled_rectangle(x0, y0, x1, y1) x0, y0, x1, y1 = rectangle_helper(x0, y0, x1, y1) (x0..x1).each do |x| (y0..y1).each do |y| point(x, y, @color) end end end
line(x0, y0, x1, y1)
click to toggle source
Draw a line using Xiaolin Wu’s antialiasing technique
# File lib/png_canvas.rb, line 149 def line(x0, y0, x1, y1) # clean params x0, y0, x1, y1 = x0.to_i, y0.to_i, x1.to_i, y1.to_i if y0 > y1 y0, y1, x0, x1 = y1, y0, x1, x0 end dx = x1 - x0 if dx < 0 sx = -1 else sx = 1 end dx *= sx dy = y1 - y0 # 'easy' cases if dy == 0 if sx > 0 ordering = :each else ordering = :reverse_each x0, x1 = x1, x0 end (x0..x1).send(ordering) { |x| point(x, y0) } return end if dx == 0 (y0..y1).each { |y| point(x0, y) } point(x1, y1) return end if dx == dy if sx > 0 ordering = :each else ordering = :reverse_each x0, x1 = x1, x0 end (x0..x1).send(ordering) do |x| point(x, y0) y0 += 1 end return end # main loop point(x0, y0) e_acc = 0 if dy > dx # vertical displacement e = (dx << 16) / dy (y0..y1).each do |i| e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xffff if e_acc <= e_acc_temp x0 += sx end w = 0xff - (e_acc >> 8) point(x0, y0, intensity(@color, w)) y0 += 1 point(x0 + sx, y0, intensity(@color, 0xff - w)) end point(x1, y1) return end # horizontal displacement e = (dy << 16) / dx ordering = sx > 0 ? :each : :reverse_each (x0..x1 - sx).send(ordering) do |i| e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xffff if e_acc <= e_acc_temp y0 += 1 end w = 0xff - (e_acc >> 8) point(x0, y0, intensity(@color, w)) x0 += sx point(x0, y0 + 1, intensity(@color, 0xff - w)) end point(x1, y1) end
load(file_path)
click to toggle source
# File lib/png_canvas.rb, line 265 def load(file_path) file = File.open file_path, 'rb' @canvas = [] file.read(8) # load png header chunks(file) do |tag, data| if tag == 'IHDR' width, height, bitdepth, colortype, compression, filter, interlace = data.unpack('N2C5') @width = width @height = height if [bitdepth, colortype, compression, filter, interlace] != [8, 2, 0, 0, 0] raise 'Unsupported PNG format' end # we ignore tRNS because we use pure white as alpha anyway elsif tag == 'IDAT' raw_data = Zlib::Inflate.inflate(data) prev = nil i = 0 @height.times do |y| filtertype = raw_data[i].ord i = i + 1 cur = raw_data[i..i + @width * 3].unpack 'C*' rgb = defilter(cur, (y == 0 ? nil : prev), filtertype) prev = cur i = i + @width * 3 row = [] j = 0 @width.times do |x| pixel = rgb[j..j + 3] row << pixel j = j + 3 end @canvas << row end end end file.close end
point(x, y, color = nil)
click to toggle source
# File lib/png_canvas.rb, line 90 def point(x, y, color = nil) return if x < 0 || y < 0 || x > (@width - 1) || y > (@height - 1) color = @color if color.nil? @canvas[y][x] = blend(@canvas[y][x], color) end
polyline(point_array)
click to toggle source
# File lib/png_canvas.rb, line 229 def polyline(point_array) (point_array.size - 1).times do |i| line( point_array[i].first, point_array[i].last, point_array[i + 1].first, point_array[i + 1].last ) end end
rectangle(x0, y0, x1, y1)
click to toggle source
# File lib/png_canvas.rb, line 115 def rectangle(x0, y0, x1, y1) x0, y0, x1, y1 = rectangle_helper(x0, y0, x1, y1) polyline([[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]) end
save(file_path)
click to toggle source
# File lib/png_canvas.rb, line 259 def save(file_path) file = File.open file_path, 'wb' file.write dump file.close end
text(x0, y0, string, color = nil)
click to toggle source
# File lib/png_canvas.rb, line 96 def text(x0, y0, string, color = nil) string = string.to_s return if string.empty? x = 0 string.each_char do |char| x += character(x + x0, y0, char, color) end end
vertical_gradient(x0, y0, x1, y1, from_color, to_color)
click to toggle source
# File lib/png_canvas.rb, line 105 def vertical_gradient(x0, y0, x1, y1, from_color, to_color) x0, y0, x1, y1 = rectangle_helper(x0, y0, x1, y1) gradient = gradient_list(from_color, to_color, y1 - y0) (x0..x1).each do |x| (y0..y1).each do |y| point(x, y, gradient[y - y0]) end end end
Private Instance Methods
blend(c1, c2)
click to toggle source
Alpha-blends two colors, using the alpha given by c2
# File lib/png_canvas.rb, line 385 def blend(c1, c2) 3.times.map { |i| c1[i] * (0xff - c2[3]) + c2[i] * c2[3] >> 8 } end
character(x0, y0, char, color)
click to toggle source
# File lib/png_canvas.rb, line 404 def character(x0, y0, char, color) x = 0 if CHARSET[char] CHARSET[char].each do |column| 8.times do |y| y_mask = 2**(y - 7).abs point(x + x0, y + y0, color) if column & y_mask == y_mask end x += 1 end else # any unknown character will appear as white space return 4 # whitespace width end x + 1 # returns char width end
chunks(f) { |tag, data| ... }
click to toggle source
# File lib/png_canvas.rb, line 371 def chunks(f) until f.eof? length = f.read(4).unpack("N")[0] tag = f.read(4) data = f.read(length) crc = f.read(4).unpack("N")[0] raise 'File is corrupted' if Zlib.crc32(tag + data) != crc yield [tag, data] end end
defilter(cur, prev, filtertype, bpp = 3)
click to toggle source
# File lib/png_canvas.rb, line 323 def defilter(cur, prev, filtertype, bpp = 3) if filtertype == 0 # No filter return cur elsif filtertype == 1 # Sub xp = 0 [bpp..cur.size].each do |xc| cur[xc] = (cur[xc] + cur[xp]) % 256 xp += 1 end elsif filtertype == 2 # Up cur.size.times do |xc| cur[xc] = (cur[xc] + prev[xc]) % 256 end elsif filtertype == 3 # Average xp = 0 cur.size.times do |xc| cur[xc] = (cur[xc] + (cur[xp] + prev[xc]) / 2) % 256 xp += 1 end elsif filtertype == 4 # Paeth xp = 0 bpp.times do |i| cur[i] = (cur[i] + prev[i]) % 256 end [bpp..cur.size].each do |xc| a = cur[xp] b = prev[xc] c = prev[xp] p = a + b - c pa = (p - a).abs pb = (p - b).abs pc = (p - c).abs if pa <= pb && pa <= pc value = a elsif pb <= pc value = b else value = c end cur[xc] = (cur[xc] + value) % 256 xp += 1 end else raise 'Unrecognized scanline filter type' end cur end
gradient_list(from, to, steps)
click to toggle source
Calculate gradient colors
# File lib/png_canvas.rb, line 395 def gradient_list(from, to, steps) delta = 4.times.map { |i| to[i] - from[i] } grad = [] (steps + 1).times do |i| grad << 4.times.map { |j| from[j] + delta[j] * i / steps } end grad end
intensity(c, i)
click to toggle source
Calculate a new alpha given a 0—0xff intensity
# File lib/png_canvas.rb, line 390 def intensity(c, i) [c[0], c[1], c[2], (c[3] * i) >> 8] end
pack_chunk(tag, data)
click to toggle source
# File lib/png_canvas.rb, line 307 def pack_chunk(tag, data) to_check = tag + data [data.size].pack('N') + to_check + [Zlib.crc32(to_check)].pack('N') end
rectangle_helper(x0, y0, x1, y1)
click to toggle source
# File lib/png_canvas.rb, line 312 def rectangle_helper(x0, y0, x1, y1) x0, y0, x1, y1 = x0.to_i, y0.to_i, x1.to_i, y1.to_i if x0 > x1 x0, x1 = x1, x0 end if y0 > y1 y0, y1 = y1, y0 end [x0, y0, x1, y1] end