Class: Libpng::ChunkWalker

Inherits:
Object
  • Object
show all
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

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_chunksObject

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_chunkObject

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_dataObject

Returns the raw IHDR data bytes (13 bytes), or raises if missing.

Raises:



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_fieldsObject

Parse IHDR fields. Returns a Hash with :width, :height, :bit_depth, :color_type, :compression, :filter, :interlace.

Raises:



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_chunkObject

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_ancillaryObject

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_chunksObject

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