libpng 1.6.58.4-aarch64-linux-ohos

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.
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libpng
4
+ # Decodes a PNG byte buffer into raw pixels via libpng's "simplified"
5
+ # read API (png_image_begin_read_from_memory + png_image_finish_read).
6
+ #
7
+ # Also parses the byte buffer via ChunkWalker to populate
8
+ # DecodedImage#bit_depth, #color_type, #interlace (from IHDR),
9
+ # #text (from tEXt/zTXt/iTXt), and #color (from gAMA/cHRM/sRGB/iCCP).
10
+ # The simplified API itself drops these ancillary chunks on read --
11
+ # walking them separately is the only way to surface their content.
12
+ #
13
+ # One instance per decode call. Ractor-safe.
14
+ class SimplifiedDecoder
15
+ def initialize(png, pixel_format: 'RGBA')
16
+ raise Error, 'input PNG buffer is nil' if png.nil?
17
+ raise Error, 'input PNG buffer must be a String' unless png.is_a?(String)
18
+
19
+ @png = png
20
+ @pixel_format = pixel_format
21
+ raise Error, "unknown pixel_format #{@pixel_format.inspect}" unless format_value
22
+ end
23
+
24
+ # Returns a Libpng::DecodedImage.
25
+ def call
26
+ fmt = format_value
27
+ img = FFI::MemoryPointer.new(:uint8, PNG_IMAGE_SIZE)
28
+ img.clear
29
+ img.put_uint32(PNG_IMAGE_OFF_VERSION, PNG_IMAGE_VERSION)
30
+
31
+ FFI::MemoryPointer.new(:uint8, @png.bytesize) do |in_buf|
32
+ in_buf.write_bytes(@png)
33
+ ok = Libpng::Binding.png_image_begin_read_from_memory(img, in_buf, @png.bytesize)
34
+ raise Error, "png_image_begin_read_from_memory failed: #{read_message(img)}" if ok.zero?
35
+
36
+ img.put_uint32(PNG_IMAGE_OFF_FORMAT, fmt)
37
+ width = img.get_uint32(PNG_IMAGE_OFF_WIDTH)
38
+ height = img.get_uint32(PNG_IMAGE_OFF_HEIGHT)
39
+ stride = width * BytesPerPixel.for_format(fmt)
40
+ out_size = stride * height
41
+
42
+ FFI::MemoryPointer.new(:uint8, out_size) do |out_buf|
43
+ ok = Libpng::Binding.png_image_finish_read(img, nil, out_buf, stride, nil)
44
+ raise Error, "png_image_finish_read failed: #{read_message(img)}" if ok.zero?
45
+
46
+ return DecodedImage.new(
47
+ width: width,
48
+ height: height,
49
+ format: @pixel_format.to_s.upcase,
50
+ pixels: out_buf.read_bytes(out_size),
51
+ **walker_metadata
52
+ )
53
+ end
54
+ end
55
+ ensure
56
+ Libpng::Binding.png_image_free(img) unless img.nil? || img.null?
57
+ end
58
+
59
+ private
60
+
61
+ def format_value
62
+ FORMAT_BY_NAME[@pixel_format.to_s.upcase]
63
+ end
64
+
65
+ # Walks the PNG once via ChunkWalker to gather all metadata fields
66
+ # the simplified API does not surface. Each accessor is best-effort:
67
+ # a malformed IHDR or text chunk should not poison the rest of the
68
+ # decode -- empty Hashes / nil values are acceptable fall-throughs.
69
+ def walker_metadata
70
+ walker = ChunkWalker.new(@png)
71
+ ihdr = safe_ihdr(walker)
72
+ {
73
+ bit_depth: ihdr[:bit_depth],
74
+ color_type: ihdr[:color_type],
75
+ interlace: ihdr[:interlace],
76
+ text: safe_text(walker),
77
+ color: safe_color(walker)
78
+ }
79
+ end
80
+
81
+ def safe_ihdr(walker)
82
+ walker.ihdr_fields
83
+ rescue ChunkWalker::FormatError
84
+ {}
85
+ end
86
+
87
+ def safe_text(walker)
88
+ walker.text_chunks
89
+ rescue ChunkWalker::FormatError
90
+ {}
91
+ end
92
+
93
+ def safe_color(walker)
94
+ walker.color_chunks
95
+ rescue ChunkWalker::FormatError
96
+ {}
97
+ end
98
+
99
+ def read_message(img)
100
+ s = img.get_bytes(PNG_IMAGE_OFF_MESSAGE, PNG_IMAGE_MESSAGE_BYTES)
101
+ s = s.split("\x00").first || ''
102
+ s.empty? ? '(no message)' : s.force_encoding('UTF-8')
103
+ rescue StandardError
104
+ '(no message)'
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libpng
4
+ # Encodes raw pixel data as a PNG via libpng's "simplified" write API
5
+ # (png_image_write_to_memory). By default the output is then walked
6
+ # chunk-by-chunk to strip the sRGB/gAMA/cHRM/iCCP chunks the
7
+ # simplified API emits -- this matches the byte layout of the
8
+ # classic libpng write path (libemf2svg's rgb2png).
9
+ #
10
+ # One instance per encode call. Ractor-safe: no shared mutable state.
11
+ class SimplifiedEncoder
12
+ def initialize(width, height, pixels,
13
+ pixel_format: 'RGBA',
14
+ convert_to_8bit: false,
15
+ strip_colorspace: true)
16
+ @width = width
17
+ @height = height
18
+ @pixels = pixels
19
+ @pixel_format = pixel_format
20
+ @convert_to_8bit = convert_to_8bit
21
+ @strip_colorspace = strip_colorspace
22
+ validate!
23
+ end
24
+
25
+ # Returns a binary String of PNG file bytes.
26
+ def call
27
+ raw = write_via_simplified_api
28
+ @strip_colorspace ? ChunkWalker.new(raw).strip_ancillary : raw
29
+ end
30
+
31
+ private
32
+
33
+ def validate!
34
+ raise Error, 'width must be positive' unless @width.positive?
35
+ raise Error, 'height must be positive' unless @height.positive?
36
+ raise Error, "unknown pixel_format #{@pixel_format.inspect}" unless format_value
37
+ return unless @pixels.bytesize < expected_size
38
+
39
+ raise Error,
40
+ "pixels too short: expected #{expected_size}, got #{@pixels.bytesize}"
41
+ end
42
+
43
+ def format_value
44
+ FORMAT_BY_NAME[@pixel_format.to_s.upcase]
45
+ end
46
+
47
+ def bytes_per_pixel
48
+ BytesPerPixel.for_format(format_value)
49
+ end
50
+
51
+ def expected_size
52
+ @width * bytes_per_pixel * @height
53
+ end
54
+
55
+ def write_via_simplified_api
56
+ img = FFI::MemoryPointer.new(:uint8, PNG_IMAGE_SIZE)
57
+ img.clear
58
+ img.put_uint32(PNG_IMAGE_OFF_VERSION, PNG_IMAGE_VERSION)
59
+ img.put_uint32(PNG_IMAGE_OFF_WIDTH, @width)
60
+ img.put_uint32(PNG_IMAGE_OFF_HEIGHT, @height)
61
+ img.put_uint32(PNG_IMAGE_OFF_FORMAT, format_value)
62
+
63
+ stride = @width * bytes_per_pixel
64
+ out_len_ptr = FFI::MemoryPointer.new(:size_t, 1)
65
+ ok = Libpng::Binding.png_image_write_to_memory(img, nil, out_len_ptr,
66
+ @convert_to_8bit ? 1 : 0,
67
+ @pixels, stride, nil)
68
+ if ok.zero?
69
+ msg = read_message(img)
70
+ Libpng::Binding.png_image_free(img)
71
+ raise Error, "png_image_write_to_memory (size query) failed: #{msg}"
72
+ end
73
+
74
+ out_len = out_len_ptr.read_uint64
75
+ raise Error, 'libpng reported zero-length PNG output' if out_len.zero?
76
+
77
+ buffer = FFI::MemoryPointer.new(:uint8, out_len)
78
+ ok = Libpng::Binding.png_image_write_to_memory(img, buffer, out_len_ptr,
79
+ @convert_to_8bit ? 1 : 0,
80
+ @pixels, stride, nil)
81
+ if ok.zero?
82
+ msg = read_message(img)
83
+ Libpng::Binding.png_image_free(img)
84
+ raise Error, "png_image_write_to_memory (write) failed: #{msg}"
85
+ end
86
+
87
+ buffer.read_bytes(out_len).tap { Libpng::Binding.png_image_free(img) }
88
+ end
89
+
90
+ def read_message(img)
91
+ s = img.get_bytes(PNG_IMAGE_OFF_MESSAGE, PNG_IMAGE_MESSAGE_BYTES)
92
+ s = s.split("\x00").first || ''
93
+ s.empty? ? '(no message)' : s.force_encoding('UTF-8')
94
+ rescue StandardError
95
+ '(no message)'
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libpng
4
+ # Encodes raw pixel data via libpng's standard write API
5
+ # (png_create_write_struct -> png_set_IHDR -> png_set_rows ->
6
+ # png_write_png(PNG_TRANSFORM_IDENTITY)). This matches the byte
7
+ # output of the "classic" libpng write path used by libemf2svg's
8
+ # rgb2png, and avoids the sRGB/gAMA chunks the simplified API
9
+ # injects by default.
10
+ #
11
+ # Options:
12
+ # pixel_format: :gray, :ga, :rgb, :rgba, or :palette
13
+ # filter: :default (adaptive), :none, :sub, :up, :avg,
14
+ # :paeth, :all, :adaptive
15
+ # compression_level: 0..9 (default 6 = Z_DEFAULT_COMPRESSION)
16
+ # interlace: :none or :adam7 (default :none)
17
+ # bit_depth: 8 or 16 (default 8; ignored for :palette which
18
+ # is always 8)
19
+ # palette: Array of [r, g, b] or [r, g, b, a] for
20
+ # pixel_format: :palette. 1..256 entries.
21
+ #
22
+ # One instance per encode call. Ractor-safe.
23
+ class StandardEncoder
24
+ def initialize(width, height, pixels,
25
+ pixel_format: 'RGBA',
26
+ filter: :default,
27
+ compression_level: 6,
28
+ interlace: :none,
29
+ bit_depth: 8,
30
+ palette: nil)
31
+ @width = width
32
+ @height = height
33
+ @pixels = pixels
34
+ @pixel_format_sym = coerce_pixel_format(pixel_format)
35
+ @filter_sym = filter.to_sym
36
+ @compression_level = compression_level
37
+ @interlace_sym = interlace.to_sym
38
+ @bit_depth = bit_depth
39
+ @palette = palette
40
+ validate!
41
+ end
42
+
43
+ # Returns a binary String of PNG file bytes.
44
+ def call
45
+ output = String.new.force_encoding('ASCII-8BIT')
46
+ callbacks = CallbackSet.new(output)
47
+
48
+ png_ptr = FFI::Pointer.new(0)
49
+ info_ptr = FFI::Pointer.new(0)
50
+ begin
51
+ png_ptr = Libpng::Binding.png_create_write_struct(LIBPNG_VER_STRING_C, nil, callbacks.error_fn, nil)
52
+ raise Error, 'png_create_write_struct returned NULL' if png_ptr.null?
53
+
54
+ info_ptr = Libpng::Binding.png_create_info_struct(png_ptr)
55
+ raise Error, 'png_create_info_struct returned NULL' if info_ptr.null?
56
+
57
+ apply_compression(png_ptr)
58
+ Libpng::Binding.png_set_write_fn(png_ptr, nil, callbacks.write_fn, nil)
59
+ Libpng::Binding.png_set_IHDR(png_ptr, info_ptr, @width, @height, effective_bit_depth,
60
+ color_type, interlace_method,
61
+ COMPRESSION_TYPE_DEFAULT, FILTER_TYPE_DEFAULT)
62
+ apply_filter(png_ptr)
63
+ apply_palette(png_ptr, info_ptr) if palette_format?
64
+
65
+ FFI::MemoryPointer.new(:uint8, @pixels.bytesize) do |px|
66
+ px.write_bytes(@pixels)
67
+ FFI::MemoryPointer.new(:pointer, @height) do |rows|
68
+ @height.times do |y|
69
+ rows.put_pointer(y * FFI.type_size(:pointer), px + (y * stride))
70
+ end
71
+ Libpng::Binding.png_set_rows(png_ptr, info_ptr, rows)
72
+ Libpng::Binding.png_write_png(png_ptr, info_ptr, TRANSFORM_IDENTITY, nil)
73
+ end
74
+ end
75
+
76
+ output
77
+ ensure
78
+ destroy(png_ptr, info_ptr)
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # Holds FFI::Function callbacks alive for the duration of the
85
+ # libpng calls. FFI::Function objects are GC'd when no Ruby
86
+ # reference remains; libpng stores the raw function pointer but
87
+ # has no idea the underlying Ruby object needs to stay alive.
88
+ class CallbackSet
89
+ attr_reader :write_fn, :error_fn
90
+
91
+ def initialize(output_accumulator)
92
+ @write_fn = FFI::Function.new(:void, %i[pointer pointer size_t]) do |_, data, len|
93
+ output_accumulator << data.read_bytes(len)
94
+ end
95
+ @error_fn = FFI::Function.new(:void, %i[pointer string]) do |_, msg|
96
+ raise Error, "libpng: #{msg}"
97
+ end
98
+ end
99
+ end
100
+
101
+ def coerce_pixel_format(value)
102
+ sym = value.to_s.downcase.to_sym
103
+ raise Error, "unknown pixel_format #{value.inspect}" unless FORMAT_TO_COLOR_TYPE.key?(sym)
104
+
105
+ sym
106
+ end
107
+
108
+ def validate!
109
+ raise Error, 'width must be positive' unless @width.positive?
110
+ raise Error, 'height must be positive' unless @height.positive?
111
+ raise Error, "unknown filter #{@filter_sym.inspect}" unless FILTER_MASK_BY_NAME.key?(@filter_sym)
112
+ raise Error, 'compression_level must be 0..9' unless (0..9).cover?(@compression_level)
113
+ unless INTERLACE_BY_NAME.key?(@interlace_sym)
114
+ raise Error,
115
+ "unknown interlace #{@interlace_sym.inspect} (expected :none or :adam7)"
116
+ end
117
+
118
+ validate_bit_depth!
119
+ validate_palette! if palette_format?
120
+ return unless @pixels.bytesize < expected_size
121
+
122
+ raise Error,
123
+ "pixels too short: expected #{expected_size}, got #{@pixels.bytesize}"
124
+ end
125
+
126
+ def validate_bit_depth!
127
+ if palette_format?
128
+ raise Error, 'bit_depth must be 8 for palette format' unless @bit_depth == 8
129
+ elsif !%i[gray rgb rgba].include?(@pixel_format_sym)
130
+ # GA: only 8 supported here for now.
131
+ raise Error, 'bit_depth must be 8' unless @bit_depth == 8
132
+ elsif ![8, 16].include?(@bit_depth)
133
+ raise Error, "bit_depth must be 8 or 16, got #{@bit_depth.inspect}"
134
+ end
135
+ end
136
+
137
+ def validate_palette!
138
+ raise Error, 'palette format requires palette: option' if @palette.nil?
139
+ raise Error, 'palette must be an Array' unless @palette.is_a?(Array)
140
+ raise Error, 'palette must have 1..256 entries' unless (1..256).cover?(@palette.length)
141
+
142
+ @palette.each_with_index do |entry, i|
143
+ next if entry.is_a?(Array) && [3, 4].include?(entry.length) && entry.all? do |v|
144
+ v.is_a?(Integer) && v.between?(0, 255)
145
+ end
146
+
147
+ raise Error, "palette[#{i}] must be [r,g,b] or [r,g,b,a] with 0..255 values"
148
+ end
149
+ end
150
+
151
+ def palette_format?
152
+ @pixel_format_sym == :palette
153
+ end
154
+
155
+ def color_type
156
+ FORMAT_TO_COLOR_TYPE[@pixel_format_sym]
157
+ end
158
+
159
+ def effective_bit_depth
160
+ palette_format? ? 8 : @bit_depth
161
+ end
162
+
163
+ def interlace_method
164
+ INTERLACE_BY_NAME[@interlace_sym]
165
+ end
166
+
167
+ def stride
168
+ @width * bytes_per_pixel
169
+ end
170
+
171
+ def bytes_per_pixel
172
+ return 1 if palette_format?
173
+
174
+ BytesPerPixel.for_color_type(color_type, bit_depth: effective_bit_depth)
175
+ end
176
+
177
+ def expected_size
178
+ stride * @height
179
+ end
180
+
181
+ def apply_compression(png_ptr)
182
+ Libpng::Binding.png_set_compression_level(png_ptr, @compression_level)
183
+ end
184
+
185
+ def apply_filter(png_ptr)
186
+ mask = FILTER_MASK_BY_NAME[@filter_sym]
187
+ Libpng::Binding.png_set_filter(png_ptr, FILTER_HEURISTIC_DEFAULT, mask) if mask
188
+ end
189
+
190
+ def apply_palette(png_ptr, info_ptr)
191
+ pal = @palette
192
+ has_alpha = pal.any? { |e| e.length == 4 }
193
+ pal_bytes = pal.map { |e| e.first(3) }.flatten.pack('C*')
194
+ FFI::MemoryPointer.new(:uint8, pal_bytes.bytesize) do |pp|
195
+ pp.write_bytes(pal_bytes)
196
+ Libpng::Binding.png_set_PLTE(png_ptr, info_ptr, pp, pal.length)
197
+ end
198
+ return unless has_alpha
199
+
200
+ alpha_bytes = pal.map { |e| e[3] || 255 }.pack('C*')
201
+ FFI::MemoryPointer.new(:uint8, alpha_bytes.bytesize) do |ap|
202
+ ap.write_bytes(alpha_bytes)
203
+ Libpng::Binding.png_set_tRNS(png_ptr, info_ptr, ap, alpha_bytes.bytesize, nil)
204
+ end
205
+ end
206
+
207
+ def destroy(png_ptr, info_ptr)
208
+ return if png_ptr.null?
209
+
210
+ FFI::MemoryPointer.new(:pointer) do |pp|
211
+ pp.write_pointer(png_ptr)
212
+ FFI::MemoryPointer.new(:pointer) do |ip|
213
+ ip.write_pointer(info_ptr)
214
+ Libpng::Binding.png_destroy_write_struct(pp, ip)
215
+ end
216
+ end
217
+ end
218
+ end
219
+ 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 = 4
15
+ VERSION = "#{LIBPNG_VERSION}.#{LIBPNG_RUBY_ITERATION}"
16
+ end
data/lib/libpng.rb ADDED
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ # Libpng is a Ruby binding for libpng (the official PNG reference library)
6
+ # via FFI. The native library is pre-compiled for each target platform and
7
+ # shipped inside the gem, so no C compiler is required at install time.
8
+ #
9
+ # Public API:
10
+ # Libpng.encode(width, height, rgba_bytes, pixel_format: "RGBA")
11
+ # Libpng.encode_standard(width, height, rgba_bytes, pixel_format: "RGBA", ...)
12
+ # Libpng.decode(png_bytes, pixel_format: "RGBA")
13
+ #
14
+ # All encode/decode state is per-call. Each call constructs a dedicated
15
+ # encoder or decoder instance, runs it, and discards it. Calls from
16
+ # different Ractors do not share state. The FFI function table
17
+ # (Libpng::Binding) loads lazily on first use and is shareable across
18
+ # Ractors.
19
+ module Libpng
20
+ # Version constants live in lib/libpng/version.rb. Autoloaded so the
21
+ # version file isn't loaded until someone references a version constant.
22
+ autoload :LIBPNG_VERSION, 'libpng/version'
23
+ autoload :LIBPNG_RUBY_ITERATION, 'libpng/version'
24
+ autoload :VERSION, 'libpng/version'
25
+
26
+ # Public-facing classes.
27
+ autoload :Error, 'libpng/error'
28
+ autoload :DecodedImage, 'libpng/decoded_image'
29
+ autoload :ChunkWalker, 'libpng/chunk_walker'
30
+ autoload :BytesPerPixel, 'libpng/bytes_per_pixel'
31
+ autoload :SimplifiedEncoder, 'libpng/simplified_encoder'
32
+ autoload :SimplifiedDecoder, 'libpng/simplified_decoder'
33
+ autoload :StandardEncoder, 'libpng/standard_encoder'
34
+
35
+ # FFI bindings. Autoloaded so that requiring `libpng` is cheap and
36
+ # does not dlopen libpng16.{so,dylib,dll} -- ext/extconf.rb needs to
37
+ # require 'libpng' to access Libpng::Recipe during the source-gem
38
+ # build, at which point the shared library doesn't exist yet.
39
+ autoload :Binding, 'libpng/binding'
40
+
41
+ # Build-time recipe (MiniPortile). Only loaded when ext/extconf.rb
42
+ # is invoked during `gem install` of the source ('ruby' platform) gem.
43
+ autoload :Recipe, 'libpng/recipe'
44
+
45
+ # ------------------------------------------------------------------
46
+ # PNG_IMAGE_FORMAT_* bit flags (png.h). Used by the simplified API.
47
+ # ------------------------------------------------------------------
48
+ FORMAT_FLAG_ALPHA = 0x01
49
+ FORMAT_FLAG_COLOR = 0x02
50
+ FORMAT_FLAG_LINEAR = 0x04
51
+ FORMAT_FLAG_COLORMAP = 0x08
52
+ FORMAT_FLAG_BGR = 0x10
53
+ FORMAT_FLAG_AFIRST = 0x20
54
+ FORMAT_FLAG_ASSOCIATED_ALPHA = 0x40
55
+
56
+ FORMAT_GRAY = 0
57
+ FORMAT_GA = FORMAT_FLAG_ALPHA
58
+ FORMAT_AG = FORMAT_FLAG_ALPHA | FORMAT_FLAG_AFIRST
59
+ FORMAT_RGB = FORMAT_FLAG_COLOR
60
+ FORMAT_BGR = FORMAT_FLAG_COLOR | FORMAT_FLAG_BGR
61
+ FORMAT_RGBA = FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA
62
+ FORMAT_ARGB = FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA | FORMAT_FLAG_AFIRST
63
+ FORMAT_BGRA = FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA | FORMAT_FLAG_BGR
64
+ FORMAT_ABGR = FORMAT_FLAG_COLOR | FORMAT_FLAG_ALPHA | FORMAT_FLAG_AFIRST | FORMAT_FLAG_BGR
65
+
66
+ FORMAT_BY_NAME = {
67
+ 'GRAY' => FORMAT_GRAY,
68
+ 'GRAYSCALE' => FORMAT_GRAY,
69
+ 'GA' => FORMAT_GA,
70
+ 'AG' => FORMAT_AG,
71
+ 'RGB' => FORMAT_RGB,
72
+ 'BGR' => FORMAT_BGR,
73
+ 'RGBA' => FORMAT_RGBA,
74
+ 'ARGB' => FORMAT_ARGB,
75
+ 'BGRA' => FORMAT_BGRA,
76
+ 'ABGR' => FORMAT_ABGR
77
+ }.freeze
78
+
79
+ # ------------------------------------------------------------------
80
+ # Standard-API color types (png.h).
81
+ # ------------------------------------------------------------------
82
+ COLOR_MASK_PALETTE = 0x01
83
+ COLOR_MASK_COLOR = 0x02
84
+ COLOR_MASK_ALPHA = 0x04
85
+
86
+ COLOR_TYPE_GRAY = 0
87
+ COLOR_TYPE_PALETTE = COLOR_MASK_COLOR | COLOR_MASK_PALETTE
88
+ COLOR_TYPE_RGB = COLOR_MASK_COLOR
89
+ COLOR_TYPE_RGB_ALPHA = COLOR_MASK_COLOR | COLOR_MASK_ALPHA
90
+ COLOR_TYPE_GRAY_ALPHA = COLOR_MASK_ALPHA
91
+
92
+ FORMAT_TO_COLOR_TYPE = {
93
+ gray: COLOR_TYPE_GRAY,
94
+ ga: COLOR_TYPE_GRAY_ALPHA,
95
+ ag: COLOR_TYPE_GRAY_ALPHA,
96
+ rgb: COLOR_TYPE_RGB,
97
+ rgba: COLOR_TYPE_RGB_ALPHA,
98
+ palette: COLOR_TYPE_PALETTE
99
+ }.freeze
100
+
101
+ # ------------------------------------------------------------------
102
+ # png_set_IHDR pass-through constants (png.h).
103
+ # ------------------------------------------------------------------
104
+ INTERLACE_NONE = 0
105
+ INTERLACE_ADAM7 = 1
106
+ COMPRESSION_TYPE_DEFAULT = 0
107
+ FILTER_TYPE_DEFAULT = 0
108
+ TRANSFORM_IDENTITY = 0x0000
109
+
110
+ INTERLACE_BY_NAME = {
111
+ none: INTERLACE_NONE,
112
+ adam7: INTERLACE_ADAM7
113
+ }.freeze
114
+
115
+ # ------------------------------------------------------------------
116
+ # png_set_filter bitmask (PNG_FILTER_*).
117
+ # ------------------------------------------------------------------
118
+ FILTER_HEURISTIC_DEFAULT = 0
119
+ FILTER_NONE = 0x08
120
+ FILTER_SUB = 0x10
121
+ FILTER_UP = 0x20
122
+ FILTER_AVG = 0x40
123
+ FILTER_PAETH = 0x80
124
+ FILTER_ALL = FILTER_NONE | FILTER_SUB | FILTER_UP | FILTER_AVG | FILTER_PAETH
125
+
126
+ FILTER_MASK_BY_NAME = {
127
+ default: nil,
128
+ adaptive: FILTER_ALL,
129
+ none: FILTER_NONE,
130
+ sub: FILTER_SUB,
131
+ up: FILTER_UP,
132
+ avg: FILTER_AVG,
133
+ paeth: FILTER_PAETH,
134
+ all: FILTER_ALL
135
+ }.freeze
136
+
137
+ # PNG_LIBPNG_VER_STRING. The C string passed as `user_png_ver` to
138
+ # png_create_write_struct. libpng checks this against its compiled-in
139
+ # version; mismatches return NULL. Must match the libpng16 binary we
140
+ # ship, which is 1.6.58.
141
+ LIBPNG_VER_STRING_C = LIBPNG_VERSION
142
+
143
+ # ------------------------------------------------------------------
144
+ # png_image struct field offsets (bytes). Used by SimplifiedEncoder /
145
+ # SimplifiedDecoder to allocate the png_image struct via a raw
146
+ # FFI::MemoryPointer (rather than FFI::Struct, which has class-level
147
+ # state that isn't shareable across non-main Ractors).
148
+ # ------------------------------------------------------------------
149
+ PNG_IMAGE_VERSION = 1
150
+ PNG_IMAGE_MESSAGE_BYTES = 64
151
+ PNG_IMAGE_SIZE = FFI.type_size(:pointer) + (7 * 4) + PNG_IMAGE_MESSAGE_BYTES
152
+ PNG_IMAGE_OFF_OPAQUE = 0
153
+ PNG_IMAGE_OFF_VERSION = FFI.type_size(:pointer)
154
+ PNG_IMAGE_OFF_WIDTH = PNG_IMAGE_OFF_VERSION + 4
155
+ PNG_IMAGE_OFF_HEIGHT = PNG_IMAGE_OFF_WIDTH + 4
156
+ PNG_IMAGE_OFF_FORMAT = PNG_IMAGE_OFF_HEIGHT + 4
157
+ PNG_IMAGE_OFF_FLAGS = PNG_IMAGE_OFF_FORMAT + 4
158
+ PNG_IMAGE_OFF_COLORMAP_ENTRIES = PNG_IMAGE_OFF_FLAGS + 4
159
+ PNG_IMAGE_OFF_WARNING_OR_ERROR = PNG_IMAGE_OFF_COLORMAP_ENTRIES + 4
160
+ PNG_IMAGE_OFF_MESSAGE = PNG_IMAGE_OFF_WARNING_OR_ERROR + 4
161
+
162
+ # ------------------------------------------------------------------
163
+ # Public API. Each method constructs a dedicated encoder/decoder
164
+ # instance, runs it, and returns its output. Keeping these as thin
165
+ # dispatchers preserves the existing module-level API while letting
166
+ # the heavy logic live in testable, single-purpose classes.
167
+ # ------------------------------------------------------------------
168
+ class << self
169
+ # Encode raw pixels via the simplified API. Output is post-
170
+ # processed to strip sRGB/gAMA/cHRM/iCCP chunks unless
171
+ # strip_colorspace: false.
172
+ def encode(width, height, pixels, **opts)
173
+ SimplifiedEncoder.new(width, height, pixels, **opts).call
174
+ end
175
+
176
+ # Encode raw pixels via the standard write API. Accepts filter,
177
+ # compression_level, interlace, bit_depth, and palette options.
178
+ # Emits only IHDR/IDAT/IEND directly (no chunk stripping needed).
179
+ def encode_standard(width, height, pixels, **opts)
180
+ StandardEncoder.new(width, height, pixels, **opts).call
181
+ end
182
+
183
+ # Decode a PNG buffer into raw pixels plus IHDR metadata.
184
+ def decode(png, **opts)
185
+ SimplifiedDecoder.new(png, **opts).call
186
+ end
187
+ end
188
+ 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.8'
36
+
37
+ spec.extensions = ['ext/extconf.rb']
38
+ end