=begin = Description Can read/write a PNG file, if it is 8bpp with a palette (optionnally with the 1st entry transparent) Unsupported: * Adam7 interlaced * non-paletted * no 8bpp * transparency other that only 1st palette fully transparent = Usage: (({ require 'libpng' # print debug messages during loading $DEBUG = true img = Png.read('picture.png') # read a pixel color if img.px(30, 60) == [255, 0, 0] puts "oh, it's red !" end # change a palette entry img.palette[4] = [0, 255, 0] # change a pixel img.lines[50][img.width-2] = 4 # save the result img.write('mypicture.png') })) = Methods --- Png.new(string, offset=0) Reads a png stream from string, starting at offset. Raises RuntimeError on error (invalid/unsupported png feature). --- Png.px(x, y) Returns the [r, g, b] array for the specified pixel, or nil if it is transparent. Raises RuntimeError on error (invalid x/y/palette entry) --- Png.write(filename, overwrite = false) Writes the current image to a new file --- Png#read(filename) Reads a PNG file = Author Yoann Guillot, 2005 =end require 'zlib' class Png def Png.read(file) Png.new(File.read(file)) end attr_reader :width, :height, :palette, :lines attr_accessor :trans def rd(raw, len) @off += len raw[(@off-len)...(@off)] end def decodeint(txt) len = 0 txt.split(//).map{ |i| len <<= 8 ; len += i[0] } len end private :decodeint, :rd def initialize(raw, off = 0) @off = off sig = rd(raw, 8).split(//).map{ |i| i[0] } if sig != [137, 80, 78, 71, 13, 10, 26, 10] raise "Invalid signature: #{raw[off...(off+8)].inspect}" end compresseddata = '' loop do len = rd(raw, 4) raise "Premature end of file" if not len len = decodeint(len) chunktype = rd(raw, 4) chunk = rd(raw, len) crc = decodeint(rd(raw, 4)) puts "Chunk #{chunktype}: #{len}o" if $DEBUG raise "Bad crc %.8X for #{chunktype}" % crc if crc != Zlib.crc32(chunktype+chunk) # http://www.w3.org/TR/PNG-Chunks.html case chunktype when 'IHDR' @width = decodeint(chunk[0..3]) @height = decodeint(chunk[4..7]) bitdepth = chunk[8] colourtype = chunk[9] # bitmask 1: palette, 2: color, 4: alpha compression = chunk[10] filter = chunk[11] interlace = chunk[12] puts "#{@width}x#{@height}x#{bitdepth}bpp, colourtype #{colourtype}, compression #{compression}, filter #{filter}, interlace #{interlace}" if $DEBUG raise 'Unhandled PNG format' if bitdepth != 8 or colourtype != 3 or compression != 0 or filter != 0 or interlace != 0 when 'IEND' break when 'PLTE' @palette = Array.new (len/3).times { |i| @palette << [chunk[3*i], chunk[3*i+1], chunk[3*i+2]] } puts "loaded #{@palette.length} colors palette" if $DEBUG when 'IDAT' compresseddata << chunk when 'tRNS' # alpha transparency palette, 0=transparent, defaults to opaque if len == 1 and chunk[0] == 0 @trans = true else puts 'unhandled transparency chunk - ignored' end else puts "ignoring unhandled chunk type #{chunktype}" end end puts 'Inflating image data' if $DEBUG img = Zlib::Inflate.inflate(compresseddata) @lines = Array.new @height.times { |i| off = i * (@width+1) raise 'Unhandled line encoding' if img[off] != 0 @lines << img[(off+1)..(off+@width)] } puts 'png load successful' if $DEBUG end # returns [r, g, b] for the pixel at (x, y), or nil if it is transparent def px(x, y) raise 'bad y' if y >= @height l = @lines[y] raise 'bad x' if x >= l.length i = l[x] raise 'bad color index' if i >= @palette.length @palette[i] if i > 0 or not @trans end def encodeint(i) ((i >> 24) & 255).chr << ((i>>16) & 255) << ((i>>8) & 255) << (i & 255) end def write_chunk(fd, id, chk) fd.write encodeint(chk.length) fd.write id fd.write chk fd.write encodeint(Zlib.crc32(id+chk)) end private :encodeint, :write_chunk def write(filename, overwrite = false) raise 'File exists' if not overwrite and File.exists? filename File.open(filename, 'w') { |fd| fd.write 137.chr << 80 << 78 << 71 << 13 << 10 << 26 << 10 chk = encodeint(@width) << encodeint(@height) << 8 << 3 << 0 << 0 << 0 write_chunk(fd, 'IHDR', chk) chk = @palette.map{ |e| e[0].chr << e[1] << e[2] }.join write_chunk(fd, 'PLTE', chk) write_chunk(fd, 'tRNS', 0.chr) if @trans chk = @lines.map { |l| 0.chr + l }.join chk = Zlib::Deflate.deflate(chk) write_chunk(fd, 'IDAT', chk) write_chunk(fd, 'IEND', '') } end def inspect "" end end