Module: ZIMG::PNG

Defined in:
lib/zimg/png.rb,
lib/zimg/png/chunks.rb,
lib/zimg/png/metadata.rb,
lib/zimg/png/scanline.rb,
lib/zimg/png/text_chunks.rb,
lib/zimg/png/adam7_decoder.rb,
lib/zimg/png/scanline_mixins.rb

Defined Under Namespace

Classes: Adam7Decoder, Chunk, Metadata, Scanline, TextChunk

Constant Summary collapse

MAGIC =
"\x89PNG\x0d\x0a\x1a\x0a"
COLOR_GRAYSCALE =

Each pixel is a grayscale sample

0
COLOR_RGB =

Each pixel is an R,G,B triple.

2
COLOR_INDEXED =

Each pixel is a palette index; a PLTE chunk must appear.

3
COLOR_GRAY_ALPHA =

Each pixel is a grayscale sample, followed by an alpha sample.

4
COLOR_RGBA =

Each pixel is an R,G,B triple, followed by an alpha sample.

6

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.from_rgb(data, width:, height:) ⇒ Object



105
106
107
108
109
# File 'lib/zimg/png.rb', line 105

def self.from_rgb(data, width:, height:)
  img = Image.new(width: width, height: height, bpp: 24)
  img.scanlines = height.times.map { |i| Scanline.new(img, i, decoded_bytes: data[width * 3 * i, width * 3]) }
  img
end

.from_rgba(data, width:, height:) ⇒ Object



111
112
113
114
115
# File 'lib/zimg/png.rb', line 111

def self.from_rgba(data, width:, height:)
  img = Image.new(width: width, height: height, bpp: 32)
  img.scanlines = height.times.map { |i| Scanline.new(img, i, decoded_bytes: data[width * 4 * i, width * 4]) }
  img
end

Instance Method Details

#[](x, y) ⇒ Object



78
79
80
81
82
# File 'lib/zimg/png.rb', line 78

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



84
85
86
87
88
89
# File 'lib/zimg/png.rb', line 84

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



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/zimg/png.rb', line 130

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.unpack1("n") & (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) } # rubocop:disable Lint/ShadowingOuterLocalVariable
      Color.new(*a, depth: hdr.depth)
    else
      raise StandardError, "color2alpha only intended for GRAYSCALE & RGB color modes"
    end

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

#adam7Object



74
75
76
# File 'lib/zimg/png.rb', line 74

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

#alpha_used?Boolean

Returns:

  • (Boolean)


70
71
72
# File 'lib/zimg/png.rb', line 70

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

#bppObject

image attributes



54
55
56
# File 'lib/zimg/png.rb', line 54

def bpp
  ihdr && @ihdr.bpp
end

#crop(params) ⇒ Object

returns new image



249
250
251
252
253
# File 'lib/zimg/png.rb', line 249

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

#crop!(params) ⇒ Object

modifies this image

Raises:



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/zimg/png.rb', line 221

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 = height - y if (y + h) > height
  w = width - x if (x + w) > 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

#deinterlaceObject

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



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/zimg/png.rb', line 261

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

#export(options = {}) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/zimg/png.rb', line 188

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..]
    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

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

#heightObject



62
63
64
# File 'lib/zimg/png.rb', line 62

def height
  ihdr && @ihdr.height
end

#ihdrObject Also known as: header, hdr

chunks access



35
36
37
# File 'lib/zimg/png.rb', line 35

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

#imagedataObject



91
92
93
94
95
96
97
98
# File 'lib/zimg/png.rb', line 91

def imagedata
  @imagedata ||=
    begin
      warn "[?] no image header, assuming non-interlaced RGB".yellow unless ihdr
      data = _imagedata
      data && !data.empty? ? _safe_inflate(data) : ""
    end
end

#imagedata=(data) ⇒ Object



100
101
102
103
# File 'lib/zimg/png.rb', line 100

def imagedata=(data)
  @scanlines = nil
  @imagedata = data
end

#interlaced?Boolean

Returns:

  • (Boolean)


66
67
68
# File 'lib/zimg/png.rb', line 66

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

#metadataObject



255
256
257
# File 'lib/zimg/png.rb', line 255

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

#plteObject Also known as: palette



46
47
48
# File 'lib/zimg/png.rb', line 46

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

#read_png(io) ⇒ Object



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/zimg/png.rb', line 13

def read_png(io)
  prev_chunk = nil
  until io.eof?
    chunk = Chunk.from_stream(io)
    # heuristics
    if prev_chunk&.check(type: true, crc: false) &&
       chunk.check(type: false, crc: false) && chunk.data && _apply_heuristics(io, prev_chunk, chunk)
      redo
    end
    chunk.idx = @chunks.size
    @chunks << chunk
    prev_chunk = chunk
    break if chunk.is_a?(Chunk::IEND)
  end
  return unless palette && hdr && hdr.depth

  palette.max_colors = 2**hdr.depth
end

#scanlinesObject



117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/zimg/png.rb', line 117

def scanlines
  @scanlines ||=
    begin
      r = []
      n = interlaced? ? adam7.scanlines_count : height.to_i
      n.times do |i|
        r << Scanline.new(self, i)
      end
      r.delete_if(&:bad?)
      r
    end
end

#to_ascii(*args) ⇒ Object



162
163
164
165
166
167
168
169
170
# File 'lib/zimg/png.rb', line 162

def to_ascii *args
  return unless 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
end

#to_rgbObject



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/zimg/png.rb', line 172

def to_rgb
  if hdr.color == COLOR_RGB
    scanlines.map(&:decoded_bytes).join
  else
    r = "\x00" * 3 * width * height
    i = -1
    each_pixel do |p|
      c = p.to_depth(8)
      r.setbyte(i += 1, c.r)
      r.setbyte(i += 1, c.g)
      r.setbyte(i += 1, c.b)
    end
    r
  end
end

#trnsObject



41
42
43
44
# File 'lib/zimg/png.rb', line 41

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



58
59
60
# File 'lib/zimg/png.rb', line 58

def width
  ihdr && @ihdr.width
end