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,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module Headers
|
|
5
|
+
BitDepth = Data.define(:floating_point, :bits_per_sample, :exponent_bits) do
|
|
6
|
+
def self.read(reader)
|
|
7
|
+
floating_point = Bit::Field.read_bool(reader)
|
|
8
|
+
bits_per_sample = if floating_point
|
|
9
|
+
Bit::Field.u32(reader, Bit::Field.val(32), Bit::Field.val(16),
|
|
10
|
+
Bit::Field.val(24), Bit::Field.bits_offset(6, 1))
|
|
11
|
+
else
|
|
12
|
+
Bit::Field.u32(reader, Bit::Field.val(8), Bit::Field.val(10),
|
|
13
|
+
Bit::Field.val(12), Bit::Field.bits_offset(6, 1))
|
|
14
|
+
end
|
|
15
|
+
exponent_bits = floating_point ? reader.read(4) + 1 : 0
|
|
16
|
+
validate!(floating_point, bits_per_sample, exponent_bits)
|
|
17
|
+
new(floating_point:, bits_per_sample:, exponent_bits:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.validate!(floating_point, bits, exponent)
|
|
21
|
+
if floating_point
|
|
22
|
+
mantissa = bits - exponent - 1
|
|
23
|
+
raise CorruptError, "invalid floating-point bit depth" unless exponent.between?(2,
|
|
24
|
+
8) && mantissa.between?(2, 23)
|
|
25
|
+
elsif bits > 31
|
|
26
|
+
raise CorruptError, "invalid integer bit depth"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
private_class_method :validate!
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module Headers
|
|
5
|
+
ColourEncoding = Data.define(:want_icc, :colour_space, :white_point, :primaries,
|
|
6
|
+
:transfer_function, :gamma, :rendering_intent,
|
|
7
|
+
:white_point_xy, :primaries_xy)
|
|
8
|
+
|
|
9
|
+
class ColourEncoding
|
|
10
|
+
DEFAULT = { want_icc: false, colour_space: 0, white_point: 1, primaries: 1,
|
|
11
|
+
transfer_function: 13, gamma: nil, rendering_intent: 1,
|
|
12
|
+
white_point_xy: nil, primaries_xy: nil }.freeze
|
|
13
|
+
CUSTOM_XY = [Bit::Field.bits(19), Bit::Field.bits_offset(19, 524_288),
|
|
14
|
+
Bit::Field.bits_offset(20, 1_048_576), Bit::Field.bits_offset(21, 2_097_152)].freeze
|
|
15
|
+
ENUM = [Bit::Field.val(0), Bit::Field.val(1), Bit::Field.bits_offset(4, 2),
|
|
16
|
+
Bit::Field.bits_offset(6, 18)].freeze
|
|
17
|
+
|
|
18
|
+
def self.read(reader)
|
|
19
|
+
return new(**DEFAULT) if Bit::Field.read_bool(reader)
|
|
20
|
+
|
|
21
|
+
want_icc = Bit::Field.read_bool(reader)
|
|
22
|
+
colour_space = read_enum(reader, [0, 1, 2, 3], "colour space")
|
|
23
|
+
raise CorruptError, "unknown colour space without ICC" if !want_icc && colour_space == 3
|
|
24
|
+
|
|
25
|
+
values = DEFAULT.merge(want_icc:, colour_space:)
|
|
26
|
+
unless want_icc
|
|
27
|
+
values[:white_point], values[:white_point_xy] = read_white_point(reader, colour_space)
|
|
28
|
+
values[:primaries], values[:primaries_xy] = read_primaries(reader, colour_space)
|
|
29
|
+
values[:transfer_function], values[:gamma] = read_transfer_function(reader, colour_space)
|
|
30
|
+
values[:rendering_intent] = read_enum(reader, [0, 1, 2, 3], "rendering intent")
|
|
31
|
+
end
|
|
32
|
+
new(**values)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.read_white_point(reader, colour_space)
|
|
36
|
+
return [1, nil] if colour_space == 2
|
|
37
|
+
|
|
38
|
+
value = read_enum(reader, [1, 2, 10, 11], "white point")
|
|
39
|
+
[value, value == 2 ? read_xy(reader) : nil]
|
|
40
|
+
end
|
|
41
|
+
private_class_method :read_white_point
|
|
42
|
+
|
|
43
|
+
def self.read_primaries(reader, colour_space)
|
|
44
|
+
return [1, nil] if [1, 2].include?(colour_space)
|
|
45
|
+
|
|
46
|
+
value = read_enum(reader, [1, 2, 9, 11], "primaries")
|
|
47
|
+
[value, value == 2 ? Array.new(3) { read_xy(reader) } : nil]
|
|
48
|
+
end
|
|
49
|
+
private_class_method :read_primaries
|
|
50
|
+
|
|
51
|
+
def self.read_xy(reader) = Array.new(2) { Num.unpack_signed(Bit::Field.u32(reader, *CUSTOM_XY)) / 1_000_000.0 }
|
|
52
|
+
private_class_method :read_xy
|
|
53
|
+
|
|
54
|
+
def self.read_transfer_function(reader, colour_space)
|
|
55
|
+
return [nil, 3_333_333] if colour_space == 2
|
|
56
|
+
|
|
57
|
+
have_gamma = Bit::Field.read_bool(reader)
|
|
58
|
+
if have_gamma
|
|
59
|
+
gamma = reader.read(24)
|
|
60
|
+
raise CorruptError, "invalid gamma" unless gamma.between?(1221, 10_000_000)
|
|
61
|
+
|
|
62
|
+
[nil, gamma]
|
|
63
|
+
else
|
|
64
|
+
value = read_enum(reader, [1, 2, 8, 13, 16, 17, 18], "transfer function")
|
|
65
|
+
raise CorruptError, "unknown transfer function without ICC" if value == 2
|
|
66
|
+
|
|
67
|
+
[value, nil]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
private_class_method :read_transfer_function
|
|
71
|
+
|
|
72
|
+
def self.read_enum(reader, allowed, name)
|
|
73
|
+
value = Bit::Field.u32(reader, *ENUM)
|
|
74
|
+
raise CorruptError, "invalid #{name}" unless allowed.include?(value)
|
|
75
|
+
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
private_class_method :read_enum
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module Headers
|
|
5
|
+
module CustomTransform
|
|
6
|
+
WEIGHT_COUNTS = [15, 55, 210].freeze
|
|
7
|
+
DEFAULT_MATRIX = [11.031566901960783, -9.866943921568629, -0.16462299647058826,
|
|
8
|
+
-3.254147380392157, 4.418770392156863, -0.16462299647058826,
|
|
9
|
+
-3.6588512862745097, 2.7129230470588235, 1.9459282392156863].freeze
|
|
10
|
+
DEFAULT_BIASES = Array.new(3, -0.0037930732552754493).freeze
|
|
11
|
+
DEFAULT_QUANT_BIASES = [0.945349926692846, 0.9299455010825141, 0.9500648966626564, 0.145].freeze
|
|
12
|
+
Opsin = Data.define(:inverse_matrix, :biases, :quant_biases)
|
|
13
|
+
TransformData = Data.define(:opsin, :weights)
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def read(reader, xyb_encoded:)
|
|
18
|
+
return defaults if Bit::Field.read_bool(reader)
|
|
19
|
+
|
|
20
|
+
opsin = xyb_encoded ? read_opsin(reader) : nil
|
|
21
|
+
mask = reader.read(3)
|
|
22
|
+
weights = WEIGHT_COUNTS.each_with_index.map do |count, index|
|
|
23
|
+
mask[index].zero? ? nil : Array.new(count) { Bit::Field.f16(reader) }
|
|
24
|
+
end
|
|
25
|
+
TransformData.new(opsin:, weights:)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def read_opsin(reader)
|
|
29
|
+
return default_opsin if Bit::Field.read_bool(reader)
|
|
30
|
+
|
|
31
|
+
Opsin.new(inverse_matrix: Array.new(9) { Bit::Field.f16(reader) },
|
|
32
|
+
biases: Array.new(3) { Bit::Field.f16(reader) },
|
|
33
|
+
quant_biases: Array.new(4) { Bit::Field.f16(reader) })
|
|
34
|
+
end
|
|
35
|
+
private_class_method :read_opsin
|
|
36
|
+
|
|
37
|
+
def defaults = TransformData.new(opsin: default_opsin, weights: Array.new(3))
|
|
38
|
+
private_class_method :defaults
|
|
39
|
+
|
|
40
|
+
def default_opsin
|
|
41
|
+
Opsin.new(inverse_matrix: DEFAULT_MATRIX, biases: DEFAULT_BIASES, quant_biases: DEFAULT_QUANT_BIASES)
|
|
42
|
+
end
|
|
43
|
+
private_class_method :default_opsin
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module Headers
|
|
5
|
+
FRAME_CROP_DISTRIBUTION = [Bit::Field.bits(8), Bit::Field.bits_offset(11, 256),
|
|
6
|
+
Bit::Field.bits_offset(14, 2304),
|
|
7
|
+
Bit::Field.bits_offset(30, 18_688)].freeze
|
|
8
|
+
FRAME_UPSAMPLING_DISTRIBUTION = [1, 2, 4, 8].map { |value| Bit::Field.val(value) }.freeze
|
|
9
|
+
|
|
10
|
+
Passes = Data.define(:count, :shifts, :downsamples, :last_passes)
|
|
11
|
+
BlendInfo = Data.define(:mode, :alpha_channel, :clamp, :source)
|
|
12
|
+
LoopFilter = Data.define(:gaborish, :gaborish_weights, :epf_iterations, :epf_sharpness,
|
|
13
|
+
:epf_channel_scale, :epf_zero_flush, :epf_quant_mul,
|
|
14
|
+
:epf_sigma_scale, :epf_border_sad_mul, :epf_sigma_for_modular)
|
|
15
|
+
FrameHeader = Data.define(:type, :encoding, :flags, :colour_transform, :chroma_subsampling, :upsampling,
|
|
16
|
+
:extra_channel_upsampling, :group_size_shift, :x_qm_scale, :b_qm_scale,
|
|
17
|
+
:passes, :dc_level, :origin_x, :origin_y, :width, :height, :blend,
|
|
18
|
+
:extra_channel_blends, :duration, :timecode, :last, :save_as_reference,
|
|
19
|
+
:save_before_colour_transform, :name, :loop_filter) do
|
|
20
|
+
def encoded_width = Num.ceil_div(width, upsampling << (3 * dc_level))
|
|
21
|
+
def encoded_height = Num.ceil_div(height, upsampling << (3 * dc_level))
|
|
22
|
+
def blocks_width = Num.ceil_div(encoded_width, 8 << max_hshift) << max_hshift
|
|
23
|
+
def blocks_height = Num.ceil_div(encoded_height, 8 << max_vshift) << max_vshift
|
|
24
|
+
def max_hshift = chroma_subsampling.map(&:first).max
|
|
25
|
+
def max_vshift = chroma_subsampling.map(&:last).max
|
|
26
|
+
|
|
27
|
+
def self.read(reader, metadata:, image_width:, image_height:)
|
|
28
|
+
return defaults(metadata, image_width, image_height) if Bit::Field.read_bool(reader)
|
|
29
|
+
|
|
30
|
+
values = read_core(reader, metadata)
|
|
31
|
+
read_geometry(reader, values, image_width, image_height)
|
|
32
|
+
read_compositing(reader, values, metadata, image_width, image_height)
|
|
33
|
+
values[:name] = Bit::Field.string(reader)
|
|
34
|
+
values[:loop_filter] = read_loop_filter(reader, values[:encoding])
|
|
35
|
+
Bit::Field.skip_extensions(reader)
|
|
36
|
+
new(**values)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.read_core(reader, metadata)
|
|
40
|
+
type = reader.read(2)
|
|
41
|
+
encoding = Bit::Field.read_bool(reader) ? :modular : :vardct
|
|
42
|
+
flags = Bit::Field.u64(reader)
|
|
43
|
+
colour_transform = if metadata.xyb_encoded
|
|
44
|
+
:xyb
|
|
45
|
+
else
|
|
46
|
+
(Bit::Field.read_bool(reader) ? :ycbcr : :none)
|
|
47
|
+
end
|
|
48
|
+
chroma_subsampling = if colour_transform == :ycbcr && flags.nobits?(32)
|
|
49
|
+
read_chroma_subsampling(reader)
|
|
50
|
+
else
|
|
51
|
+
Array.new(3) { [0, 0] }
|
|
52
|
+
end
|
|
53
|
+
upsampling = flags.nobits?(32) ? Bit::Field.u32(reader, *FRAME_UPSAMPLING_DISTRIBUTION) : 1
|
|
54
|
+
extra_upsampling = flags.nobits?(32) ? read_extra_upsampling(reader, metadata, upsampling) : []
|
|
55
|
+
group_shift = encoding == :modular ? reader.read(2) : 1
|
|
56
|
+
x_qm = encoding == :vardct && colour_transform == :xyb ? reader.read(3) : 2
|
|
57
|
+
b_qm = encoding == :vardct && colour_transform == :xyb ? reader.read(3) : 2
|
|
58
|
+
passes = type == 2 ? default_passes : read_passes(reader)
|
|
59
|
+
dc_level = type == 1 ? Bit::Field.u32(reader, *[1, 2, 3, 4].map { |value| Bit::Field.val(value) }) : 0
|
|
60
|
+
{ type:, encoding:, flags:, colour_transform:, chroma_subsampling:, upsampling:,
|
|
61
|
+
extra_channel_upsampling: extra_upsampling,
|
|
62
|
+
group_size_shift: group_shift, x_qm_scale: x_qm, b_qm_scale: b_qm, passes:, dc_level: }
|
|
63
|
+
end
|
|
64
|
+
private_class_method :read_core
|
|
65
|
+
|
|
66
|
+
def self.read_chroma_subsampling(reader)
|
|
67
|
+
raw_h = [0, 1, 1, 0]
|
|
68
|
+
raw_v = [0, 1, 0, 1]
|
|
69
|
+
modes = Array.new(3) { reader.read(2) }
|
|
70
|
+
max_h = modes.map { raw_h[_1] }.max
|
|
71
|
+
max_v = modes.map { raw_v[_1] }.max
|
|
72
|
+
modes.map { |mode| [max_h - raw_h[mode], max_v - raw_v[mode]] }
|
|
73
|
+
end
|
|
74
|
+
private_class_method :read_chroma_subsampling
|
|
75
|
+
|
|
76
|
+
def self.read_extra_upsampling(reader, metadata, colour_upsampling)
|
|
77
|
+
metadata.extra_channels.map do |channel|
|
|
78
|
+
value = Bit::Field.u32(reader, *FRAME_UPSAMPLING_DISTRIBUTION) << channel.dim_shift
|
|
79
|
+
raise CorruptError, "invalid extra-channel upsampling" unless value.between?(colour_upsampling, 8)
|
|
80
|
+
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
private_class_method :read_extra_upsampling
|
|
85
|
+
|
|
86
|
+
def self.read_passes(reader)
|
|
87
|
+
count = Bit::Field.u32(reader, Bit::Field.val(1), Bit::Field.val(2),
|
|
88
|
+
Bit::Field.val(3), Bit::Field.bits_offset(3, 4))
|
|
89
|
+
return default_passes if count == 1
|
|
90
|
+
|
|
91
|
+
downsample_count = Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1),
|
|
92
|
+
Bit::Field.val(2), Bit::Field.bits_offset(1, 3))
|
|
93
|
+
shifts = Array.new(count - 1) { reader.read(2) } << 0
|
|
94
|
+
raise CorruptError, "too many downsampling stages" if downsample_count > count
|
|
95
|
+
|
|
96
|
+
downsamples = Array.new(downsample_count) { Bit::Field.u32(reader, *FRAME_UPSAMPLING_DISTRIBUTION) }
|
|
97
|
+
last_passes = Array.new(downsample_count) do
|
|
98
|
+
Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1), Bit::Field.val(2), Bit::Field.bits(3))
|
|
99
|
+
end
|
|
100
|
+
raise CorruptError, "invalid downsampling order" unless downsamples.each_cons(2).all? { |a, b| b < a }
|
|
101
|
+
unless last_passes.each_cons(2).all? { |a, b| b > a } && last_passes.all? { |pass| pass < count }
|
|
102
|
+
raise CorruptError, "invalid last-pass order"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
Passes.new(count:, shifts:, downsamples:, last_passes:)
|
|
106
|
+
end
|
|
107
|
+
private_class_method :read_passes
|
|
108
|
+
|
|
109
|
+
def self.default_passes = Passes.new(count: 1, shifts: [0], downsamples: [], last_passes: [])
|
|
110
|
+
private_class_method :default_passes
|
|
111
|
+
|
|
112
|
+
def self.read_geometry(reader, values, image_width, image_height)
|
|
113
|
+
custom = values[:type] != 1 && Bit::Field.read_bool(reader)
|
|
114
|
+
values[:origin_x] = values[:origin_y] = 0
|
|
115
|
+
values[:width] = image_width
|
|
116
|
+
values[:height] = image_height
|
|
117
|
+
return unless custom
|
|
118
|
+
|
|
119
|
+
if [0, 3].include?(values[:type])
|
|
120
|
+
values[:origin_x] = Num.unpack_signed(Bit::Field.u32(reader, *FRAME_CROP_DISTRIBUTION))
|
|
121
|
+
values[:origin_y] = Num.unpack_signed(Bit::Field.u32(reader, *FRAME_CROP_DISTRIBUTION))
|
|
122
|
+
end
|
|
123
|
+
values[:width] = Bit::Field.u32(reader, *FRAME_CROP_DISTRIBUTION)
|
|
124
|
+
values[:height] = Bit::Field.u32(reader, *FRAME_CROP_DISTRIBUTION)
|
|
125
|
+
raise CorruptError, "empty frame" unless values[:width].positive? && values[:height].positive?
|
|
126
|
+
end
|
|
127
|
+
private_class_method :read_geometry
|
|
128
|
+
|
|
129
|
+
def self.read_compositing(reader, values, metadata, image_width, image_height)
|
|
130
|
+
regular = [0, 3].include?(values[:type])
|
|
131
|
+
partial = values[:origin_x].positive? || values[:origin_y].positive? ||
|
|
132
|
+
values[:origin_x] + values[:width] < image_width ||
|
|
133
|
+
values[:origin_y] + values[:height] < image_height
|
|
134
|
+
values[:blend] = regular ? read_blend(reader, metadata.extra_channels.length, partial) : default_blend
|
|
135
|
+
values[:extra_channel_blends] = if regular
|
|
136
|
+
metadata.extra_channels.map do
|
|
137
|
+
read_blend(reader, metadata.extra_channels.length, partial)
|
|
138
|
+
end
|
|
139
|
+
else
|
|
140
|
+
[]
|
|
141
|
+
end
|
|
142
|
+
values[:duration] = regular && metadata.animation ? read_duration(reader) : 0
|
|
143
|
+
values[:timecode] = regular && metadata.animation&.have_timecodes ? reader.read(32) : 0
|
|
144
|
+
values[:last] = regular ? Bit::Field.read_bool(reader) : false
|
|
145
|
+
values[:save_as_reference] = values[:type] != 1 && !values[:last] ? reader.read(2) : 0
|
|
146
|
+
can_reference = !values[:last] && values[:type] != 1 &&
|
|
147
|
+
(values[:duration].zero? || !values[:save_as_reference].zero?)
|
|
148
|
+
may_choose_reference = can_reference && values[:blend].mode.zero? && !partial && regular
|
|
149
|
+
values[:save_before_colour_transform] = if may_choose_reference || values[:type] == 2
|
|
150
|
+
Bit::Field.read_bool(reader)
|
|
151
|
+
else
|
|
152
|
+
values[:type] == 1
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
private_class_method :read_compositing
|
|
156
|
+
|
|
157
|
+
def self.read_duration(reader)
|
|
158
|
+
Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1), Bit::Field.bits(8), Bit::Field.bits(32))
|
|
159
|
+
end
|
|
160
|
+
private_class_method :read_duration
|
|
161
|
+
|
|
162
|
+
def self.read_blend(reader, extra_channels, partial)
|
|
163
|
+
mode = Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1),
|
|
164
|
+
Bit::Field.val(2), Bit::Field.bits_offset(2, 3))
|
|
165
|
+
raise CorruptError, "invalid blend mode" if mode > 4
|
|
166
|
+
|
|
167
|
+
uses_alpha = extra_channels.positive? && [2, 3].include?(mode)
|
|
168
|
+
alpha = if uses_alpha
|
|
169
|
+
Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1),
|
|
170
|
+
Bit::Field.val(2), Bit::Field.bits_offset(3, 3))
|
|
171
|
+
else
|
|
172
|
+
0
|
|
173
|
+
end
|
|
174
|
+
raise CorruptError, "invalid blend alpha channel" if alpha >= extra_channels && uses_alpha
|
|
175
|
+
|
|
176
|
+
clamp = (uses_alpha || mode == 4) && Bit::Field.read_bool(reader)
|
|
177
|
+
source = mode != 0 || partial ? reader.read(2) : 0
|
|
178
|
+
BlendInfo.new(mode:, alpha_channel: alpha, clamp:, source:)
|
|
179
|
+
end
|
|
180
|
+
private_class_method :read_blend
|
|
181
|
+
|
|
182
|
+
def self.default_blend = BlendInfo.new(mode: 0, alpha_channel: 0, clamp: false, source: 0)
|
|
183
|
+
private_class_method :default_blend
|
|
184
|
+
|
|
185
|
+
def self.read_loop_filter(reader, encoding)
|
|
186
|
+
return default_loop_filter if Bit::Field.read_bool(reader)
|
|
187
|
+
|
|
188
|
+
gaborish = Bit::Field.read_bool(reader)
|
|
189
|
+
weights = if gaborish && Bit::Field.read_bool(reader)
|
|
190
|
+
Array.new(3) { [Bit::Field.f16(reader), Bit::Field.f16(reader)] }
|
|
191
|
+
else
|
|
192
|
+
default_gaborish_weights
|
|
193
|
+
end
|
|
194
|
+
epf = reader.read(2)
|
|
195
|
+
options = epf.positive? ? read_epf_options(reader, encoding) : default_epf_options
|
|
196
|
+
Bit::Field.skip_extensions(reader)
|
|
197
|
+
LoopFilter.new(gaborish:, gaborish_weights: weights, epf_iterations: epf, **options)
|
|
198
|
+
end
|
|
199
|
+
private_class_method :read_loop_filter
|
|
200
|
+
|
|
201
|
+
def self.read_epf_options(reader, encoding)
|
|
202
|
+
values = default_epf_options
|
|
203
|
+
if encoding == :vardct && Bit::Field.read_bool(reader)
|
|
204
|
+
values[:epf_sharpness] = Array.new(8) { Bit::Field.f16(reader) }
|
|
205
|
+
end
|
|
206
|
+
if Bit::Field.read_bool(reader)
|
|
207
|
+
values[:epf_channel_scale] = Array.new(3) { Bit::Field.f16(reader) }
|
|
208
|
+
values[:epf_zero_flush] = Array.new(2) { Bit::Field.f16(reader) }
|
|
209
|
+
end
|
|
210
|
+
if Bit::Field.read_bool(reader)
|
|
211
|
+
values[:epf_quant_mul] = Bit::Field.f16(reader) if encoding == :vardct
|
|
212
|
+
values[:epf_sigma_scale] = Array.new(2) { Bit::Field.f16(reader) }
|
|
213
|
+
values[:epf_border_sad_mul] = Bit::Field.f16(reader)
|
|
214
|
+
end
|
|
215
|
+
values[:epf_sigma_for_modular] = Bit::Field.f16(reader) if encoding == :modular
|
|
216
|
+
values
|
|
217
|
+
end
|
|
218
|
+
private_class_method :read_epf_options
|
|
219
|
+
|
|
220
|
+
def self.default_gaborish_weights
|
|
221
|
+
Array.new(3) { [1.1 * 0.104699568, 1.1 * 0.055680538] }
|
|
222
|
+
end
|
|
223
|
+
private_class_method :default_gaborish_weights
|
|
224
|
+
|
|
225
|
+
def self.default_epf_options
|
|
226
|
+
{ epf_sharpness: Array.new(8) { |index| index / 7.0 }, epf_channel_scale: [40.0, 5.0, 3.5],
|
|
227
|
+
epf_zero_flush: [0.45, 0.6], epf_quant_mul: 0.46, epf_sigma_scale: [0.9, 6.5],
|
|
228
|
+
epf_border_sad_mul: 2.0 / 3, epf_sigma_for_modular: 1.0 }
|
|
229
|
+
end
|
|
230
|
+
private_class_method :default_epf_options
|
|
231
|
+
|
|
232
|
+
def self.default_loop_filter
|
|
233
|
+
LoopFilter.new(gaborish: true, gaborish_weights: default_gaborish_weights, epf_iterations: 2,
|
|
234
|
+
**default_epf_options)
|
|
235
|
+
end
|
|
236
|
+
private_class_method :default_loop_filter
|
|
237
|
+
|
|
238
|
+
def self.defaults(metadata, width, height)
|
|
239
|
+
new(type: 0, encoding: :vardct, flags: 0, colour_transform: metadata.xyb_encoded ? :xyb : :none,
|
|
240
|
+
chroma_subsampling: Array.new(3) { [0, 0] },
|
|
241
|
+
upsampling: 1, extra_channel_upsampling: Array.new(metadata.extra_channels.length, 1),
|
|
242
|
+
group_size_shift: 1, x_qm_scale: 3, b_qm_scale: 2, passes: default_passes,
|
|
243
|
+
dc_level: 0, origin_x: 0, origin_y: 0, width:, height:, blend: default_blend,
|
|
244
|
+
extra_channel_blends: Array.new(metadata.extra_channels.length) { default_blend }, duration: 0,
|
|
245
|
+
timecode: 0, last: true, save_as_reference: 0, save_before_colour_transform: false,
|
|
246
|
+
name: "", loop_filter: default_loop_filter)
|
|
247
|
+
end
|
|
248
|
+
private_class_method :defaults
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module Headers
|
|
5
|
+
ExtraChannel = Data.define(:type, :bit_depth, :dim_shift, :name, :alpha_associated, :spot_colour, :cfa_channel)
|
|
6
|
+
Animation = Data.define(:ticks_numerator, :ticks_denominator, :loops, :have_timecodes)
|
|
7
|
+
ToneMapping = Data.define(:intensity_target, :min_nits, :relative_to_max_display, :linear_below)
|
|
8
|
+
ImageMetadata = Data.define(:orientation, :intrinsic_size, :preview_size, :animation, :bit_depth,
|
|
9
|
+
:modular_16_bit_buffer, :extra_channels, :xyb_encoded, :colour_encoding,
|
|
10
|
+
:tone_mapping, :extensions) do
|
|
11
|
+
def self.read(reader, max_pixels: 100_000_000)
|
|
12
|
+
return defaults if Bit::Field.read_bool(reader)
|
|
13
|
+
|
|
14
|
+
extra_fields = Bit::Field.read_bool(reader)
|
|
15
|
+
optional = extra_fields ? read_optional_fields(reader, max_pixels) : {}
|
|
16
|
+
bit_depth = BitDepth.read(reader)
|
|
17
|
+
modular_16_bit_buffer = Bit::Field.read_bool(reader)
|
|
18
|
+
count = Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1),
|
|
19
|
+
Bit::Field.bits_offset(4, 2), Bit::Field.bits_offset(12, 1))
|
|
20
|
+
raise ResourceLimitError, "too many extra channels" if count > 256
|
|
21
|
+
|
|
22
|
+
extra_channels = Array.new(count) { read_extra_channel(reader) }
|
|
23
|
+
xyb_encoded = Bit::Field.read_bool(reader)
|
|
24
|
+
colour_encoding = ColourEncoding.read(reader)
|
|
25
|
+
tone_mapping = extra_fields ? read_tone_mapping(reader) : default_tone_mapping
|
|
26
|
+
Bit::Field.skip_extensions(reader)
|
|
27
|
+
new(orientation: optional.fetch(:orientation, 1), intrinsic_size: optional[:intrinsic_size],
|
|
28
|
+
preview_size: optional[:preview_size], animation: optional[:animation], bit_depth:,
|
|
29
|
+
modular_16_bit_buffer:, extra_channels:, xyb_encoded:, colour_encoding:, tone_mapping:, extensions: 0)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.defaults
|
|
33
|
+
new(orientation: 1, intrinsic_size: nil, preview_size: nil, animation: nil,
|
|
34
|
+
bit_depth: BitDepth.new(floating_point: false, bits_per_sample: 8, exponent_bits: 0),
|
|
35
|
+
modular_16_bit_buffer: true, extra_channels: [], xyb_encoded: true,
|
|
36
|
+
colour_encoding: ColourEncoding.new(**ColourEncoding::DEFAULT),
|
|
37
|
+
tone_mapping: default_tone_mapping, extensions: 0)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.read_optional_fields(reader, max_pixels)
|
|
41
|
+
values = { orientation: reader.read(3) + 1 }
|
|
42
|
+
values[:intrinsic_size] = SizeHeader.read(reader, max_pixels:) if Bit::Field.read_bool(reader)
|
|
43
|
+
values[:preview_size] = read_preview_size(reader) if Bit::Field.read_bool(reader)
|
|
44
|
+
values[:animation] = read_animation(reader) if Bit::Field.read_bool(reader)
|
|
45
|
+
values
|
|
46
|
+
end
|
|
47
|
+
private_class_method :read_optional_fields
|
|
48
|
+
|
|
49
|
+
def self.read_preview_size(reader)
|
|
50
|
+
div8 = Bit::Field.read_bool(reader)
|
|
51
|
+
if div8
|
|
52
|
+
distributions = [Bit::Field.val(16), Bit::Field.val(32), Bit::Field.bits_offset(5, 1),
|
|
53
|
+
Bit::Field.bits_offset(9, 33)]
|
|
54
|
+
height = Bit::Field.u32(reader, *distributions) * 8
|
|
55
|
+
else
|
|
56
|
+
distributions = [Bit::Field.bits_offset(6, 1), Bit::Field.bits_offset(8, 65),
|
|
57
|
+
Bit::Field.bits_offset(10, 321), Bit::Field.bits_offset(12, 1345)]
|
|
58
|
+
height = Bit::Field.u32(reader, *distributions)
|
|
59
|
+
end
|
|
60
|
+
ratio = reader.read(3)
|
|
61
|
+
width = if ratio.zero?
|
|
62
|
+
Bit::Field.u32(reader, *distributions) * (div8 ? 8 : 1)
|
|
63
|
+
else
|
|
64
|
+
ratio_width(height, ratio)
|
|
65
|
+
end
|
|
66
|
+
SizeHeader.new(width:, height:)
|
|
67
|
+
end
|
|
68
|
+
private_class_method :read_preview_size
|
|
69
|
+
|
|
70
|
+
def self.ratio_width(height, ratio)
|
|
71
|
+
numerator, denominator = SizeHeader::RATIOS.fetch(ratio - 1)
|
|
72
|
+
height * numerator / denominator
|
|
73
|
+
end
|
|
74
|
+
private_class_method :ratio_width
|
|
75
|
+
|
|
76
|
+
def self.read_animation(reader)
|
|
77
|
+
numerator = Bit::Field.u32(reader, Bit::Field.val(100), Bit::Field.val(1000),
|
|
78
|
+
Bit::Field.bits_offset(10, 1), Bit::Field.bits_offset(30, 1))
|
|
79
|
+
denominator = Bit::Field.u32(reader, Bit::Field.val(1), Bit::Field.val(1001),
|
|
80
|
+
Bit::Field.bits_offset(8, 1), Bit::Field.bits_offset(10, 1))
|
|
81
|
+
loops = Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.bits(3),
|
|
82
|
+
Bit::Field.bits(16), Bit::Field.bits(32))
|
|
83
|
+
Animation.new(ticks_numerator: numerator, ticks_denominator: denominator,
|
|
84
|
+
loops:, have_timecodes: Bit::Field.read_bool(reader))
|
|
85
|
+
end
|
|
86
|
+
private_class_method :read_animation
|
|
87
|
+
|
|
88
|
+
def self.read_extra_channel(reader)
|
|
89
|
+
return default_extra_channel if Bit::Field.read_bool(reader)
|
|
90
|
+
|
|
91
|
+
type = Bit::Field.enum(reader)
|
|
92
|
+
bit_depth = BitDepth.read(reader)
|
|
93
|
+
dim_shift = Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(3),
|
|
94
|
+
Bit::Field.val(4), Bit::Field.bits_offset(3, 1))
|
|
95
|
+
raise CorruptError, "extra-channel shift too large" if (1 << dim_shift) > 8
|
|
96
|
+
|
|
97
|
+
name = Bit::Field.string(reader)
|
|
98
|
+
alpha_associated = type.zero? ? Bit::Field.read_bool(reader) : false
|
|
99
|
+
spot_colour = type == 2 ? Array.new(4) { Bit::Field.f16(reader) } : nil
|
|
100
|
+
cfa_channel = if type == 5
|
|
101
|
+
Bit::Field.u32(reader, Bit::Field.val(1), Bit::Field.bits(2),
|
|
102
|
+
Bit::Field.bits_offset(4, 3), Bit::Field.bits_offset(8, 19))
|
|
103
|
+
end
|
|
104
|
+
ExtraChannel.new(type:, bit_depth:, dim_shift:, name:, alpha_associated:, spot_colour:, cfa_channel:)
|
|
105
|
+
end
|
|
106
|
+
private_class_method :read_extra_channel
|
|
107
|
+
|
|
108
|
+
def self.default_extra_channel
|
|
109
|
+
ExtraChannel.new(type: 0, bit_depth: BitDepth.new(floating_point: false, bits_per_sample: 8, exponent_bits: 0),
|
|
110
|
+
dim_shift: 0, name: "", alpha_associated: false, spot_colour: nil, cfa_channel: nil)
|
|
111
|
+
end
|
|
112
|
+
private_class_method :default_extra_channel
|
|
113
|
+
|
|
114
|
+
def self.read_tone_mapping(reader)
|
|
115
|
+
return default_tone_mapping if Bit::Field.read_bool(reader)
|
|
116
|
+
|
|
117
|
+
ToneMapping.new(intensity_target: Bit::Field.f16(reader), min_nits: Bit::Field.f16(reader),
|
|
118
|
+
relative_to_max_display: Bit::Field.read_bool(reader), linear_below: Bit::Field.f16(reader))
|
|
119
|
+
end
|
|
120
|
+
private_class_method :read_tone_mapping
|
|
121
|
+
|
|
122
|
+
def self.default_tone_mapping
|
|
123
|
+
ToneMapping.new(intensity_target: 255.0, min_nits: 0.0, relative_to_max_display: false, linear_below: 0.0)
|
|
124
|
+
end
|
|
125
|
+
private_class_method :default_tone_mapping
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module Headers
|
|
5
|
+
SizeHeader = Data.define(:width, :height)
|
|
6
|
+
|
|
7
|
+
class SizeHeader
|
|
8
|
+
RATIOS = [[1, 1], [12, 10], [4, 3], [3, 2], [16, 9], [5, 4], [2, 1]].freeze
|
|
9
|
+
DIMENSION = [Bit::Field.bits_offset(9, 1), Bit::Field.bits_offset(13, 1),
|
|
10
|
+
Bit::Field.bits_offset(18, 1), Bit::Field.bits_offset(30, 1)].freeze
|
|
11
|
+
|
|
12
|
+
def self.read(reader, max_pixels: 100_000_000)
|
|
13
|
+
small = Bit::Field.read_bool(reader)
|
|
14
|
+
height = small ? (reader.read(5) + 1) * 8 : Bit::Field.u32(reader, *DIMENSION)
|
|
15
|
+
ratio = reader.read(3)
|
|
16
|
+
width = if ratio.zero?
|
|
17
|
+
small ? (reader.read(5) + 1) * 8 : Bit::Field.u32(reader, *DIMENSION)
|
|
18
|
+
else
|
|
19
|
+
numerator, denominator = RATIOS.fetch(ratio - 1)
|
|
20
|
+
height * numerator / denominator
|
|
21
|
+
end
|
|
22
|
+
raise CorruptError, "empty JPEG XL image" unless width.positive? && height.positive?
|
|
23
|
+
raise ResourceLimitError, "image exceeds pixel limit" if width * height > max_pixels
|
|
24
|
+
|
|
25
|
+
new(width:, height:)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/jxl/image.rb
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
class Frame
|
|
5
|
+
attr_reader :planes, :duration, :timecode, :name, :blend_info
|
|
6
|
+
|
|
7
|
+
def initialize(planes:, duration:, timecode:, name:, blend_info:)
|
|
8
|
+
@planes = planes
|
|
9
|
+
@duration = duration
|
|
10
|
+
@timecode = timecode
|
|
11
|
+
@name = name
|
|
12
|
+
@blend_info = blend_info
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Image
|
|
17
|
+
attr_reader :width, :height, :channels, :metadata, :frames, :icc_profile
|
|
18
|
+
|
|
19
|
+
def initialize(width:, height:, channels:, metadata:, frames: [], icc_profile: nil) # rubocop:disable Metrics/ParameterLists
|
|
20
|
+
@width = width
|
|
21
|
+
@height = height
|
|
22
|
+
@channels = channels
|
|
23
|
+
@metadata = metadata
|
|
24
|
+
@frames = frames
|
|
25
|
+
@icc_profile = icc_profile
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def with(**changes)
|
|
29
|
+
self.class.new(width: changes.fetch(:width, width), height: changes.fetch(:height, height),
|
|
30
|
+
channels: changes.fetch(:channels, channels), metadata: changes.fetch(:metadata, metadata),
|
|
31
|
+
frames: changes.fetch(:frames, frames), icc_profile: changes.fetch(:icc_profile, icc_profile))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def num_color_channels = channels.length - metadata.extra_channels.length
|
|
35
|
+
def extra_channels = channels.drop(num_color_channels)
|
|
36
|
+
def color_encoding = metadata.colour_encoding
|
|
37
|
+
|
|
38
|
+
# Returns interleaved RGBA pixels with 8-bit unsigned samples.
|
|
39
|
+
def to_rgba8 = interleaved_rgba.map { |value| (Num.clamp(value, 0.0, 1.0) * 255).round }.pack("C*")
|
|
40
|
+
|
|
41
|
+
# Returns little-endian interleaved RGBA pixels with 16-bit unsigned samples.
|
|
42
|
+
def to_rgba16
|
|
43
|
+
interleaved_rgba.map { |value| (Num.clamp(value, 0.0, 1.0) * 65_535).round }.pack("v*")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns little-endian interleaved RGBA pixels with binary32 samples.
|
|
47
|
+
def to_float = interleaved_rgba.pack("e*")
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def interleaved_rgba
|
|
52
|
+
colour = num_color_channels == 1 ? [channels[0]] * 3 : channels.first(3)
|
|
53
|
+
alpha_index = metadata.extra_channels.index { _1.type.zero? }
|
|
54
|
+
alpha = extra_channels[alpha_index] if alpha_index
|
|
55
|
+
[].tap do |values|
|
|
56
|
+
(width * height).times do |pixel|
|
|
57
|
+
colour.each { |plane| values << plane.data[pixel] }
|
|
58
|
+
values << (alpha ? alpha.data[pixel] : 1.0)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|