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.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +93 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +90 -0
  5. data/Rakefile +31 -0
  6. data/docs/adr/0001-buffer-representation.md +12 -0
  7. data/docs/bench_history.md +11 -0
  8. data/docs/spec_map.md +16 -0
  9. data/exe/cjxl +21 -0
  10. data/exe/djxl +66 -0
  11. data/exe/jxlinfo +31 -0
  12. data/lib/jxl/basic_info.rb +16 -0
  13. data/lib/jxl/bit/field.rb +126 -0
  14. data/lib/jxl/bit/reader.rb +78 -0
  15. data/lib/jxl/bit/writer.rb +46 -0
  16. data/lib/jxl/codestream/reader.rb +68 -0
  17. data/lib/jxl/color/gamut.rb +80 -0
  18. data/lib/jxl/color/icc_profile.rb +243 -0
  19. data/lib/jxl/color/opsin.rb +28 -0
  20. data/lib/jxl/color/transfer.rb +108 -0
  21. data/lib/jxl/color/ycbcr.rb +63 -0
  22. data/lib/jxl/container/box.rb +7 -0
  23. data/lib/jxl/container/parser.rb +134 -0
  24. data/lib/jxl/container/signature.rb +19 -0
  25. data/lib/jxl/dct.rb +81 -0
  26. data/lib/jxl/decoder.rb +85 -0
  27. data/lib/jxl/encoder.rb +180 -0
  28. data/lib/jxl/entropy/ans_distribution.rb +212 -0
  29. data/lib/jxl/entropy/decoder.rb +226 -0
  30. data/lib/jxl/entropy/encoder.rb +69 -0
  31. data/lib/jxl/entropy/hybrid_uint.rb +55 -0
  32. data/lib/jxl/entropy/icc_stream.rb +293 -0
  33. data/lib/jxl/entropy/permutation.rb +64 -0
  34. data/lib/jxl/entropy/prefix_code.rb +177 -0
  35. data/lib/jxl/errors.rb +12 -0
  36. data/lib/jxl/features/noise.rb +140 -0
  37. data/lib/jxl/features/patches.rb +147 -0
  38. data/lib/jxl/features/splines.rb +206 -0
  39. data/lib/jxl/features/spot_colour.rb +29 -0
  40. data/lib/jxl/features/upsampling.rb +99 -0
  41. data/lib/jxl/features/upsampling_weights.bin +0 -0
  42. data/lib/jxl/filter/epf.rb +119 -0
  43. data/lib/jxl/filter/gaborish.rb +36 -0
  44. data/lib/jxl/frame/toc.rb +31 -0
  45. data/lib/jxl/headers/bit_depth.rb +32 -0
  46. data/lib/jxl/headers/colour_encoding.rb +81 -0
  47. data/lib/jxl/headers/custom_transform.rb +46 -0
  48. data/lib/jxl/headers/frame_header.rb +251 -0
  49. data/lib/jxl/headers/image_metadata.rb +128 -0
  50. data/lib/jxl/headers/size_header.rb +29 -0
  51. data/lib/jxl/image.rb +63 -0
  52. data/lib/jxl/io/npy.rb +82 -0
  53. data/lib/jxl/io/pam.rb +14 -0
  54. data/lib/jxl/io/pfm.rb +21 -0
  55. data/lib/jxl/io/pgx.rb +22 -0
  56. data/lib/jxl/io/png.rb +132 -0
  57. data/lib/jxl/io/ppm.rb +63 -0
  58. data/lib/jxl/modular/decoder.rb +371 -0
  59. data/lib/jxl/modular/group_header.rb +82 -0
  60. data/lib/jxl/modular/ma_tree.rb +95 -0
  61. data/lib/jxl/modular/predictor.rb +107 -0
  62. data/lib/jxl/modular/stream.rb +49 -0
  63. data/lib/jxl/modular/transform.rb +235 -0
  64. data/lib/jxl/modular/weighted.rb +85 -0
  65. data/lib/jxl/num.rb +37 -0
  66. data/lib/jxl/plane.rb +35 -0
  67. data/lib/jxl/render/blender.rb +61 -0
  68. data/lib/jxl/render/orientation.rb +42 -0
  69. data/lib/jxl/trace.rb +36 -0
  70. data/lib/jxl/vardct/ac_strategy.rb +65 -0
  71. data/lib/jxl/vardct/afv_basis.bin +0 -0
  72. data/lib/jxl/vardct/block_context_map.rb +63 -0
  73. data/lib/jxl/vardct/chroma_from_luma.rb +27 -0
  74. data/lib/jxl/vardct/coeff_order.rb +35 -0
  75. data/lib/jxl/vardct/decoder.rb +136 -0
  76. data/lib/jxl/vardct/default_dequant.bin.z +0 -0
  77. data/lib/jxl/vardct/dequant.rb +284 -0
  78. data/lib/jxl/vardct/hf_global.rb +30 -0
  79. data/lib/jxl/vardct/lf_global.rb +43 -0
  80. data/lib/jxl/vardct/lf_group.rb +227 -0
  81. data/lib/jxl/vardct/pass_group.rb +153 -0
  82. data/lib/jxl/vardct/quantizer.rb +27 -0
  83. data/lib/jxl/vardct/reconstructor.rb +294 -0
  84. data/lib/jxl/version.rb +5 -0
  85. data/lib/jxl.rb +216 -0
  86. data/tools/bench.rb +20 -0
  87. data/tools/compare_trace.rb +42 -0
  88. data/tools/conformance_report.rb +23 -0
  89. data/tools/fuzz.rb +51 -0
  90. data/tools/gen_afv_basis.rb +12 -0
  91. data/tools/gen_quant_tables.rb +11 -0
  92. data/tools/gen_upsampling_weights.rb +13 -0
  93. data/tools/libjxl_trace.patch +30 -0
  94. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ VERSION = "1.0.0"
5
+ end
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))