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