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.
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
@@ -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,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