libpng 1.6.58.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 424bb2ec057dfcb24902b7d42a4ca73936d54cd30ca0cbdb81df8277a97d3ba1
4
+ data.tar.gz: f424a0836d9ce74aaccacd16ab7aa6cb60aee534576b25a72a4d22d9752b4adc
5
+ SHA512:
6
+ metadata.gz: dd873ee26bb9b3a7e2c52cded5e501de1ed59bc1c1250132c60ef71ffdc99af9e5d6a09aa3399f5ddee3eff7a4ced98772fa3eb2f058076e7410e770bd0225ea
7
+ data.tar.gz: 40b078365a95aff0413904b94092eed3f76f6f0cc42dd637d5b16c1ee1d72fe2cb05c89ac9f0513f140591dc28ccbc84f997dc3f64ed0e03e53487dd04b9da70
@@ -0,0 +1,120 @@
1
+ name: build
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ paths-ignore:
7
+ - '*.adoc'
8
+ pull_request:
9
+ workflow_dispatch:
10
+
11
+ concurrency:
12
+ group: '${{ github.workflow }}-${{ github.job }}-${{ github.head_ref || github.ref_name }}'
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ prepare:
17
+ uses: metanorma/ci/.github/workflows/prepare-rake.yml@main
18
+
19
+ test:
20
+ name: Test on Ruby ${{ matrix.ruby.version }} ${{ matrix.os }}
21
+ runs-on: ${{ matrix.os }}
22
+
23
+ needs: prepare
24
+ if: needs.prepare.outputs.push-for-tag != 'true'
25
+
26
+ continue-on-error: ${{ matrix.ruby.experimental }}
27
+ strategy:
28
+ fail-fast: false
29
+ max-parallel: 5
30
+ matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
31
+
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - uses: ruby/setup-ruby@master
36
+ with:
37
+ bundler-cache: true
38
+ ruby-version: ${{ matrix.ruby.version }}
39
+ rubygems: ${{ matrix.ruby.rubygems }}
40
+
41
+ # libpng's CMake needs cmake >= 3.10 and a working zlib. Both are
42
+ # pre-installed on GitHub Actions runners; this is just a sanity check.
43
+ - name: Install build tools
44
+ if: startsWith(matrix.os, 'ubuntu')
45
+ run: |
46
+ sudo apt-get update
47
+ sudo apt-get install -y cmake ninja-build zlib1g-dev
48
+
49
+ - name: Install build tools (macOS)
50
+ if: startsWith(matrix.os, 'macos')
51
+ run: |
52
+ brew install cmake ninja
53
+
54
+ - name: Install build tools (Windows)
55
+ if: startsWith(matrix.os, 'windows')
56
+ uses: ilammy/msvc-dev-tools@v1
57
+
58
+ - run: bundle exec rake
59
+
60
+ build:
61
+ name: build ${{ matrix.os }}, ${{ matrix.ruby-version }}, ${{ matrix.platform }}
62
+ runs-on: ${{ matrix.os }}
63
+ needs: prepare
64
+ strategy:
65
+ fail-fast: false
66
+ matrix:
67
+ include:
68
+ - os: ubuntu-latest
69
+ platform: any
70
+ ruby-version: ${{ needs.prepare.outputs.default-ruby-version }}
71
+ - os: ubuntu-latest
72
+ platform: x86_64-linux
73
+ ruby-version: ${{ needs.prepare.outputs.default-ruby-version }}
74
+ - os: windows-latest
75
+ platform: x64-mingw32
76
+ ruby-version: ${{ needs.prepare.outputs.default-ruby-version }}
77
+ - os: windows-latest
78
+ platform: x64-mingw-ucrt
79
+ ruby-version: ${{ needs.prepare.outputs.default-ruby-version }}
80
+ - os: macos-latest
81
+ platform: x86_64-darwin
82
+ ruby-version: ${{ needs.prepare.outputs.default-ruby-version }}
83
+
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+
87
+ - name: Setup Ruby
88
+ uses: ruby/setup-ruby@master
89
+ with:
90
+ ruby-version: ${{ matrix.ruby-version }}
91
+ bundler-cache: true
92
+
93
+ - name: Install build tools (Linux)
94
+ if: startsWith(matrix.os, 'ubuntu')
95
+ run: |
96
+ sudo apt-get update
97
+ sudo apt-get install -y cmake ninja-build zlib1g-dev
98
+
99
+ - name: Install build tools (macOS)
100
+ if: startsWith(matrix.os, 'macos')
101
+ run: |
102
+ brew install cmake ninja
103
+
104
+ - name: Install build tools (Windows)
105
+ if: startsWith(matrix.os, 'windows')
106
+ uses: ilammy/msvc-dev-tools@v1
107
+
108
+ - run: bundle exec rake gem:native:${{ matrix.platform }}
109
+
110
+ - uses: actions/upload-artifact@v4
111
+ with:
112
+ name: pkg-${{ matrix.platform }}
113
+ path: pkg/*.gem
114
+
115
+ - name: Install gem
116
+ run: gem install -b pkg/libpng-*.gem
117
+
118
+ - name: Test conversion
119
+ run: |
120
+ ruby -rlibpng -e "png = Libpng.encode(2, 2, \"\\xff\" * 16, pixel_format: 'RGBA'); File.binwrite('test.png', png); puts Libpng.decode(png, pixel_format: 'RGBA').inspect"
@@ -0,0 +1,249 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ workflow_dispatch:
8
+ inputs:
9
+ bump-type:
10
+ description: 'What to bump before tagging'
11
+ type: choice
12
+ options:
13
+ - current
14
+ - iteration
15
+ - libpng
16
+ default: iteration
17
+ libpng-version:
18
+ description: 'New libpng version (required when bump-type=libpng, e.g. 1.6.49)'
19
+ type: string
20
+ required: false
21
+
22
+ # contents: write is needed for workflow_dispatch to push the bump commit + tag.
23
+ # id-token: write is required for RubyGems.org Trusted Publishing (OIDC).
24
+ permissions:
25
+ contents: write
26
+ id-token: write
27
+
28
+ jobs:
29
+ bump:
30
+ if: github.event_name == 'workflow_dispatch'
31
+ runs-on: ubuntu-latest
32
+ outputs:
33
+ sha: ${{ steps.sha.outputs.sha }}
34
+ new-version: ${{ steps.bump.outputs.new-version }}
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+
38
+ - uses: ruby/setup-ruby@master
39
+ with:
40
+ ruby-version: '3.3'
41
+
42
+ - name: Bump version.rb
43
+ id: bump
44
+ shell: bash
45
+ env:
46
+ BUMP_TYPE: ${{ inputs.bump-type }}
47
+ LIBPNG_VERSION_INPUT: ${{ inputs.libpng-version }}
48
+ run: |
49
+ set -euo pipefail
50
+
51
+ if [[ "$BUMP_TYPE" == "current" ]]; then
52
+ NEW_VERSION=$(ruby -Ilib -rlibpng/version -e 'puts Libpng::VERSION')
53
+ echo "new-version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
54
+ echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
55
+ echo "Releasing current VERSION (${NEW_VERSION}) -- no bump."
56
+ exit 0
57
+ fi
58
+
59
+ ruby <<'RUBY'
60
+ bump_type = ENV.fetch("BUMP_TYPE")
61
+ path = "lib/libpng/version.rb"
62
+ content = File.read(path)
63
+
64
+ case bump_type
65
+ when "iteration"
66
+ m = %r{LIBPNG_RUBY_ITERATION = (\d+)}.match(content) or
67
+ raise "Could not find LIBPNG_RUBY_ITERATION in #{path}"
68
+ content.sub!(%r{LIBPNG_RUBY_ITERATION = \d+},
69
+ "LIBPNG_RUBY_ITERATION = #{m[1].to_i + 1}") or
70
+ raise "Failed to substitute iteration"
71
+ when "libpng"
72
+ new_v = ENV.fetch("LIBPNG_VERSION_INPUT", "")
73
+ raise "libpng-version input is required when bump-type=libpng" if new_v.empty?
74
+ raise "Invalid version format: #{new_v} (expected X.Y.Z)" unless new_v.match?(/\A\d+\.\d+\.\d+\z/)
75
+ content.sub!(%r{LIBPNG_VERSION = "[^"]+"},
76
+ "LIBPNG_VERSION = \"#{new_v}\"") or
77
+ raise "Failed to substitute libpng version"
78
+ content.sub!(%r{LIBPNG_RUBY_ITERATION = \d+},
79
+ "LIBPNG_RUBY_ITERATION = 0") or
80
+ raise "Failed to reset iteration"
81
+ else
82
+ raise "Unknown bump-type: #{bump_type}"
83
+ end
84
+
85
+ File.write(path, content)
86
+ load File.expand_path(path)
87
+ File.open(ENV.fetch("GITHUB_OUTPUT"), "a") do |f|
88
+ f.puts "new-version=#{Libpng::VERSION}"
89
+ end
90
+ warn "Bumped to #{Libpng::VERSION}"
91
+ RUBY
92
+
93
+ NEW_VERSION=$(ruby -Ilib -rlibpng/version -e 'puts Libpng::VERSION')
94
+ echo "new-version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
95
+
96
+ git config user.name "github-actions[bot]"
97
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
98
+ git add lib/libpng/version.rb
99
+ git commit -m "Release v${NEW_VERSION}"
100
+ echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
101
+
102
+ - name: Show what's being released
103
+ run: |
104
+ echo "Releasing version: ${{ steps.bump.outputs.new-version }}"
105
+ git --no-pager log --oneline -1 "${{ steps.bump.outputs.sha }}"
106
+
107
+ - name: Push bump commit + tag
108
+ env:
109
+ NEW_VERSION: ${{ steps.bump.outputs.new-version }}
110
+ SHA: ${{ steps.bump.outputs.sha }}
111
+ run: |
112
+ if [[ -n "${SHA}" && "${SHA}" != "${{ github.sha }}" ]]; then
113
+ git push origin "HEAD:${{ github.event.repository.default_branch }}"
114
+ fi
115
+ git tag "v${NEW_VERSION}" "${SHA}"
116
+ git push origin "v${NEW_VERSION}"
117
+
118
+ build:
119
+ needs: bump
120
+ if: always() && !cancelled() && !failure() && (needs.bump.result == 'success' || needs.bump.result == 'skipped')
121
+ runs-on: ${{ matrix.os }}
122
+ strategy:
123
+ fail-fast: false
124
+ matrix:
125
+ include:
126
+ - os: ubuntu-latest
127
+ platform: any
128
+ ruby-version: '3.3'
129
+ - os: ubuntu-latest
130
+ platform: x86_64-linux
131
+ ruby-version: '3.3'
132
+ - os: windows-latest
133
+ platform: x64-mingw32
134
+ ruby-version: '3.3'
135
+ - os: windows-latest
136
+ platform: x64-mingw-ucrt
137
+ ruby-version: '3.3'
138
+ - os: macos-latest
139
+ platform: x86_64-darwin
140
+ ruby-version: '3.3'
141
+ steps:
142
+ - uses: actions/checkout@v4
143
+ with:
144
+ ref: ${{ needs.bump.outputs.sha || github.ref }}
145
+
146
+ - uses: ruby/setup-ruby@master
147
+ with:
148
+ ruby-version: ${{ matrix.ruby-version }}
149
+ bundler-cache: true
150
+
151
+ - name: Install build tools (Linux)
152
+ if: startsWith(matrix.os, 'ubuntu')
153
+ run: |
154
+ sudo apt-get update
155
+ sudo apt-get install -y cmake ninja-build zlib1g-dev
156
+
157
+ - name: Install build tools (macOS)
158
+ if: startsWith(matrix.os, 'macos')
159
+ run: |
160
+ brew install cmake ninja
161
+
162
+ - name: Install build tools (Windows)
163
+ if: startsWith(matrix.os, 'windows')
164
+ uses: ilammy/msvc-dev-tools@v1
165
+
166
+ - run: bundle exec rake gem:native:${{ matrix.platform }}
167
+
168
+ - uses: actions/upload-artifact@v4
169
+ with:
170
+ name: pkg-${{ matrix.platform }}
171
+ path: pkg/*.gem
172
+
173
+ - name: Install gem
174
+ run: gem install -b pkg/libpng-*.gem
175
+
176
+ - name: Test conversion
177
+ run: |
178
+ ruby -rlibpng -e "png = Libpng.encode(2, 2, \"\\xff\" * 16, pixel_format: 'RGBA'); puts Libpng.decode(png, pixel_format: 'RGBA').inspect"
179
+
180
+ cross:
181
+ needs: bump
182
+ if: always() && !cancelled() && !failure() && (needs.bump.result == 'success' || needs.bump.result == 'skipped')
183
+ runs-on: ${{ matrix.os }}
184
+ strategy:
185
+ fail-fast: false
186
+ matrix:
187
+ include:
188
+ - os: ubuntu-latest
189
+ platform: aarch64-linux
190
+ ruby-version: '3.3'
191
+ - os: macos-latest
192
+ platform: arm64-darwin
193
+ ruby-version: '3.3'
194
+ steps:
195
+ - uses: actions/checkout@v4
196
+ with:
197
+ ref: ${{ needs.bump.outputs.sha || github.ref }}
198
+
199
+ - name: Install Ubuntu cross-build packages
200
+ if: startsWith(matrix.os, 'ubuntu')
201
+ run: |
202
+ sudo apt-get update
203
+ sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binutils-aarch64-linux-gnu cmake ninja-build zlib1g-dev:arm64
204
+
205
+ - name: Install macOS build packages
206
+ if: startsWith(matrix.os, 'macos')
207
+ run: brew install cmake ninja
208
+
209
+ - uses: ruby/setup-ruby@master
210
+ with:
211
+ ruby-version: ${{ matrix.ruby-version }}
212
+ bundler-cache: true
213
+
214
+ - run: bundle exec rake gem:native:${{ matrix.platform }}
215
+
216
+ - uses: actions/upload-artifact@v4
217
+ with:
218
+ name: pkg-${{ matrix.platform }}
219
+ path: pkg/*.gem
220
+
221
+ - name: Install gem
222
+ run: gem install -b pkg/libpng-*.gem
223
+
224
+ publish:
225
+ needs: [ bump, build, cross ]
226
+ if: always() && !cancelled() && !failure()
227
+ runs-on: ubuntu-latest
228
+ steps:
229
+ - uses: actions/checkout@v4
230
+ with:
231
+ ref: ${{ needs.bump.outputs.sha || github.ref }}
232
+
233
+ - uses: ruby/setup-ruby@master
234
+ with:
235
+ ruby-version: '3.3'
236
+ bundler-cache: true
237
+
238
+ - uses: actions/download-artifact@v4
239
+ with:
240
+ pattern: pkg-*
241
+ path: pkg
242
+ merge-multiple: true
243
+
244
+ - name: Configure RubyGems credentials (Trusted Publishing / OIDC)
245
+ uses: rubygems/configure-rubygems-credentials@main
246
+
247
+ - name: Publish to rubygems.org
248
+ run: |
249
+ for gem in pkg/*.gem; do gem push -V "$gem"; done
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /ports/
11
+ /lib/libpng/*.so
12
+ /lib/libpng/*.so.*
13
+ /lib/libpng/*.dylib
14
+ /lib/libpng/*.dll
15
+ /.idea/
16
+ /.vscode/
17
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ RSpec
2
+ --format documentation
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,39 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 2.7
4
+ SuggestExtensions: false
5
+
6
+ Gemspec/DevelopmentDependencies:
7
+ Enabled: false
8
+
9
+ # The wrapper is by nature a single file with FFI declarations + several
10
+ # methods that walk libpng's chunked API; splitting would obscure the
11
+ # 1-to-1 mapping with the C API.
12
+ Metrics/AbcSize:
13
+ Max: 60
14
+ Metrics/ClassLength:
15
+ Max: 200
16
+ Metrics/CyclomaticComplexity:
17
+ Max: 20
18
+ Metrics/MethodLength:
19
+ Max: 50
20
+ Metrics/ModuleLength:
21
+ Max: 250
22
+ Metrics/PerceivedComplexity:
23
+ Max: 20
24
+ Metrics/ParameterLists:
25
+ Max: 7
26
+
27
+ Metrics/BlockLength:
28
+ Exclude:
29
+ - spec/**/*.rb
30
+ - Rakefile
31
+ - libpng.gemspec
32
+
33
+ Style/SpecialGlobalVars:
34
+ Exclude:
35
+ - ext/extconf.rb
36
+ Style/FrozenStringLiteralComment:
37
+ Exclude:
38
+ - ext/extconf.rb
39
+ - lib/libpng/recipe.rb
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,108 @@
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
+ 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.
28
+
29
+ == Usage
30
+
31
+ require "libpng"
32
+
33
+ # Encode raw RGBA pixels (row-major, top-down) as a PNG.
34
+ png = Libpng.encode(width, height, rgba_bytes, pixel_format: "RGBA")
35
+ File.binwrite("out.png", png)
36
+
37
+ # Decode a PNG into raw pixels.
38
+ 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
43
+
44
+ === Supported pixel formats
45
+
46
+ - `"GRAY"` / `"GRAYSCALE"` -- 8-bit grayscale (1 byte/pixel)
47
+ - `"GA"` / `"AG"` -- 8-bit grayscale + alpha (2 bytes/pixel)
48
+ - `"RGB"`, `"BGR"` -- 8-bit truecolor (3 bytes/pixel)
49
+ - `"RGBA"`, `"ARGB"`, `"BGRA"`, `"ABGR"` -- 8-bit truecolor + alpha (4 bytes/pixel)
50
+
51
+ === Encoding options
52
+
53
+ Libpng.encode(width, height, pixels,
54
+ pixel_format: "RGBA",
55
+ convert_to_8bit: false)
56
+
57
+ `convert_to_8bit: true` causes libpng to convert 16-bit input to 8-bit
58
+ on write (input is still passed as a packed byte buffer; use this if you
59
+ have packed 16-bit-per-channel data).
60
+
61
+ == Ractor safety
62
+
63
+ `Libpng.encode` and `Libpng.decode` are Ractor-safe. Every call
64
+ allocates and frees its own libpng `png_image`; no shared mutable state
65
+ exists on the Ruby side. Calls from multiple Ractors run concurrently
66
+ without locking.
67
+
68
+ Example:
69
+
70
+ r = Ractor.new do
71
+ Libpng.encode(2, 2, "\xFF" * 16, pixel_format: "RGBA")
72
+ end
73
+ r.take # => PNG binary
74
+
75
+ == Versioning
76
+
77
+ This gem follows a `{LIBPNG_VERSION}.{ITERATION}` scheme:
78
+
79
+ - `LIBPNG_VERSION` is the upstream libpng release the gem is built
80
+ against (currently 1.6.48).
81
+ - `ITERATION` is a counter that bumps on Ruby-side changes (recipe bug
82
+ fixes, CI tweaks, doc updates). It resets to 0 each time
83
+ `LIBPNG_VERSION` bumps.
84
+
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
87
+ the next upstream libpng.
88
+
89
+ == How it works
90
+
91
+ The gem has three layers:
92
+
93
+ 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.
103
+
104
+ == License
105
+
106
+ BSD-2-Clause; see link:LICENSE.txt[LICENSE.txt]. The gem bundles
107
+ pre-compiled libpng binaries; libpng itself is distributed under the
108
+ https://github.com/pnggroup/libpng/blob/main/LICENSE[libpng license].
data/Rakefile ADDED
@@ -0,0 +1,70 @@
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
+ x86_64-linux
38
+ aarch64-linux
39
+ x86_64-darwin
40
+ arm64-darwin
41
+ ]
42
+
43
+ platforms.each do |platform|
44
+ desc "Build pre-compiled gem for the #{platform} platform"
45
+ task "gem:native:#{platform}" do
46
+ sh "rake compile platform:#{platform} gem target_platform=#{platform}"
47
+ end
48
+
49
+ desc "Define the gem task to build on the #{platform} platform (binary gem)"
50
+ task "platform:#{platform}" do
51
+ spec = Gem::Specification.load('libpng.gemspec').dup
52
+ spec.platform = Gem::Platform.new(platform)
53
+ spec.files += Dir.glob('lib/libpng/*.{dll,so,dylib}')
54
+ spec.extensions = []
55
+ spec.dependencies.reject! { |d| d.name == 'mini_portile2' }
56
+
57
+ task = Gem::PackageTask.new(spec)
58
+ task.define
59
+ end
60
+ end
61
+
62
+ require 'rake/clean'
63
+
64
+ CLOBBER.include('pkg')
65
+ CLEAN.include('ports',
66
+ 'tmp',
67
+ 'lib/libpng/*.dll',
68
+ 'lib/libpng/*.dylib',
69
+ 'lib/libpng/*.so',
70
+ '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,6 @@
1
+ $: << File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
2
+
3
+ require 'libpng/recipe'
4
+
5
+ recipe = Libpng::Recipe.new
6
+ recipe.cook_if_not
@@ -0,0 +1,182 @@
1
+ require 'rbconfig'
2
+ require 'mini_portile2'
3
+ require 'pathname'
4
+ require 'tmpdir'
5
+ require 'open3'
6
+ require_relative 'version'
7
+
8
+ module Libpng
9
+ # MiniPortile-based recipe for building libpng from source. Mirrors the
10
+ # pattern used by emf2svg-ruby: during `gem install`, ext/extconf.rb
11
+ # invokes Recipe#cook, which downloads the libpng source tarball, runs
12
+ # configure + make, and installs the shared library into the gem's lib/
13
+ # directory. The pre-compiled gems (built via `rake gem:native:<plat>`)
14
+ # ship the .so/.dylib/.dll and disable extconf.rb entirely.
15
+ class Recipe < MiniPortileCMake
16
+ # Pinned libpng source URL + sha256. Bump deliberately to refresh
17
+ # the upstream — and remember to update both fields together.
18
+ LIBPNG_URL = "https://downloads.sourceforge.net/project/libpng/libpng16/#{Libpng::LIBPNG_VERSION}/libpng-#{Libpng::LIBPNG_VERSION}.tar.gz".freeze
19
+ # sha256 of the libpng-X.Y.Z.tar.gz tarball. Verify with:
20
+ # curl -sL <URL> | shasum -a 256
21
+ LIBPNG_SHA256 = '8c9b05b675ca7301a458df2c2e46f26e1d41ff36b8863f8c33530bc58c2e6225'.freeze
22
+
23
+ ROOT = Pathname.new(File.expand_path('../..', __dir__))
24
+
25
+ def initialize
26
+ super('libpng', Libpng::LIBPNG_VERSION)
27
+
28
+ @files << {
29
+ url: LIBPNG_URL,
30
+ sha256: LIBPNG_SHA256
31
+ }
32
+
33
+ @target = ROOT.join(@target).to_s
34
+ @printed = {}
35
+ end
36
+
37
+ # libpng ships a CMake build alongside the autotools one. We use CMake
38
+ # for consistency with mini_portile2's defaults and to handle Windows
39
+ # cleanly (autotools on native Windows is fragile).
40
+ # No additional configuration is needed; defaults build PNG_SHARED=ON
41
+ # and PNG_STATIC=OFF, which is what we want.
42
+
43
+ def cook_if_not
44
+ cook unless File.exist?(checkpoint)
45
+ end
46
+
47
+ def cook
48
+ super
49
+ FileUtils.touch(checkpoint)
50
+ end
51
+
52
+ def checkpoint
53
+ File.join(@target, "#{name}-#{version}-#{target_platform}.installed")
54
+ end
55
+
56
+ def configure_defaults
57
+ # Build a static+self-contained shared library: we only ship the .so
58
+ # to consumers, so libpng's transitive dep on zlib must be linked in.
59
+ opts = super
60
+ opts << '-DPNG_SHARED=ON'
61
+ opts << '-DPNG_STATIC=OFF'
62
+ opts << '-DPNG_TESTS=OFF'
63
+ opts << '-DPNG_FRAMEWORK=OFF'
64
+ # macOS: avoid the .framework build; we want a plain .dylib.
65
+ opts << '-DCMAKE_INSTALL_LIBDIR=lib'
66
+ opts << '-DCMAKE_BUILD_TYPE=Release'
67
+ opts
68
+ end
69
+
70
+ def install
71
+ super
72
+ # After `make install`, the .so/.dylib/.dll is under ports/<name>/<ver>/lib/.
73
+ # Copy it into the gem's lib/libpng/ so FFI can load it at runtime.
74
+ libs = Dir.glob(File.join(port_path, 'lib', shared_lib_glob))
75
+ raise "no libpng shared lib produced at #{port_path}/lib" if libs.empty?
76
+
77
+ target_dir = ROOT.join('lib', 'libpng')
78
+ FileUtils.mkdir_p(target_dir)
79
+ FileUtils.cp_r(libs, target_dir, verbose: true)
80
+
81
+ verify_libs
82
+ end
83
+
84
+ def verify_libs
85
+ each_built_lib do |path|
86
+ out, st = Open3.capture2("file #{path}")
87
+ raise "Failed to query file #{path}: #{out}" unless st.exitstatus.zero?
88
+
89
+ next if target_format.eql?('skip')
90
+
91
+ raise "Invalid file format '#{out}', /#{target_format.source}/ expected" unless target_format.match?(out)
92
+
93
+ message("Verifying #{path} ... OK\n")
94
+ end
95
+ end
96
+
97
+ def execute(action, command, command_opts = {})
98
+ super(action, command, command_opts.merge(debug: false))
99
+ end
100
+
101
+ def message(text)
102
+ return super unless text.start_with?("\rDownloading")
103
+
104
+ match = text.match(/(\rDownloading .*)\(\s*\d+%\)/)
105
+ pattern = match ? match[1] : text
106
+ return if @printed[pattern]
107
+
108
+ @printed[pattern] = true
109
+ super
110
+ end
111
+
112
+ private
113
+
114
+ def port_path
115
+ File.join(@target, 'ports', "#{name}-#{version}") # MiniPortile default
116
+ end
117
+
118
+ def shared_lib_glob
119
+ if MiniPortile.windows?
120
+ 'libpng16*.dll'
121
+ elsif MiniPortile.darwin?
122
+ 'libpng16*.dylib'
123
+ else
124
+ 'libpng16.so*'
125
+ end
126
+ end
127
+
128
+ def each_built_lib(&block)
129
+ Dir.glob(ROOT.join('lib', 'libpng', shared_lib_glob)).each(&block)
130
+ end
131
+
132
+ def host_platform
133
+ @host_platform ||=
134
+ case @host
135
+ when /\Ax86_64.*mingw32/
136
+ 'x64-mingw32'
137
+ when /\Ax86_64.*linux/
138
+ 'x86_64-linux'
139
+ when /\A(arm64|aarch64).*linux/
140
+ 'arm64-linux'
141
+ when /\Ax86_64.*(darwin|macos|osx)/
142
+ 'x86_64-darwin'
143
+ when /\A(arm64|aarch64).*(darwin|macos|osx)/
144
+ 'arm64-darwin'
145
+ else
146
+ @host
147
+ end
148
+ end
149
+
150
+ def target_platform
151
+ @target_platform ||=
152
+ case ENV.fetch('target_platform', nil)
153
+ when /\A(arm64|aarch64).*(darwin|macos|osx)/
154
+ 'arm64-darwin'
155
+ when /\Ax86_64.*(darwin|macos|osx)/
156
+ 'x86_64-darwin'
157
+ when /\A(arm64|aarch64).*linux/
158
+ 'aarch64-linux'
159
+ else
160
+ ENV.fetch('target_platform', host_platform)
161
+ end
162
+ end
163
+
164
+ def target_format
165
+ @target_format ||=
166
+ case target_platform
167
+ when 'arm64-darwin'
168
+ /Mach-O 64-bit dynamically linked shared library arm64/
169
+ when 'x86_64-darwin'
170
+ /Mach-O 64-bit dynamically linked shared library x86_64/
171
+ when 'aarch64-linux'
172
+ /ELF 64-bit LSB shared object, ARM aarch64/
173
+ when 'x86_64-linux'
174
+ /ELF 64-bit LSB shared object, x86-64/
175
+ when /\Ax64-mingw(32|-ucrt)/
176
+ /PE32\+ executable.*\(DLL\).*x86-64/
177
+ else
178
+ 'skip'
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libpng
4
+ # Gem version follows the pattern:
5
+ #
6
+ # {LIBPNG_VERSION}.{LIBPNG_RUBY_ITERATION}
7
+ #
8
+ # where LIBPNG_VERSION is the upstream libpng release this gem is
9
+ # built against, and LIBPNG_RUBY_ITERATION is a counter for Ruby-side
10
+ # changes (recipe bug fixes, CI changes, docs) that bump without a
11
+ # new libpng release. The iteration resets to 0 each time
12
+ # LIBPNG_VERSION bumps.
13
+ LIBPNG_VERSION = '1.6.58'
14
+ LIBPNG_RUBY_ITERATION = 0
15
+ VERSION = "#{LIBPNG_VERSION}.#{LIBPNG_RUBY_ITERATION}"
16
+ end
data/lib/libpng.rb ADDED
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require 'zlib'
5
+ require_relative 'libpng/version'
6
+
7
+ # Libpng is a Ruby binding for libpng (the official PNG reference library)
8
+ # via FFI. The native library is pre-compiled for each target platform and
9
+ # shipped inside the gem, so no C compiler is required at install time.
10
+ #
11
+ # The API mirrors libpng's "simplified" high-level API:
12
+ #
13
+ # Libpng.encode(width, height, rgba_bytes, pixel_format: "RGBA")
14
+ # Libpng.decode(png_bytes, pixel_format: "RGBA")
15
+ #
16
+ # All encode/decode state is per-call (libpng allocates and frees a
17
+ # png_image internally), so calls from different Ractors do not share
18
+ # state. The module's FFI function table is frozen at load time.
19
+ module Libpng
20
+ class Error < StandardError; end
21
+
22
+ extend FFI::Library
23
+
24
+ ffi_lib_flags :now, :global
25
+
26
+ lib_filename = if FFI::Platform.windows?
27
+ 'libpng16.dll'
28
+ elsif FFI::Platform.mac?
29
+ 'libpng16.dylib'
30
+ else
31
+ 'libpng16.so'
32
+ end
33
+
34
+ ffi_lib File.expand_path("libpng/#{lib_filename}", __dir__)
35
+ .gsub('/', File::ALT_SEPARATOR || File::SEPARATOR)
36
+
37
+ # libpng simplified API (png_image / png_image_*). All return int
38
+ # (1 on success, 0 on failure); the png_image's message buffer holds
39
+ # the error string on failure.
40
+ attach_function :png_image_begin_read_from_memory,
41
+ %i[pointer pointer size_t], :int
42
+ attach_function :png_image_finish_read,
43
+ %i[pointer pointer pointer int pointer], :int
44
+ attach_function :png_image_write_to_memory,
45
+ %i[pointer pointer pointer int pointer int pointer], :int
46
+ attach_function :png_image_free, [:pointer], :void
47
+
48
+ # PNG_IMAGE_FORMAT_* bit flags and named formats from png.h.
49
+ FORMAT_FLAG_ALPHA = 0x01
50
+ FORMAT_FLAG_COLOR = 0x02
51
+ FORMAT_FLAG_LINEAR = 0x04
52
+ FORMAT_FLAG_COLORMAP = 0x08
53
+ FORMAT_FLAG_BGR = 0x10
54
+ FORMAT_FLAG_AFIRST = 0x20
55
+ FORMAT_FLAG_ASSOCIATED_ALPHA = 0x40
56
+
57
+ FORMAT_GRAY = 0
58
+ FORMAT_GA = FORMAT_FLAG_ALPHA
59
+ FORMAT_AG = FORMAT_FLAG_ALPHA | FORMAT_FLAG_AFIRST
60
+ FORMAT_RGB = FORMAT_FLAG_COLOR
61
+ FORMAT_BGR = FORMAT_FLAG_COLOR | FORMAT_FLAG_BGR
62
+ FORMAT_RGBA = FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA
63
+ FORMAT_ARGB = FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA | FORMAT_FLAG_AFIRST
64
+ FORMAT_BGRA = FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA | FORMAT_FLAG_BGR
65
+ FORMAT_ABGR = FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA | FORMAT_FLAG_AFIRST | FORMAT_FLAG_BGR
66
+
67
+ FORMAT_BY_NAME = {
68
+ 'GRAY' => FORMAT_GRAY,
69
+ 'GRAYSCALE' => FORMAT_GRAY,
70
+ 'GA' => FORMAT_GA,
71
+ 'AG' => FORMAT_AG,
72
+ 'RGB' => FORMAT_RGB,
73
+ 'BGR' => FORMAT_BGR,
74
+ 'RGBA' => FORMAT_RGBA,
75
+ 'ARGB' => FORMAT_ARGB,
76
+ 'BGRA' => FORMAT_BGRA,
77
+ 'ABGR' => FORMAT_ABGR
78
+ }.freeze
79
+
80
+ # png_image::version value libpng checks for. Defined in png.h as
81
+ # PNG_IMAGE_VERSION == 1.
82
+ PNG_IMAGE_VERSION = 1
83
+
84
+ # Buffer size for libpng's per-image error message (PNG_IMAGE_MESSAGE_BYTES).
85
+ # The png_image struct stores a fixed-size char buffer of this length.
86
+ PNG_IMAGE_MESSAGE_BYTES = 64
87
+
88
+ # png_image struct field offsets (bytes). png_image is laid out as:
89
+ # void* opaque (pointer-width)
90
+ # png_uint_32 version (uint32)
91
+ # png_uint_32 width (uint32)
92
+ # png_uint_32 height (uint32)
93
+ # png_uint_32 format (uint32)
94
+ # png_uint_32 flags (uint32)
95
+ # png_uint_32 colormap_entries (uint32)
96
+ # png_uint_32 warning_or_error (uint32)
97
+ # char[64] message
98
+ # Total = pointer-size + 7*4 + 64. The fixed width makes it safe to
99
+ # allocate via FFI::MemoryPointer directly so the wrapper stays
100
+ # Ractor-safe (FFI::Struct has class-level state that isn't shareable
101
+ # across non-main Ractors).
102
+ PNG_IMAGE_SIZE = FFI.type_size(:pointer) + (7 * 4) + PNG_IMAGE_MESSAGE_BYTES
103
+ PNG_IMAGE_OFF_OPAQUE = 0
104
+ PNG_IMAGE_OFF_VERSION = FFI.type_size(:pointer)
105
+ PNG_IMAGE_OFF_WIDTH = PNG_IMAGE_OFF_VERSION + 4
106
+ PNG_IMAGE_OFF_HEIGHT = PNG_IMAGE_OFF_WIDTH + 4
107
+ PNG_IMAGE_OFF_FORMAT = PNG_IMAGE_OFF_HEIGHT + 4
108
+ PNG_IMAGE_OFF_FLAGS = PNG_IMAGE_OFF_FORMAT + 4
109
+ PNG_IMAGE_OFF_COLORMAP_ENTRIES = PNG_IMAGE_OFF_FLAGS + 4
110
+ PNG_IMAGE_OFF_WARNING_OR_ERROR = PNG_IMAGE_OFF_COLORMAP_ENTRIES + 4
111
+ PNG_IMAGE_OFF_MESSAGE = PNG_IMAGE_OFF_WARNING_OR_ERROR + 4
112
+
113
+ # Result struct for Libpng.decode (returned as a plain Hash for simplicity).
114
+ DecodedImage = Struct.new(:width, :height, :format, :pixels, keyword_init: true)
115
+
116
+ class << self
117
+ # Encode raw pixel data as a PNG.
118
+ #
119
+ # +width+, +height+ image dimensions in pixels
120
+ # +pixels+ String of raw pixel bytes (row-major, top-down)
121
+ # +pixel_format+ "RGB", "RGBA", "GRAY", "GA", "BGR", "BGRA"
122
+ # (default: "RGBA")
123
+ # +convert_to_8bit+ when true, libpng converts 16-bit input to 8-bit
124
+ # on write (default: false)
125
+ # +strip_colorspace+ when true, removes any sRGB/gAMA/cHRM/iCCP chunks
126
+ # libpng's simplified API emits by default, leaving
127
+ # only IHDR, IDAT, and IEND. This matches the
128
+ # "classic" libpng write path (e.g. libemf2svg) and
129
+ # produces byte-identical output. Default: true.
130
+ #
131
+ # Returns a String containing the PNG file bytes. Raises Libpng::Error
132
+ # on failure (bad dimensions, insufficient input bytes, invalid
133
+ # pixel_format, or libpng-internal error).
134
+ #
135
+ # Ractor-safe: every call allocates and frees its own png_image; no
136
+ # shared mutable state.
137
+ def encode(width, height, pixels, pixel_format: 'RGBA',
138
+ convert_to_8bit: false, strip_colorspace: true)
139
+ raw = encode_ancillary(width, height, pixels,
140
+ pixel_format: pixel_format,
141
+ convert_to_8bit: convert_to_8bit)
142
+ strip_colorspace ? strip_ancillary_chunks(raw) : raw
143
+ end
144
+
145
+ # Decode a PNG file into raw pixels.
146
+ #
147
+ # +png+ String containing PNG file bytes
148
+ # +pixel_format+ desired output format ("RGB", "RGBA", "GRAY", etc.)
149
+ # (default: "RGBA")
150
+ #
151
+ # Returns a DecodedImage Struct with #width, #height, #format (String),
152
+ # and #pixels (binary String).
153
+ #
154
+ # Ractor-safe: same reason as encode.
155
+ def decode(png, pixel_format: 'RGBA')
156
+ fmt = FORMAT_BY_NAME[pixel_format.to_s.upcase] ||
157
+ raise(Error, "unknown pixel_format #{pixel_format.inspect}")
158
+
159
+ img = FFI::MemoryPointer.new(:uint8, PNG_IMAGE_SIZE)
160
+ img.clear
161
+ img.put_uint32(PNG_IMAGE_OFF_VERSION, PNG_IMAGE_VERSION)
162
+
163
+ begin
164
+ FFI::MemoryPointer.new(:uint8, png.bytesize) do |in_buf|
165
+ in_buf.write_bytes(png)
166
+ ok = png_image_begin_read_from_memory(img, in_buf, png.bytesize)
167
+ if ok.zero?
168
+ msg = read_message(img)
169
+ raise Error, "png_image_begin_read_from_memory failed: #{msg}"
170
+ end
171
+
172
+ img.put_uint32(PNG_IMAGE_OFF_FORMAT, fmt)
173
+ width = img.get_uint32(PNG_IMAGE_OFF_WIDTH)
174
+ height = img.get_uint32(PNG_IMAGE_OFF_HEIGHT)
175
+ stride = width * bytes_per_pixel_for_format(fmt)
176
+ out_size = stride * height
177
+
178
+ FFI::MemoryPointer.new(:uint8, out_size) do |out_buf|
179
+ ok = png_image_finish_read(img, nil, out_buf, stride, nil)
180
+ if ok.zero?
181
+ msg = read_message(img)
182
+ raise Error, "png_image_finish_read failed: #{msg}"
183
+ end
184
+ return DecodedImage.new(width: width,
185
+ height: height,
186
+ format: pixel_format.to_s.upcase,
187
+ pixels: out_buf.read_bytes(out_size))
188
+ end
189
+ end
190
+ ensure
191
+ png_image_free(img)
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ # Lower-level encode that produces the raw libpng output (with sRGB
198
+ # or gAMA chunks depending on libpng's defaults). Public #encode strips
199
+ # these by default to match the classic libpng write path.
200
+ def encode_ancillary(width, height, pixels, pixel_format:, convert_to_8bit:)
201
+ raise Error, 'width must be positive' unless width.positive?
202
+ raise Error, 'height must be positive' unless height.positive?
203
+
204
+ fmt = FORMAT_BY_NAME[pixel_format.to_s.upcase] ||
205
+ raise(Error, "unknown pixel_format #{pixel_format.inspect}")
206
+
207
+ bytes_per_pixel = bytes_per_pixel_for_format(fmt)
208
+ stride = width * bytes_per_pixel
209
+ expected = stride * height
210
+ raise Error, "pixels too short: expected #{expected}, got #{pixels.bytesize}" if pixels.bytesize < expected
211
+
212
+ img = FFI::MemoryPointer.new(:uint8, PNG_IMAGE_SIZE)
213
+ img.clear
214
+ img.put_uint32(PNG_IMAGE_OFF_VERSION, PNG_IMAGE_VERSION)
215
+ img.put_uint32(PNG_IMAGE_OFF_WIDTH, width)
216
+ img.put_uint32(PNG_IMAGE_OFF_HEIGHT, height)
217
+ img.put_uint32(PNG_IMAGE_OFF_FORMAT, fmt)
218
+
219
+ out_len_ptr = FFI::MemoryPointer.new(:size_t, 1)
220
+ ok = png_image_write_to_memory(img, nil, out_len_ptr,
221
+ convert_to_8bit ? 1 : 0,
222
+ pixels, stride, nil)
223
+ if ok.zero?
224
+ msg = read_message(img)
225
+ png_image_free(img)
226
+ raise Error, "png_image_write_to_memory (size query) failed: #{msg}"
227
+ end
228
+
229
+ out_len = out_len_ptr.read_uint64
230
+ raise Error, 'libpng reported zero-length PNG output' if out_len.zero?
231
+
232
+ buffer = FFI::MemoryPointer.new(:uint8, out_len)
233
+ ok = png_image_write_to_memory(img, buffer, out_len_ptr,
234
+ convert_to_8bit ? 1 : 0,
235
+ pixels, stride, nil)
236
+ if ok.zero?
237
+ msg = read_message(img)
238
+ png_image_free(img)
239
+ raise Error, "png_image_write_to_memory (write) failed: #{msg}"
240
+ end
241
+
242
+ result = buffer.read_bytes(out_len)
243
+ png_image_free(img)
244
+ result
245
+ end
246
+
247
+ # Walk the PNG chunk list, keeping only IHDR, IDAT, and IEND. The PNG
248
+ # signature (8 bytes) is preserved. We do NOT need to recompute CRCs —
249
+ # the simplified API's emitted chunks (including the ones we drop) are
250
+ # all correctly CRCd; we only need to walk the chunk list and keep the
251
+ # ones we want, byte-for-byte.
252
+ def strip_ancillary_chunks(png_bytes)
253
+ sig = png_bytes.bytes[0, 8]
254
+ raise Error, 'not a PNG file (bad signature)' unless sig == [137, 80, 78, 71, 13, 10, 26, 10]
255
+
256
+ kept = sig.pack('C*')
257
+ offset = 8
258
+ while offset + 8 <= png_bytes.bytesize
259
+ len = png_bytes.bytes[offset, 4].pack('C*').unpack1('N')
260
+ type = png_bytes.bytes[offset + 4, 4].pack('C*')
261
+ chunk_total = 12 + len
262
+ raise Error, "PNG chunk at offset #{offset} runs past EOF" if offset + chunk_total > png_bytes.bytesize
263
+
264
+ crc_input = png_bytes.bytes[offset + 4, 4 + len].pack('C*')
265
+ crc_actual = png_bytes.bytes[offset + 8 + len, 4].pack('C*').unpack1('N')
266
+ crc_expected = Zlib.crc32(crc_input)
267
+ raise Error, "PNG chunk CRC mismatch at offset #{offset} (#{type})" unless crc_actual == crc_expected
268
+
269
+ kept << png_bytes.bytes[offset, chunk_total].pack('C*') if %w[IHDR IDAT IEND].include?(type)
270
+ offset += chunk_total
271
+ break if type == 'IEND'
272
+ end
273
+ kept.force_encoding('ASCII-8BIT')
274
+ end
275
+
276
+ def read_message(img)
277
+ # img[:message] is an Array field (char[64]); FFI returns a
278
+ # CharArray, which doesn't respond to .null? but does support to_ptr
279
+ # and to_s.
280
+
281
+ s = img[:message].to_s
282
+ s.empty? ? '(no message)' : s.force_encoding('UTF-8')
283
+ rescue StandardError
284
+ '(no message)'
285
+ end
286
+
287
+ def bytes_per_pixel_for_format(fmt)
288
+ case fmt
289
+ when FORMAT_GRAY then 1
290
+ when FORMAT_GA, FORMAT_AG then 2
291
+ when FORMAT_RGB, FORMAT_BGR then 3
292
+ when FORMAT_RGBA, FORMAT_ARGB, FORMAT_BGRA, FORMAT_ABGR then 4
293
+ else raise Error, "no bytes-per-pixel mapping for format 0x#{fmt.to_s(16)}"
294
+ end
295
+ end
296
+ end
297
+ end
data/libpng.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/libpng/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'libpng'
7
+ spec.version = Libpng::VERSION
8
+ spec.authors = ['Ribose Inc.']
9
+ spec.email = ['open.source@ribose.com']
10
+
11
+ spec.summary = 'libpng for Ruby (pre-compiled, FFI-based).'
12
+ spec.description = 'Ruby binding for libpng via FFI. The native ' \
13
+ 'libpng16 shared library is pre-compiled for ' \
14
+ 'each target platform and shipped inside the ' \
15
+ 'gem, so no C compiler is required at install time.'
16
+ spec.homepage = 'https://github.com/claricle/libpng-ruby'
17
+ spec.license = 'BSD-2-Clause'
18
+ spec.required_ruby_version = '>= 2.7.0'
19
+
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+ spec.metadata['source_code_uri'] = spec.homepage
22
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/README.adoc#versioning"
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ f.match(%r{\A(?:test|spec|features|tmp|ports)/})
28
+ end
29
+ end
30
+ spec.bindir = 'exe'
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ['lib']
33
+
34
+ spec.add_dependency 'ffi', '~> 1.0'
35
+ spec.add_dependency 'mini_portile2', '~> 2.6'
36
+
37
+ spec.extensions = ['ext/extconf.rb']
38
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: libpng
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.6.58.0
5
+ platform: ruby
6
+ authors:
7
+ - Ribose Inc.
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mini_portile2
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.6'
40
+ description: Ruby binding for libpng via FFI. The native libpng16 shared library is
41
+ pre-compiled for each target platform and shipped inside the gem, so no C compiler
42
+ is required at install time.
43
+ email:
44
+ - open.source@ribose.com
45
+ executables: []
46
+ extensions:
47
+ - ext/extconf.rb
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".github/workflows/build.yml"
51
+ - ".github/workflows/release.yml"
52
+ - ".gitignore"
53
+ - ".rspec"
54
+ - ".rubocop.yml"
55
+ - Gemfile
56
+ - LICENSE.txt
57
+ - README.adoc
58
+ - Rakefile
59
+ - bin/console
60
+ - bin/setup
61
+ - ext/extconf.rb
62
+ - lib/libpng.rb
63
+ - lib/libpng/recipe.rb
64
+ - lib/libpng/version.rb
65
+ - libpng.gemspec
66
+ homepage: https://github.com/claricle/libpng-ruby
67
+ licenses:
68
+ - BSD-2-Clause
69
+ metadata:
70
+ homepage_uri: https://github.com/claricle/libpng-ruby
71
+ source_code_uri: https://github.com/claricle/libpng-ruby
72
+ changelog_uri: https://github.com/claricle/libpng-ruby/blob/main/README.adoc#versioning
73
+ rubygems_mfa_required: 'true'
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 2.7.0
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 4.0.16
89
+ specification_version: 4
90
+ summary: libpng for Ruby (pre-compiled, FFI-based).
91
+ test_files: []