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,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
|