Class: ZPNG::Image

Inherits:
Object
  • Object
show all
Includes:
BMP::Reader, DeepCopyable
Defined in:
lib/zpng/image.rb

Constant Summary collapse

PNG_HDR =
"\x89PNG\x0d\x0a\x1a\x0a".force_encoding('binary')
BMP_HDR =
"BM".force_encoding('binary')

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from BMP::Reader

#_read_bmp

Methods included from DeepCopyable

#deep_copy

Constructor Details

#initialize(x, h = {}) ⇒ Image

possible input params:

IO      of opened image file
String  with image file already readed
Hash    of image parameters to create new blank image


20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/zpng/image.rb', line 20

def initialize x, h={}
  @chunks = []
  @color_class = Color
  @format = :png
  @verbose =
    case h[:verbose]
    when true;  1
    when false; 0
    else h[:verbose].to_i
    end

  case x
    when IO
      _from_io x
    when String
      _from_io StringIO.new(x)
    when Hash
      _from_hash x
    else
      raise NotSupported, "unsupported input data type #{x.class}"
  end
  if palette && hdr && hdr.depth
    palette.max_colors = 2**hdr.depth
  end
end

Instance Attribute Details

#chunksObject

Returns the value of attribute chunks.



5
6
7
# File 'lib/zpng/image.rb', line 5

def chunks
  @chunks
end

#color_classObject

now only for (limited) BMP support



8
9
10
# File 'lib/zpng/image.rb', line 8

def color_class
  @color_class
end

#extradataObject

Returns the value of attribute extradata.



5
6
7
# File 'lib/zpng/image.rb', line 5

def extradata
  @extradata
end

#formatObject

Returns the value of attribute format.



5
6
7
# File 'lib/zpng/image.rb', line 5

def format
  @format
end

#imagedataObject

Returns the value of attribute imagedata.



5
6
7
# File 'lib/zpng/image.rb', line 5

def imagedata
  @imagedata
end

#scanlinesObject

Returns the value of attribute scanlines.



5
6
7
# File 'lib/zpng/image.rb', line 5

def scanlines
  @scanlines
end

#verboseObject

Returns the value of attribute verbose.



5
6
7
# File 'lib/zpng/image.rb', line 5

def verbose
  @verbose
end

Class Method Details

.load(fname, h = {}) ⇒ Object Also known as: load_file, from_file

load image from file



66
67
68
69
70
# File 'lib/zpng/image.rb', line 66

def load fname, h={}
  open(fname,"rb") do |f|
    self.new(f,h)
  end
end

Instance Method Details

#==(other_image) ⇒ Object



425
426
427
428
429
430
431
432
433
# File 'lib/zpng/image.rb', line 425

def == other_image
  return false unless other_image.is_a?(Image)
  return false if width  != other_image.width
  return false if height != other_image.height
  each_pixel do |c,x,y|
    return false if c != other_image[x,y]
  end
  true
end

#[](x, y) ⇒ Object



302
303
304
305
306
# File 'lib/zpng/image.rb', line 302

def [] x, y
  # extracting this check into a module => +1-2% speed
  x,y = adam7.convert_coords(x,y) if interlaced?
  scanlines[y][x]
end

#[]=(x, y, newcolor) ⇒ Object



308
309
310
311
312
313
# File 'lib/zpng/image.rb', line 308

def []= x, y, newcolor
  # extracting these checks into a module => +1-2% speed
  decode_all_scanlines
  x,y = adam7.convert_coords(x,y) if interlaced?
  scanlines[y][x] = newcolor
end

#_alpha_color(color) ⇒ Object

internal helper method for color types 0 (grayscale) and 2 (truecolor)



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/zpng/image.rb', line 143

def _alpha_color color
  return nil unless trns

  # For color type 0 (grayscale), the tRNS chunk contains a single gray level value, stored in the format:
  #
  #   Gray:  2 bytes, range 0 .. (2^bitdepth)-1
  #
  # For color type 2 (truecolor), the tRNS chunk contains a single RGB color value, stored in the format:
  #
  #   Red:   2 bytes, range 0 .. (2^bitdepth)-1
  #   Green: 2 bytes, range 0 .. (2^bitdepth)-1
  #   Blue:  2 bytes, range 0 .. (2^bitdepth)-1
  #
  # (If the image bit depth is less than 16, the least significant bits are used and the others are 0)
  # Pixels of the specified gray level are to be treated as transparent (equivalent to alpha value 0);
  # all other pixels are to be treated as fully opaque ( alpha = (2^bitdepth)-1 )

  @alpha_color ||=
    case hdr.color
    when COLOR_GRAYSCALE
      v = trns.data.unpack('n')[0] & (2**hdr.depth-1)
      Color.from_grayscale(v, :depth => hdr.depth)
    when COLOR_RGB
      a = trns.data.unpack('n3').map{ |v| v & (2**hdr.depth-1) }
      Color.new(*a, :depth => hdr.depth)
    else
      raise Exception, "color2alpha only intended for GRAYSCALE & RGB color modes"
    end

  color == @alpha_color ? 0 : (2**hdr.depth-1)
end

#adam7Object



60
61
62
# File 'lib/zpng/image.rb', line 60

def adam7
  @adam7 ||= Adam7Decoder.new(self)
end

#alpha_used?Boolean

Returns:

  • (Boolean)


219
220
221
# File 'lib/zpng/image.rb', line 219

def alpha_used?
  ihdr && @ihdr.alpha_used?
end

#bppObject

image attributes



199
200
201
# File 'lib/zpng/image.rb', line 199

def bpp
  ihdr && @ihdr.bpp
end

#crop(params) ⇒ Object

returns new image



415
416
417
418
419
# File 'lib/zpng/image.rb', line 415

def crop params
  decode_all_scanlines
  # deep copy first, then crop!
  deep_copy.crop!(params)
end

#crop!(params) ⇒ Object

modifies this image

Raises:



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/zpng/image.rb', line 387

def crop! params
  decode_all_scanlines

  x,y,h,w = (params[:x]||0), (params[:y]||0), params[:height], params[:width]
  raise ArgumentError, "negative params not allowed" if [x,y,h,w].any?{ |x| x < 0 }

  # adjust crop sizes if they greater than image sizes
  h = self.height-y if (y+h) > self.height
  w = self.width-x if (x+w) > self.width
  raise ArgumentError, "negative params not allowed (p2)" if [x,y,h,w].any?{ |x| x < 0 }

  # delete excess scanlines at tail
  scanlines[(y+h)..-1] = [] if (y+h) < scanlines.size

  # delete excess scanlines at head
  scanlines[0,y] = [] if y > 0

  # crop remaining scanlines
  scanlines.each{ |l| l.crop!(x,w) }

  # modify header
  hdr.height, hdr.width = h, w

  # return self
  self
end

#decode_all_scanlinesObject

we must decode all scanlines before doing any modifications or scanlines decoded AFTER modification of UPPER ones will be decoded wrong



317
318
319
320
321
# File 'lib/zpng/image.rb', line 317

def decode_all_scanlines
  return if @all_scanlines_decoded || new_image?
  @all_scanlines_decoded = true
  scanlines.each(&:decode!)
end

#deinterlaceObject

returns new deinterlaced image if deinterlaced OR returns self if no need to deinterlace



445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/zpng/image.rb', line 445

def deinterlace
  return self unless interlaced?

  # copy all but 'interlace' header params
  h = Hash[*%w'width height depth color compression filter'.map{ |k| [k.to_sym, hdr.send(k)] }.flatten]

  # don't auto-add palette chunk
  h[:palette] = nil

  # create new img
  new_img = self.class.new h

  # copy all but hdr/imagedata/end chunks
  chunks.each do |chunk|
    next if chunk.is_a?(Chunk::IHDR)
    next if chunk.is_a?(Chunk::IDAT)
    next if chunk.is_a?(Chunk::IEND)
    new_img.chunks << chunk.deep_copy
  end

  # pixel-by-pixel copy
  each_pixel do |c,x,y|
    new_img[x,y] = c
  end

  new_img
end

#each_block(bw, bh, &block) ⇒ Object



356
357
358
359
360
361
362
363
# File 'lib/zpng/image.rb', line 356

def each_block bw,bh, &block
  0.upto(height/bh-1) do |by|
    0.upto(width/bw-1) do |bx|
      b = extract_block(bx*bw, by*bh, bw, bh)
      yield b
    end
  end
end

#each_pixel(&block) ⇒ Object



435
436
437
438
439
440
441
# File 'lib/zpng/image.rb', line 435

def each_pixel &block
  height.times do |y|
    width.times do |x|
      yield(self[x,y], x, y)
    end
  end
end

#exportObject



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/zpng/image.rb', line 365

def export
  # XXX creating new IDAT must be BEFORE deleting old IDAT chunks
  idat = Chunk::IDAT.new(
    :data => Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
  )

  # delete old IDAT chunks
  @chunks.delete_if{ |c| c.is_a?(Chunk::IDAT) }

  # add newly created IDAT
  @chunks << idat

  # delete IEND chunk(s) b/c we just added a new chunk and IEND must be the last one
  @chunks.delete_if{ |c| c.is_a?(Chunk::IEND) }

  # add fresh new IEND
  @chunks << Chunk::IEND.new

  PNG_HDR + @chunks.map(&:export).join
end

#extract_block(x, y = nil, w = nil, h = nil) ⇒ Object



348
349
350
351
352
353
354
# File 'lib/zpng/image.rb', line 348

def extract_block x,y=nil,w=nil,h=nil
  if x.is_a?(Hash)
    Block.new(self,x[:x], x[:y], x[:width], x[:height])
  else
    Block.new(self,x,y,w,h)
  end
end

#grayscale?Boolean

Returns:

  • (Boolean)


211
212
213
# File 'lib/zpng/image.rb', line 211

def grayscale?
  ihdr && @ihdr.grayscale?
end

#heightObject



207
208
209
# File 'lib/zpng/image.rb', line 207

def height
  ihdr && @ihdr.height
end

#ihdrObject Also known as: header, hdr

chunks access



180
181
182
# File 'lib/zpng/image.rb', line 180

def ihdr
  @ihdr ||= @chunks.find{ |c| c.is_a?(Chunk::IHDR) }
end

#imagedata_sizeObject



250
251
252
253
254
255
256
# File 'lib/zpng/image.rb', line 250

def imagedata_size
  if new_image?
    @scanlines.map(&:size).inject(&:+)
  else
    imagedata.size
  end
end

#inspectObject



46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/zpng/image.rb', line 46

def inspect
  "#<ZPNG::Image " +
  %w'width height bpp chunks scanlines'.map do |k|
    v = case (v = send(k))
      when Array
        "[#{v.size} entries]"
      when String
        v.size > 40 ? "[#{v.bytesize} bytes]" : v.inspect
      else v.inspect
    end
    "#{k}=#{v}"
  end.compact.join(", ") + ">"
end

#interlaced?Boolean

Returns:

  • (Boolean)


215
216
217
# File 'lib/zpng/image.rb', line 215

def interlaced?
  ihdr && @ihdr.interlace != 0
end

#metadataObject

# try to get imagedata size in bytes, w/o storing entire decompressed

# stream in memory. used in bin/zpng
# result: less memory used on big images, but speed gain near 1-2% in best case,
#         and 2x slower in worst case because imagedata decoded 2 times
def imagedata_size
  if @imagedata
    # already decompressed
    @imagedata.size
  else
    zi = nil
    @imagedata_size ||=
      begin
        zi = Zlib::Inflate.new(Zlib::MAX_WBITS)
        io = StringIO.new(_imagedata)
        while !io.eof? && !zi.finished?
          n = zi.inflate(io.read(16384))
        end
        zi.finish unless zi.finished?
        zi.total_out
      ensure
        zi.close if zi && !zi.closed?
      end
  end
end


298
299
300
# File 'lib/zpng/image.rb', line 298

def 
  @metadata ||= Metadata.new(self)
end

#new_image?Boolean Also known as: new?

flag that image is just created, and NOT loaded from file as in Rails’ ActiveRecord::Base#new_record?

Returns:

  • (Boolean)


82
83
84
# File 'lib/zpng/image.rb', line 82

def new_image?
  @new_image
end

#pixelsObject



421
422
423
# File 'lib/zpng/image.rb', line 421

def pixels
  Pixels.new(self)
end

#plteObject Also known as: palette



191
192
193
# File 'lib/zpng/image.rb', line 191

def plte
  @plte ||= @chunks.find{ |c| c.is_a?(Chunk::PLTE) }
end

#save(fname) ⇒ Object

save image to file



76
77
78
# File 'lib/zpng/image.rb', line 76

def save fname
  File.open(fname,"wb"){ |f| f << export }
end

#to_ascii(*args) ⇒ Object



336
337
338
339
340
341
342
343
344
345
346
# File 'lib/zpng/image.rb', line 336

def to_ascii *args
  if scanlines.any?
    if interlaced?
      height.times.map{ |y| width.times.map{ |x| self[x,y].to_ascii(*args) }.join }.join("\n")
    else
      scanlines.map{ |l| l.to_ascii(*args) }.join("\n")
    end
  else
    super()
  end
end

#trnsObject



186
187
188
189
# File 'lib/zpng/image.rb', line 186

def trns
  # not used "@trns ||= ..." here b/c it will call find() each time of there's no TRNS chunk
  defined?(@trns) ? @trns : (@trns=@chunks.find{ |c| c.is_a?(Chunk::TRNS) })
end

#widthObject



203
204
205
# File 'lib/zpng/image.rb', line 203

def width
  ihdr && @ihdr.width
end