libpng 1.6.58.4-aarch64-linux → 1.6.58.5-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9834653469e42649a444d48f9af9e39e2dcfeeee589b9055faa2be211d05f2b1
4
- data.tar.gz: 289091d9bc8185ce39dbbe50d4600b3abd0be6f1bd8898375b69bee450eac0bd
3
+ metadata.gz: 89fa7393749554967c34ebe20efcbd49f363173c69300f89179dda7ec7423852
4
+ data.tar.gz: 4c4294acaa4c03b0230526ecab16c7712fda4cdc637dfa8ad9968bb558d00a64
5
5
  SHA512:
6
- metadata.gz: 2c6cae57ff2bb325c3160368b00bf027656ac2e78316836434b7432b071fb2a9945726fda0f65c8ffb1a6115f4238d08a11a8b20a0e0d89cb0e8368ceb0abcd1
7
- data.tar.gz: 6ac5f58da78f92895c93e22e3b4b82d31984d109a2e5faf8ccbc9a34b100d22e02d002a943c91288e53ef6ffac8df2f1dbdc3d87e6086e25063f5af726dcd2f8
6
+ metadata.gz: d37bd40ed11951a91e1e6a7ddb5f4f655822f0b57c778dd48d93807547bc636768b1d08a3275cae927f5ad32309c0fd918eac556c15e0a347b2444e7898db911
7
+ data.tar.gz: 9a71584b9007b471af11f019e54cc6ee44dd062a79cac2caf9df6e462fe400ee050a4d93c1226c67a25e16552a1388bcaffdb8b7bcaacfc9e3055c11f4dfa4e3
data/.rubocop.yml CHANGED
@@ -22,7 +22,7 @@ Metrics/ModuleLength:
22
22
  Metrics/PerceivedComplexity:
23
23
  Max: 20
24
24
  Metrics/ParameterLists:
25
- Max: 9
25
+ Max: 10
26
26
 
27
27
  Metrics/BlockLength:
28
28
  Exclude:
@@ -37,3 +37,11 @@ Style/FrozenStringLiteralComment:
37
37
  Exclude:
38
38
  - ext/extconf.rb
39
39
  - lib/libpng/recipe.rb
40
+
41
+ # libpng's C function names use mixed case + digits (e.g.
42
+ # png_set_strip_16, png_set_expand_gray_1_2_4_to_8). We attach them
43
+ # via FFI with their canonical names so docs and headers cross-
44
+ # reference cleanly.
45
+ Naming/VariableNumber:
46
+ Exclude:
47
+ - lib/libpng/binding.rb
data/CHANGELOG.md CHANGED
@@ -7,6 +7,42 @@ This gem follows a `{LIBPNG_VERSION}.{LIBPNG_RUBY_ITERATION}` version
7
7
  scheme. `LIBPNG_VERSION` is the upstream libpng release; `ITERATION`
8
8
  bumps for Ruby-side changes and resets to 0 when LIBPNG_VERSION bumps.
9
9
 
10
+ ## [1.6.58.5] - 2026-07-26
11
+
12
+ ### Added
13
+ - **Write-side metadata on `encode_standard`**. Six new keyword options:
14
+ - `text:` -- `Hash<String,String>` of tEXt/iTXt entries. Non-ASCII
15
+ values automatically use iTXt (UTF-8) instead of tEXt (Latin-1).
16
+ - `gamma:` -- `Float` file gamma (e.g. `0.45455` for sRGB).
17
+ - `srgb_intent:` -- `Integer 0..3` rendering intent.
18
+ - `chromaticities:` -- `Hash` with the 8 cHRM primaries.
19
+ - `icc_profile:` -- `Hash` with `:name` and `:data` for an ICC
20
+ color profile (libpng compresses internally).
21
+ - `phys:` -- `Hash` with `:pixels_per_unit_x/y` and `:unit`
22
+ (`0` unknown, `1` meters).
23
+ - **`pHYs` chunk read support**. `Libpng::ChunkWalker#phys_chunk`
24
+ returns a Hash with `:pixels_per_unit_x/y`, `:unit`, and (when
25
+ `unit==1`) computed `:dpi_x/y`. `DecodedImage#phys` is populated
26
+ by both decode paths.
27
+ - **`Libpng.decode_standard`** -- standard libpng read API
28
+ (`png_create_read_struct` -> `png_read_info` -> transforms ->
29
+ `png_read_image`). Counterpart to `encode_standard`; gives
30
+ callers explicit control over which transforms apply
31
+ (palette-to-RGB, gray-to-RGB, alpha add/strip, 16-to-8 demotion,
32
+ interlace handling). Returns the same `DecodedImage` shape as
33
+ `decode`, so metadata access is uniform across both decode paths.
34
+ - New classes/modules: `Libpng::StandardDecoder`,
35
+ `Libpng::MetadataWriter`, `Libpng::TextWriter`, `Libpng::TextEntry`.
36
+ - 27 new specs across `spec/write_metadata_spec.rb` (round-trips for
37
+ every metadata type + validation errors) and
38
+ `spec/standard_decoder_spec.rb` (transform coverage + Ractor safety).
39
+
40
+ ### Fixed
41
+ - Internal: refactored `StandardEncoder` to delegate metadata writing
42
+ to `MetadataWriter`. Class length back under rubocop limits.
43
+ - Corrected `FILLER_BEFORE`/`FILLER_AFTER` constant values (the code
44
+ had them swapped -- libpng uses `BEFORE=0, AFTER=1`).
45
+
10
46
  ## [1.6.58.4] - 2026-07-26
11
47
 
12
48
  ### Added
data/CLAUDE.md CHANGED
@@ -20,12 +20,15 @@ loaded via `autoload` from `lib/libpng.rb`. **Never use `require_relative`
20
20
  | `lib/libpng.rb` | Module + FFI setup + constants + public dispatch (`encode`/`decode`/`encode_standard`) + autoloads |
21
21
  | `lib/libpng/version.rb` | `LIBPNG_VERSION`, `LIBPNG_RUBY_ITERATION`, `VERSION` |
22
22
  | `lib/libpng/error.rb` | `Libpng::Error` |
23
- | `lib/libpng/decoded_image.rb` | `Libpng::DecodedImage` (Struct returned by `decode`) |
24
- | `lib/libpng/chunk_walker.rb` | `Libpng::ChunkWalker` (walk/strip/extract metadata: `#each_chunk`, `#ihdr_fields`, `#text_chunks` for tEXt/zTXt/iTXt, `#color_chunks` for gAMA/cHRM/sRGB/iCCP, `#strip_ancillary`) |
23
+ | `lib/libpng/decoded_image.rb` | `Libpng::DecodedImage` (Struct returned by `decode` / `decode_standard`) |
24
+ | `lib/libpng/chunk_walker.rb` | `Libpng::ChunkWalker` (walk/strip/extract metadata: `#each_chunk`, `#ihdr_fields`, `#text_chunks` for tEXt/zTXt/iTXt, `#color_chunks` for gAMA/cHRM/sRGB/iCCP, `#phys_chunk` for pHYs, `#strip_ancillary`) |
25
25
  | `lib/libpng/bytes_per_pixel.rb` | `Libpng::BytesPerPixel` (pure-data lookup) |
26
26
  | `lib/libpng/simplified_encoder.rb` | `Libpng::SimplifiedEncoder` (libpng simplified write API) |
27
- | `lib/libpng/simplified_decoder.rb` | `Libpng::SimplifiedDecoder` (libpng simplified read API + IHDR metadata) |
28
- | `lib/libpng/standard_encoder.rb` | `Libpng::StandardEncoder` (libpng standard write API with filter/compression/interlace/bit_depth/palette options) |
27
+ | `lib/libpng/simplified_decoder.rb` | `Libpng::SimplifiedDecoder` (libpng simplified read API + metadata via ChunkWalker) |
28
+ | `lib/libpng/standard_encoder.rb` | `Libpng::StandardEncoder` (libpng standard write API; filter/compression/interlace/bit_depth/palette + metadata via MetadataWriter) |
29
+ | `lib/libpng/standard_decoder.rb` | `Libpng::StandardDecoder` (libpng standard read API; explicit transform control) |
30
+ | `lib/libpng/metadata_writer.rb` | `Libpng::MetadataWriter` (validates + writes text/gAMA/sRGB/cHRM/iCCP/pHYs onto a png_ptr/info_ptr pair) |
31
+ | `lib/libpng/text_writer.rb` | `Libpng::TextWriter` + `Libpng::TextEntry` (builds png_text struct array, calls `png_set_text`) |
29
32
  | `lib/libpng/recipe.rb` | `Libpng::Recipe < MiniPortileCMake` (builds libpng from source for the source gem) |
30
33
  | `ext/extconf.rb` | Gem extension entry. Triggers `Libpng::Recipe` autoload via `require 'libpng'`, then emits a dummy Makefile |
31
34
 
@@ -33,7 +36,9 @@ loaded via `autoload` from `lib/libpng.rb`. **Never use `require_relative`
33
36
 
34
37
  ```ruby
35
38
  Libpng.encode(width, height, pixels, pixel_format:, convert_to_8bit:, strip_colorspace:)
36
- Libpng.encode_standard(width, height, pixels, pixel_format:, filter:, compression_level:, interlace:, bit_depth:, palette:)
39
+ Libpng.encode_standard(width, height, pixels, pixel_format:, filter:, compression_level:, interlace:, bit_depth:, palette:, **metadata)
40
+ Libpng.decode(png, pixel_format:)
41
+ Libpng.decode_standard(png, pixel_format:, bit_depth:)
37
42
  Libpng.decode(png, pixel_format:)
38
43
  ```
39
44
 
@@ -58,7 +63,7 @@ Ruby 4.0 removed `Ractor#take`; use the helper `ractor_result(r)` which prefers
58
63
  ```sh
59
64
  bundle install
60
65
  bundle exec rake compile # build libpng via MiniPortile (needs cmake + zlib)
61
- bundle exec rake spec # all specs (~105 examples)
66
+ bundle exec rake spec # all specs (~132 examples)
62
67
  bundle exec rspec spec/libpng_spec.rb:17 # single spec by line
63
68
  bundle exec rake rubocop
64
69
  bundle exec rake # default: spec + rubocop
@@ -96,12 +101,14 @@ The workflow bumps `lib/libpng/version.rb`, pushes a `v*` tag, builds all 11 pla
96
101
  - `spec/libpng_spec.rb` — `encode`/`decode` simplified API
97
102
  - `spec/libpng_standard_spec.rb` — `encode_standard` core
98
103
  - `spec/standard_encoder_options_spec.rb` — `interlace:`/`bit_depth:`/`palette:` options
104
+ - `spec/write_metadata_spec.rb` — text/gAMA/sRGB/cHRM/pHYs write round-trips + validation
99
105
  - `spec/decoded_image_metadata_spec.rb` — IHDR metadata + `ChunkWalker`
100
- - `spec/text_chunk_spec.rb` — tEXt/zTXt/iTXt parsing
101
- - `spec/color_metadata_spec.rb` — gAMA/cHRM/sRGB/iCCP parsing
106
+ - `spec/text_chunk_spec.rb` — tEXt/zTXt/iTXt parsing (read side)
107
+ - `spec/color_metadata_spec.rb` — gAMA/cHRM/sRGB/iCCP parsing (read side)
108
+ - `spec/standard_decoder_spec.rb` — `decode_standard` transforms + Ractor safety
102
109
  - `spec/malformed_input_spec.rb` — corrupt PNG input handling
103
110
  - `spec/ractor_spec.rb` — Ractor safety for simplified API
104
- - `spec/ractor_standard_spec.rb` — Ractor safety for standard API
111
+ - `spec/ractor_standard_spec.rb` — Ractor safety for standard write API
105
112
  - `spec/benchmark_spec.rb` — encode/decode timing comparison
106
113
 
107
114
  ## See also
data/README.adoc CHANGED
@@ -71,6 +71,7 @@ for the current set):
71
71
  decoded.interlace # => 0 (PNG_INTERLACE_NONE, from IHDR)
72
72
  decoded.text # => {"Title" => "Sunset", "Author" => "Jane"} (tEXt/zTXt/iTXt)
73
73
  decoded.color # => {:gamma => 0.45455, :srgb_intent => 0, ...} (gAMA/cHRM/sRGB/iCCP)
74
+ decoded.phys # => {:pixels_per_unit_x => 3779, :unit => 1, :dpi_x => 95.99, ...} (pHYs)
74
75
 
75
76
  === Supported pixel formats
76
77
 
@@ -109,7 +110,14 @@ by `libemf2svg`'s `rgb2png` -- use `encode_standard` instead of
109
110
  compression_level: 6,
110
111
  interlace: :none,
111
112
  bit_depth: 8,
112
- palette: nil)
113
+ palette: nil,
114
+ # Metadata writers (each optional):
115
+ text: { "Title" => "Sunset", "Author" => "Jane" },
116
+ gamma: 0.45455,
117
+ srgb_intent: 0,
118
+ chromaticities: { white_point_x: 0.3127, ... },
119
+ icc_profile: { name: "sRGB IEC61966-2.1", data: bytes },
120
+ phys: { pixels_per_unit_x: 3779, pixels_per_unit_y: 3779, unit: 1 })
113
121
 
114
122
  `pixel_format`:: `:gray`, `:ga`, `:rgb`, `:rgba`, or `:palette`. The
115
123
  byte-order variants (`BGR`, `ARGB`, `BGRA`, `ABGR`) are simplified-API
@@ -148,12 +156,29 @@ Differences from `encode`:
148
156
  `ensure` block calls `png_destroy_write_struct` to release the C
149
157
  state.
150
158
 
159
+ === Standard API (decode_standard)
160
+
161
+ `Libpng.decode_standard` is the read-side counterpart to
162
+ `encode_standard`. It calls `png_create_read_struct` +
163
+ `png_set_read_fn` (memory reader) + `png_read_info` + transforms +
164
+ `png_read_image`, giving the caller explicit control over which
165
+ transforms apply (palette-to-RGB, gray-to-RGB, alpha add/strip,
166
+ 16-to-8 demotion, interlace handling).
167
+
168
+ decoded = Libpng.decode_standard(png_bytes, pixel_format: "RGBA", bit_depth: 8)
169
+
170
+ Returns the same `Libpng::DecodedImage` shape as `decode`, so metadata
171
+ fields (`text`, `color`, `phys`) work uniformly across both decode
172
+ paths. Use `decode` for the common case (auto-transforms); use
173
+ `decode_standard` when you need to know exactly which transforms
174
+ applied (e.g. for parity with `libemf2svg`'s `png2rgb`).
175
+
151
176
  == Ractor safety
152
177
 
153
- `Libpng.encode` and `Libpng.decode` are Ractor-safe. Every call
154
- allocates and frees its own libpng `png_image`; no shared mutable state
155
- exists on the Ruby side. Calls from multiple Ractors run concurrently
156
- without locking.
178
+ `Libpng.encode`, `Libpng.encode_standard`, `Libpng.decode`, and
179
+ `Libpng.decode_standard` are all Ractor-safe. Every call allocates and
180
+ frees its own libpng state; no shared mutable state exists on the Ruby
181
+ side. Calls from multiple Ractors run concurrently without locking.
157
182
 
158
183
  Example (Ruby 3.x uses `Ractor#take`; Ruby 4.0+ uses `Ractor#value`):
159
184
 
@@ -28,7 +28,9 @@ module Libpng
28
28
  # __dir__ is lib/libpng/; the shared library ships at lib/libpng/.
29
29
  ffi_lib File.expand_path(lib_filename, __dir__)
30
30
 
31
+ # ----------------------------------------------------------------
31
32
  # libpng simplified API (png_image / png_image_*).
33
+ # ----------------------------------------------------------------
32
34
  attach_function :png_image_begin_read_from_memory,
33
35
  %i[pointer pointer size_t], :int
34
36
  attach_function :png_image_finish_read,
@@ -37,7 +39,9 @@ module Libpng
37
39
  %i[pointer pointer pointer int pointer int pointer], :int
38
40
  attach_function :png_image_free, [:pointer], :void
39
41
 
40
- # libpng standard (non-simplified) write API.
42
+ # ----------------------------------------------------------------
43
+ # libpng standard write API.
44
+ # ----------------------------------------------------------------
41
45
  attach_function :png_create_write_struct,
42
46
  %i[string pointer pointer pointer], :pointer
43
47
  attach_function :png_create_info_struct, [:pointer], :pointer
@@ -53,5 +57,77 @@ module Libpng
53
57
  attach_function :png_set_write_fn,
54
58
  %i[pointer pointer pointer pointer], :void
55
59
  attach_function :png_write_png, %i[pointer pointer int pointer], :void
60
+
61
+ # ----------------------------------------------------------------
62
+ # Metadata writers (called between png_set_IHDR and png_write_png).
63
+ # ----------------------------------------------------------------
64
+ # png_text is a struct; we pass an array pointer + count.
65
+ attach_function :png_set_text,
66
+ %i[pointer pointer pointer int], :void
67
+ # file_gamma is a double.
68
+ attach_function :png_set_gAMA,
69
+ %i[pointer pointer double], :void
70
+ attach_function :png_set_sRGB,
71
+ %i[pointer pointer int], :void
72
+ # 8 chromaticity doubles (white_x, white_y, r_x, r_y, g_x, g_y, b_x, b_y).
73
+ attach_function :png_set_cHRM,
74
+ %i[pointer pointer double double double double
75
+ double double double double], :void
76
+ # png_set_iCCP(png_ptr, info_ptr, name, compression_type, profile, length)
77
+ attach_function :png_set_iCCP,
78
+ %i[pointer pointer string int pointer uint], :void
79
+ # png_set_pHYs(png_ptr, info_ptr, res_x, res_y, unit_type)
80
+ attach_function :png_set_pHYs,
81
+ %i[pointer pointer uint32 uint32 int], :void
82
+
83
+ # ----------------------------------------------------------------
84
+ # libpng standard read API.
85
+ # ----------------------------------------------------------------
86
+ attach_function :png_create_read_struct,
87
+ %i[string pointer pointer pointer], :pointer
88
+ attach_function :png_destroy_read_struct, %i[pointer pointer pointer], :void
89
+ attach_function :png_set_read_fn,
90
+ %i[pointer pointer pointer pointer], :void
91
+ attach_function :png_read_info, %i[pointer pointer], :void
92
+ attach_function :png_read_update_info, %i[pointer pointer], :void
93
+ attach_function :png_read_image, %i[pointer pointer], :void
94
+ attach_function :png_read_end, %i[pointer pointer], :void
95
+ # Returns the byte count per row AFTER any transforms are applied.
96
+ attach_function :png_get_rowbytes, %i[pointer pointer], :size_t
97
+ # Returns the post-transform color type / bit depth / channels.
98
+ attach_function :png_get_color_type, %i[pointer pointer], :uint8
99
+ attach_function :png_get_bit_depth, %i[pointer pointer], :uint8
100
+ attach_function :png_get_channels, %i[pointer pointer], :uint8
101
+ # Returns post-transform width / height (transforms rarely change these).
102
+ attach_function :png_get_image_width, %i[pointer pointer], :uint32
103
+ attach_function :png_get_image_height, %i[pointer pointer], :uint32
104
+ attach_function :png_get_interlace_type, %i[pointer pointer], :uint8
105
+ # png_get_IHDR signature differs from png_set_IHDR: widths are passed
106
+ # by reference (pointer to uint32) so libpng can write them back.
107
+ attach_function :png_get_IHDR,
108
+ %i[pointer pointer pointer pointer pointer pointer
109
+ pointer pointer pointer], :uint32
110
+
111
+ # ----------------------------------------------------------------
112
+ # Read-side transforms (call between png_read_info and png_read_image).
113
+ # Each takes only (png_ptr); they toggle internal state.
114
+ # ----------------------------------------------------------------
115
+ attach_function :png_set_expand, [:pointer], :void
116
+ attach_function :png_set_palette_to_rgb, [:pointer], :void
117
+ attach_function :png_set_tRNS_to_alpha, [:pointer], :void
118
+ attach_function :png_set_strip_alpha, [:pointer], :void
119
+ attach_function :png_set_strip_16, [:pointer], :void
120
+ attach_function :png_set_expand_gray_1_2_4_to_8, [:pointer], :void
121
+ attach_function :png_set_gray_to_rgb, [:pointer], :void
122
+ attach_function :png_set_rgb_to_gray, %i[pointer int double double], :void
123
+ attach_function :png_set_filler, %i[pointer uint int], :void
124
+ attach_function :png_set_add_alpha, %i[pointer uint int], :void
125
+ attach_function :png_set_packing, [:pointer], :void
126
+ attach_function :png_set_swap, [:pointer], :void
127
+ attach_function :png_set_interlace_handling, [:pointer], :int
128
+ attach_function :png_set_gamma, %i[pointer double double], :void
129
+
130
+ # PNG_INFO_* bit flags returned by png_get_valid.
131
+ attach_function :png_get_valid, %i[pointer pointer uint], :uint
56
132
  end
57
133
  end
@@ -159,6 +159,23 @@ module Libpng
159
159
  result
160
160
  end
161
161
 
162
+ # Parse the pHYs chunk (physical pixel dimensions). Returns nil if
163
+ # no pHYs chunk is present. Otherwise a Hash with:
164
+ #
165
+ # :pixels_per_unit_x -> Integer (uint32 from chunk)
166
+ # :pixels_per_unit_y -> Integer (uint32 from chunk)
167
+ # :unit -> Integer 0 (unknown) or 1 (meters)
168
+ # :dpi_x -> Float, only when unit == 1; otherwise nil
169
+ # :dpi_y -> Float, only when unit == 1; otherwise nil
170
+ #
171
+ # DPI conversion: 1 inch = 0.0254 m, so dpi = pixels_per_meter * 0.0254.
172
+ def phys_chunk
173
+ each_chunk do |type, data, _|
174
+ return parse_phys(data) if type == 'pHYs'
175
+ end
176
+ nil
177
+ end
178
+
162
179
  private
163
180
 
164
181
  def verify_signature
@@ -267,5 +284,23 @@ module Libpng
267
284
  rescue Zlib::Error
268
285
  # Skip malformed iCCP rather than failing the whole decode.
269
286
  end
287
+
288
+ def parse_phys(data)
289
+ return if data.length < 9
290
+
291
+ ppux, ppuy, unit = data.unpack('NNC')
292
+ result = {
293
+ pixels_per_unit_x: ppux,
294
+ pixels_per_unit_y: ppuy,
295
+ unit: unit
296
+ }
297
+ # DPI = pixels-per-meter * (1 inch / 0.0254 m).
298
+ # Only meaningful when unit == PHYS_TYPE_METER.
299
+ if unit == Libpng::PHYS_TYPE_METER
300
+ result[:dpi_x] = (ppux * Libpng::INCH_PER_METER).round(2)
301
+ result[:dpi_y] = (ppuy * Libpng::INCH_PER_METER).round(2)
302
+ end
303
+ result
304
+ end
270
305
  end
271
306
  end
@@ -15,8 +15,11 @@ module Libpng
15
15
  # value. Empty Hash when no text chunks present.
16
16
  # +color+ Hash<Symbol,*> of gAMA/cHRM/sRGB/iCCP fields. Empty
17
17
  # Hash when no color metadata present.
18
+ # +phys+ Hash<Symbol,*> from pHYs chunk, or nil when absent.
19
+ # Keys: :pixels_per_unit_x, :pixels_per_unit_y, :unit,
20
+ # and (when unit==1) :dpi_x, :dpi_y.
18
21
  DecodedImage = Struct.new(:width, :height, :format, :pixels,
19
22
  :bit_depth, :color_type, :interlace,
20
- :text, :color,
23
+ :text, :color, :phys,
21
24
  keyword_init: true)
22
25
  end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libpng
4
+ # Validates and writes PNG metadata chunks (text, gamma, sRGB,
5
+ # chromaticities, ICC profile, pHYs) onto a (png_ptr, info_ptr)
6
+ # pair. Extracted from StandardEncoder so the encoder stays focused
7
+ # on pixel + IHDR/PLTE concerns; this module owns the metadata story.
8
+ #
9
+ # The same module is reusable by any future encoder that wants to
10
+ # emit metadata -- it doesn't depend on StandardEncoder's state.
11
+ module MetadataWriter
12
+ REQUIRED_CHRM_KEYS = %i[
13
+ white_point_x white_point_y
14
+ red_x red_y
15
+ green_x green_y
16
+ blue_x blue_y
17
+ ].freeze
18
+
19
+ module_function
20
+
21
+ # Validate the metadata portion of an options Hash. Raises
22
+ # Libpng::Error on any invalid value. Keys with nil values are
23
+ # treated as absent.
24
+ def validate!(options)
25
+ validate_text!(options[:text]) if options[:text]
26
+ validate_gamma!(options[:gamma]) if options[:gamma]
27
+ validate_srgb_intent!(options[:srgb_intent]) if options[:srgb_intent]
28
+ validate_chromaticities!(options[:chromaticities]) if options[:chromaticities]
29
+ validate_icc_profile!(options[:icc_profile]) if options[:icc_profile]
30
+ validate_phys!(options[:phys]) if options[:phys]
31
+ end
32
+
33
+ # Apply metadata writers in spec-defined order: text first
34
+ # (tEXt/zTXt/iTXt come after PLTE but before oFFs/pHYs in the
35
+ # canonical chunk ordering), then color-space chunks (sRGB, gAMA,
36
+ # cHRM, iCCP), then pHYs. libpng sorts these into the right file
37
+ # order on write, so call order here just needs to be consistent.
38
+ def apply(png_ptr, info_ptr, options)
39
+ apply_text(png_ptr, info_ptr, options[:text]) if options[:text]
40
+ apply_srgb(png_ptr, info_ptr, options[:srgb_intent]) if options[:srgb_intent]
41
+ apply_gamma(png_ptr, info_ptr, options[:gamma]) if options[:gamma]
42
+ apply_chromaticities(png_ptr, info_ptr, options[:chromaticities]) if options[:chromaticities]
43
+ apply_icc_profile(png_ptr, info_ptr, options[:icc_profile]) if options[:icc_profile]
44
+ apply_phys(png_ptr, info_ptr, options[:phys]) if options[:phys]
45
+ end
46
+
47
+ def validate_text!(text)
48
+ raise Error, 'text must be a Hash' unless text.is_a?(Hash)
49
+
50
+ text.each do |key, value|
51
+ unless key.is_a?(String) && key.length.between?(1, 79)
52
+ raise Error, "text key #{key.inspect} must be a 1..79 char ASCII String"
53
+ end
54
+ raise Error, "text[#{key.inspect}] must be a String, got #{value.class}" unless value.is_a?(String)
55
+ end
56
+ end
57
+
58
+ def validate_gamma!(gamma)
59
+ return if gamma.is_a?(Float) && gamma.positive? && gamma <= 1.0
60
+
61
+ raise Error, 'gamma must be a Float 0 < g <= 1'
62
+ end
63
+
64
+ def validate_srgb_intent!(intent)
65
+ return if intent.is_a?(Integer) && (0..3).cover?(intent)
66
+
67
+ raise Error, 'srgb_intent must be an Integer 0..3'
68
+ end
69
+
70
+ def validate_chromaticities!(chrm)
71
+ missing = REQUIRED_CHRM_KEYS.reject { |k| chrm.is_a?(Hash) && chrm.key?(k) }
72
+ return if missing.empty?
73
+
74
+ raise Error, "chromaticities missing keys: #{missing.inspect}"
75
+ end
76
+
77
+ def validate_icc_profile!(profile)
78
+ unless profile.is_a?(Hash) && profile[:name].is_a?(String) && profile[:data].is_a?(String)
79
+ raise Error, 'icc_profile must be a Hash with :name (String) and :data (binary String)'
80
+ end
81
+ return if profile[:name].length.between?(1, 79)
82
+
83
+ raise Error, 'icc_profile[:name] must be 1..79 chars'
84
+ end
85
+
86
+ def validate_phys!(phys)
87
+ unless phys.is_a?(Hash) && phys.key?(:pixels_per_unit_x) && phys.key?(:pixels_per_unit_y) && phys.key?(:unit)
88
+ raise Error, 'phys must be a Hash with :pixels_per_unit_x, :pixels_per_unit_y, :unit'
89
+ end
90
+ unless phys[:unit].is_a?(Integer) && [0, 1].include?(phys[:unit])
91
+ raise Error, 'phys[:unit] must be 0 (unknown) or 1 (meters)'
92
+ end
93
+
94
+ [phys[:pixels_per_unit_x], phys[:pixels_per_unit_y]].each do |v|
95
+ unless v.is_a?(Integer) && v.between?(0, 0xFFFFFFFF)
96
+ raise Error, 'phys pixels_per_unit_* must be Integer 0..2^32-1'
97
+ end
98
+ end
99
+ end
100
+
101
+ def apply_text(png_ptr, info_ptr, text)
102
+ entries = text.map { |k, v| Libpng::TextEntry.new(key: k, value: v) }
103
+ Libpng::TextWriter.call(png_ptr, info_ptr, entries)
104
+ end
105
+
106
+ def apply_srgb(png_ptr, info_ptr, intent)
107
+ Libpng::Binding.png_set_sRGB(png_ptr, info_ptr, intent)
108
+ end
109
+
110
+ def apply_gamma(png_ptr, info_ptr, gamma)
111
+ Libpng::Binding.png_set_gAMA(png_ptr, info_ptr, gamma)
112
+ end
113
+
114
+ def apply_chromaticities(png_ptr, info_ptr, chrm)
115
+ Libpng::Binding.png_set_cHRM(
116
+ png_ptr, info_ptr,
117
+ chrm[:white_point_x], chrm[:white_point_y],
118
+ chrm[:red_x], chrm[:red_y],
119
+ chrm[:green_x], chrm[:green_y],
120
+ chrm[:blue_x], chrm[:blue_y]
121
+ )
122
+ end
123
+
124
+ def apply_icc_profile(png_ptr, info_ptr, profile)
125
+ data = profile[:data]
126
+ FFI::MemoryPointer.new(:uint8, data.bytesize) do |buf|
127
+ buf.write_bytes(data)
128
+ Libpng::Binding.png_set_iCCP(png_ptr, info_ptr, profile[:name], 0, buf, data.bytesize)
129
+ end
130
+ end
131
+
132
+ def apply_phys(png_ptr, info_ptr, phys)
133
+ Libpng::Binding.png_set_pHYs(png_ptr, info_ptr,
134
+ phys[:pixels_per_unit_x],
135
+ phys[:pixels_per_unit_y],
136
+ phys[:unit])
137
+ end
138
+ end
139
+ end
@@ -74,7 +74,8 @@ module Libpng
74
74
  color_type: ihdr[:color_type],
75
75
  interlace: ihdr[:interlace],
76
76
  text: safe_text(walker),
77
- color: safe_color(walker)
77
+ color: safe_color(walker),
78
+ phys: safe_phys(walker)
78
79
  }
79
80
  end
80
81
 
@@ -96,6 +97,12 @@ module Libpng
96
97
  {}
97
98
  end
98
99
 
100
+ def safe_phys(walker)
101
+ walker.phys_chunk
102
+ rescue ChunkWalker::FormatError
103
+ nil
104
+ end
105
+
99
106
  def read_message(img)
100
107
  s = img.get_bytes(PNG_IMAGE_OFF_MESSAGE, PNG_IMAGE_MESSAGE_BYTES)
101
108
  s = s.split("\x00").first || ''
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libpng
4
+ # Decodes a PNG byte buffer into raw pixels via libpng's standard
5
+ # read API (png_create_read_struct -> png_set_read_fn -> png_read_info
6
+ # -> optional transforms -> png_read_image). This is the read-side
7
+ # counterpart to StandardEncoder; together they expose libpng's full
8
+ # low-level read/write surface, complementing the simplified API.
9
+ #
10
+ # Why use this instead of Libpng.decode (the simplified path)?
11
+ # - Explicit control over which transforms apply (the simplified
12
+ # API auto-applies a fixed set).
13
+ # - Useful when the caller wants to inspect the source pixel format
14
+ # before deciding how to expand it (e.g. skip palette expansion
15
+ # for performance on already-RGBA images).
16
+ #
17
+ # Options:
18
+ # pixel_format: The desired output format. One of:
19
+ # :gray, :ga, :rgb, :rgba (default).
20
+ # Determines which expansion / stripping
21
+ # transforms get applied.
22
+ # bit_depth: 8 (default) or 16. 16-bit output preserves
23
+ # the source's full range when the source is
24
+ # also 16-bit; otherwise the source is
25
+ # promoted (8 -> 16 by zero-extension) or
26
+ # demoted (16 -> 8 by png_set_strip_16).
27
+ #
28
+ # Output:
29
+ # Returns a Libpng::DecodedImage. The :text, :color, :phys
30
+ # metadata fields are populated via ChunkWalker (same as the
31
+ # simplified path) so callers get the same metadata story
32
+ # regardless of which decode path they use.
33
+ #
34
+ # One instance per decode call. Ractor-safe.
35
+ class StandardDecoder
36
+ # Read-state accumulator. The libpng read callback reads PNG bytes
37
+ # out of this; the offset advances with each call. Wrapping the
38
+ # state in a small struct keeps the FFI::Function closure capture
39
+ # explicit (and the closure itself becomes trivially shareable
40
+ # because the struct's instance vars are not mutated outside the
41
+ # callback).
42
+ ReadState = Struct.new(:bytes, :offset, keyword_init: true)
43
+
44
+ # PNG file signature: 8 bytes.
45
+ SIGNATURE_BYTES = [137, 80, 78, 71, 13, 10, 26, 10].freeze
46
+
47
+ def initialize(png, pixel_format: 'RGBA', bit_depth: 8)
48
+ raise Error, 'input PNG buffer is nil' if png.nil?
49
+ raise Error, 'input PNG buffer must be a String' unless png.is_a?(String)
50
+ raise Error, 'input is too short to be a PNG' if png.bytesize < 8
51
+ raise Error, 'input is not a PNG (bad signature)' unless png.bytes.first(8) == SIGNATURE_BYTES
52
+
53
+ @png = png
54
+ @pixel_format_sym = coerce_pixel_format(pixel_format)
55
+ @target_bit_depth = bit_depth
56
+ raise Error, 'bit_depth must be 8 or 16' unless [8, 16].include?(@target_bit_depth)
57
+ end
58
+
59
+ # Returns a Libpng::DecodedImage.
60
+ def call
61
+ output = String.new.force_encoding('ASCII-8BIT')
62
+ state = ReadState.new(bytes: @png, offset: 0)
63
+ read_fn = make_read_fn(state)
64
+ error_fn = make_error_fn
65
+
66
+ png_ptr = FFI::Pointer.new(0)
67
+ info_ptr = FFI::Pointer.new(0)
68
+ begin
69
+ png_ptr = Libpng::Binding.png_create_read_struct(LIBPNG_VER_STRING_C, nil, error_fn, nil)
70
+ raise Error, 'png_create_read_struct returned NULL' if png_ptr.null?
71
+
72
+ info_ptr = Libpng::Binding.png_create_info_struct(png_ptr)
73
+ raise Error, 'png_create_info_struct returned NULL' if info_ptr.null?
74
+
75
+ # Read callback + flush (NULL); we read from memory, not a file.
76
+ Libpng::Binding.png_set_read_fn(png_ptr, nil, read_fn, nil)
77
+
78
+ Libpng::Binding.png_read_info(png_ptr, info_ptr)
79
+ width = Libpng::Binding.png_get_image_width(png_ptr, info_ptr)
80
+ height = Libpng::Binding.png_get_image_height(png_ptr, info_ptr)
81
+ src_bit_depth = Libpng::Binding.png_get_bit_depth(png_ptr, info_ptr)
82
+ src_color_type = Libpng::Binding.png_get_color_type(png_ptr, info_ptr)
83
+
84
+ apply_transforms(png_ptr, info_ptr, src_bit_depth, src_color_type)
85
+ # png_set_interlace_handling returns the pass count (1 for
86
+ # non-interlaced, 7 for Adam7). png_read_image handles the
87
+ # multi-pass accumulation automatically once this is set.
88
+ Libpng::Binding.png_set_interlace_handling(png_ptr)
89
+ Libpng::Binding.png_read_update_info(png_ptr, info_ptr)
90
+
91
+ row_bytes = Libpng::Binding.png_get_rowbytes(png_ptr, info_ptr)
92
+ out_size = row_bytes * height
93
+
94
+ FFI::MemoryPointer.new(:uint8, out_size) do |out_buf|
95
+ FFI::MemoryPointer.new(:pointer, height) do |rows|
96
+ height.times do |y|
97
+ rows.put_pointer(y * FFI.type_size(:pointer), out_buf + (y * row_bytes))
98
+ end
99
+ Libpng::Binding.png_read_image(png_ptr, rows)
100
+ end
101
+ output << out_buf.read_bytes(out_size)
102
+ end
103
+
104
+ Libpng::Binding.png_read_end(png_ptr, info_ptr)
105
+
106
+ metadata = walker_metadata
107
+ DecodedImage.new(
108
+ width: width,
109
+ height: height,
110
+ format: @pixel_format_sym.upcase.to_s,
111
+ pixels: output,
112
+ bit_depth: metadata[:bit_depth],
113
+ color_type: metadata[:color_type],
114
+ interlace: metadata[:interlace],
115
+ text: metadata[:text],
116
+ color: metadata[:color],
117
+ phys: metadata[:phys]
118
+ )
119
+ ensure
120
+ destroy(png_ptr, info_ptr)
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def coerce_pixel_format(value)
127
+ sym = value.to_s.downcase.to_sym
128
+ raise Error, "unknown pixel_format #{value.inspect}" unless FORMAT_TO_COLOR_TYPE.key?(sym)
129
+ raise Error, 'pixel_format :palette is write-only' if sym == :palette
130
+
131
+ sym
132
+ end
133
+
134
+ # Apply libpng transforms based on the requested output format and
135
+ # the source's actual color type / bit depth. The ordering matters
136
+ # -- libpng documents the canonical sequence in libpng-manual.txt
137
+ # section "Reading PNG files step-by-step".
138
+ def apply_transforms(png_ptr, info_ptr, src_bit_depth, src_color_type)
139
+ # 1. Expand everything to 8-bit first (sub-8 gray + palette).
140
+ if src_bit_depth < 8
141
+ Libpng::Binding.png_set_expand(png_ptr)
142
+ elsif src_bit_depth == 16 && @target_bit_depth == 8
143
+ Libpng::Binding.png_set_strip_16(png_ptr)
144
+ end
145
+
146
+ target_color_type = FORMAT_TO_COLOR_TYPE[@pixel_format_sym]
147
+ src_has_alpha = src_color_type.anybits?(COLOR_MASK_ALPHA)
148
+ src_has_color = src_color_type.anybits?(COLOR_MASK_COLOR)
149
+ target_has_alpha = target_color_type.anybits?(COLOR_MASK_ALPHA)
150
+ target_has_color = target_color_type.anybits?(COLOR_MASK_COLOR)
151
+
152
+ # 2. Palette -> RGB (always; we don't expose palette output).
153
+ Libpng::Binding.png_set_palette_to_rgb(png_ptr) if src_color_type == COLOR_TYPE_PALETTE
154
+
155
+ # 3. tRNS -> explicit alpha (only meaningful when caller wants alpha).
156
+ if target_has_alpha && png_get_valid(png_ptr, info_ptr, PNG_INFO_TRNS).positive?
157
+ Libpng::Binding.png_set_tRNS_to_alpha(png_ptr)
158
+ end
159
+
160
+ # 4. Gray <-> RGB conversion.
161
+ if !src_has_color && target_has_color
162
+ Libpng::Binding.png_set_gray_to_rgb(png_ptr)
163
+ elsif src_has_color && !target_has_color
164
+ Libpng::Binding.png_set_rgb_to_gray(png_ptr, RGB_TO_GRAY_DEFAULT, -1.0, -1.0)
165
+ end
166
+
167
+ # 5. Add / strip alpha to match target.
168
+ if target_has_alpha && !src_has_alpha
169
+ # Add a fully-opaque alpha channel after the RGB bytes.
170
+ Libpng::Binding.png_set_add_alpha(png_ptr, 0xFF, FILLER_AFTER)
171
+ elsif !target_has_alpha && src_has_alpha
172
+ Libpng::Binding.png_set_strip_alpha(png_ptr)
173
+ end
174
+ end
175
+
176
+ def png_get_valid(png_ptr, info_ptr, flag)
177
+ Libpng::Binding.png_get_valid(png_ptr, info_ptr, flag)
178
+ end
179
+
180
+ def make_read_fn(state)
181
+ # Closure captures `state`. Reads up to `len` bytes into `dst`,
182
+ # starting from the current offset and advancing it.
183
+ FFI::Function.new(:void, %i[pointer pointer size_t]) do |_png_ptr, dst, len|
184
+ avail = state.bytes.bytesize - state.offset
185
+ n = [len, avail].min
186
+ if n.positive?
187
+ dst.write_bytes(state.bytes.byteslice(state.offset, n), 0, n)
188
+ state.offset += n
189
+ end
190
+ # If the caller asked for more than we have, libpng treats it as
191
+ # an error (premature EOF) and longjmps out via the error handler.
192
+ end
193
+ end
194
+
195
+ # libpng's standard error path uses setjmp/longjmp. We install an
196
+ # error_fn that raises a Ruby exception instead -- the raise itself
197
+ # unwinds the C stack via FFI's safety mechanisms (rb_protect-style).
198
+ def make_error_fn
199
+ FFI::Function.new(:void, %i[pointer string]) do |_png_ptr, msg|
200
+ raise Error, "libpng: #{msg}"
201
+ end
202
+ end
203
+
204
+ def walker_metadata
205
+ walker = ChunkWalker.new(@png)
206
+ ihdr = walker.ihdr_fields
207
+ {
208
+ bit_depth: ihdr[:bit_depth],
209
+ color_type: ihdr[:color_type],
210
+ interlace: ihdr[:interlace],
211
+ text: walker.text_chunks,
212
+ color: walker.color_chunks,
213
+ phys: walker.phys_chunk
214
+ }
215
+ rescue ChunkWalker::FormatError
216
+ {
217
+ bit_depth: nil, color_type: nil, interlace: nil,
218
+ text: {}, color: {}, phys: nil
219
+ }
220
+ end
221
+
222
+ def destroy(png_ptr, info_ptr)
223
+ return if png_ptr.null?
224
+
225
+ FFI::MemoryPointer.new(:pointer) do |pp|
226
+ pp.write_pointer(png_ptr)
227
+ FFI::MemoryPointer.new(:pointer) do |ip|
228
+ ip.write_pointer(info_ptr)
229
+ Libpng::Binding.png_destroy_read_struct(pp, ip, nil)
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
@@ -19,15 +19,32 @@ module Libpng
19
19
  # palette: Array of [r, g, b] or [r, g, b, a] for
20
20
  # pixel_format: :palette. 1..256 entries.
21
21
  #
22
+ # Metadata options (forwarded to MetadataWriter; written between
23
+ # IHDR/PLTE and png_write_png):
24
+ # text: Hash<String,String> of tEXt keyword -> value.
25
+ # Values with non-ASCII bytes use iTXt (UTF-8).
26
+ # gamma: Float file gamma (e.g. 0.45455 for sRGB).
27
+ # srgb_intent: Integer 0..3 (perceptual, relative-colorimetric,
28
+ # saturation, absolute-colorimetric).
29
+ # chromaticities: Hash with :white_point_x/y, :red_x/y,
30
+ # :green_x/y, :blue_x/y (each a Float 0..1).
31
+ # icc_profile: Hash with :name (String) and :data (binary
32
+ # String). libpng compresses via zlib internally.
33
+ # phys: Hash with :pixels_per_unit_x, :pixels_per_unit_y,
34
+ # and :unit (0 = unknown, 1 = meters).
35
+ #
22
36
  # One instance per encode call. Ractor-safe.
23
37
  class StandardEncoder
38
+ METADATA_KEYS = %i[text gamma srgb_intent chromaticities icc_profile phys].freeze
39
+
24
40
  def initialize(width, height, pixels,
25
41
  pixel_format: 'RGBA',
26
42
  filter: :default,
27
43
  compression_level: 6,
28
44
  interlace: :none,
29
45
  bit_depth: 8,
30
- palette: nil)
46
+ palette: nil,
47
+ **metadata)
31
48
  @width = width
32
49
  @height = height
33
50
  @pixels = pixels
@@ -37,6 +54,7 @@ module Libpng
37
54
  @interlace_sym = interlace.to_sym
38
55
  @bit_depth = bit_depth
39
56
  @palette = palette
57
+ @metadata = metadata
40
58
  validate!
41
59
  end
42
60
 
@@ -61,6 +79,9 @@ module Libpng
61
79
  COMPRESSION_TYPE_DEFAULT, FILTER_TYPE_DEFAULT)
62
80
  apply_filter(png_ptr)
63
81
  apply_palette(png_ptr, info_ptr) if palette_format?
82
+ # Metadata writers run AFTER IHDR/PLTE and BEFORE png_write_png
83
+ # (which is what calls png_write_info -> emits all queued chunks).
84
+ MetadataWriter.apply(png_ptr, info_ptr, @metadata)
64
85
 
65
86
  FFI::MemoryPointer.new(:uint8, @pixels.bytesize) do |px|
66
87
  px.write_bytes(@pixels)
@@ -117,6 +138,7 @@ module Libpng
117
138
 
118
139
  validate_bit_depth!
119
140
  validate_palette! if palette_format?
141
+ MetadataWriter.validate!(@metadata)
120
142
  return unless @pixels.bytesize < expected_size
121
143
 
122
144
  raise Error,
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libpng
4
+ # One user-supplied text entry, normalized. The encoder decides
5
+ # whether to emit it as tEXt (Latin-1 ASCII) or iTXt (UTF-8 with
6
+ # potentially multibyte characters) based on whether the value
7
+ # contains non-ASCII bytes.
8
+ TextEntry = Struct.new(:key, :value, keyword_init: true) do
9
+ def compression
10
+ value.ascii_only? ? TEXT_COMPRESSION_NONE : TEXT_COMPRESSION_ITXT_KEY
11
+ end
12
+ end
13
+
14
+ # Writes an Array of TextEntry as a png_text array via png_set_text.
15
+ # All FFI allocations live inside the block; strings must outlive
16
+ # the png_set_text call, which copies their bytes into libpng's
17
+ # internal storage.
18
+ #
19
+ # The png_text struct layout is computed dynamically via
20
+ # FFI.type_size so the code is portable across 32-bit and 64-bit
21
+ # builds (though the gem currently only ships 64-bit binaries).
22
+ module TextWriter
23
+ module_function
24
+
25
+ def call(png_ptr, info_ptr, entries)
26
+ return if entries.empty?
27
+
28
+ struct_size = text_struct_size
29
+ FFI::MemoryPointer.new(:uint8, struct_size * entries.length) do |array|
30
+ entries.each_with_index do |entry, i|
31
+ populate_entry(array + (i * struct_size), entry)
32
+ end
33
+ Libpng::Binding.png_set_text(png_ptr, info_ptr, array, entries.length)
34
+ end
35
+ end
36
+
37
+ # png_text struct layout (computed once):
38
+ # int compression (offset 0)
39
+ # char* key (after alignment)
40
+ # char* text
41
+ # size_t text_length
42
+ # size_t itxt_length
43
+ # char* lang
44
+ # char* lang_key
45
+ def text_struct_size
46
+ int_size = FFI.type_size(:int)
47
+ ptr_size = FFI.type_size(:pointer)
48
+ sz_size = FFI.type_size(:size_t)
49
+ natural = ptr_size # both pointer and size_t align to pointer width
50
+ offset = align_up(int_size, natural)
51
+ 2.times { offset += ptr_size } # key, text
52
+ 2.times { offset += sz_size } # text_length, itxt_length
53
+ 2.times { offset += ptr_size } # lang, lang_key
54
+ align_up(offset, natural) # round up to natural struct alignment
55
+ end
56
+
57
+ def populate_entry(slot, entry)
58
+ int_size = FFI.type_size(:int)
59
+ ptr_size = FFI.type_size(:pointer)
60
+ sz_size = FFI.type_size(:size_t)
61
+ natural = ptr_size
62
+
63
+ slot.put_int(0, entry.compression)
64
+
65
+ # Allocate persistent C strings for key, text. These must
66
+ # outlive png_set_text; once that returns libpng has copied
67
+ # the bytes internally.
68
+ key_bytes = "#{entry.key.b}\x00"
69
+ val_bytes = "#{entry.value.b}\x00"
70
+ key_ptr = FFI::MemoryPointer.new(:uint8, key_bytes.bytesize, false)
71
+ key_ptr.write_bytes(key_bytes)
72
+ val_ptr = FFI::MemoryPointer.new(:uint8, val_bytes.bytesize, false)
73
+ val_ptr.write_bytes(val_bytes)
74
+
75
+ offset = align_up(int_size, natural)
76
+ slot.put_pointer(offset, key_ptr)
77
+ offset += ptr_size
78
+ slot.put_pointer(offset, val_ptr)
79
+ offset += ptr_size
80
+ put_size(slot, offset, entry.value.bytesize)
81
+ offset += sz_size
82
+ itxt_len = entry.value.ascii_only? ? 0 : entry.value.bytesize
83
+ put_size(slot, offset, itxt_len)
84
+ offset += sz_size
85
+ slot.put_pointer(offset, nil)
86
+ offset += ptr_size
87
+ slot.put_pointer(offset, nil)
88
+ end
89
+
90
+ # Write a size_t value to a slot at the given offset. FFI doesn't
91
+ # expose put_size_t directly; choose the right writer based on the
92
+ # platform's size_t width (8 bytes on 64-bit, 4 on 32-bit).
93
+ def put_size(slot, offset, value)
94
+ if FFI.type_size(:size_t) == 8
95
+ slot.put_uint64(offset, value)
96
+ else
97
+ slot.put_uint32(offset, value)
98
+ end
99
+ end
100
+
101
+ def align_up(value, alignment)
102
+ ((value + alignment - 1) / alignment) * alignment
103
+ end
104
+ end
105
+ end
@@ -11,6 +11,6 @@ module Libpng
11
11
  # new libpng release. The iteration resets to 0 each time
12
12
  # LIBPNG_VERSION bumps.
13
13
  LIBPNG_VERSION = '1.6.58'
14
- LIBPNG_RUBY_ITERATION = 4
14
+ LIBPNG_RUBY_ITERATION = 5
15
15
  VERSION = "#{LIBPNG_VERSION}.#{LIBPNG_RUBY_ITERATION}"
16
16
  end
data/lib/libpng.rb CHANGED
@@ -31,6 +31,10 @@ module Libpng
31
31
  autoload :SimplifiedEncoder, 'libpng/simplified_encoder'
32
32
  autoload :SimplifiedDecoder, 'libpng/simplified_decoder'
33
33
  autoload :StandardEncoder, 'libpng/standard_encoder'
34
+ autoload :StandardDecoder, 'libpng/standard_decoder'
35
+ autoload :TextWriter, 'libpng/text_writer'
36
+ autoload :TextEntry, 'libpng/text_writer'
37
+ autoload :MetadataWriter, 'libpng/metadata_writer'
34
38
 
35
39
  # FFI bindings. Autoloaded so that requiring `libpng` is cheap and
36
40
  # does not dlopen libpng16.{so,dylib,dll} -- ext/extconf.rb needs to
@@ -134,6 +138,76 @@ module Libpng
134
138
  all: FILTER_ALL
135
139
  }.freeze
136
140
 
141
+ # ------------------------------------------------------------------
142
+ # PNG_TEXT_COMP_* (compression field of png_text struct).
143
+ # Used by the write path to choose tEXt / zTXt / iTXt output.
144
+ # libpng's own constants are PNG_TEXT_COMPRESSION_{NONE,zTXt,iTXt}_KEY;
145
+ # we use SCREAMING_SNAKE_CASE per Ruby convention.
146
+ # ------------------------------------------------------------------
147
+ TEXT_COMPRESSION_NONE = -1 # tEXt
148
+ TEXT_COMPRESSION_ZTXT_KEY = 0 # zTXt (zlib)
149
+ TEXT_COMPRESSION_ITXT_KEY = 1 # iTXt (no compression by default)
150
+ TEXT_COMPRESSION_LAST = 2 # marker; not used as a real value
151
+
152
+ TEXT_COMPRESSION_BY_NAME = {
153
+ text: TEXT_COMPRESSION_NONE,
154
+ ztxt: TEXT_COMPRESSION_ZTXT_KEY,
155
+ itxt: TEXT_COMPRESSION_ITXT_KEY
156
+ }.freeze
157
+
158
+ # ------------------------------------------------------------------
159
+ # sRGB rendering intents (png.h).
160
+ # ------------------------------------------------------------------
161
+ SRGB_INTENT_PERCEPTUAL = 0
162
+ SRGB_INTENT_RELATIVE_COLORIMETRIC = 1
163
+ SRGB_INTENT_SATURATION = 2
164
+ SRGB_INTENT_ABSOLUTE = 3
165
+
166
+ # ------------------------------------------------------------------
167
+ # pHYs unit types (png.h).
168
+ # ------------------------------------------------------------------
169
+ PHYS_TYPE_UNKNOWN = 0
170
+ PHYS_TYPE_METER = 1
171
+
172
+ # Meters-per-inch (1 inch = 0.0254 m). Used to convert pHYs pixels-per-meter
173
+ # to DPI: dpi = ppm * 0.0254.
174
+ INCH_PER_METER = 0.0254
175
+
176
+ # ------------------------------------------------------------------
177
+ # PNG_INFO_* bit flags (png.h). Returned by png_get_valid to test
178
+ # which chunks are present in an info_ptr. Renamed to SCREAMING_SNAKE
179
+ # to match Ruby convention; libpng uses mixed case (PNG_INFO_tRNS,
180
+ # PNG_INFO_bKGD, etc.) which doesn't pass Ruby style checks.
181
+ # ------------------------------------------------------------------
182
+ PNG_INFO_IHDR = 0x0001
183
+ PNG_INFO_PLTE = 0x0002
184
+ PNG_INFO_TRNS = 0x0004
185
+ PNG_INFO_BKGD = 0x0008
186
+ PNG_INFO_HIST = 0x0010
187
+ PNG_INFO_PHYS = 0x0020
188
+ PNG_INFO_OFFS = 0x0040
189
+ PNG_INFO_TIME = 0x0080
190
+ PNG_INFO_PCAL = 0x0100
191
+ PNG_INFO_SRGB = 0x0200
192
+ PNG_INFO_ICCP = 0x0400
193
+ PNG_INFO_SBIT = 0x0800
194
+ PNG_INFO_SPLT = 0x1000
195
+ PNG_INFO_IDAT = 0x2000
196
+ PNG_INFO_ACTL = 0x4000
197
+ PNG_INFO_EXIF = 0x8000
198
+ PNG_INFO_GAMA = 0x10000
199
+ PNG_INFO_CHRM = 0x20000
200
+
201
+ # png_set_rgb_to_gray error_action values.
202
+ RGB_TO_GRAY_DEFAULT = 1 # silent
203
+ RGB_TO_GRAY_WARN = 2 # warn on error
204
+ RGB_TO_GRAY_ERR = 3 # raise error
205
+
206
+ # png_set_filler / png_set_add_alpha filler_position values.
207
+ # Matches png.h: PNG_FILLER_BEFORE = 0, PNG_FILLER_AFTER = 1.
208
+ FILLER_BEFORE = 0
209
+ FILLER_AFTER = 1
210
+
137
211
  # PNG_LIBPNG_VER_STRING. The C string passed as `user_png_ver` to
138
212
  # png_create_write_struct. libpng checks this against its compiled-in
139
213
  # version; mismatches return NULL. Must match the libpng16 binary we
@@ -174,15 +248,28 @@ module Libpng
174
248
  end
175
249
 
176
250
  # Encode raw pixels via the standard write API. Accepts filter,
177
- # compression_level, interlace, bit_depth, and palette options.
178
- # Emits only IHDR/IDAT/IEND directly (no chunk stripping needed).
251
+ # compression_level, interlace, bit_depth, and palette options,
252
+ # plus text/gamma/srgb_intent/chromaticities/icc_profile/phys
253
+ # metadata writers. Emits only the chunks libpng's standard path
254
+ # produces (no post-hoc stripping needed).
179
255
  def encode_standard(width, height, pixels, **opts)
180
256
  StandardEncoder.new(width, height, pixels, **opts).call
181
257
  end
182
258
 
183
- # Decode a PNG buffer into raw pixels plus IHDR metadata.
259
+ # Decode a PNG buffer via libpng's simplified read API. Returns a
260
+ # DecodedImage with width/height/format/pixels plus IHDR fields
261
+ # and text/color/phys metadata (parsed via ChunkWalker).
184
262
  def decode(png, **opts)
185
263
  SimplifiedDecoder.new(png, **opts).call
186
264
  end
265
+
266
+ # Decode a PNG buffer via libpng's standard read API
267
+ # (png_create_read_struct -> png_read_info -> optional transforms
268
+ # -> png_read_image). Use this instead of `decode` when you need
269
+ # explicit control over which transforms apply. Returns a
270
+ # DecodedImage with the same metadata fields as `decode`.
271
+ def decode_standard(png, **opts)
272
+ StandardDecoder.new(png, **opts).call
273
+ end
187
274
  end
188
275
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libpng
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.58.4
4
+ version: 1.6.58.5
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.
@@ -54,10 +54,13 @@ files:
54
54
  - lib/libpng/decoded_image.rb
55
55
  - lib/libpng/error.rb
56
56
  - lib/libpng/libpng16.so
57
+ - lib/libpng/metadata_writer.rb
57
58
  - lib/libpng/recipe.rb
58
59
  - lib/libpng/simplified_decoder.rb
59
60
  - lib/libpng/simplified_encoder.rb
61
+ - lib/libpng/standard_decoder.rb
60
62
  - lib/libpng/standard_encoder.rb
63
+ - lib/libpng/text_writer.rb
61
64
  - lib/libpng/version.rb
62
65
  - libpng.gemspec
63
66
  homepage: https://github.com/claricle/libpng-ruby