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