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
- #[](x, y) ⇒ Object
- #[]=(x, y, newcolor) ⇒ Object
- #_alpha_color(color) ⇒ Object
- #adam7 ⇒ Object
- #alpha_used? ⇒ Boolean
-
#bpp ⇒ Object
image attributes.
-
#crop(params) ⇒ Object
returns new image.
-
#crop!(params) ⇒ Object
modifies this image.
-
#deinterlace ⇒ Object
returns new deinterlaced image if deinterlaced OR returns self if no need to deinterlace.
- #export(options = {}) ⇒ Object
- #height ⇒ Object
-
#ihdr ⇒ Object
(also: #header, #hdr)
chunks access.
- #imagedata ⇒ Object
- #imagedata=(data) ⇒ Object
- #interlaced? ⇒ Boolean
- #metadata ⇒ Object
- #plte ⇒ Object (also: #palette)
- #read_png(io) ⇒ Object
- #scanlines ⇒ Object
- #to_ascii(*args) ⇒ Object
- #to_rgb ⇒ Object
- #trns ⇒ Object
- #width ⇒ Object
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 |
#adam7 ⇒ Object
74 75 76 |
# File 'lib/zimg/png.rb', line 74 def adam7 @adam7 ||= Adam7Decoder.new(width, height, bpp) end |
#alpha_used? ⇒ Boolean
70 71 72 |
# File 'lib/zimg/png.rb', line 70 def alpha_used? ihdr && @ihdr.alpha_used? end |
#bpp ⇒ Object
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
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 |
#deinterlace ⇒ Object
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( = {}) # allow :zlib_level => nil [:zlib_level] = 9 unless .key?(:zlib_level) if .fetch(:repack, true) data = Zlib::Deflate.deflate(scanlines.map(&:export).join, [: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 |
#height ⇒ Object
62 63 64 |
# File 'lib/zimg/png.rb', line 62 def height ihdr && @ihdr.height end |
#ihdr ⇒ Object 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 |
#imagedata ⇒ Object
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
66 67 68 |
# File 'lib/zimg/png.rb', line 66 def interlaced? ihdr && @ihdr.interlace != 0 end |
#metadata ⇒ Object
255 256 257 |
# File 'lib/zimg/png.rb', line 255 def @metadata ||= Metadata.new(self) end |
#plte ⇒ Object 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 |
#scanlines ⇒ Object
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_rgb ⇒ Object
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 |
#trns ⇒ Object
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 |
#width ⇒ Object
58 59 60 |
# File 'lib/zimg/png.rb', line 58 def width ihdr && @ihdr.width end |