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


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

def initialize x, h={}
  @chunks = []
  @extradata = []
  @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



70
71
72
73
74
# File 'lib/zpng/image.rb', line 70

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

Instance Method Details

#==(other_image) ⇒ Object



500
501
502
503
504
505
506
507
508
# File 'lib/zpng/image.rb', line 500

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



366
367
368
369
370
# File 'lib/zpng/image.rb', line 366

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



372
373
374
375
376
377
# File 'lib/zpng/image.rb', line 372

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)



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/zpng/image.rb', line 179

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



64
65
66
# File 'lib/zpng/image.rb', line 64

def adam7
  @adam7 ||= Adam7Decoder.new(width, height, bpp)
end

#alpha_used?Boolean

Returns:

  • (Boolean)


255
256
257
# File 'lib/zpng/image.rb', line 255

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

#bppObject

image attributes



235
236
237
# File 'lib/zpng/image.rb', line 235

def bpp
  ihdr && @ihdr.bpp
end

#crop(params) ⇒ Object

returns new image



490
491
492
493
494
# File 'lib/zpng/image.rb', line 490

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

#crop!(params) ⇒ Object

modifies this image

Raises:



462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/zpng/image.rb', line 462

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



381
382
383
384
385
# File 'lib/zpng/image.rb', line 381

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



523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'lib/zpng/image.rb', line 523

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



420
421
422
423
424
425
426
427
# File 'lib/zpng/image.rb', line 420

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



510
511
512
513
514
515
516
517
518
519
# File 'lib/zpng/image.rb', line 510

def each_pixel &block
  e = Enumerator.new do |ee|
    height.times do |y|
      width.times do |x|
        ee.yield(self[x,y], x, y)
      end
    end
  end
  block_given? ? e.each(&block) : e
end

#export(options = {}) ⇒ Object



429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/zpng/image.rb', line 429

def export options = {}
  # allow :zlib_level => nil
  options[:zlib_level] = 9 unless options.key?(:zlib_level)

  if options.fetch(:repack, true)
    data = Zlib::Deflate.deflate(scanlines.map(&:export).join, options[:zlib_level])

    idats = @chunks.find_all{ |c| c.is_a?(Chunk::IDAT) }
    case idats.size
    when 0
      # add new IDAT
      @chunks << Chunk::IDAT.new( :data => data )
    when 1
      idats[0].data = data
    else
      idats[0].data = data
      # delete other IDAT chunks
      @chunks -= idats[1..-1]
    end
  end

  unless @chunks.last.is_a?(Chunk::IEND)
    # delete old IEND chunk(s) b/c IEND must be the last one
    @chunks.delete_if{ |c| c.is_a?(Chunk::IEND) }

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

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

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



412
413
414
415
416
417
418
# File 'lib/zpng/image.rb', line 412

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)


247
248
249
# File 'lib/zpng/image.rb', line 247

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

#heightObject



243
244
245
# File 'lib/zpng/image.rb', line 243

def height
  ihdr && @ihdr.height
end

#ihdrObject Also known as: header, hdr

chunks access



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

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

#imagedata_sizeObject



329
330
331
332
333
334
335
# File 'lib/zpng/image.rb', line 329

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

#inspectObject



50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/zpng/image.rb', line 50

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)


251
252
253
# File 'lib/zpng/image.rb', line 251

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


362
363
364
# File 'lib/zpng/image.rb', line 362

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)


86
87
88
# File 'lib/zpng/image.rb', line 86

def new_image?
  @new_image
end

#pixelsObject



496
497
498
# File 'lib/zpng/image.rb', line 496

def pixels
  Pixels.new(self)
end

#plteObject Also known as: palette



227
228
229
# File 'lib/zpng/image.rb', line 227

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

#save(fname, options = {}) ⇒ Object

save image to file



80
81
82
# File 'lib/zpng/image.rb', line 80

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

#to_ascii(*args) ⇒ Object



400
401
402
403
404
405
406
407
408
409
410
# File 'lib/zpng/image.rb', line 400

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



222
223
224
225
# File 'lib/zpng/image.rb', line 222

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



239
240
241
# File 'lib/zpng/image.rb', line 239

def width
  ihdr && @ihdr.width
end