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,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
|