jxl 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +93 -0
- data/LICENSE.txt +21 -0
- data/README.md +90 -0
- data/Rakefile +31 -0
- data/docs/adr/0001-buffer-representation.md +12 -0
- data/docs/bench_history.md +11 -0
- data/docs/spec_map.md +16 -0
- data/exe/cjxl +21 -0
- data/exe/djxl +66 -0
- data/exe/jxlinfo +31 -0
- data/lib/jxl/basic_info.rb +16 -0
- data/lib/jxl/bit/field.rb +126 -0
- data/lib/jxl/bit/reader.rb +78 -0
- data/lib/jxl/bit/writer.rb +46 -0
- data/lib/jxl/codestream/reader.rb +68 -0
- data/lib/jxl/color/gamut.rb +80 -0
- data/lib/jxl/color/icc_profile.rb +243 -0
- data/lib/jxl/color/opsin.rb +28 -0
- data/lib/jxl/color/transfer.rb +108 -0
- data/lib/jxl/color/ycbcr.rb +63 -0
- data/lib/jxl/container/box.rb +7 -0
- data/lib/jxl/container/parser.rb +134 -0
- data/lib/jxl/container/signature.rb +19 -0
- data/lib/jxl/dct.rb +81 -0
- data/lib/jxl/decoder.rb +85 -0
- data/lib/jxl/encoder.rb +180 -0
- data/lib/jxl/entropy/ans_distribution.rb +212 -0
- data/lib/jxl/entropy/decoder.rb +226 -0
- data/lib/jxl/entropy/encoder.rb +69 -0
- data/lib/jxl/entropy/hybrid_uint.rb +55 -0
- data/lib/jxl/entropy/icc_stream.rb +293 -0
- data/lib/jxl/entropy/permutation.rb +64 -0
- data/lib/jxl/entropy/prefix_code.rb +177 -0
- data/lib/jxl/errors.rb +12 -0
- data/lib/jxl/features/noise.rb +140 -0
- data/lib/jxl/features/patches.rb +147 -0
- data/lib/jxl/features/splines.rb +206 -0
- data/lib/jxl/features/spot_colour.rb +29 -0
- data/lib/jxl/features/upsampling.rb +99 -0
- data/lib/jxl/features/upsampling_weights.bin +0 -0
- data/lib/jxl/filter/epf.rb +119 -0
- data/lib/jxl/filter/gaborish.rb +36 -0
- data/lib/jxl/frame/toc.rb +31 -0
- data/lib/jxl/headers/bit_depth.rb +32 -0
- data/lib/jxl/headers/colour_encoding.rb +81 -0
- data/lib/jxl/headers/custom_transform.rb +46 -0
- data/lib/jxl/headers/frame_header.rb +251 -0
- data/lib/jxl/headers/image_metadata.rb +128 -0
- data/lib/jxl/headers/size_header.rb +29 -0
- data/lib/jxl/image.rb +63 -0
- data/lib/jxl/io/npy.rb +82 -0
- data/lib/jxl/io/pam.rb +14 -0
- data/lib/jxl/io/pfm.rb +21 -0
- data/lib/jxl/io/pgx.rb +22 -0
- data/lib/jxl/io/png.rb +132 -0
- data/lib/jxl/io/ppm.rb +63 -0
- data/lib/jxl/modular/decoder.rb +371 -0
- data/lib/jxl/modular/group_header.rb +82 -0
- data/lib/jxl/modular/ma_tree.rb +95 -0
- data/lib/jxl/modular/predictor.rb +107 -0
- data/lib/jxl/modular/stream.rb +49 -0
- data/lib/jxl/modular/transform.rb +235 -0
- data/lib/jxl/modular/weighted.rb +85 -0
- data/lib/jxl/num.rb +37 -0
- data/lib/jxl/plane.rb +35 -0
- data/lib/jxl/render/blender.rb +61 -0
- data/lib/jxl/render/orientation.rb +42 -0
- data/lib/jxl/trace.rb +36 -0
- data/lib/jxl/vardct/ac_strategy.rb +65 -0
- data/lib/jxl/vardct/afv_basis.bin +0 -0
- data/lib/jxl/vardct/block_context_map.rb +63 -0
- data/lib/jxl/vardct/chroma_from_luma.rb +27 -0
- data/lib/jxl/vardct/coeff_order.rb +35 -0
- data/lib/jxl/vardct/decoder.rb +136 -0
- data/lib/jxl/vardct/default_dequant.bin.z +0 -0
- data/lib/jxl/vardct/dequant.rb +284 -0
- data/lib/jxl/vardct/hf_global.rb +30 -0
- data/lib/jxl/vardct/lf_global.rb +43 -0
- data/lib/jxl/vardct/lf_group.rb +227 -0
- data/lib/jxl/vardct/pass_group.rb +153 -0
- data/lib/jxl/vardct/quantizer.rb +27 -0
- data/lib/jxl/vardct/reconstructor.rb +294 -0
- data/lib/jxl/version.rb +5 -0
- data/lib/jxl.rb +216 -0
- data/tools/bench.rb +20 -0
- data/tools/compare_trace.rb +42 -0
- data/tools/conformance_report.rb +23 -0
- data/tools/fuzz.rb +51 -0
- data/tools/gen_afv_basis.rb +12 -0
- data/tools/gen_quant_tables.rb +11 -0
- data/tools/gen_upsampling_weights.rb +13 -0
- data/tools/libjxl_trace.patch +30 -0
- metadata +135 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module VarDCT
|
|
5
|
+
module Reconstructor
|
|
6
|
+
QUANT_BIASES = [0.945349926692846, 0.9299455010825141, 0.9500648966626564, 0.145].freeze
|
|
7
|
+
AFV_BASIS_PATH = File.join(__dir__, "afv_basis.bin")
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def reconstruct(frame, global, lf_group, pass_group, quant_biases: QUANT_BIASES)
|
|
12
|
+
width = frame.blocks_width * 8
|
|
13
|
+
height = frame.blocks_height * 8
|
|
14
|
+
output = frame.chroma_subsampling.map { |hshift, vshift| Plane.new(width >> hshift, height >> vshift) }
|
|
15
|
+
pass_group.blocks.each do |block|
|
|
16
|
+
pixels = reconstruct_block(frame, global, lf_group, block, quant_biases)
|
|
17
|
+
pixels.each_with_index do |channel, c|
|
|
18
|
+
hshift, vshift = frame.chroma_subsampling[c]
|
|
19
|
+
next unless block.x.nobits?((1 << hshift) - 1) && block.y.nobits?((1 << vshift) - 1)
|
|
20
|
+
|
|
21
|
+
blit!(channel, output[c], (block.x >> hshift) * 8, (block.y >> vshift) * 8)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
output.each_with_index.map do |plane, channel|
|
|
25
|
+
hshift, vshift = frame.chroma_subsampling[channel]
|
|
26
|
+
crop(plane, Num.ceil_div(frame.encoded_width, 1 << hshift),
|
|
27
|
+
Num.ceil_div(frame.encoded_height, 1 << vshift))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reconstruct_block(frame, global, lf_group, block, quant_biases)
|
|
32
|
+
coefficients = dequantize(frame, global, lf_group, block, quant_biases)
|
|
33
|
+
coefficients.each.map do |canonical|
|
|
34
|
+
plane_from(transform(canonical, block.strategy), block.strategy.width, block.strategy.height)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
private_class_method :reconstruct_block
|
|
38
|
+
|
|
39
|
+
def transform(coefficients, strategy)
|
|
40
|
+
return identity(coefficients) if strategy.id == 1
|
|
41
|
+
return dct2x2(coefficients) if strategy.id == 2
|
|
42
|
+
return dct4x4(coefficients) if strategy.id == 3
|
|
43
|
+
return dct4x8(coefficients, vertical: true) if strategy.id == 12
|
|
44
|
+
return dct4x8(coefficients, vertical: false) if strategy.id == 13
|
|
45
|
+
return afv(coefficients, strategy.id - 14) if strategy.id.between?(14, 17)
|
|
46
|
+
|
|
47
|
+
physical = from_canonical(coefficients, strategy.width, strategy.height)
|
|
48
|
+
JXL::DCT.idct2d(physical, strategy.width, strategy.height)
|
|
49
|
+
end
|
|
50
|
+
private_class_method :transform
|
|
51
|
+
|
|
52
|
+
def identity(coefficients)
|
|
53
|
+
output = Array.new(64, 0.0)
|
|
54
|
+
dcs = hadamard4(coefficients[0], coefficients[1], coefficients[8], coefficients[9])
|
|
55
|
+
2.times do |y|
|
|
56
|
+
2.times do |x|
|
|
57
|
+
dc = dcs[(y * 2) + x]
|
|
58
|
+
residuals = Array.new(4) do |iy|
|
|
59
|
+
Array.new(4) { |ix| coefficients[((y + (iy * 2)) * 8) + x + (ix * 2)] }
|
|
60
|
+
end
|
|
61
|
+
center = dc - ((residuals.flatten.sum - residuals[0][0]) / 16.0)
|
|
62
|
+
4.times do |iy|
|
|
63
|
+
4.times { |ix| output[((((y * 4) + iy) * 8) + (x * 4)) + ix] = residuals[iy][ix] + center }
|
|
64
|
+
end
|
|
65
|
+
output[(((((y * 4) + 1) * 8) + (x * 4)) + 1)] = center
|
|
66
|
+
output[(y * 4 * 8) + (x * 4)] = coefficients[((y + 2) * 8) + x + 2] + center
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
output
|
|
70
|
+
end
|
|
71
|
+
private_class_method :identity
|
|
72
|
+
|
|
73
|
+
def dct2x2(coefficients)
|
|
74
|
+
values = coefficients.dup
|
|
75
|
+
[2, 4, 8].each do |size|
|
|
76
|
+
half = size / 2
|
|
77
|
+
output = values.dup
|
|
78
|
+
half.times do |y|
|
|
79
|
+
half.times do |x|
|
|
80
|
+
transformed = hadamard4(values[(y * 8) + x], values[(y * 8) + half + x],
|
|
81
|
+
values[((y + half) * 8) + x], values[((y + half) * 8) + half + x])
|
|
82
|
+
transformed.each_with_index do |value, index|
|
|
83
|
+
output[(((((y * 2) + (index / 2)) * 8) + (x * 2)) + (index % 2))] = value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
values = output
|
|
88
|
+
end
|
|
89
|
+
values
|
|
90
|
+
end
|
|
91
|
+
private_class_method :dct2x2
|
|
92
|
+
|
|
93
|
+
def dct4x4(coefficients)
|
|
94
|
+
output = Array.new(64, 0.0)
|
|
95
|
+
dcs = hadamard4(coefficients[0], coefficients[1], coefficients[8], coefficients[9])
|
|
96
|
+
2.times do |y|
|
|
97
|
+
2.times do |x|
|
|
98
|
+
block = Array.new(16, 0.0)
|
|
99
|
+
block[0] = dcs[(y * 2) + x]
|
|
100
|
+
4.times do |iy|
|
|
101
|
+
4.times do |ix|
|
|
102
|
+
next if ix.zero? && iy.zero?
|
|
103
|
+
|
|
104
|
+
block[(iy * 4) + ix] = coefficients[((y + (iy * 2)) * 8) + x + (ix * 2)]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
blit_values!(JXL::DCT.idct2d(from_canonical(block, 4, 4), 4, 4),
|
|
108
|
+
output, 4, 4, 8, x * 4, y * 4)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
output
|
|
112
|
+
end
|
|
113
|
+
private_class_method :dct4x4
|
|
114
|
+
|
|
115
|
+
def dct4x8(coefficients, vertical:)
|
|
116
|
+
output = Array.new(64, 0.0)
|
|
117
|
+
dcs = [coefficients[0] + coefficients[8], coefficients[0] - coefficients[8]]
|
|
118
|
+
2.times do |part|
|
|
119
|
+
block = Array.new(32, 0.0)
|
|
120
|
+
block[0] = dcs[part]
|
|
121
|
+
4.times do |iy|
|
|
122
|
+
8.times do |ix|
|
|
123
|
+
next if ix.zero? && iy.zero?
|
|
124
|
+
|
|
125
|
+
block[(iy * 8) + ix] = coefficients[((part + (iy * 2)) * 8) + ix]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
pixels = JXL::DCT.idct2d(block, 8, 4)
|
|
129
|
+
if vertical
|
|
130
|
+
blit_values!(pixels, output, 8, 4, 8, 0, part * 4)
|
|
131
|
+
else
|
|
132
|
+
transposed = Array.new(32) { |index| pixels[((index % 4) * 8) + (index / 4)] }
|
|
133
|
+
blit_values!(transposed, output, 4, 8, 8, part * 4, 0)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
output
|
|
137
|
+
end
|
|
138
|
+
private_class_method :dct4x8
|
|
139
|
+
|
|
140
|
+
def afv(coefficients, kind)
|
|
141
|
+
output = Array.new(64, 0.0)
|
|
142
|
+
corner_x = kind & 1
|
|
143
|
+
corner_y = kind / 2
|
|
144
|
+
dcs = [(coefficients[0] + coefficients[8] + coefficients[1]) * 4.0,
|
|
145
|
+
coefficients[0] + coefficients[8] - coefficients[1], coefficients[0] - coefficients[8]]
|
|
146
|
+
corner_coefficients = Array.new(16) do |index|
|
|
147
|
+
index.zero? ? dcs[0] : coefficients[((index / 4) * 16) + ((index % 4) * 2)]
|
|
148
|
+
end
|
|
149
|
+
corner = Array.new(16) do |pixel|
|
|
150
|
+
16.times.sum { |coefficient| corner_coefficients[coefficient] * afv_basis[(coefficient * 16) + pixel] }
|
|
151
|
+
end
|
|
152
|
+
4.times do |y|
|
|
153
|
+
4.times do |x|
|
|
154
|
+
source_x = corner_x == 1 ? 3 - x : x
|
|
155
|
+
source_y = corner_y == 1 ? 3 - y : y
|
|
156
|
+
output[((y + (corner_y * 4)) * 8) + (corner_x * 4) + x] = corner[(source_y * 4) + source_x]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
side = Array.new(16, 0.0)
|
|
161
|
+
side[0] = dcs[1]
|
|
162
|
+
4.times do |y|
|
|
163
|
+
4.times do |x|
|
|
164
|
+
side[(y * 4) + x] = coefficients[(y * 16) + (x * 2) + 1] unless x.zero? && y.zero?
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
blit_values!(JXL::DCT.idct2d(from_canonical(side, 4, 4), 4, 4),
|
|
168
|
+
output, 4, 4, 8, corner_x == 1 ? 0 : 4, corner_y * 4)
|
|
169
|
+
|
|
170
|
+
opposite = Array.new(32, 0.0)
|
|
171
|
+
opposite[0] = dcs[2]
|
|
172
|
+
4.times do |y|
|
|
173
|
+
8.times do |x|
|
|
174
|
+
opposite[(y * 8) + x] = coefficients[((1 + (y * 2)) * 8) + x] unless x.zero? && y.zero?
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
blit_values!(JXL::DCT.idct2d(opposite, 8, 4), output, 8, 4, 8, 0, corner_y == 1 ? 0 : 4)
|
|
178
|
+
output
|
|
179
|
+
end
|
|
180
|
+
private_class_method :afv
|
|
181
|
+
|
|
182
|
+
def afv_basis = @afv_basis ||= File.binread(AFV_BASIS_PATH).unpack("e*").freeze
|
|
183
|
+
private_class_method :afv_basis
|
|
184
|
+
|
|
185
|
+
def hadamard4(a, b, c, d) = [a + b + c + d, a + b - c - d, a - b + c - d, a - b - c + d]
|
|
186
|
+
private_class_method :hadamard4
|
|
187
|
+
|
|
188
|
+
def blit_values!(source, target, width, height, stride, x, y)
|
|
189
|
+
height.times { |row| target[((y + row) * stride) + x, width] = source[row * width, width] }
|
|
190
|
+
end
|
|
191
|
+
private_class_method :blit_values!
|
|
192
|
+
|
|
193
|
+
def dequantize(frame, global, lf_group, block, quant_biases)
|
|
194
|
+
strategy = block.strategy
|
|
195
|
+
scale = global.quantizer.inverse_ac(lf_group.quant_field[block.x, block.y])
|
|
196
|
+
channel_scales = [1.25**(2 - frame.x_qm_scale), 1.0, 1.25**(2 - frame.b_qm_scale)]
|
|
197
|
+
values = block.coefficients.each_with_index.map do |quantized, channel|
|
|
198
|
+
matrix = global.dequant.matrix(strategy, channel)
|
|
199
|
+
quantized.each_with_index.map do |value, index|
|
|
200
|
+
adjust_bias(value, channel, quant_biases) * matrix[index] * scale * channel_scales[channel]
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
if frame.max_hshift.zero? && frame.max_vshift.zero?
|
|
204
|
+
add_ac_correlation!(values, global.chroma_from_luma, lf_group, block)
|
|
205
|
+
end
|
|
206
|
+
3.times do |channel|
|
|
207
|
+
hshift, vshift = frame.chroma_subsampling[channel]
|
|
208
|
+
add_dc!(values[channel], lf_group.dc[channel], block, hshift, vshift)
|
|
209
|
+
end
|
|
210
|
+
values
|
|
211
|
+
end
|
|
212
|
+
private_class_method :dequantize
|
|
213
|
+
|
|
214
|
+
def adjust_bias(value, channel, biases)
|
|
215
|
+
return 0.0 if value.zero?
|
|
216
|
+
return value.negative? ? -biases[channel] : biases[channel] if value.abs == 1
|
|
217
|
+
|
|
218
|
+
value - (biases[3] / value)
|
|
219
|
+
end
|
|
220
|
+
private_class_method :adjust_bias
|
|
221
|
+
|
|
222
|
+
def add_ac_correlation!(values, correlation, lf_group, block)
|
|
223
|
+
tile_x = block.x / 8
|
|
224
|
+
tile_y = block.y / 8
|
|
225
|
+
x_factor = correlation.x_ratio(lf_group.ytox_map[tile_x, tile_y])
|
|
226
|
+
b_factor = correlation.b_ratio(lf_group.ytob_map[tile_x, tile_y])
|
|
227
|
+
values[1].length.times do |index|
|
|
228
|
+
values[0][index] += values[1][index] * x_factor
|
|
229
|
+
values[2][index] += values[1][index] * b_factor
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
private_class_method :add_ac_correlation!
|
|
233
|
+
|
|
234
|
+
def add_dc!(coefficients, dc, block, hshift = 0, vshift = 0)
|
|
235
|
+
strategy = block.strategy
|
|
236
|
+
block_x = block.x >> hshift
|
|
237
|
+
block_y = block.y >> vshift
|
|
238
|
+
if strategy.blocks_x == 1 && strategy.blocks_y == 1
|
|
239
|
+
coefficients[0] = dc[block_x, block_y]
|
|
240
|
+
return
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
samples = Array.new(strategy.blocks_x * strategy.blocks_y) do |index|
|
|
244
|
+
dc[block_x + (index % strategy.blocks_x), block_y + (index / strategy.blocks_x)]
|
|
245
|
+
end
|
|
246
|
+
low = JXL::DCT.dct2d(samples, strategy.blocks_x, strategy.blocks_y)
|
|
247
|
+
strategy.blocks_y.times do |fy|
|
|
248
|
+
strategy.blocks_x.times do |fx|
|
|
249
|
+
value = low[(fy * strategy.blocks_x) + fx]
|
|
250
|
+
value *= JXL::DCT.resample_scale(strategy.blocks_x, strategy.width, fx)
|
|
251
|
+
value *= JXL::DCT.resample_scale(strategy.blocks_y, strategy.height, fy)
|
|
252
|
+
set_canonical!(coefficients, strategy.width, strategy.height, fx, fy, value)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
private_class_method :add_dc!
|
|
257
|
+
|
|
258
|
+
def set_canonical!(values, width, height, x, y, value)
|
|
259
|
+
values[width > height ? (y * width) + x : (x * height) + y] = value
|
|
260
|
+
end
|
|
261
|
+
private_class_method :set_canonical!
|
|
262
|
+
|
|
263
|
+
def from_canonical(values, width, height)
|
|
264
|
+
return values if width > height
|
|
265
|
+
|
|
266
|
+
Array.new(values.length) do |index|
|
|
267
|
+
x = index % width
|
|
268
|
+
y = index / width
|
|
269
|
+
values[(x * height) + y]
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
private_class_method :from_canonical
|
|
273
|
+
|
|
274
|
+
def plane_from(values, width, height)
|
|
275
|
+
Plane.new(width, height).tap { _1.data.replace(values) }
|
|
276
|
+
end
|
|
277
|
+
private_class_method :plane_from
|
|
278
|
+
|
|
279
|
+
def blit!(source, target, x, y)
|
|
280
|
+
source.height.times do |row|
|
|
281
|
+
target.data[((y + row) * target.width) + x, source.width] = source.data[row * source.width, source.width]
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
private_class_method :blit!
|
|
285
|
+
|
|
286
|
+
def crop(source, width, height)
|
|
287
|
+
target = Plane.new(width, height)
|
|
288
|
+
height.times { |y| target.data[y * width, width] = source.data[y * source.width, width] }
|
|
289
|
+
target
|
|
290
|
+
end
|
|
291
|
+
private_class_method :crop
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
data/lib/jxl/version.rb
ADDED
data/lib/jxl.rb
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "jxl/version"
|
|
4
|
+
require_relative "jxl/errors"
|
|
5
|
+
require_relative "jxl/num"
|
|
6
|
+
require_relative "jxl/plane"
|
|
7
|
+
require_relative "jxl/image"
|
|
8
|
+
require_relative "jxl/trace"
|
|
9
|
+
require_relative "jxl/io/npy"
|
|
10
|
+
require_relative "jxl/io/ppm"
|
|
11
|
+
require_relative "jxl/io/pam"
|
|
12
|
+
require_relative "jxl/io/pfm"
|
|
13
|
+
require_relative "jxl/io/pgx"
|
|
14
|
+
require_relative "jxl/io/png"
|
|
15
|
+
require_relative "jxl/dct"
|
|
16
|
+
require_relative "jxl/container/signature"
|
|
17
|
+
require_relative "jxl/container/box"
|
|
18
|
+
require_relative "jxl/container/parser"
|
|
19
|
+
require_relative "jxl/bit/reader"
|
|
20
|
+
require_relative "jxl/bit/writer"
|
|
21
|
+
require_relative "jxl/bit/field"
|
|
22
|
+
require_relative "jxl/entropy/hybrid_uint"
|
|
23
|
+
require_relative "jxl/entropy/prefix_code"
|
|
24
|
+
require_relative "jxl/entropy/encoder"
|
|
25
|
+
require_relative "jxl/entropy/ans_distribution"
|
|
26
|
+
require_relative "jxl/entropy/decoder"
|
|
27
|
+
require_relative "jxl/entropy/icc_stream"
|
|
28
|
+
require_relative "jxl/entropy/permutation"
|
|
29
|
+
require_relative "jxl/vardct/ac_strategy"
|
|
30
|
+
require_relative "jxl/vardct/dequant"
|
|
31
|
+
require_relative "jxl/vardct/quantizer"
|
|
32
|
+
require_relative "jxl/vardct/block_context_map"
|
|
33
|
+
require_relative "jxl/vardct/chroma_from_luma"
|
|
34
|
+
require_relative "jxl/modular/ma_tree"
|
|
35
|
+
require_relative "jxl/modular/predictor"
|
|
36
|
+
require_relative "jxl/modular/weighted"
|
|
37
|
+
require_relative "jxl/modular/group_header"
|
|
38
|
+
require_relative "jxl/modular/transform"
|
|
39
|
+
require_relative "jxl/modular/decoder"
|
|
40
|
+
require_relative "jxl/modular/stream"
|
|
41
|
+
require_relative "jxl/vardct/lf_global"
|
|
42
|
+
require_relative "jxl/vardct/lf_group"
|
|
43
|
+
require_relative "jxl/vardct/coeff_order"
|
|
44
|
+
require_relative "jxl/vardct/hf_global"
|
|
45
|
+
require_relative "jxl/vardct/pass_group"
|
|
46
|
+
require_relative "jxl/vardct/reconstructor"
|
|
47
|
+
require_relative "jxl/color/opsin"
|
|
48
|
+
require_relative "jxl/color/transfer"
|
|
49
|
+
require_relative "jxl/color/ycbcr"
|
|
50
|
+
require_relative "jxl/color/icc_profile"
|
|
51
|
+
require_relative "jxl/color/gamut"
|
|
52
|
+
require_relative "jxl/filter/gaborish"
|
|
53
|
+
require_relative "jxl/filter/epf"
|
|
54
|
+
require_relative "jxl/features/upsampling"
|
|
55
|
+
require_relative "jxl/features/noise"
|
|
56
|
+
require_relative "jxl/features/patches"
|
|
57
|
+
require_relative "jxl/features/splines"
|
|
58
|
+
require_relative "jxl/features/spot_colour"
|
|
59
|
+
require_relative "jxl/render/blender"
|
|
60
|
+
require_relative "jxl/render/orientation"
|
|
61
|
+
require_relative "jxl/vardct/decoder"
|
|
62
|
+
require_relative "jxl/headers/size_header"
|
|
63
|
+
require_relative "jxl/headers/bit_depth"
|
|
64
|
+
require_relative "jxl/headers/colour_encoding"
|
|
65
|
+
require_relative "jxl/headers/image_metadata"
|
|
66
|
+
require_relative "jxl/headers/custom_transform"
|
|
67
|
+
require_relative "jxl/headers/frame_header"
|
|
68
|
+
require_relative "jxl/frame/toc"
|
|
69
|
+
require_relative "jxl/basic_info"
|
|
70
|
+
require_relative "jxl/codestream/reader"
|
|
71
|
+
require_relative "jxl/encoder"
|
|
72
|
+
require_relative "jxl/decoder"
|
|
73
|
+
|
|
74
|
+
module JXL
|
|
75
|
+
module_function
|
|
76
|
+
|
|
77
|
+
# Encodes an image as a lossless Modular JPEG XL codestream.
|
|
78
|
+
def encode(image, **) = Encoder.new(image, **).encode
|
|
79
|
+
|
|
80
|
+
# Reads basic image metadata without decoding pixels.
|
|
81
|
+
def info(data, **) = BasicInfo.read(data, **)
|
|
82
|
+
|
|
83
|
+
# Decodes a JPEG XL codestream or container into an Image.
|
|
84
|
+
def decode(data, pixel_format: :native, color_space: :original, desired_scale: 1,
|
|
85
|
+
max_pixels: 100_000_000, max_memory: 2 << 30, strict: true,
|
|
86
|
+
apply_orientation: true, render_spot_colors: true) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/ParameterLists
|
|
87
|
+
validate_options!(pixel_format, color_space, desired_scale)
|
|
88
|
+
frames = Codestream::Reader.new(data, max_pixels:).read_all_headers
|
|
89
|
+
enforce_memory_limit!(frames.first.basic_info, max_memory)
|
|
90
|
+
references = Array.new(4)
|
|
91
|
+
dc_frames = Array.new(4)
|
|
92
|
+
output = nil
|
|
93
|
+
displayed = []
|
|
94
|
+
frames.each do |parsed|
|
|
95
|
+
frame = parsed.frame_header
|
|
96
|
+
raw = if frame.encoding == :vardct
|
|
97
|
+
VarDCT::Decoder.decode(parsed, references:, dc_frames:)
|
|
98
|
+
else
|
|
99
|
+
Modular::Decoder.decode_parsed(parsed, render: true, references:)
|
|
100
|
+
end
|
|
101
|
+
if frame.type == 1
|
|
102
|
+
dc_frames[frame.dc_level - 1] = raw
|
|
103
|
+
next
|
|
104
|
+
end
|
|
105
|
+
image = frame.encoding == :modular ? convert_modular_colour(raw, parsed) : raw
|
|
106
|
+
if frame.type == 2
|
|
107
|
+
references[frame.save_as_reference] = frame.save_before_colour_transform ? raw : image
|
|
108
|
+
else
|
|
109
|
+
composed = Render::Blender.compose(image, frame, references, parsed.basic_info.width, parsed.basic_info.height)
|
|
110
|
+
if !frame.last && (frame.duration.zero? || !frame.save_as_reference.zero?)
|
|
111
|
+
references[frame.save_as_reference] = frame.save_before_colour_transform ? raw : composed
|
|
112
|
+
end
|
|
113
|
+
rendered = render_spot_colors ? Features::SpotColour.apply(composed) : composed
|
|
114
|
+
output = apply_orientation ? Render::Orientation.apply(rendered) : rendered
|
|
115
|
+
if !frame.duration.zero? || frame.last
|
|
116
|
+
displayed << Frame.new(planes: output.channels, duration: frame.duration, timecode: frame.timecode,
|
|
117
|
+
name: frame.name, blend_info: frame.blend)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
output ||= raise(CorruptError, "codestream has no display frame")
|
|
122
|
+
encoding = frames.first.basic_info.metadata.colour_encoding
|
|
123
|
+
profile = if encoding.want_icc && frames.first.frame_header.colour_transform == :xyb
|
|
124
|
+
Color::ICCProfile.create(encoding.with(want_icc: false, transfer_function: 8))
|
|
125
|
+
else
|
|
126
|
+
frames.first.icc_profile || Color::ICCProfile.create(encoding)
|
|
127
|
+
end
|
|
128
|
+
output = output.with(frames: displayed.freeze, icc_profile: profile)
|
|
129
|
+
output = convert_output_colour(output, color_space)
|
|
130
|
+
output = downsample(output, desired_scale) unless desired_scale == 1
|
|
131
|
+
validate_samples!(output, strict)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def validate_options!(pixel_format, color_space, desired_scale)
|
|
135
|
+
raise ArgumentError, "invalid pixel format" unless %i[native rgba8 rgba16 float32].include?(pixel_format)
|
|
136
|
+
raise ArgumentError, "invalid color space" unless %i[original srgb linear_srgb].include?(color_space)
|
|
137
|
+
raise ArgumentError, "invalid desired scale" unless [1, 2, 4, 8].include?(desired_scale)
|
|
138
|
+
end
|
|
139
|
+
private_class_method :validate_options!
|
|
140
|
+
|
|
141
|
+
def enforce_memory_limit!(basic, limit)
|
|
142
|
+
return unless limit
|
|
143
|
+
|
|
144
|
+
channels = 3 + basic.metadata.extra_channels.length
|
|
145
|
+
estimate = basic.width * basic.height * channels * 64
|
|
146
|
+
raise ResourceLimitError, "decoded image exceeds memory limit" if estimate > limit
|
|
147
|
+
end
|
|
148
|
+
private_class_method :enforce_memory_limit!
|
|
149
|
+
|
|
150
|
+
def convert_output_colour(image, target)
|
|
151
|
+
return image if target == :original
|
|
152
|
+
|
|
153
|
+
encoding = image.metadata.colour_encoding
|
|
154
|
+
channels = image.channels.map do |source|
|
|
155
|
+
Plane.new(source.width, source.height).tap { |plane| plane.data.replace(source.data) }
|
|
156
|
+
end
|
|
157
|
+
if encoding.want_icc
|
|
158
|
+
Color::Transfer.from_linear!(channels, 13) if target == :srgb
|
|
159
|
+
return image.with(channels:)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
intensity = image.metadata.tone_mapping.intensity_target
|
|
163
|
+
Color::Transfer.to_linear!(channels, encoding.transfer_function, gamma: encoding.gamma,
|
|
164
|
+
intensity_target: intensity)
|
|
165
|
+
Color::Gamut.to_srgb!(channels, encoding)
|
|
166
|
+
Color::Transfer.from_linear!(channels, 13) if target == :srgb
|
|
167
|
+
image.with(channels:)
|
|
168
|
+
end
|
|
169
|
+
private_class_method :convert_output_colour
|
|
170
|
+
|
|
171
|
+
def downsample(image, scale)
|
|
172
|
+
width = Num.ceil_div(image.width, scale)
|
|
173
|
+
height = Num.ceil_div(image.height, scale)
|
|
174
|
+
channels = image.channels.map do |source|
|
|
175
|
+
Plane.new(width, height).tap do |target|
|
|
176
|
+
height.times do |y|
|
|
177
|
+
width.times do |x|
|
|
178
|
+
target[x, y] = source[[x * scale, source.width - 1].min, [y * scale, source.height - 1].min]
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
image.with(width:, height:, channels:)
|
|
184
|
+
end
|
|
185
|
+
private_class_method :downsample
|
|
186
|
+
|
|
187
|
+
def validate_samples!(image, strict)
|
|
188
|
+
image.channels.each do |plane|
|
|
189
|
+
plane.data.map! do |value|
|
|
190
|
+
next value if value.finite?
|
|
191
|
+
raise CorruptError, "non-finite decoded sample" if strict
|
|
192
|
+
|
|
193
|
+
0.0
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
image
|
|
197
|
+
end
|
|
198
|
+
private_class_method :validate_samples!
|
|
199
|
+
|
|
200
|
+
def convert_modular_colour(image, parsed)
|
|
201
|
+
return image unless parsed.frame_header.colour_transform == :xyb
|
|
202
|
+
|
|
203
|
+
opsin = parsed.transform_data.opsin
|
|
204
|
+
channels = Color::Opsin.to_linear(image.channels, opsin,
|
|
205
|
+
intensity_target: image.metadata.tone_mapping.intensity_target)
|
|
206
|
+
encoding = image.metadata.colour_encoding
|
|
207
|
+
return image.with(channels:) if encoding.want_icc
|
|
208
|
+
|
|
209
|
+
Color::Transfer.from_linear!(
|
|
210
|
+
channels, encoding.transfer_function, gamma: encoding.gamma,
|
|
211
|
+
intensity_target: image.metadata.tone_mapping.intensity_target
|
|
212
|
+
)
|
|
213
|
+
image.with(channels:)
|
|
214
|
+
end
|
|
215
|
+
private_class_method :convert_modular_colour
|
|
216
|
+
end
|
data/tools/bench.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/jxl"
|
|
5
|
+
|
|
6
|
+
root = ENV.fetch("JXL_CORPUS", "/private/tmp/jxl-conformance/testcases")
|
|
7
|
+
names = ENV.fetch("JXL_BENCH_CASES", "alpha_triangles,grayscale,lz77_flower").split(",")
|
|
8
|
+
puts "case seconds MP/s"
|
|
9
|
+
puts "---------------------------- -------- ---------"
|
|
10
|
+
names.each do |name|
|
|
11
|
+
path = File.join(root, name, "input.jxl")
|
|
12
|
+
next unless File.file?(path)
|
|
13
|
+
|
|
14
|
+
data = File.binread(path)
|
|
15
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
16
|
+
image = JXL.decode(data)
|
|
17
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
18
|
+
rate = image.width * image.height / elapsed / 1_000_000.0
|
|
19
|
+
puts format("%<name>-28s %<elapsed>8.3f %<rate>9.3f", name:, elapsed:, rate:)
|
|
20
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
unless ARGV.length == 2
|
|
5
|
+
warn "usage: #{$PROGRAM_NAME} REFERENCE ACTUAL"
|
|
6
|
+
exit 2
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
reference_path, actual_path = ARGV
|
|
10
|
+
reference = File.foreach(reference_path)
|
|
11
|
+
actual = File.foreach(actual_path)
|
|
12
|
+
context = []
|
|
13
|
+
line_number = 0
|
|
14
|
+
|
|
15
|
+
loop do
|
|
16
|
+
expected = begin
|
|
17
|
+
reference.next
|
|
18
|
+
rescue StopIteration
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
received = begin
|
|
22
|
+
actual.next
|
|
23
|
+
rescue StopIteration
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
break if expected.nil? && received.nil?
|
|
27
|
+
|
|
28
|
+
line_number += 1
|
|
29
|
+
if expected != received
|
|
30
|
+
warn "mismatch at line #{line_number}"
|
|
31
|
+
warn "reference: #{expected&.chomp || '<end of file>'}"
|
|
32
|
+
warn "actual: #{received&.chomp || '<end of file>'}"
|
|
33
|
+
warn "previous #{context.length} lines:"
|
|
34
|
+
context.each { |line| warn " #{line.chomp}" }
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
context << expected
|
|
39
|
+
context.shift if context.length > 20
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
puts "matched #{line_number} lines"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
path = ENV.fetch("JXL_CONFORMANCE_RESULTS") do
|
|
7
|
+
abort "set JXL_CONFORMANCE_RESULTS to the official conformance runner JSON"
|
|
8
|
+
end
|
|
9
|
+
results = JSON.parse(File.read(path))
|
|
10
|
+
|
|
11
|
+
puts "test case | status | max RMSE | peak"
|
|
12
|
+
puts "------------------------------+--------+-----------+-----------"
|
|
13
|
+
results.each do |result|
|
|
14
|
+
comparisons = result.filter_map { |key, value| value if key.match?(/\Aframe\d+_compare_npy\z/) }
|
|
15
|
+
rms = comparisons.filter_map { _1["actual_rmse"] }.max
|
|
16
|
+
peak = comparisons.filter_map { _1["actual_peak_error"] }.max
|
|
17
|
+
values = { name: result.fetch("test_id"), status: result["success"] ? "pass" : "FAIL",
|
|
18
|
+
rms: rms ? format("%.3g", rms) : "-", peak: peak ? format("%.3g", peak) : "-" }
|
|
19
|
+
puts format("%<name>-30s| %<status>-7s| %<rms>9s | %<peak>9s", **values)
|
|
20
|
+
end
|
|
21
|
+
passed = results.count { _1["success"] }
|
|
22
|
+
puts format("%<name>-30s| %<passed>d/%<total>d", name: "", passed:, total: results.length)
|
|
23
|
+
exit 1 unless passed == results.length
|
data/tools/fuzz.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "timeout"
|
|
5
|
+
require_relative "../lib/jxl"
|
|
6
|
+
|
|
7
|
+
mode = ARGV.fetch(0) { abort "usage: tools/fuzz.rb truncate|bitflip|box" }
|
|
8
|
+
root = ENV.fetch("JXL_CORPUS", "/private/tmp/jxl-conformance/testcases")
|
|
9
|
+
files = Dir.glob(File.join(root, "**", "input.jxl"))
|
|
10
|
+
abort "no JPEG XL inputs under #{root}" if files.empty?
|
|
11
|
+
random = Random.new(Integer(ENV.fetch("SEED", "1"), 10))
|
|
12
|
+
|
|
13
|
+
def exercise(bytes, label)
|
|
14
|
+
Timeout.timeout(Integer(ENV.fetch("FUZZ_TIMEOUT", "30"), 10)) { JXL.decode(bytes, strict: false) }
|
|
15
|
+
rescue JXL::Error
|
|
16
|
+
nil
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
warn "#{label}: #{e.class}: #{e.message}"
|
|
19
|
+
raise
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
case mode
|
|
23
|
+
when "truncate"
|
|
24
|
+
files.each do |path|
|
|
25
|
+
source = File.binread(path)
|
|
26
|
+
200.times.map { source.bytesize * _1 / 200 }.uniq.each do |length|
|
|
27
|
+
exercise(source.byteslice(0, length), "#{path}:#{length}")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
when "bitflip"
|
|
31
|
+
Integer(ENV.fetch("FUZZ_ITERATIONS", "10000"), 10).times do |index|
|
|
32
|
+
path = files[index % files.length]
|
|
33
|
+
bytes = File.binread(path)
|
|
34
|
+
position = random.rand(bytes.bytesize)
|
|
35
|
+
bytes.setbyte(position, bytes.getbyte(position) ^ (1 << random.rand(8)))
|
|
36
|
+
exercise(bytes, "#{path}:#{index}")
|
|
37
|
+
end
|
|
38
|
+
when "box"
|
|
39
|
+
files.each do |path|
|
|
40
|
+
source = File.binread(path)
|
|
41
|
+
next unless JXL::Container::Signature.detect(source) == :container
|
|
42
|
+
|
|
43
|
+
[0, 1, 7, 8, 0xFFFF_FFFF].each do |size|
|
|
44
|
+
bytes = source.dup
|
|
45
|
+
bytes[0, 4] = [size].pack("N")
|
|
46
|
+
exercise(bytes, "#{path}:box-size-#{size}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
else abort "unknown fuzz mode: #{mode}"
|
|
50
|
+
end
|
|
51
|
+
puts "#{mode}: #{files.length} inputs passed"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source, target = ARGV
|
|
4
|
+
abort "usage: ruby tools/gen_afv_basis.rb dec_transforms-inl.h TARGET" unless source && target
|
|
5
|
+
|
|
6
|
+
body = File.read(source)[/k4x4AFVBasis\[16\]\[16\] = \{(.*?)\n \};/m, 1]
|
|
7
|
+
abort "AFV basis was not found" unless body
|
|
8
|
+
|
|
9
|
+
values = body.scan(/[-+]?(?:\d+\.\d*|\.\d+)(?:[eE][-+]?\d+)?/).map(&:to_f)
|
|
10
|
+
abort "expected 256 AFV basis values, got #{values.length}" unless values.length == 256
|
|
11
|
+
|
|
12
|
+
File.binwrite(target, values.pack("e*"))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
source, target = ARGV
|
|
6
|
+
abort "usage: ruby tools/gen_quant_tables.rb DUMP TARGET" unless source && target
|
|
7
|
+
|
|
8
|
+
bytes = File.binread(source)
|
|
9
|
+
abort "quantization dump must contain matching matrix and inverse-matrix halves" unless (bytes.bytesize % 8).zero?
|
|
10
|
+
|
|
11
|
+
File.binwrite(target, Zlib::Deflate.deflate(bytes.byteslice(0, bytes.bytesize / 2), Zlib::BEST_COMPRESSION))
|