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.
- checksums.yaml +7 -0
- data/.github/workflows/build.yml +211 -0
- data/.github/workflows/release.yml +299 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +39 -0
- data/CHANGELOG.md +127 -0
- data/CLAUDE.md +110 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +27 -0
- data/README.adoc +238 -0
- data/Rakefile +74 -0
- data/bin/console +5 -0
- data/bin/setup +5 -0
- data/ext/extconf.rb +19 -0
- data/lib/libpng/binding.rb +57 -0
- data/lib/libpng/bytes_per_pixel.rb +43 -0
- data/lib/libpng/chunk_walker.rb +271 -0
- data/lib/libpng/decoded_image.rb +22 -0
- data/lib/libpng/error.rb +8 -0
- data/lib/libpng/libpng16.so +0 -0
- data/lib/libpng/recipe.rb +247 -0
- data/lib/libpng/simplified_decoder.rb +107 -0
- data/lib/libpng/simplified_encoder.rb +98 -0
- data/lib/libpng/standard_encoder.rb +219 -0
- data/lib/libpng/version.rb +16 -0
- data/lib/libpng.rb +188 -0
- data/libpng.gemspec +38 -0
- metadata +90 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
module Libpng
|
|
6
|
+
# FFI bindings to the bundled `libpng16.{so,dylib,dll}`. Lives in its
|
|
7
|
+
# own autoloaded module so that requiring `libpng` (the top-level
|
|
8
|
+
# entry point used by `ext/extconf.rb` during the source-gem build)
|
|
9
|
+
# does NOT eagerly dlopen the shared library -- it may not exist yet
|
|
10
|
+
# at that point.
|
|
11
|
+
#
|
|
12
|
+
# The first time an encoder or decoder references `Libpng::Binding`,
|
|
13
|
+
# this file loads, ffi_lib fires, and the FFI functions attach. From
|
|
14
|
+
# then on, calls go directly through the attached C functions.
|
|
15
|
+
module Binding
|
|
16
|
+
extend FFI::Library
|
|
17
|
+
|
|
18
|
+
ffi_lib_flags :now, :global
|
|
19
|
+
|
|
20
|
+
lib_filename = if FFI::Platform.windows?
|
|
21
|
+
'libpng16.dll'
|
|
22
|
+
elsif FFI::Platform.mac?
|
|
23
|
+
'libpng16.dylib'
|
|
24
|
+
else
|
|
25
|
+
'libpng16.so'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# __dir__ is lib/libpng/; the shared library ships at lib/libpng/.
|
|
29
|
+
ffi_lib File.expand_path(lib_filename, __dir__)
|
|
30
|
+
|
|
31
|
+
# libpng simplified API (png_image / png_image_*).
|
|
32
|
+
attach_function :png_image_begin_read_from_memory,
|
|
33
|
+
%i[pointer pointer size_t], :int
|
|
34
|
+
attach_function :png_image_finish_read,
|
|
35
|
+
%i[pointer pointer pointer int pointer], :int
|
|
36
|
+
attach_function :png_image_write_to_memory,
|
|
37
|
+
%i[pointer pointer pointer int pointer int pointer], :int
|
|
38
|
+
attach_function :png_image_free, [:pointer], :void
|
|
39
|
+
|
|
40
|
+
# libpng standard (non-simplified) write API.
|
|
41
|
+
attach_function :png_create_write_struct,
|
|
42
|
+
%i[string pointer pointer pointer], :pointer
|
|
43
|
+
attach_function :png_create_info_struct, [:pointer], :pointer
|
|
44
|
+
attach_function :png_destroy_write_struct, %i[pointer pointer], :void
|
|
45
|
+
attach_function :png_set_IHDR,
|
|
46
|
+
%i[pointer pointer uint32 uint32 int int int int int], :void
|
|
47
|
+
attach_function :png_set_rows, %i[pointer pointer pointer], :void
|
|
48
|
+
attach_function :png_set_PLTE, %i[pointer pointer pointer int], :void
|
|
49
|
+
attach_function :png_set_tRNS,
|
|
50
|
+
%i[pointer pointer pointer int pointer], :void
|
|
51
|
+
attach_function :png_set_filter, %i[pointer int int], :void
|
|
52
|
+
attach_function :png_set_compression_level, %i[pointer int], :void
|
|
53
|
+
attach_function :png_set_write_fn,
|
|
54
|
+
%i[pointer pointer pointer pointer], :void
|
|
55
|
+
attach_function :png_write_png, %i[pointer pointer int pointer], :void
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Libpng
|
|
4
|
+
# Maps pixel formats to bytes-per-pixel. Used by both the simplified
|
|
5
|
+
# and standard encoders / decoders to compute row stride.
|
|
6
|
+
#
|
|
7
|
+
# Pure data class -- no I/O, no allocations beyond the lookup itself.
|
|
8
|
+
module BytesPerPixel
|
|
9
|
+
# Per-channel byte count for the supported bit depths. Linear (16-bit)
|
|
10
|
+
# formats aren't currently exposed via the simplified API path; the
|
|
11
|
+
# standard encoder uses this table when bit_depth: 16 is requested.
|
|
12
|
+
CHANNEL_BYTES_8_BIT = 1
|
|
13
|
+
CHANNEL_BYTES_16_BIT = 2
|
|
14
|
+
|
|
15
|
+
# Channels per PNG_COLOR_TYPE_* (png.h).
|
|
16
|
+
CHANNELS_BY_COLOR_TYPE = {
|
|
17
|
+
Libpng::COLOR_TYPE_GRAY => 1,
|
|
18
|
+
Libpng::COLOR_TYPE_GRAY_ALPHA => 2,
|
|
19
|
+
Libpng::COLOR_TYPE_RGB => 3,
|
|
20
|
+
Libpng::COLOR_TYPE_RGB_ALPHA => 4,
|
|
21
|
+
Libpng::COLOR_TYPE_PALETTE => 1
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def self.for_format(format_value)
|
|
25
|
+
case format_value
|
|
26
|
+
when Libpng::FORMAT_GRAY then 1
|
|
27
|
+
when Libpng::FORMAT_GA, Libpng::FORMAT_AG then 2
|
|
28
|
+
when Libpng::FORMAT_RGB, Libpng::FORMAT_BGR then 3
|
|
29
|
+
when Libpng::FORMAT_RGBA, Libpng::FORMAT_ARGB,
|
|
30
|
+
Libpng::FORMAT_BGRA, Libpng::FORMAT_ABGR then 4
|
|
31
|
+
else
|
|
32
|
+
raise Libpng::Error, "no bytes-per-pixel mapping for format 0x#{format_value.to_s(16)}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.for_color_type(color_type, bit_depth: 8)
|
|
37
|
+
channels = CHANNELS_BY_COLOR_TYPE[color_type] ||
|
|
38
|
+
raise(Libpng::Error, "unknown color_type #{color_type}")
|
|
39
|
+
channel_bytes = bit_depth == 16 ? CHANNEL_BYTES_16_BIT : CHANNEL_BYTES_8_BIT
|
|
40
|
+
channels * channel_bytes
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zlib'
|
|
4
|
+
|
|
5
|
+
module Libpng
|
|
6
|
+
# Walks a PNG byte buffer chunk-by-chunk. Used to strip ancillary
|
|
7
|
+
# chunks (sRGB, gAMA, cHRM, iCCP, etc.) that libpng's simplified
|
|
8
|
+
# write API emits by default, and to extract metadata (IHDR fields,
|
|
9
|
+
# text chunks, color chunks, pHYs) during decode.
|
|
10
|
+
#
|
|
11
|
+
# The chunk layout is documented in the PNG spec:
|
|
12
|
+
# [4 bytes length][4 bytes type][length bytes data][4 bytes CRC]
|
|
13
|
+
# Length and CRC are big-endian uint32. The signature is the 8 bytes
|
|
14
|
+
# before the first chunk: 89 50 4E 47 0D 0A 1A 0A.
|
|
15
|
+
class ChunkWalker
|
|
16
|
+
SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10].freeze
|
|
17
|
+
|
|
18
|
+
# Raised on any structural issue: bad signature, chunk running past
|
|
19
|
+
# EOF, CRC mismatch, missing IEND.
|
|
20
|
+
class FormatError < Error; end
|
|
21
|
+
|
|
22
|
+
# PNG gAMA fixed-point scale (per spec: gamma = uint32 / 100000).
|
|
23
|
+
GAMMA_SCALE = 100_000.0
|
|
24
|
+
|
|
25
|
+
# PNG cHRM fixed-point scale (per spec: chromaticity = uint32 / 100000).
|
|
26
|
+
CHRM_SCALE = 100_000.0
|
|
27
|
+
|
|
28
|
+
def initialize(png_bytes)
|
|
29
|
+
@bytes = png_bytes.bytes
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Iterate every chunk in order. Yields [type_string, data_bytes,
|
|
33
|
+
# offset] to the block. Returns an Enumerator if no block given.
|
|
34
|
+
def each_chunk
|
|
35
|
+
return enum_for(:each_chunk) unless block_given?
|
|
36
|
+
|
|
37
|
+
verify_signature
|
|
38
|
+
offset = 8
|
|
39
|
+
until offset + 8 > @bytes.length
|
|
40
|
+
len = @bytes[offset, 4].pack('C*').unpack1('N')
|
|
41
|
+
type = @bytes[offset + 4, 4].pack('C*')
|
|
42
|
+
chunk_total = 12 + len
|
|
43
|
+
if offset + chunk_total > @bytes.length
|
|
44
|
+
raise FormatError,
|
|
45
|
+
"PNG chunk at offset #{offset} (#{type}) runs past EOF"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
crc_input = @bytes[offset + 4, 4 + len].pack('C*')
|
|
49
|
+
crc_actual = @bytes[offset + 8 + len, 4].pack('C*').unpack1('N')
|
|
50
|
+
crc_expected = Zlib.crc32(crc_input)
|
|
51
|
+
raise FormatError, "PNG chunk CRC mismatch at offset #{offset} (#{type})" unless crc_actual == crc_expected
|
|
52
|
+
|
|
53
|
+
data = @bytes[offset + 8, len].pack('C*')
|
|
54
|
+
yield type, data, offset
|
|
55
|
+
|
|
56
|
+
offset += chunk_total
|
|
57
|
+
break if type == 'IEND'
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns a new binary String containing only the PNG signature
|
|
62
|
+
# plus IHDR, IDAT, and IEND chunks. Other chunks (sRGB, gAMA, cHRM,
|
|
63
|
+
# iCCP, tEXt, zTXt, iTXt, bKGD, pHYs, oFFs, tIME, sCAL, hIST,
|
|
64
|
+
# sPLT, unknown chunks) are dropped.
|
|
65
|
+
def strip_ancillary
|
|
66
|
+
kept = SIGNATURE.pack('C*')
|
|
67
|
+
each_chunk do |type, data, _offset|
|
|
68
|
+
next unless %w[IHDR IDAT IEND].include?(type)
|
|
69
|
+
|
|
70
|
+
len_bytes = [data.length].pack('N')
|
|
71
|
+
type_bytes = type
|
|
72
|
+
crc_bytes = [Zlib.crc32(type_bytes + data)].pack('N')
|
|
73
|
+
kept << len_bytes << type_bytes << data << crc_bytes
|
|
74
|
+
end
|
|
75
|
+
kept.force_encoding('ASCII-8BIT')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns the raw IHDR data bytes (13 bytes), or raises if missing.
|
|
79
|
+
def ihdr_data
|
|
80
|
+
each_chunk { |type, data, _| return data if type == 'IHDR' }
|
|
81
|
+
raise FormatError, 'PNG has no IHDR chunk'
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Parse IHDR fields. Returns a Hash with :width, :height,
|
|
85
|
+
# :bit_depth, :color_type, :compression, :filter, :interlace.
|
|
86
|
+
def ihdr_fields
|
|
87
|
+
data = ihdr_data
|
|
88
|
+
raise FormatError, "IHDR is #{data.length} bytes, expected 13" unless data.length == 13
|
|
89
|
+
|
|
90
|
+
width, height, bd, ct, comp, filt, intc = data.unpack('NNCCCCC')
|
|
91
|
+
{
|
|
92
|
+
width: width,
|
|
93
|
+
height: height,
|
|
94
|
+
bit_depth: bd,
|
|
95
|
+
color_type: ct,
|
|
96
|
+
compression: comp,
|
|
97
|
+
filter: filt,
|
|
98
|
+
interlace: intc
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Parse text chunks (tEXt, zTXt, iTXt) into a flat Hash of
|
|
103
|
+
# keyword -> UTF-8 String. When the same keyword appears in
|
|
104
|
+
# multiple chunks, the last occurrence wins (PNG spec permits
|
|
105
|
+
# this; behavior matches libpng's read path).
|
|
106
|
+
#
|
|
107
|
+
# - tEXt: Latin-1 keyword + Latin-1 value, transcoded to UTF-8.
|
|
108
|
+
# - zTXt: Latin-1 keyword + zlib-compressed Latin-1 value,
|
|
109
|
+
# inflated then transcoded to UTF-8.
|
|
110
|
+
# - iTXt: UTF-8 keyword + UTF-8 value (optionally zlib-compressed),
|
|
111
|
+
# with separate language tag and translated keyword that are
|
|
112
|
+
# dropped here (callers who need them should walk chunks directly).
|
|
113
|
+
#
|
|
114
|
+
# Malformed chunks are silently skipped: a single broken zTXt
|
|
115
|
+
# should not poison the rest of the decode.
|
|
116
|
+
def text_chunks
|
|
117
|
+
result = {}
|
|
118
|
+
each_chunk do |type, data, _|
|
|
119
|
+
case type
|
|
120
|
+
when 'tEXt'
|
|
121
|
+
parse_text(data, result)
|
|
122
|
+
when 'zTXt'
|
|
123
|
+
parse_ztxt(data, result)
|
|
124
|
+
when 'iTXt'
|
|
125
|
+
parse_itxt(data, result)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Parse color-space chunks (gAMA, cHRM, sRGB, iCCP) into a Hash.
|
|
132
|
+
# Returns Symbol keys:
|
|
133
|
+
#
|
|
134
|
+
# :gamma -> Float (gAMA, value/100000)
|
|
135
|
+
# :white_point_x, :white_point_y -> Float (cHRM)
|
|
136
|
+
# :red_x, :red_y, :green_x, :green_y, :blue_x, :blue_y -> Float
|
|
137
|
+
# :srgb_intent -> Integer 0..3 (sRGB)
|
|
138
|
+
# :icc_profile_name -> UTF-8 String (iCCP, profile name)
|
|
139
|
+
# :icc_profile -> binary String (iCCP, decompressed bytes)
|
|
140
|
+
#
|
|
141
|
+
# Malformed chunks (e.g. truncated cHRM, bad iCCP compression) are
|
|
142
|
+
# silently skipped. ICC profiles can be large (10KB-1MB); callers
|
|
143
|
+
# who don't need them should walk chunks manually rather than call
|
|
144
|
+
# this method.
|
|
145
|
+
def color_chunks
|
|
146
|
+
result = {}
|
|
147
|
+
each_chunk do |type, data, _|
|
|
148
|
+
case type
|
|
149
|
+
when 'gAMA'
|
|
150
|
+
parse_gama(data, result)
|
|
151
|
+
when 'cHRM'
|
|
152
|
+
parse_chrm(data, result)
|
|
153
|
+
when 'sRGB'
|
|
154
|
+
parse_srgb(data, result)
|
|
155
|
+
when 'iCCP'
|
|
156
|
+
parse_iccp(data, result)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
result
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def verify_signature
|
|
165
|
+
sig = @bytes.first(8)
|
|
166
|
+
raise FormatError, 'not a PNG file (bad signature)' unless sig == SIGNATURE
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Split a binary string at the first NUL byte. Returns [head, tail]
|
|
170
|
+
# where head excludes the NUL and tail is everything after. Returns
|
|
171
|
+
# [nil, nil] when no NUL is present.
|
|
172
|
+
def split_at_null(bytes)
|
|
173
|
+
idx = bytes.index("\x00")
|
|
174
|
+
return [nil, nil] if idx.nil?
|
|
175
|
+
|
|
176
|
+
[bytes[0, idx], bytes[(idx + 1)..]]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Force Latin-1 byte sequence into UTF-8. Bytes 0x80-0xFF that have
|
|
180
|
+
# no assigned meaning in ISO-8859-1 still round-trip through this
|
|
181
|
+
# encoding because Latin-1 is a strict superset of the byte values.
|
|
182
|
+
def latin1_to_utf8(bytes)
|
|
183
|
+
bytes.force_encoding('ISO-8859-1').encode('UTF-8')
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def parse_text(data, result)
|
|
187
|
+
key, value = split_at_null(data)
|
|
188
|
+
return if key.nil?
|
|
189
|
+
|
|
190
|
+
result[key] = latin1_to_utf8(value)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def parse_ztxt(data, result)
|
|
194
|
+
key, rest = split_at_null(data)
|
|
195
|
+
return if key.nil? || rest.nil? || rest.length < 2
|
|
196
|
+
|
|
197
|
+
_compression_method = rest.getbyte(0)
|
|
198
|
+
inflated = Zlib.inflate(rest[1..])
|
|
199
|
+
result[key] = latin1_to_utf8(inflated)
|
|
200
|
+
rescue Zlib::Error
|
|
201
|
+
# Skip malformed zTXt rather than failing the whole decode.
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def parse_itxt(data, result)
|
|
205
|
+
# iTXt layout:
|
|
206
|
+
# keyword\0
|
|
207
|
+
# compression_flag (1 byte, 0 = uncompressed, 1 = zlib)
|
|
208
|
+
# compression_method (1 byte, 0 = zlib/deflate)
|
|
209
|
+
# language_tag\0 (ASCII, may be empty)
|
|
210
|
+
# translated_keyword\0 (UTF-8, may be empty)
|
|
211
|
+
# text (UTF-8, optionally zlib-compressed)
|
|
212
|
+
key, rest = split_at_null(data)
|
|
213
|
+
return if key.nil? || rest.nil? || rest.length < 3
|
|
214
|
+
|
|
215
|
+
compression_flag = rest.getbyte(0)
|
|
216
|
+
_compression_method = rest.getbyte(1)
|
|
217
|
+
_lang, after_lang = split_at_null(rest[2..])
|
|
218
|
+
return if after_lang.nil?
|
|
219
|
+
|
|
220
|
+
_translated, text_bytes = split_at_null(after_lang)
|
|
221
|
+
return if text_bytes.nil?
|
|
222
|
+
|
|
223
|
+
text_bytes = Zlib.inflate(text_bytes) if compression_flag == 1
|
|
224
|
+
result[key] = text_bytes.force_encoding('UTF-8')
|
|
225
|
+
rescue Zlib::Error
|
|
226
|
+
# Skip malformed iTXt rather than failing the whole decode.
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def parse_gama(data, result)
|
|
230
|
+
return if data.length < 4
|
|
231
|
+
|
|
232
|
+
result[:gamma] = data.unpack1('N') / GAMMA_SCALE
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def parse_chrm(data, result)
|
|
236
|
+
return if data.length < 32
|
|
237
|
+
|
|
238
|
+
wp_x, wp_y, rx, ry, gx, gy, bx, by = data.unpack('N8')
|
|
239
|
+
result[:white_point_x] = wp_x / CHRM_SCALE
|
|
240
|
+
result[:white_point_y] = wp_y / CHRM_SCALE
|
|
241
|
+
result[:red_x] = rx / CHRM_SCALE
|
|
242
|
+
result[:red_y] = ry / CHRM_SCALE
|
|
243
|
+
result[:green_x] = gx / CHRM_SCALE
|
|
244
|
+
result[:green_y] = gy / CHRM_SCALE
|
|
245
|
+
result[:blue_x] = bx / CHRM_SCALE
|
|
246
|
+
result[:blue_y] = by / CHRM_SCALE
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def parse_srgb(data, result)
|
|
250
|
+
return if data.empty?
|
|
251
|
+
|
|
252
|
+
result[:srgb_intent] = data.getbyte(0)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def parse_iccp(data, result)
|
|
256
|
+
# iCCP layout:
|
|
257
|
+
# profile_name\0
|
|
258
|
+
# compression_method (1 byte, must be 0 = zlib/deflate)
|
|
259
|
+
# compressed_profile (zlib stream)
|
|
260
|
+
name, rest = split_at_null(data)
|
|
261
|
+
return if name.nil? || rest.nil? || rest.length < 2
|
|
262
|
+
|
|
263
|
+
_compression_method = rest.getbyte(0)
|
|
264
|
+
profile = Zlib.inflate(rest[1..])
|
|
265
|
+
result[:icc_profile_name] = name.force_encoding('UTF-8')
|
|
266
|
+
result[:icc_profile] = profile
|
|
267
|
+
rescue Zlib::Error
|
|
268
|
+
# Skip malformed iCCP rather than failing the whole decode.
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Libpng
|
|
4
|
+
# Result of {Libpng.decode}. A frozen-keyword Struct so it crosses
|
|
5
|
+
# Ractor boundaries cleanly via moving semantics.
|
|
6
|
+
#
|
|
7
|
+
# +width+ image width in pixels (Integer)
|
|
8
|
+
# +height+ image height in pixels (Integer)
|
|
9
|
+
# +format+ pixel format String ("RGBA", "RGB", "GRAY", "GA")
|
|
10
|
+
# +pixels+ binary String of raw pixel bytes, row-major top-down
|
|
11
|
+
# +bit_depth+ bits per channel (8 or 16). nil if not extracted.
|
|
12
|
+
# +color_type+ PNG_COLOR_TYPE_* integer from IHDR. nil if not extracted.
|
|
13
|
+
# +interlace+ PNG_INTERLACE_* integer from IHDR. nil if not extracted.
|
|
14
|
+
# +text+ Hash<String,String> of tEXt/zTXt/iTXt keyword -> UTF-8
|
|
15
|
+
# value. Empty Hash when no text chunks present.
|
|
16
|
+
# +color+ Hash<Symbol,*> of gAMA/cHRM/sRGB/iCCP fields. Empty
|
|
17
|
+
# Hash when no color metadata present.
|
|
18
|
+
DecodedImage = Struct.new(:width, :height, :format, :pixels,
|
|
19
|
+
:bit_depth, :color_type, :interlace,
|
|
20
|
+
:text, :color,
|
|
21
|
+
keyword_init: true)
|
|
22
|
+
end
|
data/lib/libpng/error.rb
ADDED
|
Binary file
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
require 'rbconfig'
|
|
2
|
+
require 'mini_portile2'
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'open3'
|
|
6
|
+
|
|
7
|
+
module Libpng
|
|
8
|
+
# MiniPortile-based recipe for building libpng from source. Mirrors the
|
|
9
|
+
# pattern used by emf2svg-ruby: during `gem install`, ext/extconf.rb
|
|
10
|
+
# invokes Recipe#cook, which downloads the libpng source tarball, runs
|
|
11
|
+
# configure + make, and installs the shared library into the gem's lib/
|
|
12
|
+
# directory. The pre-compiled gems (built via `rake gem:native:<plat>`)
|
|
13
|
+
# ship the .so/.dylib/.dll and disable extconf.rb entirely.
|
|
14
|
+
class Recipe < MiniPortileCMake
|
|
15
|
+
# Pinned libpng source URL + sha256. Bump deliberately to refresh
|
|
16
|
+
# the upstream — and remember to update both fields together.
|
|
17
|
+
LIBPNG_URL = "https://downloads.sourceforge.net/project/libpng/libpng16/#{Libpng::LIBPNG_VERSION}/libpng-#{Libpng::LIBPNG_VERSION}.tar.gz".freeze
|
|
18
|
+
# sha256 of the libpng-X.Y.Z.tar.gz tarball. Verify with:
|
|
19
|
+
# curl -sL <URL> | shasum -a 256
|
|
20
|
+
LIBPNG_SHA256 = '8c9b05b675ca7301a458df2c2e46f26e1d41ff36b8863f8c33530bc58c2e6225'.freeze
|
|
21
|
+
|
|
22
|
+
ROOT = Pathname.new(File.expand_path('../..', __dir__))
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
super('libpng', Libpng::LIBPNG_VERSION)
|
|
26
|
+
|
|
27
|
+
@files << {
|
|
28
|
+
url: LIBPNG_URL,
|
|
29
|
+
sha256: LIBPNG_SHA256
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@target = ROOT.join(@target).to_s
|
|
33
|
+
@printed = {}
|
|
34
|
+
setup_cross_compile if cross_compile?
|
|
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 shared lib lives under ports/<name>/<ver>/.
|
|
73
|
+
# On Linux/macOS that's lib/. On Windows, CMake's GNUInstallDirs puts
|
|
74
|
+
# the .dll in bin/ and the import library (.dll.a) in lib/ — we only
|
|
75
|
+
# ship the .dll, so search both.
|
|
76
|
+
libs = Dir.glob(File.join(port_path, shared_lib_install_glob))
|
|
77
|
+
raise "no libpng shared lib produced under #{port_path}" if libs.empty?
|
|
78
|
+
|
|
79
|
+
target_dir = ROOT.join('lib', 'libpng')
|
|
80
|
+
FileUtils.mkdir_p(target_dir)
|
|
81
|
+
FileUtils.cp_r(libs, target_dir, verbose: true)
|
|
82
|
+
|
|
83
|
+
verify_libs
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def verify_libs
|
|
87
|
+
each_built_lib do |path|
|
|
88
|
+
out, st = Open3.capture2("file #{path}")
|
|
89
|
+
raise "Failed to query file #{path}: #{out}" unless st.exitstatus.zero?
|
|
90
|
+
|
|
91
|
+
next if target_format.eql?('skip')
|
|
92
|
+
|
|
93
|
+
raise "Invalid file format '#{out}', /#{target_format.source}/ expected" unless target_format.match?(out)
|
|
94
|
+
|
|
95
|
+
message("Verifying #{path} ... OK\n")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def execute(action, command, command_opts = {})
|
|
100
|
+
super(action, command, command_opts.merge(debug: false))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def message(text)
|
|
104
|
+
return super unless text.start_with?("\rDownloading")
|
|
105
|
+
|
|
106
|
+
match = text.match(/(\rDownloading .*)\(\s*\d+%\)/)
|
|
107
|
+
pattern = match ? match[1] : text
|
|
108
|
+
return if @printed[pattern]
|
|
109
|
+
|
|
110
|
+
@printed[pattern] = true
|
|
111
|
+
super
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def port_path
|
|
117
|
+
File.join(@target, 'ports', "#{name}-#{version}") # MiniPortile default
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def shared_lib_glob
|
|
121
|
+
if MiniPortile.windows?
|
|
122
|
+
'libpng16*.dll'
|
|
123
|
+
elsif MiniPortile.darwin?
|
|
124
|
+
'libpng16*.dylib'
|
|
125
|
+
else
|
|
126
|
+
'libpng16.so*'
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Glob (with port_path prefix) for the freshly installed shared lib.
|
|
131
|
+
# On Windows the .dll installs to bin/; on Unix-likes it stays in lib/.
|
|
132
|
+
def shared_lib_install_glob
|
|
133
|
+
if MiniPortile.windows?
|
|
134
|
+
'{bin,lib}/libpng16*.dll'
|
|
135
|
+
else
|
|
136
|
+
"lib/#{shared_lib_glob}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def each_built_lib(&block)
|
|
141
|
+
Dir.glob(ROOT.join('lib', 'libpng', shared_lib_glob)).each(&block)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def host_platform
|
|
145
|
+
@host_platform ||=
|
|
146
|
+
case @host
|
|
147
|
+
when /\Ax86_64.*mingw32/
|
|
148
|
+
'x64-mingw32'
|
|
149
|
+
when /\A(aarch64|arm64).*mingw/
|
|
150
|
+
'aarch64-mingw-ucrt'
|
|
151
|
+
when /\Ax86_64.*linux-musl/
|
|
152
|
+
'x86_64-linux-musl'
|
|
153
|
+
when /\A(aarch64|arm64).*linux-musl/
|
|
154
|
+
'aarch64-linux-musl'
|
|
155
|
+
when /\Ax86_64.*linux/
|
|
156
|
+
'x86_64-linux'
|
|
157
|
+
when /\A(aarch64|arm64).*linux/
|
|
158
|
+
'aarch64-linux'
|
|
159
|
+
when /\Ax86_64.*(darwin|macos|osx)/
|
|
160
|
+
'x86_64-darwin'
|
|
161
|
+
when /\A(arm64|aarch64).*(darwin|macos|osx)/
|
|
162
|
+
'arm64-darwin'
|
|
163
|
+
else
|
|
164
|
+
@host
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def target_platform
|
|
169
|
+
@target_platform ||=
|
|
170
|
+
case ENV.fetch('target_platform', nil)
|
|
171
|
+
when /\A(arm64|aarch64).*(darwin|macos|osx)/
|
|
172
|
+
'arm64-darwin'
|
|
173
|
+
when /\Ax86_64.*(darwin|macos|osx)/
|
|
174
|
+
'x86_64-darwin'
|
|
175
|
+
when /\A(arm64|aarch64).*linux-musl/
|
|
176
|
+
'aarch64-linux-musl'
|
|
177
|
+
when /\A(arm64|aarch64).*linux-ohos/
|
|
178
|
+
# OHOS (OpenHarmony / Huawei HarmonyOS PC) is musl-based arm64.
|
|
179
|
+
# The resulting binary is ELF64 aarch64 linked against musl,
|
|
180
|
+
# identical at the file-format level to aarch64-linux-musl --
|
|
181
|
+
# only the gem's platform label differs so RubyGems on OHOS
|
|
182
|
+
# selects the right variant.
|
|
183
|
+
'aarch64-linux-ohos'
|
|
184
|
+
when /\A(arm64|aarch64).*linux/
|
|
185
|
+
'aarch64-linux'
|
|
186
|
+
when /\Ax86_64.*linux-musl/
|
|
187
|
+
'x86_64-linux-musl'
|
|
188
|
+
else
|
|
189
|
+
ENV.fetch('target_platform', host_platform)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def cross_compile?
|
|
194
|
+
target_platform != host_platform
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Configure MiniPortile + CMake for cross-compilation. Native builds
|
|
198
|
+
# (host_platform == target_platform) skip this entirely.
|
|
199
|
+
def setup_cross_compile
|
|
200
|
+
# All targeted platforms now have native runners (ubuntu-24.04-arm for
|
|
201
|
+
# aarch64-linux, windows-11-arm for aarch64-mingw-ucrt, Alpine containers
|
|
202
|
+
# for the musl variants). This hook is kept as a seam for future
|
|
203
|
+
# cross-compile targets (e.g. aarch64-linux on an x86_64 host).
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def cpu_type
|
|
207
|
+
case target_platform
|
|
208
|
+
when 'aarch64-linux', 'aarch64-linux-musl', 'aarch64-linux-ohos',
|
|
209
|
+
'arm64-darwin', 'aarch64-mingw-ucrt' then 'aarch64'
|
|
210
|
+
when 'x86_64-linux', 'x86_64-linux-musl', 'x86_64-darwin', /\Ax64-mingw/ then 'x86_64'
|
|
211
|
+
else
|
|
212
|
+
super
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def cmake_system_name
|
|
217
|
+
case target_platform
|
|
218
|
+
when 'aarch64-linux', 'x86_64-linux', 'aarch64-linux-musl',
|
|
219
|
+
'x86_64-linux-musl', 'aarch64-linux-ohos' then 'Linux'
|
|
220
|
+
when 'arm64-darwin', 'x86_64-darwin' then 'Darwin'
|
|
221
|
+
when /\A(aarch64-)?mingw/, /\Ax64-mingw/ then 'Windows'
|
|
222
|
+
else
|
|
223
|
+
super
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def target_format
|
|
228
|
+
@target_format ||=
|
|
229
|
+
case target_platform
|
|
230
|
+
when 'arm64-darwin'
|
|
231
|
+
/Mach-O 64-bit dynamically linked shared library arm64/
|
|
232
|
+
when 'x86_64-darwin'
|
|
233
|
+
/Mach-O 64-bit dynamically linked shared library x86_64/
|
|
234
|
+
when 'aarch64-linux', 'aarch64-linux-musl', 'aarch64-linux-ohos'
|
|
235
|
+
/ELF 64-bit LSB shared object, ARM aarch64/
|
|
236
|
+
when 'x86_64-linux', 'x86_64-linux-musl'
|
|
237
|
+
/ELF 64-bit LSB shared object, x86-64/
|
|
238
|
+
when 'aarch64-mingw-ucrt'
|
|
239
|
+
/PE32\+ executable.*\(DLL\).*ARM64/
|
|
240
|
+
when /\Ax64-mingw(32|-ucrt)/
|
|
241
|
+
/PE32\+ executable.*\(DLL\).*x86-64/
|
|
242
|
+
else
|
|
243
|
+
'skip'
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|