libpng 1.6.58.3 → 1.6.58.4
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/.github/workflows/build.yml +27 -6
- data/.github/workflows/release.yml +20 -2
- data/.gitignore +2 -0
- data/.rspec +1 -1
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +7 -6
- data/README.adoc +6 -2
- data/Rakefile +1 -0
- data/lib/libpng/chunk_walker.rb +169 -1
- data/lib/libpng/decoded_image.rb +5 -0
- data/lib/libpng/recipe.rb +12 -3
- data/lib/libpng/simplified_decoder.rb +37 -14
- data/lib/libpng/version.rb +1 -1
- metadata +1 -22
- data/TODO.roadmap/01-claude-md-update.md +0 -27
- data/TODO.roadmap/02-encode-standard-ractor-spec.md +0 -24
- data/TODO.roadmap/03-mini-portile2-bump.md +0 -26
- data/TODO.roadmap/04-changelog.md +0 -34
- data/TODO.roadmap/05-benchmarks.md +0 -41
- data/TODO.roadmap/06-malformed-input-tests.md +0 -47
- data/TODO.roadmap/07-interlace-option.md +0 -40
- data/TODO.roadmap/08-palette-support.md +0 -54
- data/TODO.roadmap/09-16-bit-encode.md +0 -53
- data/TODO.roadmap/10-libpng-1.6.59-bump.md +0 -59
- data/TODO.roadmap/11-read-side-metadata.md +0 -59
- data/TODO.roadmap/12-full-api-surface.md +0 -61
- data/TODO.roadmap/13-c-extension-setjmp.md +0 -77
- data/TODO.roadmap/14-progressive-decode.md +0 -47
- data/TODO.roadmap/15-file-io-variants.md +0 -57
- data/TODO.roadmap/16-architecture-refactor.md +0 -97
- data/TODO.roadmap/17-ruby-version-policy.md +0 -46
- data/TODO.roadmap/18-dependabot.md +0 -52
- data/TODO.roadmap/19-text-chunk-read.md +0 -104
- data/TODO.roadmap/20-color-metadata-read.md +0 -100
- data/TODO.roadmap/README.md +0 -42
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3764dc7e75c29be74bf732ba3d3ac4a66b17c4d20f5e1404f7d1d4fb4a7bff5d
|
|
4
|
+
data.tar.gz: 81d239f7c17eef66172e93543c949d1cee3a5d5bd281f997c3e26153e7beafe5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d7d3caf69638aaf251a34631346007aed404d662a5d238dadf628b0f202ff2db51a751c6de310e76358c974c9b8d12207cb9f89f0f7c8b6d45fd47ae8d0a5be
|
|
7
|
+
data.tar.gz: aedc2495d938619919ba47c65fb1f54f3b38af2cddcf39d38fef0eb9464031d5dcdc3922f3e6230d2bf05c612e271c28e0878ef263b3200c5a15c14336453c97
|
data/.github/workflows/build.yml
CHANGED
|
@@ -143,6 +143,12 @@ jobs:
|
|
|
143
143
|
platform: x86_64-linux-musl
|
|
144
144
|
- os: ubuntu-24.04-arm
|
|
145
145
|
platform: aarch64-linux-musl
|
|
146
|
+
# OHOS (OpenHarmony / Huawei HarmonyOS PC) is musl-based arm64.
|
|
147
|
+
# Built inside the same Alpine container as aarch64-linux-musl;
|
|
148
|
+
# the resulting binary is byte-compatible. Only the gem's
|
|
149
|
+
# platform label differs so RubyGems on OHOS selects it.
|
|
150
|
+
- os: ubuntu-24.04-arm
|
|
151
|
+
platform: aarch64-linux-ohos
|
|
146
152
|
|
|
147
153
|
steps:
|
|
148
154
|
- uses: actions/checkout@v4
|
|
@@ -168,12 +174,27 @@ jobs:
|
|
|
168
174
|
git config --global --add safe.directory /work
|
|
169
175
|
bundle install --jobs 4
|
|
170
176
|
bundle exec rake "gem:native:${PLATFORM}"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
case "${PLATFORM}" in
|
|
178
|
+
*-ohos)
|
|
179
|
+
# OHOS gem is labeled aarch64-linux-ohos so RubyGems on OHOS
|
|
180
|
+
# selects it, but Alpine Ruby reports *-linux-musl -- a
|
|
181
|
+
# platform mismatch. The .so inside is musl-compatible, so
|
|
182
|
+
# unpack the gem and load it via -Ilib directly to verify
|
|
183
|
+
# the binary works without going through RubyGems install.
|
|
184
|
+
mkdir -p /tmp/ohos-smoke
|
|
185
|
+
gem unpack pkg/libpng-*.gem --target /tmp/ohos-smoke
|
|
186
|
+
(cd /tmp/ohos-smoke/libpng-* && \
|
|
187
|
+
ruby -Ilib -e 'require "libpng"; png = Libpng.encode(2, 2, "\xff" * 16, pixel_format: "RGBA"); puts Libpng.decode(png, pixel_format: "RGBA").inspect')
|
|
188
|
+
;;
|
|
189
|
+
*)
|
|
190
|
+
gem install -b pkg/libpng-*.gem
|
|
191
|
+
# Smoke test from /tmp so bundler's source-tree LOAD_PATH doesn't
|
|
192
|
+
# shadow the installed gem. Activate via `gem` so RubyGems resolves
|
|
193
|
+
# the platform-specific binary gem (Alpine Ruby's platform string
|
|
194
|
+
# is *-linux-musl, which must match the gem's platform suffix).
|
|
195
|
+
(cd /tmp && ruby -e 'gem "libpng"; require "libpng"; png = Libpng.encode(2, 2, "\xff" * 16, pixel_format: "RGBA"); File.binwrite("test.png", png); puts Libpng.decode(png, pixel_format: "RGBA").inspect')
|
|
196
|
+
;;
|
|
197
|
+
esac
|
|
177
198
|
BUILD_EOF
|
|
178
199
|
chmod +x /tmp/alpine-build.sh
|
|
179
200
|
docker run --rm \
|
|
@@ -201,6 +201,10 @@ jobs:
|
|
|
201
201
|
platform: x86_64-linux-musl
|
|
202
202
|
- os: ubuntu-24.04-arm
|
|
203
203
|
platform: aarch64-linux-musl
|
|
204
|
+
# OHOS: same Alpine-built musl arm64 binary as aarch64-linux-musl,
|
|
205
|
+
# packaged under a distinct platform label.
|
|
206
|
+
- os: ubuntu-24.04-arm
|
|
207
|
+
platform: aarch64-linux-ohos
|
|
204
208
|
steps:
|
|
205
209
|
- uses: actions/checkout@v4
|
|
206
210
|
with:
|
|
@@ -218,8 +222,22 @@ jobs:
|
|
|
218
222
|
git config --global --add safe.directory /work
|
|
219
223
|
bundle install --jobs 4
|
|
220
224
|
bundle exec rake "gem:native:${PLATFORM}"
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
case "${PLATFORM}" in
|
|
226
|
+
*-ohos)
|
|
227
|
+
# OHOS gem is labeled aarch64-linux-ohos so RubyGems on OHOS
|
|
228
|
+
# selects it, but Alpine Ruby reports *-linux-musl. The .so
|
|
229
|
+
# inside is musl-compatible -- unpack and load via -Ilib to
|
|
230
|
+
# verify the binary without going through RubyGems install.
|
|
231
|
+
mkdir -p /tmp/ohos-smoke
|
|
232
|
+
gem unpack pkg/libpng-*.gem --target /tmp/ohos-smoke
|
|
233
|
+
(cd /tmp/ohos-smoke/libpng-* && \
|
|
234
|
+
ruby -Ilib -e 'require "libpng"; png = Libpng.encode(2, 2, "\xff" * 16, pixel_format: "RGBA"); puts Libpng.decode(png, pixel_format: "RGBA").inspect')
|
|
235
|
+
;;
|
|
236
|
+
*)
|
|
237
|
+
gem install -b pkg/libpng-*.gem
|
|
238
|
+
(cd /tmp && ruby -e 'gem "libpng"; require "libpng"; png = Libpng.encode(2, 2, "\xff" * 16, pixel_format: "RGBA"); puts Libpng.decode(png, pixel_format: "RGBA").inspect')
|
|
239
|
+
;;
|
|
240
|
+
esac
|
|
223
241
|
BUILD_EOF
|
|
224
242
|
chmod +x /tmp/alpine-build.sh
|
|
225
243
|
docker run --rm \
|
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ 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.4] - 2026-07-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Libpng::ChunkWalker#text_chunks` -- parses `tEXt`, `zTXt`, and `iTXt`
|
|
14
|
+
chunks into a flat Hash of keyword -> UTF-8 String. zTXt values are
|
|
15
|
+
zlib-inflated; tEXt/zTXt values are transcoded from Latin-1 to UTF-8;
|
|
16
|
+
iTXt values stay UTF-8 (with optional compression). Malformed chunks
|
|
17
|
+
are silently skipped so a single broken entry doesn't poison the rest
|
|
18
|
+
of the decode.
|
|
19
|
+
- `Libpng::ChunkWalker#color_chunks` -- parses `gAMA`, `cHRM`, `sRGB`,
|
|
20
|
+
and `iCCP` chunks into a Hash with Symbol keys (`:gamma`,
|
|
21
|
+
`:white_point_x/y`, `:red_x/y`, `:green_x/y`, `:blue_x/y`,
|
|
22
|
+
`:srgb_intent`, `:icc_profile_name`, `:icc_profile`). iCCP profiles
|
|
23
|
+
are zlib-decompressed into raw binary bytes for downstream ICC
|
|
24
|
+
libraries.
|
|
25
|
+
- `Libpng::DecodedImage#text` and `#color` -- new keyword Struct fields,
|
|
26
|
+
populated by `Libpng.decode` via the new ChunkWalker accessors. Both
|
|
27
|
+
default to an empty Hash when the source PNG has no relevant chunks.
|
|
28
|
+
- New platform: `aarch64-linux-ohos` (OpenHarmony / Huawei HarmonyOS PC).
|
|
29
|
+
OHOS is musl-based arm64 -- the resulting shared library is
|
|
30
|
+
byte-compatible with `aarch64-linux-musl`; only the gem's platform
|
|
31
|
+
label differs so RubyGems on OHOS selects the right variant. Built in
|
|
32
|
+
the same Alpine container as the musl gem.
|
|
33
|
+
- Specs: 24 new specs across `spec/text_chunk_spec.rb` and
|
|
34
|
+
`spec/color_metadata_spec.rb` covering all 7 chunk types, malformed
|
|
35
|
+
input handling, Ractor moving, and end-to-end metadata exposure on
|
|
36
|
+
`DecodedImage`.
|
|
37
|
+
|
|
10
38
|
## [1.6.58.3] - 2026-07-26
|
|
11
39
|
|
|
12
40
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
6
6
|
|
|
7
7
|
`libpng` is a Ruby gem that wraps the official libpng shared library via FFI. The native `libpng16.{so,dylib,dll}` is **pre-compiled per target platform and shipped inside the gem** — `gem install libpng` must not require a C compiler on the host.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
11 platform gems are published per release (10 native + the source `ruby` gem). Native targets: `x86_64-linux`, `x86_64-linux-musl`, `aarch64-linux`, `aarch64-linux-musl`, `aarch64-linux-ohos`, `x64-mingw32`, `x64-mingw-ucrt`, `aarch64-mingw-ucrt`, `x86_64-darwin`, `arm64-darwin`.
|
|
10
10
|
|
|
11
11
|
## Architecture
|
|
12
12
|
|
|
@@ -21,7 +21,7 @@ loaded via `autoload` from `lib/libpng.rb`. **Never use `require_relative`
|
|
|
21
21
|
| `lib/libpng/version.rb` | `LIBPNG_VERSION`, `LIBPNG_RUBY_ITERATION`, `VERSION` |
|
|
22
22
|
| `lib/libpng/error.rb` | `Libpng::Error` |
|
|
23
23
|
| `lib/libpng/decoded_image.rb` | `Libpng::DecodedImage` (Struct returned by `decode`) |
|
|
24
|
-
| `lib/libpng/chunk_walker.rb` | `Libpng::ChunkWalker` (walk/strip/extract metadata
|
|
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`) |
|
|
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
27
|
| `lib/libpng/simplified_decoder.rb` | `Libpng::SimplifiedDecoder` (libpng simplified read API + IHDR metadata) |
|
|
@@ -58,7 +58,7 @@ Ruby 4.0 removed `Ractor#take`; use the helper `ractor_result(r)` which prefers
|
|
|
58
58
|
```sh
|
|
59
59
|
bundle install
|
|
60
60
|
bundle exec rake compile # build libpng via MiniPortile (needs cmake + zlib)
|
|
61
|
-
bundle exec rake spec # all specs (~
|
|
61
|
+
bundle exec rake spec # all specs (~105 examples)
|
|
62
62
|
bundle exec rspec spec/libpng_spec.rb:17 # single spec by line
|
|
63
63
|
bundle exec rake rubocop
|
|
64
64
|
bundle exec rake # default: spec + rubocop
|
|
@@ -67,7 +67,7 @@ bundle exec rake gem:native:arm64-darwin # build a pre-compiled gem
|
|
|
67
67
|
bundle exec rake gem:native:any # source gem (compiles on install)
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
Platform gem tasks: `x64-mingw32`, `x64-mingw-ucrt`, `aarch64-mingw-ucrt`, `x86_64-linux`, `x86_64-linux-musl`, `aarch64-linux`, `aarch64-linux-musl`, `x86_64-darwin`, `arm64-darwin`.
|
|
70
|
+
Platform gem tasks: `x64-mingw32`, `x64-mingw-ucrt`, `aarch64-mingw-ucrt`, `x86_64-linux`, `x86_64-linux-musl`, `aarch64-linux`, `aarch64-linux-musl`, `aarch64-linux-ohos`, `x86_64-darwin`, `arm64-darwin`.
|
|
71
71
|
|
|
72
72
|
## Release process
|
|
73
73
|
|
|
@@ -79,7 +79,7 @@ gh workflow run release.yml --repo claricle/libpng-ruby --ref main -f bump-type=
|
|
|
79
79
|
# or: -f bump-type=current # release current VERSION as-is
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
The workflow bumps `lib/libpng/version.rb`, pushes a `v*` tag, builds all
|
|
82
|
+
The workflow bumps `lib/libpng/version.rb`, pushes a `v*` tag, builds all 11 platform gems (10 native + the source `ruby` gem), and publishes to RubyGems via OIDC Trusted Publishing.
|
|
83
83
|
|
|
84
84
|
**Never** push tags or merge to main directly — always go through PRs.
|
|
85
85
|
|
|
@@ -97,6 +97,8 @@ The workflow bumps `lib/libpng/version.rb`, pushes a `v*` tag, builds all 10 pla
|
|
|
97
97
|
- `spec/libpng_standard_spec.rb` — `encode_standard` core
|
|
98
98
|
- `spec/standard_encoder_options_spec.rb` — `interlace:`/`bit_depth:`/`palette:` options
|
|
99
99
|
- `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
|
|
100
102
|
- `spec/malformed_input_spec.rb` — corrupt PNG input handling
|
|
101
103
|
- `spec/ractor_spec.rb` — Ractor safety for simplified API
|
|
102
104
|
- `spec/ractor_standard_spec.rb` — Ractor safety for standard API
|
|
@@ -104,6 +106,5 @@ The workflow bumps `lib/libpng/version.rb`, pushes a `v*` tag, builds all 10 pla
|
|
|
104
106
|
|
|
105
107
|
## See also
|
|
106
108
|
|
|
107
|
-
- `TODO.roadmap/` — prioritized backlog with status (done/planned/wontdo) per item
|
|
108
109
|
- `CHANGELOG.md` — release history
|
|
109
110
|
- `REPORT-complex-api-needs.md` — historical report that drove `encode_standard`
|
data/README.adoc
CHANGED
|
@@ -31,8 +31,9 @@ source tarball at install time and compiles it via CMake (requires
|
|
|
31
31
|
|
|
32
32
|
== Supported platforms
|
|
33
33
|
|
|
34
|
-
The following pre-compiled platform gems are published for each
|
|
35
|
-
|
|
34
|
+
The following 11 pre-compiled platform gems are published for each
|
|
35
|
+
release -- 10 native targets plus the platform-agnostic source (`ruby`)
|
|
36
|
+
gem (see the link:https://rubygems.org/gems/libpng/versions[RubyGems versions page]
|
|
36
37
|
for the current set):
|
|
37
38
|
|
|
38
39
|
[cols="1,1,3", options="header"]
|
|
@@ -43,6 +44,7 @@ for the current set):
|
|
|
43
44
|
| `x86_64-linux-musl` | x86_64 Linux (musl, e.g. Alpine) | `ruby:<ver>-alpine` on x86_64
|
|
44
45
|
| `aarch64-linux` | ARM64 Linux (glibc) | `ubuntu-24.04-arm` (native)
|
|
45
46
|
| `aarch64-linux-musl` | ARM64 Linux (musl) | `ruby:<ver>-alpine` on arm64
|
|
47
|
+
| `aarch64-linux-ohos` | ARM64 OpenHarmony / Huawei HarmonyOS PC | `ruby:<ver>-alpine` on arm64 (byte-compatible with `aarch64-linux-musl`)
|
|
46
48
|
| `x64-mingw32` | x64 Windows, RubyInstaller < 3.0 (MSVCRT) | `windows-latest`
|
|
47
49
|
| `x64-mingw-ucrt` | x64 Windows, RubyInstaller >= 3.0 (UCRT) | `windows-latest`
|
|
48
50
|
| `aarch64-mingw-ucrt` | ARM64 Windows (Ruby >= 3.4) | `windows-11-arm` (native)
|
|
@@ -67,6 +69,8 @@ for the current set):
|
|
|
67
69
|
decoded.bit_depth # => 8 (from IHDR)
|
|
68
70
|
decoded.color_type # => 6 (PNG_COLOR_TYPE_RGB_ALPHA, from IHDR)
|
|
69
71
|
decoded.interlace # => 0 (PNG_INTERLACE_NONE, from IHDR)
|
|
72
|
+
decoded.text # => {"Title" => "Sunset", "Author" => "Jane"} (tEXt/zTXt/iTXt)
|
|
73
|
+
decoded.color # => {:gamma => 0.45455, :srgb_intent => 0, ...} (gAMA/cHRM/sRGB/iCCP)
|
|
70
74
|
|
|
71
75
|
=== Supported pixel formats
|
|
72
76
|
|
data/Rakefile
CHANGED
data/lib/libpng/chunk_walker.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Libpng
|
|
|
6
6
|
# Walks a PNG byte buffer chunk-by-chunk. Used to strip ancillary
|
|
7
7
|
# chunks (sRGB, gAMA, cHRM, iCCP, etc.) that libpng's simplified
|
|
8
8
|
# write API emits by default, and to extract metadata (IHDR fields,
|
|
9
|
-
# text chunks,
|
|
9
|
+
# text chunks, color chunks, pHYs) during decode.
|
|
10
10
|
#
|
|
11
11
|
# The chunk layout is documented in the PNG spec:
|
|
12
12
|
# [4 bytes length][4 bytes type][length bytes data][4 bytes CRC]
|
|
@@ -19,6 +19,12 @@ module Libpng
|
|
|
19
19
|
# EOF, CRC mismatch, missing IEND.
|
|
20
20
|
class FormatError < Error; end
|
|
21
21
|
|
|
22
|
+
# PNG gAMA fixed-point scale (per spec: gamma = uint32 / 100000).
|
|
23
|
+
GAMMA_SCALE = 100_000.0
|
|
24
|
+
|
|
25
|
+
# PNG cHRM fixed-point scale (per spec: chromaticity = uint32 / 100000).
|
|
26
|
+
CHRM_SCALE = 100_000.0
|
|
27
|
+
|
|
22
28
|
def initialize(png_bytes)
|
|
23
29
|
@bytes = png_bytes.bytes
|
|
24
30
|
end
|
|
@@ -93,11 +99,173 @@ module Libpng
|
|
|
93
99
|
}
|
|
94
100
|
end
|
|
95
101
|
|
|
102
|
+
# Parse text chunks (tEXt, zTXt, iTXt) into a flat Hash of
|
|
103
|
+
# keyword -> UTF-8 String. When the same keyword appears in
|
|
104
|
+
# multiple chunks, the last occurrence wins (PNG spec permits
|
|
105
|
+
# this; behavior matches libpng's read path).
|
|
106
|
+
#
|
|
107
|
+
# - tEXt: Latin-1 keyword + Latin-1 value, transcoded to UTF-8.
|
|
108
|
+
# - zTXt: Latin-1 keyword + zlib-compressed Latin-1 value,
|
|
109
|
+
# inflated then transcoded to UTF-8.
|
|
110
|
+
# - iTXt: UTF-8 keyword + UTF-8 value (optionally zlib-compressed),
|
|
111
|
+
# with separate language tag and translated keyword that are
|
|
112
|
+
# dropped here (callers who need them should walk chunks directly).
|
|
113
|
+
#
|
|
114
|
+
# Malformed chunks are silently skipped: a single broken zTXt
|
|
115
|
+
# should not poison the rest of the decode.
|
|
116
|
+
def text_chunks
|
|
117
|
+
result = {}
|
|
118
|
+
each_chunk do |type, data, _|
|
|
119
|
+
case type
|
|
120
|
+
when 'tEXt'
|
|
121
|
+
parse_text(data, result)
|
|
122
|
+
when 'zTXt'
|
|
123
|
+
parse_ztxt(data, result)
|
|
124
|
+
when 'iTXt'
|
|
125
|
+
parse_itxt(data, result)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Parse color-space chunks (gAMA, cHRM, sRGB, iCCP) into a Hash.
|
|
132
|
+
# Returns Symbol keys:
|
|
133
|
+
#
|
|
134
|
+
# :gamma -> Float (gAMA, value/100000)
|
|
135
|
+
# :white_point_x, :white_point_y -> Float (cHRM)
|
|
136
|
+
# :red_x, :red_y, :green_x, :green_y, :blue_x, :blue_y -> Float
|
|
137
|
+
# :srgb_intent -> Integer 0..3 (sRGB)
|
|
138
|
+
# :icc_profile_name -> UTF-8 String (iCCP, profile name)
|
|
139
|
+
# :icc_profile -> binary String (iCCP, decompressed bytes)
|
|
140
|
+
#
|
|
141
|
+
# Malformed chunks (e.g. truncated cHRM, bad iCCP compression) are
|
|
142
|
+
# silently skipped. ICC profiles can be large (10KB-1MB); callers
|
|
143
|
+
# who don't need them should walk chunks manually rather than call
|
|
144
|
+
# this method.
|
|
145
|
+
def color_chunks
|
|
146
|
+
result = {}
|
|
147
|
+
each_chunk do |type, data, _|
|
|
148
|
+
case type
|
|
149
|
+
when 'gAMA'
|
|
150
|
+
parse_gama(data, result)
|
|
151
|
+
when 'cHRM'
|
|
152
|
+
parse_chrm(data, result)
|
|
153
|
+
when 'sRGB'
|
|
154
|
+
parse_srgb(data, result)
|
|
155
|
+
when 'iCCP'
|
|
156
|
+
parse_iccp(data, result)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
result
|
|
160
|
+
end
|
|
161
|
+
|
|
96
162
|
private
|
|
97
163
|
|
|
98
164
|
def verify_signature
|
|
99
165
|
sig = @bytes.first(8)
|
|
100
166
|
raise FormatError, 'not a PNG file (bad signature)' unless sig == SIGNATURE
|
|
101
167
|
end
|
|
168
|
+
|
|
169
|
+
# Split a binary string at the first NUL byte. Returns [head, tail]
|
|
170
|
+
# where head excludes the NUL and tail is everything after. Returns
|
|
171
|
+
# [nil, nil] when no NUL is present.
|
|
172
|
+
def split_at_null(bytes)
|
|
173
|
+
idx = bytes.index("\x00")
|
|
174
|
+
return [nil, nil] if idx.nil?
|
|
175
|
+
|
|
176
|
+
[bytes[0, idx], bytes[(idx + 1)..]]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Force Latin-1 byte sequence into UTF-8. Bytes 0x80-0xFF that have
|
|
180
|
+
# no assigned meaning in ISO-8859-1 still round-trip through this
|
|
181
|
+
# encoding because Latin-1 is a strict superset of the byte values.
|
|
182
|
+
def latin1_to_utf8(bytes)
|
|
183
|
+
bytes.force_encoding('ISO-8859-1').encode('UTF-8')
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def parse_text(data, result)
|
|
187
|
+
key, value = split_at_null(data)
|
|
188
|
+
return if key.nil?
|
|
189
|
+
|
|
190
|
+
result[key] = latin1_to_utf8(value)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def parse_ztxt(data, result)
|
|
194
|
+
key, rest = split_at_null(data)
|
|
195
|
+
return if key.nil? || rest.nil? || rest.length < 2
|
|
196
|
+
|
|
197
|
+
_compression_method = rest.getbyte(0)
|
|
198
|
+
inflated = Zlib.inflate(rest[1..])
|
|
199
|
+
result[key] = latin1_to_utf8(inflated)
|
|
200
|
+
rescue Zlib::Error
|
|
201
|
+
# Skip malformed zTXt rather than failing the whole decode.
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def parse_itxt(data, result)
|
|
205
|
+
# iTXt layout:
|
|
206
|
+
# keyword\0
|
|
207
|
+
# compression_flag (1 byte, 0 = uncompressed, 1 = zlib)
|
|
208
|
+
# compression_method (1 byte, 0 = zlib/deflate)
|
|
209
|
+
# language_tag\0 (ASCII, may be empty)
|
|
210
|
+
# translated_keyword\0 (UTF-8, may be empty)
|
|
211
|
+
# text (UTF-8, optionally zlib-compressed)
|
|
212
|
+
key, rest = split_at_null(data)
|
|
213
|
+
return if key.nil? || rest.nil? || rest.length < 3
|
|
214
|
+
|
|
215
|
+
compression_flag = rest.getbyte(0)
|
|
216
|
+
_compression_method = rest.getbyte(1)
|
|
217
|
+
_lang, after_lang = split_at_null(rest[2..])
|
|
218
|
+
return if after_lang.nil?
|
|
219
|
+
|
|
220
|
+
_translated, text_bytes = split_at_null(after_lang)
|
|
221
|
+
return if text_bytes.nil?
|
|
222
|
+
|
|
223
|
+
text_bytes = Zlib.inflate(text_bytes) if compression_flag == 1
|
|
224
|
+
result[key] = text_bytes.force_encoding('UTF-8')
|
|
225
|
+
rescue Zlib::Error
|
|
226
|
+
# Skip malformed iTXt rather than failing the whole decode.
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def parse_gama(data, result)
|
|
230
|
+
return if data.length < 4
|
|
231
|
+
|
|
232
|
+
result[:gamma] = data.unpack1('N') / GAMMA_SCALE
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def parse_chrm(data, result)
|
|
236
|
+
return if data.length < 32
|
|
237
|
+
|
|
238
|
+
wp_x, wp_y, rx, ry, gx, gy, bx, by = data.unpack('N8')
|
|
239
|
+
result[:white_point_x] = wp_x / CHRM_SCALE
|
|
240
|
+
result[:white_point_y] = wp_y / CHRM_SCALE
|
|
241
|
+
result[:red_x] = rx / CHRM_SCALE
|
|
242
|
+
result[:red_y] = ry / CHRM_SCALE
|
|
243
|
+
result[:green_x] = gx / CHRM_SCALE
|
|
244
|
+
result[:green_y] = gy / CHRM_SCALE
|
|
245
|
+
result[:blue_x] = bx / CHRM_SCALE
|
|
246
|
+
result[:blue_y] = by / CHRM_SCALE
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def parse_srgb(data, result)
|
|
250
|
+
return if data.empty?
|
|
251
|
+
|
|
252
|
+
result[:srgb_intent] = data.getbyte(0)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def parse_iccp(data, result)
|
|
256
|
+
# iCCP layout:
|
|
257
|
+
# profile_name\0
|
|
258
|
+
# compression_method (1 byte, must be 0 = zlib/deflate)
|
|
259
|
+
# compressed_profile (zlib stream)
|
|
260
|
+
name, rest = split_at_null(data)
|
|
261
|
+
return if name.nil? || rest.nil? || rest.length < 2
|
|
262
|
+
|
|
263
|
+
_compression_method = rest.getbyte(0)
|
|
264
|
+
profile = Zlib.inflate(rest[1..])
|
|
265
|
+
result[:icc_profile_name] = name.force_encoding('UTF-8')
|
|
266
|
+
result[:icc_profile] = profile
|
|
267
|
+
rescue Zlib::Error
|
|
268
|
+
# Skip malformed iCCP rather than failing the whole decode.
|
|
269
|
+
end
|
|
102
270
|
end
|
|
103
271
|
end
|
data/lib/libpng/decoded_image.rb
CHANGED
|
@@ -11,7 +11,12 @@ module Libpng
|
|
|
11
11
|
# +bit_depth+ bits per channel (8 or 16). nil if not extracted.
|
|
12
12
|
# +color_type+ PNG_COLOR_TYPE_* integer from IHDR. nil if not extracted.
|
|
13
13
|
# +interlace+ PNG_INTERLACE_* integer from IHDR. nil if not extracted.
|
|
14
|
+
# +text+ Hash<String,String> of tEXt/zTXt/iTXt keyword -> UTF-8
|
|
15
|
+
# value. Empty Hash when no text chunks present.
|
|
16
|
+
# +color+ Hash<Symbol,*> of gAMA/cHRM/sRGB/iCCP fields. Empty
|
|
17
|
+
# Hash when no color metadata present.
|
|
14
18
|
DecodedImage = Struct.new(:width, :height, :format, :pixels,
|
|
15
19
|
:bit_depth, :color_type, :interlace,
|
|
20
|
+
:text, :color,
|
|
16
21
|
keyword_init: true)
|
|
17
22
|
end
|
data/lib/libpng/recipe.rb
CHANGED
|
@@ -174,6 +174,13 @@ module Libpng
|
|
|
174
174
|
'x86_64-darwin'
|
|
175
175
|
when /\A(arm64|aarch64).*linux-musl/
|
|
176
176
|
'aarch64-linux-musl'
|
|
177
|
+
when /\A(arm64|aarch64).*linux-ohos/
|
|
178
|
+
# OHOS (OpenHarmony / Huawei HarmonyOS PC) is musl-based arm64.
|
|
179
|
+
# The resulting binary is ELF64 aarch64 linked against musl,
|
|
180
|
+
# identical at the file-format level to aarch64-linux-musl --
|
|
181
|
+
# only the gem's platform label differs so RubyGems on OHOS
|
|
182
|
+
# selects the right variant.
|
|
183
|
+
'aarch64-linux-ohos'
|
|
177
184
|
when /\A(arm64|aarch64).*linux/
|
|
178
185
|
'aarch64-linux'
|
|
179
186
|
when /\Ax86_64.*linux-musl/
|
|
@@ -198,7 +205,8 @@ module Libpng
|
|
|
198
205
|
|
|
199
206
|
def cpu_type
|
|
200
207
|
case target_platform
|
|
201
|
-
when 'aarch64-linux', 'aarch64-linux-musl', '
|
|
208
|
+
when 'aarch64-linux', 'aarch64-linux-musl', 'aarch64-linux-ohos',
|
|
209
|
+
'arm64-darwin', 'aarch64-mingw-ucrt' then 'aarch64'
|
|
202
210
|
when 'x86_64-linux', 'x86_64-linux-musl', 'x86_64-darwin', /\Ax64-mingw/ then 'x86_64'
|
|
203
211
|
else
|
|
204
212
|
super
|
|
@@ -207,7 +215,8 @@ module Libpng
|
|
|
207
215
|
|
|
208
216
|
def cmake_system_name
|
|
209
217
|
case target_platform
|
|
210
|
-
when 'aarch64-linux', 'x86_64-linux', 'aarch64-linux-musl',
|
|
218
|
+
when 'aarch64-linux', 'x86_64-linux', 'aarch64-linux-musl',
|
|
219
|
+
'x86_64-linux-musl', 'aarch64-linux-ohos' then 'Linux'
|
|
211
220
|
when 'arm64-darwin', 'x86_64-darwin' then 'Darwin'
|
|
212
221
|
when /\A(aarch64-)?mingw/, /\Ax64-mingw/ then 'Windows'
|
|
213
222
|
else
|
|
@@ -222,7 +231,7 @@ module Libpng
|
|
|
222
231
|
/Mach-O 64-bit dynamically linked shared library arm64/
|
|
223
232
|
when 'x86_64-darwin'
|
|
224
233
|
/Mach-O 64-bit dynamically linked shared library x86_64/
|
|
225
|
-
when 'aarch64-linux', 'aarch64-linux-musl'
|
|
234
|
+
when 'aarch64-linux', 'aarch64-linux-musl', 'aarch64-linux-ohos'
|
|
226
235
|
/ELF 64-bit LSB shared object, ARM aarch64/
|
|
227
236
|
when 'x86_64-linux', 'x86_64-linux-musl'
|
|
228
237
|
/ELF 64-bit LSB shared object, x86-64/
|
|
@@ -4,9 +4,11 @@ module Libpng
|
|
|
4
4
|
# Decodes a PNG byte buffer into raw pixels via libpng's "simplified"
|
|
5
5
|
# read API (png_image_begin_read_from_memory + png_image_finish_read).
|
|
6
6
|
#
|
|
7
|
-
# Also parses the
|
|
8
|
-
# #color_type, #interlace
|
|
9
|
-
#
|
|
7
|
+
# Also parses the byte buffer via ChunkWalker to populate
|
|
8
|
+
# DecodedImage#bit_depth, #color_type, #interlace (from IHDR),
|
|
9
|
+
# #text (from tEXt/zTXt/iTXt), and #color (from gAMA/cHRM/sRGB/iCCP).
|
|
10
|
+
# The simplified API itself drops these ancillary chunks on read --
|
|
11
|
+
# walking them separately is the only way to surface their content.
|
|
10
12
|
#
|
|
11
13
|
# One instance per decode call. Ractor-safe.
|
|
12
14
|
class SimplifiedDecoder
|
|
@@ -41,16 +43,12 @@ module Libpng
|
|
|
41
43
|
ok = Libpng::Binding.png_image_finish_read(img, nil, out_buf, stride, nil)
|
|
42
44
|
raise Error, "png_image_finish_read failed: #{read_message(img)}" if ok.zero?
|
|
43
45
|
|
|
44
|
-
pixels = out_buf.read_bytes(out_size)
|
|
45
|
-
metadata = extract_metadata
|
|
46
46
|
return DecodedImage.new(
|
|
47
47
|
width: width,
|
|
48
48
|
height: height,
|
|
49
49
|
format: @pixel_format.to_s.upcase,
|
|
50
|
-
pixels:
|
|
51
|
-
|
|
52
|
-
color_type: metadata[:color_type],
|
|
53
|
-
interlace: metadata[:interlace]
|
|
50
|
+
pixels: out_buf.read_bytes(out_size),
|
|
51
|
+
**walker_metadata
|
|
54
52
|
)
|
|
55
53
|
end
|
|
56
54
|
end
|
|
@@ -64,11 +62,36 @@ module Libpng
|
|
|
64
62
|
FORMAT_BY_NAME[@pixel_format.to_s.upcase]
|
|
65
63
|
end
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
# Walks the PNG once via ChunkWalker to gather all metadata fields
|
|
66
|
+
# the simplified API does not surface. Each accessor is best-effort:
|
|
67
|
+
# a malformed IHDR or text chunk should not poison the rest of the
|
|
68
|
+
# decode -- empty Hashes / nil values are acceptable fall-throughs.
|
|
69
|
+
def walker_metadata
|
|
70
|
+
walker = ChunkWalker.new(@png)
|
|
71
|
+
ihdr = safe_ihdr(walker)
|
|
72
|
+
{
|
|
73
|
+
bit_depth: ihdr[:bit_depth],
|
|
74
|
+
color_type: ihdr[:color_type],
|
|
75
|
+
interlace: ihdr[:interlace],
|
|
76
|
+
text: safe_text(walker),
|
|
77
|
+
color: safe_color(walker)
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def safe_ihdr(walker)
|
|
82
|
+
walker.ihdr_fields
|
|
83
|
+
rescue ChunkWalker::FormatError
|
|
84
|
+
{}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def safe_text(walker)
|
|
88
|
+
walker.text_chunks
|
|
89
|
+
rescue ChunkWalker::FormatError
|
|
90
|
+
{}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def safe_color(walker)
|
|
94
|
+
walker.color_chunks
|
|
72
95
|
rescue ChunkWalker::FormatError
|
|
73
96
|
{}
|
|
74
97
|
end
|
data/lib/libpng/version.rb
CHANGED
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -59,27 +59,6 @@ files:
|
|
|
59
59
|
- LICENSE.txt
|
|
60
60
|
- README.adoc
|
|
61
61
|
- Rakefile
|
|
62
|
-
- TODO.roadmap/01-claude-md-update.md
|
|
63
|
-
- TODO.roadmap/02-encode-standard-ractor-spec.md
|
|
64
|
-
- TODO.roadmap/03-mini-portile2-bump.md
|
|
65
|
-
- TODO.roadmap/04-changelog.md
|
|
66
|
-
- TODO.roadmap/05-benchmarks.md
|
|
67
|
-
- TODO.roadmap/06-malformed-input-tests.md
|
|
68
|
-
- TODO.roadmap/07-interlace-option.md
|
|
69
|
-
- TODO.roadmap/08-palette-support.md
|
|
70
|
-
- TODO.roadmap/09-16-bit-encode.md
|
|
71
|
-
- TODO.roadmap/10-libpng-1.6.59-bump.md
|
|
72
|
-
- TODO.roadmap/11-read-side-metadata.md
|
|
73
|
-
- TODO.roadmap/12-full-api-surface.md
|
|
74
|
-
- TODO.roadmap/13-c-extension-setjmp.md
|
|
75
|
-
- TODO.roadmap/14-progressive-decode.md
|
|
76
|
-
- TODO.roadmap/15-file-io-variants.md
|
|
77
|
-
- TODO.roadmap/16-architecture-refactor.md
|
|
78
|
-
- TODO.roadmap/17-ruby-version-policy.md
|
|
79
|
-
- TODO.roadmap/18-dependabot.md
|
|
80
|
-
- TODO.roadmap/19-text-chunk-read.md
|
|
81
|
-
- TODO.roadmap/20-color-metadata-read.md
|
|
82
|
-
- TODO.roadmap/README.md
|
|
83
62
|
- bin/console
|
|
84
63
|
- bin/setup
|
|
85
64
|
- ext/extconf.rb
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# 01 - CLAUDE.md update
|
|
2
|
-
|
|
3
|
-
- **Priority**: P0
|
|
4
|
-
- **Status**: Done
|
|
5
|
-
|
|
6
|
-
## Context
|
|
7
|
-
|
|
8
|
-
The agent-facing `CLAUDE.md` was out of sync after `encode_standard`,
|
|
9
|
-
the platform matrix expansion, and the architecture refactor. Future
|
|
10
|
-
Claude instances would land in a codebase that no longer matched the
|
|
11
|
-
documented structure.
|
|
12
|
-
|
|
13
|
-
## Approach
|
|
14
|
-
|
|
15
|
-
Re-created `CLAUDE.md` with:
|
|
16
|
-
- File-responsibility table for the new autoloaded structure
|
|
17
|
-
- Public API surface (encode / encode_standard / decode) with options
|
|
18
|
-
- FFI binding inventory (simplified + standard APIs)
|
|
19
|
-
- Ractor safety section including the Ruby 4.0 `#value` migration
|
|
20
|
-
- Code quality rules (no `require_relative`, no `send`, no `iv_get`,
|
|
21
|
-
no `respond_to?` -- the project-wide rules)
|
|
22
|
-
- Spec inventory
|
|
23
|
-
- Cross-links to `TODO.roadmap/`, `CHANGELOG.md`, `REPORT-complex-api-needs.md`
|
|
24
|
-
|
|
25
|
-
## Delivered
|
|
26
|
-
|
|
27
|
-
PR #7 (this branch).
|