libpng 1.6.58.1 → 1.6.58.3

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -4
  3. data/CHANGELOG.md +99 -0
  4. data/CLAUDE.md +109 -0
  5. data/README.adoc +149 -23
  6. data/TODO.roadmap/01-claude-md-update.md +27 -0
  7. data/TODO.roadmap/02-encode-standard-ractor-spec.md +24 -0
  8. data/TODO.roadmap/03-mini-portile2-bump.md +26 -0
  9. data/TODO.roadmap/04-changelog.md +34 -0
  10. data/TODO.roadmap/05-benchmarks.md +41 -0
  11. data/TODO.roadmap/06-malformed-input-tests.md +47 -0
  12. data/TODO.roadmap/07-interlace-option.md +40 -0
  13. data/TODO.roadmap/08-palette-support.md +54 -0
  14. data/TODO.roadmap/09-16-bit-encode.md +53 -0
  15. data/TODO.roadmap/10-libpng-1.6.59-bump.md +59 -0
  16. data/TODO.roadmap/11-read-side-metadata.md +59 -0
  17. data/TODO.roadmap/12-full-api-surface.md +61 -0
  18. data/TODO.roadmap/13-c-extension-setjmp.md +77 -0
  19. data/TODO.roadmap/14-progressive-decode.md +47 -0
  20. data/TODO.roadmap/15-file-io-variants.md +57 -0
  21. data/TODO.roadmap/16-architecture-refactor.md +97 -0
  22. data/TODO.roadmap/17-ruby-version-policy.md +46 -0
  23. data/TODO.roadmap/18-dependabot.md +52 -0
  24. data/TODO.roadmap/19-text-chunk-read.md +104 -0
  25. data/TODO.roadmap/20-color-metadata-read.md +100 -0
  26. data/TODO.roadmap/README.md +42 -0
  27. data/ext/extconf.rb +9 -2
  28. data/lib/libpng/binding.rb +57 -0
  29. data/lib/libpng/bytes_per_pixel.rb +43 -0
  30. data/lib/libpng/chunk_walker.rb +103 -0
  31. data/lib/libpng/decoded_image.rb +17 -0
  32. data/lib/libpng/error.rb +8 -0
  33. data/lib/libpng/recipe.rb +0 -1
  34. data/lib/libpng/simplified_decoder.rb +84 -0
  35. data/lib/libpng/simplified_encoder.rb +98 -0
  36. data/lib/libpng/standard_encoder.rb +219 -0
  37. data/lib/libpng/version.rb +1 -1
  38. data/lib/libpng.rb +135 -244
  39. data/libpng.gemspec +1 -1
  40. metadata +35 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91ec3180573925adf91a3fb1463a008d05a113f633ca4a4dcd00546008585699
4
- data.tar.gz: 34cc29c477d3bd9939067304ef3fc21357b773df42022bf16c6ef52323f25c5b
3
+ metadata.gz: b8e15eabb884474f99879a934d6472eba94e686b19dd734f87a6ab80b7f815d2
4
+ data.tar.gz: 511877de0b69d9813a7eec8ea2d5e25ab932a32a9edf89139ac8a5e635adf426
5
5
  SHA512:
6
- metadata.gz: 4489b62efba7933a588fabafd4d0cb65122a77d0a363a476a80db1e726affcffbe70a02ac7783b916b867d59ad9a0f6ebd1319d12421a3ab65e74d92822e143c
7
- data.tar.gz: 34042f2512c7234881c36bbcb140b283f267288d7b0575a71131773a681419f5542b9419de40355a5190050f9415b0081af3396e7115153fb6026575b58d5415
6
+ metadata.gz: 1bdb8b75f3ca27c415793d03ba234b18e63a155266e1f187012866eceda8c3e40b5409375a3045a1686ea6c3bed527c8d8be710aeb2a68483f362273c5bf467e
7
+ data.tar.gz: ae9cf74ed74fc27bf55ddc92a4a50b2cd347a0319e3d8eb6821e2456a7dd762e062061b7a208708ef358161251b7ae488f80a46aa783effe85b164280f49f036
data/.rubocop.yml CHANGED
@@ -10,19 +10,19 @@ Gemspec/DevelopmentDependencies:
10
10
  # methods that walk libpng's chunked API; splitting would obscure the
11
11
  # 1-to-1 mapping with the C API.
12
12
  Metrics/AbcSize:
13
- Max: 60
13
+ Max: 70
14
14
  Metrics/ClassLength:
15
15
  Max: 200
16
16
  Metrics/CyclomaticComplexity:
17
17
  Max: 20
18
18
  Metrics/MethodLength:
19
- Max: 50
19
+ Max: 70
20
20
  Metrics/ModuleLength:
21
- Max: 250
21
+ Max: 350
22
22
  Metrics/PerceivedComplexity:
23
23
  Max: 20
24
24
  Metrics/ParameterLists:
25
- Max: 7
25
+ Max: 9
26
26
 
27
27
  Metrics/BlockLength:
28
28
  Exclude:
data/CHANGELOG.md ADDED
@@ -0,0 +1,99 @@
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.3] - 2026-07-26
11
+
12
+ ### Added
13
+ - `Libpng.encode_standard` -- standard libpng write API
14
+ (`png_create_write_struct` -> `png_set_IHDR` -> `png_set_rows` ->
15
+ `png_write_png(PNG_TRANSFORM_IDENTITY)`). Mirrors libemf2svg's
16
+ `rgb2png` byte layout and emits only IHDR/IDAT/IEND directly (no
17
+ post-hoc chunk stripping).
18
+ - `interlace:` option on `encode_standard` -- `:none` (default) or
19
+ `:adam7`.
20
+ - `bit_depth:` option on `encode_standard` -- 8 (default) or 16 for
21
+ RGB/RGBA/GRAY.
22
+ - `pixel_format: :palette` with `palette:` option on `encode_standard` --
23
+ emits PNG_COLOR_TYPE_PALETTE via `png_set_PLTE` (and `png_set_tRNS`
24
+ when any palette entry has alpha).
25
+ - `Libpng::DecodedImage#bit_depth`, `#color_type`, `#interlace` -- IHDR
26
+ metadata fields populated by walking the source PNG after decode.
27
+ - `Libpng::ChunkWalker` -- public class for walking PNG chunks:
28
+ `#each_chunk`, `#strip_ancillary`, `#ihdr_fields`.
29
+ - `Libpng::BytesPerPixel` -- pure-data helper module mapping formats
30
+ and color types to bytes-per-pixel (8- and 16-bit aware).
31
+ - Architecture refactor: split the monolithic `lib/libpng.rb` (466
32
+ lines) into MECE per-concern files using Ruby `autoload`. New files:
33
+ `lib/libpng/error.rb`, `decoded_image.rb`, `chunk_walker.rb`,
34
+ `bytes_per_pixel.rb`, `simplified_encoder.rb`,
35
+ `simplified_decoder.rb`, `standard_encoder.rb`. Public API
36
+ unchanged.
37
+ - Specs: malformed-input suite, encode_standard Ractor suite,
38
+ benchmark suite, options suite for interlace/bit_depth/palette,
39
+ metadata suite for DecodedImage + ChunkWalker.
40
+
41
+ ### Changed
42
+ - `mini_portile2` dependency bumped from `~> 2.6` to `~> 2.8`.
43
+ - Removed all `require_relative` from library code in favor of
44
+ `autoload` (per project code-quality rules).
45
+ - `ext/extconf.rb` now triggers `Libpng::Recipe` autoload via
46
+ `require 'libpng'` instead of reaching into `lib/libpng/recipe.rb`
47
+ directly.
48
+
49
+ ### Fixed
50
+ - Filter option documentation: clarified that `:default`, `:adaptive`,
51
+ and `:all` produce byte-identical output but exercise different
52
+ libpng code paths. `:none` forces no filtering.
53
+ - Removed redundant `.dup.freeze` on `LIBPNG_VER_STRING_C` (the source
54
+ string is already a frozen literal).
55
+
56
+ ## [1.6.58.2] - 2026-07-26
57
+
58
+ ### Added
59
+ - `Libpng.encode_standard` initial release -- standard libpng write
60
+ API with memory-stream output via `png_set_write_fn` FFI callback
61
+ (no Tempfile), filter control, compression level control, and error
62
+ handling via `png_set_error_fn` callback.
63
+ - Specs: 32 specs covering encode_standard round-trips, filter
64
+ variants, compression levels, chunk-layout assertions, parity check
65
+ against simplified `encode`.
66
+
67
+ ## [1.6.58.1] - 2026-07-25
68
+
69
+ ### Added
70
+ - Full platform coverage: `aarch64-linux-musl`, `aarch64-linux` (native
71
+ on `ubuntu-24.04-arm`), `aarch64-mingw-ucrt` (native on
72
+ `windows-11-arm`), `x86_64-linux-musl` (Alpine via `docker run`).
73
+ - `step-security/msvc-dev-cmd@v1` replaces deleted `ilammy/msvc-dev-tools`
74
+ action.
75
+ - Release workflow tolerates already-published gems (skip-on-conflict).
76
+ - Specs: Ractor safety suite for `encode` and `decode` (Ruby 3.x and 4.0).
77
+
78
+ ### Fixed
79
+ - `ext/extconf.rb` now emits a dummy Makefile so RubyGems is satisfied
80
+ when installing the source (`ruby` platform) gem.
81
+ - Recipe globs `{bin,lib}/libpng16*.dll` on Windows (CMake's
82
+ GNUInstallDirs puts the .dll in `bin/`, not `lib/`).
83
+ - Alpine build runs inside `docker run` (not the `container:` field)
84
+ so it works on arm64 Ubuntu runners.
85
+ - `git config --global --add safe.directory /work` in Alpine container
86
+ so `git ls-files` works for the gemspec.
87
+
88
+ ## [1.6.58.0] - 2026-07-25
89
+
90
+ ### Added
91
+ - Initial pre-compiled libpng gem. Bundles libpng 1.6.58 shared
92
+ libraries for x86_64 Linux, x86_64 macOS, arm64 macOS, x64 Windows
93
+ (MSVCRT and UCRT).
94
+ - `Libpng.encode` / `Libpng.decode` -- simplified API binding via FFI.
95
+ - `strip_colorspace:` option on `encode` to drop sRGB/gAMA chunks the
96
+ simplified API emits by default.
97
+ - `convert_to_8bit:` option on `encode` for 16-bit input.
98
+ - MiniPortile-based recipe for building libpng from source when
99
+ installing the platform-agnostic `ruby` gem.
data/CLAUDE.md ADDED
@@ -0,0 +1,109 @@
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
+ 10 platform gems are published per release (see README for the full matrix).
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 from PNG bytes) |
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 (~81 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`, `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 10 platform gems, 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/malformed_input_spec.rb` — corrupt PNG input handling
101
+ - `spec/ractor_spec.rb` — Ractor safety for simplified API
102
+ - `spec/ractor_standard_spec.rb` — Ractor safety for standard API
103
+ - `spec/benchmark_spec.rb` — encode/decode timing comparison
104
+
105
+ ## See also
106
+
107
+ - `TODO.roadmap/` — prioritized backlog with status (done/planned/wontdo) per item
108
+ - `CHANGELOG.md` — release history
109
+ - `REPORT-complex-api-needs.md` — historical report that drove `encode_standard`
data/README.adoc CHANGED
@@ -22,9 +22,33 @@ Or install manually:
22
22
 
23
23
  gem install libpng
24
24
 
25
- The gem downloads a pre-compiled binary for your platform automatically.
26
- No C compiler, libpng development headers, or system packages are needed
27
- at install time.
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 pre-compiled platform gems are published for each release
35
+ (see the link:https://rubygems.org/gems/libpng/versions[RubyGems versions page]
36
+ for the current set):
37
+
38
+ [cols="1,1,3", options="header"]
39
+ |===
40
+ | Gem platform | Target | Build host
41
+ | `ruby` (source) | Any platform (compiles on install) | n/a
42
+ | `x86_64-linux` | x86_64 Linux (glibc) | `ubuntu-latest`
43
+ | `x86_64-linux-musl` | x86_64 Linux (musl, e.g. Alpine) | `ruby:<ver>-alpine` on x86_64
44
+ | `aarch64-linux` | ARM64 Linux (glibc) | `ubuntu-24.04-arm` (native)
45
+ | `aarch64-linux-musl` | ARM64 Linux (musl) | `ruby:<ver>-alpine` on arm64
46
+ | `x64-mingw32` | x64 Windows, RubyInstaller < 3.0 (MSVCRT) | `windows-latest`
47
+ | `x64-mingw-ucrt` | x64 Windows, RubyInstaller >= 3.0 (UCRT) | `windows-latest`
48
+ | `aarch64-mingw-ucrt` | ARM64 Windows (Ruby >= 3.4) | `windows-11-arm` (native)
49
+ | `x86_64-darwin` | Intel macOS | `macos-15-intel`
50
+ | `arm64-darwin` | Apple Silicon macOS | `macos-latest`
51
+ |===
28
52
 
29
53
  == Usage
30
54
 
@@ -36,10 +60,13 @@ at install time.
36
60
 
37
61
  # Decode a PNG into raw pixels.
38
62
  decoded = Libpng.decode(File.binread("out.png"), pixel_format: "RGBA")
39
- decoded.width # => Integer
40
- decoded.height # => Integer
41
- decoded.format # => "RGBA"
42
- decoded.pixels # => binary String of RGBA bytes
63
+ decoded.width # => Integer
64
+ decoded.height # => Integer
65
+ decoded.format # => "RGBA"
66
+ decoded.pixels # => binary String of RGBA bytes
67
+ decoded.bit_depth # => 8 (from IHDR)
68
+ decoded.color_type # => 6 (PNG_COLOR_TYPE_RGB_ALPHA, from IHDR)
69
+ decoded.interlace # => 0 (PNG_INTERLACE_NONE, from IHDR)
43
70
 
44
71
  === Supported pixel formats
45
72
 
@@ -52,12 +79,71 @@ at install time.
52
79
 
53
80
  Libpng.encode(width, height, pixels,
54
81
  pixel_format: "RGBA",
55
- convert_to_8bit: false)
82
+ convert_to_8bit: false,
83
+ strip_colorspace: true)
56
84
 
57
- `convert_to_8bit: true` causes libpng to convert 16-bit input to 8-bit
85
+ `convert_to_8bit: true`:: causes libpng to convert 16-bit input to 8-bit
58
86
  on write (input is still passed as a packed byte buffer; use this if you
59
87
  have packed 16-bit-per-channel data).
60
88
 
89
+ `strip_colorspace: true`:: (default) walks the PNG chunk list after
90
+ encode and keeps only `IHDR`, `IDAT`, and `IEND`, dropping the sRGB/gAMA
91
+ chunks libpng's simplified API emits by default. This matches the
92
+ "classic" libpng write path (e.g. libemf2svg) and produces byte-identical
93
+ output. Set to `false` to keep the ancillary color chunks.
94
+
95
+ === Standard API (encode_standard)
96
+
97
+ For parity with libpng's classic write path -- the same code path used
98
+ by `libemf2svg`'s `rgb2png` -- use `encode_standard` instead of
99
+ `encode`. It calls `png_create_write_struct` + `png_set_IHDR` +
100
+ `png_set_rows` + `png_write_png(PNG_TRANSFORM_IDENTITY)` directly.
101
+
102
+ Libpng.encode_standard(width, height, pixels,
103
+ pixel_format: "RGBA",
104
+ filter: :default,
105
+ compression_level: 6,
106
+ interlace: :none,
107
+ bit_depth: 8,
108
+ palette: nil)
109
+
110
+ `pixel_format`:: `:gray`, `:ga`, `:rgb`, `:rgba`, or `:palette`. The
111
+ byte-order variants (`BGR`, `ARGB`, `BGRA`, `ABGR`) are simplified-API
112
+ only -- the standard API always emits host-order.
113
+
114
+ `filter`:: `:default` (adaptive -- libpng picks the best filter per
115
+ row, the libemf2svg default), `:adaptive` (same output, exercises a
116
+ different libpng code path), `:none`, `:sub`, `:up`, `:avg`, `:paeth`,
117
+ `:all`.
118
+
119
+ `compression_level`:: zlib level 0-9. Default 6 (Z_DEFAULT_COMPRESSION,
120
+ matching libemf2svg).
121
+
122
+ `interlace`:: `:none` (default) or `:adam7`.
123
+
124
+ `bit_depth`:: 8 (default) or 16. Only valid for `:gray`, `:rgb`,
125
+ `:rgba`. 16-bit callers must provide 2 bytes per channel
126
+ (host-order little-endian). Always 8 for `:palette`.
127
+
128
+ `palette`:: Array of `[r, g, b]` or `[r, g, b, a]` (1..256 entries).
129
+ Required when `pixel_format: :palette`. If any entry has alpha, a `tRNS`
130
+ chunk is emitted.
131
+
132
+ Output is written directly into a Ruby String via `png_set_write_fn` --
133
+ no `Tempfile`, no disk I/O. The FFI write callback is held on a local
134
+ `Array` so it survives the libpng calls.
135
+
136
+ Differences from `encode`:
137
+ - Emits only `IHDR` / `IDAT` / `IEND` (no post-hoc chunk stripping
138
+ needed).
139
+ - Supports palette, 16-bit, and interlaced output -- simplified API
140
+ doesn't.
141
+ - Errors are raised via a `png_set_error_fn` callback (set on
142
+ `png_create_write_struct`). libpng expects error callbacks to
143
+ longjmp; we rb_raise instead, which unwinds the Ruby stack. The
144
+ `ensure` block calls `png_destroy_write_struct` to release the C
145
+ state.
146
+
61
147
  == Ractor safety
62
148
 
63
149
  `Libpng.encode` and `Libpng.decode` are Ractor-safe. Every call
@@ -65,25 +151,26 @@ allocates and frees its own libpng `png_image`; no shared mutable state
65
151
  exists on the Ruby side. Calls from multiple Ractors run concurrently
66
152
  without locking.
67
153
 
68
- Example:
154
+ Example (Ruby 3.x uses `Ractor#take`; Ruby 4.0+ uses `Ractor#value`):
69
155
 
70
156
  r = Ractor.new do
71
157
  Libpng.encode(2, 2, "\xFF" * 16, pixel_format: "RGBA")
72
158
  end
73
- r.take # => PNG binary
159
+ r.take # Ruby 3.x
160
+ r.value # Ruby 4.0+ (Ractor#take was removed in 4.0)
74
161
 
75
162
  == Versioning
76
163
 
77
164
  This gem follows a `{LIBPNG_VERSION}.{ITERATION}` scheme:
78
165
 
79
166
  - `LIBPNG_VERSION` is the upstream libpng release the gem is built
80
- against (currently 1.6.48).
167
+ against (currently `1.6.58`).
81
168
  - `ITERATION` is a counter that bumps on Ruby-side changes (recipe bug
82
169
  fixes, CI tweaks, doc updates). It resets to 0 each time
83
170
  `LIBPNG_VERSION` bumps.
84
171
 
85
- For example, `1.6.48.0` is the first release against libpng 1.6.48,
86
- `1.6.48.1` is a Ruby-side fix, and `1.6.49.0` would be a release against
172
+ For example, `1.6.58.0` is the first release against libpng 1.6.58,
173
+ `1.6.58.1` is a Ruby-side fix, and `1.6.59.0` would be a release against
87
174
  the next upstream libpng.
88
175
 
89
176
  == How it works
@@ -91,15 +178,54 @@ the next upstream libpng.
91
178
  The gem has three layers:
92
179
 
93
180
  1. `lib/libpng.rb` -- Ruby FFI wrapper. Loads `libpng16.{so,dylib,dll}`
94
- from the gem's `lib/libpng/` directory at runtime.
95
- 2. `lib/libpng/recipe.rb` -- a MiniPortileCMake recipe that downloads
96
- the libpng source tarball and runs CMake to produce the shared
97
- library. This is invoked by `ext/extconf.rb` when the gem is installed
98
- from source.
99
- 3. `Rakefile` defines `gem:native:<platform>` tasks that pre-compile
100
- the gem for each target platform. The pre-compiled gems bundle the
101
- `.so`/`.dylib`/`.dll` and disable `extconf.rb`, so installation
102
- requires no C compiler.
181
+ from the gem's `lib/libpng/` directory at runtime and exposes
182
+ `Libpng.encode` / `Libpng.decode` against libpng's "simplified"
183
+ high-level C API (`png_image_*`). The wrapper manually lays out the
184
+ `png_image` struct via explicit byte offsets into an
185
+ `FFI::MemoryPointer` rather than `FFI::Struct`, because `FFI::Struct`
186
+ carries class-level state that is not shareable across non-main
187
+ Ractors.
188
+
189
+ 2. `lib/libpng/recipe.rb` -- a `MiniPortileCMake` recipe that downloads
190
+ the libpng source tarball pinned by `LIBPNG_URL` / `LIBPNG_SHA256`,
191
+ runs CMake with `PNG_SHARED=ON PNG_STATIC=OFF PNG_TESTS=OFF`, and
192
+ copies the resulting shared lib into `lib/libpng/`. Invoked only when
193
+ installing the **source** (`ruby` platform) gem.
194
+
195
+ 3. `ext/extconf.rb` -- gem extension entry point. Calls `Recipe#cook_if_not`,
196
+ which is a no-op once the per-platform checkpoint file
197
+ (`ports/.../<name>-<ver>-<platform>.installed`) exists. After cooking,
198
+ emits a dummy `Makefile` via `mkmf` so RubyGems is satisfied (no Ruby
199
+ C extension is compiled). The pre-compiled platform gems unset
200
+ `spec.extensions`, so this never runs for them.
201
+
202
+ === Pre-compiled platform gems
203
+
204
+ `Rakefile` defines `gem:native:<platform>` tasks for each target. Each
205
+ task clones the gemspec, sets `spec.platform`, adds
206
+ `lib/libpng/*.{dll,so,dylib}` to `spec.files`, removes the
207
+ `mini_portile2` dependency (no longer needed at install time), and
208
+ unsets `spec.extensions` -- the resulting gem installs with no compiler.
209
+
210
+ The release workflow (`.github/workflows/release.yml`) builds all 10
211
+ platforms in parallel on the appropriate native runner:
212
+
213
+ - x86_64 Linux glibc -- `ubuntu-latest`
214
+ - ARM64 Linux glibc -- `ubuntu-24.04-arm` (native)
215
+ - x86_64 Linux musl -- `ruby:<ver>-alpine` via `docker run` on `ubuntu-latest`
216
+ - ARM64 Linux musl -- `ruby:<ver>-alpine` via `docker run` on `ubuntu-24.04-arm`
217
+ - x64 Windows (both MSVCRT and UCRT) -- `windows-latest` with `step-security/msvc-dev-cmd`
218
+ - ARM64 Windows -- `windows-11-arm` (native) with `arch: arm64`
219
+ - Intel macOS -- `macos-15-intel`
220
+ - Apple Silicon macOS -- `macos-latest`
221
+
222
+ Linux glibc and macOS builds run natively on the matching runner. Linux
223
+ musl builds run inside `ruby:<ver>-alpine` containers via `docker run`
224
+ rather than the workflow `container:` field, because Actions' JS runtime
225
+ has no arm64-musl build and therefore cannot run Alpine containers on
226
+ arm64 runners. Windows builds use the Visual Studio toolchain via
227
+ `step-security/msvc-dev-cmd` (a maintained drop-in for the deleted
228
+ `ilammy/msvc-dev-tools`).
103
229
 
104
230
  == License
105
231
 
@@ -0,0 +1,27 @@
1
+ # 01 - CLAUDE.md update
2
+
3
+ - **Priority**: P0
4
+ - **Status**: Done
5
+
6
+ ## Context
7
+
8
+ The agent-facing `CLAUDE.md` was out of sync after `encode_standard`,
9
+ the platform matrix expansion, and the architecture refactor. Future
10
+ Claude instances would land in a codebase that no longer matched the
11
+ documented structure.
12
+
13
+ ## Approach
14
+
15
+ Re-created `CLAUDE.md` with:
16
+ - File-responsibility table for the new autoloaded structure
17
+ - Public API surface (encode / encode_standard / decode) with options
18
+ - FFI binding inventory (simplified + standard APIs)
19
+ - Ractor safety section including the Ruby 4.0 `#value` migration
20
+ - Code quality rules (no `require_relative`, no `send`, no `iv_get`,
21
+ no `respond_to?` -- the project-wide rules)
22
+ - Spec inventory
23
+ - Cross-links to `TODO.roadmap/`, `CHANGELOG.md`, `REPORT-complex-api-needs.md`
24
+
25
+ ## Delivered
26
+
27
+ PR #7 (this branch).
@@ -0,0 +1,24 @@
1
+ # 02 - encode_standard Ractor spec
2
+
3
+ - **Priority**: P0
4
+ - **Status**: Done
5
+
6
+ ## Context
7
+
8
+ PR #6 added `encode_standard` and claimed Ractor safety (every call
9
+ allocates its own `png_struct`), but only `encode` and `decode` had
10
+ Ractor specs. Unverified claims are worse than no claims.
11
+
12
+ ## Approach
13
+
14
+ New spec file `spec/ractor_standard_spec.rb` with two cases:
15
+ 1. Encode inside a Ractor, round-trip the bytes in the main Ractor.
16
+ 2. 8 concurrent Ractors running `encode_standard(filter: :sub)` and
17
+ verifying decode round-trip.
18
+
19
+ Uses the `ractor_result(ractor)` helper (prefers `#value` on Ruby 4.0+,
20
+ falls back to `#take` on Ruby 3.x).
21
+
22
+ ## Delivered
23
+
24
+ PR #7 (this branch).
@@ -0,0 +1,26 @@
1
+ # 03 - Bump mini_portile2 dependency
2
+
3
+ - **Priority**: P0
4
+ - **Status**: Done
5
+
6
+ ## Context
7
+
8
+ The gemspec constrained `mini_portile2` to `~> 2.6`. Current upstream
9
+ is 2.8.9. The constraint prevented the source gem from picking up
10
+ mini_portile2 bug fixes and CMake improvements.
11
+
12
+ ## Approach
13
+
14
+ Bumped `libpng.gemspec`:
15
+
16
+ ```ruby
17
+ spec.add_dependency 'mini_portile2', '~> 2.8'
18
+ ```
19
+
20
+ Allowed versions 2.8.x. Did not jump to `>= 2.8` (unbounded) because
21
+ mini_portile2 has had breaking changes between major versions in the
22
+ past; staying on `~> 2.8` is the conservative choice.
23
+
24
+ ## Delivered
25
+
26
+ PR #7 (this branch).
@@ -0,0 +1,34 @@
1
+ # 04 - CHANGELOG.md
2
+
3
+ - **Priority**: P1
4
+ - **Status**: Done
5
+
6
+ ## Context
7
+
8
+ Release notes lived in PR descriptions and git tags -- invisible to
9
+ users tracking what changed between versions. The README's
10
+ `Versioning` section described the scheme but not what each release
11
+ actually delivered.
12
+
13
+ ## Approach
14
+
15
+ New `CHANGELOG.md` following the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
16
+ format. Retroactively populated with entries for every released
17
+ version:
18
+
19
+ - 1.6.58.0 -- initial release
20
+ - 1.6.58.1 -- parsanol platform parity (musl, Windows ARM64, native arm64 runners)
21
+ - 1.6.58.2 -- encode_standard initial release
22
+ - 1.6.58.3 -- architecture refactor, encode_standard options
23
+ (interlace/bit_depth/palette), read-side metadata, autoload+OOP
24
+
25
+ Each entry categorizes changes as Added / Changed / Fixed per Keep a
26
+ Changelog conventions.
27
+
28
+ The RubyGems `changelog_uri` metadata in `libpng.gemspec` already
29
+ points at `README.adoc#versioning`. Future: should it point at
30
+ `CHANGELOG.md` instead?
31
+
32
+ ## Delivered
33
+
34
+ PR #7 (this branch).
@@ -0,0 +1,41 @@
1
+ # 05 - Benchmarks for encode paths
2
+
3
+ - **Priority**: P1
4
+ - **Status**: Done
5
+
6
+ ## Context
7
+
8
+ `encode_standard` was claimed to be ~3x faster than the original
9
+ Tempfile-based approach (per `REPORT-complex-api-needs.md`), but no
10
+ benchmark existed to verify or catch regressions.
11
+
12
+ ## Approach
13
+
14
+ New spec file `spec/benchmark_spec.rb`. Three image sizes (16x16,
15
+ 128x128, 512x512). For each:
16
+
17
+ - `Libpng.encode` (simplified, strip_colorspace on)
18
+ - `Libpng.encode_standard` (memory write)
19
+ - `Libpng.decode` (simplified, RGBA)
20
+
21
+ Measures median of 50 iterations using `Benchmark.realtime`. Prints
22
+ comparable numbers via `warn` so they show in CI logs but don't affect
23
+ test pass/fail.
24
+
25
+ Sanity assertions (not timing-based):
26
+ - Both PNGs start with `\x89PNG`
27
+ - Standard PNG decodes to the same pixels
28
+
29
+ Benchmarks are smoke tests, not performance gates. Specific timings
30
+ vary by machine and CI runner load; gating on absolute numbers would
31
+ be flaky.
32
+
33
+ ## Delivered
34
+
35
+ PR #7 (this branch).
36
+
37
+ ## Future work
38
+
39
+ - Add a `rake benchmark` task that runs only `spec/benchmark_spec.rb`
40
+ - Track benchmark results across releases (maybe via GitHub Actions
41
+ artifacts) to spot performance regressions over time
@@ -0,0 +1,47 @@
1
+ # 06 - Malformed input tests for decode
2
+
3
+ - **Priority**: P1
4
+ - **Status**: Done
5
+
6
+ ## Context
7
+
8
+ The original spec covered "non-PNG input" and "truncated input" but
9
+ not:
10
+ - Empty / nil input
11
+ - Corrupt IHDR CRC
12
+ - Corrupt IDAT CRC
13
+ - Zero-dimension IHDR
14
+ - Impossibly large dimensions
15
+ - PNG missing IEND
16
+ - PNG truncated mid-IDAT
17
+
18
+ libpng's simplified read API is well-behaved on bad input (captures
19
+ errors into the `png_image` message buffer instead of aborting), but
20
+ the wrapper should still raise `Libpng::Error` rather than propagate
21
+ `NoMethodError` or segfault.
22
+
23
+ ## Approach
24
+
25
+ New spec file `spec/malformed_input_spec.rb`. Each case constructs a
26
+ broken PNG and asserts that `Libpng.decode` raises `Libpng::Error`
27
+ (or, for borderline cases, doesn't crash).
28
+
29
+ Helper methods on the spec:
30
+ - `with_chunk(png, type, replacement_data)` -- splice replacement data
31
+ into a specific chunk
32
+ - `corrupt_crc(png, type)` -- XOR the first CRC byte of a chunk
33
+
34
+ ## What we learned
35
+
36
+ - `nil` input previously raised `NoMethodError: undefined method
37
+ 'bytesize' for nil`. Fixed by adding a nil guard in
38
+ `Libpng::SimplifiedDecoder#initialize`.
39
+ - Zero-dimension IHDR detection works (libpng rejects).
40
+ - Corrupt IHDR CRC is detected by libpng and raises cleanly.
41
+ - Corrupt IDAT CRC: libpng's simplified API may either error or
42
+ accept (zlib is sometimes able to decode despite the bad CRC). The
43
+ spec tolerates both outcomes.
44
+
45
+ ## Delivered
46
+
47
+ PR #7 (this branch).