libpng 1.6.58.2 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +99 -0
- data/CLAUDE.md +109 -0
- data/README.adoc +32 -15
- data/TODO.roadmap/01-claude-md-update.md +27 -0
- data/TODO.roadmap/02-encode-standard-ractor-spec.md +24 -0
- data/TODO.roadmap/03-mini-portile2-bump.md +26 -0
- data/TODO.roadmap/04-changelog.md +34 -0
- data/TODO.roadmap/05-benchmarks.md +41 -0
- data/TODO.roadmap/06-malformed-input-tests.md +47 -0
- data/TODO.roadmap/07-interlace-option.md +40 -0
- data/TODO.roadmap/08-palette-support.md +54 -0
- data/TODO.roadmap/09-16-bit-encode.md +53 -0
- data/TODO.roadmap/10-libpng-1.6.59-bump.md +59 -0
- data/TODO.roadmap/11-read-side-metadata.md +59 -0
- data/TODO.roadmap/12-full-api-surface.md +61 -0
- data/TODO.roadmap/13-c-extension-setjmp.md +77 -0
- data/TODO.roadmap/14-progressive-decode.md +47 -0
- data/TODO.roadmap/15-file-io-variants.md +57 -0
- data/TODO.roadmap/16-architecture-refactor.md +97 -0
- data/TODO.roadmap/17-ruby-version-policy.md +46 -0
- data/TODO.roadmap/18-dependabot.md +52 -0
- data/TODO.roadmap/19-text-chunk-read.md +104 -0
- data/TODO.roadmap/20-color-metadata-read.md +100 -0
- data/TODO.roadmap/README.md +42 -0
- data/ext/extconf.rb +9 -2
- data/lib/libpng/binding.rb +57 -0
- data/lib/libpng/bytes_per_pixel.rb +43 -0
- data/lib/libpng/chunk_walker.rb +103 -0
- data/lib/libpng/decoded_image.rb +17 -0
- data/lib/libpng/error.rb +8 -0
- data/lib/libpng/recipe.rb +0 -1
- data/lib/libpng/simplified_decoder.rb +84 -0
- data/lib/libpng/simplified_encoder.rb +98 -0
- data/lib/libpng/standard_encoder.rb +219 -0
- data/lib/libpng/version.rb +1 -1
- data/lib/libpng.rb +99 -385
- data/libpng.gemspec +1 -1
- metadata +34 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8e15eabb884474f99879a934d6472eba94e686b19dd734f87a6ab80b7f815d2
|
|
4
|
+
data.tar.gz: 511877de0b69d9813a7eec8ea2d5e25ab932a32a9edf89139ac8a5e635adf426
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bdb8b75f3ca27c415793d03ba234b18e63a155266e1f187012866eceda8c3e40b5409375a3045a1686ea6c3bed527c8d8be710aeb2a68483f362273c5bf467e
|
|
7
|
+
data.tar.gz: ae9cf74ed74fc27bf55ddc92a4a50b2cd347a0319e3d8eb6821e2456a7dd762e062061b7a208708ef358161251b7ae488f80a46aa783effe85b164280f49f036
|
data/.rubocop.yml
CHANGED
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
|
@@ -60,10 +60,13 @@ for the current set):
|
|
|
60
60
|
|
|
61
61
|
# Decode a PNG into raw pixels.
|
|
62
62
|
decoded = Libpng.decode(File.binread("out.png"), pixel_format: "RGBA")
|
|
63
|
-
decoded.width
|
|
64
|
-
decoded.height
|
|
65
|
-
decoded.format
|
|
66
|
-
decoded.pixels
|
|
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)
|
|
67
70
|
|
|
68
71
|
=== Supported pixel formats
|
|
69
72
|
|
|
@@ -94,33 +97,47 @@ output. Set to `false` to keep the ancillary color chunks.
|
|
|
94
97
|
For parity with libpng's classic write path -- the same code path used
|
|
95
98
|
by `libemf2svg`'s `rgb2png` -- use `encode_standard` instead of
|
|
96
99
|
`encode`. It calls `png_create_write_struct` + `png_set_IHDR` +
|
|
97
|
-
`png_set_rows` + `png_write_png(PNG_TRANSFORM_IDENTITY)` directly
|
|
98
|
-
filter and zlib-compression control exposed:
|
|
100
|
+
`png_set_rows` + `png_write_png(PNG_TRANSFORM_IDENTITY)` directly.
|
|
99
101
|
|
|
100
102
|
Libpng.encode_standard(width, height, pixels,
|
|
101
103
|
pixel_format: "RGBA",
|
|
102
104
|
filter: :default,
|
|
103
|
-
compression_level: 6
|
|
105
|
+
compression_level: 6,
|
|
106
|
+
interlace: :none,
|
|
107
|
+
bit_depth: 8,
|
|
108
|
+
palette: nil)
|
|
104
109
|
|
|
105
|
-
`pixel_format`::
|
|
106
|
-
variants (`BGR`, `ARGB`, `BGRA`, `ABGR`) are simplified-API
|
|
107
|
-
the standard API always emits host-order.
|
|
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.
|
|
108
113
|
|
|
109
|
-
`filter`::
|
|
110
|
-
|
|
111
|
-
`:up`, `:avg`, `:paeth`,
|
|
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`.
|
|
112
118
|
|
|
113
119
|
`compression_level`:: zlib level 0-9. Default 6 (Z_DEFAULT_COMPRESSION,
|
|
114
120
|
matching libemf2svg).
|
|
115
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
|
+
|
|
116
132
|
Output is written directly into a Ruby String via `png_set_write_fn` --
|
|
117
133
|
no `Tempfile`, no disk I/O. The FFI write callback is held on a local
|
|
118
|
-
Array so it survives the libpng calls.
|
|
134
|
+
`Array` so it survives the libpng calls.
|
|
119
135
|
|
|
120
136
|
Differences from `encode`:
|
|
121
137
|
- Emits only `IHDR` / `IDAT` / `IEND` (no post-hoc chunk stripping
|
|
122
138
|
needed).
|
|
123
|
-
-
|
|
139
|
+
- Supports palette, 16-bit, and interlaced output -- simplified API
|
|
140
|
+
doesn't.
|
|
124
141
|
- Errors are raised via a `png_set_error_fn` callback (set on
|
|
125
142
|
`png_create_write_struct`). libpng expects error callbacks to
|
|
126
143
|
longjmp; we rb_raise instead, which unwinds the Ruby stack. The
|
|
@@ -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).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 07 - interlace: option on encode_standard
|
|
2
|
+
|
|
3
|
+
- **Priority**: P1
|
|
4
|
+
- **Status**: Done
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
`encode_standard` hardcoded `INTERLACE_NONE`. Some use cases (progressive
|
|
9
|
+
image loading for web) require Adam7 interlacing.
|
|
10
|
+
|
|
11
|
+
## Approach
|
|
12
|
+
|
|
13
|
+
New option on `Libpng::StandardEncoder`:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
interlace: :none | :adam7
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Constants already defined in `lib/libpng.rb` (`INTERLACE_NONE`,
|
|
20
|
+
`INTERLACE_ADAM7`). Added `INTERLACE_BY_NAME` lookup table for
|
|
21
|
+
symbol-to-constant mapping.
|
|
22
|
+
|
|
23
|
+
Validation: rejects unknown symbols with a clear error.
|
|
24
|
+
|
|
25
|
+
Specs in `spec/standard_encoder_options_spec.rb`:
|
|
26
|
+
- Default is `:none` (verify via IHDR walk)
|
|
27
|
+
- `:adam7` produces IHDR with `interlace=INTERLACE_ADAM7`
|
|
28
|
+
- Adam7 output is larger than :none for small images (per-pass
|
|
29
|
+
overhead exceeds compression savings)
|
|
30
|
+
- Round-trips through `Libpng.decode` to the original pixels
|
|
31
|
+
|
|
32
|
+
## Delivered
|
|
33
|
+
|
|
34
|
+
PR #7 (this branch).
|
|
35
|
+
|
|
36
|
+
## Why not also add to encode (simplified API)?
|
|
37
|
+
|
|
38
|
+
libpng's simplified API doesn't expose interlace control -- it always
|
|
39
|
+
emits non-interlaced PNGs. Adding it there would require a separate
|
|
40
|
+
write path, which is what `encode_standard` already is.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# 08 - Palette support
|
|
2
|
+
|
|
3
|
+
- **Priority**: P2
|
|
4
|
+
- **Status**: Done
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Report item #6: `encode_standard` only supported direct-color formats
|
|
9
|
+
(GRAY, GA, RGB, RGBA). Indexed-color images (1-bpp MONOPATTERN brushes
|
|
10
|
+
in EMF, indexed-color sprites, simple icons) compress much better as
|
|
11
|
+
`PNG_COLOR_TYPE_PALETTE` than as RGBA.
|
|
12
|
+
|
|
13
|
+
## Approach
|
|
14
|
+
|
|
15
|
+
New `pixel_format: :palette` mode on `Libpng::StandardEncoder`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
Libpng.encode_standard(width, height, indices,
|
|
19
|
+
pixel_format: :palette,
|
|
20
|
+
palette: [[r, g, b], ...]) # or [r, g, b, a]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The encoder:
|
|
24
|
+
1. Validates the palette (1..256 entries; each `[r,g,b]` or `[r,g,b,a]`
|
|
25
|
+
with 0..255 values).
|
|
26
|
+
2. Always uses `bit_depth=8` for palette format (1/2/4-bit packed
|
|
27
|
+
indices are possible but require row stride juggling that's not
|
|
28
|
+
worth the complexity for v1).
|
|
29
|
+
3. Calls `png_set_PLTE` with the RGB bytes.
|
|
30
|
+
4. If any palette entry has alpha, calls `png_set_tRNS` with the alpha
|
|
31
|
+
values.
|
|
32
|
+
|
|
33
|
+
New FFI bindings attached:
|
|
34
|
+
- `png_set_PLTE` -- `[:pointer, :pointer, :pointer, :int], :void`
|
|
35
|
+
- `png_set_tRNS` -- `[:pointer, :pointer, :pointer, :int, :pointer], :void`
|
|
36
|
+
|
|
37
|
+
The tRNS 5th arg is `png_color_16p trans_color` -- pass NULL (nil).
|
|
38
|
+
|
|
39
|
+
Specs verify:
|
|
40
|
+
- Palette PNG round-trips via RGBA decode (each index expands to its
|
|
41
|
+
palette color)
|
|
42
|
+
- Alpha entries are accepted
|
|
43
|
+
- Missing palette, oversized palette, malformed entries all raise
|
|
44
|
+
|
|
45
|
+
## Delivered
|
|
46
|
+
|
|
47
|
+
PR #7 (this branch).
|
|
48
|
+
|
|
49
|
+
## Future work
|
|
50
|
+
|
|
51
|
+
- 1/2/4-bit packed palette indices for smaller files. Requires row
|
|
52
|
+
stride changes and is niche.
|
|
53
|
+
- Palette extraction on decode (currently `decode` always expands
|
|
54
|
+
palette to RGBA/RGB).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# 09 - 16-bit encode support
|
|
2
|
+
|
|
3
|
+
- **Priority**: P2
|
|
4
|
+
- **Status**: Done
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
`encode_standard` hardcoded `bit_depth=8`. Some scientific imaging and
|
|
9
|
+
photography workflows require 16-bit per channel for higher dynamic
|
|
10
|
+
range.
|
|
11
|
+
|
|
12
|
+
## Approach
|
|
13
|
+
|
|
14
|
+
New option on `Libpng::StandardEncoder`:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
bit_depth: 8 | 16
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The encoder:
|
|
21
|
+
1. Validates `bit_depth` is 8 or 16 (or 8 only for `:ga` / `:palette`).
|
|
22
|
+
2. Passes the bit_depth to `png_set_IHDR`.
|
|
23
|
+
3. Computes `stride = width * channels * (bit_depth == 16 ? 2 : 1)`
|
|
24
|
+
via `Libpng::BytesPerPixel.for_color_type(color_type, bit_depth:)`.
|
|
25
|
+
|
|
26
|
+
Caller is responsible for providing 2 bytes per channel
|
|
27
|
+
(host-order little-endian on x86/arm -- the typical convention).
|
|
28
|
+
|
|
29
|
+
## New helper
|
|
30
|
+
|
|
31
|
+
`Libpng::BytesPerPixel.for_color_type(color_type, bit_depth: 8)` --
|
|
32
|
+
maps a `PNG_COLOR_TYPE_*` plus bit depth to bytes per pixel. Uses the
|
|
33
|
+
`CHANNELS_BY_COLOR_TYPE` table and `CHANNEL_BYTES_8_BIT` /
|
|
34
|
+
`CHANNEL_BYTES_16_BIT` constants.
|
|
35
|
+
|
|
36
|
+
## Specs
|
|
37
|
+
|
|
38
|
+
- bit_depth: 8 (default) -- verify IHDR
|
|
39
|
+
- bit_depth: 16 -- verify IHDR has bit_depth=16
|
|
40
|
+
- bit_depth: 16 with palette format -- reject (palette is always 8)
|
|
41
|
+
- bit_depth: 12 -- reject (only 8 and 16 supported)
|
|
42
|
+
|
|
43
|
+
## Delivered
|
|
44
|
+
|
|
45
|
+
PR #7 (this branch).
|
|
46
|
+
|
|
47
|
+
## Future work
|
|
48
|
+
|
|
49
|
+
- `convert_to_16bit:` on `encode` (simplified API) -- currently only
|
|
50
|
+
`convert_to_8bit:` exists.
|
|
51
|
+
- 16-bit decode: `decode` currently always produces 8-bit output even
|
|
52
|
+
when the source PNG is 16-bit. This is libpng's simplified API
|
|
53
|
+
behavior.
|