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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Codestream
5
+ ParsedHeaders = Data.define(:basic_info, :transform_data, :icc_profile, :frame_header, :toc, :sections)
6
+
7
+ class Reader
8
+ def initialize(data, max_pixels: 100_000_000)
9
+ @container = Container::Parser.new(data, ignore_brob: true)
10
+ @stream = @container.codestream
11
+ @max_pixels = max_pixels
12
+ end
13
+
14
+ def read_headers
15
+ read_all_headers.first
16
+ end
17
+
18
+ def read_all_headers
19
+ bits = Bit::Reader.new(@stream, 2)
20
+ size = Headers::SizeHeader.read(bits, max_pixels: @max_pixels)
21
+ metadata = Headers::ImageMetadata.read(bits, max_pixels: @max_pixels)
22
+ basic = BasicInfo.new(width: size.width, height: size.height, metadata:, container: @container,
23
+ bits_consumed: bits.bits_read)
24
+ transform_data = Headers::CustomTransform.read(bits, xyb_encoded: metadata.xyb_encoded)
25
+ icc_profile = Entropy::ICCStream.read(bits) if metadata.colour_encoding.want_icc
26
+
27
+ bits.zero_pad_to_byte
28
+ offset = 2 + bits.byte_position
29
+ frames = []
30
+ loop do
31
+ raise TruncatedError, "missing frame header" if offset > @stream.bytesize
32
+
33
+ frame_bits = Bit::Reader.new(@stream, offset)
34
+ frame = Headers::FrameHeader.read(frame_bits, metadata:, image_width: size.width, image_height: size.height)
35
+ toc = Frame::TOC.read(frame_bits, toc_entries(frame))
36
+ section_start = offset + frame_bits.byte_position
37
+ sections = read_sections(section_start, toc)
38
+ frames << ParsedHeaders.new(basic_info: basic, transform_data:, icc_profile:, frame_header: frame,
39
+ toc:, sections:)
40
+ break if frame.last
41
+
42
+ offset = section_start + toc.total_size
43
+ raise TruncatedError, "missing final frame" if offset >= @stream.bytesize
44
+ end
45
+ frames
46
+ end
47
+
48
+ private
49
+
50
+ def read_sections(start, toc)
51
+ raise TruncatedError, "frame sections are truncated" if start + toc.total_size > @stream.bytesize
52
+
53
+ toc.offsets.zip(toc.sizes).map { |offset, length| @stream.byteslice(start + offset, length) }
54
+ end
55
+
56
+ def toc_entries(frame)
57
+ group_dim = 128 << frame.group_size_shift
58
+ width = frame.encoded_width
59
+ height = frame.encoded_height
60
+ groups = Num.ceil_div(width, group_dim) * Num.ceil_div(height, group_dim)
61
+ dc_groups = Num.ceil_div(frame.blocks_width, group_dim) * Num.ceil_div(frame.blocks_height, group_dim)
62
+ return 1 if groups == 1 && frame.passes.count.eql?(1)
63
+
64
+ 2 + dc_groups + (groups * frame.passes.count)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Color
5
+ module Gamut
6
+ WHITE_POINTS = { 1 => [0.3127, 0.3290], 10 => [1.0 / 3, 1.0 / 3], 11 => [0.314, 0.351] }.freeze
7
+ PRIMARIES = ICCProfile::PRIMARIES
8
+ BRADFORD = ICCProfile::BRADFORD
9
+ BRADFORD_INVERSE = ICCProfile::BRADFORD_INVERSE
10
+
11
+ module_function
12
+
13
+ def to_srgb!(channels, encoding)
14
+ return channels if encoding.colour_space == 1
15
+
16
+ source_white = encoding.white_point == 2 ? encoding.white_point_xy : WHITE_POINTS.fetch(encoding.white_point)
17
+ source_primaries = encoding.primaries == 2 ? encoding.primaries_xy : PRIMARIES.fetch(encoding.primaries)
18
+ transform = multiply(inverse(rgb_to_xyz(PRIMARIES.fetch(1), WHITE_POINTS.fetch(1))),
19
+ multiply(adaptation(source_white, WHITE_POINTS.fetch(1)),
20
+ rgb_to_xyz(source_primaries, source_white)))
21
+ red, green, blue = channels.first(3).map(&:data)
22
+ red.length.times do |index|
23
+ values = [red[index], green[index], blue[index]]
24
+ red[index] = transform[0].each_with_index.sum { |value, channel| value * values[channel] }
25
+ green[index] = transform[1].each_with_index.sum { |value, channel| value * values[channel] }
26
+ blue[index] = transform[2].each_with_index.sum { |value, channel| value * values[channel] }
27
+ end
28
+ channels
29
+ end
30
+
31
+ def rgb_to_xyz(primaries, white)
32
+ matrix = [primaries.map { |x, y| x / y }, Array.new(3, 1.0),
33
+ primaries.map { |x, y| (1.0 - x - y) / y }]
34
+ scales = multiply(inverse(matrix), xy_to_xyz(white))
35
+ matrix.map { |row| row.each_with_index.map { |value, column| value * scales[column] } }
36
+ end
37
+ private_class_method :rgb_to_xyz
38
+
39
+ def adaptation(source_white, target_white)
40
+ source = multiply(BRADFORD, xy_to_xyz(source_white))
41
+ target = multiply(BRADFORD, xy_to_xyz(target_white))
42
+ diagonal = Array.new(3) { |y| Array.new(3) { |x| x == y ? target[y] / source[y] : 0.0 } }
43
+ multiply(BRADFORD_INVERSE, multiply(diagonal, BRADFORD))
44
+ end
45
+ private_class_method :adaptation
46
+
47
+ def xy_to_xyz(point)
48
+ x, y = point
49
+ [x / y, 1.0, (1.0 - x - y) / y]
50
+ end
51
+ private_class_method :xy_to_xyz
52
+
53
+ def multiply(a, b)
54
+ return a.map { |row| row.each_with_index.sum { |value, index| value * b[index] } } unless b.first.is_a?(Array)
55
+
56
+ Array.new(a.length) do |y|
57
+ Array.new(b.first.length) do |x|
58
+ b.length.times.sum { |index| a[y][index] * b[index][x] }
59
+ end
60
+ end
61
+ end
62
+ private_class_method :multiply
63
+
64
+ def inverse(matrix)
65
+ a, b, c = matrix[0]
66
+ d, e, f = matrix[1]
67
+ g, h, i = matrix[2]
68
+ determinant = (a * ((e * i) - (f * h))) - (b * ((d * i) - (f * g))) + (c * ((d * h) - (e * g)))
69
+ raise CorruptError, "singular colour primary matrix" if determinant.zero?
70
+
71
+ [[(e * i) - (f * h), (c * h) - (b * i), (b * f) - (c * e)],
72
+ [(f * g) - (d * i), (a * i) - (c * g), (c * d) - (a * f)],
73
+ [(d * h) - (e * g), (b * g) - (a * h), (a * e) - (b * d)]].map do |row|
74
+ row.map { _1 / determinant }
75
+ end
76
+ end
77
+ private_class_method :inverse
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+
5
+ module JXL
6
+ module Color
7
+ module ICCProfile # rubocop:disable Metrics/ModuleLength
8
+ WHITE_POINTS = { 1 => [0.3127, 0.3290], 10 => [1.0 / 3, 1.0 / 3], 11 => [0.314, 0.351] }.freeze
9
+ PRIMARIES = {
10
+ 1 => [[0.639998686, 0.330010138], [0.300003784, 0.600003357], [0.150002046, 0.059997204]],
11
+ 9 => [[0.708, 0.292], [0.170, 0.797], [0.131, 0.046]],
12
+ 11 => [[0.680, 0.320], [0.265, 0.690], [0.150, 0.060]]
13
+ }.freeze
14
+ BRADFORD = [[0.8951, 0.2664, -0.1614], [-0.7502, 1.7135, 0.0367],
15
+ [0.0389, -0.0685, 1.0296]].freeze
16
+ BRADFORD_INVERSE = [[0.9869929, -0.1470543, 0.1599627], [0.4323053, 0.5183603, 0.0492912],
17
+ [-0.0085287, 0.0400428, 0.9684867]].freeze
18
+ TRANSFER_NAMES = { 1 => "709", 8 => "Lin", 13 => "SRG", 16 => "PQ", 17 => "DCI", 18 => "HLG" }.freeze
19
+ INTENT_NAMES = %w[Per Rel Sat Abs].freeze
20
+
21
+ module_function
22
+
23
+ def create(encoding)
24
+ raise UnsupportedFeatureError, "ICC profile for embedded encoding" if encoding.want_icc
25
+ unless [0, 1].include?(encoding.colour_space)
26
+ raise UnsupportedFeatureError, "ICC profile colour space #{encoding.colour_space}"
27
+ end
28
+
29
+ white = encoding.white_point == 2 ? encoding.white_point_xy : WHITE_POINTS.fetch(encoding.white_point)
30
+ primaries = if encoding.colour_space == 1
31
+ PRIMARIES[1]
32
+ elsif encoding.primaries == 2
33
+ encoding.primaries_xy
34
+ else
35
+ PRIMARIES.fetch(encoding.primaries)
36
+ end
37
+ raise CorruptError, "missing custom ICC white point" unless white
38
+ raise CorruptError, "missing custom ICC primaries" unless primaries
39
+
40
+ header = create_header(encoding)
41
+ table = +"\0\0\0\0".b
42
+ tags = +"".b
43
+ entries = []
44
+ add_tag!(entries, tags, "desc", mluc(description(encoding)))
45
+ add_tag!(entries, tags, "cprt", mluc("CC0"))
46
+ white_xyz = encoding.colour_space == 1 ? xy_to_xyz(white) : [0.964203, 1.0, 0.824905]
47
+ add_tag!(entries, tags, "wtpt", xyz_tag(white_xyz))
48
+ if encoding.colour_space.zero?
49
+ adaptation = adapt_to_d50(white)
50
+ add_tag!(entries, tags, "chad", fixed_tag("sf32", adaptation.flatten))
51
+ add_cicp!(entries, tags, encoding)
52
+ rgb_matrix(primaries, white).transpose.each_with_index do |column, index|
53
+ add_tag!(entries, tags, %w[rXYZ gXYZ bXYZ][index], xyz_tag(column))
54
+ end
55
+ end
56
+ trc_offset = tags.bytesize
57
+ trc = transfer_tag(encoding)
58
+ tags << trc << ("\0" * ((-trc.bytesize) & 3))
59
+ trc_names = encoding.colour_space == 1 ? ["kTRC"] : %w[rTRC gTRC bTRC]
60
+ trc_names.each { entries << [_1, trc_offset, tags.bytesize - trc_offset] }
61
+
62
+ table[0, 4] = [entries.length].pack("N")
63
+ base = header.bytesize + 4 + (entries.length * 12)
64
+ entries.each { |name, offset, size| table << name << [base + offset, size].pack("N2") }
65
+ profile = header + table + tags
66
+ profile[0, 4] = [profile.bytesize].pack("N")
67
+ checksum = profile.dup
68
+ checksum[44, 4] = checksum[64, 4] = "\0" * 4
69
+ profile[84, 16] = Digest::MD5.digest(checksum)
70
+ profile
71
+ end
72
+
73
+ def create_header(encoding)
74
+ header = "\0" * 128
75
+ header[4, 4] = "jxl "
76
+ header[8, 4] = [0x04400000].pack("N")
77
+ header[12, 4] = "mntr"
78
+ header[16, 4] = encoding.colour_space == 1 ? "GRAY" : "RGB "
79
+ header[20, 4] = "XYZ "
80
+ header[24, 12] = [2019, 12, 1, 0, 0, 0].pack("n6")
81
+ header[36, 8] = "acspAPPL"
82
+ header[64, 4] = [encoding.rendering_intent].pack("N")
83
+ header[68, 12] = [0x0000F6D6, 0x00010000, 0x0000D32D].pack("N3")
84
+ header[80, 4] = "jxl "
85
+ header
86
+ end
87
+ private_class_method :create_header
88
+
89
+ def add_tag!(entries, tags, name, data)
90
+ offset = tags.bytesize
91
+ tags << data << ("\0" * ((-data.bytesize) & 3))
92
+ entries << [name, offset, tags.bytesize - offset]
93
+ end
94
+ private_class_method :add_tag!
95
+
96
+ def add_cicp!(entries, tags, encoding)
97
+ return unless encoding.white_point == 1 && encoding.primaries != 2 && encoding.transfer_function
98
+
99
+ primaries = encoding.primaries == 11 ? 12 : encoding.primaries
100
+ add_tag!(entries, tags, "cicp", "cicp\0\0\0\0".b + [primaries, encoding.transfer_function, 0, 1].pack("C4"))
101
+ end
102
+ private_class_method :add_cicp!
103
+
104
+ def mluc(text)
105
+ "mluc".b + [0, 1, 12].pack("N3") + "enUS" + [text.bytesize * 2, 28].pack("N2") +
106
+ text.bytes.flat_map { [0, _1] }.pack("C*")
107
+ end
108
+ private_class_method :mluc
109
+
110
+ def xyz_tag(values) = fixed_tag("XYZ ", values)
111
+ private_class_method :xyz_tag
112
+
113
+ def fixed_tag(type, values)
114
+ type.b + [0].pack("N") + values.map { fixed(_1) }.pack("N*")
115
+ end
116
+ private_class_method :fixed_tag
117
+
118
+ def fixed(value)
119
+ rounded = (Num.f32(value) * 65_536).round
120
+ unless rounded.between?(-2_147_483_648, 2_147_483_647)
121
+ raise CorruptError, "ICC fixed-point value is out of range"
122
+ end
123
+
124
+ Num.u32(rounded)
125
+ end
126
+ private_class_method :fixed
127
+
128
+ def transfer_tag(encoding)
129
+ return sampled_transfer_tag(encoding.transfer_function) if [16, 18].include?(encoding.transfer_function)
130
+
131
+ params = if encoding.gamma
132
+ [1.0 / (encoding.gamma / 10_000_000.0)]
133
+ else
134
+ case encoding.transfer_function
135
+ when 13 then [2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 0.04045]
136
+ when 1 then [1.0 / 0.45, 1.0 / 1.099, 0.099 / 1.099, 1.0 / 4.5, 0.081]
137
+ when 8 then [1.0, 1.0, 0.0, 1.0, 0.0]
138
+ when 17 then [2.6, 1.0, 0.0, 1.0, 0.0]
139
+ else raise UnsupportedFeatureError, "ICC transfer function #{encoding.transfer_function}"
140
+ end
141
+ end
142
+ type = params.one? ? 0 : 3
143
+ "para".b + [0, type, 0].pack("Nn2") + params.map { fixed(_1) }.pack("N*")
144
+ end
145
+ private_class_method :transfer_tag
146
+
147
+ def sampled_transfer_tag(function)
148
+ count = 4096
149
+ values = Array.new(count) do |index|
150
+ linear = Transfer.decode(index.fdiv(count - 1), function)
151
+ (Num.clamp(linear, 0.0, 1.0) * 65_535).round
152
+ end
153
+ "curv".b + [0, count].pack("N2") + values.pack("n*")
154
+ end
155
+ private_class_method :sampled_transfer_tag
156
+
157
+ def description(encoding)
158
+ return "sRGB" if encoding.colour_space.zero? && encoding.white_point == 1 &&
159
+ encoding.primaries == 1 && encoding.transfer_function == 13
160
+
161
+ space = encoding.colour_space == 1 ? "Gra" : "RGB"
162
+ white = if encoding.white_point == 2
163
+ encoding.white_point_xy.join(";")
164
+ else
165
+ { 1 => "D65", 10 => "EER", 11 => "DCI" }.fetch(encoding.white_point)
166
+ end
167
+ intent = INTENT_NAMES.fetch(encoding.rendering_intent)
168
+ transfer = if encoding.gamma
169
+ "g#{encoding.gamma / 10_000_000.0}"
170
+ else
171
+ TRANSFER_NAMES.fetch(encoding.transfer_function)
172
+ end
173
+ parts = [space, white]
174
+ if encoding.colour_space.zero?
175
+ primary = if encoding.primaries == 2
176
+ encoding.primaries_xy.flatten.join(";")
177
+ else
178
+ { 1 => "SRG", 9 => "202", 11 => "DCI" }.fetch(encoding.primaries)
179
+ end
180
+ parts << primary
181
+ end
182
+ (parts + [intent, transfer]).join("_")
183
+ end
184
+ private_class_method :description
185
+
186
+ def rgb_matrix(primaries, white)
187
+ matrix = [primaries.map(&:first), primaries.map(&:last),
188
+ primaries.map { |x, y| 1.0 - x - y }]
189
+ scales = multiply(inverse(matrix), xy_to_xyz(white))
190
+ to_xyz = matrix.map { |row| row.each_with_index.map { |value, x| f32(value * scales[x]) } }
191
+ multiply(adapt_to_d50(white), to_xyz)
192
+ end
193
+ private_class_method :rgb_matrix
194
+
195
+ def adapt_to_d50(white)
196
+ source = multiply(BRADFORD, xy_to_xyz(white))
197
+ target = multiply(BRADFORD, [0.96422, 1.0, 0.82521])
198
+ diagonal = Array.new(3) { |y| Array.new(3) { |x| x == y ? f32(target[y] / source[y]) : 0.0 } }
199
+ multiply(BRADFORD_INVERSE, multiply(diagonal, BRADFORD))
200
+ end
201
+ private_class_method :adapt_to_d50
202
+
203
+ def xy_to_xyz(point)
204
+ x, y = point
205
+ [f32(x / y), 1.0, f32((1.0 - x - y) / y)]
206
+ end
207
+ private_class_method :xy_to_xyz
208
+
209
+ def multiply(a, b)
210
+ unless b.first.is_a?(Array)
211
+ return a.map do |row|
212
+ f32(row.each_with_index.sum { |value, index| f32(value) * f32(b[index]) })
213
+ end
214
+ end
215
+
216
+ Array.new(a.length) do |y|
217
+ Array.new(b.first.length) do |x|
218
+ f32(b.length.times.sum { |index| f32(a[y][index]) * f32(b[index][x]) })
219
+ end
220
+ end
221
+ end
222
+ private_class_method :multiply
223
+
224
+ def inverse(matrix)
225
+ a, b, c = matrix[0]
226
+ d, e, f = matrix[1]
227
+ g, h, i = matrix[2]
228
+ determinant = (a * ((e * i) - (f * h))) - (b * ((d * i) - (f * g))) + (c * ((d * h) - (e * g)))
229
+ raise CorruptError, "singular ICC primary matrix" if determinant.zero?
230
+
231
+ [[(e * i) - (f * h), (c * h) - (b * i), (b * f) - (c * e)],
232
+ [(f * g) - (d * i), (a * i) - (c * g), (c * d) - (a * f)],
233
+ [(d * h) - (e * g), (b * g) - (a * h), (a * e) - (b * d)]].map do |row|
234
+ row.map { f32(_1 / determinant) }
235
+ end
236
+ end
237
+ private_class_method :inverse
238
+
239
+ def f32(value) = Num.f32(value)
240
+ private_class_method :f32
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Color
5
+ module Opsin
6
+ module_function
7
+
8
+ def to_linear(channels, transform, intensity_target: 255.0)
9
+ output = Array.new(3) { Plane.new(channels[0].width, channels[0].height) }
10
+ matrix = transform.inverse_matrix.map { _1 * (255.0 / intensity_target) }
11
+ bias_roots = transform.biases.map { Math.cbrt(_1) }
12
+ channels[0].data.length.times do |index|
13
+ x = channels[0].data[index]
14
+ y = channels[1].data[index]
15
+ b = channels[2].data[index]
16
+ mixed = [((y + x - bias_roots[0])**3) + transform.biases[0],
17
+ ((y - x - bias_roots[1])**3) + transform.biases[1],
18
+ ((b - bias_roots[2])**3) + transform.biases[2]]
19
+ 3.times do |channel|
20
+ offset = channel * 3
21
+ output[channel].data[index] = matrix[offset, 3].each_with_index.sum { |value, c| value * mixed[c] }
22
+ end
23
+ end
24
+ output + channels.drop(3)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Color
5
+ module Transfer
6
+ module_function
7
+
8
+ def from_linear!(channels, function, gamma: nil, intensity_target: 255.0)
9
+ channels.first(3).each do |plane|
10
+ plane.data.map! { encode(_1, function, gamma:, intensity_target:) }
11
+ end
12
+ channels
13
+ end
14
+
15
+ def encode(value, function, gamma: nil, intensity_target: 255.0)
16
+ return value if function == 8
17
+ return value.positive? ? value**(gamma / 10_000_000.0) : 0.0 if function.nil? && gamma
18
+ return value.positive? ? value**(1.0 / 2.6) : 0.0 if function == 17
19
+ return bt709(value) if function == 1
20
+ return pq(value, intensity_target) if function == 16
21
+ return hlg(value) if function == 18
22
+ raise UnsupportedFeatureError, "transfer function #{function}" unless function == 13
23
+
24
+ sign = value.negative? ? -1.0 : 1.0
25
+ magnitude = value.abs
26
+ sign * (magnitude <= 0.0031308 ? 12.92 * magnitude : (1.055 * (magnitude**(1.0 / 2.4))) - 0.055)
27
+ end
28
+
29
+ def to_linear!(channels, function, gamma: nil, intensity_target: 255.0)
30
+ channels.first(3).each do |plane|
31
+ plane.data.map! { decode(_1, function, gamma:, intensity_target:) }
32
+ end
33
+ channels
34
+ end
35
+
36
+ def decode(value, function, gamma: nil, intensity_target: 255.0)
37
+ return value if function == 8
38
+ return signed_power(value, 10_000_000.0 / gamma) if function.nil? && gamma
39
+ return signed_power(value, 2.6) if function == 17
40
+ return value < 0.081 ? value / 4.5 : ((value + 0.099) / 1.099)**(1.0 / 0.45) if function == 1
41
+ return inverse_pq(value, intensity_target) if function == 16
42
+ return inverse_hlg(value) if function == 18
43
+ raise UnsupportedFeatureError, "transfer function #{function}" unless function == 13
44
+
45
+ sign = value.negative? ? -1.0 : 1.0
46
+ magnitude = value.abs
47
+ sign * (magnitude <= 0.04045 ? magnitude / 12.92 : ((magnitude + 0.055) / 1.055)**2.4)
48
+ end
49
+
50
+ def bt709(value)
51
+ value < 0.018 ? 4.5 * value : (1.099 * (value**0.45)) - 0.099
52
+ end
53
+ private_class_method :bt709
54
+
55
+ def pq(value, intensity_target)
56
+ return 0.0 if value.zero?
57
+
58
+ sign = value.negative? ? -1.0 : 1.0
59
+ m1 = 2610.0 / 16_384
60
+ m2 = (2523.0 / 4096) * 128
61
+ scaled = (value.abs * intensity_target / 10_000.0)**m1
62
+ sign * ((((3424.0 / 4096) + (scaled * (2413.0 / 4096) * 32)) /
63
+ (1.0 + (scaled * (2392.0 / 4096) * 32)))**m2)
64
+ end
65
+ private_class_method :pq
66
+
67
+ def hlg(value)
68
+ return 0.0 if value.zero?
69
+
70
+ sign = value.negative? ? -1.0 : 1.0
71
+ magnitude = value.abs
72
+ encoded = if magnitude <= 1.0 / 12
73
+ Math.sqrt(3.0 * magnitude)
74
+ else
75
+ (0.17883277 * Math.log((12 * magnitude) - (1 - (4 * 0.17883277)))) + 0.5599107295
76
+ end
77
+ sign * encoded
78
+ end
79
+ private_class_method :hlg
80
+
81
+ def inverse_pq(value, intensity_target)
82
+ sign = value.negative? ? -1.0 : 1.0
83
+ m1 = 2610.0 / 16_384
84
+ m2 = (2523.0 / 4096) * 128
85
+ encoded = value.abs**(1.0 / m2)
86
+ numerator = [encoded - (3424.0 / 4096), 0.0].max
87
+ denominator = ((2413.0 / 4096) * 32) - (encoded * (2392.0 / 4096) * 32)
88
+ sign * ((numerator / denominator)**(1.0 / m1)) * 10_000.0 / intensity_target
89
+ end
90
+ private_class_method :inverse_pq
91
+
92
+ def inverse_hlg(value)
93
+ sign = value.negative? ? -1.0 : 1.0
94
+ magnitude = value.abs
95
+ linear = if magnitude <= 0.5
96
+ magnitude * magnitude / 3.0
97
+ else
98
+ (Math.exp((magnitude - 0.5599107295) / 0.17883277) + (1 - (4 * 0.17883277))) / 12.0
99
+ end
100
+ sign * linear
101
+ end
102
+ private_class_method :inverse_hlg
103
+
104
+ def signed_power(value, exponent) = (value.negative? ? -1.0 : 1.0) * (value.abs**exponent)
105
+ private_class_method :signed_power
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Color
5
+ module YCbCr
6
+ module_function
7
+
8
+ def to_rgb(channels, frame)
9
+ full = channels.first(3).each_with_index.map do |plane, channel|
10
+ hshift, vshift = frame.chroma_subsampling[channel]
11
+ hshift.times { plane = double_horizontal(plane) }
12
+ vshift.times { plane = double_vertical(plane) }
13
+ crop(plane, frame.encoded_width, frame.encoded_height)
14
+ end
15
+ output = Array.new(3) { Plane.new(frame.encoded_width, frame.encoded_height) }
16
+ full[0].data.length.times do |index|
17
+ cb = full[0].data[index]
18
+ y = full[1].data[index] + (128.0 / 255)
19
+ cr = full[2].data[index]
20
+ output[0].data[index] = y + (1.402 * cr)
21
+ output[1].data[index] = y - ((0.114 * 1.772 / 0.587) * cb) - ((0.299 * 1.402 / 0.587) * cr)
22
+ output[2].data[index] = y + (1.772 * cb)
23
+ end
24
+ output + channels.drop(3)
25
+ end
26
+
27
+ def double_horizontal(source)
28
+ Plane.new(source.width * 2, source.height).tap do |output|
29
+ source.height.times do |y|
30
+ source.width.times do |x|
31
+ current = 0.75 * source[x, y]
32
+ output[x * 2, y] = current + (0.25 * source[[x - 1, 0].max, y])
33
+ output[(x * 2) + 1, y] = current + (0.25 * source[[x + 1, source.width - 1].min, y])
34
+ end
35
+ end
36
+ end
37
+ end
38
+ private_class_method :double_horizontal
39
+
40
+ def double_vertical(source)
41
+ Plane.new(source.width, source.height * 2).tap do |output|
42
+ source.height.times do |y|
43
+ source.width.times do |x|
44
+ current = 0.75 * source[x, y]
45
+ output[x, y * 2] = current + (0.25 * source[x, [y - 1, 0].max])
46
+ output[x, (y * 2) + 1] = current + (0.25 * source[x, [y + 1, source.height - 1].min])
47
+ end
48
+ end
49
+ end
50
+ end
51
+ private_class_method :double_vertical
52
+
53
+ def crop(source, width, height)
54
+ return source if source.width == width && source.height == height
55
+
56
+ Plane.new(width, height).tap do |output|
57
+ height.times { |y| output.data[y * width, width] = source.data[y * source.width, width] }
58
+ end
59
+ end
60
+ private_class_method :crop
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Container
5
+ Box = Data.define(:type, :payload, :size, :offset, :uuid)
6
+ end
7
+ end