libpng 1.6.58.1-aarch64-linux
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 +7 -0
- data/.github/workflows/build.yml +190 -0
- data/.github/workflows/release.yml +281 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.rubocop.yml +39 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +27 -0
- data/README.adoc +108 -0
- data/Rakefile +73 -0
- data/bin/console +5 -0
- data/bin/setup +5 -0
- data/ext/extconf.rb +12 -0
- data/lib/libpng/libpng16.so +0 -0
- data/lib/libpng/recipe.rb +239 -0
- data/lib/libpng/version.rb +16 -0
- data/lib/libpng.rb +297 -0
- data/libpng.gemspec +38 -0
- metadata +80 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
require 'rubocop/rake_task'
|
|
9
|
+
|
|
10
|
+
RuboCop::RakeTask.new
|
|
11
|
+
|
|
12
|
+
task default: %i[spec rubocop]
|
|
13
|
+
|
|
14
|
+
task :compile do
|
|
15
|
+
require_relative 'ext/extconf'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
task spec: :compile
|
|
19
|
+
|
|
20
|
+
desc 'Build install-compilation gem'
|
|
21
|
+
task 'gem:native:any' do
|
|
22
|
+
sh 'rake platform:any gem'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
require 'rubygems/package_task'
|
|
26
|
+
|
|
27
|
+
desc 'Define the gem task to build on any platform (compile on install)'
|
|
28
|
+
task 'platform:any' do
|
|
29
|
+
spec = Gem::Specification.load('libpng.gemspec').dup
|
|
30
|
+
task = Gem::PackageTask.new(spec)
|
|
31
|
+
task.define
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
platforms = %w[
|
|
35
|
+
x64-mingw32
|
|
36
|
+
x64-mingw-ucrt
|
|
37
|
+
aarch64-mingw-ucrt
|
|
38
|
+
x86_64-linux
|
|
39
|
+
x86_64-linux-musl
|
|
40
|
+
aarch64-linux
|
|
41
|
+
aarch64-linux-musl
|
|
42
|
+
x86_64-darwin
|
|
43
|
+
arm64-darwin
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
platforms.each do |platform|
|
|
47
|
+
desc "Build pre-compiled gem for the #{platform} platform"
|
|
48
|
+
task "gem:native:#{platform}" do
|
|
49
|
+
sh "rake compile platform:#{platform} gem target_platform=#{platform}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
desc "Define the gem task to build on the #{platform} platform (binary gem)"
|
|
53
|
+
task "platform:#{platform}" do
|
|
54
|
+
spec = Gem::Specification.load('libpng.gemspec').dup
|
|
55
|
+
spec.platform = Gem::Platform.new(platform)
|
|
56
|
+
spec.files += Dir.glob('lib/libpng/*.{dll,so,dylib}')
|
|
57
|
+
spec.extensions = []
|
|
58
|
+
spec.dependencies.reject! { |d| d.name == 'mini_portile2' }
|
|
59
|
+
|
|
60
|
+
task = Gem::PackageTask.new(spec)
|
|
61
|
+
task.define
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
require 'rake/clean'
|
|
66
|
+
|
|
67
|
+
CLOBBER.include('pkg')
|
|
68
|
+
CLEAN.include('ports',
|
|
69
|
+
'tmp',
|
|
70
|
+
'lib/libpng/*.dll',
|
|
71
|
+
'lib/libpng/*.dylib',
|
|
72
|
+
'lib/libpng/*.so',
|
|
73
|
+
'lib/libpng/*.so.*')
|
data/bin/console
ADDED
data/bin/setup
ADDED
data/ext/extconf.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
|
|
2
|
+
|
|
3
|
+
require 'libpng/recipe'
|
|
4
|
+
require 'mkmf'
|
|
5
|
+
|
|
6
|
+
recipe = Libpng::Recipe.new
|
|
7
|
+
recipe.cook_if_not
|
|
8
|
+
|
|
9
|
+
# RubyGems requires every extconf.rb to leave a Makefile behind, even if
|
|
10
|
+
# no native extension is compiled (libpng is built by Recipe#cook above
|
|
11
|
+
# and loaded via FFI at runtime). dummy_makefile satisfies that contract.
|
|
12
|
+
create_makefile('libpng/dummy')
|
|
Binary file
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
setup_cross_compile if cross_compile?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# libpng ships a CMake build alongside the autotools one. We use CMake
|
|
39
|
+
# for consistency with mini_portile2's defaults and to handle Windows
|
|
40
|
+
# cleanly (autotools on native Windows is fragile).
|
|
41
|
+
# No additional configuration is needed; defaults build PNG_SHARED=ON
|
|
42
|
+
# and PNG_STATIC=OFF, which is what we want.
|
|
43
|
+
|
|
44
|
+
def cook_if_not
|
|
45
|
+
cook unless File.exist?(checkpoint)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cook
|
|
49
|
+
super
|
|
50
|
+
FileUtils.touch(checkpoint)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def checkpoint
|
|
54
|
+
File.join(@target, "#{name}-#{version}-#{target_platform}.installed")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def configure_defaults
|
|
58
|
+
# Build a static+self-contained shared library: we only ship the .so
|
|
59
|
+
# to consumers, so libpng's transitive dep on zlib must be linked in.
|
|
60
|
+
opts = super
|
|
61
|
+
opts << '-DPNG_SHARED=ON'
|
|
62
|
+
opts << '-DPNG_STATIC=OFF'
|
|
63
|
+
opts << '-DPNG_TESTS=OFF'
|
|
64
|
+
opts << '-DPNG_FRAMEWORK=OFF'
|
|
65
|
+
# macOS: avoid the .framework build; we want a plain .dylib.
|
|
66
|
+
opts << '-DCMAKE_INSTALL_LIBDIR=lib'
|
|
67
|
+
opts << '-DCMAKE_BUILD_TYPE=Release'
|
|
68
|
+
opts
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def install
|
|
72
|
+
super
|
|
73
|
+
# After `make install`, the shared lib lives under ports/<name>/<ver>/.
|
|
74
|
+
# On Linux/macOS that's lib/. On Windows, CMake's GNUInstallDirs puts
|
|
75
|
+
# the .dll in bin/ and the import library (.dll.a) in lib/ — we only
|
|
76
|
+
# ship the .dll, so search both.
|
|
77
|
+
libs = Dir.glob(File.join(port_path, shared_lib_install_glob))
|
|
78
|
+
raise "no libpng shared lib produced under #{port_path}" if libs.empty?
|
|
79
|
+
|
|
80
|
+
target_dir = ROOT.join('lib', 'libpng')
|
|
81
|
+
FileUtils.mkdir_p(target_dir)
|
|
82
|
+
FileUtils.cp_r(libs, target_dir, verbose: true)
|
|
83
|
+
|
|
84
|
+
verify_libs
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def verify_libs
|
|
88
|
+
each_built_lib do |path|
|
|
89
|
+
out, st = Open3.capture2("file #{path}")
|
|
90
|
+
raise "Failed to query file #{path}: #{out}" unless st.exitstatus.zero?
|
|
91
|
+
|
|
92
|
+
next if target_format.eql?('skip')
|
|
93
|
+
|
|
94
|
+
raise "Invalid file format '#{out}', /#{target_format.source}/ expected" unless target_format.match?(out)
|
|
95
|
+
|
|
96
|
+
message("Verifying #{path} ... OK\n")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def execute(action, command, command_opts = {})
|
|
101
|
+
super(action, command, command_opts.merge(debug: false))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def message(text)
|
|
105
|
+
return super unless text.start_with?("\rDownloading")
|
|
106
|
+
|
|
107
|
+
match = text.match(/(\rDownloading .*)\(\s*\d+%\)/)
|
|
108
|
+
pattern = match ? match[1] : text
|
|
109
|
+
return if @printed[pattern]
|
|
110
|
+
|
|
111
|
+
@printed[pattern] = true
|
|
112
|
+
super
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def port_path
|
|
118
|
+
File.join(@target, 'ports', "#{name}-#{version}") # MiniPortile default
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def shared_lib_glob
|
|
122
|
+
if MiniPortile.windows?
|
|
123
|
+
'libpng16*.dll'
|
|
124
|
+
elsif MiniPortile.darwin?
|
|
125
|
+
'libpng16*.dylib'
|
|
126
|
+
else
|
|
127
|
+
'libpng16.so*'
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Glob (with port_path prefix) for the freshly installed shared lib.
|
|
132
|
+
# On Windows the .dll installs to bin/; on Unix-likes it stays in lib/.
|
|
133
|
+
def shared_lib_install_glob
|
|
134
|
+
if MiniPortile.windows?
|
|
135
|
+
'{bin,lib}/libpng16*.dll'
|
|
136
|
+
else
|
|
137
|
+
"lib/#{shared_lib_glob}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def each_built_lib(&block)
|
|
142
|
+
Dir.glob(ROOT.join('lib', 'libpng', shared_lib_glob)).each(&block)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def host_platform
|
|
146
|
+
@host_platform ||=
|
|
147
|
+
case @host
|
|
148
|
+
when /\Ax86_64.*mingw32/
|
|
149
|
+
'x64-mingw32'
|
|
150
|
+
when /\A(aarch64|arm64).*mingw/
|
|
151
|
+
'aarch64-mingw-ucrt'
|
|
152
|
+
when /\Ax86_64.*linux-musl/
|
|
153
|
+
'x86_64-linux-musl'
|
|
154
|
+
when /\A(aarch64|arm64).*linux-musl/
|
|
155
|
+
'aarch64-linux-musl'
|
|
156
|
+
when /\Ax86_64.*linux/
|
|
157
|
+
'x86_64-linux'
|
|
158
|
+
when /\A(aarch64|arm64).*linux/
|
|
159
|
+
'aarch64-linux'
|
|
160
|
+
when /\Ax86_64.*(darwin|macos|osx)/
|
|
161
|
+
'x86_64-darwin'
|
|
162
|
+
when /\A(arm64|aarch64).*(darwin|macos|osx)/
|
|
163
|
+
'arm64-darwin'
|
|
164
|
+
else
|
|
165
|
+
@host
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def target_platform
|
|
170
|
+
@target_platform ||=
|
|
171
|
+
case ENV.fetch('target_platform', nil)
|
|
172
|
+
when /\A(arm64|aarch64).*(darwin|macos|osx)/
|
|
173
|
+
'arm64-darwin'
|
|
174
|
+
when /\Ax86_64.*(darwin|macos|osx)/
|
|
175
|
+
'x86_64-darwin'
|
|
176
|
+
when /\A(arm64|aarch64).*linux-musl/
|
|
177
|
+
'aarch64-linux-musl'
|
|
178
|
+
when /\A(arm64|aarch64).*linux/
|
|
179
|
+
'aarch64-linux'
|
|
180
|
+
when /\Ax86_64.*linux-musl/
|
|
181
|
+
'x86_64-linux-musl'
|
|
182
|
+
else
|
|
183
|
+
ENV.fetch('target_platform', host_platform)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def cross_compile?
|
|
188
|
+
target_platform != host_platform
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Configure MiniPortile + CMake for cross-compilation. Native builds
|
|
192
|
+
# (host_platform == target_platform) skip this entirely.
|
|
193
|
+
def setup_cross_compile
|
|
194
|
+
# All targeted platforms now have native runners (ubuntu-24.04-arm for
|
|
195
|
+
# aarch64-linux, windows-11-arm for aarch64-mingw-ucrt, Alpine containers
|
|
196
|
+
# for the musl variants). This hook is kept as a seam for future
|
|
197
|
+
# cross-compile targets (e.g. aarch64-linux on an x86_64 host).
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def cpu_type
|
|
201
|
+
case target_platform
|
|
202
|
+
when 'aarch64-linux', 'aarch64-linux-musl', 'arm64-darwin', 'aarch64-mingw-ucrt' then 'aarch64'
|
|
203
|
+
when 'x86_64-linux', 'x86_64-linux-musl', 'x86_64-darwin', /\Ax64-mingw/ then 'x86_64'
|
|
204
|
+
else
|
|
205
|
+
super
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def cmake_system_name
|
|
210
|
+
case target_platform
|
|
211
|
+
when 'aarch64-linux', 'x86_64-linux', 'aarch64-linux-musl', 'x86_64-linux-musl' then 'Linux'
|
|
212
|
+
when 'arm64-darwin', 'x86_64-darwin' then 'Darwin'
|
|
213
|
+
when /\A(aarch64-)?mingw/, /\Ax64-mingw/ then 'Windows'
|
|
214
|
+
else
|
|
215
|
+
super
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def target_format
|
|
220
|
+
@target_format ||=
|
|
221
|
+
case target_platform
|
|
222
|
+
when 'arm64-darwin'
|
|
223
|
+
/Mach-O 64-bit dynamically linked shared library arm64/
|
|
224
|
+
when 'x86_64-darwin'
|
|
225
|
+
/Mach-O 64-bit dynamically linked shared library x86_64/
|
|
226
|
+
when 'aarch64-linux', 'aarch64-linux-musl'
|
|
227
|
+
/ELF 64-bit LSB shared object, ARM aarch64/
|
|
228
|
+
when 'x86_64-linux', 'x86_64-linux-musl'
|
|
229
|
+
/ELF 64-bit LSB shared object, x86-64/
|
|
230
|
+
when 'aarch64-mingw-ucrt'
|
|
231
|
+
/PE32\+ executable.*\(DLL\).*ARM64/
|
|
232
|
+
when /\Ax64-mingw(32|-ucrt)/
|
|
233
|
+
/PE32\+ executable.*\(DLL\).*x86-64/
|
|
234
|
+
else
|
|
235
|
+
'skip'
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
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 = 1
|
|
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
|