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,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module Container
|
|
5
|
+
class Parser
|
|
6
|
+
attr_reader :codestream, :metadata, :level
|
|
7
|
+
|
|
8
|
+
def initialize(data, ignore_brob: false)
|
|
9
|
+
@data = data.b
|
|
10
|
+
@metadata = {}
|
|
11
|
+
@level = 5
|
|
12
|
+
@boxes = []
|
|
13
|
+
parse(ignore_brob)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def each_box(&block)
|
|
17
|
+
return @boxes.each unless block
|
|
18
|
+
|
|
19
|
+
@boxes.each(&block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def parse(ignore_brob)
|
|
25
|
+
case Signature.detect(@data)
|
|
26
|
+
when :codestream then @codestream = @data
|
|
27
|
+
when :container then parse_container(ignore_brob)
|
|
28
|
+
else raise SignatureError, "not a JPEG XL stream"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse_container(ignore_brob)
|
|
33
|
+
offset = 0
|
|
34
|
+
while offset < @data.bytesize
|
|
35
|
+
box, offset = read_box(offset)
|
|
36
|
+
@boxes << box
|
|
37
|
+
end
|
|
38
|
+
validate_signature_box
|
|
39
|
+
collect_contents(ignore_brob)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def read_box(offset)
|
|
43
|
+
ensure_available(offset, 8)
|
|
44
|
+
size32, type = @data.byteslice(offset, 8).unpack("Na4")
|
|
45
|
+
header_size = 8
|
|
46
|
+
size = size32
|
|
47
|
+
if size32 == 1
|
|
48
|
+
ensure_available(offset, 16)
|
|
49
|
+
size = @data.unpack1("Q>", offset: offset + 8)
|
|
50
|
+
header_size = 16
|
|
51
|
+
elsif size32.zero?
|
|
52
|
+
size = @data.bytesize - offset
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
uuid = nil
|
|
56
|
+
if type == "uuid"
|
|
57
|
+
ensure_available(offset, header_size + 16)
|
|
58
|
+
uuid = @data.byteslice(offset + header_size, 16)
|
|
59
|
+
header_size += 16
|
|
60
|
+
end
|
|
61
|
+
raise CorruptError, "box size smaller than header" if size < header_size
|
|
62
|
+
|
|
63
|
+
ensure_available(offset, size)
|
|
64
|
+
payload = @data.byteslice(offset + header_size, size - header_size)
|
|
65
|
+
[Box.new(type:, payload:, size:, offset:, uuid:), offset + size]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ensure_available(offset, length)
|
|
69
|
+
return if offset >= 0 && length >= 0 && offset + length <= @data.bytesize
|
|
70
|
+
|
|
71
|
+
raise TruncatedError, "truncated JPEG XL box"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def validate_signature_box
|
|
75
|
+
first = @boxes.first
|
|
76
|
+
return if first&.type == "JXL " && first.size == 12 && first.payload == "\r\n\x87\n".b
|
|
77
|
+
|
|
78
|
+
raise SignatureError, "invalid JPEG XL container signature"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def collect_contents(ignore_brob)
|
|
82
|
+
chunks = []
|
|
83
|
+
chunk_kind = nil
|
|
84
|
+
expected_part = 0
|
|
85
|
+
final_part_seen = false
|
|
86
|
+
|
|
87
|
+
@boxes.drop(1).each do |box|
|
|
88
|
+
case box.type
|
|
89
|
+
when "ftyp" then validate_file_type(box)
|
|
90
|
+
when "jxll" then read_level(box)
|
|
91
|
+
when "jxlc"
|
|
92
|
+
raise CorruptError, "mixed jxlc and jxlp boxes" if chunk_kind == :parts
|
|
93
|
+
raise CorruptError, "multiple jxlc boxes" unless chunks.empty?
|
|
94
|
+
|
|
95
|
+
chunk_kind = :single
|
|
96
|
+
chunks << box.payload
|
|
97
|
+
when "jxlp"
|
|
98
|
+
raise CorruptError, "mixed jxlc and jxlp boxes" if chunk_kind == :single
|
|
99
|
+
raise CorruptError, "jxlp after final part" if final_part_seen
|
|
100
|
+
|
|
101
|
+
chunk_kind = :parts
|
|
102
|
+
index = box.payload&.unpack1("N") || raise(TruncatedError, "missing jxlp index")
|
|
103
|
+
final_part_seen = !index.nobits?(0x8000_0000)
|
|
104
|
+
raise CorruptError, "out-of-order jxlp box" unless (index & 0x7FFF_FFFF) == expected_part
|
|
105
|
+
|
|
106
|
+
expected_part += 1
|
|
107
|
+
chunks << box.payload.byteslice(4..)
|
|
108
|
+
when "Exif" then @metadata[:exif] = box.payload
|
|
109
|
+
when "xml " then @metadata[:xmp] = box.payload
|
|
110
|
+
when "jumb" then @metadata[:jumbf] = box.payload
|
|
111
|
+
when "jbrd" then @metadata[:jpeg_reconstruction] = box.payload
|
|
112
|
+
when "brob"
|
|
113
|
+
raise UnsupportedFeatureError, "Brotli-compressed box" unless ignore_brob
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
raise CorruptError, "missing final jxlp box" if chunk_kind == :parts && !final_part_seen
|
|
117
|
+
raise CorruptError, "missing JPEG XL codestream box" if chunks.empty?
|
|
118
|
+
|
|
119
|
+
@codestream = chunks.join.b
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def validate_file_type(box)
|
|
123
|
+
raise CorruptError, "invalid JPEG XL file type box" unless box.payload&.start_with?("jxl \x00\x00\x00\x00".b)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def read_level(box)
|
|
127
|
+
valid = box.payload&.bytesize == 1 && [5, 10].include?(box.payload.getbyte(0))
|
|
128
|
+
raise CorruptError, "invalid jxll box" unless valid
|
|
129
|
+
|
|
130
|
+
@level = box.payload.getbyte(0)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module Container
|
|
5
|
+
module Signature
|
|
6
|
+
CODESTREAM = "\xFF\x0A".b
|
|
7
|
+
CONTAINER = "\x00\x00\x00\x0CJXL \r\n\x87\n".b
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def detect(data)
|
|
12
|
+
return :codestream if data.start_with?(CODESTREAM)
|
|
13
|
+
return :container if data.start_with?(CONTAINER)
|
|
14
|
+
|
|
15
|
+
:unknown
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/jxl/dct.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module DCT
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
SQRT2 = Math.sqrt(2.0)
|
|
8
|
+
|
|
9
|
+
def inverse_matrix(size)
|
|
10
|
+
@inverse_matrices ||= {}
|
|
11
|
+
@inverse_matrices[size] ||= Array.new(size) do |x|
|
|
12
|
+
Array.new(size) do |frequency|
|
|
13
|
+
frequency.zero? ? 1.0 : SQRT2 * Math.cos(((x + 0.5) * frequency * Math::PI) / size)
|
|
14
|
+
end.freeze
|
|
15
|
+
end.freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def forward_matrix(size)
|
|
19
|
+
@forward_matrices ||= {}
|
|
20
|
+
@forward_matrices[size] ||= inverse_matrix(size).transpose.map { |row| row.map { _1 / size }.freeze }.freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def idct1d(coefficients)
|
|
24
|
+
multiply(inverse_matrix(coefficients.length), coefficients)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dct1d(samples)
|
|
28
|
+
multiply(forward_matrix(samples.length), samples)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def idct2d(coefficients, width, height)
|
|
32
|
+
transform2d(coefficients, width, height, inverse_matrix(width), inverse_matrix(height))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def dct2d(samples, width, height)
|
|
36
|
+
transform2d(samples, width, height, forward_matrix(width), forward_matrix(height))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def resample_scale(from, to, frequency)
|
|
40
|
+
raise ArgumentError, "invalid DCT resampling ratio" unless to == from * 8
|
|
41
|
+
|
|
42
|
+
scale = 1.0
|
|
43
|
+
size = to
|
|
44
|
+
while size > from
|
|
45
|
+
scale /= Math.cos((frequency * Math::PI) / (2 * size))
|
|
46
|
+
size /= 2
|
|
47
|
+
end
|
|
48
|
+
scale
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def transform2d(values, width, height, horizontal, vertical) # rubocop:disable Metrics/AbcSize
|
|
52
|
+
raise ArgumentError, "block dimensions do not match" unless values.length == width * height
|
|
53
|
+
|
|
54
|
+
rows = Array.new(values.length)
|
|
55
|
+
height.times do |y|
|
|
56
|
+
width.times do |x|
|
|
57
|
+
row = horizontal[x]
|
|
58
|
+
sum = 0.0
|
|
59
|
+
width.times { |frequency| sum += row[frequency] * values[(y * width) + frequency] }
|
|
60
|
+
rows[(y * width) + x] = sum
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
columns = Array.new(values.length)
|
|
64
|
+
height.times do |y|
|
|
65
|
+
width.times do |x|
|
|
66
|
+
row = vertical[y]
|
|
67
|
+
sum = 0.0
|
|
68
|
+
height.times { |frequency| sum += row[frequency] * rows[(frequency * width) + x] }
|
|
69
|
+
columns[(y * width) + x] = sum
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
columns
|
|
73
|
+
end
|
|
74
|
+
private_class_method :transform2d
|
|
75
|
+
|
|
76
|
+
def multiply(matrix, vector)
|
|
77
|
+
matrix.map { |row| row.each_with_index.sum { |value, i| value * vector[i] } }
|
|
78
|
+
end
|
|
79
|
+
private_class_method :multiply
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/jxl/decoder.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
Event = Data.define(:type, :basic_info, :frame, :image) do
|
|
5
|
+
def width = basic_info&.width || image&.width
|
|
6
|
+
def height = basic_info&.height || image&.height
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Decoder
|
|
10
|
+
EVENTS = %i[basic_info color_encoding frame full_image].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(**options)
|
|
13
|
+
@options = options
|
|
14
|
+
@buffer = +"".b
|
|
15
|
+
@events = []
|
|
16
|
+
@subscriptions = EVENTS
|
|
17
|
+
@finished = false
|
|
18
|
+
@decoded = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Selects which decoder events are queued.
|
|
22
|
+
def subscribe(*events)
|
|
23
|
+
unknown = events - EVENTS
|
|
24
|
+
raise ArgumentError, "unknown decoder event: #{unknown.first}" unless unknown.empty?
|
|
25
|
+
|
|
26
|
+
@subscriptions = events
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Appends compressed input and emits any available metadata events.
|
|
31
|
+
def feed(bytes)
|
|
32
|
+
raise IOError, "decoder is finished" if @finished
|
|
33
|
+
raise ArgumentError, "input must be a String" unless bytes.is_a?(String)
|
|
34
|
+
|
|
35
|
+
@buffer << bytes.b
|
|
36
|
+
limit = @options.fetch(:max_memory, 2 << 30)
|
|
37
|
+
raise ResourceLimitError, "compressed input exceeds memory limit" if limit && @buffer.bytesize > limit
|
|
38
|
+
|
|
39
|
+
read_basic_info
|
|
40
|
+
read_image
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Marks input complete and emits frame and full-image events.
|
|
45
|
+
def finish
|
|
46
|
+
@finished = true
|
|
47
|
+
return self if @decoded
|
|
48
|
+
|
|
49
|
+
read_basic_info(force: true)
|
|
50
|
+
read_image(force: true)
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns and removes the next subscribed event.
|
|
55
|
+
def next_event = @events.shift
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def read_basic_info(force: false)
|
|
60
|
+
return if @basic_info
|
|
61
|
+
|
|
62
|
+
@basic_info = BasicInfo.read(@buffer, max_pixels: @options.fetch(:max_pixels, 100_000_000))
|
|
63
|
+
push(Event.new(type: :basic_info, basic_info: @basic_info, frame: nil, image: nil))
|
|
64
|
+
push(Event.new(type: :color_encoding, basic_info: @basic_info, frame: nil, image: nil))
|
|
65
|
+
rescue FormatError
|
|
66
|
+
raise if force
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def read_image(force: false)
|
|
70
|
+
return unless @basic_info
|
|
71
|
+
return if @decoded
|
|
72
|
+
|
|
73
|
+
image = JXL.decode(@buffer, **@options)
|
|
74
|
+
image.frames.each { |frame| push(Event.new(type: :frame, basic_info: @basic_info, frame:, image: nil)) }
|
|
75
|
+
push(Event.new(type: :full_image, basic_info: @basic_info, frame: nil, image:))
|
|
76
|
+
@decoded = true
|
|
77
|
+
rescue FormatError
|
|
78
|
+
raise if force
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def push(event)
|
|
82
|
+
@events << event if @subscriptions.include?(event.type)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/jxl/encoder.rb
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
class Encoder # rubocop:disable Metrics/ClassLength
|
|
5
|
+
SIZE_DISTRIBUTION = Headers::SizeHeader::DIMENSION
|
|
6
|
+
|
|
7
|
+
def initialize(image, bits_per_sample: 8)
|
|
8
|
+
raise ArgumentError, "encoder requires RGB" unless image.channels.length >= 3
|
|
9
|
+
raise ArgumentError, "unsupported bit depth" unless bits_per_sample.between?(1, 16)
|
|
10
|
+
|
|
11
|
+
@image = image
|
|
12
|
+
@bits_per_sample = bits_per_sample
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def encode
|
|
16
|
+
sections = frame_sections
|
|
17
|
+
writer = Bit::Writer.new
|
|
18
|
+
write_image_header(writer)
|
|
19
|
+
writer.align
|
|
20
|
+
write_frame_header(writer)
|
|
21
|
+
writer.write(1, 0).align # TOC is not permuted
|
|
22
|
+
sections.each { Bit::Field.write_u32(writer, _1.bytesize, *Frame::TOC_DISTRIBUTIONS) }
|
|
23
|
+
writer.align
|
|
24
|
+
Container::Signature::CODESTREAM + writer.bytes + sections.join
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def write_image_header(writer)
|
|
30
|
+
write_size(writer)
|
|
31
|
+
writer.write(1, 0) # custom metadata
|
|
32
|
+
writer.write(1, 0) # no optional metadata
|
|
33
|
+
writer.write(1, 0) # integer samples
|
|
34
|
+
Bit::Field.write_u32(writer, @bits_per_sample, Bit::Field.val(8), Bit::Field.val(10),
|
|
35
|
+
Bit::Field.val(12), Bit::Field.bits_offset(6, 1))
|
|
36
|
+
writer.write(1, 1) # 16-bit modular buffer
|
|
37
|
+
Bit::Field.write_u32(writer, 0, Bit::Field.val(0), Bit::Field.val(1),
|
|
38
|
+
Bit::Field.bits_offset(4, 2), Bit::Field.bits_offset(12, 1))
|
|
39
|
+
writer.write(1, 0) # RGB, not XYB
|
|
40
|
+
writer.write(1, 1) # default sRGB colour encoding
|
|
41
|
+
Bit::Field.write_u64(writer, 0) # metadata extensions
|
|
42
|
+
writer.write(1, 1) # default transform data
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def write_size(writer)
|
|
46
|
+
writer.write(1, 0)
|
|
47
|
+
Bit::Field.write_u32(writer, @image.height, *SIZE_DISTRIBUTION)
|
|
48
|
+
writer.write(3, 0)
|
|
49
|
+
Bit::Field.write_u32(writer, @image.width, *SIZE_DISTRIBUTION)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def write_frame_header(writer)
|
|
53
|
+
writer.write(1, 0).write(2, 0).write(1, 1) # custom regular Modular frame
|
|
54
|
+
Bit::Field.write_u64(writer, 0)
|
|
55
|
+
writer.write(1, 0) # no colour transform
|
|
56
|
+
Bit::Field.write_u32(writer, 1, *Headers::FRAME_UPSAMPLING_DISTRIBUTION)
|
|
57
|
+
writer.write(2, 3) # 1024-pixel groups
|
|
58
|
+
Bit::Field.write_u32(writer, 1, Bit::Field.val(1), Bit::Field.val(2),
|
|
59
|
+
Bit::Field.val(3), Bit::Field.bits_offset(3, 4))
|
|
60
|
+
writer.write(1, 0) # full-size frame
|
|
61
|
+
Bit::Field.write_u32(writer, 0, Bit::Field.val(0), Bit::Field.val(1),
|
|
62
|
+
Bit::Field.val(2), Bit::Field.bits_offset(2, 3))
|
|
63
|
+
writer.write(1, 1) # last frame
|
|
64
|
+
Bit::Field.write_u32(writer, 0, Bit::Field.val(0), Bit::Field.bits(4),
|
|
65
|
+
Bit::Field.bits_offset(5, 16), Bit::Field.bits_offset(10, 48))
|
|
66
|
+
writer.write(1, 0).write(1, 0).write(2, 0) # no loop filters
|
|
67
|
+
Bit::Field.write_u64(writer, 0) # loop-filter extensions
|
|
68
|
+
Bit::Field.write_u64(writer, 0) # frame extensions
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def frame_sections # rubocop:disable Metrics/AbcSize
|
|
72
|
+
channels = transformed_channels
|
|
73
|
+
return [single_group_section(channels)] if @image.width <= 1024 && @image.height <= 1024
|
|
74
|
+
|
|
75
|
+
groups_x = Num.ceil_div(@image.width, 1024)
|
|
76
|
+
groups_y = Num.ceil_div(@image.height, 1024)
|
|
77
|
+
groups = Array.new(groups_x * groups_y) do |group|
|
|
78
|
+
x = (group % groups_x) * 1024
|
|
79
|
+
y = (group / groups_x) * 1024
|
|
80
|
+
width = [1024, @image.width - x].min
|
|
81
|
+
height = [1024, @image.height - y].min
|
|
82
|
+
channels.flat_map { residuals(region(_1, x, y, width, height), width, height) }
|
|
83
|
+
end
|
|
84
|
+
packed = groups.flatten.map { Num.pack_signed(_1) }
|
|
85
|
+
entropy = Entropy::Encoder.new(packed)
|
|
86
|
+
global = global_section(entropy)
|
|
87
|
+
dc_groups = Num.ceil_div(Num.ceil_div(@image.width, 8), 1024) *
|
|
88
|
+
Num.ceil_div(Num.ceil_div(@image.height, 8), 1024)
|
|
89
|
+
group_sections = groups.map do |group|
|
|
90
|
+
group_section(entropy, group.map { Num.pack_signed(_1) })
|
|
91
|
+
end
|
|
92
|
+
[global, *Array.new(dc_groups + 1, "".b), *group_sections]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def single_group_section(channels)
|
|
96
|
+
packed = channels.flat_map { residuals(_1, @image.width, @image.height) }.map { Num.pack_signed(_1) }
|
|
97
|
+
entropy = Entropy::Encoder.new(packed)
|
|
98
|
+
writer = Bit::Writer.new
|
|
99
|
+
writer.write(1, 1).write(1, 1) # default DC quantization and global MA tree
|
|
100
|
+
write_tree(writer)
|
|
101
|
+
entropy.write_header(writer)
|
|
102
|
+
write_group_header(writer)
|
|
103
|
+
entropy.write_symbols(writer)
|
|
104
|
+
writer.bytes
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def global_section(entropy)
|
|
108
|
+
writer = Bit::Writer.new
|
|
109
|
+
writer.write(1, 1).write(1, 1)
|
|
110
|
+
write_tree(writer)
|
|
111
|
+
entropy.write_header(writer)
|
|
112
|
+
write_group_header(writer)
|
|
113
|
+
writer.bytes
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def group_section(entropy, values)
|
|
117
|
+
writer = Bit::Writer.new
|
|
118
|
+
writer.write(1, 1).write(1, 1)
|
|
119
|
+
Bit::Field.write_u32(writer, 0, Bit::Field.val(0), Bit::Field.val(1),
|
|
120
|
+
Bit::Field.bits_offset(4, 2), Bit::Field.bits_offset(8, 18))
|
|
121
|
+
entropy.write_symbols(writer, values)
|
|
122
|
+
writer.bytes
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def write_tree(writer)
|
|
126
|
+
tree = Entropy::Encoder.new([0, 5, 0, 0, 0])
|
|
127
|
+
tree.write(writer, context_count: 6)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def write_group_header(writer)
|
|
131
|
+
writer.write(1, 1).write(1, 1) # global tree and default weighted header
|
|
132
|
+
Bit::Field.write_u32(writer, 1, Bit::Field.val(0), Bit::Field.val(1),
|
|
133
|
+
Bit::Field.bits_offset(4, 2), Bit::Field.bits_offset(8, 18))
|
|
134
|
+
Bit::Field.write_u32(writer, 0, *Array.new(4) { |value| Bit::Field.val(value) })
|
|
135
|
+
Bit::Field.write_u32(writer, 0, Bit::Field.bits(3), Bit::Field.bits_offset(6, 8),
|
|
136
|
+
Bit::Field.bits_offset(10, 72), Bit::Field.bits_offset(13, 1096))
|
|
137
|
+
Bit::Field.write_u32(writer, 6, Bit::Field.val(6), Bit::Field.bits(2),
|
|
138
|
+
Bit::Field.bits_offset(4, 2), Bit::Field.bits_offset(6, 10))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def transformed_channels # rubocop:disable Metrics/AbcSize
|
|
142
|
+
maximum = (1 << @bits_per_sample) - 1
|
|
143
|
+
source = @image.channels.first(3).map { |plane| plane.data.map { sample(_1, maximum) } }
|
|
144
|
+
length = @image.width * @image.height
|
|
145
|
+
Array.new(3) { Array.new(length) }.tap do |output|
|
|
146
|
+
length.times do |index|
|
|
147
|
+
red, green, blue = source.map { _1[index] }
|
|
148
|
+
second = red - blue
|
|
149
|
+
temporary = blue + (second >> 1)
|
|
150
|
+
third = green - temporary
|
|
151
|
+
output[0][index] = Num.i32(temporary + (third >> 1))
|
|
152
|
+
output[1][index] = Num.i32(second)
|
|
153
|
+
output[2][index] = Num.i32(third)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def sample(value, maximum)
|
|
159
|
+
value = (value * maximum).round if value.is_a?(Float)
|
|
160
|
+
raise ArgumentError, "sample is outside bit depth" unless value.between?(0, maximum)
|
|
161
|
+
|
|
162
|
+
value
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def region(values, x, y, width, height)
|
|
166
|
+
height.times.flat_map { |row| values.slice(((y + row) * @image.width) + x, width) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def residuals(values, width, height)
|
|
170
|
+
plane = Plane.new(width, height)
|
|
171
|
+
values.each_with_index.map do |value, index|
|
|
172
|
+
x = index % width
|
|
173
|
+
y = index / width
|
|
174
|
+
residual = Num.i32(value - Modular::Predictor.predict(5, plane, x, y))
|
|
175
|
+
plane[x, y] = value
|
|
176
|
+
residual
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|