Class: Libpng::ChunkWalker
- Inherits:
-
Object
- Object
- Libpng::ChunkWalker
- Defined in:
- lib/libpng/chunk_walker.rb
Overview
Walks a PNG byte buffer chunk-by-chunk. Used to strip ancillary chunks (sRGB, gAMA, cHRM, iCCP, etc.) that libpng's simplified write API emits by default, and to extract metadata (IHDR fields, text chunks, color chunks, pHYs) during decode.
The chunk layout is documented in the PNG spec:
[4 bytes length][4 bytes type][length bytes data][4 bytes CRC]
Length and CRC are big-endian uint32. The signature is the 8 bytes before the first chunk: 89 50 4E 47 0D 0A 1A 0A.
Defined Under Namespace
Classes: FormatError
Constant Summary collapse
- SIGNATURE =
[137, 80, 78, 71, 13, 10, 26, 10].freeze
- GAMMA_SCALE =
PNG gAMA fixed-point scale (per spec: gamma = uint32 / 100000).
100_000.0- CHRM_SCALE =
PNG cHRM fixed-point scale (per spec: chromaticity = uint32 / 100000).
100_000.0
Instance Method Summary collapse
-
#color_chunks ⇒ Object
Parse color-space chunks (gAMA, cHRM, sRGB, iCCP) into a Hash.
-
#each_chunk ⇒ Object
Iterate every chunk in order.
-
#ihdr_data ⇒ Object
Returns the raw IHDR data bytes (13 bytes), or raises if missing.
-
#ihdr_fields ⇒ Object
Parse IHDR fields.
-
#initialize(png_bytes) ⇒ ChunkWalker
constructor
A new instance of ChunkWalker.
-
#phys_chunk ⇒ Object
Parse the pHYs chunk (physical pixel dimensions).
-
#strip_ancillary ⇒ Object
Returns a new binary String containing only the PNG signature plus IHDR, IDAT, and IEND chunks.
-
#text_chunks ⇒ Object
Parse text chunks (tEXt, zTXt, iTXt) into a flat Hash of keyword -> UTF-8 String.
Constructor Details
#initialize(png_bytes) ⇒ ChunkWalker
Returns a new instance of ChunkWalker.
28 29 30 |
# File 'lib/libpng/chunk_walker.rb', line 28 def initialize(png_bytes) @bytes = png_bytes.bytes end |
Instance Method Details
#color_chunks ⇒ Object
Parse color-space chunks (gAMA, cHRM, sRGB, iCCP) into a Hash. Returns Symbol keys:
:gamma -> Float (gAMA, value/100000)
:white_point_x, :white_point_y -> Float (cHRM)
:red_x, :red_y, :green_x, :green_y, :blue_x, :blue_y -> Float
:srgb_intent -> Integer 0..3 (sRGB)
:icc_profile_name -> UTF-8 String (iCCP, profile name)
:icc_profile -> binary String (iCCP, decompressed bytes)
Malformed chunks (e.g. truncated cHRM, bad iCCP compression) are silently skipped. ICC profiles can be large (10KB-1MB); callers who don't need them should walk chunks manually rather than call this method.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/libpng/chunk_walker.rb', line 145 def color_chunks result = {} each_chunk do |type, data, _| case type when 'gAMA' parse_gama(data, result) when 'cHRM' parse_chrm(data, result) when 'sRGB' parse_srgb(data, result) when 'iCCP' parse_iccp(data, result) end end result end |
#each_chunk ⇒ Object
Iterate every chunk in order. Yields [type_string, data_bytes, offset] to the block. Returns an Enumerator if no block given.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/libpng/chunk_walker.rb', line 34 def each_chunk return enum_for(:each_chunk) unless block_given? verify_signature offset = 8 until offset + 8 > @bytes.length len = @bytes[offset, 4].pack('C*').unpack1('N') type = @bytes[offset + 4, 4].pack('C*') chunk_total = 12 + len if offset + chunk_total > @bytes.length raise FormatError, "PNG chunk at offset #{offset} (#{type}) runs past EOF" end crc_input = @bytes[offset + 4, 4 + len].pack('C*') crc_actual = @bytes[offset + 8 + len, 4].pack('C*').unpack1('N') crc_expected = Zlib.crc32(crc_input) raise FormatError, "PNG chunk CRC mismatch at offset #{offset} (#{type})" unless crc_actual == crc_expected data = @bytes[offset + 8, len].pack('C*') yield type, data, offset offset += chunk_total break if type == 'IEND' end end |
#ihdr_data ⇒ Object
Returns the raw IHDR data bytes (13 bytes), or raises if missing.
79 80 81 82 |
# File 'lib/libpng/chunk_walker.rb', line 79 def ihdr_data each_chunk { |type, data, _| return data if type == 'IHDR' } raise FormatError, 'PNG has no IHDR chunk' end |
#ihdr_fields ⇒ Object
Parse IHDR fields. Returns a Hash with :width, :height, :bit_depth, :color_type, :compression, :filter, :interlace.
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/libpng/chunk_walker.rb', line 86 def ihdr_fields data = ihdr_data raise FormatError, "IHDR is #{data.length} bytes, expected 13" unless data.length == 13 width, height, bd, ct, comp, filt, intc = data.unpack('NNCCCCC') { width: width, height: height, bit_depth: bd, color_type: ct, compression: comp, filter: filt, interlace: intc } end |
#phys_chunk ⇒ Object
Parse the pHYs chunk (physical pixel dimensions). Returns nil if no pHYs chunk is present. Otherwise a Hash with:
:pixels_per_unit_x -> Integer (uint32 from chunk)
:pixels_per_unit_y -> Integer (uint32 from chunk)
:unit -> Integer 0 (unknown) or 1 (meters)
:dpi_x -> Float, only when unit == 1; otherwise nil
:dpi_y -> Float, only when unit == 1; otherwise nil
DPI conversion: 1 inch = 0.0254 m, so dpi = pixels_per_meter * 0.0254.
172 173 174 175 176 177 |
# File 'lib/libpng/chunk_walker.rb', line 172 def phys_chunk each_chunk do |type, data, _| return parse_phys(data) if type == 'pHYs' end nil end |
#strip_ancillary ⇒ Object
Returns a new binary String containing only the PNG signature plus IHDR, IDAT, and IEND chunks. Other chunks (sRGB, gAMA, cHRM, iCCP, tEXt, zTXt, iTXt, bKGD, pHYs, oFFs, tIME, sCAL, hIST, sPLT, unknown chunks) are dropped.
65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/libpng/chunk_walker.rb', line 65 def strip_ancillary kept = SIGNATURE.pack('C*') each_chunk do |type, data, _offset| next unless %w[IHDR IDAT IEND].include?(type) len_bytes = [data.length].pack('N') type_bytes = type crc_bytes = [Zlib.crc32(type_bytes + data)].pack('N') kept << len_bytes << type_bytes << data << crc_bytes end kept.force_encoding('ASCII-8BIT') end |
#text_chunks ⇒ Object
Parse text chunks (tEXt, zTXt, iTXt) into a flat Hash of keyword -> UTF-8 String. When the same keyword appears in multiple chunks, the last occurrence wins (PNG spec permits this; behavior matches libpng's read path).
- tEXt: Latin-1 keyword + Latin-1 value, transcoded to UTF-8.
- zTXt: Latin-1 keyword + zlib-compressed Latin-1 value, inflated then transcoded to UTF-8.
- iTXt: UTF-8 keyword + UTF-8 value (optionally zlib-compressed), with separate language tag and translated keyword that are dropped here (callers who need them should walk chunks directly).
Malformed chunks are silently skipped: a single broken zTXt should not poison the rest of the decode.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/libpng/chunk_walker.rb', line 116 def text_chunks result = {} each_chunk do |type, data, _| case type when 'tEXt' parse_text(data, result) when 'zTXt' parse_ztxt(data, result) when 'iTXt' parse_itxt(data, result) end end result end |