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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Modular
5
+ StreamState = Data.define(:tree, :entropy)
6
+ StreamResult = Data.define(:channels, :header)
7
+
8
+ module Stream
9
+ module_function
10
+
11
+ def read_global_state(reader, max_nodes: 1 << 20)
12
+ return StreamState.new(tree: nil, entropy: nil) unless Bit::Field.read_bool(reader)
13
+
14
+ tree = MATree.read(reader, max_nodes:)
15
+ StreamState.new(tree:, entropy: Entropy::Decoder.read(reader, tree.leaf_count, defer_state: true))
16
+ end
17
+
18
+ def decode(reader, shapes, state:, stream_id:, metadata:, max_nodes: 1 << 20)
19
+ return StreamResult.new(channels: [], header: nil) if shapes.empty?
20
+
21
+ original_shapes = shapes.map(&:dup)
22
+ header = GroupHeader.read(reader)
23
+ shapes, = TransformOps.encoded_shapes_for(shapes.map(&:dup), header.transforms)
24
+ tree, entropy = code(reader, header, state, shapes, max_nodes)
25
+ decoder = entropy.fork(reader, distance_multiplier: shapes.map(&:first).max)
26
+ channels = Decoder.decode_channels(decoder, tree, header.weighted, shapes, stream_id)
27
+ decoder.final_state!
28
+ Decoder.inverse_transforms!(channels, header, metadata)
29
+ unless channels.map { [_1.width, _1.height] } == original_shapes
30
+ raise CorruptError, "modular transforms changed the requested stream shape"
31
+ end
32
+
33
+ StreamResult.new(channels:, header:)
34
+ end
35
+
36
+ def code(reader, header, state, shapes, max_nodes)
37
+ if header.use_global_tree
38
+ raise CorruptError, "missing global MA tree" unless state.tree && state.entropy
39
+
40
+ return [state.tree, state.entropy]
41
+ end
42
+
43
+ limit = [max_nodes, 1024 + shapes.sum { |width, height| width * height }].min
44
+ tree = MATree.read(reader, max_nodes: limit)
45
+ [tree, Entropy::Decoder.read(reader, tree.leaf_count, defer_state: true)]
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Modular
5
+ module TransformOps
6
+ module_function
7
+
8
+ DELTAS = [
9
+ [0, 0, 0], [4, 4, 4], [11, 0, 0], [0, 0, -13], [0, -12, 0], [-10, -10, -10],
10
+ [-18, -18, -18], [-27, -27, -27], [-18, -18, 0], [0, 0, -32], [-32, 0, 0], [-37, -37, -37],
11
+ [0, -32, -32], [24, 24, 45], [50, 50, 50], [-45, -24, -24], [-24, -45, -45], [0, -24, -24],
12
+ [-34, -34, 0], [-24, 0, -24], [-45, -45, -24], [64, 64, 64], [-32, 0, -32], [0, -32, 0],
13
+ [-32, 0, 32], [-24, -45, -24], [45, 24, 45], [24, -24, -45], [-45, -24, 24], [80, 80, 80],
14
+ [64, 0, 0], [0, 0, -64], [0, -64, -64], [-24, -24, 45], [96, 96, 96], [64, 64, 0],
15
+ [45, -24, -24], [34, -34, 0], [112, 112, 112], [24, -45, -45], [45, 45, -24], [0, -32, 32],
16
+ [24, -24, 45], [0, 96, 96], [45, -24, 24], [24, -45, -24], [-24, -45, 24], [0, -64, 0],
17
+ [96, 0, 0], [128, 128, 128], [64, 0, 64], [144, 144, 144], [96, 96, 0], [-36, -36, 36],
18
+ [45, -24, -45], [45, -45, -24], [0, 0, -96], [0, 128, 128], [0, 96, 0], [45, 24, -45],
19
+ [-128, 0, 0], [24, -45, 24], [-45, 24, -45], [64, 0, -64], [64, -64, -64], [96, 0, 96],
20
+ [45, -45, 24], [24, 45, -45], [64, 64, -64], [128, 128, 0], [0, 0, -128], [-24, 45, -45]
21
+ ].freeze
22
+
23
+ def encoded_shapes(width, height, count, transforms)
24
+ shapes = Array.new(count) { [width, height] }
25
+ shifts = Array.new(count) { [0, 0] }
26
+ encoded_shapes_for(shapes, transforms, shifts:)
27
+ end
28
+
29
+ def encoded_shapes_for(shapes, transforms, shifts: Array.new(shapes.length) { [0, 0] }, meta: 0)
30
+ transforms.each do |transform|
31
+ case transform.id
32
+ when 0 then validate_range!(shapes, transform.begin_channel, 3)
33
+ when 1 then meta = meta_palette!(shapes, shifts, meta, transform)
34
+ when 2 then meta = meta_squeeze!(shapes, shifts, meta, transform)
35
+ end
36
+ end
37
+ [shapes, meta, shifts]
38
+ end
39
+
40
+ def inverse!(channels, transform, weighted_header, bit_depth)
41
+ case transform.id
42
+ when 1 then inverse_palette!(channels, transform, weighted_header, bit_depth)
43
+ when 2 then inverse_squeeze!(channels, transform.squeezes)
44
+ end
45
+ end
46
+
47
+ def meta_palette!(shapes, shifts, meta, transform)
48
+ validate_range!(shapes, transform.begin_channel, transform.num_channels)
49
+ base = shapes.fetch(transform.begin_channel)
50
+ transform.num_channels.times do |i|
51
+ raise CorruptError, "palette channel dimensions differ" unless shapes[transform.begin_channel + i] == base
52
+ end
53
+ shapes.slice!(transform.begin_channel + 1, transform.num_channels - 1)
54
+ shifts.slice!(transform.begin_channel + 1, transform.num_channels - 1)
55
+ shapes.unshift([transform.num_colors + transform.num_deltas, transform.num_channels])
56
+ shifts.unshift([-1, -1])
57
+ transform.begin_channel >= meta ? meta + 1 : meta + 2 - transform.num_channels
58
+ end
59
+ private_class_method :meta_palette!
60
+
61
+ def meta_squeeze!(shapes, shifts, meta, transform)
62
+ transform.squeezes.replace(default_squeezes(shapes, meta)) if transform.squeezes.empty?
63
+ transform.squeezes.each do |squeeze|
64
+ validate_range!(shapes, squeeze.begin_channel, squeeze.num_channels)
65
+ finish = squeeze.begin_channel + squeeze.num_channels - 1
66
+ offset = squeeze.in_place ? finish + 1 : shapes.length
67
+ squeeze.num_channels.times do |i|
68
+ width, height = shapes.fetch(squeeze.begin_channel + i)
69
+ hshift, vshift = shifts.fetch(squeeze.begin_channel + i)
70
+ average = squeeze.horizontal ? [(width + 1) / 2, height] : [width, (height + 1) / 2]
71
+ residual = squeeze.horizontal ? [width / 2, height] : [width, height / 2]
72
+ hshift += 1 if squeeze.horizontal && hshift >= 0
73
+ vshift += 1 if !squeeze.horizontal && vshift >= 0
74
+ shapes[squeeze.begin_channel + i] = average
75
+ shifts[squeeze.begin_channel + i] = [hshift, vshift]
76
+ shapes.insert(offset + i, residual)
77
+ shifts.insert(offset + i, [hshift, vshift])
78
+ end
79
+ meta += squeeze.num_channels if squeeze.begin_channel < meta
80
+ end
81
+ meta
82
+ end
83
+ private_class_method :meta_squeeze!
84
+
85
+ def default_squeezes(shapes, meta)
86
+ count = shapes.length - meta
87
+ width, height = shapes.fetch(meta)
88
+ result = []
89
+ if count > 2 && shapes[meta + 1] == [width, height]
90
+ result << Squeeze.new(horizontal: true, in_place: false, begin_channel: meta + 1, num_channels: 2)
91
+ result << Squeeze.new(horizontal: false, in_place: false, begin_channel: meta + 1, num_channels: 2)
92
+ end
93
+ params = { in_place: true, begin_channel: meta, num_channels: count }
94
+ if width <= height && height > 8
95
+ result << Squeeze.new(horizontal: false, **params)
96
+ height = (height + 1) / 2
97
+ end
98
+ while width > 8 || height > 8
99
+ if width > 8
100
+ result << Squeeze.new(horizontal: true, **params)
101
+ width = (width + 1) / 2
102
+ end
103
+ if height > 8
104
+ result << Squeeze.new(horizontal: false, **params)
105
+ height = (height + 1) / 2
106
+ end
107
+ end
108
+ result
109
+ end
110
+ private_class_method :default_squeezes
111
+
112
+ def inverse_palette!(channels, transform, weighted_header, bit_depth)
113
+ palette = channels.fetch(0)
114
+ index = channels.fetch(transform.begin_channel + 1)
115
+ outputs = Array.new(transform.num_channels) { Plane.new(index.width, index.height) }
116
+ outputs.each_with_index do |output, channel|
117
+ weighted = Weighted.new(weighted_header, output.width) if transform.predictor == 6
118
+ output.height.times do |y|
119
+ output.width.times do |x|
120
+ palette_index = index[x, y]
121
+ value = palette_value(palette, palette_index, channel, [bit_depth, 24].min)
122
+ if palette_index < transform.num_deltas
123
+ guess = weighted&.predict(output, x, y) || Predictor.predict(transform.predictor, output, x, y)
124
+ value += guess
125
+ end
126
+ output[x, y] = value
127
+ weighted&.update(value, x)
128
+ end
129
+ end
130
+ end
131
+ channels[transform.begin_channel + 1] = outputs.shift
132
+ channels.insert(transform.begin_channel + 2, *outputs)
133
+ channels.shift
134
+ end
135
+ private_class_method :inverse_palette!
136
+
137
+ def palette_value(palette, index, channel, bit_depth)
138
+ if index.negative?
139
+ entry = -(index + 1) % (1 + (2 * (DELTAS.length - 1)))
140
+ value = DELTAS[(entry + 1) >> 1].fetch(channel, 0) * (entry.odd? ? 1 : -1)
141
+ return bit_depth > 8 ? value << (bit_depth - 8) : value
142
+ end
143
+ if index >= palette.width + 64
144
+ value = (index - palette.width - 64) / (5**channel) % 5
145
+ return (value * ((1 << bit_depth) - 1)) >> 2 if channel < 3
146
+ elsif index >= palette.width
147
+ return 0 if channel >= 3
148
+
149
+ value = ((index - palette.width) >> (channel * 2)) & 3
150
+ return ((value * ((1 << bit_depth) - 1)) >> 2) + (1 << [bit_depth - 3, 0].max)
151
+ end
152
+ palette[index, channel]
153
+ end
154
+ private_class_method :palette_value
155
+
156
+ def inverse_squeeze!(channels, squeezes)
157
+ squeezes.reverse_each do |squeeze|
158
+ finish = squeeze.begin_channel + squeeze.num_channels - 1
159
+ offset = squeeze.in_place ? finish + 1 : channels.length + squeeze.begin_channel - finish - 1
160
+ squeeze.num_channels.times do |i|
161
+ channel = squeeze.begin_channel + i
162
+ channels[channel] = unsqueeze(channels.fetch(channel), channels.fetch(offset + i), squeeze.horizontal)
163
+ end
164
+ channels.slice!(offset, squeeze.num_channels)
165
+ end
166
+ end
167
+ private_class_method :inverse_squeeze!
168
+
169
+ def unsqueeze(average, residual, horizontal)
170
+ output = Plane.new(horizontal ? average.width + residual.width : average.width,
171
+ horizontal ? average.height : average.height + residual.height)
172
+ horizontal ? unsqueeze_horizontal!(output, average, residual) : unsqueeze_vertical!(output, average, residual)
173
+ output
174
+ end
175
+ private_class_method :unsqueeze
176
+
177
+ def unsqueeze_horizontal!(output, average, residual)
178
+ average.height.times do |y|
179
+ residual.width.times do |x|
180
+ avg = average[x, y]
181
+ following = x + 1 < average.width ? average[x + 1, y] : avg
182
+ tendency = smooth_tendency(x.zero? ? avg : output[(x * 2) - 1, y], avg, following)
183
+ diff = residual[x, y] + tendency
184
+ first = avg + Num.cdiv(diff, 2)
185
+ output[x * 2, y] = first
186
+ output[(x * 2) + 1, y] = first - diff
187
+ end
188
+ output[output.width - 1, y] = average[average.width - 1, y] if output.width.odd?
189
+ end
190
+ end
191
+ private_class_method :unsqueeze_horizontal!
192
+
193
+ def unsqueeze_vertical!(output, average, residual)
194
+ residual.height.times do |y|
195
+ average.width.times do |x|
196
+ avg = average[x, y]
197
+ following = y + 1 < average.height ? average[x, y + 1] : avg
198
+ tendency = smooth_tendency(y.zero? ? avg : output[x, (y * 2) - 1], avg, following)
199
+ diff = residual[x, y] + tendency
200
+ first = avg + Num.cdiv(diff, 2)
201
+ output[x, y * 2] = first
202
+ output[x, (y * 2) + 1] = first - diff
203
+ end
204
+ end
205
+ return unless output.height.odd?
206
+
207
+ average.width.times { |x| output[x, output.height - 1] = average[x, average.height - 1] }
208
+ end
209
+ private_class_method :unsqueeze_vertical!
210
+
211
+ def smooth_tendency(previous, average, following)
212
+ if average.between?(following, previous)
213
+ diff = Num.cdiv((4 * previous) - (3 * following) - average + 6, 12)
214
+ diff = (2 * (previous - average)) + 1 if diff - (diff & 1) > 2 * (previous - average)
215
+ diff = 2 * (average - following) if diff + (diff & 1) > 2 * (average - following)
216
+ diff
217
+ elsif average.between?(previous, following)
218
+ diff = Num.cdiv((4 * previous) - (3 * following) - average - 6, 12)
219
+ diff = (2 * (previous - average)) - 1 if diff + (diff & 1) < 2 * (previous - average)
220
+ diff = 2 * (average - following) if diff - (diff & 1) < 2 * (average - following)
221
+ diff
222
+ else
223
+ 0
224
+ end
225
+ end
226
+ private_class_method :smooth_tendency
227
+
228
+ def validate_range!(channels, first, count)
229
+ valid = first && count.positive? && first + count <= channels.length
230
+ raise CorruptError, "transform channel range" unless valid
231
+ end
232
+ private_class_method :validate_range!
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Modular
5
+ class Weighted
6
+ DIV_LOOKUP = Array.new(64) { |i| (1 << 24) / (i + 1) }.freeze
7
+
8
+ attr_reader :property
9
+
10
+ def initialize(header, width)
11
+ @header = header
12
+ @width = width
13
+ @stride = width + 2
14
+ @pred_errors = Array.new(4) { Array.new(@stride * 2, 0) }
15
+ @errors = Array.new(@stride * 2, 0)
16
+ end
17
+
18
+ def predict(plane, x, y) # rubocop:disable Metrics/AbcSize
19
+ n = Predictor.neighbors(plane, x, y)
20
+ positions(x, y)
21
+ weights = @pred_errors.each_with_index.map do |errors, i|
22
+ sum = Num.u32(errors[@pos_n] + errors[@pos_ne] + errors[@pos_nw])
23
+ error_weight(sum, @header.weights[i])
24
+ end
25
+
26
+ north, west, northeast, northwest, northnorth =
27
+ %i[top left topright topleft toptop].map { n.fetch(_1) << 3 }
28
+ error_west = x.zero? ? 0 : @errors[@current_row + x - 1]
29
+ error_north = @errors[@pos_n]
30
+ error_northwest = @errors[@pos_nw]
31
+ error_northeast = @errors[@pos_ne]
32
+ @property = [error_west, error_north, error_northwest, error_northeast].max_by(&:abs)
33
+
34
+ sum = error_north + error_west
35
+ p = @header.p
36
+ @predictions = [
37
+ west + northeast - north,
38
+ north - (((sum + error_northeast) * p[0]) >> 5),
39
+ west - (((sum + error_northwest) * p[1]) >> 5),
40
+ north - (((error_northwest * p[2]) + (error_north * p[3]) + (error_northeast * p[4]) +
41
+ ((northnorth - north) * p[5]) + ((northwest - west) * p[6])) >> 5)
42
+ ]
43
+ @prediction = weighted_average(weights)
44
+ same_sign = ((error_north ^ error_west) | (error_north ^ error_northwest)).positive?
45
+ return (@prediction + 3) >> 3 if same_sign
46
+
47
+ @prediction = @prediction.clamp([west, northeast, north].min, [west, northeast, north].max)
48
+ (@prediction + 3) >> 3
49
+ end
50
+
51
+ def update(value, x)
52
+ scaled = value << 3
53
+ @errors[@current_row + x] = Num.i32(@prediction - scaled)
54
+ @pred_errors.each_with_index do |errors, i|
55
+ error = Num.u32(((@predictions[i] - scaled).abs + 3) >> 3)
56
+ errors[@current_row + x] = error
57
+ errors[@previous_row + x + 1] = Num.u32(errors[@previous_row + x + 1] + error)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def positions(x, y)
64
+ @current_row = y.odd? ? 0 : @stride
65
+ @previous_row = y.odd? ? @stride : 0
66
+ @pos_n = @previous_row + x
67
+ @pos_ne = x < @width - 1 ? @pos_n + 1 : @pos_n
68
+ @pos_nw = x.positive? ? @pos_n - 1 : @pos_n
69
+ end
70
+
71
+ def error_weight(error, max_weight)
72
+ shift = [(error + 1).bit_length - 6, 0].max
73
+ 4 + ((max_weight * DIV_LOOKUP[error >> shift]) >> shift)
74
+ end
75
+
76
+ def weighted_average(weights)
77
+ log_weight = weights.sum.bit_length - 1
78
+ weights.map! { _1 >> (log_weight - 4) }
79
+ sum = weights.sum
80
+ ((weights.each_with_index.sum { |weight, i| @predictions[i] * weight } + (sum >> 1) - 1) *
81
+ DIV_LOOKUP[sum - 1]) >> 24
82
+ end
83
+ end
84
+ end
85
+ end
data/lib/jxl/num.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Num
5
+ module_function
6
+
7
+ def cdiv(a, b)
8
+ quotient = a.abs / b.abs
9
+ a.negative? == b.negative? ? quotient : -quotient
10
+ end
11
+
12
+ def sar(value, bits) = value >> bits
13
+ def clamp(value, low, high) = value.clamp(low, high)
14
+ def u32(value) = value & 0xFFFF_FFFF
15
+
16
+ def i32(value)
17
+ value = u32(value)
18
+ value >= 0x8000_0000 ? value - 0x1_0000_0000 : value
19
+ end
20
+
21
+ def f32(value) = [value].pack("f").unpack1("f")
22
+ def unpack_signed(value) = (value >> 1) ^ -(value & 1)
23
+ def pack_signed(value) = value.negative? ? ((-value << 1) - 1) : (value << 1)
24
+
25
+ def ceil_log2(value)
26
+ raise ArgumentError, "value must be positive" unless value.positive?
27
+
28
+ (value - 1).bit_length
29
+ end
30
+
31
+ def ceil_div(value, divisor)
32
+ raise ArgumentError, "divisor must be positive" unless divisor.positive?
33
+
34
+ value.div(divisor) + (value.modulo(divisor).zero? ? 0 : 1)
35
+ end
36
+ end
37
+ end
data/lib/jxl/plane.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ class Plane
5
+ attr_reader :width, :height, :data
6
+
7
+ def initialize(width, height, fill = 0)
8
+ unless valid_dimension?(width) && valid_dimension?(height)
9
+ raise ArgumentError, "dimensions must be non-negative integers"
10
+ end
11
+
12
+ @width = width
13
+ @height = height
14
+ @data = Array.new(width * height, fill)
15
+ end
16
+
17
+ def [](x, y) = @data.fetch(index(x, y))
18
+
19
+ def []=(x, y, value)
20
+ @data[index(x, y)] = value
21
+ end
22
+
23
+ def raw = @data
24
+
25
+ private
26
+
27
+ def valid_dimension?(value) = value.is_a?(Integer) && !value.negative?
28
+
29
+ def index(x, y)
30
+ raise IndexError, "pixel outside plane" unless x.between?(0, @width - 1) && y.between?(0, @height - 1)
31
+
32
+ (y * @width) + x
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Render
5
+ module Blender
6
+ MODES = [1, 2, 4, 6, 3].freeze
7
+
8
+ module_function
9
+
10
+ def compose(foreground, frame, references, width, height)
11
+ colour_channels = foreground.channels.length - foreground.metadata.extra_channels.length
12
+ infos = Array.new(colour_channels, frame.blend) + frame.extra_channel_blends
13
+ background = infos.each_with_index.map do |info, channel|
14
+ source = references[info.source]&.channels&.[](channel)
15
+ copy_plane(source, width, height)
16
+ end
17
+ blends = infos.map do |info|
18
+ Features::PatchBlend.new(mode: MODES.fetch(info.mode), alpha_channel: info.alpha_channel,
19
+ clamp: info.clamp)
20
+ end
21
+ composite_region!(background, foreground.channels, blends, foreground.metadata,
22
+ frame.origin_x, frame.origin_y, colour_channels)
23
+ Image.new(width:, height:, channels: background, metadata: foreground.metadata)
24
+ end
25
+
26
+ def copy_plane(source, width, height)
27
+ output = Plane.new(width, height, 0.0)
28
+ return output unless source
29
+
30
+ rows = [height, source.height].min
31
+ columns = [width, source.width].min
32
+ rows.times { |y| output.data[y * width, columns] = source.data[y * source.width, columns] }
33
+ output
34
+ end
35
+ private_class_method :copy_plane
36
+
37
+ def composite_region!(background, foreground, blends, metadata, origin_x, origin_y, colour_channels)
38
+ foreground.first.height.times do |y|
39
+ target_y = origin_y + y
40
+ next unless target_y.between?(0, background.first.height - 1)
41
+
42
+ foreground.first.width.times do |x|
43
+ target_x = origin_x + x
44
+ next unless target_x.between?(0, background.first.width - 1)
45
+
46
+ target = (target_y * background.first.width) + target_x
47
+ source = (y * foreground.first.width) + x
48
+ old = background.map { _1.data[target] }
49
+ new = foreground.map { _1.data[source] }
50
+ blends.each_index.reverse_each do |channel|
51
+ background[channel].data[target] = Features::Patches.blend_value(
52
+ old[channel], new[channel], blends[channel], old, new, metadata, colour_channels:, channel:
53
+ )
54
+ end
55
+ end
56
+ end
57
+ end
58
+ private_class_method :composite_region!
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Render
5
+ module Orientation
6
+ module_function
7
+
8
+ def apply(image)
9
+ orientation = image.metadata.orientation
10
+ return image if orientation == 1
11
+
12
+ width, height = orientation >= 5 ? [image.height, image.width] : [image.width, image.height]
13
+ channels = image.channels.map do |source|
14
+ Plane.new(width, height).tap do |target|
15
+ height.times do |y|
16
+ width.times do |x|
17
+ source_x, source_y = source_coordinates(x, y, source.width, source.height, orientation)
18
+ target[x, y] = source[source_x, source_y]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ image.with(width:, height:, channels:)
24
+ end
25
+
26
+ def source_coordinates(x, y, width, height, orientation)
27
+ case orientation
28
+ when 1 then [x, y]
29
+ when 2 then [width - 1 - x, y]
30
+ when 3 then [width - 1 - x, height - 1 - y]
31
+ when 4 then [x, height - 1 - y]
32
+ when 5 then [y, x]
33
+ when 6 then [y, height - 1 - x]
34
+ when 7 then [width - 1 - y, height - 1 - x]
35
+ when 8 then [width - 1 - y, x]
36
+ else raise CorruptError, "invalid orientation"
37
+ end
38
+ end
39
+ private_class_method :source_coordinates
40
+ end
41
+ end
42
+ end
data/lib/jxl/trace.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Trace
5
+ module_function
6
+
7
+ def br(bits, value, position) = write("BR n=#{bits} val=#{value} pos=#{position}")
8
+ def field(name, value) = write("FIELD name=#{name} val=#{value}")
9
+ def symbol(context, value) = write("SYM ctx=#{context} val=#{value}")
10
+ def token(context, value) = write_to("JXL_TOKEN_TRACE_FILE", "TOK ctx=#{context} token=#{value}")
11
+
12
+ def write(line)
13
+ return unless (path = ENV.fetch("JXL_TRACE_FILE", nil))
14
+
15
+ (@file ||= File.open(path, "w")).puts(line)
16
+ @file.flush
17
+ end
18
+
19
+ def write_to(variable, line)
20
+ return unless (path = ENV.fetch(variable, nil))
21
+
22
+ (@files ||= {})[path] ||= File.open(path, "w")
23
+ @files[path].puts(line)
24
+ @files[path].flush
25
+ end
26
+
27
+ def close
28
+ @file&.close
29
+ @file = nil
30
+ @files&.each_value(&:close)
31
+ @files = nil
32
+ end
33
+ end
34
+ end
35
+
36
+ at_exit { JXL::Trace.close }
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module VarDCT
5
+ AC_STRATEGY_NAMES = %i[
6
+ dct8x8 identity dct2x2 dct4x4 dct16x16 dct32x32 dct16x8 dct8x16 dct32x8 dct8x32 dct32x16 dct16x32
7
+ dct4x8 dct8x4 afv0 afv1 afv2 afv3 dct64x64 dct64x32 dct32x64 dct128x128 dct128x64 dct64x128
8
+ dct256x256 dct256x128 dct128x256
9
+ ].freeze
10
+ BLOCK_WIDTHS = [1, 1, 1, 1, 2, 4, 1, 2, 1, 4, 2, 4, 1, 1, 1, 1, 1, 1, 8, 4, 8, 16, 8, 16, 32, 16, 32].freeze
11
+ BLOCK_HEIGHTS = [1, 1, 1, 1, 2, 4, 2, 1, 4, 1, 4, 2, 1, 1, 1, 1, 1, 1, 8, 8, 4, 16, 16, 8, 32, 32, 16].freeze
12
+ STRATEGY_ORDERS = [0, 1, 1, 1, 2, 3, 4, 4, 5, 5, 6, 6, 1, 1, 1, 1, 1, 1, 7, 8, 8, 9, 10, 10, 11, 12, 12].freeze
13
+
14
+ AcStrategy = Data.define(:id, :name, :blocks_x, :blocks_y) do
15
+ def self.[](id)
16
+ raise CorruptError, "invalid AC strategy" unless id.between?(0, AC_STRATEGY_NAMES.length - 1)
17
+
18
+ new(id:, name: AC_STRATEGY_NAMES[id], blocks_x: BLOCK_WIDTHS[id], blocks_y: BLOCK_HEIGHTS[id])
19
+ end
20
+
21
+ def width = blocks_x * 8
22
+ def height = blocks_y * 8
23
+ def coefficient_count = width * height
24
+ def order = STRATEGY_ORDERS[id]
25
+ def log2_blocks = (blocks_x * blocks_y).bit_length - 1
26
+
27
+ def natural_order
28
+ columns, rows = [blocks_x, blocks_y].minmax.reverse
29
+ ratio = columns / rows
30
+ ratio_mask = ratio - 1
31
+ ratio_shift = ratio.bit_length - 1
32
+ result = Array.new(coefficient_count)
33
+ current = columns * rows
34
+ (0...(columns * 8)).each do |diagonal|
35
+ 0.upto(diagonal) do |j|
36
+ x, y = diagonal.odd? ? [diagonal - j, j] : [j, diagonal - j]
37
+ next unless y.nobits?(ratio_mask)
38
+
39
+ y >>= ratio_shift
40
+ value = if x < columns && y < rows
41
+ (y * columns) + x
42
+ else
43
+ current.tap { current += 1 }
44
+ end
45
+ result[value] = (y * columns * 8) + x
46
+ end
47
+ end
48
+ ((columns * 8) - 1).downto(1) do |ip|
49
+ diagonal = ip - 1
50
+ 0.upto(diagonal) do |j|
51
+ x = (columns * 8) - 1 - (diagonal - j)
52
+ y = (columns * 8) - 1 - j
53
+ x, y = y, x if diagonal.odd?
54
+ next unless y.nobits?(ratio_mask)
55
+
56
+ y >>= ratio_shift
57
+ result[current] = (y * columns * 8) + x
58
+ current += 1
59
+ end
60
+ end
61
+ result
62
+ end
63
+ end
64
+ end
65
+ end
Binary file