Module: Libpng::MetadataWriter

Defined in:
lib/libpng/metadata_writer.rb

Overview

Validates and writes PNG metadata chunks (text, gamma, sRGB, chromaticities, ICC profile, pHYs) onto a (png_ptr, info_ptr) pair. Extracted from StandardEncoder so the encoder stays focused on pixel + IHDR/PLTE concerns; this module owns the metadata story.

The same module is reusable by any future encoder that wants to emit metadata -- it doesn't depend on StandardEncoder's state.

Constant Summary collapse

REQUIRED_CHRM_KEYS =
%i[
  white_point_x white_point_y
  red_x red_y
  green_x green_y
  blue_x blue_y
].freeze

Class Method Summary collapse

Class Method Details

.apply(png_ptr, info_ptr, options) ⇒ Object

Apply metadata writers in spec-defined order: text first (tEXt/zTXt/iTXt come after PLTE but before oFFs/pHYs in the canonical chunk ordering), then color-space chunks (sRGB, gAMA, cHRM, iCCP), then pHYs. libpng sorts these into the right file order on write, so call order here just needs to be consistent.



38
39
40
41
42
43
44
45
# File 'lib/libpng/metadata_writer.rb', line 38

def apply(png_ptr, info_ptr, options)
  apply_text(png_ptr, info_ptr, options[:text]) if options[:text]
  apply_srgb(png_ptr, info_ptr, options[:srgb_intent]) if options[:srgb_intent]
  apply_gamma(png_ptr, info_ptr, options[:gamma]) if options[:gamma]
  apply_chromaticities(png_ptr, info_ptr, options[:chromaticities]) if options[:chromaticities]
  apply_icc_profile(png_ptr, info_ptr, options[:icc_profile]) if options[:icc_profile]
  apply_phys(png_ptr, info_ptr, options[:phys]) if options[:phys]
end

.apply_chromaticities(png_ptr, info_ptr, chrm) ⇒ Object



114
115
116
117
118
119
120
121
122
# File 'lib/libpng/metadata_writer.rb', line 114

def apply_chromaticities(png_ptr, info_ptr, chrm)
  Libpng::Binding.png_set_cHRM(
    png_ptr, info_ptr,
    chrm[:white_point_x], chrm[:white_point_y],
    chrm[:red_x], chrm[:red_y],
    chrm[:green_x], chrm[:green_y],
    chrm[:blue_x], chrm[:blue_y]
  )
end

.apply_gamma(png_ptr, info_ptr, gamma) ⇒ Object



110
111
112
# File 'lib/libpng/metadata_writer.rb', line 110

def apply_gamma(png_ptr, info_ptr, gamma)
  Libpng::Binding.png_set_gAMA(png_ptr, info_ptr, gamma)
end

.apply_icc_profile(png_ptr, info_ptr, profile) ⇒ Object



124
125
126
127
128
129
130
# File 'lib/libpng/metadata_writer.rb', line 124

def apply_icc_profile(png_ptr, info_ptr, profile)
  data = profile[:data]
  FFI::MemoryPointer.new(:uint8, data.bytesize) do |buf|
    buf.write_bytes(data)
    Libpng::Binding.png_set_iCCP(png_ptr, info_ptr, profile[:name], 0, buf, data.bytesize)
  end
end

.apply_phys(png_ptr, info_ptr, phys) ⇒ Object



132
133
134
135
136
137
# File 'lib/libpng/metadata_writer.rb', line 132

def apply_phys(png_ptr, info_ptr, phys)
  Libpng::Binding.png_set_pHYs(png_ptr, info_ptr,
                               phys[:pixels_per_unit_x],
                               phys[:pixels_per_unit_y],
                               phys[:unit])
end

.apply_srgb(png_ptr, info_ptr, intent) ⇒ Object



106
107
108
# File 'lib/libpng/metadata_writer.rb', line 106

def apply_srgb(png_ptr, info_ptr, intent)
  Libpng::Binding.png_set_sRGB(png_ptr, info_ptr, intent)
end

.apply_text(png_ptr, info_ptr, text) ⇒ Object



101
102
103
104
# File 'lib/libpng/metadata_writer.rb', line 101

def apply_text(png_ptr, info_ptr, text)
  entries = text.map { |k, v| Libpng::TextEntry.new(key: k, value: v) }
  Libpng::TextWriter.call(png_ptr, info_ptr, entries)
end

.validate!(options) ⇒ Object

Validate the metadata portion of an options Hash. Raises Libpng::Error on any invalid value. Keys with nil values are treated as absent.



24
25
26
27
28
29
30
31
# File 'lib/libpng/metadata_writer.rb', line 24

def validate!(options)
  validate_text!(options[:text]) if options[:text]
  validate_gamma!(options[:gamma]) if options[:gamma]
  validate_srgb_intent!(options[:srgb_intent]) if options[:srgb_intent]
  validate_chromaticities!(options[:chromaticities]) if options[:chromaticities]
  validate_icc_profile!(options[:icc_profile]) if options[:icc_profile]
  validate_phys!(options[:phys]) if options[:phys]
end

.validate_chromaticities!(chrm) ⇒ Object

Raises:



70
71
72
73
74
75
# File 'lib/libpng/metadata_writer.rb', line 70

def validate_chromaticities!(chrm)
  missing = REQUIRED_CHRM_KEYS.reject { |k| chrm.is_a?(Hash) && chrm.key?(k) }
  return if missing.empty?

  raise Error, "chromaticities missing keys: #{missing.inspect}"
end

.validate_gamma!(gamma) ⇒ Object

Raises:



58
59
60
61
62
# File 'lib/libpng/metadata_writer.rb', line 58

def validate_gamma!(gamma)
  return if gamma.is_a?(Float) && gamma.positive? && gamma <= 1.0

  raise Error, 'gamma must be a Float 0 < g <= 1'
end

.validate_icc_profile!(profile) ⇒ Object

Raises:



77
78
79
80
81
82
83
84
# File 'lib/libpng/metadata_writer.rb', line 77

def validate_icc_profile!(profile)
  unless profile.is_a?(Hash) && profile[:name].is_a?(String) && profile[:data].is_a?(String)
    raise Error, 'icc_profile must be a Hash with :name (String) and :data (binary String)'
  end
  return if profile[:name].length.between?(1, 79)

  raise Error, 'icc_profile[:name] must be 1..79 chars'
end

.validate_phys!(phys) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/libpng/metadata_writer.rb', line 86

def validate_phys!(phys)
  unless phys.is_a?(Hash) && phys.key?(:pixels_per_unit_x) && phys.key?(:pixels_per_unit_y) && phys.key?(:unit)
    raise Error, 'phys must be a Hash with :pixels_per_unit_x, :pixels_per_unit_y, :unit'
  end
  unless phys[:unit].is_a?(Integer) && [0, 1].include?(phys[:unit])
    raise Error, 'phys[:unit] must be 0 (unknown) or 1 (meters)'
  end

  [phys[:pixels_per_unit_x], phys[:pixels_per_unit_y]].each do |v|
    unless v.is_a?(Integer) && v.between?(0, 0xFFFFFFFF)
      raise Error, 'phys pixels_per_unit_* must be Integer 0..2^32-1'
    end
  end
end

.validate_srgb_intent!(intent) ⇒ Object

Raises:



64
65
66
67
68
# File 'lib/libpng/metadata_writer.rb', line 64

def validate_srgb_intent!(intent)
  return if intent.is_a?(Integer) && (0..3).cover?(intent)

  raise Error, 'srgb_intent must be an Integer 0..3'
end

.validate_text!(text) ⇒ Object

Raises:



47
48
49
50
51
52
53
54
55
56
# File 'lib/libpng/metadata_writer.rb', line 47

def validate_text!(text)
  raise Error, 'text must be a Hash' unless text.is_a?(Hash)

  text.each do |key, value|
    unless key.is_a?(String) && key.length.between?(1, 79)
      raise Error, "text key #{key.inspect} must be a 1..79 char ASCII String"
    end
    raise Error, "text[#{key.inspect}] must be a String, got #{value.class}" unless value.is_a?(String)
  end
end