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,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Entropy
5
+ module Histogram
6
+ LOG_RANGE = 12
7
+ RANGE = 1 << LOG_RANGE
8
+ HUFFMAN_BLOCK = [[3, 10], [7, 12], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
9
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2]].freeze
10
+ LOG_COUNT_HUFFMAN = [HUFFMAN_BLOCK,
11
+ HUFFMAN_BLOCK.dup.tap { |b| b[1] = [5, 0] },
12
+ HUFFMAN_BLOCK.dup.tap { |b| b[1] = [6, 11] },
13
+ HUFFMAN_BLOCK.dup.tap { |b| b[1] = [5, 0] },
14
+ HUFFMAN_BLOCK.dup.tap { |b| b[1] = [7, 13] },
15
+ HUFFMAN_BLOCK.dup.tap { |b| b[1] = [5, 0] },
16
+ HUFFMAN_BLOCK.dup.tap { |b| b[1] = [6, 11] },
17
+ HUFFMAN_BLOCK.dup.tap { |b| b[1] = [5, 0] }].flatten(1).freeze
18
+
19
+ module_function
20
+
21
+ def read(reader)
22
+ return read_simple(reader) unless reader.read(1).zero?
23
+ return flat(varlen8(reader) + 1) unless reader.read(1).zero?
24
+
25
+ read_general(reader)
26
+ end
27
+
28
+ def read_simple(reader)
29
+ symbols = Array.new(reader.read(1) + 1) { varlen8(reader) }
30
+ raise CorruptError, "duplicate histogram symbol" unless symbols.uniq.length == symbols.length
31
+ return Array.new(symbols.first + 1, 0).tap { |counts| counts[symbols.first] = RANGE } if symbols.one?
32
+
33
+ counts = Array.new(symbols.max + 1, 0)
34
+ counts[symbols[0]] = reader.read(LOG_RANGE)
35
+ counts[symbols[1]] = RANGE - counts[symbols[0]]
36
+ raise CorruptError, "empty histogram symbol" if counts.any?(&:negative?)
37
+
38
+ counts
39
+ end
40
+ private_class_method :read_simple
41
+
42
+ def flat(length)
43
+ raise CorruptError, "histogram alphabet is too large" unless length.between?(1, RANGE)
44
+
45
+ quotient, remainder = RANGE.divmod(length)
46
+ Array.new(length) { |index| quotient + (index < remainder ? 1 : 0) }
47
+ end
48
+
49
+ def read_general(reader) # rubocop:disable Metrics/AbcSize
50
+ shift = read_shift(reader)
51
+ length = varlen8(reader) + 3
52
+ counts = Array.new(length, 0)
53
+ log_counts = Array.new(length, 0)
54
+ same = Array.new(length, 0)
55
+ omit_log = -1
56
+ omit_position = -1
57
+ index = 0
58
+ while index < length
59
+ bits, symbol = LOG_COUNT_HUFFMAN.fetch(reader.peek(7))
60
+ reader.skip(bits)
61
+ log_counts[index] = symbol - 1
62
+ if log_counts[index] == LOG_RANGE
63
+ run = varlen8(reader)
64
+ same[index] = run + 5
65
+ index += run + 4
66
+ next
67
+ end
68
+ if log_counts[index] > omit_log
69
+ omit_log = log_counts[index]
70
+ omit_position = index
71
+ end
72
+ index += 1
73
+ end
74
+ if omit_position.negative? || (omit_position + 1 < length && log_counts[omit_position + 1] == LOG_RANGE)
75
+ raise CorruptError, "invalid histogram"
76
+ end
77
+
78
+ fill_counts(reader, counts, log_counts, same, omit_position, shift)
79
+ end
80
+ private_class_method :read_general
81
+
82
+ # rubocop:disable-next Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/ParameterLists,Metrics/PerceivedComplexity
83
+ def fill_counts(reader, counts, log_counts, same, omit_position, shift)
84
+ total = 0
85
+ previous = 0
86
+ remaining = 0
87
+ counts.each_index do |index|
88
+ if same[index].positive?
89
+ remaining = same[index] - 1
90
+ previous = index.positive? ? counts[index - 1] : 0
91
+ end
92
+ if remaining.positive?
93
+ counts[index] = previous
94
+ remaining -= 1
95
+ else
96
+ code = log_counts[index]
97
+ next if index == omit_position || code.negative?
98
+
99
+ precision = population_precision(code, shift)
100
+ counts[index] = (1 << code) + (reader.read(precision) << (code - precision))
101
+ end
102
+ total += counts[index]
103
+ end
104
+ counts[omit_position] = RANGE - total
105
+ raise CorruptError, "histogram counts exceed range" unless counts[omit_position].positive?
106
+
107
+ counts
108
+ end
109
+ private_class_method :fill_counts
110
+
111
+ def read_shift(reader)
112
+ log = 0
113
+ log += 1 while log < 12 && !reader.read(1).zero?
114
+ shift = (reader.read(log) | (1 << log)) - 1
115
+ raise CorruptError, "invalid histogram shift" if shift > LOG_RANGE + 1
116
+
117
+ shift
118
+ end
119
+ private_class_method :read_shift
120
+
121
+ def population_precision(log_count, shift)
122
+ [log_count, shift - ((LOG_RANGE - log_count) >> 1)].min.clamp(0, log_count)
123
+ end
124
+ private_class_method :population_precision
125
+
126
+ def varlen8(reader)
127
+ return 0 if reader.read(1).zero?
128
+
129
+ bits = reader.read(3)
130
+ bits.zero? ? 1 : reader.read(bits) + (1 << bits)
131
+ end
132
+ private_class_method :varlen8
133
+ end
134
+
135
+ AliasEntry = Data.define(:cutoff, :right_symbol, :right_offset, :left_frequency, :right_frequency)
136
+
137
+ class AliasTable
138
+ attr_reader :frequencies, :log_alphabet_size
139
+
140
+ def initialize(distribution, log_alphabet_size)
141
+ @frequencies = distribution.dup
142
+ @log_alphabet_size = log_alphabet_size
143
+ @entries = build(distribution.dup)
144
+ end
145
+
146
+ def lookup(value)
147
+ index = value >> entry_log_size
148
+ position = value & (entry_size - 1)
149
+ entry = @entries.fetch(index)
150
+ if position < entry.cutoff
151
+ [index, position, entry.left_frequency]
152
+ else
153
+ [entry.right_symbol, position + entry.right_offset, entry.right_frequency]
154
+ end
155
+ end
156
+
157
+ def entries = @entries.dup
158
+
159
+ private
160
+
161
+ def entry_log_size = Histogram::LOG_RANGE - log_alphabet_size
162
+ def entry_size = 1 << entry_log_size
163
+
164
+ # rubocop:disable-next Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
165
+ def build(distribution)
166
+ distribution.pop while distribution.last&.zero?
167
+ distribution = [Histogram::RANGE] if distribution.empty?
168
+ table_size = 1 << log_alphabet_size
169
+ raise CorruptError, "ANS alphabet is too large" if distribution.length > table_size
170
+ raise CorruptError, "ANS frequencies do not sum to 4096" unless distribution.sum == Histogram::RANGE
171
+
172
+ single = distribution.index(Histogram::RANGE)
173
+ if single
174
+ return Array.new(table_size) do |index|
175
+ AliasEntry.new(cutoff: 0, right_symbol: single, right_offset: entry_size * index,
176
+ left_frequency: 0, right_frequency: Histogram::RANGE)
177
+ end
178
+ end
179
+
180
+ cutoffs = distribution + Array.new(table_size - distribution.length, 0)
181
+ underfull = []
182
+ overfull = []
183
+ cutoffs.each_index do |index|
184
+ (cutoffs[index] > entry_size ? overfull : underfull) << index unless cutoffs[index] == entry_size
185
+ end
186
+ right_symbols = Array.new(table_size)
187
+ right_offsets = Array.new(table_size, 0)
188
+ until overfull.empty?
189
+ over = overfull.pop
190
+ under = underfull.pop or raise CorruptError, "invalid ANS distribution"
191
+ cutoffs[over] -= entry_size - cutoffs[under]
192
+ right_symbols[under] = over
193
+ right_offsets[under] = cutoffs[over]
194
+ (cutoffs[over] > entry_size ? overfull : underfull) << over unless cutoffs[over] == entry_size
195
+ end
196
+
197
+ Array.new(table_size) do |index|
198
+ if cutoffs[index] == entry_size
199
+ right_symbols[index] = index
200
+ cutoffs[index] = 0
201
+ else
202
+ right_offsets[index] -= cutoffs[index]
203
+ end
204
+ left = distribution.fetch(index, 0)
205
+ right = distribution.fetch(right_symbols[index], 0)
206
+ AliasEntry.new(cutoff: cutoffs[index], right_symbol: right_symbols[index],
207
+ right_offset: right_offsets[index], left_frequency: left, right_frequency: right)
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Entropy
5
+ LZ77 = Data.define(:enabled, :min_symbol, :min_length, :length_config, :distance_context)
6
+
7
+ class Decoder
8
+ WINDOW_SIZE = 1 << 20
9
+ SPECIAL_DISTANCES = [
10
+ [0, 1], [1, 0], [1, 1], [-1, 1], [0, 2], [2, 0], [1, 2], [-1, 2],
11
+ [2, 1], [-2, 1], [2, 2], [-2, 2], [0, 3], [3, 0], [1, 3], [-1, 3],
12
+ [3, 1], [-3, 1], [2, 3], [-2, 3], [3, 2], [-3, 2], [0, 4], [4, 0],
13
+ [1, 4], [-1, 4], [4, 1], [-4, 1], [3, 3], [-3, 3], [2, 4], [-2, 4],
14
+ [4, 2], [-4, 2], [0, 5], [3, 4], [-3, 4], [4, 3], [-4, 3], [5, 0],
15
+ [1, 5], [-1, 5], [5, 1], [-5, 1], [2, 5], [-2, 5], [5, 2], [-5, 2],
16
+ [4, 4], [-4, 4], [3, 5], [-3, 5], [5, 3], [-5, 3], [0, 6], [6, 0],
17
+ [1, 6], [-1, 6], [6, 1], [-6, 1], [2, 6], [-2, 6], [6, 2], [-6, 2],
18
+ [4, 5], [-4, 5], [5, 4], [-5, 4], [3, 6], [-3, 6], [6, 3], [-6, 3],
19
+ [0, 7], [7, 0], [1, 7], [-1, 7], [5, 5], [-5, 5], [7, 1], [-7, 1],
20
+ [4, 6], [-4, 6], [6, 4], [-6, 4], [2, 7], [-2, 7], [7, 2], [-7, 2],
21
+ [3, 7], [-3, 7], [7, 3], [-7, 3], [5, 6], [-5, 6], [6, 5], [-6, 5],
22
+ [8, 0], [4, 7], [-4, 7], [7, 4], [-7, 4], [8, 1], [8, 2], [6, 6],
23
+ [-6, 6], [8, 3], [5, 7], [-5, 7], [7, 5], [-7, 5], [8, 4], [6, 7],
24
+ [-6, 7], [7, 6], [-7, 6], [8, 5], [7, 7], [-7, 7], [8, 6], [8, 7]
25
+ ].freeze
26
+
27
+ attr_reader :context_map
28
+
29
+ def self.decode_context_map(reader, count) = read_context_map(reader, count)
30
+
31
+ # rubocop:disable-next Metrics/CyclomaticComplexity
32
+ def self.read(reader, context_count, disallow_lz77: false, distance_multiplier: 0, defer_state: false)
33
+ raise ResourceLimitError, "too many entropy contexts" unless context_count.between?(1, 65_536)
34
+
35
+ lz77 = read_lz77(reader)
36
+ raise CorruptError, "LZ77 is not allowed here" if lz77.enabled && disallow_lz77
37
+
38
+ context_count += 1 if lz77.enabled
39
+ length_config = lz77.enabled ? HybridUint.read(reader, 8) : HybridUint.new
40
+ context_map = context_count > 1 ? read_context_map(reader, context_count) : [0]
41
+ histogram_count = context_map.max + 1
42
+ prefix = !reader.read(1).zero?
43
+ log_alphabet_size = prefix ? 15 : reader.read(2) + 5
44
+ configs = Array.new(histogram_count) { HybridUint.read(reader, log_alphabet_size) }
45
+ codes = read_codes(reader, histogram_count, log_alphabet_size, prefix)
46
+ distance_context = context_map.last
47
+ lz77 = lz77.with(length_config:, distance_context:)
48
+ new(reader, context_map, configs, codes, prefix, lz77, distance_multiplier, defer_state)
49
+ end
50
+
51
+ # rubocop:disable-next Metrics/ParameterLists
52
+ def initialize(reader, context_map, configs, codes, prefix, lz77, distance_multiplier, defer_state)
53
+ @reader = reader
54
+ @context_map = context_map
55
+ @configs = configs
56
+ @codes = codes
57
+ @prefix = prefix
58
+ @lz77 = lz77
59
+ @default_distance_multiplier = distance_multiplier
60
+ start unless defer_state
61
+ end
62
+
63
+ def start(distance_multiplier: @default_distance_multiplier)
64
+ raise InternalError, "entropy decoder already started" if @started
65
+
66
+ @state = @prefix ? 0x13_0000 : @reader.read(32)
67
+ @special_distances = if distance_multiplier.zero?
68
+ []
69
+ else
70
+ SPECIAL_DISTANCES.map do |x, y|
71
+ [x + (distance_multiplier * y), 1].max
72
+ end
73
+ end
74
+ initialize_window if @lz77.enabled
75
+ @started = true
76
+ self
77
+ end
78
+
79
+ def fork(reader, distance_multiplier:)
80
+ self.class.new(reader, @context_map, @configs, @codes, @prefix, @lz77, distance_multiplier, false)
81
+ end
82
+
83
+ def read_uint(context) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
84
+ raise InternalError, "entropy decoder has not started" unless @started
85
+
86
+ max_context = context_map.length - (@lz77.enabled ? 2 : 1)
87
+ raise CorruptError, "invalid entropy context" unless context.between?(0, max_context)
88
+ return copy_symbol(context) if @copy_remaining&.positive?
89
+
90
+ histogram = context_map.fetch(context)
91
+ token = read_symbol(histogram)
92
+ Trace.token(histogram, token)
93
+ if @lz77.enabled && token >= @lz77.min_symbol
94
+ start_copy(token)
95
+ return copy_symbol(context)
96
+ end
97
+
98
+ value = @configs.fetch(histogram).decode(token, @reader)
99
+ store(value) if @lz77.enabled
100
+ Trace.symbol(context, value)
101
+ value
102
+ end
103
+
104
+ def final_state!
105
+ raise CorruptError, "invalid ANS final state (#{@state})" unless @state == 0x13_0000
106
+
107
+ true
108
+ end
109
+
110
+ def self.read_context_map(reader, count) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
111
+ map = Array.new(count, 0)
112
+ if reader.read(1).zero?
113
+ use_mtf = !reader.read(1).zero?
114
+ nested = read(reader, 1, disallow_lz77: count <= 2)
115
+ map.map! { nested.read_uint(0) }
116
+ nested.final_state!
117
+ inverse_mtf!(map) if use_mtf
118
+ else
119
+ bits = reader.read(2)
120
+ map.map! { reader.read(bits) } unless bits.zero?
121
+ end
122
+ raise CorruptError, "context map has too many clusters" if map.max >= 256
123
+
124
+ expected = (0..map.max).to_a
125
+ raise CorruptError, "incomplete context map" unless map.uniq.sort == expected
126
+
127
+ map
128
+ end
129
+ private_class_method :read_context_map
130
+
131
+ def self.inverse_mtf!(map)
132
+ values = (0..255).to_a
133
+ map.map! do |index|
134
+ raise CorruptError, "invalid move-to-front index" if index >= values.length
135
+
136
+ value = values.delete_at(index)
137
+ values.unshift(value)
138
+ value
139
+ end
140
+ end
141
+ private_class_method :inverse_mtf!
142
+
143
+ def self.read_lz77(reader)
144
+ enabled = !reader.read(1).zero?
145
+ unless enabled
146
+ return LZ77.new(enabled: false, min_symbol: 0, min_length: 0, length_config: nil,
147
+ distance_context: nil)
148
+ end
149
+
150
+ min_symbol = Bit::Field.u32(reader, Bit::Field.val(224), Bit::Field.val(512), Bit::Field.val(4096),
151
+ Bit::Field.bits_offset(15, 8))
152
+ min_length = Bit::Field.u32(reader, Bit::Field.val(3), Bit::Field.val(4),
153
+ Bit::Field.bits_offset(2, 5), Bit::Field.bits_offset(8, 9))
154
+ LZ77.new(enabled: true, min_symbol:, min_length:, length_config: nil, distance_context: nil)
155
+ end
156
+ private_class_method :read_lz77
157
+
158
+ def self.read_codes(reader, count, log_alphabet_size, prefix)
159
+ if prefix
160
+ sizes = Array.new(count) { varlen16(reader) + 1 }
161
+ limit = 1 << log_alphabet_size
162
+ raise CorruptError, "prefix alphabet is too large" if sizes.any? { |size| size > limit }
163
+
164
+ sizes.map { |size| size == 1 ? PrefixCode.constant(0) : PrefixCode.read(reader, size) }
165
+ else
166
+ Array.new(count) { AliasTable.new(Histogram.read(reader), log_alphabet_size) }
167
+ end
168
+ end
169
+ private_class_method :read_codes
170
+
171
+ def self.varlen16(reader)
172
+ return 0 if reader.read(1).zero?
173
+
174
+ bits = reader.read(4)
175
+ bits.zero? ? 1 : reader.read(bits) + (1 << bits)
176
+ end
177
+ private_class_method :varlen16
178
+
179
+ private
180
+
181
+ def read_symbol(histogram)
182
+ return @codes.fetch(histogram).read(@reader) if @prefix
183
+
184
+ residue = @state & 4095
185
+ symbol, offset, frequency = @codes.fetch(histogram).lookup(residue)
186
+ @state = (frequency * (@state >> 12)) + offset
187
+ @state = (@state << 16) | @reader.read(16) if @state < (1 << 16)
188
+ symbol
189
+ end
190
+
191
+ def initialize_window
192
+ @window = Array.new(WINDOW_SIZE, 0)
193
+ @decoded = 0
194
+ @copy_position = 0
195
+ @copy_remaining = 0
196
+ end
197
+
198
+ def start_copy(token)
199
+ @copy_remaining = @lz77.length_config.decode(token - @lz77.min_symbol, @reader) + @lz77.min_length
200
+ distance_token = read_symbol(@lz77.distance_context)
201
+ distance = @configs.fetch(@lz77.distance_context).decode(distance_token, @reader)
202
+ distance = if distance < @special_distances.length
203
+ @special_distances[distance]
204
+ else
205
+ distance + 1 - @special_distances.length
206
+ end
207
+ distance = [distance, @decoded, WINDOW_SIZE].min
208
+ @copy_position = @decoded - distance
209
+ end
210
+
211
+ def copy_symbol(context)
212
+ value = @window[@copy_position & (WINDOW_SIZE - 1)]
213
+ @copy_position += 1
214
+ @copy_remaining -= 1
215
+ store(value)
216
+ Trace.symbol(context, value)
217
+ value
218
+ end
219
+
220
+ def store(value)
221
+ @window[@decoded & (WINDOW_SIZE - 1)] = value
222
+ @decoded += 1
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Entropy
5
+ class Encoder
6
+ def initialize(values)
7
+ @config = HybridUint.new(split_exponent: 0, msb_in_token: 0, lsb_in_token: 0)
8
+ @symbols = encode(values)
9
+ @alphabet_size = alphabet_size
10
+ end
11
+
12
+ def write_header(writer, context_count: 1)
13
+ writer.write(1, 0) # no LZ77
14
+ writer.write(1, 1).write(2, 0) if context_count > 1 # one histogram for every context
15
+ writer.write(1, 1) # prefix coding
16
+ writer.write(4, 0) # HybridUint split exponent 0
17
+ write_varlen16(writer, @alphabet_size - 1)
18
+ write_uniform_prefix_code(writer) if @alphabet_size > 1
19
+ writer
20
+ end
21
+
22
+ def write_symbols(writer, values = nil)
23
+ return writer if @alphabet_size == 1
24
+
25
+ width = @alphabet_size.bit_length - 1
26
+ (values ? encode(values) : @symbols).each do |token, extra_count, extra_bits|
27
+ raise ArgumentError, "symbol exceeds entropy alphabet" if token >= @alphabet_size
28
+
29
+ writer.write(width, reverse_bits(token, width))
30
+ writer.write(extra_count, extra_bits)
31
+ end
32
+ writer
33
+ end
34
+
35
+ def write(writer, context_count: 1)
36
+ write_header(writer, context_count:)
37
+ write_symbols(writer)
38
+ end
39
+
40
+ private
41
+
42
+ def alphabet_size
43
+ maximum = @symbols.map(&:first).max || 0
44
+ maximum.zero? ? 1 : 1 << maximum.bit_length
45
+ end
46
+
47
+ def encode(values) = values.map { @config.encode(_1) }
48
+
49
+ def write_varlen16(writer, value)
50
+ return writer.write(1, 0) if value.zero?
51
+
52
+ width = value.bit_length - 1
53
+ writer.write(1, 1).write(4, width).write(width, value - (1 << width))
54
+ end
55
+
56
+ def write_uniform_prefix_code(writer)
57
+ length = @alphabet_size.bit_length - 1
58
+ writer.write(2, 0)
59
+ PrefixCode::CODE_LENGTH_ORDER.each do |symbol|
60
+ symbol == length ? writer.write(4, 7) : writer.write(2, 0)
61
+ end
62
+ end
63
+
64
+ def reverse_bits(value, width)
65
+ width.times.reduce(0) { |result, bit| (result << 1) | value[bit] }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Entropy
5
+ HybridUint = Data.define(:split_exponent, :msb_in_token, :lsb_in_token) do
6
+ def initialize(split_exponent: 4, msb_in_token: 2, lsb_in_token: 0)
7
+ unless split_exponent.between?(0, 15) && msb_in_token >= 0 && lsb_in_token >= 0 &&
8
+ msb_in_token + lsb_in_token <= split_exponent
9
+ raise CorruptError, "invalid hybrid integer configuration"
10
+ end
11
+
12
+ super
13
+ end
14
+
15
+ def split_token = 1 << split_exponent
16
+
17
+ def decode(token, reader)
18
+ return token if token < split_token
19
+
20
+ token_bits = msb_in_token + lsb_in_token
21
+ extra_bits = split_exponent - token_bits + ((token - split_token) >> token_bits)
22
+ extra_bits &= 31
23
+
24
+ low = token & ((1 << lsb_in_token) - 1)
25
+ token >>= lsb_in_token
26
+ high = (1 << msb_in_token) | (token & ((1 << msb_in_token) - 1))
27
+ Num.u32((((high << extra_bits) | reader.read(extra_bits)) << lsb_in_token) | low)
28
+ end
29
+
30
+ def encode(value) # rubocop:disable Metrics/AbcSize
31
+ return [value, 0, 0] if value < split_token
32
+
33
+ exponent = value.bit_length - 1
34
+ mantissa = value - (1 << exponent)
35
+ token = split_token + ((exponent - split_exponent) << (msb_in_token + lsb_in_token)) +
36
+ ((mantissa >> (exponent - msb_in_token)) << lsb_in_token) +
37
+ (mantissa & ((1 << lsb_in_token) - 1))
38
+ bit_count = exponent - msb_in_token - lsb_in_token
39
+ bits = (value >> lsb_in_token) & ((1 << bit_count) - 1)
40
+ [token, bit_count, bits]
41
+ end
42
+
43
+ def self.read(reader, log_alphabet_size)
44
+ split = reader.read(Num.ceil_log2(log_alphabet_size + 1))
45
+ return new(split_exponent: split, msb_in_token: 0, lsb_in_token: 0) if split == log_alphabet_size
46
+
47
+ msb = reader.read(Num.ceil_log2(split + 1))
48
+ raise CorruptError, "invalid hybrid integer configuration" if msb > split
49
+
50
+ lsb = reader.read(Num.ceil_log2(split - msb + 1))
51
+ new(split_exponent: split, msb_in_token: msb, lsb_in_token: lsb)
52
+ end
53
+ end
54
+ end
55
+ end