class Rbimg::PNG
Constants
- COLOR_TYPES
- REQUIRED_CHUNKS
Attributes
bit_depth[R]
compression_method[R]
filter_method[R]
height[R]
interlace_method[R]
pixel_size[R]
pixels[R]
width[R]
Public Class Methods
combine(*images, divider: nil, as: :row)
click to toggle source
# File lib/image_types/png.rb, line 18 def self.combine(*images, divider: nil, as: :row) raise ArgumentError.new("as: must be :row or :col") if as != :row && as != :col raise ArgumentError.new("Images and divider must all be an Rbimg::PNG") if !images.all?{|i| i.is_a?(Rbimg::PNG)} type = images.first.type height = images.first.height width = images.first.width bit_depth = images.first.bit_depth color_type = COLOR_TYPES[type] logical_pixel_width = width * Rbimg::PNG.pixel_size_for(color_type: color_type) if divider width_multiplier = logical_pixel_width / width divider_width = divider.width * width_multiplier if as == :row raise ArgumentError.new("divider must have the same height as images if aligning as a row") if divider.height != height elsif as == :col raise ArgumentError.new("divider must have the same width as images if aligning as a column") if divider.width != width end raise ArgumentError.new("divider must have the same type and bit_depth as images") if divider.type != type || divider.bit_depth != bit_depth end images.each do |i| if i.type != type || i.height != height || i.width != width || i.bit_depth != bit_depth raise ArgumentError.new("Currently all images must have the same type, height, width, and bit_depth to be combined") end end if as == :row new_width = images.length * width new_height = height if divider new_width += (divider.width * (images.length - 1)) end new_pixels = height.times.map do |row| row_start = row * logical_pixel_width divider_row_start = row * divider_width if divider images.map do |img| row_pixels = img.pixels[row_start...(row_start + logical_pixel_width)] ((img == images.last) || divider.nil?) ? row_pixels : row_pixels + divider.pixels[divider_row_start...(divider_row_start + divider_width)] end end.flatten else new_width = width new_height = images.length * height if divider new_height += (divider.height * (images.length - 1)) end new_pixels = images.map do |img| img_pixels = img.pixels if divider && img != images.last img_pixels + divider.pixels else img_pixels end end.flatten end begin new_img = Rbimg::PNG.new(pixels: new_pixels, type: type, width: new_width, height: new_height, bit_depth: bit_depth) rescue binding.pry end end
new(pixels: nil, type: nil, width: nil, height: nil, bit_depth: 8, compression_method: 0, filter_method: 0, interlace_method: 0, palette: nil)
click to toggle source
# File lib/image_types/png.rb, line 267 def initialize(pixels: nil, type: nil, width: nil, height: nil, bit_depth: 8, compression_method: 0, filter_method: 0, interlace_method: 0, palette: nil) @pixels, @width, @height, @compression_method, @filter_method, @interlace_method = pixels, width, height, compression_method, filter_method, interlace_method @bit_depth = bit_depth type = :greyscale if type.nil? @type = type.is_a?(Integer) ? type : COLOR_TYPES[type] raise ArgumentError.new("#{type} is not a valid color type. Please use one of: #{COLOR_TYPES.keys}") if type.nil? raise ArgumentError.new("Palettes are not compatible with color types 0 and 4") if palette && (@type == 0 || @type == 4) raise ArgumentError.new("palette must be an array") if palette && !palette.is_a?(Array) @signature = [137, 80, 78, 71, 13, 10, 26, 10] @chunks = [ Chunk.IHDR( width: @width, height: @height, bit_depth: @bit_depth, color_type: @type, compression_method: @compression_method, filter_method: @filter_method, interlace_method: interlace_method ), *Chunk.IDATs( pixels, color_type: @type, bit_depth: @bit_depth, width: @width, height: @height ), Chunk.IEND ] @chunks.insert(1, Chunk.PLTE(palette)) if !palette.nil? @pixel_size = Rbimg::PNG.pixel_size_for(color_type: @type) end
pixel_size_for(color_type:)
click to toggle source
# File lib/image_types/png.rb, line 90 def self.pixel_size_for(color_type:) case color_type when 0 1 when 2 3 when 3 1 when 4 2 when 6 4 else raise ArgumentError.new("#{color_type} is not a valid color type. Must be 0,2,3,4, or 6") end end
read(path: nil, data: nil)
click to toggle source
# File lib/image_types/png.rb, line 107 def self.read(path: nil, data: nil) raise ArgumentError.new(".read must be initialized with a path or a datastream") if (path.nil? && data.nil?) || (!path.nil? && !data.nil?) raise ArgumentError.new("data must be an array of byte integers or a byte string") if data && !data.is_a?(Array) && !data.is_a?(String) raise ArgumentError.new("data must be an array of byte integers or a byte string") if data && data.is_a?(Array) && !data.first.is_a?(Integer) path += ".png" if path && !path.end_with?('.png') begin if path data = File.read(path).bytes else data = data.bytes if data.is_a?(String) end chunk_start = 8 chunks = [] loop do len_end = chunk_start + 4 type_end = len_end + 4 len = Byteman.buf2int(data[chunk_start...len_end]) type = data[len_end...type_end] chunk_end = type_end + len + 4 case type.pack("C*") when "IHDR" chunks << Chunk.readIHDR(data[chunk_start...chunk_end]) when "IDAT" chunks << Chunk.readIDAT(data[chunk_start...chunk_end]) when "PLTE" chunks << Chunk.readPLTE(data[chunk_start...chunk_end]) else chunks << data[chunk_start...chunk_end] end chunk_start = chunk_end #TODO: Make sure last chunk is IEND break if chunk_end >= data.length - 1 end width = chunks.first[:width] height = chunks.first[:height] bit_depth = chunks.first[:bit_depth] color_type = chunks.first[:color_type] compression_method = chunks.first[:compression_method] filter_method = chunks.first[:filter_method] interlace_method = chunks.first[:interlace_method] all_idats = chunks.filter{ |c| c.is_a?(Hash) && c[:type] == "IDAT" } compressed_pixels = all_idats.reduce([]) { |mem, idat| mem + idat[:compressed_pixels] } pixels_and_filter = Zlib::Inflate.inflate(compressed_pixels.pack("C*")).unpack("C*") logical_pixel_width = Rbimg::PNG.pixel_size_for(color_type: color_type) * width pixel_width = (logical_pixel_width * (bit_depth / 8.0)).ceil scanline_filters = Array.new(height, nil) pixels = Array.new(pixels_and_filter.length - height, nil) pixels_and_filter.each.with_index do |pixel,i| scanline = i / (pixel_width + 1) pixel_number = (i % (pixel_width + 1)) - 1 pixel_loc = scanline * pixel_width + pixel_number if (pixel_number == -1) scanline_filters[scanline] = pixel else case scanline_filters[scanline] when 0 pixels[pixel_loc] = pixel when 1 x = pixel_number bpp = (pixel_width / width) * (bit_depth / 8.0).ceil prev_raw = x - bpp < 0 ? 0 : pixels[pixel_loc - bpp] new_pixel = (pixel + prev_raw) % 256 pixels[pixel_loc] = new_pixel when 2 x = pixel_number prev_raw = scanline == 0 ? 0 : pixels[pixel_loc - pixel_width] new_pixel = (pixel + prev_raw) % 256 pixels[pixel_loc] = new_pixel when 3 x = pixel_number bpp = (pixel_width / width) * (bit_depth / 8.0).ceil left_pix = x - bpp < 0 ? 0 : pixels[pixel_loc - bpp] above_pix = scanline == 0 ? 0 : pixels[pixel_loc - pixel_width] new_pixel = (pixel + ((left_pix + above_pix) / 2).floor) % 256 pixels[pixel_loc] = new_pixel when 4 x = pixel_number bpp = (pixel_width / width) * (bit_depth / 8.0).ceil paeth_predictor = Proc.new do |left, above, upper_left| p = left + above - upper_left pa = (p - left).abs pb = (p - above).abs pc = (p - upper_left).abs if pa <= pb && pa <= pc left elsif pb <= pc above else upper_left end end left_pix = x - bpp < 0 ? 0 : pixels[pixel_loc - bpp] above_pix = scanline == 0 ? 0 : pixels[pixel_loc - pixel_width] upper_left_pix = scanline == 0 ? 0 : x - bpp < 0 ? 0 : pixels[pixel_loc - pixel_width - bpp] pp_out = paeth_predictor[left_pix, above_pix, upper_left_pix] new_pixel = (pixel + pp_out) % 256 pixels[pixel_loc] = new_pixel else raise Rbimg::FormatError.new("Incorrect Filtering type used on this PNG file") end end end if bit_depth != 8 corrected_pixels = Array.new(logical_pixel_width * height, nil) height.times do |row_num| row_start = row_num * pixel_width row_end = row_start + pixel_width row_data = pixels[row_start...row_end] binary_data = Byteman.buf2int(row_data).to_s(2) pad_size = ((logical_pixel_width * bit_depth) / 8.0).ceil * 8 binary_data = Byteman.pad(num: binary_data, len: pad_size, type: :bits) corrected_row_start = row_num * logical_pixel_width logical_pixel_width.times do |pixel_num_in_row| binary_segment_start = pixel_num_in_row * bit_depth binary_segment_end = binary_segment_start + bit_depth binary_segment = binary_data[binary_segment_start...binary_segment_end] logical_pixel_value = binary_segment.to_i(2) corrected_pixel_location = corrected_row_start + pixel_num_in_row corrected_pixels[corrected_pixel_location] = logical_pixel_value end end else corrected_pixels = pixels end args = {pixels: corrected_pixels, type: color_type, width: width, height: height, bit_depth: bit_depth} plte = chunks.find{|c| c[:type] == "PLTE" unless c.is_a?(Array)} args[:palette] = plte[:chunk_data] if plte new(**args) rescue Errno::ENOENT => e raise ArgumentError.new("Invalid path #{path}") rescue => e raise e if e.is_a?(Rbimg::FormatError) raise Rbimg::FormatError.new("This PNG file is not in the correct format or has been corrupted :)") end end
Public Instance Methods
bytes()
click to toggle source
# File lib/image_types/png.rb, line 323 def bytes all_data.pack("C*") end
pixel(num)
click to toggle source
# File lib/image_types/png.rb, line 302 def pixel(num) start = num * @pixel_size pend = start + @pixel_size pixels[start...pend] end
row(rownum)
click to toggle source
# File lib/image_types/png.rb, line 308 def row(rownum) return nil if rownum > self.height pix_width = pixel_size * width start = rownum * pix_width pend = start + pix_width pixels[start...pend] end
type()
click to toggle source
# File lib/image_types/png.rb, line 317 def type COLOR_TYPES.each do |k,v| return k if v == @type end end
write(path: Dir.pwd + "/output.png")
click to toggle source
# File lib/image_types/png.rb, line 327 def write(path: Dir.pwd + "/output.png") postscript = path.split(".").last == "png" ? "" : ".png" File.write(path + postscript, bytes) end
Private Instance Methods
all_data()
click to toggle source
# File lib/image_types/png.rb, line 336 def all_data @signature + @chunks.map(&:data).flatten end