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,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Headers
5
+ BitDepth = Data.define(:floating_point, :bits_per_sample, :exponent_bits) do
6
+ def self.read(reader)
7
+ floating_point = Bit::Field.read_bool(reader)
8
+ bits_per_sample = if floating_point
9
+ Bit::Field.u32(reader, Bit::Field.val(32), Bit::Field.val(16),
10
+ Bit::Field.val(24), Bit::Field.bits_offset(6, 1))
11
+ else
12
+ Bit::Field.u32(reader, Bit::Field.val(8), Bit::Field.val(10),
13
+ Bit::Field.val(12), Bit::Field.bits_offset(6, 1))
14
+ end
15
+ exponent_bits = floating_point ? reader.read(4) + 1 : 0
16
+ validate!(floating_point, bits_per_sample, exponent_bits)
17
+ new(floating_point:, bits_per_sample:, exponent_bits:)
18
+ end
19
+
20
+ def self.validate!(floating_point, bits, exponent)
21
+ if floating_point
22
+ mantissa = bits - exponent - 1
23
+ raise CorruptError, "invalid floating-point bit depth" unless exponent.between?(2,
24
+ 8) && mantissa.between?(2, 23)
25
+ elsif bits > 31
26
+ raise CorruptError, "invalid integer bit depth"
27
+ end
28
+ end
29
+ private_class_method :validate!
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Headers
5
+ ColourEncoding = Data.define(:want_icc, :colour_space, :white_point, :primaries,
6
+ :transfer_function, :gamma, :rendering_intent,
7
+ :white_point_xy, :primaries_xy)
8
+
9
+ class ColourEncoding
10
+ DEFAULT = { want_icc: false, colour_space: 0, white_point: 1, primaries: 1,
11
+ transfer_function: 13, gamma: nil, rendering_intent: 1,
12
+ white_point_xy: nil, primaries_xy: nil }.freeze
13
+ CUSTOM_XY = [Bit::Field.bits(19), Bit::Field.bits_offset(19, 524_288),
14
+ Bit::Field.bits_offset(20, 1_048_576), Bit::Field.bits_offset(21, 2_097_152)].freeze
15
+ ENUM = [Bit::Field.val(0), Bit::Field.val(1), Bit::Field.bits_offset(4, 2),
16
+ Bit::Field.bits_offset(6, 18)].freeze
17
+
18
+ def self.read(reader)
19
+ return new(**DEFAULT) if Bit::Field.read_bool(reader)
20
+
21
+ want_icc = Bit::Field.read_bool(reader)
22
+ colour_space = read_enum(reader, [0, 1, 2, 3], "colour space")
23
+ raise CorruptError, "unknown colour space without ICC" if !want_icc && colour_space == 3
24
+
25
+ values = DEFAULT.merge(want_icc:, colour_space:)
26
+ unless want_icc
27
+ values[:white_point], values[:white_point_xy] = read_white_point(reader, colour_space)
28
+ values[:primaries], values[:primaries_xy] = read_primaries(reader, colour_space)
29
+ values[:transfer_function], values[:gamma] = read_transfer_function(reader, colour_space)
30
+ values[:rendering_intent] = read_enum(reader, [0, 1, 2, 3], "rendering intent")
31
+ end
32
+ new(**values)
33
+ end
34
+
35
+ def self.read_white_point(reader, colour_space)
36
+ return [1, nil] if colour_space == 2
37
+
38
+ value = read_enum(reader, [1, 2, 10, 11], "white point")
39
+ [value, value == 2 ? read_xy(reader) : nil]
40
+ end
41
+ private_class_method :read_white_point
42
+
43
+ def self.read_primaries(reader, colour_space)
44
+ return [1, nil] if [1, 2].include?(colour_space)
45
+
46
+ value = read_enum(reader, [1, 2, 9, 11], "primaries")
47
+ [value, value == 2 ? Array.new(3) { read_xy(reader) } : nil]
48
+ end
49
+ private_class_method :read_primaries
50
+
51
+ def self.read_xy(reader) = Array.new(2) { Num.unpack_signed(Bit::Field.u32(reader, *CUSTOM_XY)) / 1_000_000.0 }
52
+ private_class_method :read_xy
53
+
54
+ def self.read_transfer_function(reader, colour_space)
55
+ return [nil, 3_333_333] if colour_space == 2
56
+
57
+ have_gamma = Bit::Field.read_bool(reader)
58
+ if have_gamma
59
+ gamma = reader.read(24)
60
+ raise CorruptError, "invalid gamma" unless gamma.between?(1221, 10_000_000)
61
+
62
+ [nil, gamma]
63
+ else
64
+ value = read_enum(reader, [1, 2, 8, 13, 16, 17, 18], "transfer function")
65
+ raise CorruptError, "unknown transfer function without ICC" if value == 2
66
+
67
+ [value, nil]
68
+ end
69
+ end
70
+ private_class_method :read_transfer_function
71
+
72
+ def self.read_enum(reader, allowed, name)
73
+ value = Bit::Field.u32(reader, *ENUM)
74
+ raise CorruptError, "invalid #{name}" unless allowed.include?(value)
75
+
76
+ value
77
+ end
78
+ private_class_method :read_enum
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Headers
5
+ module CustomTransform
6
+ WEIGHT_COUNTS = [15, 55, 210].freeze
7
+ DEFAULT_MATRIX = [11.031566901960783, -9.866943921568629, -0.16462299647058826,
8
+ -3.254147380392157, 4.418770392156863, -0.16462299647058826,
9
+ -3.6588512862745097, 2.7129230470588235, 1.9459282392156863].freeze
10
+ DEFAULT_BIASES = Array.new(3, -0.0037930732552754493).freeze
11
+ DEFAULT_QUANT_BIASES = [0.945349926692846, 0.9299455010825141, 0.9500648966626564, 0.145].freeze
12
+ Opsin = Data.define(:inverse_matrix, :biases, :quant_biases)
13
+ TransformData = Data.define(:opsin, :weights)
14
+
15
+ module_function
16
+
17
+ def read(reader, xyb_encoded:)
18
+ return defaults if Bit::Field.read_bool(reader)
19
+
20
+ opsin = xyb_encoded ? read_opsin(reader) : nil
21
+ mask = reader.read(3)
22
+ weights = WEIGHT_COUNTS.each_with_index.map do |count, index|
23
+ mask[index].zero? ? nil : Array.new(count) { Bit::Field.f16(reader) }
24
+ end
25
+ TransformData.new(opsin:, weights:)
26
+ end
27
+
28
+ def read_opsin(reader)
29
+ return default_opsin if Bit::Field.read_bool(reader)
30
+
31
+ Opsin.new(inverse_matrix: Array.new(9) { Bit::Field.f16(reader) },
32
+ biases: Array.new(3) { Bit::Field.f16(reader) },
33
+ quant_biases: Array.new(4) { Bit::Field.f16(reader) })
34
+ end
35
+ private_class_method :read_opsin
36
+
37
+ def defaults = TransformData.new(opsin: default_opsin, weights: Array.new(3))
38
+ private_class_method :defaults
39
+
40
+ def default_opsin
41
+ Opsin.new(inverse_matrix: DEFAULT_MATRIX, biases: DEFAULT_BIASES, quant_biases: DEFAULT_QUANT_BIASES)
42
+ end
43
+ private_class_method :default_opsin
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Headers
5
+ FRAME_CROP_DISTRIBUTION = [Bit::Field.bits(8), Bit::Field.bits_offset(11, 256),
6
+ Bit::Field.bits_offset(14, 2304),
7
+ Bit::Field.bits_offset(30, 18_688)].freeze
8
+ FRAME_UPSAMPLING_DISTRIBUTION = [1, 2, 4, 8].map { |value| Bit::Field.val(value) }.freeze
9
+
10
+ Passes = Data.define(:count, :shifts, :downsamples, :last_passes)
11
+ BlendInfo = Data.define(:mode, :alpha_channel, :clamp, :source)
12
+ LoopFilter = Data.define(:gaborish, :gaborish_weights, :epf_iterations, :epf_sharpness,
13
+ :epf_channel_scale, :epf_zero_flush, :epf_quant_mul,
14
+ :epf_sigma_scale, :epf_border_sad_mul, :epf_sigma_for_modular)
15
+ FrameHeader = Data.define(:type, :encoding, :flags, :colour_transform, :chroma_subsampling, :upsampling,
16
+ :extra_channel_upsampling, :group_size_shift, :x_qm_scale, :b_qm_scale,
17
+ :passes, :dc_level, :origin_x, :origin_y, :width, :height, :blend,
18
+ :extra_channel_blends, :duration, :timecode, :last, :save_as_reference,
19
+ :save_before_colour_transform, :name, :loop_filter) do
20
+ def encoded_width = Num.ceil_div(width, upsampling << (3 * dc_level))
21
+ def encoded_height = Num.ceil_div(height, upsampling << (3 * dc_level))
22
+ def blocks_width = Num.ceil_div(encoded_width, 8 << max_hshift) << max_hshift
23
+ def blocks_height = Num.ceil_div(encoded_height, 8 << max_vshift) << max_vshift
24
+ def max_hshift = chroma_subsampling.map(&:first).max
25
+ def max_vshift = chroma_subsampling.map(&:last).max
26
+
27
+ def self.read(reader, metadata:, image_width:, image_height:)
28
+ return defaults(metadata, image_width, image_height) if Bit::Field.read_bool(reader)
29
+
30
+ values = read_core(reader, metadata)
31
+ read_geometry(reader, values, image_width, image_height)
32
+ read_compositing(reader, values, metadata, image_width, image_height)
33
+ values[:name] = Bit::Field.string(reader)
34
+ values[:loop_filter] = read_loop_filter(reader, values[:encoding])
35
+ Bit::Field.skip_extensions(reader)
36
+ new(**values)
37
+ end
38
+
39
+ def self.read_core(reader, metadata)
40
+ type = reader.read(2)
41
+ encoding = Bit::Field.read_bool(reader) ? :modular : :vardct
42
+ flags = Bit::Field.u64(reader)
43
+ colour_transform = if metadata.xyb_encoded
44
+ :xyb
45
+ else
46
+ (Bit::Field.read_bool(reader) ? :ycbcr : :none)
47
+ end
48
+ chroma_subsampling = if colour_transform == :ycbcr && flags.nobits?(32)
49
+ read_chroma_subsampling(reader)
50
+ else
51
+ Array.new(3) { [0, 0] }
52
+ end
53
+ upsampling = flags.nobits?(32) ? Bit::Field.u32(reader, *FRAME_UPSAMPLING_DISTRIBUTION) : 1
54
+ extra_upsampling = flags.nobits?(32) ? read_extra_upsampling(reader, metadata, upsampling) : []
55
+ group_shift = encoding == :modular ? reader.read(2) : 1
56
+ x_qm = encoding == :vardct && colour_transform == :xyb ? reader.read(3) : 2
57
+ b_qm = encoding == :vardct && colour_transform == :xyb ? reader.read(3) : 2
58
+ passes = type == 2 ? default_passes : read_passes(reader)
59
+ dc_level = type == 1 ? Bit::Field.u32(reader, *[1, 2, 3, 4].map { |value| Bit::Field.val(value) }) : 0
60
+ { type:, encoding:, flags:, colour_transform:, chroma_subsampling:, upsampling:,
61
+ extra_channel_upsampling: extra_upsampling,
62
+ group_size_shift: group_shift, x_qm_scale: x_qm, b_qm_scale: b_qm, passes:, dc_level: }
63
+ end
64
+ private_class_method :read_core
65
+
66
+ def self.read_chroma_subsampling(reader)
67
+ raw_h = [0, 1, 1, 0]
68
+ raw_v = [0, 1, 0, 1]
69
+ modes = Array.new(3) { reader.read(2) }
70
+ max_h = modes.map { raw_h[_1] }.max
71
+ max_v = modes.map { raw_v[_1] }.max
72
+ modes.map { |mode| [max_h - raw_h[mode], max_v - raw_v[mode]] }
73
+ end
74
+ private_class_method :read_chroma_subsampling
75
+
76
+ def self.read_extra_upsampling(reader, metadata, colour_upsampling)
77
+ metadata.extra_channels.map do |channel|
78
+ value = Bit::Field.u32(reader, *FRAME_UPSAMPLING_DISTRIBUTION) << channel.dim_shift
79
+ raise CorruptError, "invalid extra-channel upsampling" unless value.between?(colour_upsampling, 8)
80
+
81
+ value
82
+ end
83
+ end
84
+ private_class_method :read_extra_upsampling
85
+
86
+ def self.read_passes(reader)
87
+ count = Bit::Field.u32(reader, Bit::Field.val(1), Bit::Field.val(2),
88
+ Bit::Field.val(3), Bit::Field.bits_offset(3, 4))
89
+ return default_passes if count == 1
90
+
91
+ downsample_count = Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1),
92
+ Bit::Field.val(2), Bit::Field.bits_offset(1, 3))
93
+ shifts = Array.new(count - 1) { reader.read(2) } << 0
94
+ raise CorruptError, "too many downsampling stages" if downsample_count > count
95
+
96
+ downsamples = Array.new(downsample_count) { Bit::Field.u32(reader, *FRAME_UPSAMPLING_DISTRIBUTION) }
97
+ last_passes = Array.new(downsample_count) do
98
+ Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1), Bit::Field.val(2), Bit::Field.bits(3))
99
+ end
100
+ raise CorruptError, "invalid downsampling order" unless downsamples.each_cons(2).all? { |a, b| b < a }
101
+ unless last_passes.each_cons(2).all? { |a, b| b > a } && last_passes.all? { |pass| pass < count }
102
+ raise CorruptError, "invalid last-pass order"
103
+ end
104
+
105
+ Passes.new(count:, shifts:, downsamples:, last_passes:)
106
+ end
107
+ private_class_method :read_passes
108
+
109
+ def self.default_passes = Passes.new(count: 1, shifts: [0], downsamples: [], last_passes: [])
110
+ private_class_method :default_passes
111
+
112
+ def self.read_geometry(reader, values, image_width, image_height)
113
+ custom = values[:type] != 1 && Bit::Field.read_bool(reader)
114
+ values[:origin_x] = values[:origin_y] = 0
115
+ values[:width] = image_width
116
+ values[:height] = image_height
117
+ return unless custom
118
+
119
+ if [0, 3].include?(values[:type])
120
+ values[:origin_x] = Num.unpack_signed(Bit::Field.u32(reader, *FRAME_CROP_DISTRIBUTION))
121
+ values[:origin_y] = Num.unpack_signed(Bit::Field.u32(reader, *FRAME_CROP_DISTRIBUTION))
122
+ end
123
+ values[:width] = Bit::Field.u32(reader, *FRAME_CROP_DISTRIBUTION)
124
+ values[:height] = Bit::Field.u32(reader, *FRAME_CROP_DISTRIBUTION)
125
+ raise CorruptError, "empty frame" unless values[:width].positive? && values[:height].positive?
126
+ end
127
+ private_class_method :read_geometry
128
+
129
+ def self.read_compositing(reader, values, metadata, image_width, image_height)
130
+ regular = [0, 3].include?(values[:type])
131
+ partial = values[:origin_x].positive? || values[:origin_y].positive? ||
132
+ values[:origin_x] + values[:width] < image_width ||
133
+ values[:origin_y] + values[:height] < image_height
134
+ values[:blend] = regular ? read_blend(reader, metadata.extra_channels.length, partial) : default_blend
135
+ values[:extra_channel_blends] = if regular
136
+ metadata.extra_channels.map do
137
+ read_blend(reader, metadata.extra_channels.length, partial)
138
+ end
139
+ else
140
+ []
141
+ end
142
+ values[:duration] = regular && metadata.animation ? read_duration(reader) : 0
143
+ values[:timecode] = regular && metadata.animation&.have_timecodes ? reader.read(32) : 0
144
+ values[:last] = regular ? Bit::Field.read_bool(reader) : false
145
+ values[:save_as_reference] = values[:type] != 1 && !values[:last] ? reader.read(2) : 0
146
+ can_reference = !values[:last] && values[:type] != 1 &&
147
+ (values[:duration].zero? || !values[:save_as_reference].zero?)
148
+ may_choose_reference = can_reference && values[:blend].mode.zero? && !partial && regular
149
+ values[:save_before_colour_transform] = if may_choose_reference || values[:type] == 2
150
+ Bit::Field.read_bool(reader)
151
+ else
152
+ values[:type] == 1
153
+ end
154
+ end
155
+ private_class_method :read_compositing
156
+
157
+ def self.read_duration(reader)
158
+ Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1), Bit::Field.bits(8), Bit::Field.bits(32))
159
+ end
160
+ private_class_method :read_duration
161
+
162
+ def self.read_blend(reader, extra_channels, partial)
163
+ mode = Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1),
164
+ Bit::Field.val(2), Bit::Field.bits_offset(2, 3))
165
+ raise CorruptError, "invalid blend mode" if mode > 4
166
+
167
+ uses_alpha = extra_channels.positive? && [2, 3].include?(mode)
168
+ alpha = if uses_alpha
169
+ Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1),
170
+ Bit::Field.val(2), Bit::Field.bits_offset(3, 3))
171
+ else
172
+ 0
173
+ end
174
+ raise CorruptError, "invalid blend alpha channel" if alpha >= extra_channels && uses_alpha
175
+
176
+ clamp = (uses_alpha || mode == 4) && Bit::Field.read_bool(reader)
177
+ source = mode != 0 || partial ? reader.read(2) : 0
178
+ BlendInfo.new(mode:, alpha_channel: alpha, clamp:, source:)
179
+ end
180
+ private_class_method :read_blend
181
+
182
+ def self.default_blend = BlendInfo.new(mode: 0, alpha_channel: 0, clamp: false, source: 0)
183
+ private_class_method :default_blend
184
+
185
+ def self.read_loop_filter(reader, encoding)
186
+ return default_loop_filter if Bit::Field.read_bool(reader)
187
+
188
+ gaborish = Bit::Field.read_bool(reader)
189
+ weights = if gaborish && Bit::Field.read_bool(reader)
190
+ Array.new(3) { [Bit::Field.f16(reader), Bit::Field.f16(reader)] }
191
+ else
192
+ default_gaborish_weights
193
+ end
194
+ epf = reader.read(2)
195
+ options = epf.positive? ? read_epf_options(reader, encoding) : default_epf_options
196
+ Bit::Field.skip_extensions(reader)
197
+ LoopFilter.new(gaborish:, gaborish_weights: weights, epf_iterations: epf, **options)
198
+ end
199
+ private_class_method :read_loop_filter
200
+
201
+ def self.read_epf_options(reader, encoding)
202
+ values = default_epf_options
203
+ if encoding == :vardct && Bit::Field.read_bool(reader)
204
+ values[:epf_sharpness] = Array.new(8) { Bit::Field.f16(reader) }
205
+ end
206
+ if Bit::Field.read_bool(reader)
207
+ values[:epf_channel_scale] = Array.new(3) { Bit::Field.f16(reader) }
208
+ values[:epf_zero_flush] = Array.new(2) { Bit::Field.f16(reader) }
209
+ end
210
+ if Bit::Field.read_bool(reader)
211
+ values[:epf_quant_mul] = Bit::Field.f16(reader) if encoding == :vardct
212
+ values[:epf_sigma_scale] = Array.new(2) { Bit::Field.f16(reader) }
213
+ values[:epf_border_sad_mul] = Bit::Field.f16(reader)
214
+ end
215
+ values[:epf_sigma_for_modular] = Bit::Field.f16(reader) if encoding == :modular
216
+ values
217
+ end
218
+ private_class_method :read_epf_options
219
+
220
+ def self.default_gaborish_weights
221
+ Array.new(3) { [1.1 * 0.104699568, 1.1 * 0.055680538] }
222
+ end
223
+ private_class_method :default_gaborish_weights
224
+
225
+ def self.default_epf_options
226
+ { epf_sharpness: Array.new(8) { |index| index / 7.0 }, epf_channel_scale: [40.0, 5.0, 3.5],
227
+ epf_zero_flush: [0.45, 0.6], epf_quant_mul: 0.46, epf_sigma_scale: [0.9, 6.5],
228
+ epf_border_sad_mul: 2.0 / 3, epf_sigma_for_modular: 1.0 }
229
+ end
230
+ private_class_method :default_epf_options
231
+
232
+ def self.default_loop_filter
233
+ LoopFilter.new(gaborish: true, gaborish_weights: default_gaborish_weights, epf_iterations: 2,
234
+ **default_epf_options)
235
+ end
236
+ private_class_method :default_loop_filter
237
+
238
+ def self.defaults(metadata, width, height)
239
+ new(type: 0, encoding: :vardct, flags: 0, colour_transform: metadata.xyb_encoded ? :xyb : :none,
240
+ chroma_subsampling: Array.new(3) { [0, 0] },
241
+ upsampling: 1, extra_channel_upsampling: Array.new(metadata.extra_channels.length, 1),
242
+ group_size_shift: 1, x_qm_scale: 3, b_qm_scale: 2, passes: default_passes,
243
+ dc_level: 0, origin_x: 0, origin_y: 0, width:, height:, blend: default_blend,
244
+ extra_channel_blends: Array.new(metadata.extra_channels.length) { default_blend }, duration: 0,
245
+ timecode: 0, last: true, save_as_reference: 0, save_before_colour_transform: false,
246
+ name: "", loop_filter: default_loop_filter)
247
+ end
248
+ private_class_method :defaults
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Headers
5
+ ExtraChannel = Data.define(:type, :bit_depth, :dim_shift, :name, :alpha_associated, :spot_colour, :cfa_channel)
6
+ Animation = Data.define(:ticks_numerator, :ticks_denominator, :loops, :have_timecodes)
7
+ ToneMapping = Data.define(:intensity_target, :min_nits, :relative_to_max_display, :linear_below)
8
+ ImageMetadata = Data.define(:orientation, :intrinsic_size, :preview_size, :animation, :bit_depth,
9
+ :modular_16_bit_buffer, :extra_channels, :xyb_encoded, :colour_encoding,
10
+ :tone_mapping, :extensions) do
11
+ def self.read(reader, max_pixels: 100_000_000)
12
+ return defaults if Bit::Field.read_bool(reader)
13
+
14
+ extra_fields = Bit::Field.read_bool(reader)
15
+ optional = extra_fields ? read_optional_fields(reader, max_pixels) : {}
16
+ bit_depth = BitDepth.read(reader)
17
+ modular_16_bit_buffer = Bit::Field.read_bool(reader)
18
+ count = Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(1),
19
+ Bit::Field.bits_offset(4, 2), Bit::Field.bits_offset(12, 1))
20
+ raise ResourceLimitError, "too many extra channels" if count > 256
21
+
22
+ extra_channels = Array.new(count) { read_extra_channel(reader) }
23
+ xyb_encoded = Bit::Field.read_bool(reader)
24
+ colour_encoding = ColourEncoding.read(reader)
25
+ tone_mapping = extra_fields ? read_tone_mapping(reader) : default_tone_mapping
26
+ Bit::Field.skip_extensions(reader)
27
+ new(orientation: optional.fetch(:orientation, 1), intrinsic_size: optional[:intrinsic_size],
28
+ preview_size: optional[:preview_size], animation: optional[:animation], bit_depth:,
29
+ modular_16_bit_buffer:, extra_channels:, xyb_encoded:, colour_encoding:, tone_mapping:, extensions: 0)
30
+ end
31
+
32
+ def self.defaults
33
+ new(orientation: 1, intrinsic_size: nil, preview_size: nil, animation: nil,
34
+ bit_depth: BitDepth.new(floating_point: false, bits_per_sample: 8, exponent_bits: 0),
35
+ modular_16_bit_buffer: true, extra_channels: [], xyb_encoded: true,
36
+ colour_encoding: ColourEncoding.new(**ColourEncoding::DEFAULT),
37
+ tone_mapping: default_tone_mapping, extensions: 0)
38
+ end
39
+
40
+ def self.read_optional_fields(reader, max_pixels)
41
+ values = { orientation: reader.read(3) + 1 }
42
+ values[:intrinsic_size] = SizeHeader.read(reader, max_pixels:) if Bit::Field.read_bool(reader)
43
+ values[:preview_size] = read_preview_size(reader) if Bit::Field.read_bool(reader)
44
+ values[:animation] = read_animation(reader) if Bit::Field.read_bool(reader)
45
+ values
46
+ end
47
+ private_class_method :read_optional_fields
48
+
49
+ def self.read_preview_size(reader)
50
+ div8 = Bit::Field.read_bool(reader)
51
+ if div8
52
+ distributions = [Bit::Field.val(16), Bit::Field.val(32), Bit::Field.bits_offset(5, 1),
53
+ Bit::Field.bits_offset(9, 33)]
54
+ height = Bit::Field.u32(reader, *distributions) * 8
55
+ else
56
+ distributions = [Bit::Field.bits_offset(6, 1), Bit::Field.bits_offset(8, 65),
57
+ Bit::Field.bits_offset(10, 321), Bit::Field.bits_offset(12, 1345)]
58
+ height = Bit::Field.u32(reader, *distributions)
59
+ end
60
+ ratio = reader.read(3)
61
+ width = if ratio.zero?
62
+ Bit::Field.u32(reader, *distributions) * (div8 ? 8 : 1)
63
+ else
64
+ ratio_width(height, ratio)
65
+ end
66
+ SizeHeader.new(width:, height:)
67
+ end
68
+ private_class_method :read_preview_size
69
+
70
+ def self.ratio_width(height, ratio)
71
+ numerator, denominator = SizeHeader::RATIOS.fetch(ratio - 1)
72
+ height * numerator / denominator
73
+ end
74
+ private_class_method :ratio_width
75
+
76
+ def self.read_animation(reader)
77
+ numerator = Bit::Field.u32(reader, Bit::Field.val(100), Bit::Field.val(1000),
78
+ Bit::Field.bits_offset(10, 1), Bit::Field.bits_offset(30, 1))
79
+ denominator = Bit::Field.u32(reader, Bit::Field.val(1), Bit::Field.val(1001),
80
+ Bit::Field.bits_offset(8, 1), Bit::Field.bits_offset(10, 1))
81
+ loops = Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.bits(3),
82
+ Bit::Field.bits(16), Bit::Field.bits(32))
83
+ Animation.new(ticks_numerator: numerator, ticks_denominator: denominator,
84
+ loops:, have_timecodes: Bit::Field.read_bool(reader))
85
+ end
86
+ private_class_method :read_animation
87
+
88
+ def self.read_extra_channel(reader)
89
+ return default_extra_channel if Bit::Field.read_bool(reader)
90
+
91
+ type = Bit::Field.enum(reader)
92
+ bit_depth = BitDepth.read(reader)
93
+ dim_shift = Bit::Field.u32(reader, Bit::Field.val(0), Bit::Field.val(3),
94
+ Bit::Field.val(4), Bit::Field.bits_offset(3, 1))
95
+ raise CorruptError, "extra-channel shift too large" if (1 << dim_shift) > 8
96
+
97
+ name = Bit::Field.string(reader)
98
+ alpha_associated = type.zero? ? Bit::Field.read_bool(reader) : false
99
+ spot_colour = type == 2 ? Array.new(4) { Bit::Field.f16(reader) } : nil
100
+ cfa_channel = if type == 5
101
+ Bit::Field.u32(reader, Bit::Field.val(1), Bit::Field.bits(2),
102
+ Bit::Field.bits_offset(4, 3), Bit::Field.bits_offset(8, 19))
103
+ end
104
+ ExtraChannel.new(type:, bit_depth:, dim_shift:, name:, alpha_associated:, spot_colour:, cfa_channel:)
105
+ end
106
+ private_class_method :read_extra_channel
107
+
108
+ def self.default_extra_channel
109
+ ExtraChannel.new(type: 0, bit_depth: BitDepth.new(floating_point: false, bits_per_sample: 8, exponent_bits: 0),
110
+ dim_shift: 0, name: "", alpha_associated: false, spot_colour: nil, cfa_channel: nil)
111
+ end
112
+ private_class_method :default_extra_channel
113
+
114
+ def self.read_tone_mapping(reader)
115
+ return default_tone_mapping if Bit::Field.read_bool(reader)
116
+
117
+ ToneMapping.new(intensity_target: Bit::Field.f16(reader), min_nits: Bit::Field.f16(reader),
118
+ relative_to_max_display: Bit::Field.read_bool(reader), linear_below: Bit::Field.f16(reader))
119
+ end
120
+ private_class_method :read_tone_mapping
121
+
122
+ def self.default_tone_mapping
123
+ ToneMapping.new(intensity_target: 255.0, min_nits: 0.0, relative_to_max_display: false, linear_below: 0.0)
124
+ end
125
+ private_class_method :default_tone_mapping
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ module Headers
5
+ SizeHeader = Data.define(:width, :height)
6
+
7
+ class SizeHeader
8
+ RATIOS = [[1, 1], [12, 10], [4, 3], [3, 2], [16, 9], [5, 4], [2, 1]].freeze
9
+ DIMENSION = [Bit::Field.bits_offset(9, 1), Bit::Field.bits_offset(13, 1),
10
+ Bit::Field.bits_offset(18, 1), Bit::Field.bits_offset(30, 1)].freeze
11
+
12
+ def self.read(reader, max_pixels: 100_000_000)
13
+ small = Bit::Field.read_bool(reader)
14
+ height = small ? (reader.read(5) + 1) * 8 : Bit::Field.u32(reader, *DIMENSION)
15
+ ratio = reader.read(3)
16
+ width = if ratio.zero?
17
+ small ? (reader.read(5) + 1) * 8 : Bit::Field.u32(reader, *DIMENSION)
18
+ else
19
+ numerator, denominator = RATIOS.fetch(ratio - 1)
20
+ height * numerator / denominator
21
+ end
22
+ raise CorruptError, "empty JPEG XL image" unless width.positive? && height.positive?
23
+ raise ResourceLimitError, "image exceeds pixel limit" if width * height > max_pixels
24
+
25
+ new(width:, height:)
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/jxl/image.rb ADDED
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JXL
4
+ class Frame
5
+ attr_reader :planes, :duration, :timecode, :name, :blend_info
6
+
7
+ def initialize(planes:, duration:, timecode:, name:, blend_info:)
8
+ @planes = planes
9
+ @duration = duration
10
+ @timecode = timecode
11
+ @name = name
12
+ @blend_info = blend_info
13
+ end
14
+ end
15
+
16
+ class Image
17
+ attr_reader :width, :height, :channels, :metadata, :frames, :icc_profile
18
+
19
+ def initialize(width:, height:, channels:, metadata:, frames: [], icc_profile: nil) # rubocop:disable Metrics/ParameterLists
20
+ @width = width
21
+ @height = height
22
+ @channels = channels
23
+ @metadata = metadata
24
+ @frames = frames
25
+ @icc_profile = icc_profile
26
+ end
27
+
28
+ def with(**changes)
29
+ self.class.new(width: changes.fetch(:width, width), height: changes.fetch(:height, height),
30
+ channels: changes.fetch(:channels, channels), metadata: changes.fetch(:metadata, metadata),
31
+ frames: changes.fetch(:frames, frames), icc_profile: changes.fetch(:icc_profile, icc_profile))
32
+ end
33
+
34
+ def num_color_channels = channels.length - metadata.extra_channels.length
35
+ def extra_channels = channels.drop(num_color_channels)
36
+ def color_encoding = metadata.colour_encoding
37
+
38
+ # Returns interleaved RGBA pixels with 8-bit unsigned samples.
39
+ def to_rgba8 = interleaved_rgba.map { |value| (Num.clamp(value, 0.0, 1.0) * 255).round }.pack("C*")
40
+
41
+ # Returns little-endian interleaved RGBA pixels with 16-bit unsigned samples.
42
+ def to_rgba16
43
+ interleaved_rgba.map { |value| (Num.clamp(value, 0.0, 1.0) * 65_535).round }.pack("v*")
44
+ end
45
+
46
+ # Returns little-endian interleaved RGBA pixels with binary32 samples.
47
+ def to_float = interleaved_rgba.pack("e*")
48
+
49
+ private
50
+
51
+ def interleaved_rgba
52
+ colour = num_color_channels == 1 ? [channels[0]] * 3 : channels.first(3)
53
+ alpha_index = metadata.extra_channels.index { _1.type.zero? }
54
+ alpha = extra_channels[alpha_index] if alpha_index
55
+ [].tap do |values|
56
+ (width * height).times do |pixel|
57
+ colour.each { |plane| values << plane.data[pixel] }
58
+ values << (alpha ? alpha.data[pixel] : 1.0)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end