libpng 1.6.58.4-aarch64-linux-ohos
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 +7 -0
- data/.github/workflows/build.yml +211 -0
- data/.github/workflows/release.yml +299 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +39 -0
- data/CHANGELOG.md +127 -0
- data/CLAUDE.md +110 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +27 -0
- data/README.adoc +238 -0
- data/Rakefile +74 -0
- data/bin/console +5 -0
- data/bin/setup +5 -0
- data/ext/extconf.rb +19 -0
- 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/libpng16.so +0 -0
- data/lib/libpng/recipe.rb +247 -0
- 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 +16 -0
- data/lib/libpng.rb +188 -0
- data/libpng.gemspec +38 -0
- metadata +90 -0
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/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2026 Ribose Inc.
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
7
|
+
this list of conditions and the following disclaimer.
|
|
8
|
+
|
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright
|
|
10
|
+
notice, this list of conditions and the following disclaimer in the
|
|
11
|
+
documentation and/or other materials provided with the distribution.
|
|
12
|
+
|
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
14
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
15
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
16
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
17
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
18
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
19
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
20
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
21
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
22
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
23
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
24
|
+
|
|
25
|
+
This gem bundles pre-compiled binaries of libpng, which is itself
|
|
26
|
+
distributed under the libpng license (see the upstream LICENSE file
|
|
27
|
+
in https://github.com/pnggroup/libpng for details).
|
data/README.adoc
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
= libpng: pre-compiled libpng for Ruby
|
|
2
|
+
|
|
3
|
+
image:https://img.shields.io/badge/Ruby-%3E%3D%202.7.0-ruby.svg[Ruby]
|
|
4
|
+
image:https://img.shields.io/badge/license-BSD--2--Clause-blue.svg[License]
|
|
5
|
+
|
|
6
|
+
`libpng` is a Ruby binding for the official PNG reference library
|
|
7
|
+
(https://github.com/pnggroup/libpng[libpng]). The native shared library
|
|
8
|
+
is pre-compiled for each target platform and shipped inside the gem, so
|
|
9
|
+
`gem install libpng` Just Works without a C compiler on the host.
|
|
10
|
+
|
|
11
|
+
== Installation
|
|
12
|
+
|
|
13
|
+
Add to your Gemfile:
|
|
14
|
+
|
|
15
|
+
gem "libpng"
|
|
16
|
+
|
|
17
|
+
Then:
|
|
18
|
+
|
|
19
|
+
bundle install
|
|
20
|
+
|
|
21
|
+
Or install manually:
|
|
22
|
+
|
|
23
|
+
gem install libpng
|
|
24
|
+
|
|
25
|
+
RubyGems automatically selects the pre-compiled gem that matches your
|
|
26
|
+
platform. No C compiler, libpng development headers, or system packages
|
|
27
|
+
are required at install time when a binary gem is available. The
|
|
28
|
+
platform-agnostic `ruby` gem is the fallback: it downloads the libpng
|
|
29
|
+
source tarball at install time and compiles it via CMake (requires
|
|
30
|
+
`cmake`, `ninja`, and zlib headers).
|
|
31
|
+
|
|
32
|
+
== Supported platforms
|
|
33
|
+
|
|
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]
|
|
37
|
+
for the current set):
|
|
38
|
+
|
|
39
|
+
[cols="1,1,3", options="header"]
|
|
40
|
+
|===
|
|
41
|
+
| Gem platform | Target | Build host
|
|
42
|
+
| `ruby` (source) | Any platform (compiles on install) | n/a
|
|
43
|
+
| `x86_64-linux` | x86_64 Linux (glibc) | `ubuntu-latest`
|
|
44
|
+
| `x86_64-linux-musl` | x86_64 Linux (musl, e.g. Alpine) | `ruby:<ver>-alpine` on x86_64
|
|
45
|
+
| `aarch64-linux` | ARM64 Linux (glibc) | `ubuntu-24.04-arm` (native)
|
|
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`)
|
|
48
|
+
| `x64-mingw32` | x64 Windows, RubyInstaller < 3.0 (MSVCRT) | `windows-latest`
|
|
49
|
+
| `x64-mingw-ucrt` | x64 Windows, RubyInstaller >= 3.0 (UCRT) | `windows-latest`
|
|
50
|
+
| `aarch64-mingw-ucrt` | ARM64 Windows (Ruby >= 3.4) | `windows-11-arm` (native)
|
|
51
|
+
| `x86_64-darwin` | Intel macOS | `macos-15-intel`
|
|
52
|
+
| `arm64-darwin` | Apple Silicon macOS | `macos-latest`
|
|
53
|
+
|===
|
|
54
|
+
|
|
55
|
+
== Usage
|
|
56
|
+
|
|
57
|
+
require "libpng"
|
|
58
|
+
|
|
59
|
+
# Encode raw RGBA pixels (row-major, top-down) as a PNG.
|
|
60
|
+
png = Libpng.encode(width, height, rgba_bytes, pixel_format: "RGBA")
|
|
61
|
+
File.binwrite("out.png", png)
|
|
62
|
+
|
|
63
|
+
# Decode a PNG into raw pixels.
|
|
64
|
+
decoded = Libpng.decode(File.binread("out.png"), pixel_format: "RGBA")
|
|
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)
|
|
74
|
+
|
|
75
|
+
=== Supported pixel formats
|
|
76
|
+
|
|
77
|
+
- `"GRAY"` / `"GRAYSCALE"` -- 8-bit grayscale (1 byte/pixel)
|
|
78
|
+
- `"GA"` / `"AG"` -- 8-bit grayscale + alpha (2 bytes/pixel)
|
|
79
|
+
- `"RGB"`, `"BGR"` -- 8-bit truecolor (3 bytes/pixel)
|
|
80
|
+
- `"RGBA"`, `"ARGB"`, `"BGRA"`, `"ABGR"` -- 8-bit truecolor + alpha (4 bytes/pixel)
|
|
81
|
+
|
|
82
|
+
=== Encoding options
|
|
83
|
+
|
|
84
|
+
Libpng.encode(width, height, pixels,
|
|
85
|
+
pixel_format: "RGBA",
|
|
86
|
+
convert_to_8bit: false,
|
|
87
|
+
strip_colorspace: true)
|
|
88
|
+
|
|
89
|
+
`convert_to_8bit: true`:: causes libpng to convert 16-bit input to 8-bit
|
|
90
|
+
on write (input is still passed as a packed byte buffer; use this if you
|
|
91
|
+
have packed 16-bit-per-channel data).
|
|
92
|
+
|
|
93
|
+
`strip_colorspace: true`:: (default) walks the PNG chunk list after
|
|
94
|
+
encode and keeps only `IHDR`, `IDAT`, and `IEND`, dropping the sRGB/gAMA
|
|
95
|
+
chunks libpng's simplified API emits by default. This matches the
|
|
96
|
+
"classic" libpng write path (e.g. libemf2svg) and produces byte-identical
|
|
97
|
+
output. Set to `false` to keep the ancillary color chunks.
|
|
98
|
+
|
|
99
|
+
=== Standard API (encode_standard)
|
|
100
|
+
|
|
101
|
+
For parity with libpng's classic write path -- the same code path used
|
|
102
|
+
by `libemf2svg`'s `rgb2png` -- use `encode_standard` instead of
|
|
103
|
+
`encode`. It calls `png_create_write_struct` + `png_set_IHDR` +
|
|
104
|
+
`png_set_rows` + `png_write_png(PNG_TRANSFORM_IDENTITY)` directly.
|
|
105
|
+
|
|
106
|
+
Libpng.encode_standard(width, height, pixels,
|
|
107
|
+
pixel_format: "RGBA",
|
|
108
|
+
filter: :default,
|
|
109
|
+
compression_level: 6,
|
|
110
|
+
interlace: :none,
|
|
111
|
+
bit_depth: 8,
|
|
112
|
+
palette: nil)
|
|
113
|
+
|
|
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.
|
|
117
|
+
|
|
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`.
|
|
122
|
+
|
|
123
|
+
`compression_level`:: zlib level 0-9. Default 6 (Z_DEFAULT_COMPRESSION,
|
|
124
|
+
matching libemf2svg).
|
|
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
|
+
|
|
136
|
+
Output is written directly into a Ruby String via `png_set_write_fn` --
|
|
137
|
+
no `Tempfile`, no disk I/O. The FFI write callback is held on a local
|
|
138
|
+
`Array` so it survives the libpng calls.
|
|
139
|
+
|
|
140
|
+
Differences from `encode`:
|
|
141
|
+
- Emits only `IHDR` / `IDAT` / `IEND` (no post-hoc chunk stripping
|
|
142
|
+
needed).
|
|
143
|
+
- Supports palette, 16-bit, and interlaced output -- simplified API
|
|
144
|
+
doesn't.
|
|
145
|
+
- Errors are raised via a `png_set_error_fn` callback (set on
|
|
146
|
+
`png_create_write_struct`). libpng expects error callbacks to
|
|
147
|
+
longjmp; we rb_raise instead, which unwinds the Ruby stack. The
|
|
148
|
+
`ensure` block calls `png_destroy_write_struct` to release the C
|
|
149
|
+
state.
|
|
150
|
+
|
|
151
|
+
== Ractor safety
|
|
152
|
+
|
|
153
|
+
`Libpng.encode` and `Libpng.decode` are Ractor-safe. Every call
|
|
154
|
+
allocates and frees its own libpng `png_image`; no shared mutable state
|
|
155
|
+
exists on the Ruby side. Calls from multiple Ractors run concurrently
|
|
156
|
+
without locking.
|
|
157
|
+
|
|
158
|
+
Example (Ruby 3.x uses `Ractor#take`; Ruby 4.0+ uses `Ractor#value`):
|
|
159
|
+
|
|
160
|
+
r = Ractor.new do
|
|
161
|
+
Libpng.encode(2, 2, "\xFF" * 16, pixel_format: "RGBA")
|
|
162
|
+
end
|
|
163
|
+
r.take # Ruby 3.x
|
|
164
|
+
r.value # Ruby 4.0+ (Ractor#take was removed in 4.0)
|
|
165
|
+
|
|
166
|
+
== Versioning
|
|
167
|
+
|
|
168
|
+
This gem follows a `{LIBPNG_VERSION}.{ITERATION}` scheme:
|
|
169
|
+
|
|
170
|
+
- `LIBPNG_VERSION` is the upstream libpng release the gem is built
|
|
171
|
+
against (currently `1.6.58`).
|
|
172
|
+
- `ITERATION` is a counter that bumps on Ruby-side changes (recipe bug
|
|
173
|
+
fixes, CI tweaks, doc updates). It resets to 0 each time
|
|
174
|
+
`LIBPNG_VERSION` bumps.
|
|
175
|
+
|
|
176
|
+
For example, `1.6.58.0` is the first release against libpng 1.6.58,
|
|
177
|
+
`1.6.58.1` is a Ruby-side fix, and `1.6.59.0` would be a release against
|
|
178
|
+
the next upstream libpng.
|
|
179
|
+
|
|
180
|
+
== How it works
|
|
181
|
+
|
|
182
|
+
The gem has three layers:
|
|
183
|
+
|
|
184
|
+
1. `lib/libpng.rb` -- Ruby FFI wrapper. Loads `libpng16.{so,dylib,dll}`
|
|
185
|
+
from the gem's `lib/libpng/` directory at runtime and exposes
|
|
186
|
+
`Libpng.encode` / `Libpng.decode` against libpng's "simplified"
|
|
187
|
+
high-level C API (`png_image_*`). The wrapper manually lays out the
|
|
188
|
+
`png_image` struct via explicit byte offsets into an
|
|
189
|
+
`FFI::MemoryPointer` rather than `FFI::Struct`, because `FFI::Struct`
|
|
190
|
+
carries class-level state that is not shareable across non-main
|
|
191
|
+
Ractors.
|
|
192
|
+
|
|
193
|
+
2. `lib/libpng/recipe.rb` -- a `MiniPortileCMake` recipe that downloads
|
|
194
|
+
the libpng source tarball pinned by `LIBPNG_URL` / `LIBPNG_SHA256`,
|
|
195
|
+
runs CMake with `PNG_SHARED=ON PNG_STATIC=OFF PNG_TESTS=OFF`, and
|
|
196
|
+
copies the resulting shared lib into `lib/libpng/`. Invoked only when
|
|
197
|
+
installing the **source** (`ruby` platform) gem.
|
|
198
|
+
|
|
199
|
+
3. `ext/extconf.rb` -- gem extension entry point. Calls `Recipe#cook_if_not`,
|
|
200
|
+
which is a no-op once the per-platform checkpoint file
|
|
201
|
+
(`ports/.../<name>-<ver>-<platform>.installed`) exists. After cooking,
|
|
202
|
+
emits a dummy `Makefile` via `mkmf` so RubyGems is satisfied (no Ruby
|
|
203
|
+
C extension is compiled). The pre-compiled platform gems unset
|
|
204
|
+
`spec.extensions`, so this never runs for them.
|
|
205
|
+
|
|
206
|
+
=== Pre-compiled platform gems
|
|
207
|
+
|
|
208
|
+
`Rakefile` defines `gem:native:<platform>` tasks for each target. Each
|
|
209
|
+
task clones the gemspec, sets `spec.platform`, adds
|
|
210
|
+
`lib/libpng/*.{dll,so,dylib}` to `spec.files`, removes the
|
|
211
|
+
`mini_portile2` dependency (no longer needed at install time), and
|
|
212
|
+
unsets `spec.extensions` -- the resulting gem installs with no compiler.
|
|
213
|
+
|
|
214
|
+
The release workflow (`.github/workflows/release.yml`) builds all 10
|
|
215
|
+
platforms in parallel on the appropriate native runner:
|
|
216
|
+
|
|
217
|
+
- x86_64 Linux glibc -- `ubuntu-latest`
|
|
218
|
+
- ARM64 Linux glibc -- `ubuntu-24.04-arm` (native)
|
|
219
|
+
- x86_64 Linux musl -- `ruby:<ver>-alpine` via `docker run` on `ubuntu-latest`
|
|
220
|
+
- ARM64 Linux musl -- `ruby:<ver>-alpine` via `docker run` on `ubuntu-24.04-arm`
|
|
221
|
+
- x64 Windows (both MSVCRT and UCRT) -- `windows-latest` with `step-security/msvc-dev-cmd`
|
|
222
|
+
- ARM64 Windows -- `windows-11-arm` (native) with `arch: arm64`
|
|
223
|
+
- Intel macOS -- `macos-15-intel`
|
|
224
|
+
- Apple Silicon macOS -- `macos-latest`
|
|
225
|
+
|
|
226
|
+
Linux glibc and macOS builds run natively on the matching runner. Linux
|
|
227
|
+
musl builds run inside `ruby:<ver>-alpine` containers via `docker run`
|
|
228
|
+
rather than the workflow `container:` field, because Actions' JS runtime
|
|
229
|
+
has no arm64-musl build and therefore cannot run Alpine containers on
|
|
230
|
+
arm64 runners. Windows builds use the Visual Studio toolchain via
|
|
231
|
+
`step-security/msvc-dev-cmd` (a maintained drop-in for the deleted
|
|
232
|
+
`ilammy/msvc-dev-tools`).
|
|
233
|
+
|
|
234
|
+
== License
|
|
235
|
+
|
|
236
|
+
BSD-2-Clause; see link:LICENSE.txt[LICENSE.txt]. The gem bundles
|
|
237
|
+
pre-compiled libpng binaries; libpng itself is distributed under the
|
|
238
|
+
https://github.com/pnggroup/libpng/blob/main/LICENSE[libpng license].
|
data/Rakefile
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
require 'rubocop/rake_task'
|
|
9
|
+
|
|
10
|
+
RuboCop::RakeTask.new
|
|
11
|
+
|
|
12
|
+
task default: %i[spec rubocop]
|
|
13
|
+
|
|
14
|
+
task :compile do
|
|
15
|
+
require_relative 'ext/extconf'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
task spec: :compile
|
|
19
|
+
|
|
20
|
+
desc 'Build install-compilation gem'
|
|
21
|
+
task 'gem:native:any' do
|
|
22
|
+
sh 'rake platform:any gem'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
require 'rubygems/package_task'
|
|
26
|
+
|
|
27
|
+
desc 'Define the gem task to build on any platform (compile on install)'
|
|
28
|
+
task 'platform:any' do
|
|
29
|
+
spec = Gem::Specification.load('libpng.gemspec').dup
|
|
30
|
+
task = Gem::PackageTask.new(spec)
|
|
31
|
+
task.define
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
platforms = %w[
|
|
35
|
+
x64-mingw32
|
|
36
|
+
x64-mingw-ucrt
|
|
37
|
+
aarch64-mingw-ucrt
|
|
38
|
+
x86_64-linux
|
|
39
|
+
x86_64-linux-musl
|
|
40
|
+
aarch64-linux
|
|
41
|
+
aarch64-linux-musl
|
|
42
|
+
aarch64-linux-ohos
|
|
43
|
+
x86_64-darwin
|
|
44
|
+
arm64-darwin
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
platforms.each do |platform|
|
|
48
|
+
desc "Build pre-compiled gem for the #{platform} platform"
|
|
49
|
+
task "gem:native:#{platform}" do
|
|
50
|
+
sh "rake compile platform:#{platform} gem target_platform=#{platform}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "Define the gem task to build on the #{platform} platform (binary gem)"
|
|
54
|
+
task "platform:#{platform}" do
|
|
55
|
+
spec = Gem::Specification.load('libpng.gemspec').dup
|
|
56
|
+
spec.platform = Gem::Platform.new(platform)
|
|
57
|
+
spec.files += Dir.glob('lib/libpng/*.{dll,so,dylib}')
|
|
58
|
+
spec.extensions = []
|
|
59
|
+
spec.dependencies.reject! { |d| d.name == 'mini_portile2' }
|
|
60
|
+
|
|
61
|
+
task = Gem::PackageTask.new(spec)
|
|
62
|
+
task.define
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
require 'rake/clean'
|
|
67
|
+
|
|
68
|
+
CLOBBER.include('pkg')
|
|
69
|
+
CLEAN.include('ports',
|
|
70
|
+
'tmp',
|
|
71
|
+
'lib/libpng/*.dll',
|
|
72
|
+
'lib/libpng/*.dylib',
|
|
73
|
+
'lib/libpng/*.so',
|
|
74
|
+
'lib/libpng/*.so.*')
|
data/bin/console
ADDED
data/bin/setup
ADDED
data/ext/extconf.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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'))
|
|
9
|
+
|
|
10
|
+
require 'mkmf'
|
|
11
|
+
require 'libpng' # triggers autoload setup; Libpng::Recipe loads lazily
|
|
12
|
+
|
|
13
|
+
recipe = Libpng::Recipe.new
|
|
14
|
+
recipe.cook_if_not
|
|
15
|
+
|
|
16
|
+
# RubyGems requires every extconf.rb to leave a Makefile behind, even if
|
|
17
|
+
# no native extension is compiled (libpng is built by Recipe#cook above
|
|
18
|
+
# and loaded via FFI at runtime). dummy_makefile satisfies that contract.
|
|
19
|
+
create_makefile('libpng/dummy')
|