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,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Features
5
+ PatchReference = Data.define(:frame, :x, :y, :width, :height)
6
+ PatchBlend = Data.define(:mode, :alpha_channel, :clamp)
7
+ Patch = Data.define(:reference, :x, :y, :blends)
8
+
9
+ module Patches
10
+ module_function
11
+
12
+ def read(reader, width, height, extra_channels, references)
13
+ decoder = Entropy::Decoder.read(reader, 10, distance_multiplier: 0)
14
+ count = decoder.read_uint(0)
15
+ limit = 1024 + (width * height / 4)
16
+ raise ResourceLimitError, "too many patch references" if count > limit
17
+
18
+ ref_positions = []
19
+ patches = []
20
+ count.times do
21
+ ref = decoder.read_uint(1)
22
+ source = references[ref]
23
+ raise CorruptError, "invalid patch reference" unless source
24
+
25
+ x = decoder.read_uint(3)
26
+ y = decoder.read_uint(3)
27
+ patch_width = decoder.read_uint(2) + 1
28
+ patch_height = decoder.read_uint(2) + 1
29
+ if x + patch_width > source.width || y + patch_height > source.height
30
+ raise CorruptError, "patch exceeds reference frame"
31
+ end
32
+
33
+ reference = PatchReference.new(frame: ref, x:, y:, width: patch_width, height: patch_height)
34
+ ref_positions << reference
35
+ read_positions(decoder, patches, reference, width, height, extra_channels, limit)
36
+ end
37
+ decoder.final_state!
38
+ patches.freeze
39
+ end
40
+
41
+ def read_positions(decoder, patches, reference, width, height, extra_channels, limit)
42
+ count = decoder.read_uint(7) + 1
43
+ raise ResourceLimitError, "too many patch positions" if patches.length + count > limit
44
+
45
+ previous_x = previous_y = 0
46
+ count.times do |index|
47
+ x = index.zero? ? decoder.read_uint(4) : previous_x + Num.unpack_signed(decoder.read_uint(6))
48
+ y = index.zero? ? decoder.read_uint(4) : previous_y + Num.unpack_signed(decoder.read_uint(6))
49
+ if x.negative? || y.negative? || x + reference.width > width || y + reference.height > height
50
+ raise CorruptError, "patch exceeds target frame"
51
+ end
52
+
53
+ blends = Array.new(extra_channels + 1) { read_blend(decoder, extra_channels) }
54
+ patches << Patch.new(reference:, x:, y:, blends:)
55
+ previous_x = x
56
+ previous_y = y
57
+ end
58
+ end
59
+ private_class_method :read_positions
60
+
61
+ def read_blend(decoder, extra_channels)
62
+ mode = decoder.read_uint(5)
63
+ raise CorruptError, "invalid patch blend mode" unless mode.between?(0, 7)
64
+
65
+ uses_alpha = mode.between?(4, 7)
66
+ alpha = uses_alpha && extra_channels > 1 ? decoder.read_uint(8) : 0
67
+ raise CorruptError, "invalid patch alpha channel" if alpha >= extra_channels && uses_alpha
68
+
69
+ clamp = (uses_alpha || mode == 3) && !decoder.read_uint(9).zero?
70
+ PatchBlend.new(mode:, alpha_channel: alpha, clamp:)
71
+ end
72
+ private_class_method :read_blend
73
+
74
+ def apply(channels, patches, references, metadata)
75
+ return channels if patches.nil? || patches.empty?
76
+
77
+ patches.each do |patch|
78
+ source = references.fetch(patch.reference.frame).channels
79
+ patch.reference.height.times do |dy|
80
+ patch.reference.width.times do |dx|
81
+ target_index = ((patch.y + dy) * channels[0].width) + patch.x + dx
82
+ source_index = ((patch.reference.y + dy) * source[0].width) + patch.reference.x + dx
83
+ blend_pixel!(channels, source, target_index, source_index, patch.blends, metadata)
84
+ end
85
+ end
86
+ end
87
+ channels
88
+ end
89
+
90
+ def blend_pixel!(background, foreground, target, source, blends, metadata)
91
+ old = background.map { _1.data[target] }
92
+ new = foreground.map { _1.data[source] }
93
+ colour_channels = background.length - metadata.extra_channels.length
94
+ metadata.extra_channels.each_index do |index|
95
+ channel = colour_channels + index
96
+ background[channel].data[target] = blend_value(old[channel], new[channel], blends[1 + index],
97
+ old, new, metadata, colour_channels:, channel:)
98
+ end
99
+ colour_channels.times do |channel|
100
+ background[channel].data[target] = blend_value(old[channel], new[channel], blends[0], old, new, metadata,
101
+ colour_channels:, channel:)
102
+ end
103
+ end
104
+ private_class_method :blend_pixel!
105
+
106
+ def blend_value(old, new, info, background, foreground, metadata, colour_channels: 3, channel: nil)
107
+ alpha_index = colour_channels + info.alpha_channel
108
+ alpha = foreground[alpha_index] || 1.0
109
+ case info.mode
110
+ when 0 then old
111
+ when 1 then new
112
+ when 2 then old + new
113
+ when 3 then old * (info.clamp ? Num.clamp(new, 0.0, 1.0) : new)
114
+ when 4 then alpha_blend(old, background[alpha_index], new, alpha, metadata, info,
115
+ alpha: channel == alpha_index)
116
+ when 5 then alpha_blend(new, alpha, old, background[alpha_index], metadata, info,
117
+ alpha: channel == alpha_index)
118
+ when 6 then channel == alpha_index ? old : old + (blend_alpha(alpha, info) * new)
119
+ when 7
120
+ channel == alpha_index ? new : new + (blend_alpha(background[alpha_index] || 1.0, info) * old)
121
+ end
122
+ end
123
+
124
+ def blend(...) = blend_value(...)
125
+ private_class_method :blend
126
+
127
+ def blend_alpha(value, info) = info.clamp ? Num.clamp(value, 0.0, 1.0) : value
128
+ private_class_method :blend_alpha
129
+
130
+ def alpha_blend(bottom, bottom_alpha, top, top_alpha, metadata, info, alpha: false)
131
+ bottom_alpha ||= 1.0
132
+ top_alpha = blend_alpha(top_alpha, info)
133
+ output_alpha = bottom_alpha + (top_alpha * (1.0 - bottom_alpha))
134
+ return output_alpha if alpha
135
+ return 0.0 if output_alpha.zero?
136
+
137
+ associated = metadata.extra_channels[info.alpha_channel]&.alpha_associated
138
+ if associated
139
+ ((1.0 - top_alpha) * bottom) + top
140
+ else
141
+ (((1.0 - top_alpha) * bottom * bottom_alpha) + (top_alpha * top)) / output_alpha
142
+ end
143
+ end
144
+ private_class_method :alpha_blend
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Features
5
+ QuantizedSpline = Data.define(:deltas, :colour_dct, :sigma_dct)
6
+ SplineSet = Data.define(:quantization_adjustment, :starting_points, :splines)
7
+
8
+ module Splines # rubocop:disable Metrics/ModuleLength
9
+ CHANNEL_WEIGHTS = [0.0042, 0.075, 0.07, 0.3333].freeze
10
+ SQRT_HALF = Math.sqrt(0.5)
11
+ SQRT_TWO = Math.sqrt(2.0)
12
+
13
+ module_function
14
+
15
+ def read(reader, width, height)
16
+ decoder = Entropy::Decoder.read(reader, 6)
17
+ count = decoder.read_uint(2) + 1
18
+ limit = [1 << 20, width * height / 2].min
19
+ raise ResourceLimitError, "too many splines" if count > limit
20
+
21
+ starting_points = []
22
+ last_x = last_y = 0
23
+ count.times do |index|
24
+ dx = decoder.read_uint(1)
25
+ dy = decoder.read_uint(1)
26
+ x = index.zero? ? dx : last_x + Num.unpack_signed(dx)
27
+ y = index.zero? ? dy : last_y + Num.unpack_signed(dy)
28
+ validate_position!(x, y)
29
+ starting_points << [x.to_f, y.to_f]
30
+ last_x = x
31
+ last_y = y
32
+ end
33
+ adjustment = Num.unpack_signed(decoder.read_uint(0))
34
+ total_points = count
35
+ splines = Array.new(count) do
36
+ points = decoder.read_uint(3)
37
+ total_points += points
38
+ raise ResourceLimitError, "too many spline control points" if total_points > limit
39
+
40
+ deltas = Array.new(points) do
41
+ [Num.unpack_signed(decoder.read_uint(4)),
42
+ Num.unpack_signed(decoder.read_uint(4))]
43
+ end
44
+ colour = Array.new(3) { Array.new(32) { Num.unpack_signed(decoder.read_uint(5)) } }
45
+ sigma = Array.new(32) { Num.unpack_signed(decoder.read_uint(5)) }
46
+ QuantizedSpline.new(deltas:, colour_dct: colour, sigma_dct: sigma)
47
+ end
48
+ decoder.final_state!
49
+ SplineSet.new(quantization_adjustment: adjustment, starting_points:, splines:)
50
+ end
51
+
52
+ def apply(channels, set, correlation)
53
+ return channels unless set
54
+
55
+ set.splines.each_with_index do |quantized, index|
56
+ spline = dequantize(quantized, set.starting_points[index], set.quantization_adjustment, correlation)
57
+ draw!(channels, spline)
58
+ end
59
+ channels
60
+ end
61
+
62
+ def dequantize(quantized, start, adjustment, correlation)
63
+ inv_quant = adjustment >= 0 ? 1.0 / (1.0 + (0.125 * adjustment)) : 1.0 - (0.125 * adjustment)
64
+ points = [start]
65
+ delta_x = delta_y = 0
66
+ x, y = start.map(&:round)
67
+ quantized.deltas.each do |ddx, ddy|
68
+ delta_x += ddx
69
+ delta_y += ddy
70
+ x += delta_x
71
+ y += delta_y
72
+ validate_position!(x, y)
73
+ points << [x.to_f, y.to_f]
74
+ end
75
+ colour = quantized.colour_dct.each_with_index.map do |coefficients, channel|
76
+ coefficients.each_with_index.map do |value, index|
77
+ value * (index.zero? ? SQRT_HALF : 1.0) * CHANNEL_WEIGHTS[channel] * inv_quant
78
+ end
79
+ end
80
+ 32.times do |index|
81
+ colour[0][index] += correlation.x_ratio(0) * colour[1][index]
82
+ colour[2][index] += correlation.b_ratio(0) * colour[1][index]
83
+ end
84
+ sigma = quantized.sigma_dct.each_with_index.map do |value, index|
85
+ value * (index.zero? ? SQRT_HALF : 1.0) * CHANNEL_WEIGHTS[3] * inv_quant
86
+ end
87
+ [points, colour, sigma]
88
+ end
89
+ private_class_method :dequantize
90
+
91
+ def draw!(channels, spline)
92
+ points, colour_dct, sigma_dct = spline
93
+ curve = catmull_rom(points)
94
+ samples = equally_spaced(curve)
95
+ arc_length = ((samples.length - 2) * 1.0) + samples.last[1]
96
+ return unless arc_length.positive?
97
+
98
+ samples.each_with_index do |(point, intensity), index|
99
+ progress = [1.0, index / arc_length].min
100
+ colour = colour_dct.map { continuous_idct(_1, 31 * progress) }
101
+ sigma = continuous_idct(sigma_dct, 31 * progress)
102
+ draw_sample!(channels, point, intensity, colour, sigma)
103
+ end
104
+ end
105
+ private_class_method :draw!
106
+
107
+ def catmull_rom(control_points)
108
+ return control_points if control_points.one?
109
+
110
+ points = control_points.dup
111
+ points.unshift(add(points[0], subtract(points[0], points[1])))
112
+ points << add(points[-1], subtract(points[-1], points[-2]))
113
+ result = []
114
+ (points.length - 3).times do |start|
115
+ p = points.slice(start, 4)
116
+ result << p[1]
117
+ distances = 3.times.map { |index| Math.sqrt(Math.hypot(*subtract(p[index + 1], p[index]))) }
118
+ raise CorruptError, "identical spline control points" if distances.any?(&:zero?)
119
+
120
+ times = [0.0]
121
+ distances.each { times << (times.last + _1) }
122
+ 1.upto(15) do |step|
123
+ time = distances[0] + (step.fdiv(16) * distances[1])
124
+ a = 3.times.map do |index|
125
+ add(p[index], scale(subtract(p[index + 1], p[index]), (time - times[index]) / distances[index]))
126
+ end
127
+ b = 2.times.map do |index|
128
+ add(a[index], scale(subtract(a[index + 1], a[index]),
129
+ (time - times[index]) / (distances[index] + distances[index + 1])))
130
+ end
131
+ result << add(b[0], scale(subtract(b[1], b[0]), (time - times[1]) / distances[1]))
132
+ end
133
+ end
134
+ result << points[-2]
135
+ end
136
+ private_class_method :catmull_rom
137
+
138
+ def equally_spaced(points)
139
+ output = [[points.first, 1.0]]
140
+ current = points.first
141
+ next_index = 0
142
+ loop do
143
+ previous = current
144
+ length = 0.0
145
+ loop do
146
+ if next_index == points.length
147
+ output << [previous, length]
148
+ return output
149
+ end
150
+ target = points[next_index]
151
+ distance = Math.hypot(*subtract(target, previous))
152
+ if length + distance >= 1.0
153
+ current = add(previous, scale(subtract(target, previous), (1.0 - length) / distance))
154
+ output << [current, 1.0]
155
+ break
156
+ end
157
+ length += distance
158
+ previous = target
159
+ next_index += 1
160
+ end
161
+ end
162
+ end
163
+ private_class_method :equally_spaced
164
+
165
+ def continuous_idct(coefficients, time)
166
+ coefficients.each_with_index.sum do |value, index|
167
+ SQRT_TWO * value * Math.cos((Math::PI / 32) * index * (time + 0.5))
168
+ end
169
+ end
170
+ private_class_method :continuous_idct
171
+
172
+ def draw_sample!(channels, point, intensity, colour, sigma)
173
+ return unless sigma.finite? && !sigma.zero? && intensity.finite?
174
+
175
+ max_colour = [0.01, *colour.map { (_1 * intensity).abs }].max
176
+ distance = Math.sqrt(-2.0 * sigma * sigma * ((Math.log(0.1) * 5) - Math.log(max_colour)))
177
+ return unless distance.finite?
178
+
179
+ x0 = [0, (point[0] - distance).round].max
180
+ x1 = [channels[0].width - 1, (point[0] + distance).round].min
181
+ y0 = [0, (point[1] - distance).round].max
182
+ y1 = [channels[0].height - 1, (point[1] + distance).round].min
183
+ y0.upto(y1) do |y|
184
+ x0.upto(x1) do |x|
185
+ radius = Math.hypot(x - point[0], y - point[1])
186
+ factor = Math.erf(((radius * 0.5) + 0.353553391) / sigma) -
187
+ Math.erf(((radius * 0.5) - 0.353553391) / sigma)
188
+ local = 0.25 * sigma * intensity * factor * factor
189
+ 3.times { |channel| channels[channel][x, y] += colour[channel] * local }
190
+ end
191
+ end
192
+ end
193
+ private_class_method :draw_sample!
194
+
195
+ def validate_position!(x, y)
196
+ raise CorruptError, "spline coordinate is out of bounds" unless x.abs < (1 << 23) && y.abs < (1 << 23)
197
+ end
198
+ private_class_method :validate_position!
199
+
200
+ def add(a, b) = [a[0] + b[0], a[1] + b[1]]
201
+ def subtract(a, b) = [a[0] - b[0], a[1] - b[1]]
202
+ def scale(point, value) = [point[0] * value, point[1] * value]
203
+ private_class_method :add, :subtract, :scale
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Features
5
+ module SpotColour
6
+ module_function
7
+
8
+ def apply(image)
9
+ spots = image.metadata.extra_channels.each_index.select { image.metadata.extra_channels[_1].type == 2 }
10
+ return image if spots.empty?
11
+
12
+ colour = image.channels.first(image.num_color_channels).map do |source|
13
+ Plane.new(source.width, source.height).tap { _1.data.replace(source.data) }
14
+ end
15
+ spots.each do |extra_index|
16
+ spot = image.metadata.extra_channels[extra_index].spot_colour
17
+ samples = image.extra_channels[extra_index]
18
+ colour.each_with_index do |plane, channel|
19
+ plane.data.each_index do |pixel|
20
+ mix = spot[3] * samples.data[pixel]
21
+ plane.data[pixel] = (mix * spot[channel]) + ((1.0 - mix) * plane.data[pixel])
22
+ end
23
+ end
24
+ end
25
+ image.with(channels: colour + image.extra_channels)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Features
5
+ module Upsampling
6
+ WEIGHTS_PATH = File.join(__dir__, "upsampling_weights.bin")
7
+ COUNTS = [15, 55, 210].freeze
8
+
9
+ module_function
10
+
11
+ def apply(channels, frame, transform_data)
12
+ factor = frame.upsampling
13
+ return channels if factor == 1
14
+
15
+ channels.each_with_index.map do |channel, index|
16
+ channel_factor = index < 3 ? factor : frame.extra_channel_upsampling[index - 3]
17
+ next channel if channel_factor == 1
18
+
19
+ weight_index = channel_factor.bit_length - 2
20
+ weights = transform_data.weights[weight_index] || default_weights[weight_index]
21
+ upsample(channel, channel_factor, weights, frame.width, frame.height)
22
+ end
23
+ end
24
+
25
+ def upsample(source, factor, weights, width = source.width * factor, height = source.height * factor)
26
+ kernels = kernels(factor, weights)
27
+ output = Plane.new(width, height)
28
+ source.height.times do |y|
29
+ source.width.times do |x|
30
+ neighbourhood = Array.new(25) do |index|
31
+ sample(source, x + (index % 5) - 2, y + (index / 5) - 2)
32
+ end
33
+ minimum, maximum = neighbourhood.minmax
34
+ factor.times do |oy|
35
+ target_y = (y * factor) + oy
36
+ next if target_y >= height
37
+
38
+ factor.times do |ox|
39
+ target_x = (x * factor) + ox
40
+ next if target_x >= width
41
+
42
+ value = 25.times.sum { |index| neighbourhood[index] * kernels[(oy * factor) + ox][index] }
43
+ output[target_x, target_y] = Num.clamp(value, minimum, maximum)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ output
49
+ end
50
+
51
+ def kernels(factor, weights)
52
+ half = factor / 2
53
+ output = Array.new(factor * factor) { Array.new(25) }
54
+ half.times do |ky|
55
+ half.times do |kx|
56
+ 5.times do |py|
57
+ 5.times do |px|
58
+ i, j = [(5 * kx) + px, (5 * ky) + py].minmax
59
+ weight = weights[(5 * half * i) - (i * (i - 1) / 2) + j - i]
60
+ set_symmetric!(output, factor, kx, ky, px, py, weight)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ output
66
+ end
67
+
68
+ def set_symmetric!(kernels, factor, kx, ky, px, py, weight)
69
+ kernels[(ky * factor) + kx][(py * 5) + px] = weight
70
+ kernels[(ky * factor) + factor - 1 - kx][(py * 5) + 4 - px] = weight
71
+ kernels[((factor - 1 - ky) * factor) + kx][((4 - py) * 5) + px] = weight
72
+ kernels[((factor - 1 - ky) * factor) + factor - 1 - kx][((4 - py) * 5) + 4 - px] = weight
73
+ end
74
+ private_class_method :set_symmetric!
75
+
76
+ def default_weights
77
+ @default_weights ||= begin
78
+ values = File.binread(WEIGHTS_PATH).unpack("e*")
79
+ offset = 0
80
+ COUNTS.map { |count| values.slice(offset, count).tap { offset += count }.freeze }.freeze
81
+ end
82
+ end
83
+ private_class_method :default_weights
84
+
85
+ def sample(plane, x, y)
86
+ plane[mirror(x, plane.width), mirror(y, plane.height)]
87
+ end
88
+ private_class_method :sample
89
+
90
+ def mirror(value, size)
91
+ return 0 if size == 1
92
+
93
+ value %= 2 * size
94
+ value < size ? value : (2 * size) - value - 1
95
+ end
96
+ private_class_method :mirror
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Filter
5
+ module EPF
6
+ INV_SIGMA_NUM = -1.17157287525381
7
+ MIN_SIGMA = -3.9052429175127
8
+ PLUS = [[0, 0], [-1, 0], [0, -1], [1, 0], [0, 1]].freeze
9
+ CROSS = [[-1, 0], [0, -1], [0, 1], [1, 0]].freeze
10
+ WIDE = [[-2, 0], [-1, -1], [-1, 0], [-1, 1], [0, -2], [0, -1],
11
+ [0, 1], [0, 2], [1, -1], [1, 0], [1, 1], [2, 0]].freeze
12
+
13
+ module_function
14
+
15
+ def apply(channels, loop_filter, global, lf_group)
16
+ return channels if loop_filter.epf_iterations.zero?
17
+
18
+ sigma = sigma_map(loop_filter, global, lf_group)
19
+ apply_filter(channels, sigma, loop_filter)
20
+ end
21
+
22
+ def apply_modular(channels, loop_filter)
23
+ return channels if loop_filter.epf_iterations.zero?
24
+
25
+ sigma = Plane.new(Num.ceil_div(channels.first.width, 8), Num.ceil_div(channels.first.height, 8),
26
+ INV_SIGMA_NUM / loop_filter.epf_sigma_for_modular)
27
+ apply_filter(channels, sigma, loop_filter)
28
+ end
29
+
30
+ def apply_filter(channels, sigma, loop_filter)
31
+ output = channels
32
+ output = filter(output, sigma, loop_filter, WIDE, PLUS, loop_filter.epf_sigma_scale[0]) \
33
+ if loop_filter.epf_iterations >= 3
34
+ output = filter(output, sigma, loop_filter, CROSS, PLUS, 1.0) if loop_filter.epf_iterations >= 1
35
+ if loop_filter.epf_iterations >= 2
36
+ output = filter(output, sigma, loop_filter, CROSS, [[0, 0]], loop_filter.epf_sigma_scale[1])
37
+ end
38
+ output
39
+ end
40
+ private_class_method :apply_filter
41
+
42
+ def sigma_map(loop_filter, global, lf_group)
43
+ width = lf_group.quant_field.width
44
+ height = lf_group.quant_field.height
45
+ output = Plane.new(width, height)
46
+ height.times do |y|
47
+ width.times do |x|
48
+ index = (y * width) + x
49
+ next unless lf_group.strategy_first[index]
50
+
51
+ strategy = lf_group.strategies[index]
52
+ quant = lf_group.quant_field[x, y]
53
+ base = loop_filter.epf_quant_mul / (global.quantizer.scale * quant * INV_SIGMA_NUM)
54
+ strategy.blocks_y.times do |dy|
55
+ strategy.blocks_x.times do |dx|
56
+ sharpness = lf_group.sharpness[x + dx, y + dy]
57
+ value = [base * loop_filter.epf_sharpness[sharpness], -1e-4].min
58
+ output[x + dx, y + dy] = 1.0 / value
59
+ end
60
+ end
61
+ end
62
+ end
63
+ output
64
+ end
65
+
66
+ def filter(channels, sigma, loop_filter, neighbours, patch, sigma_scale) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
67
+ sources = channels.first(3)
68
+ grayscale = sources[0].equal?(sources[1]) && sources[0].equal?(sources[2])
69
+ filtered_sources = grayscale ? sources.first(1) : sources
70
+ scales = grayscale ? [loop_filter.epf_channel_scale.sum] : loop_filter.epf_channel_scale
71
+ outputs = filtered_sources.map { Plane.new(_1.width, _1.height) }
72
+ sources[0].height.times do |y|
73
+ sources[0].width.times do |x|
74
+ inv_sigma = sigma[x / 8, y / 8]
75
+ if inv_sigma < MIN_SIGMA
76
+ outputs.each_index { |c| outputs[c][x, y] = filtered_sources[c][x, y] }
77
+ next
78
+ end
79
+
80
+ border = [x % 8, y % 8].any? { _1.zero? || _1 == 7 }
81
+ multiplier = 1.65 * sigma_scale
82
+ multiplier *= loop_filter.epf_border_sad_mul if border
83
+ weighted = filtered_sources.map { _1[x, y] }
84
+ total = 1.0
85
+ neighbours.each do |dx, dy|
86
+ sad = patch_sad(filtered_sources, x, y, dx, dy, patch, scales)
87
+ weight = [1.0 + (sad * inv_sigma * multiplier), 0.0].max
88
+ total += weight
89
+ weighted.each_index { |c| weighted[c] += weight * sample(filtered_sources[c], x + dx, y + dy) }
90
+ end
91
+ outputs.each_index { |c| outputs[c][x, y] = weighted[c] / total }
92
+ end
93
+ end
94
+ (grayscale ? Array.new(3, outputs.first) : outputs) + channels.drop(3)
95
+ end
96
+ private_class_method :filter
97
+
98
+ def patch_sad(channels, x, y, dx, dy, patch, scales)
99
+ channels.each_with_index.sum do |plane, channel|
100
+ patch.sum do |px, py|
101
+ (sample(plane, x + px, y + py) - sample(plane, x + dx + px, y + dy + py)).abs
102
+ end * scales[channel]
103
+ end
104
+ end
105
+ private_class_method :patch_sad
106
+
107
+ def sample(plane, x, y) = plane[mirror(x, plane.width), mirror(y, plane.height)]
108
+ private_class_method :sample
109
+
110
+ def mirror(value, size)
111
+ return 0 if size == 1
112
+
113
+ value %= 2 * size
114
+ value < size ? value : (2 * size) - value - 1
115
+ end
116
+ private_class_method :mirror
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Filter
5
+ module Gaborish
6
+ module_function
7
+
8
+ def apply(channels, loop_filter)
9
+ return channels unless loop_filter.gaborish
10
+
11
+ channels.first(3).each_with_index.map do |plane, channel|
12
+ convolve(plane, loop_filter.gaborish_weights[channel])
13
+ end + channels.drop(3)
14
+ end
15
+
16
+ def convolve(source, weights)
17
+ side, diagonal = weights
18
+ normalization = 1.0 / (1.0 + (4 * (side + diagonal)))
19
+ output = Plane.new(source.width, source.height)
20
+ source.height.times do |y|
21
+ source.width.times do |x|
22
+ sides = sample(source, x - 1, y) + sample(source, x + 1, y) +
23
+ sample(source, x, y - 1) + sample(source, x, y + 1)
24
+ diagonals = sample(source, x - 1, y - 1) + sample(source, x + 1, y - 1) +
25
+ sample(source, x - 1, y + 1) + sample(source, x + 1, y + 1)
26
+ output[x, y] = (source[x, y] + (side * sides) + (diagonal * diagonals)) * normalization
27
+ end
28
+ end
29
+ output
30
+ end
31
+
32
+ def sample(plane, x, y) = plane[Num.clamp(x, 0, plane.width - 1), Num.clamp(y, 0, plane.height - 1)]
33
+ private_class_method :sample
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ class Frame
5
+ TOC_DISTRIBUTIONS = [Bit::Field.bits(10), Bit::Field.bits_offset(14, 1024),
6
+ Bit::Field.bits_offset(22, 17_408), Bit::Field.bits_offset(30, 4_211_712)].freeze
7
+
8
+ TOC = Data.define(:sizes, :offsets, :total_size, :permutation) do
9
+ def self.read(reader, count)
10
+ raise ResourceLimitError, "too many TOC entries" unless count.between?(1, 65_536)
11
+
12
+ permutation = Bit::Field.read_bool(reader) ? Entropy::Permutation.read(reader, count) : []
13
+
14
+ reader.zero_pad_to_byte
15
+ sizes = Array.new(count) { Bit::Field.u32(reader, *TOC_DISTRIBUTIONS) }
16
+ reader.zero_pad_to_byte
17
+ offset = 0
18
+ offsets = sizes.map do |size|
19
+ current = offset
20
+ offset += size
21
+ current
22
+ end
23
+ unless permutation.empty?
24
+ sizes = permutation.map { |index| sizes.fetch(index) }
25
+ offsets = permutation.map { |index| offsets.fetch(index) }
26
+ end
27
+ new(sizes:, offsets:, total_size: offset, permutation:)
28
+ end
29
+ end
30
+ end
31
+ end