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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +99 -0
  4. data/CLAUDE.md +109 -0
  5. data/README.adoc +32 -15
  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 +99 -385
  39. data/libpng.gemspec +1 -1
  40. metadata +34 -3
@@ -0,0 +1,59 @@
1
+ # 10 - Libpng 1.6.59 bump when released
2
+
3
+ - **Priority**: Future
4
+ - **Status**: Planned (waiting on upstream)
5
+
6
+ ## Context
7
+
8
+ The gem currently bundles libpng 1.6.58. Upstream `libpng16` branch is
9
+ at `1.6.59.git` (unreleased as of 2026-07-26). 1.6.59 will include:
10
+
11
+ - Memory safety fix: prevent double-free of `png_struct` members on
12
+ allocation failure (`cae47455`).
13
+ - Windows/MinGW import-library symlink fix (`78ae423b`) -- doesn't
14
+ affect us since we ship the .dll directly.
15
+ - CMake policy bump to 4.4 (`7ed557b4`).
16
+ - Various CI/license maintenance.
17
+
18
+ ## Trigger
19
+
20
+ Tag `v1.6.59` on https://github.com/pnggroup/libpng.
21
+
22
+ ## Approach
23
+
24
+ The release workflow already has a `bump-type=libpng` mode:
25
+
26
+ ```sh
27
+ gh workflow run release.yml --repo claricle/libpng-ruby \
28
+ --ref main \
29
+ -f bump-type=libpng \
30
+ -f libpng-version=1.6.59
31
+ ```
32
+
33
+ This bumps `LIBPNG_VERSION = "1.6.59"` in `lib/libpng/version.rb`,
34
+ resets `LIBPNG_RUBY_ITERATION` to 0, and triggers a 1.6.59.0 release.
35
+
36
+ Manual steps before triggering:
37
+ 1. Update `LIBPNG_URL` and `LIBPNG_SHA256` in `lib/libpng/recipe.rb`
38
+ once the tarball is on sourceforge. Verify with:
39
+ `curl -sL <URL> | shasum -a 256`
40
+ 2. Smoke-test locally: `bundle exec rake compile` (downloads new
41
+ source, builds, copies the .so/.dylib/.dll into `lib/libpng/`).
42
+ 3. Run the full spec suite to catch any API regressions.
43
+
44
+ ## Risks
45
+
46
+ - API surface: 1.6.59 is unlikely to add new public functions
47
+ (the diff between 1.6.58 and 1.6.59.git shows only bug fixes and
48
+ build improvements in `png.h`).
49
+ - Binary layout: `png_image` struct size and field offsets are
50
+ unchanged.
51
+ - Version string: `LIBPNG_VER_STRING_C` in `lib/libpng.rb` would need
52
+ to update to match the new binary, since `png_create_write_struct`
53
+ checks `user_png_ver` against the compiled-in version.
54
+
55
+ ## Won't do
56
+
57
+ Don't ship a `1.6.59.git`-suffixed version. The `.git` suffix in our
58
+ `{LIBPNG_VERSION}.{ITERATION}` scheme would be confusing for users.
59
+ Wait for the actual 1.6.59 release.
@@ -0,0 +1,59 @@
1
+ # 11 - Read-side metadata getters
2
+
3
+ - **Priority**: P2
4
+ - **Status**: Done
5
+
6
+ ## Context
7
+
8
+ `Libpng::DecodedImage` returned only `width`, `height`, `format`, and
9
+ `pixels`. Callers wanting bit_depth, color_type, or interlace method
10
+ had to walk the PNG themselves.
11
+
12
+ ## Approach
13
+
14
+ Two pieces:
15
+
16
+ 1. **Expanded Struct**: `Libpng::DecodedImage` now has `bit_depth`,
17
+ `color_type`, `interlace` keyword fields (in addition to the
18
+ existing four).
19
+
20
+ 2. **`Libpng::ChunkWalker`**: new public class for walking PNG chunk
21
+ layout. Methods:
22
+ - `#each_chunk` -- yields `[type, data, offset]`
23
+ - `#strip_ancillary` -- returns a new binary String with only
24
+ IHDR/IDAT/IEND (used by SimplifiedEncoder when `strip_colorspace:
25
+ true`)
26
+ - `#ihdr_data` -- raw IHDR bytes
27
+ - `#ihdr_fields` -- parsed Hash: `:width, :height, :bit_depth,
28
+ :color_type, :compression, :filter, :interlace`
29
+
30
+ 3. **`Libpng::SimplifiedDecoder`** uses `ChunkWalker#ihdr_fields` after
31
+ the libpng decode to populate the new Struct fields. Best-effort:
32
+ if the walker raises (e.g. corrupt PNG), the metadata stays nil and
33
+ the libpng-side error propagates.
34
+
35
+ ## Refactor side-effect
36
+
37
+ The chunk-stripping logic in `Libpng::SimplifiedEncoder` was rewritten
38
+ to use `ChunkWalker#strip_ancillary` instead of inline code. DRY win:
39
+ one place that understands PNG chunk layout.
40
+
41
+ ## Specs
42
+
43
+ `spec/decoded_image_metadata_spec.rb` verifies:
44
+ - IHDR metadata is populated for RGBA/RGB/GRAY decode
45
+ - Adam7 interlace is reflected after `encode_standard(interlace:
46
+ :adam7)` -> decode
47
+ - `ChunkWalker` parses IHDR fields correctly
48
+ - `ChunkWalker` raises `FormatError` on non-PNG input
49
+
50
+ ## Delivered
51
+
52
+ PR #7 (this branch).
53
+
54
+ ## Future work
55
+
56
+ - Expose text chunks (tEXt/zTXt/iTXt) -- see [TODO 19](19-text-chunk-read.md)
57
+ - Expose color metadata (gAMA/cHRM/sRGB/iCCP) -- see [TODO 20](20-color-metadata-read.md)
58
+ - Ancillary chunk preservation on decode (currently we expand to
59
+ pixels and drop everything else)
@@ -0,0 +1,61 @@
1
+ # 12 - Full libpng API surface binding
2
+
3
+ - **Priority**: Skip
4
+ - **Status**: Won't do
5
+
6
+ ## Context
7
+
8
+ We expose 13 of libpng's 172 exported functions (~8%). The audit
9
+ (see PR #6 discussion) identified gaps in:
10
+
11
+ - Standard read API (10 functions)
12
+ - Write-side chunk setters (15+: PLTE, tRNS, gAMA, cHRM, sRGB, iCCP,
13
+ bKGD, pHYs, oFFs, tIME, sCAL, sPLT, hIST, text, unknown chunks)
14
+ - Info getters (~20)
15
+ - Read transformations (~20)
16
+ - Advanced compression controls
17
+ - Progressive/streaming decode
18
+ - File I/O variants
19
+ - Filter heuristics
20
+ - Interlacing helpers
21
+ - CRC error handling
22
+ - Memory/struct internals
23
+
24
+ ## Why we won't do this
25
+
26
+ 1. **Scope creep**: The gem's purpose is encode/decode with libemf2svg
27
+ parity, not a complete libpng binding.
28
+ 2. **Maintenance cost**: Each function needs docs, tests, and version
29
+ compatibility checks across libpng releases.
30
+ 3. **Ractor safety risk**: Many libpng functions are stateful
31
+ (`png_set_*` mutates `png_struct`). Exposing them naively could
32
+ introduce Ractor-safety violations.
33
+ 4. **Alternatives exist**: Users needing low-level libpng access can
34
+ `attach_function` against the bundled `libpng16.so` directly
35
+ (it contains all 172 functions).
36
+ 5. **YAGNI**: No user has requested specific functions beyond what
37
+ `encode_standard` provides.
38
+
39
+ ## When this position might change
40
+
41
+ - A specific user need that can't be met by extending the existing
42
+ options-based API.
43
+ - A major libpng release that introduces a high-demand feature
44
+ (unlikely in the 1.6.x series).
45
+ - A formal request from emfsvg or another downstream consumer.
46
+
47
+ ## Recommended path forward (if needed)
48
+
49
+ Instead of exposing `png_set_gAMA` etc. as individual Ruby methods,
50
+ add higher-level options to `encode_standard`:
51
+
52
+ ```ruby
53
+ Libpng.encode_standard(width, height, pixels,
54
+ pixel_format: "RGBA",
55
+ gamma: 0.45455, # emits gAMA
56
+ chromaticities: {...}, # emits cHRM
57
+ srgb: 0, # emits sRGB
58
+ icc_profile: File.read("profile.icc")) # emits iCCP
59
+ ```
60
+
61
+ This keeps the API Ruby-idiomatic and limits the testing surface.
@@ -0,0 +1,77 @@
1
+ # 13 - C extension wrapper for setjmp
2
+
3
+ - **Priority**: Skip
4
+ - **Status**: Won't do
5
+
6
+ ## Context
7
+
8
+ `encode_standard` uses `png_set_error_fn` to register an FFI callback
9
+ that calls `rb_raise` instead of libpng's default `abort()`. libpng
10
+ expects error callbacks to `longjmp` back to a `setjmp` point set up
11
+ by the caller. Calling `rb_raise` from the FFI callback unwinds the
12
+ Ruby stack via Ruby's own `longjmp`-based exception machinery.
13
+
14
+ This works for input-validation errors (most cases) but has a subtle
15
+ caveat: libpng's internal state between the callback invocation and
16
+ the `setjmp` target is not cleaned up. Our `ensure` block calls
17
+ `png_destroy_write_struct` to release the C state, which is sufficient
18
+ in practice.
19
+
20
+ ## Why we won't do this
21
+
22
+ A C extension could properly wrap the standard API with `setjmp`:
23
+ ```c
24
+ if (setjmp(png_jmpbuf(png_ptr))) {
25
+ /* longjmp target -- copy error message, return error code */
26
+ }
27
+ ```
28
+
29
+ This would give clean error handling without Ruby stack unwinding
30
+ concerns.
31
+
32
+ But:
33
+
34
+ 1. **Adds a build dependency**: A C extension means the gem requires a
35
+ C compiler at install time on every platform. We currently ship
36
+ pre-compiled binary gems for 9 platforms specifically to avoid
37
+ this. Adding a C extension would require shipping pre-compiled
38
+ extensions for each platform too -- a significant build/release
39
+ burden.
40
+
41
+ 2. **Current approach works**: The rb_raise-from-callback pattern has
42
+ not caused any user-visible issues in testing or production use.
43
+
44
+ 3. **Documented in README**: The caveat is called out in the
45
+ `encode_standard` documentation. Users who need bulletproof error
46
+ handling for adversarial input can validate before calling.
47
+
48
+ 4. **Simplified API already handles it cleanly**: The `png_image_*`
49
+ API uses an internal `setjmp` and captures errors into the message
50
+ buffer. For decode paths (where adversarial input is most likely),
51
+ `Libpng.decode` is the right choice.
52
+
53
+ ## When this position might change
54
+
55
+ - A real-world crash report attributed to the rb_raise-from-callback
56
+ pattern.
57
+ - A user needing to call `encode_standard` in a tight loop with
58
+ potentially-corrupt pixel buffers (where the half-freed png_struct
59
+ could accumulate state).
60
+
61
+ ## Alternative under consideration
62
+
63
+ If we ever do add a C extension, it would be a tiny `.c` file (maybe
64
+ 50 lines) exposing a single function:
65
+
66
+ ```c
67
+ int libpng_encode_to_memory_safe(
68
+ uint32_t width, uint32_t height, int bit_depth, int color_type,
69
+ int interlace, int filter, int compression_level,
70
+ const void *pixels, int stride,
71
+ const void *palette, int palette_entries,
72
+ void **out, size_t *out_len,
73
+ char *err_msg, size_t err_msg_len);
74
+ ```
75
+
76
+ This would be the only C function in the gem. FFI would bind it
77
+ directly. All libpng state would be confined inside the C function.
@@ -0,0 +1,47 @@
1
+ # 14 - Progressive/streaming decode
2
+
3
+ - **Priority**: Skip
4
+ - **Status**: Won't do
5
+
6
+ ## Context
7
+
8
+ libpng supports progressive (streaming) decode via
9
+ `png_set_progressive_read_fn` + `png_process_data`. This lets callers
10
+ feed bytes incrementally as they arrive (e.g. over a network socket)
11
+ rather than buffering the entire PNG first.
12
+
13
+ ## Why we won't do this
14
+
15
+ 1. **No use case yet**: The gem's consumers (emfsvg, image processing
16
+ tools) work with complete PNGs in memory.
17
+
18
+ 2. **API complexity**: Progressive decode requires a stateful reader
19
+ object (`Libpng::ProgressiveReader`?) with `#feed(bytes)` and
20
+ `#finish` methods, plus callback hooks for row completion. This is
21
+ a significantly different API surface from the current
22
+ `Libpng.decode(png_bytes)`.
23
+
24
+ 3. **Ruby I/O is usually buffered anyway**: Ruby's `IO.read`,
25
+ `Net::HTTP.get`, etc. all return complete strings. Streaming is the
26
+ exception, not the rule.
27
+
28
+ 4. **Memory savings are minimal**: A typical PNG is 100KB-5MB.
29
+ Buffering the whole thing in a String is fine for almost all use
30
+ cases.
31
+
32
+ ## When this position might change
33
+
34
+ - A user building a streaming image service (e.g. progressive JPEG-like
35
+ loading for web).
36
+ - A request from a framework like Rack that wants to decode PNG
37
+ uploads as they arrive.
38
+
39
+ ## Alternative if needed
40
+
41
+ Use a tempfile + standard `Libpng.decode`:
42
+ ```ruby
43
+ File.open(upload_path, 'wb') { |f| f.write(chunk) until done }
44
+ decoded = Libpng.decode(File.binread(upload_path))
45
+ ```
46
+
47
+ Not streaming, but avoids holding the entire upload in Ruby memory.
@@ -0,0 +1,57 @@
1
+ # 15 - File I/O variants
2
+
3
+ - **Priority**: Skip
4
+ - **Status**: Won't do
5
+
6
+ ## Context
7
+
8
+ libpng exposes file-based I/O via `png_init_io(FILE*)`, plus the
9
+ simplified API has `png_image_begin_read_from_file` /
10
+ `png_image_write_to_file`. We currently only use the memory-based
11
+ variants (`_from_memory`, `_to_memory`, `png_set_write_fn` with
12
+ in-memory callback).
13
+
14
+ ## Why we won't do this
15
+
16
+ 1. **`FILE*` lifetime is awkward across FFI**: A Ruby `File` object
17
+ wraps a `FILE*`. Passing it to libpng via FFI requires either:
18
+ - Reaching into Ruby's internal struct layout (fragile, version-
19
+ dependent)
20
+ - Opening a separate `FILE*` via `fopen(3)` and managing its
21
+ lifetime manually (error-prone)
22
+
23
+ 2. **Memory-stream output is the right default**: It avoids disk I/O
24
+ entirely, lets callers decide where to persist (or not), and works
25
+ identically across all 10 platforms.
26
+
27
+ 3. **Ruby already has good file I/O**:
28
+ ```ruby
29
+ png = Libpng.encode(width, height, pixels, pixel_format: "RGBA")
30
+ File.binwrite("out.png", png)
31
+ ```
32
+ is idiomatic Ruby. Adding `Libpng.encode_to_file` would be a
33
+ duplicate API for marginal benefit.
34
+
35
+ 4. **Simplified API file variants are POSIX-only**:
36
+ `png_image_begin_read_from_file` takes a `const char *filename`,
37
+ which doesn't handle Windows wide-char paths cleanly. The memory
38
+ variants sidestep this entirely.
39
+
40
+ ## When this position might change
41
+
42
+ - A user with truly huge images (multi-GB) where holding the encoded
43
+ PNG in memory before writing to disk is wasteful.
44
+ - A performance benchmark showing `File.binwrite` is a real bottleneck
45
+ (unlikely -- disk I/O is fast and `File.binwrite` uses `write(2)`
46
+ directly).
47
+
48
+ ## Alternative under consideration
49
+
50
+ If streaming-to-file becomes important, the right design is:
51
+ ```ruby
52
+ Libpng.encode_standard(width, height, pixels, output: io_object)
53
+ ```
54
+ where `io_object` is anything responding to `#<<` (String, File,
55
+ StringIO, etc.). The internal FFI write callback would call
56
+ `io_object << data.read_bytes(len)`. This is more flexible than a
57
+ file-only variant and requires no new libpng functions.
@@ -0,0 +1,97 @@
1
+ # 16 - Architecture refactor: autoload + OOP
2
+
3
+ - **Priority**: P0
4
+ - **Status**: Done
5
+
6
+ ## Context
7
+
8
+ `lib/libpng.rb` was a 466-line monolith mixing:
9
+ - Module definition
10
+ - FFI library setup
11
+ - 13 `attach_function` calls
12
+ - ~40 constants (FORMAT_*, COLOR_TYPE_*, FILTER_*, INTERLACE_*, etc.)
13
+ - `png_image` struct layout constants
14
+ - `encode`, `decode`, `encode_standard` methods (each 50-100 lines)
15
+ - 5 private helper methods
16
+
17
+ Plus the file used `require_relative 'libpng/version'`, violating the
18
+ project-wide rule against `require_relative` for internal library code.
19
+
20
+ ## Approach
21
+
22
+ Split into MECE per-concern files under `lib/libpng/`, loaded via
23
+ `autoload` from `lib/libpng.rb`:
24
+
25
+ | File | Responsibility |
26
+ |---|---|
27
+ | `error.rb` | `Libpng::Error` |
28
+ | `decoded_image.rb` | `Libpng::DecodedImage` Struct |
29
+ | `chunk_walker.rb` | `Libpng::ChunkWalker` (PNG chunk walking utility class) |
30
+ | `bytes_per_pixel.rb` | `Libpng::BytesPerPixel` (pure-data lookup) |
31
+ | `simplified_encoder.rb` | `Libpng::SimplifiedEncoder` (encode via simplified API) |
32
+ | `simplified_decoder.rb` | `Libpng::SimplifiedDecoder` (decode + metadata extraction) |
33
+ | `standard_encoder.rb` | `Libpng::StandardEncoder` (encode_standard with all options) |
34
+
35
+ Each encoder/decoder is a class instantiated per call:
36
+ ```ruby
37
+ class Libpng::SimplifiedEncoder
38
+ def initialize(width, height, pixels, **opts)
39
+ # validate + store
40
+ end
41
+
42
+ def call
43
+ # do the work, return result
44
+ end
45
+ end
46
+ ```
47
+
48
+ The public API stays as module methods for backward compat:
49
+ ```ruby
50
+ def self.encode(width, height, pixels, **opts)
51
+ SimplifiedEncoder.new(width, height, pixels, **opts).call
52
+ end
53
+ ```
54
+
55
+ ## Why per-call instances
56
+
57
+ - **OCP**: New encode options (e.g. `gamma:`) extend the encoder class
58
+ without touching `Libpng.encode`.
59
+ - **Testability**: Each encoder is independently testable.
60
+ - **MECE**: Validation lives in the encoder, FFI calls live in the
61
+ encoder, dispatch lives in the module. Three concerns, three places.
62
+ - **Ractor-safe by construction**: Instances are local to one call,
63
+ never shared.
64
+
65
+ The per-call allocation cost (~1 object) is negligible compared to the
66
+ libpng C call.
67
+
68
+ ## Why autoload instead of eager
69
+
70
+ - Faster `require 'libpng'` for users who only need one API
71
+ - Project rule: never `require_relative` for internal library code
72
+ - Lazy loading of rarely-used classes (e.g. `Recipe` is only needed
73
+ during source-gem install)
74
+
75
+ ## What changed in ext/extconf.rb
76
+
77
+ Before:
78
+ ```ruby
79
+ $LOAD_PATH << File.expand_path('../lib', __dir__)
80
+ require 'libpng/recipe' # violation: require with path
81
+ ```
82
+
83
+ After:
84
+ ```ruby
85
+ $LOAD_PATH << File.expand_path('../lib', __dir__)
86
+ require 'libpng' # triggers autoload setup
87
+ recipe = Libpng::Recipe.new # accesses Recipe constant, triggers autoload
88
+ ```
89
+
90
+ ## Specs
91
+
92
+ All existing specs still pass without modification (the public API is
93
+ unchanged). 81 examples, 0 failures.
94
+
95
+ ## Delivered
96
+
97
+ PR #7 (this branch).
@@ -0,0 +1,46 @@
1
+ # 17 - Ruby version support policy
2
+
3
+ - **Priority**: Future
4
+ - **Status**: Planned
5
+
6
+ ## Context
7
+
8
+ The gemspec declares `required_ruby_version = '>= 2.7.0'`. Ruby 2.7
9
+ reached EOL in March 2023. Ruby 3.0 reached EOL in March 2024. Ruby
10
+ 3.1 is in security maintenance. Ruby 3.2, 3.3, 3.4 are current.
11
+
12
+ Ractors require Ruby 3.0+. Our Ractor specs guard with
13
+ `return unless defined?(Ractor)` so they no-op on 2.7, but the
14
+ `return` at file top-level might warn on some Ruby versions.
15
+
16
+ ## Proposal
17
+
18
+ Bump `required_ruby_version` to `>= 3.1.0` in a future minor release.
19
+
20
+ Rationale:
21
+ - Drops EOL versions (2.7, 3.0) -- no security backports
22
+ - Keeps 3.1 (security maintenance) and all current versions (3.2, 3.3, 3.4)
23
+ - Aligns with `mini_portile2 ~> 2.8` which also requires 3.1+
24
+ - Removes the `return unless defined?(Ractor)` guard from spec files
25
+ (the gem can assume Ractor exists)
26
+
27
+ ## Risks
28
+
29
+ - Some users may still be on 2.7 (legacy deployments). They'd be
30
+ stuck on the last 2.7-compatible release.
31
+ - RubyInstaller ARM64 builds start at 3.4.1, so Windows ARM64 users
32
+ are unaffected.
33
+
34
+ ## Approach
35
+
36
+ 1. Wait one release cycle after announcing the deprecation.
37
+ 2. Bump `required_ruby_version` in `libpng.gemspec`.
38
+ 3. Update the README Ruby version badge.
39
+ 4. Remove the Ractor guard in `spec/ractor_spec.rb` and
40
+ `spec/ractor_standard_spec.rb`.
41
+
42
+ ## Trigger
43
+
44
+ Either:
45
+ - ruby-setup-ruby drops 2.7 and 3.0 from its version matrix, OR
46
+ - A security feature in libpng requires Ruby 3.1+ APIs.
@@ -0,0 +1,52 @@
1
+ # 18 - Dependabot for automated dependency bumps
2
+
3
+ - **Priority**: Future
4
+ - **Status**: Planned
5
+
6
+ ## Context
7
+
8
+ The gem has runtime dependencies on `ffi` and `mini_portile2`, and dev
9
+ dependencies on `rake`, `rspec`, and `rubocop`. These are bumped
10
+ manually when someone notices a new version. Dependabot would automate
11
+ this and open PRs when new versions are released.
12
+
13
+ ## Proposal
14
+
15
+ Add `.github/dependabot.yml`:
16
+
17
+ ```yaml
18
+ version: 2
19
+ updates:
20
+ - package-ecosystem: "bundler"
21
+ directory: "/"
22
+ schedule:
23
+ interval: "weekly"
24
+ open-pull-requests-limit: 5
25
+ groups:
26
+ dev-deps:
27
+ dependency-type: "development"
28
+ runtime-deps:
29
+ dependency-type: "production"
30
+
31
+ - package-ecosystem: "github-actions"
32
+ directory: "/"
33
+ schedule:
34
+ interval: "weekly"
35
+ ```
36
+
37
+ Two groups:
38
+ - **dev-deps**: rake, rspec, rubocop -- batch into one PR per week
39
+ - **runtime-deps**: ffi, mini_portile2 -- separate PRs (more careful
40
+ review since these affect end users)
41
+
42
+ Plus GitHub Actions version bumps (checkout, upload-artifact, etc.).
43
+
44
+ ## Risks
45
+
46
+ - Slightly noisier PR queue.
47
+ - Some bumps may break CI (e.g. rubocop new cops). Auto-merge is not
48
+ appropriate; each PR needs human review.
49
+
50
+ ## Trigger
51
+
52
+ When the project stabilizes after the current refactor wave.
@@ -0,0 +1,104 @@
1
+ # 19 - Text chunk (tEXt/zTXt/iTXt) read support
2
+
3
+ - **Priority**: P2
4
+ - **Status**: Planned
5
+
6
+ ## Context
7
+
8
+ PNG images often carry metadata in text chunks:
9
+ - `tEXt`: Latin-1 key/value pairs
10
+ - `zTXt`: zlib-compressed Latin-1
11
+ - `iTXt`: UTF-8 with optional compression, optional language tag
12
+
13
+ Common keys: `Title`, `Author`, `Description`, `Copyright`,
14
+ `Software`, `Creation Time`.
15
+
16
+ Our `ChunkWalker` already iterates all chunks. We just need to parse
17
+ the text chunk format and expose it on `DecodedImage`.
18
+
19
+ ## Proposal
20
+
21
+ Extend `Libpng::ChunkWalker` with text-chunk parsing, and extend
22
+ `DecodedImage` to carry a `text` field.
23
+
24
+ ```ruby
25
+ decoded = Libpng.decode(File.binread("photo.png"), pixel_format: "RGB")
26
+ decoded.text # => { "Title" => "Sunset", "Author" => "Jane", ... }
27
+ decoded.text["Title"] # => "Sunset"
28
+ ```
29
+
30
+ Text values are UTF-8 strings (converted from Latin-1 for tEXt/zTXt;
31
+ already UTF-8 for iTXt). Keys are ASCII.
32
+
33
+ ## Implementation
34
+
35
+ Add to `chunk_walker.rb`:
36
+
37
+ ```ruby
38
+ def text_chunks
39
+ result = {}
40
+ each_chunk do |type, data, _|
41
+ case type
42
+ when 'tEXt' then parse_text(data, result)
43
+ when 'zTXt' then parse_ztxt(data, result)
44
+ when 'iTXt' then parse_itxt(data, result)
45
+ end
46
+ end
47
+ result
48
+ end
49
+
50
+ private
51
+
52
+ def parse_text(data, result)
53
+ null_idx = data.index("\x00")
54
+ return if null_idx.nil?
55
+ key = data[0, null_idx]
56
+ val = data[(null_idx + 1)..]
57
+ result[key] = val.force_encoding('ISO-8859-1').encode('UTF-8')
58
+ end
59
+
60
+ def parse_ztxt(data, result)
61
+ null_idx = data.index("\x00")
62
+ return if null_idx.nil?
63
+ key = data[0, null_idx]
64
+ # data[null_idx+1] is compression method (0 = zlib)
65
+ compressed = data[(null_idx + 2)..]
66
+ val = Zlib.inflate(compressed)
67
+ result[key] = val.force_encoding('ISO-8859-1').encode('UTF-8')
68
+ end
69
+
70
+ def parse_itxt(data, result)
71
+ # iTXt is more complex: keyword\0 compression_flag compression_method
72
+ # language_tag\0 translated_keyword\0 text
73
+ # ... parse accordingly
74
+ end
75
+ ```
76
+
77
+ Then in `simplified_decoder.rb`:
78
+
79
+ ```ruby
80
+ metadata = extract_metadata
81
+ text = ChunkWalker.new(@png).text_chunks rescue {}
82
+ return DecodedImage.new(..., text: text)
83
+ ```
84
+
85
+ And in `decoded_image.rb`:
86
+
87
+ ```ruby
88
+ DecodedImage = Struct.new(:width, :height, :format, :pixels,
89
+ :bit_depth, :color_type, :interlace, :text,
90
+ keyword_init: true)
91
+ ```
92
+
93
+ ## Specs
94
+
95
+ - Round-trip: encode a PNG with tEXt chunks (via libpng's standard API,
96
+ once we expose `png_set_text`), decode, verify text matches.
97
+ - zTXt decompression works.
98
+ - iTXt UTF-8 round-trip.
99
+ - Malformed text chunk doesn't break decode (best-effort).
100
+
101
+ ## Out of scope
102
+
103
+ - Writing text chunks (`png_set_text`) -- separate TODO if needed.
104
+ - International text chunk ordering / precedence rules.