libpng 1.6.58.2 → 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/.rubocop.yml +1 -1
- data/CHANGELOG.md +127 -0
- data/CLAUDE.md +110 -0
- data/README.adoc +38 -17
- data/Rakefile +1 -0
- data/ext/extconf.rb +9 -2
- data/lib/libpng/binding.rb +57 -0
- data/lib/libpng/bytes_per_pixel.rb +43 -0
- data/lib/libpng/chunk_walker.rb +271 -0
- data/lib/libpng/decoded_image.rb +22 -0
- data/lib/libpng/error.rb +8 -0
- data/lib/libpng/recipe.rb +12 -4
- data/lib/libpng/simplified_decoder.rb +107 -0
- data/lib/libpng/simplified_encoder.rb +98 -0
- data/lib/libpng/standard_encoder.rb +219 -0
- data/lib/libpng/version.rb +1 -1
- data/lib/libpng.rb +99 -385
- data/libpng.gemspec +1 -1
- metadata +13 -3
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/.rubocop.yml
CHANGED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `libpng` Ruby gem are documented here.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
This gem follows a `{LIBPNG_VERSION}.{LIBPNG_RUBY_ITERATION}` version
|
|
7
|
+
scheme. `LIBPNG_VERSION` is the upstream libpng release; `ITERATION`
|
|
8
|
+
bumps for Ruby-side changes and resets to 0 when LIBPNG_VERSION bumps.
|
|
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
|
+
|
|
38
|
+
## [1.6.58.3] - 2026-07-26
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- `Libpng.encode_standard` -- standard libpng write API
|
|
42
|
+
(`png_create_write_struct` -> `png_set_IHDR` -> `png_set_rows` ->
|
|
43
|
+
`png_write_png(PNG_TRANSFORM_IDENTITY)`). Mirrors libemf2svg's
|
|
44
|
+
`rgb2png` byte layout and emits only IHDR/IDAT/IEND directly (no
|
|
45
|
+
post-hoc chunk stripping).
|
|
46
|
+
- `interlace:` option on `encode_standard` -- `:none` (default) or
|
|
47
|
+
`:adam7`.
|
|
48
|
+
- `bit_depth:` option on `encode_standard` -- 8 (default) or 16 for
|
|
49
|
+
RGB/RGBA/GRAY.
|
|
50
|
+
- `pixel_format: :palette` with `palette:` option on `encode_standard` --
|
|
51
|
+
emits PNG_COLOR_TYPE_PALETTE via `png_set_PLTE` (and `png_set_tRNS`
|
|
52
|
+
when any palette entry has alpha).
|
|
53
|
+
- `Libpng::DecodedImage#bit_depth`, `#color_type`, `#interlace` -- IHDR
|
|
54
|
+
metadata fields populated by walking the source PNG after decode.
|
|
55
|
+
- `Libpng::ChunkWalker` -- public class for walking PNG chunks:
|
|
56
|
+
`#each_chunk`, `#strip_ancillary`, `#ihdr_fields`.
|
|
57
|
+
- `Libpng::BytesPerPixel` -- pure-data helper module mapping formats
|
|
58
|
+
and color types to bytes-per-pixel (8- and 16-bit aware).
|
|
59
|
+
- Architecture refactor: split the monolithic `lib/libpng.rb` (466
|
|
60
|
+
lines) into MECE per-concern files using Ruby `autoload`. New files:
|
|
61
|
+
`lib/libpng/error.rb`, `decoded_image.rb`, `chunk_walker.rb`,
|
|
62
|
+
`bytes_per_pixel.rb`, `simplified_encoder.rb`,
|
|
63
|
+
`simplified_decoder.rb`, `standard_encoder.rb`. Public API
|
|
64
|
+
unchanged.
|
|
65
|
+
- Specs: malformed-input suite, encode_standard Ractor suite,
|
|
66
|
+
benchmark suite, options suite for interlace/bit_depth/palette,
|
|
67
|
+
metadata suite for DecodedImage + ChunkWalker.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
- `mini_portile2` dependency bumped from `~> 2.6` to `~> 2.8`.
|
|
71
|
+
- Removed all `require_relative` from library code in favor of
|
|
72
|
+
`autoload` (per project code-quality rules).
|
|
73
|
+
- `ext/extconf.rb` now triggers `Libpng::Recipe` autoload via
|
|
74
|
+
`require 'libpng'` instead of reaching into `lib/libpng/recipe.rb`
|
|
75
|
+
directly.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
- Filter option documentation: clarified that `:default`, `:adaptive`,
|
|
79
|
+
and `:all` produce byte-identical output but exercise different
|
|
80
|
+
libpng code paths. `:none` forces no filtering.
|
|
81
|
+
- Removed redundant `.dup.freeze` on `LIBPNG_VER_STRING_C` (the source
|
|
82
|
+
string is already a frozen literal).
|
|
83
|
+
|
|
84
|
+
## [1.6.58.2] - 2026-07-26
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
- `Libpng.encode_standard` initial release -- standard libpng write
|
|
88
|
+
API with memory-stream output via `png_set_write_fn` FFI callback
|
|
89
|
+
(no Tempfile), filter control, compression level control, and error
|
|
90
|
+
handling via `png_set_error_fn` callback.
|
|
91
|
+
- Specs: 32 specs covering encode_standard round-trips, filter
|
|
92
|
+
variants, compression levels, chunk-layout assertions, parity check
|
|
93
|
+
against simplified `encode`.
|
|
94
|
+
|
|
95
|
+
## [1.6.58.1] - 2026-07-25
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
- Full platform coverage: `aarch64-linux-musl`, `aarch64-linux` (native
|
|
99
|
+
on `ubuntu-24.04-arm`), `aarch64-mingw-ucrt` (native on
|
|
100
|
+
`windows-11-arm`), `x86_64-linux-musl` (Alpine via `docker run`).
|
|
101
|
+
- `step-security/msvc-dev-cmd@v1` replaces deleted `ilammy/msvc-dev-tools`
|
|
102
|
+
action.
|
|
103
|
+
- Release workflow tolerates already-published gems (skip-on-conflict).
|
|
104
|
+
- Specs: Ractor safety suite for `encode` and `decode` (Ruby 3.x and 4.0).
|
|
105
|
+
|
|
106
|
+
### Fixed
|
|
107
|
+
- `ext/extconf.rb` now emits a dummy Makefile so RubyGems is satisfied
|
|
108
|
+
when installing the source (`ruby` platform) gem.
|
|
109
|
+
- Recipe globs `{bin,lib}/libpng16*.dll` on Windows (CMake's
|
|
110
|
+
GNUInstallDirs puts the .dll in `bin/`, not `lib/`).
|
|
111
|
+
- Alpine build runs inside `docker run` (not the `container:` field)
|
|
112
|
+
so it works on arm64 Ubuntu runners.
|
|
113
|
+
- `git config --global --add safe.directory /work` in Alpine container
|
|
114
|
+
so `git ls-files` works for the gemspec.
|
|
115
|
+
|
|
116
|
+
## [1.6.58.0] - 2026-07-25
|
|
117
|
+
|
|
118
|
+
### Added
|
|
119
|
+
- Initial pre-compiled libpng gem. Bundles libpng 1.6.58 shared
|
|
120
|
+
libraries for x86_64 Linux, x86_64 macOS, arm64 macOS, x64 Windows
|
|
121
|
+
(MSVCRT and UCRT).
|
|
122
|
+
- `Libpng.encode` / `Libpng.decode` -- simplified API binding via FFI.
|
|
123
|
+
- `strip_colorspace:` option on `encode` to drop sRGB/gAMA chunks the
|
|
124
|
+
simplified API emits by default.
|
|
125
|
+
- `convert_to_8bit:` option on `encode` for 16-bit input.
|
|
126
|
+
- MiniPortile-based recipe for building libpng from source when
|
|
127
|
+
installing the platform-agnostic `ruby` gem.
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
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
|
+
|
|
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
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
The library is split into MECE per-concern files under `lib/libpng/`,
|
|
14
|
+
loaded via `autoload` from `lib/libpng.rb`. **Never use `require_relative`
|
|
15
|
+
(or `require` with a path) for internal library code** — add an
|
|
16
|
+
`autoload` entry to `lib/libpng.rb` instead.
|
|
17
|
+
|
|
18
|
+
| File | Responsibility |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `lib/libpng.rb` | Module + FFI setup + constants + public dispatch (`encode`/`decode`/`encode_standard`) + autoloads |
|
|
21
|
+
| `lib/libpng/version.rb` | `LIBPNG_VERSION`, `LIBPNG_RUBY_ITERATION`, `VERSION` |
|
|
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`) |
|
|
25
|
+
| `lib/libpng/bytes_per_pixel.rb` | `Libpng::BytesPerPixel` (pure-data lookup) |
|
|
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) |
|
|
29
|
+
| `lib/libpng/recipe.rb` | `Libpng::Recipe < MiniPortileCMake` (builds libpng from source for the source gem) |
|
|
30
|
+
| `ext/extconf.rb` | Gem extension entry. Triggers `Libpng::Recipe` autoload via `require 'libpng'`, then emits a dummy Makefile |
|
|
31
|
+
|
|
32
|
+
### Public API
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
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:)
|
|
37
|
+
Libpng.decode(png, pixel_format:)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Each is a thin dispatcher that constructs a dedicated encoder/decoder instance and calls `#call`. The instance is discarded after the call (per-call state, Ractor-safe).
|
|
41
|
+
|
|
42
|
+
### FFI binding
|
|
43
|
+
|
|
44
|
+
`Libpng` extends `FFI::Library` and attaches:
|
|
45
|
+
- 4 simplified-API functions (`png_image_*`)
|
|
46
|
+
- 10 standard-API functions (`png_create_write_struct`, `png_set_IHDR`, `png_set_PLTE`, `png_set_tRNS`, `png_set_filter`, `png_set_compression_level`, `png_set_write_fn`, `png_write_png`, `png_set_rows`, `png_destroy_write_struct`, `png_create_info_struct`)
|
|
47
|
+
|
|
48
|
+
FFI calls inside encoder classes use the qualified form: `Libpng.png_set_IHDR(...)`. The function table is set up once at module load and is shareable across Ractors.
|
|
49
|
+
|
|
50
|
+
### Ractor safety
|
|
51
|
+
|
|
52
|
+
Every encoder/decoder allocates its own `png_image` (simplified API) or `png_struct` (standard API) per call. No shared mutable state on the Ruby side. The Ractor specs in `spec/ractor_spec.rb` and `spec/ractor_standard_spec.rb` verify this across Ruby 3.3, 3.4, and 4.0.
|
|
53
|
+
|
|
54
|
+
Ruby 4.0 removed `Ractor#take`; use the helper `ractor_result(r)` which prefers `#value` (4.0+) and falls back to `#take` (3.x).
|
|
55
|
+
|
|
56
|
+
## Common commands
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
bundle install
|
|
60
|
+
bundle exec rake compile # build libpng via MiniPortile (needs cmake + zlib)
|
|
61
|
+
bundle exec rake spec # all specs (~105 examples)
|
|
62
|
+
bundle exec rspec spec/libpng_spec.rb:17 # single spec by line
|
|
63
|
+
bundle exec rake rubocop
|
|
64
|
+
bundle exec rake # default: spec + rubocop
|
|
65
|
+
|
|
66
|
+
bundle exec rake gem:native:arm64-darwin # build a pre-compiled gem
|
|
67
|
+
bundle exec rake gem:native:any # source gem (compiles on install)
|
|
68
|
+
```
|
|
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`, `aarch64-linux-ohos`, `x86_64-darwin`, `arm64-darwin`.
|
|
71
|
+
|
|
72
|
+
## Release process
|
|
73
|
+
|
|
74
|
+
Releases are tag-triggered via `.github/workflows/release.yml`. Trigger with:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
gh workflow run release.yml --repo claricle/libpng-ruby --ref main -f bump-type=iteration
|
|
78
|
+
# or: -f bump-type=libpng -f libpng-version=1.6.59
|
|
79
|
+
# or: -f bump-type=current # release current VERSION as-is
|
|
80
|
+
```
|
|
81
|
+
|
|
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
|
+
|
|
84
|
+
**Never** push tags or merge to main directly — always go through PRs.
|
|
85
|
+
|
|
86
|
+
## Code quality rules (project-wide)
|
|
87
|
+
|
|
88
|
+
- **Never** `require_relative` for internal library code — use `autoload`.
|
|
89
|
+
- **Never** `send` to call private methods. Redesign the API boundary instead.
|
|
90
|
+
- **Never** `instance_variable_set`/`get` across objects.
|
|
91
|
+
- **Never** `respond_to?` for type checks — use `is_a?` or design the type hierarchy so the check isn't needed.
|
|
92
|
+
- **DRY / MECE / OCP**: each concern lives in exactly one file. New pixel format = add to `FORMAT_BY_NAME` + `COLOR_TYPE_BY_FORMAT` + `BytesPerPixel` (and `FORMAT_TO_COLOR_TYPE` if standard-API).
|
|
93
|
+
|
|
94
|
+
## Specs
|
|
95
|
+
|
|
96
|
+
- `spec/libpng_spec.rb` — `encode`/`decode` simplified API
|
|
97
|
+
- `spec/libpng_standard_spec.rb` — `encode_standard` core
|
|
98
|
+
- `spec/standard_encoder_options_spec.rb` — `interlace:`/`bit_depth:`/`palette:` options
|
|
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
|
|
102
|
+
- `spec/malformed_input_spec.rb` — corrupt PNG input handling
|
|
103
|
+
- `spec/ractor_spec.rb` — Ractor safety for simplified API
|
|
104
|
+
- `spec/ractor_standard_spec.rb` — Ractor safety for standard API
|
|
105
|
+
- `spec/benchmark_spec.rb` — encode/decode timing comparison
|
|
106
|
+
|
|
107
|
+
## See also
|
|
108
|
+
|
|
109
|
+
- `CHANGELOG.md` — release history
|
|
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)
|
|
@@ -60,10 +62,15 @@ for the current set):
|
|
|
60
62
|
|
|
61
63
|
# Decode a PNG into raw pixels.
|
|
62
64
|
decoded = Libpng.decode(File.binread("out.png"), pixel_format: "RGBA")
|
|
63
|
-
decoded.width
|
|
64
|
-
decoded.height
|
|
65
|
-
decoded.format
|
|
66
|
-
decoded.pixels
|
|
65
|
+
decoded.width # => Integer
|
|
66
|
+
decoded.height # => Integer
|
|
67
|
+
decoded.format # => "RGBA"
|
|
68
|
+
decoded.pixels # => binary String of RGBA bytes
|
|
69
|
+
decoded.bit_depth # => 8 (from IHDR)
|
|
70
|
+
decoded.color_type # => 6 (PNG_COLOR_TYPE_RGB_ALPHA, from IHDR)
|
|
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)
|
|
67
74
|
|
|
68
75
|
=== Supported pixel formats
|
|
69
76
|
|
|
@@ -94,33 +101,47 @@ output. Set to `false` to keep the ancillary color chunks.
|
|
|
94
101
|
For parity with libpng's classic write path -- the same code path used
|
|
95
102
|
by `libemf2svg`'s `rgb2png` -- use `encode_standard` instead of
|
|
96
103
|
`encode`. It calls `png_create_write_struct` + `png_set_IHDR` +
|
|
97
|
-
`png_set_rows` + `png_write_png(PNG_TRANSFORM_IDENTITY)` directly
|
|
98
|
-
filter and zlib-compression control exposed:
|
|
104
|
+
`png_set_rows` + `png_write_png(PNG_TRANSFORM_IDENTITY)` directly.
|
|
99
105
|
|
|
100
106
|
Libpng.encode_standard(width, height, pixels,
|
|
101
107
|
pixel_format: "RGBA",
|
|
102
108
|
filter: :default,
|
|
103
|
-
compression_level: 6
|
|
109
|
+
compression_level: 6,
|
|
110
|
+
interlace: :none,
|
|
111
|
+
bit_depth: 8,
|
|
112
|
+
palette: nil)
|
|
104
113
|
|
|
105
|
-
`pixel_format`::
|
|
106
|
-
variants (`BGR`, `ARGB`, `BGRA`, `ABGR`) are simplified-API
|
|
107
|
-
the standard API always emits host-order.
|
|
114
|
+
`pixel_format`:: `:gray`, `:ga`, `:rgb`, `:rgba`, or `:palette`. The
|
|
115
|
+
byte-order variants (`BGR`, `ARGB`, `BGRA`, `ABGR`) are simplified-API
|
|
116
|
+
only -- the standard API always emits host-order.
|
|
108
117
|
|
|
109
|
-
`filter`::
|
|
110
|
-
|
|
111
|
-
`:up`, `:avg`, `:paeth`,
|
|
118
|
+
`filter`:: `:default` (adaptive -- libpng picks the best filter per
|
|
119
|
+
row, the libemf2svg default), `:adaptive` (same output, exercises a
|
|
120
|
+
different libpng code path), `:none`, `:sub`, `:up`, `:avg`, `:paeth`,
|
|
121
|
+
`:all`.
|
|
112
122
|
|
|
113
123
|
`compression_level`:: zlib level 0-9. Default 6 (Z_DEFAULT_COMPRESSION,
|
|
114
124
|
matching libemf2svg).
|
|
115
125
|
|
|
126
|
+
`interlace`:: `:none` (default) or `:adam7`.
|
|
127
|
+
|
|
128
|
+
`bit_depth`:: 8 (default) or 16. Only valid for `:gray`, `:rgb`,
|
|
129
|
+
`:rgba`. 16-bit callers must provide 2 bytes per channel
|
|
130
|
+
(host-order little-endian). Always 8 for `:palette`.
|
|
131
|
+
|
|
132
|
+
`palette`:: Array of `[r, g, b]` or `[r, g, b, a]` (1..256 entries).
|
|
133
|
+
Required when `pixel_format: :palette`. If any entry has alpha, a `tRNS`
|
|
134
|
+
chunk is emitted.
|
|
135
|
+
|
|
116
136
|
Output is written directly into a Ruby String via `png_set_write_fn` --
|
|
117
137
|
no `Tempfile`, no disk I/O. The FFI write callback is held on a local
|
|
118
|
-
Array so it survives the libpng calls.
|
|
138
|
+
`Array` so it survives the libpng calls.
|
|
119
139
|
|
|
120
140
|
Differences from `encode`:
|
|
121
141
|
- Emits only `IHDR` / `IDAT` / `IEND` (no post-hoc chunk stripping
|
|
122
142
|
needed).
|
|
123
|
-
-
|
|
143
|
+
- Supports palette, 16-bit, and interlaced output -- simplified API
|
|
144
|
+
doesn't.
|
|
124
145
|
- Errors are raised via a `png_set_error_fn` callback (set on
|
|
125
146
|
`png_create_write_struct`). libpng expects error callbacks to
|
|
126
147
|
longjmp; we rb_raise instead, which unwinds the Ruby stack. The
|
data/Rakefile
CHANGED
data/ext/extconf.rb
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# extconf.rb is invoked by RubyGems when installing the source ('ruby'
|
|
4
|
+
# platform) gem. It needs to find lib/libpng.rb on the load path so it
|
|
5
|
+
# can trigger Libpng::Recipe autoload. We don't `require_relative`
|
|
6
|
+
# because that pins to a specific path; instead we extend $LOAD_PATH
|
|
7
|
+
# and let the normal autoload machinery do the work.
|
|
8
|
+
$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
|
|
2
9
|
|
|
3
|
-
require 'libpng/recipe'
|
|
4
10
|
require 'mkmf'
|
|
11
|
+
require 'libpng' # triggers autoload setup; Libpng::Recipe loads lazily
|
|
5
12
|
|
|
6
13
|
recipe = Libpng::Recipe.new
|
|
7
14
|
recipe.cook_if_not
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
module Libpng
|
|
6
|
+
# FFI bindings to the bundled `libpng16.{so,dylib,dll}`. Lives in its
|
|
7
|
+
# own autoloaded module so that requiring `libpng` (the top-level
|
|
8
|
+
# entry point used by `ext/extconf.rb` during the source-gem build)
|
|
9
|
+
# does NOT eagerly dlopen the shared library -- it may not exist yet
|
|
10
|
+
# at that point.
|
|
11
|
+
#
|
|
12
|
+
# The first time an encoder or decoder references `Libpng::Binding`,
|
|
13
|
+
# this file loads, ffi_lib fires, and the FFI functions attach. From
|
|
14
|
+
# then on, calls go directly through the attached C functions.
|
|
15
|
+
module Binding
|
|
16
|
+
extend FFI::Library
|
|
17
|
+
|
|
18
|
+
ffi_lib_flags :now, :global
|
|
19
|
+
|
|
20
|
+
lib_filename = if FFI::Platform.windows?
|
|
21
|
+
'libpng16.dll'
|
|
22
|
+
elsif FFI::Platform.mac?
|
|
23
|
+
'libpng16.dylib'
|
|
24
|
+
else
|
|
25
|
+
'libpng16.so'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# __dir__ is lib/libpng/; the shared library ships at lib/libpng/.
|
|
29
|
+
ffi_lib File.expand_path(lib_filename, __dir__)
|
|
30
|
+
|
|
31
|
+
# libpng simplified API (png_image / png_image_*).
|
|
32
|
+
attach_function :png_image_begin_read_from_memory,
|
|
33
|
+
%i[pointer pointer size_t], :int
|
|
34
|
+
attach_function :png_image_finish_read,
|
|
35
|
+
%i[pointer pointer pointer int pointer], :int
|
|
36
|
+
attach_function :png_image_write_to_memory,
|
|
37
|
+
%i[pointer pointer pointer int pointer int pointer], :int
|
|
38
|
+
attach_function :png_image_free, [:pointer], :void
|
|
39
|
+
|
|
40
|
+
# libpng standard (non-simplified) write API.
|
|
41
|
+
attach_function :png_create_write_struct,
|
|
42
|
+
%i[string pointer pointer pointer], :pointer
|
|
43
|
+
attach_function :png_create_info_struct, [:pointer], :pointer
|
|
44
|
+
attach_function :png_destroy_write_struct, %i[pointer pointer], :void
|
|
45
|
+
attach_function :png_set_IHDR,
|
|
46
|
+
%i[pointer pointer uint32 uint32 int int int int int], :void
|
|
47
|
+
attach_function :png_set_rows, %i[pointer pointer pointer], :void
|
|
48
|
+
attach_function :png_set_PLTE, %i[pointer pointer pointer int], :void
|
|
49
|
+
attach_function :png_set_tRNS,
|
|
50
|
+
%i[pointer pointer pointer int pointer], :void
|
|
51
|
+
attach_function :png_set_filter, %i[pointer int int], :void
|
|
52
|
+
attach_function :png_set_compression_level, %i[pointer int], :void
|
|
53
|
+
attach_function :png_set_write_fn,
|
|
54
|
+
%i[pointer pointer pointer pointer], :void
|
|
55
|
+
attach_function :png_write_png, %i[pointer pointer int pointer], :void
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Libpng
|
|
4
|
+
# Maps pixel formats to bytes-per-pixel. Used by both the simplified
|
|
5
|
+
# and standard encoders / decoders to compute row stride.
|
|
6
|
+
#
|
|
7
|
+
# Pure data class -- no I/O, no allocations beyond the lookup itself.
|
|
8
|
+
module BytesPerPixel
|
|
9
|
+
# Per-channel byte count for the supported bit depths. Linear (16-bit)
|
|
10
|
+
# formats aren't currently exposed via the simplified API path; the
|
|
11
|
+
# standard encoder uses this table when bit_depth: 16 is requested.
|
|
12
|
+
CHANNEL_BYTES_8_BIT = 1
|
|
13
|
+
CHANNEL_BYTES_16_BIT = 2
|
|
14
|
+
|
|
15
|
+
# Channels per PNG_COLOR_TYPE_* (png.h).
|
|
16
|
+
CHANNELS_BY_COLOR_TYPE = {
|
|
17
|
+
Libpng::COLOR_TYPE_GRAY => 1,
|
|
18
|
+
Libpng::COLOR_TYPE_GRAY_ALPHA => 2,
|
|
19
|
+
Libpng::COLOR_TYPE_RGB => 3,
|
|
20
|
+
Libpng::COLOR_TYPE_RGB_ALPHA => 4,
|
|
21
|
+
Libpng::COLOR_TYPE_PALETTE => 1
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def self.for_format(format_value)
|
|
25
|
+
case format_value
|
|
26
|
+
when Libpng::FORMAT_GRAY then 1
|
|
27
|
+
when Libpng::FORMAT_GA, Libpng::FORMAT_AG then 2
|
|
28
|
+
when Libpng::FORMAT_RGB, Libpng::FORMAT_BGR then 3
|
|
29
|
+
when Libpng::FORMAT_RGBA, Libpng::FORMAT_ARGB,
|
|
30
|
+
Libpng::FORMAT_BGRA, Libpng::FORMAT_ABGR then 4
|
|
31
|
+
else
|
|
32
|
+
raise Libpng::Error, "no bytes-per-pixel mapping for format 0x#{format_value.to_s(16)}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.for_color_type(color_type, bit_depth: 8)
|
|
37
|
+
channels = CHANNELS_BY_COLOR_TYPE[color_type] ||
|
|
38
|
+
raise(Libpng::Error, "unknown color_type #{color_type}")
|
|
39
|
+
channel_bytes = bit_depth == 16 ? CHANNEL_BYTES_16_BIT : CHANNEL_BYTES_8_BIT
|
|
40
|
+
channels * channel_bytes
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|