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,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
|
|
Binary file
|
|
@@ -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
|