libpng 1.6.58.4 → 1.6.58.5
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 +4 -4
- data/.rubocop.yml +9 -1
- data/CHANGELOG.md +36 -0
- data/CLAUDE.md +16 -9
- data/README.adoc +30 -5
- data/lib/libpng/binding.rb +77 -1
- data/lib/libpng/chunk_walker.rb +35 -0
- data/lib/libpng/decoded_image.rb +4 -1
- data/lib/libpng/metadata_writer.rb +139 -0
- data/lib/libpng/simplified_decoder.rb +8 -1
- data/lib/libpng/standard_decoder.rb +234 -0
- data/lib/libpng/standard_encoder.rb +23 -1
- data/lib/libpng/text_writer.rb +105 -0
- data/lib/libpng/version.rb +1 -1
- data/lib/libpng.rb +90 -3
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9edc7cf7190ff782780b914a63bc3a26cd3c1dd71199c26ec144e3e3f72619f
|
|
4
|
+
data.tar.gz: bed30533f0f5b33e4064213aa6bacecb8e815ca56234a8991ae8af70627cb40f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e94cbd6af3fbc2accb177d8f3adc9565582d1853e9f998ff314934b6b5f2816f058164b40a32ee7661b55ea5c58e920c03d600d30e49e7f1d51d5f71e280f3f7
|
|
7
|
+
data.tar.gz: 88fca2579a17b68e184b381483687e43eef88d5a104ac74deb12056bd376e494db7e3b795312fad2042219979a0c9119b2daef8cd4dd2e47c6cf49925a2f5274
|
data/.rubocop.yml
CHANGED
|
@@ -22,7 +22,7 @@ Metrics/ModuleLength:
|
|
|
22
22
|
Metrics/PerceivedComplexity:
|
|
23
23
|
Max: 20
|
|
24
24
|
Metrics/ParameterLists:
|
|
25
|
-
Max:
|
|
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 +
|
|
28
|
-
| `lib/libpng/standard_encoder.rb` | `Libpng::StandardEncoder` (libpng standard write API
|
|
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 (~
|
|
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`
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
data/lib/libpng/binding.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
data/lib/libpng/chunk_walker.rb
CHANGED
|
@@ -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
|
data/lib/libpng/decoded_image.rb
CHANGED
|
@@ -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
|
data/lib/libpng/version.rb
CHANGED
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
|
-
#
|
|
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
|
|
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
|
+
version: 1.6.58.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -68,10 +68,13 @@ files:
|
|
|
68
68
|
- lib/libpng/chunk_walker.rb
|
|
69
69
|
- lib/libpng/decoded_image.rb
|
|
70
70
|
- lib/libpng/error.rb
|
|
71
|
+
- lib/libpng/metadata_writer.rb
|
|
71
72
|
- lib/libpng/recipe.rb
|
|
72
73
|
- lib/libpng/simplified_decoder.rb
|
|
73
74
|
- lib/libpng/simplified_encoder.rb
|
|
75
|
+
- lib/libpng/standard_decoder.rb
|
|
74
76
|
- lib/libpng/standard_encoder.rb
|
|
77
|
+
- lib/libpng/text_writer.rb
|
|
75
78
|
- lib/libpng/version.rb
|
|
76
79
|
- libpng.gemspec
|
|
77
80
|
homepage: https://github.com/claricle/libpng-ruby
|