zpng 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +3 -4
- data/Gemfile.lock +18 -16
- data/README.md +62 -48
- data/README.md.tpl +11 -1
- data/Rakefile +43 -0
- data/VERSION +1 -1
- data/lib/zpng/chunk.rb +114 -17
- data/lib/zpng/cli.rb +30 -5
- data/lib/zpng/color.rb +30 -0
- data/lib/zpng/image.rb +122 -19
- data/lib/zpng/scan_line.rb +138 -74
- data/samples/captcha_4bpp.png +0 -0
- data/spec/create_image_spec.rb +78 -0
- data/spec/crop_spec.rb +93 -0
- data/spec/image_spec.rb +54 -0
- data/spec/modify_spec.rb +2 -9
- data/spec/running_pixel_spec.rb +47 -0
- data/spec/spec_helper.rb +7 -1
- data/zpng.gemspec +17 -15
- metadata +57 -38
    
        data/lib/zpng/cli.rb
    CHANGED
    
    | @@ -6,8 +6,8 @@ require 'pp' | |
| 6 6 | 
             
            class ZPNG::CLI
         | 
| 7 7 |  | 
| 8 8 | 
             
              ACTIONS = {
         | 
| 9 | 
            -
                'info'      => 'General image info',
         | 
| 10 9 | 
             
                'chunks'    => 'Show file chunks (default)',
         | 
| 10 | 
            +
                %w'i info'      => 'General image info (default)',
         | 
| 11 11 | 
             
                'ascii'     => 'Try to display image as ASCII (works best with monochrome images)',
         | 
| 12 12 | 
             
                'scanlines' => 'Show scanlines info',
         | 
| 13 13 | 
             
                'palette'   => 'Show palette'
         | 
| @@ -22,7 +22,7 @@ class ZPNG::CLI | |
| 22 22 | 
             
                @actions = []
         | 
| 23 23 | 
             
                @options = { :verbose => 0 }
         | 
| 24 24 | 
             
                optparser = OptionParser.new do |opts|
         | 
| 25 | 
            -
                  opts.banner = "Usage: zpng [options]"
         | 
| 25 | 
            +
                  opts.banner = "Usage: zpng [options] filename.png"
         | 
| 26 26 |  | 
| 27 27 | 
             
                  opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
         | 
| 28 28 | 
             
                    @options[:verbose] += 1
         | 
| @@ -32,12 +32,24 @@ class ZPNG::CLI | |
| 32 32 | 
             
                  end
         | 
| 33 33 |  | 
| 34 34 | 
             
                  ACTIONS.each do |t,desc|
         | 
| 35 | 
            -
                     | 
| 35 | 
            +
                    if t.is_a?(Array)
         | 
| 36 | 
            +
                      opts.on *[ "-#{t[0]}", "--#{t[1]}", desc, eval("lambda{ |_| @actions << :#{t[1]} }") ]
         | 
| 37 | 
            +
                    else
         | 
| 38 | 
            +
                      opts.on *[ "-#{t[0].upcase}", "--#{t}", desc, eval("lambda{ |_| @actions << :#{t} }") ]
         | 
| 39 | 
            +
                    end
         | 
| 36 40 | 
             
                  end
         | 
| 37 41 |  | 
| 38 | 
            -
                  opts.on "-E", "--extract-chunk  | 
| 42 | 
            +
                  opts.on "-E", "--extract-chunk ID", "extract a single chunk" do |id|
         | 
| 39 43 | 
             
                    @actions << [:extract_chunk, id.to_i]
         | 
| 40 44 | 
             
                  end
         | 
| 45 | 
            +
                  opts.on "-U", "--unpack-imagedata", "unpack Image Data (IDAT) chunk(s), output to stdout" do
         | 
| 46 | 
            +
                    @actions << :unpack_imagedata
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  opts.on "-c", "--crop GEOMETRY", "crop image, {WIDTH}x{HEIGHT}+{X}+{Y},",
         | 
| 50 | 
            +
                  "puts results on stdout unless --ascii given" do |x|
         | 
| 51 | 
            +
                    @actions << [:crop, x]
         | 
| 52 | 
            +
                  end
         | 
| 41 53 | 
             
                end
         | 
| 42 54 |  | 
| 43 55 | 
             
                if (argv = optparser.parse(@argv)).empty?
         | 
| @@ -80,12 +92,25 @@ class ZPNG::CLI | |
| 80 92 | 
             
                end
         | 
| 81 93 | 
             
              end
         | 
| 82 94 |  | 
| 95 | 
            +
              def unpack_imagedata
         | 
| 96 | 
            +
                print @img.imagedata
         | 
| 97 | 
            +
              end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
              def crop geometry
         | 
| 100 | 
            +
                unless geometry =~ /\A(\d+)x(\d+)\+(\d+)\+(\d+)\Z/i
         | 
| 101 | 
            +
                  STDERR.puts "[!] invalid geometry #{geometry.inspect}, must be WxH+X+Y, like 100x100+10+10"
         | 
| 102 | 
            +
                  exit 1
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
                @img.crop! :width => $1.to_i, :height => $2.to_i, :x => $3.to_i, :y => $4.to_i
         | 
| 105 | 
            +
                print @img.export unless @actions.include?(:ascii)
         | 
| 106 | 
            +
              end
         | 
| 107 | 
            +
             | 
| 83 108 | 
             
              def load_file fname
         | 
| 84 109 | 
             
                @img = ZPNG::Image.new fname
         | 
| 85 110 | 
             
              end
         | 
| 86 111 |  | 
| 87 112 | 
             
              def info
         | 
| 88 | 
            -
                puts "[.] image size #{@img.width}x#{@img.height}"
         | 
| 113 | 
            +
                puts "[.] image size #{@img.width || '?'}x#{@img.height || '?'}"
         | 
| 89 114 | 
             
                puts "[.] uncompressed imagedata size = #{@img.imagedata.size} bytes"
         | 
| 90 115 | 
             
                puts "[.] palette = #{@img.palette}" if @img.palette
         | 
| 91 116 | 
             
              end
         | 
    
        data/lib/zpng/color.rb
    CHANGED
    
    | @@ -1,7 +1,13 @@ | |
| 1 1 | 
             
            module ZPNG
         | 
| 2 2 | 
             
              class Color < Struct.new(:r,:g,:b,:a)
         | 
| 3 3 |  | 
| 4 | 
            +
                def initialize *args
         | 
| 5 | 
            +
                  super
         | 
| 6 | 
            +
                  self.a ||= 0xff
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 4 9 | 
             
                alias :alpha :a
         | 
| 10 | 
            +
                def alpha= v; self.a = v; end
         | 
| 5 11 |  | 
| 6 12 | 
             
                BLACK = Color.new(0,0,0,0xff)
         | 
| 7 13 | 
             
                WHITE = Color.new(0xff,0xff,0xff,0xff)
         | 
| @@ -14,12 +20,36 @@ module ZPNG | |
| 14 20 | 
             
                  r == 0 && g == 0 && b == 0
         | 
| 15 21 | 
             
                end
         | 
| 16 22 |  | 
| 23 | 
            +
                def transparent?
         | 
| 24 | 
            +
                  a == 0
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 17 27 | 
             
                def to_grayscale
         | 
| 18 28 | 
             
                  (r+g+b)/3
         | 
| 19 29 | 
             
                end
         | 
| 20 30 |  | 
| 31 | 
            +
                def self.from_grayscale value, alpha = nil
         | 
| 32 | 
            +
                  Color.new value,value,value, alpha
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 21 35 | 
             
                def to_s
         | 
| 22 36 | 
             
                  "%02X%02X%02X" % [r,g,b]
         | 
| 23 37 | 
             
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def to_i
         | 
| 40 | 
            +
                  ((a||0) << 24) + ((r||0) << 16) + ((g||0) << 8) + (b||0)
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def inspect
         | 
| 44 | 
            +
                  if r && g && b && a
         | 
| 45 | 
            +
                    "#<ZPNG::Color #%02x%02x%02x a=%d>" % [r,g,b,a]
         | 
| 46 | 
            +
                  else
         | 
| 47 | 
            +
                    rs = r ? "%02x" % r : "??"
         | 
| 48 | 
            +
                    gs = g ? "%02x" % g : "??"
         | 
| 49 | 
            +
                    bs = b ? "%02x" % b : "??"
         | 
| 50 | 
            +
                    as = a ? "%d"   % a : "?"
         | 
| 51 | 
            +
                    "#<ZPNG::Color #%s%s%s%s a=%s>" % [rs,gs,bs,as]
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
                end
         | 
| 24 54 | 
             
              end
         | 
| 25 55 | 
             
            end
         | 
    
        data/lib/zpng/image.rb
    CHANGED
    
    | @@ -5,7 +5,55 @@ module ZPNG | |
| 5 5 |  | 
| 6 6 | 
             
                PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a"
         | 
| 7 7 |  | 
| 8 | 
            -
                def initialize x | 
| 8 | 
            +
                def initialize x
         | 
| 9 | 
            +
                  @chunks = []
         | 
| 10 | 
            +
                  case x
         | 
| 11 | 
            +
                    when IO
         | 
| 12 | 
            +
                      _from_string x.read
         | 
| 13 | 
            +
                    when String
         | 
| 14 | 
            +
                      _from_string x
         | 
| 15 | 
            +
                    when Hash
         | 
| 16 | 
            +
                      _from_hash x
         | 
| 17 | 
            +
                    else
         | 
| 18 | 
            +
                      raise "unsupported input data type #{x.class}"
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                  if palette && hdr && hdr.depth
         | 
| 21 | 
            +
                    palette.max_colors = 2**hdr.depth
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                # load image from file
         | 
| 26 | 
            +
                def self.load fname
         | 
| 27 | 
            +
                  open(fname,"rb") do |f|
         | 
| 28 | 
            +
                    Image.new(f)
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
                alias :load_file :load
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                # save image to file
         | 
| 34 | 
            +
                def save fname
         | 
| 35 | 
            +
                  File.open(fname,"wb"){ |f| f << export }
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                # flag that image is just created, and NOT loaded from file
         | 
| 39 | 
            +
                # as in Rails' ActiveRecord::Base#new_record?
         | 
| 40 | 
            +
                def new_image?
         | 
| 41 | 
            +
                  @new_image
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
                alias :new? :new_image?
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                private
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def _from_hash h
         | 
| 48 | 
            +
                  @new_image = true
         | 
| 49 | 
            +
                  @chunks << (@header  = Chunk::IHDR.new(h))
         | 
| 50 | 
            +
                  if @header.palette_used?
         | 
| 51 | 
            +
                    @chunks << (@palette = Chunk::PLTE.new)
         | 
| 52 | 
            +
                    @palette[0] = h[:bg] || Color::BLACK # add default bg color
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def _from_string x
         | 
| 9 57 | 
             
                  if x
         | 
| 10 58 | 
             
                    if x[PNG_HDR]
         | 
| 11 59 | 
             
                      # raw image data
         | 
| @@ -23,7 +71,6 @@ module ZPNG | |
| 23 71 |  | 
| 24 72 | 
             
                  io = StringIO.new(data)
         | 
| 25 73 | 
             
                  io.seek PNG_HDR.size
         | 
| 26 | 
            -
                  @chunks = []
         | 
| 27 74 | 
             
                  while !io.eof?
         | 
| 28 75 | 
             
                    chunk = Chunk.from_stream(io)
         | 
| 29 76 | 
             
                    chunk.idx = @chunks.size
         | 
| @@ -44,6 +91,7 @@ module ZPNG | |
| 44 91 | 
             
                  end
         | 
| 45 92 | 
             
                end
         | 
| 46 93 |  | 
| 94 | 
            +
                public
         | 
| 47 95 | 
             
                def dump
         | 
| 48 96 | 
             
                  @chunks.each do |chunk|
         | 
| 49 97 | 
             
                    puts "[.] #{chunk.inspect} #{chunk.crc_ok? ? 'CRC OK'.green : 'CRC ERROR'.red}"
         | 
| @@ -58,13 +106,21 @@ module ZPNG | |
| 58 106 | 
             
                  @header && @header.height
         | 
| 59 107 | 
             
                end
         | 
| 60 108 |  | 
| 109 | 
            +
                def grayscale?
         | 
| 110 | 
            +
                  @header && @header.grayscale?
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
             | 
| 61 113 | 
             
                def imagedata
         | 
| 62 | 
            -
                   | 
| 63 | 
            -
                     | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 114 | 
            +
                  @imagedata ||=
         | 
| 115 | 
            +
                    begin
         | 
| 116 | 
            +
                      if @header
         | 
| 117 | 
            +
                        raise "only non-interlaced mode is supported for imagedata" if @header.interlace != 0
         | 
| 118 | 
            +
                      else
         | 
| 119 | 
            +
                        puts "[?] no image header, assuming non-interlaced RGB".yellow
         | 
| 120 | 
            +
                      end
         | 
| 121 | 
            +
                      data = @chunks.find_all{ |c| c.is_a?(Chunk::IDAT) }.map(&:data).join
         | 
| 122 | 
            +
                      (data && data.size > 0) ? Zlib::Inflate.inflate(data) : ''
         | 
| 123 | 
            +
                    end
         | 
| 68 124 | 
             
                end
         | 
| 69 125 |  | 
| 70 126 | 
             
                def [] x, y
         | 
| @@ -72,30 +128,36 @@ module ZPNG | |
| 72 128 | 
             
                end
         | 
| 73 129 |  | 
| 74 130 | 
             
                def []= x, y, newpixel
         | 
| 75 | 
            -
                   | 
| 76 | 
            -
                  # or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
         | 
| 77 | 
            -
                  _decode_all_scanlines unless @_all_scanlines_decoded
         | 
| 131 | 
            +
                  decode_all_scanlines
         | 
| 78 132 | 
             
                  scanlines[y][x] = newpixel
         | 
| 79 133 | 
             
                end
         | 
| 80 134 |  | 
| 81 | 
            -
                 | 
| 135 | 
            +
                # we must decode all scanlines before doing any modifications
         | 
| 136 | 
            +
                # or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
         | 
| 137 | 
            +
                def decode_all_scanlines
         | 
| 138 | 
            +
                  return if @all_scanlines_decoded
         | 
| 139 | 
            +
                  @all_scanlines_decoded = true
         | 
| 82 140 | 
             
                  scanlines.each(&:decode!)
         | 
| 83 | 
            -
                  @_all_scanlines_decoded = true
         | 
| 84 141 | 
             
                end
         | 
| 85 142 |  | 
| 86 143 | 
             
                def scanlines
         | 
| 87 144 | 
             
                  @scanlines ||=
         | 
| 88 145 | 
             
                    begin
         | 
| 89 146 | 
             
                      r = []
         | 
| 90 | 
            -
                      height.times do |i|
         | 
| 147 | 
            +
                      height.to_i.times do |i|
         | 
| 91 148 | 
             
                        r << ScanLine.new(self,i)
         | 
| 92 149 | 
             
                      end
         | 
| 150 | 
            +
                      r.delete_if(&:bad?)
         | 
| 93 151 | 
             
                      r
         | 
| 94 152 | 
             
                    end
         | 
| 95 153 | 
             
                end
         | 
| 96 154 |  | 
| 97 155 | 
             
                def to_s h={}
         | 
| 98 | 
            -
                  scanlines. | 
| 156 | 
            +
                  if scanlines.any?
         | 
| 157 | 
            +
                    scanlines.map{ |l| l.to_s(h) }.join("\n")
         | 
| 158 | 
            +
                  else
         | 
| 159 | 
            +
                    super()
         | 
| 160 | 
            +
                  end
         | 
| 99 161 | 
             
                end
         | 
| 100 162 |  | 
| 101 163 | 
             
                def extract_block x,y=nil,w=nil,h=nil
         | 
| @@ -118,14 +180,55 @@ module ZPNG | |
| 118 180 | 
             
                def export
         | 
| 119 181 | 
             
                  imagedata # fill @imagedata, if not already filled
         | 
| 120 182 |  | 
| 121 | 
            -
                  # delete  | 
| 122 | 
            -
                   | 
| 123 | 
            -
                  @chunks.delete_if{ |c| c.type == 'IDAT' && c != first_idat }
         | 
| 183 | 
            +
                  # delete old IDAT chunks
         | 
| 184 | 
            +
                  @chunks.delete_if{ |c| c.is_a?(Chunk::IDAT) }
         | 
| 124 185 |  | 
| 125 186 | 
             
                  # fill first_idat @data with compressed imagedata
         | 
| 126 | 
            -
                   | 
| 187 | 
            +
                  @chunks << Chunk::IDAT.new(
         | 
| 188 | 
            +
                    :data => Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
         | 
| 189 | 
            +
                  )
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                  # delete IEND chunk(s) b/c we just added a new chunk and IEND must be the last one
         | 
| 192 | 
            +
                  @chunks.delete_if{ |c| c.is_a?(Chunk::IEND) }
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                  # add fresh new IEND
         | 
| 195 | 
            +
                  @chunks << Chunk::IEND.new
         | 
| 127 196 |  | 
| 128 197 | 
             
                  PNG_HDR + @chunks.map(&:export).join
         | 
| 129 198 | 
             
                end
         | 
| 199 | 
            +
             | 
| 200 | 
            +
                # modifies this image
         | 
| 201 | 
            +
                def crop! params
         | 
| 202 | 
            +
                  decode_all_scanlines
         | 
| 203 | 
            +
             | 
| 204 | 
            +
                  x,y,h,w = (params[:x]||0), (params[:y]||0), params[:height], params[:width]
         | 
| 205 | 
            +
                  raise "negative params not allowed" if [x,y,h,w].any?{ |x| x < 0 }
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                  # adjust crop sizes if they greater than image sizes
         | 
| 208 | 
            +
                  h = self.height-y if (y+h) > self.height
         | 
| 209 | 
            +
                  w = self.width-x if (x+w) > self.width
         | 
| 210 | 
            +
                  raise "negative params not allowed (p2)" if [x,y,h,w].any?{ |x| x < 0 }
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                  # delete excess scanlines at tail
         | 
| 213 | 
            +
                  scanlines[(y+h)..-1] = [] if (y+h) < scanlines.size
         | 
| 214 | 
            +
             | 
| 215 | 
            +
                  # delete excess scanlines at head
         | 
| 216 | 
            +
                  scanlines[0,y] = [] if y > 0
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                  # crop remaining scanlines
         | 
| 219 | 
            +
                  scanlines.each{ |l| l.crop!(x,w) }
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                  # modify header
         | 
| 222 | 
            +
                  hdr.height, hdr.width = h, w
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                  # return self
         | 
| 225 | 
            +
                  self
         | 
| 226 | 
            +
                end
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                # returns new image
         | 
| 229 | 
            +
                def crop params
         | 
| 230 | 
            +
                  # deep copy first, then crop!
         | 
| 231 | 
            +
                  Marshal.load(Marshal.dump(self)).crop!(params)
         | 
| 232 | 
            +
                end
         | 
| 130 233 | 
             
              end
         | 
| 131 234 | 
             
            end
         | 
    
        data/lib/zpng/scan_line.rb
    CHANGED
    
    | @@ -1,3 +1,4 @@ | |
| 1 | 
            +
            #coding: binary
         | 
| 1 2 | 
             
            module ZPNG
         | 
| 2 3 | 
             
              class ScanLine
         | 
| 3 4 | 
             
                FILTER_NONE           = 0
         | 
| @@ -12,17 +13,41 @@ module ZPNG | |
| 12 13 | 
             
                  @image,@idx = image,idx
         | 
| 13 14 | 
             
                  @bpp = image.hdr.bpp
         | 
| 14 15 | 
             
                  raise "[!] zero bpp" if @bpp == 0
         | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 16 | 
            +
             | 
| 17 | 
            +
                  # Bytes Per Pixel, if bpp = 8, 16, 24, 32
         | 
| 18 | 
            +
                  # NULL otherwise
         | 
| 19 | 
            +
                  @BPP = (@bpp%8 == 0) && (@bpp>>3)
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  if @image.new?
         | 
| 22 | 
            +
                    @decoded_bytes = "\x00" * (size-1)
         | 
| 23 | 
            +
                    @filter = FILTER_NONE
         | 
| 17 24 | 
             
                  else
         | 
| 18 | 
            -
                    @offset = idx* | 
| 25 | 
            +
                    @offset = idx*size
         | 
| 26 | 
            +
                    if @filter = image.imagedata[@offset]
         | 
| 27 | 
            +
                      @filter = @filter.ord
         | 
| 28 | 
            +
                    else
         | 
| 29 | 
            +
                      STDERR.puts "[!] #{self.class}: ##@idx: no data at pos 0, scanline dropped".red
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                    @offset += 1
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                # ScanLine is BAD if it has no filter
         | 
| 36 | 
            +
                def bad?
         | 
| 37 | 
            +
                  !@filter
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                # total scanline size in bytes, INCLUDING leading 'filter' byte
         | 
| 41 | 
            +
                def size
         | 
| 42 | 
            +
                  if @BPP
         | 
| 43 | 
            +
                    image.width*@BPP+1
         | 
| 44 | 
            +
                  else
         | 
| 45 | 
            +
                    (image.width*@bpp/8.0+1).ceil
         | 
| 19 46 | 
             
                  end
         | 
| 20 | 
            -
                  @filter = image.imagedata[@offset].ord
         | 
| 21 | 
            -
                  @offset += 1
         | 
| 22 47 | 
             
                end
         | 
| 23 48 |  | 
| 24 49 | 
             
                def inspect
         | 
| 25 | 
            -
                  "#<ZPNG::ScanLine " + (instance_variables-[:@image, :@decoded]).
         | 
| 50 | 
            +
                  "#<ZPNG::ScanLine " + (instance_variables-[:@image, :@decoded, :@BPP]).
         | 
| 26 51 | 
             
                      map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
         | 
| 27 52 | 
             
                      join(", ") + ">"
         | 
| 28 53 | 
             
                end
         | 
| @@ -34,6 +59,7 @@ module ZPNG | |
| 34 59 |  | 
| 35 60 | 
             
                  @image.width.times.map do |i|
         | 
| 36 61 | 
             
                    px = decode_pixel(i)
         | 
| 62 | 
            +
            #        printf "[d] %08x %s %s\n", px.to_i, px.inspect, px.to_s unless px.black?
         | 
| 37 63 | 
             
                    px.white?? white : (px.black?? black : unknown)
         | 
| 38 64 | 
             
                  end.join
         | 
| 39 65 | 
             
                end
         | 
| @@ -43,40 +69,37 @@ module ZPNG | |
| 43 69 | 
             
                end
         | 
| 44 70 |  | 
| 45 71 | 
             
                def []= x, newcolor
         | 
| 72 | 
            +
                  if image.hdr.palette_used?
         | 
| 73 | 
            +
                    color_idx = image.palette.find_or_add(newcolor)
         | 
| 74 | 
            +
                    raise "no color #{newcolor.inspect} in palette" unless color_idx
         | 
| 75 | 
            +
                  elsif image.grayscale?
         | 
| 76 | 
            +
                    color_idx = newcolor.to_grayscale
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 46 79 | 
             
                  case @bpp
         | 
| 47 | 
            -
                  when 1
         | 
| 48 | 
            -
                     | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
                          raise "1bpp pixel can only be WHITE or BLACK, got #{newcolor.inspect}"
         | 
| 60 | 
            -
                        end
         | 
| 61 | 
            -
                      end
         | 
| 62 | 
            -
                    if flag
         | 
| 63 | 
            -
                      # turn pixel on
         | 
| 64 | 
            -
                      decoded_bytes[x/8] = (decoded_bytes[x/8].ord | (1<<(7-(x%8)))).chr
         | 
| 65 | 
            -
                    else
         | 
| 66 | 
            -
                      # turn pixel off
         | 
| 67 | 
            -
                      decoded_bytes[x/8] = (decoded_bytes[x/8].ord & (0xff-(1<<(7-(x%8))))).chr
         | 
| 68 | 
            -
                    end
         | 
| 80 | 
            +
                  when 1,2,4
         | 
| 81 | 
            +
                    pos = x*@bpp/8
         | 
| 82 | 
            +
                    b = decoded_bytes[pos].ord
         | 
| 83 | 
            +
                    mask  = 2**@bpp-1
         | 
| 84 | 
            +
                    shift = 8-(x%(8/@bpp)+1)*@bpp
         | 
| 85 | 
            +
                    raise "invalid shift #{shift}" if shift < 0 || shift > 7
         | 
| 86 | 
            +
             | 
| 87 | 
            +
            #        printf "[d] %s x=%2d bpp=%d pos=%d mask=%08b shift=%d decoded_bytes=#{decoded_bytes.inspect}\n", self.to_s, x, @bpp, pos, mask, shift
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    b = (b & (0xff-(mask<<shift))) | ((color_idx & mask) << shift)
         | 
| 90 | 
            +
                    decoded_bytes[pos] = b.chr
         | 
| 91 | 
            +
             | 
| 69 92 | 
             
                  when 8
         | 
| 70 93 | 
             
                    if image.hdr.palette_used?
         | 
| 71 | 
            -
                      decoded_bytes[x] =  | 
| 94 | 
            +
                      decoded_bytes[x] = color_idx.chr
         | 
| 72 95 | 
             
                    else
         | 
| 73 96 | 
             
                      decoded_bytes[x] = ((newcolor.r + newcolor.g + newcolor.b)/3).chr
         | 
| 74 97 | 
             
                    end
         | 
| 75 98 | 
             
                  when 16
         | 
| 76 99 | 
             
                    if image.hdr.palette_used? && image.hdr.alpha_used?
         | 
| 77 | 
            -
                      decoded_bytes[x*2] =  | 
| 100 | 
            +
                      decoded_bytes[x*2] = color_idx.chr
         | 
| 78 101 | 
             
                      decoded_bytes[x*2+1] = (newcolor.alpha || 0xff).chr
         | 
| 79 | 
            -
                    elsif image. | 
| 102 | 
            +
                    elsif image.grayscale? && image.hdr.alpha_used?
         | 
| 80 103 | 
             
                      decoded_bytes[x*2] = newcolor.to_grayscale.chr
         | 
| 81 104 | 
             
                      decoded_bytes[x*2+1] = (newcolor.alpha || 0xff).chr
         | 
| 82 105 | 
             
                    else
         | 
| @@ -97,56 +120,51 @@ module ZPNG | |
| 97 120 | 
             
                      decoded_bytes[x*@BPP, @BPP]
         | 
| 98 121 | 
             
                    else
         | 
| 99 122 | 
             
                      # 1, 2 or 4 bits per pixel
         | 
| 100 | 
            -
                      decoded_bytes[x*@bpp/8 | 
| 123 | 
            +
                      decoded_bytes[x*@bpp/8]
         | 
| 101 124 | 
             
                    end
         | 
| 102 125 |  | 
| 103 | 
            -
                   | 
| 104 | 
            -
             | 
| 105 | 
            -
             | 
| 106 | 
            -
             | 
| 107 | 
            -
             | 
| 108 | 
            -
             | 
| 109 | 
            -
                       | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
| 113 | 
            -
                       | 
| 114 | 
            -
             | 
| 115 | 
            -
                       | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
                       | 
| 119 | 
            -
             | 
| 120 | 
            -
                    return image.palette[idx]
         | 
| 121 | 
            -
                  end
         | 
| 122 | 
            -
             | 
| 123 | 
            -
                  case @bpp
         | 
| 124 | 
            -
                  when 1
         | 
| 125 | 
            -
                    r=g=b= (raw.ord & (1<<(7-(x%8)))) == 0 ? 0 : 0xff
         | 
| 126 | 
            -
                  when 8
         | 
| 127 | 
            -
                    if colormode == ZPNG::Chunk::IHDR::COLOR_GRAYSCALE
         | 
| 128 | 
            -
                      r=g=b= raw.ord
         | 
| 126 | 
            +
                  color, alpha =
         | 
| 127 | 
            +
                    case @bpp
         | 
| 128 | 
            +
                    when 1,2,4
         | 
| 129 | 
            +
                      mask  = 2**@bpp-1
         | 
| 130 | 
            +
                      shift = 8-(x%(8/@bpp)+1)*@bpp
         | 
| 131 | 
            +
                      raise "invalid shift #{shift}" if shift < 0 || shift > 7
         | 
| 132 | 
            +
                      [(raw.ord >> shift) & mask, nil]
         | 
| 133 | 
            +
                    when 8
         | 
| 134 | 
            +
                      [raw.ord, nil]
         | 
| 135 | 
            +
                    when 16
         | 
| 136 | 
            +
                      raw.unpack 'C2'
         | 
| 137 | 
            +
                    when 24
         | 
| 138 | 
            +
                      # RGB
         | 
| 139 | 
            +
                      return Color.new(*raw.unpack('C3'))
         | 
| 140 | 
            +
                    when 32
         | 
| 141 | 
            +
                      # RGBA
         | 
| 142 | 
            +
                      return Color.new(*raw.unpack('C4'))
         | 
| 129 143 | 
             
                    else
         | 
| 130 | 
            -
                      raise "unexpected  | 
| 144 | 
            +
                      raise "unexpected bpp #{@bpp}"
         | 
| 131 145 | 
             
                    end
         | 
| 132 | 
            -
             | 
| 133 | 
            -
             | 
| 134 | 
            -
             | 
| 135 | 
            -
                      a  | 
| 136 | 
            -
             | 
| 137 | 
            -
             | 
| 146 | 
            +
             | 
| 147 | 
            +
                  if image.grayscale?
         | 
| 148 | 
            +
                    if [1,2,4].include?(@bpp)
         | 
| 149 | 
            +
                      #color should be extended to a 8-bit range
         | 
| 150 | 
            +
                      if color%2 == 0
         | 
| 151 | 
            +
                        color <<= (8-@bpp)
         | 
| 152 | 
            +
                      else
         | 
| 153 | 
            +
                        (8-@bpp).times{ color = color*2 + 1 }
         | 
| 154 | 
            +
                      end
         | 
| 138 155 | 
             
                    end
         | 
| 139 | 
            -
             | 
| 140 | 
            -
             | 
| 141 | 
            -
             | 
| 142 | 
            -
                     | 
| 143 | 
            -
             | 
| 156 | 
            +
                    Color.from_grayscale(color, alpha)
         | 
| 157 | 
            +
                  elsif image.palette
         | 
| 158 | 
            +
                    color = image.palette[color]
         | 
| 159 | 
            +
                    color.alpha = alpha
         | 
| 160 | 
            +
                    color
         | 
| 161 | 
            +
                  else
         | 
| 162 | 
            +
                    raise "cannot decode color"
         | 
| 144 163 | 
             
                  end
         | 
| 145 | 
            -
             | 
| 146 | 
            -
                  Color.new(r,g,b,a)
         | 
| 147 164 | 
             
                end
         | 
| 148 165 |  | 
| 149 166 | 
             
                def decoded_bytes
         | 
| 167 | 
            +
                  raise if caller.size > 50
         | 
| 150 168 | 
             
                  @decoded_bytes ||=
         | 
| 151 169 | 
             
                    begin
         | 
| 152 170 | 
             
                      # number of bytes per complete pixel, rounding up to one
         | 
| @@ -170,11 +188,12 @@ module ZPNG | |
| 170 188 | 
             
                  true
         | 
| 171 189 | 
             
                end
         | 
| 172 190 |  | 
| 191 | 
            +
                private
         | 
| 173 192 | 
             
                def decode_byte x, b0, bpp1
         | 
| 174 193 | 
             
                  raw = @image.imagedata[@offset+x]
         | 
| 175 194 |  | 
| 176 195 | 
             
                  unless raw
         | 
| 177 | 
            -
                    STDERR.puts "[!]  | 
| 196 | 
            +
                    STDERR.puts "[!] #{self.class}: ##@idx: no data at pos #{x}".red
         | 
| 178 197 | 
             
                    raw = 0.chr
         | 
| 179 198 | 
             
                  end
         | 
| 180 199 |  | 
| @@ -214,9 +233,54 @@ module ZPNG | |
| 214 233 | 
             
                  (pa <= pb) ? (pa <= pc ? a : c) : (pb <= pc ? b : c)
         | 
| 215 234 | 
             
                end
         | 
| 216 235 |  | 
| 236 | 
            +
                public
         | 
| 237 | 
            +
                def crop! x, w
         | 
| 238 | 
            +
                  if @BPP
         | 
| 239 | 
            +
                    # great, crop is byte-aligned! :)
         | 
| 240 | 
            +
                    decoded_bytes[0,x*@BPP]   = ''
         | 
| 241 | 
            +
                    decoded_bytes[w*@BPP..-1] = ''
         | 
| 242 | 
            +
                  else
         | 
| 243 | 
            +
                    # oh, no we have to shift bits in a whole line :(
         | 
| 244 | 
            +
                    case @bpp
         | 
| 245 | 
            +
                    when 1,2,4
         | 
| 246 | 
            +
                      cut_bits_head = @bpp*x
         | 
| 247 | 
            +
                      if cut_bits_head > 8
         | 
| 248 | 
            +
                        # cut whole head bytes
         | 
| 249 | 
            +
                        decoded_bytes[0,cut_bits_head/8] = ''
         | 
| 250 | 
            +
                      end
         | 
| 251 | 
            +
                      cut_bits_head %= 8
         | 
| 252 | 
            +
                      if cut_bits_head > 0
         | 
| 253 | 
            +
                        # bit-shift all remaining bytes
         | 
| 254 | 
            +
                        (w*@bpp/8.0).ceil.times do |i|
         | 
| 255 | 
            +
                          decoded_bytes[i] = ((
         | 
| 256 | 
            +
                            (decoded_bytes[i].ord<<cut_bits_head) |
         | 
| 257 | 
            +
                            (decoded_bytes[i+1].ord>>(8-cut_bits_head))
         | 
| 258 | 
            +
                          ) & 0xff).chr
         | 
| 259 | 
            +
                        end
         | 
| 260 | 
            +
                      end
         | 
| 261 | 
            +
             | 
| 262 | 
            +
                      new_width_bits = w*@bpp
         | 
| 263 | 
            +
                      diff = decoded_bytes.size*8 - new_width_bits
         | 
| 264 | 
            +
                      raise if diff < 0
         | 
| 265 | 
            +
                      if diff > 8
         | 
| 266 | 
            +
                        # cut whole tail bytes
         | 
| 267 | 
            +
                        decoded_bytes[(new_width_bits/8.0).ceil..-1] = ''
         | 
| 268 | 
            +
                      end
         | 
| 269 | 
            +
                      diff %= 8
         | 
| 270 | 
            +
                      if diff > 0
         | 
| 271 | 
            +
                        # zero tail bits of last byte
         | 
| 272 | 
            +
                        decoded_bytes[-1] = (decoded_bytes[-1].ord & (0xff-(2**diff)+1)).chr
         | 
| 273 | 
            +
                      end
         | 
| 274 | 
            +
             | 
| 275 | 
            +
                    else
         | 
| 276 | 
            +
                      raise "unexpected bpp=#@bpp"
         | 
| 277 | 
            +
                    end
         | 
| 278 | 
            +
                  end
         | 
| 279 | 
            +
                end
         | 
| 280 | 
            +
             | 
| 217 281 | 
             
                def export
         | 
| 218 282 | 
             
                  # we export in FILTER_NONE mode
         | 
| 219 | 
            -
                   | 
| 283 | 
            +
                  FILTER_NONE.chr + decoded_bytes
         | 
| 220 284 | 
             
                end
         | 
| 221 285 | 
             
              end
         | 
| 222 286 | 
             
            end
         |