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.
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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development do
8
+ gem 'rake', '~> 13.0'
9
+ gem 'rspec', '~> 3.13'
10
+ gem 'rubocop', '~> 1.65', require: false
11
+ gem 'rubocop-performance', require: false
12
+ end
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
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ # frozen_string_literal: true
3
+
4
+ # Console entry point for the libpng gem.
5
+ exec ruby -Ilib -rlibpng -e 'puts "Libpng #{Libpng::VERSION} loaded"; binding.irb' "$@"
data/bin/setup ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'libpng'
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')