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,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module VarDCT
5
+ DEFAULT_BLOCK_CONTEXT_MAP = [
6
+ 0, 1, 2, 2, 3, 3, 4, 5, 6, 6, 6, 6, 6,
7
+ 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14,
8
+ 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14
9
+ ].freeze
10
+ BLOCK_DC_DISTRIBUTION = [Bit::Field.bits(4), Bit::Field.bits_offset(8, 16),
11
+ Bit::Field.bits_offset(16, 272), Bit::Field.bits_offset(32, 65_808)].freeze
12
+ BLOCK_QF_DISTRIBUTION = [Bit::Field.bits(2), Bit::Field.bits_offset(3, 4),
13
+ Bit::Field.bits_offset(5, 12), Bit::Field.bits_offset(8, 44)].freeze
14
+
15
+ BlockContextMap = Data.define(:dc_thresholds, :qf_thresholds, :context_map,
16
+ :num_contexts, :num_dc_contexts) do
17
+ def self.read(reader)
18
+ return defaults if Bit::Field.read_bool(reader)
19
+
20
+ dc = Array.new(3) do
21
+ Array.new(reader.read(4)) { Num.unpack_signed(Bit::Field.u32(reader, *BLOCK_DC_DISTRIBUTION)) }
22
+ end
23
+ num_dc = dc.reduce(1) { |total, thresholds| total * (thresholds.length + 1) }
24
+ qf = Array.new(reader.read(4)) { Bit::Field.u32(reader, *BLOCK_QF_DISTRIBUTION) + 1 }
25
+ raise CorruptError, "block context map is too large" if num_dc * (qf.length + 1) > 64
26
+
27
+ read_map(reader, dc, qf, num_dc)
28
+ end
29
+
30
+ def self.defaults
31
+ new(dc_thresholds: Array.new(3) { [] }, qf_thresholds: [], context_map: DEFAULT_BLOCK_CONTEXT_MAP,
32
+ num_contexts: 15, num_dc_contexts: 1)
33
+ end
34
+
35
+ def self.read_map(reader, dc, qf, num_dc)
36
+ map = Entropy::Decoder.decode_context_map(reader, 3 * 13 * num_dc * (qf.length + 1))
37
+ raise CorruptError, "too many block contexts" if map.max >= 16
38
+
39
+ new(dc_thresholds: dc, qf_thresholds: qf, context_map: map,
40
+ num_contexts: map.max + 1, num_dc_contexts: num_dc)
41
+ end
42
+ private_class_method :read_map
43
+
44
+ def context(dc_index, quant, order, channel)
45
+ quant_index = qf_thresholds.count { quant > _1 }
46
+ index = ((channel < 2 ? channel ^ 1 : 2) * 13) + order
47
+ context_map.fetch((((index * (qf_thresholds.length + 1)) + quant_index) * num_dc_contexts) + dc_index)
48
+ end
49
+
50
+ def nonzero_context(nonzeros, block_context)
51
+ nonzeros = 64 if nonzeros >= 64
52
+ bucket = nonzeros < 8 ? nonzeros : 4 + (nonzeros / 2)
53
+ (bucket * num_contexts) + block_context
54
+ end
55
+
56
+ def coefficient_context_offset(block_context)
57
+ (num_contexts * 37) + (458 * block_context)
58
+ end
59
+
60
+ def ac_context_count = num_contexts * (37 + 458)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module VarDCT
5
+ CFL_FACTOR_DISTRIBUTION = [Bit::Field.val(84), Bit::Field.val(256),
6
+ Bit::Field.bits_offset(8, 2), Bit::Field.bits_offset(16, 258)].freeze
7
+
8
+ ChromaFromLuma = Data.define(:color_factor, :base_x, :base_b, :dc_x, :dc_b) do
9
+ def self.read_dc(reader)
10
+ return new(color_factor: 84, base_x: 0.0, base_b: 1.0, dc_x: 0, dc_b: 0) if Bit::Field.read_bool(reader)
11
+
12
+ factor = Bit::Field.u32(reader, *CFL_FACTOR_DISTRIBUTION)
13
+ raise CorruptError, "zero chroma correlation factor" if factor.zero?
14
+
15
+ base_x = Bit::Field.f16(reader)
16
+ base_b = Bit::Field.f16(reader)
17
+ raise CorruptError, "chroma correlation is out of range" if base_x.abs > 4.0 || base_b.abs > 4.0
18
+
19
+ new(color_factor: factor, base_x:, base_b:, dc_x: reader.read(8) - 128, dc_b: reader.read(8) - 128)
20
+ end
21
+
22
+ def dc_factors = [base_x + dc_x.fdiv(color_factor), 0.0, base_b + dc_b.fdiv(color_factor)]
23
+ def x_ratio(factor) = base_x + factor.fdiv(color_factor)
24
+ def b_ratio(factor) = base_b + factor.fdiv(color_factor)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module VarDCT
5
+ module CoeffOrder
6
+ ORDER_REPRESENTATIVES = [0, 1, 4, 5, 6, 8, 10, 18, 19, 21, 22, 24, 25].freeze
7
+
8
+ module_function
9
+
10
+ def read(reader, used_orders, used_strategies)
11
+ decoder = Entropy::Decoder.read(reader, 8) unless used_orders.zero?
12
+ used_order_ids = used_strategies.map { AcStrategy[_1].order }.uniq
13
+ result = {}
14
+ ORDER_REPRESENTATIVES.each_with_index do |strategy_id, order|
15
+ next if !used_order_ids.include?(order) && used_orders[order].zero?
16
+
17
+ strategy = AcStrategy[strategy_id]
18
+ natural = strategy.natural_order
19
+ 3.times do |channel|
20
+ permutation = if used_orders[order].zero?
21
+ natural
22
+ else
23
+ Entropy::Permutation.read_with_decoder(
24
+ decoder, natural.length, skip: strategy.blocks_x * strategy.blocks_y
25
+ ).map { natural[_1] }
26
+ end
27
+ result[[order, channel]] = permutation if used_order_ids.include?(order)
28
+ end
29
+ end
30
+ decoder&.final_state!
31
+ result
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module VarDCT
5
+ module Decoder
6
+ module_function
7
+
8
+ def decode(parsed, references: [], dc_frames: [])
9
+ global, lf, pass = if parsed.sections.one?
10
+ read_single_section(parsed, references, dc_frames)
11
+ else
12
+ read_grouped_sections(parsed, references, dc_frames)
13
+ end
14
+ frame = parsed.frame_header
15
+ if frame.flags.nobits?(128 | 32) && frame.max_hshift.zero? && frame.max_vshift.zero?
16
+ LFGroup.smooth!(lf, global.quantizer.dc_steps(global.dequant))
17
+ end
18
+ channels = Reconstructor.reconstruct(parsed.frame_header, global, lf, pass,
19
+ quant_biases: parsed.transform_data.opsin.quant_biases)
20
+ channels.concat(normalize_extra_channels(global.extra_channels, parsed.basic_info.metadata))
21
+ channels = Filter::Gaborish.apply(channels, parsed.frame_header.loop_filter)
22
+ channels = Filter::EPF.apply(channels, parsed.frame_header.loop_filter, global, lf)
23
+ channels = Features::Patches.apply(channels, global.patches, references, parsed.basic_info.metadata)
24
+ channels = Features::Splines.apply(channels, global.splines, global.chroma_from_luma)
25
+ channels = Features::Upsampling.apply(channels, parsed.frame_header, parsed.transform_data)
26
+ channels = Features::Noise.apply(channels, global.noise, global.chroma_from_luma)
27
+ channels = convert_colour(channels, parsed)
28
+ if parsed.basic_info.metadata.colour_encoding.colour_space == 1
29
+ channels = [channels.first] + channels.last(parsed.basic_info.metadata.extra_channels.length)
30
+ end
31
+ channels.map! { crop(_1, parsed.frame_header.width, parsed.frame_header.height) }
32
+ Image.new(width: parsed.frame_header.width, height: parsed.frame_header.height,
33
+ channels:, metadata: parsed.basic_info.metadata)
34
+ end
35
+
36
+ def read_single_section(parsed, references, dc_frames)
37
+ frame = parsed.frame_header
38
+ reader = Bit::Reader.new(parsed.sections.first)
39
+ global = LFGlobal.read(reader, frame, parsed.basic_info.metadata, references:)
40
+ lf = LFGroup.read(reader, frame, parsed.basic_info.metadata, global, dc_frame: dc_frames[frame.dc_level])
41
+ hf = HFGlobal.read(reader, frame, global, [lf], metadata: parsed.basic_info.metadata)
42
+ passes = frame.passes.count.times.map { |pass| PassGroup.read(reader, frame, global, lf, hf, pass_index: pass) }
43
+ global = finish_extra_channels(global)
44
+ [global, lf, PassGroup.merge(passes)]
45
+ end
46
+ private_class_method :read_single_section
47
+
48
+ def read_grouped_sections(parsed, references, dc_frames)
49
+ frame = parsed.frame_header
50
+ dc_groups = dc_group_count(frame)
51
+ global = LFGlobal.read(section_reader(parsed, 0), frame, parsed.basic_info.metadata, references:)
52
+ lf_groups = Array.new(dc_groups) do |group|
53
+ LFGroup.read(section_reader(parsed, 1 + group), frame, parsed.basic_info.metadata, global,
54
+ group_index: group, group_count: dc_groups, dc_frame: dc_frames[frame.dc_level])
55
+ end
56
+ lf = LFGroup.merge(lf_groups, frame)
57
+ hf = HFGlobal.read(section_reader(parsed, 1 + dc_groups), frame, global, lf_groups,
58
+ metadata: parsed.basic_info.metadata)
59
+ group_dim = 128 << frame.group_size_shift
60
+ count = Num.ceil_div(frame.encoded_width, group_dim) * Num.ceil_div(frame.encoded_height, group_dim)
61
+ blocks = count.times.flat_map do |group|
62
+ passes = frame.passes.count.times.map do |pass|
63
+ reader = section_reader(parsed, 2 + dc_groups + (pass * count) + group)
64
+ PassGroup.read(reader, frame, global, lf, hf, pass_index: pass, group_index: group)
65
+ end
66
+ PassGroup.merge(passes).blocks
67
+ end
68
+ global = finish_extra_channels(global)
69
+ [global, lf, PassGroup.new(blocks:)]
70
+ end
71
+ private_class_method :read_grouped_sections
72
+
73
+ def finish_extra_channels(global)
74
+ return global unless global.modular_group
75
+
76
+ global.with(extra_channels: Modular::Decoder.finish_grouped_stream(global.modular_group))
77
+ end
78
+ private_class_method :finish_extra_channels
79
+
80
+ def dc_group_count(frame)
81
+ group_dim = 128 << frame.group_size_shift
82
+ Num.ceil_div(frame.blocks_width, group_dim) * Num.ceil_div(frame.blocks_height, group_dim)
83
+ end
84
+ private_class_method :dc_group_count
85
+
86
+ def section_reader(parsed, index) = Bit::Reader.new(parsed.sections.fetch(index))
87
+ private_class_method :section_reader
88
+
89
+ def convert_colour(channels, parsed)
90
+ frame = parsed.frame_header
91
+ return Color::YCbCr.to_rgb(channels, frame) if frame.colour_transform == :ycbcr
92
+ return channels unless frame.colour_transform == :xyb
93
+
94
+ opsin = parsed.transform_data.opsin
95
+ linear = Color::Opsin.to_linear(channels, opsin,
96
+ intensity_target: parsed.basic_info.metadata.tone_mapping.intensity_target)
97
+ encoding = parsed.basic_info.metadata.colour_encoding
98
+ return linear if encoding.want_icc
99
+
100
+ Color::Transfer.from_linear!(
101
+ linear, encoding.transfer_function, gamma: encoding.gamma,
102
+ intensity_target: parsed.basic_info.metadata.tone_mapping.intensity_target
103
+ )
104
+ end
105
+ private_class_method :convert_colour
106
+
107
+ def normalize_extra_channels(channels, metadata)
108
+ channels.each_with_index.map do |plane, index|
109
+ depth = metadata.extra_channels[index].bit_depth
110
+ unless depth.floating_point
111
+ scale = 1.0 / ((1 << depth.bits_per_sample) - 1)
112
+ next Plane.new(plane.width, plane.height).tap { _1.data.replace(plane.data.map { |value| value * scale }) }
113
+ end
114
+
115
+ unless depth.bits_per_sample == 32 && depth.exponent_bits == 8
116
+ raise UnsupportedFeatureError, "non-binary32 extra channel"
117
+ end
118
+
119
+ Plane.new(plane.width, plane.height).tap do |output|
120
+ output.data.replace(plane.data.map { [Num.u32(_1)].pack("L<").unpack1("e") })
121
+ end
122
+ end
123
+ end
124
+ private_class_method :normalize_extra_channels
125
+
126
+ def crop(source, width, height)
127
+ return source if source.width == width && source.height == height
128
+
129
+ Plane.new(width, height).tap do |output|
130
+ height.times { |y| output.data[y * width, width] = source.data[y * source.width, width] }
131
+ end
132
+ end
133
+ private_class_method :crop
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module VarDCT
5
+ class Dequant # rubocop:disable Metrics/ClassLength
6
+ DEFAULT_DC = [1.0 / 4096, 1.0 / 512, 1.0 / 256].freeze
7
+ TABLE_WIDTHS = [1, 1, 1, 1, 2, 4, 1, 1, 2, 1, 1, 8, 4, 16, 8, 32, 16].freeze
8
+ TABLE_HEIGHTS = [1, 1, 1, 1, 2, 4, 2, 4, 4, 1, 1, 8, 8, 16, 16, 32, 32].freeze
9
+ STRATEGY_TABLE = [0, 1, 2, 3, 4, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 10, 10,
10
+ 11, 12, 12, 13, 14, 14, 15, 16, 16].freeze
11
+ TABLE_LENGTHS = TABLE_WIDTHS.zip(TABLE_HEIGHTS).map { |width, height| 3 * width * height * 64 }.freeze
12
+ TABLE_OFFSETS = TABLE_LENGTHS.each_with_object([0]) do |length, offsets|
13
+ offsets << (offsets.last + length)
14
+ end.freeze
15
+ TABLE_PATH = File.join(__dir__, "default_dequant.bin.z")
16
+
17
+ attr_reader :dc
18
+
19
+ def initialize(dc = DEFAULT_DC)
20
+ @dc = dc
21
+ @tables = nil
22
+ end
23
+
24
+ def self.read_dc(reader)
25
+ return new if Bit::Field.read_bool(reader)
26
+
27
+ dc = Array.new(3) { Bit::Field.f16(reader) / 128.0 }
28
+ raise CorruptError, "invalid DC quantization" unless dc.all?(&:positive?)
29
+
30
+ new(dc.freeze)
31
+ end
32
+
33
+ def matrix(strategy, channel)
34
+ raise ArgumentError, "invalid dequant channel" unless channel.between?(0, 2)
35
+
36
+ table = STRATEGY_TABLE.fetch(strategy.id)
37
+ channel_length = TABLE_LENGTHS.fetch(table) / 3
38
+ offset = TABLE_OFFSETS.fetch(table) + (channel * channel_length)
39
+ (@tables || self.class.default_tables).slice(offset, channel_length)
40
+ end
41
+
42
+ def read_matrices!(reader, state:, metadata:, stream_base:)
43
+ return self if Bit::Field.read_bool(reader)
44
+
45
+ @tables = self.class.default_tables.dup
46
+ TABLE_WIDTHS.each_index do |table|
47
+ mode = reader.read(3)
48
+ next if mode.zero?
49
+
50
+ values = if mode == 7
51
+ read_raw_matrix(reader, state, metadata, stream_base + table, table)
52
+ else
53
+ self.class.read_parametric_matrix(reader, mode, table)
54
+ end
55
+ @tables[TABLE_OFFSETS[table], TABLE_LENGTHS[table]] = values
56
+ end
57
+ self
58
+ end
59
+
60
+ def read_raw_matrix(reader, state, metadata, stream_id, table)
61
+ denominator = Bit::Field.f16(reader)
62
+ raise CorruptError, "invalid raw quantization denominator" unless denominator.positive?
63
+
64
+ width = TABLE_WIDTHS[table] * 8
65
+ height = TABLE_HEIGHTS[table] * 8
66
+ result = Modular::Stream.decode(
67
+ reader, Array.new(3) { [width, height] }, state:, stream_id:, metadata:
68
+ )
69
+ values = result.channels.flat_map(&:data)
70
+ raise CorruptError, "invalid raw quantization matrix" unless values.all?(&:positive?)
71
+
72
+ values.map { _1 * denominator }
73
+ end
74
+ private :read_raw_matrix
75
+
76
+ def self.read_parametric_matrix(reader, mode, table)
77
+ raise CorruptError, "invalid parametric quantization mode" unless TABLE_LENGTHS[table] == 3 * 64 || mode == 6
78
+
79
+ weights = case mode
80
+ when 1 then identity_weights(read_values(reader, 3, 3, scale: 64))
81
+ when 2 then dct2_weights(read_values(reader, 3, 6, scale: 64))
82
+ when 3 then read_dct4_weights(reader)
83
+ when 4 then dct4x8_weights(read_values(reader, 3, 1).flatten, read_dct_params(reader))
84
+ when 5 then read_afv_weights(reader)
85
+ when 6 then dct_weights(TABLE_WIDTHS[table] * 8, TABLE_HEIGHTS[table] * 8,
86
+ read_dct_params(reader))
87
+ else raise CorruptError, "invalid parametric quantization mode"
88
+ end
89
+ raise CorruptError, "invalid parametric quantization matrix" unless weights.all? { _1 >= 1e-8 && _1 < 1e8 }
90
+
91
+ weights.map { 1.0 / _1 }
92
+ end
93
+
94
+ def self.read_values(reader, channels, count, scale: 1)
95
+ Array.new(channels) do
96
+ Array.new(count) do
97
+ value = Bit::Field.f16(reader) * scale
98
+ raise CorruptError, "quantization weight is too small" if value.abs < 1e-8
99
+
100
+ value
101
+ end
102
+ end
103
+ end
104
+ private_class_method :read_values
105
+
106
+ def self.read_dct_params(reader)
107
+ count = reader.read(4) + 1
108
+ bands = Array.new(3) do
109
+ Array.new(count) { Bit::Field.f16(reader) }.tap do |values|
110
+ raise CorruptError, "distance band seed is too small" if values.first < 1e-8
111
+
112
+ values[0] *= 64.0
113
+ end
114
+ end
115
+ [count, bands]
116
+ end
117
+ private_class_method :read_dct_params
118
+
119
+ def self.identity_weights(values)
120
+ values.flat_map do |base, axis, diagonal|
121
+ Array.new(64, base).tap do |weights|
122
+ weights[1] = weights[8] = axis
123
+ weights[9] = diagonal
124
+ end
125
+ end
126
+ end
127
+ private_class_method :identity_weights
128
+
129
+ def self.dct2_weights(values)
130
+ values.flat_map do |channel|
131
+ Array.new(64).tap do |weights|
132
+ weights[0] = 1.0
133
+ weights[1] = weights[8] = channel[0]
134
+ weights[9] = channel[1]
135
+ 2.times do |y|
136
+ 2.times do |x|
137
+ weights[(y * 8) + x + 2] = weights[((y + 2) * 8) + x] = channel[2]
138
+ weights[((y + 2) * 8) + x + 2] = channel[3]
139
+ end
140
+ end
141
+ 4.times do |y|
142
+ 4.times do |x|
143
+ weights[(y * 8) + x + 4] = weights[((y + 4) * 8) + x] = channel[4]
144
+ weights[((y + 4) * 8) + x + 4] = channel[5]
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ private_class_method :dct2_weights
151
+
152
+ def self.dct4_weights(params, multipliers)
153
+ base = dct_weights(4, 4, params)
154
+ 3.times.flat_map do |channel|
155
+ weights = Array.new(64) do |index|
156
+ x = index % 8
157
+ y = index / 8
158
+ base[(channel * 16) + ((y / 2) * 4) + (x / 2)]
159
+ end
160
+ weights[1] /= multipliers[channel][0]
161
+ weights[8] /= multipliers[channel][0]
162
+ weights[9] /= multipliers[channel][1]
163
+ weights
164
+ end
165
+ end
166
+ private_class_method :dct4_weights
167
+
168
+ def self.read_dct4_weights(reader)
169
+ multipliers = read_values(reader, 3, 2)
170
+ dct4_weights(read_dct_params(reader), multipliers)
171
+ end
172
+ private_class_method :read_dct4_weights
173
+
174
+ def self.dct4x8_weights(multipliers, params)
175
+ base = dct_weights(4, 8, params)
176
+ 3.times.flat_map do |channel|
177
+ weights = Array.new(64) do |index|
178
+ x = index % 8
179
+ y = index / 8
180
+ base[(channel * 32) + ((y / 2) * 8) + x]
181
+ end
182
+ weights[8] /= multipliers[channel]
183
+ weights
184
+ end
185
+ end
186
+ private_class_method :dct4x8_weights
187
+
188
+ def self.read_afv_weights(reader)
189
+ special = Array.new(3) do
190
+ Array.new(9) { Bit::Field.f16(reader) }.tap do |values|
191
+ 6.times { |index| values[index] *= 64.0 }
192
+ end
193
+ end
194
+ afv_weights(special, read_dct_params(reader), read_dct_params(reader))
195
+ end
196
+ private_class_method :read_afv_weights
197
+
198
+ def self.afv_weights(special, params4x8, params4x4)
199
+ frequencies = [0, 0, 0.8517778890324296, 5.37778436506804,
200
+ 0, 0, 4.734747904497923, 5.449245381693219,
201
+ 1.6598270267479331, 4, 7.275749096817861, 10.423227632456525,
202
+ 2.662932286148962, 7.630657783650829, 8.962388608184032, 12.97166202570235]
203
+ base4x8 = dct_weights(4, 8, params4x8)
204
+ base4x4 = dct_weights(4, 4, params4x4)
205
+ low = 0.8517778890324296
206
+ high = 12.97166202570235 - low + 1e-6
207
+ 3.times.flat_map do |channel| # rubocop:disable Metrics/BlockLength
208
+ controls = [special[channel][5]]
209
+ 3.times { |index| controls << (controls.last * multiplier(special[channel][index + 6])) }
210
+ raise CorruptError, "invalid AFV distance bands" unless controls.all? { _1 >= 1e-8 }
211
+
212
+ Array.new(64).tap do |weights|
213
+ weights[0] = 1.0
214
+ weights[8] = special[channel][0]
215
+ weights[1] = special[channel][1]
216
+ weights[16] = special[channel][2]
217
+ weights[2] = special[channel][3]
218
+ weights[18] = special[channel][4]
219
+ 4.times do |y|
220
+ 4.times do |x|
221
+ next if x < 2 && y < 2
222
+
223
+ weights[(2 * y * 8) + (2 * x)] = interpolate(frequencies[(y * 4) + x] - low, high, controls)
224
+ end
225
+ end
226
+ 4.times do |y|
227
+ 8.times do |x|
228
+ next if x.zero? && y.zero?
229
+
230
+ weights[(((2 * y) + 1) * 8) + x] = base4x8[(channel * 32) + (y * 8) + x]
231
+ end
232
+ end
233
+ 4.times do |y|
234
+ 4.times do |x|
235
+ next if x.zero? && y.zero?
236
+
237
+ weights[(2 * y * 8) + (2 * x) + 1] = base4x4[(channel * 16) + (y * 4) + x]
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+ private_class_method :afv_weights
244
+
245
+ def self.dct_weights(rows, columns, params)
246
+ count, channels = params
247
+ channels.flat_map do |encoded|
248
+ bands = [encoded.first]
249
+ encoded.drop(1).each { bands << (bands.last * multiplier(_1)) }
250
+ scale = (count - 1) / (Math.sqrt(2.0) + 1e-6)
251
+ Array.new(rows * columns) do |index|
252
+ x = index % columns
253
+ y = index / columns
254
+ distance = Math.hypot(x * scale / (columns - 1), y * scale / (rows - 1))
255
+ count == 1 ? bands.first : interpolate(distance, Math.sqrt(2.0) + 1e-6, bands)
256
+ end
257
+ end
258
+ end
259
+ private_class_method :dct_weights
260
+
261
+ def self.interpolate(position, maximum, values)
262
+ scaled = position * (values.length - 1) / maximum
263
+ index = [scaled.to_i, values.length - 2].min
264
+ a = values[index]
265
+ a * ((values[index + 1] / a)**(scaled - index))
266
+ end
267
+ private_class_method :interpolate
268
+
269
+ def self.multiplier(value) = value.positive? ? 1.0 + value : 1.0 / (1.0 - value)
270
+ private_class_method :multiplier
271
+
272
+ def self.default_tables
273
+ @default_tables ||= begin
274
+ require "zlib"
275
+ bytes = Zlib::Inflate.inflate(File.binread(TABLE_PATH))
276
+ expected = TABLE_OFFSETS.last * 4
277
+ raise InternalError, "invalid default dequant table data" unless bytes.bytesize == expected
278
+
279
+ bytes.unpack("e*").freeze
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module VarDCT
5
+ ORDER_DISTRIBUTION = [Bit::Field.val(0x5F), Bit::Field.val(0x13),
6
+ Bit::Field.val(0), Bit::Field.bits(13)].freeze
7
+ HFPass = Data.define(:orders, :entropy)
8
+ HFGlobal = Data.define(:histogram_count, :passes) do
9
+ def self.read(reader, frame, lf_global, lf_groups, metadata: nil)
10
+ group_dim = 128 << frame.group_size_shift
11
+ dc_groups = Num.ceil_div(frame.blocks_width, group_dim) * Num.ceil_div(frame.blocks_height, group_dim)
12
+ lf_global.dequant.read_matrices!(
13
+ reader, state: lf_global.modular_state, metadata:, stream_base: 1 + (3 * dc_groups)
14
+ )
15
+
16
+ group_count = Num.ceil_div(frame.encoded_width, group_dim) * Num.ceil_div(frame.encoded_height, group_dim)
17
+ histogram_count = 1 + reader.read((group_count - 1).bit_length)
18
+ used = lf_groups.flat_map(&:used_strategies).uniq
19
+ passes = Array.new(frame.passes.count) do
20
+ used_orders = Bit::Field.u32(reader, *ORDER_DISTRIBUTION)
21
+ orders = CoeffOrder.read(reader, used_orders, used)
22
+ contexts = histogram_count * lf_global.block_context_map.ac_context_count
23
+ entropy = Entropy::Decoder.read(reader, contexts, defer_state: true)
24
+ HFPass.new(orders:, entropy:)
25
+ end
26
+ new(histogram_count:, passes:)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module VarDCT
5
+ LFGlobal = Data.define(:dequant, :quantizer, :block_context_map, :chroma_from_luma,
6
+ :modular_state, :modular_group, :extra_channels, :noise, :patches, :splines) do
7
+ def self.read(reader, frame, metadata = nil, references: [])
8
+ patches = if frame.flags.anybits?(2)
9
+ Features::Patches.read(reader, frame.encoded_width, frame.encoded_height,
10
+ metadata.extra_channels.length, references)
11
+ end
12
+ splines = Features::Splines.read(reader, frame.encoded_width, frame.encoded_height) if frame.flags.anybits?(16)
13
+ noise = frame.flags.anybits?(1) ? Array.new(8) { reader.read(10) / 1024.0 } : nil
14
+ dequant = Dequant.read_dc(reader)
15
+ quantizer = Quantizer.read(reader)
16
+ block_context_map = BlockContextMap.read(reader)
17
+ chroma_from_luma = ChromaFromLuma.read_dc(reader)
18
+ modular_state = Modular::Stream.read_global_state(reader)
19
+ shapes = if metadata
20
+ metadata.extra_channels.each_index.map do |index|
21
+ upsampling = frame.extra_channel_upsampling[index]
22
+ [Num.ceil_div(frame.width, upsampling), Num.ceil_div(frame.height, upsampling)]
23
+ end
24
+ else
25
+ []
26
+ end
27
+ shifts = frame.extra_channel_upsampling.first(shapes.length).map do |upsampling|
28
+ shift = upsampling.bit_length - frame.upsampling.bit_length
29
+ [shift, shift]
30
+ end
31
+ modular_group = unless shapes.empty?
32
+ Modular::Decoder.read_grouped_stream(
33
+ reader, shapes, state: modular_state, metadata:,
34
+ group_dim: 128 << frame.group_size_shift, shifts:
35
+ )
36
+ end
37
+ extra_channels = modular_group&.channels || []
38
+ new(dequant:, quantizer:, block_context_map:, chroma_from_luma:, modular_state:,
39
+ modular_group:, extra_channels:, noise:, patches:, splines:)
40
+ end
41
+ end
42
+ end
43
+ end