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
data/lib/jxl/io/npy.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module IO
|
|
5
|
+
module NPY
|
|
6
|
+
MAGIC = "\x93NUMPY".b
|
|
7
|
+
FORMATS = {
|
|
8
|
+
"<f4" => "e*",
|
|
9
|
+
"<f8" => "E*",
|
|
10
|
+
"|u1" => "C*",
|
|
11
|
+
"<u2" => "v*",
|
|
12
|
+
"<i4" => "l<*"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def load(path)
|
|
18
|
+
data = File.binread(path)
|
|
19
|
+
raise FormatError, "invalid NPY signature" unless data.start_with?(MAGIC)
|
|
20
|
+
|
|
21
|
+
major = data.getbyte(6)
|
|
22
|
+
header_offset, header_length = header_location(data, major)
|
|
23
|
+
header = data.byteslice(header_offset, header_length)
|
|
24
|
+
descriptor = header[/['"]descr['"]\s*:\s*['"]([^'"]+)['"]/, 1]
|
|
25
|
+
fortran = header[/['"]fortran_order['"]\s*:\s*(True|False)/, 1]
|
|
26
|
+
raise FormatError, "invalid NPY header" unless descriptor && fortran
|
|
27
|
+
raise UnsupportedFeatureError, "Fortran-order NPY array" if fortran == "True"
|
|
28
|
+
|
|
29
|
+
shape = parse_shape(header)
|
|
30
|
+
format = FORMATS[descriptor] || raise(UnsupportedFeatureError, "NPY dtype #{descriptor}")
|
|
31
|
+
values = data.byteslice((header_offset + header_length)..).unpack(format)
|
|
32
|
+
expected = shape.inject(1, :*)
|
|
33
|
+
unless values.length == expected
|
|
34
|
+
raise CorruptError,
|
|
35
|
+
"NPY shape contains #{expected} values, got #{values.length}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
[shape, values]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def dump(path, shape, values, descriptor: "<f4")
|
|
42
|
+
format = FORMATS[descriptor] || raise(UnsupportedFeatureError, "NPY dtype #{descriptor}")
|
|
43
|
+
expected = shape.inject(1, :*)
|
|
44
|
+
raise ArgumentError, "shape does not match values" unless expected == values.length
|
|
45
|
+
|
|
46
|
+
shape_text = shape.length == 1 ? "#{shape.first}," : shape.join(", ")
|
|
47
|
+
dictionary = "{'descr': '#{descriptor}', 'fortran_order': False, 'shape': (#{shape_text}), }"
|
|
48
|
+
padding = 16 - ((MAGIC.bytesize + 4 + dictionary.bytesize + 1) % 16)
|
|
49
|
+
header = "#{dictionary}#{' ' * padding}\n"
|
|
50
|
+
File.binwrite(path, MAGIC + [1, 0, header.bytesize].pack("CCv") + header + values.pack(format))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def dump_image(path, image)
|
|
54
|
+
frame_planes = image.frames.empty? ? [image.channels] : image.frames.map(&:planes)
|
|
55
|
+
channels = image.num_color_channels + image.metadata.extra_channels.length
|
|
56
|
+
values = frame_planes.flat_map { |planes| planes.first(channels).map(&:data).transpose.flatten }
|
|
57
|
+
dump(path, [frame_planes.length, image.height, image.width, channels], values)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def header_location(data, major)
|
|
61
|
+
case major
|
|
62
|
+
when 1 then [10, data.unpack1("v", offset: 8)]
|
|
63
|
+
when 2, 3 then [12, data.unpack1("V", offset: 8)]
|
|
64
|
+
else raise UnsupportedFeatureError, "NPY version #{major}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
private_class_method :header_location
|
|
68
|
+
|
|
69
|
+
def parse_shape(header)
|
|
70
|
+
text = header[/['"]shape['"]\s*:\s*\(([^)]*)\)/, 1]
|
|
71
|
+
raise FormatError, "invalid NPY shape" unless text
|
|
72
|
+
|
|
73
|
+
text.split(",").filter_map do |part|
|
|
74
|
+
next if part.strip.empty?
|
|
75
|
+
|
|
76
|
+
Integer(part, 10)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
private_class_method :parse_shape
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/jxl/io/pam.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module IO
|
|
5
|
+
module PAM
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def dump(image)
|
|
9
|
+
header = "P7\nWIDTH #{image.width}\nHEIGHT #{image.height}\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n"
|
|
10
|
+
header.b + image.to_rgba8
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/jxl/io/pfm.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module IO
|
|
5
|
+
module PFM
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def dump(image)
|
|
9
|
+
grayscale = image.num_color_channels == 1
|
|
10
|
+
planes = grayscale ? [image.channels.first] : image.channels.first(3)
|
|
11
|
+
values = []
|
|
12
|
+
(image.height - 1).downto(0) do |y|
|
|
13
|
+
image.width.times do |x|
|
|
14
|
+
planes.each { |plane| values << plane[x, y] }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
"#{grayscale ? 'Pf' : 'PF'}\n#{image.width} #{image.height}\n-1.0\n".b + values.pack("e*")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/jxl/io/pgx.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module IO
|
|
5
|
+
module PGX
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def dump(image)
|
|
9
|
+
raise UnsupportedFeatureError, "PGX requires grayscale without extra channels" \
|
|
10
|
+
unless image.num_color_channels == 1 && image.extra_channels.empty?
|
|
11
|
+
|
|
12
|
+
bits = image.metadata.bit_depth.bits_per_sample
|
|
13
|
+
raise UnsupportedFeatureError, "PGX supports at most 16 bits" if bits > 16
|
|
14
|
+
|
|
15
|
+
bits = bits <= 8 ? 8 : 16
|
|
16
|
+
maximum = (1 << bits) - 1
|
|
17
|
+
pixels = image.channels.first.data.map { (Num.clamp(_1, 0.0, 1.0) * maximum).round }
|
|
18
|
+
"PG ML + #{bits} #{image.width} #{image.height}\n".b + pixels.pack(bits == 8 ? "C*" : "n*")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/jxl/io/png.rb
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module JXL
|
|
6
|
+
module IO
|
|
7
|
+
module PNG
|
|
8
|
+
SIGNATURE = "\x89PNG\r\n\x1A\n".b
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def dump(image) # rubocop:disable Metrics/AbcSize
|
|
13
|
+
planes, colour_type = output_planes(image)
|
|
14
|
+
header = [image.width, image.height, 8, colour_type, 0, 0, 0].pack("N2C5")
|
|
15
|
+
pixels = (image.width * image.height).times.flat_map do |pixel|
|
|
16
|
+
planes.map { (Num.clamp(_1.data[pixel], 0.0, 1.0) * 255).round }
|
|
17
|
+
end
|
|
18
|
+
stride = image.width * planes.length
|
|
19
|
+
rows = pixels.each_slice(stride).map { "\0".b + _1.pack("C*") }.join
|
|
20
|
+
SIGNATURE + chunk("IHDR", header) + chunk("iCCP", icc(image)) +
|
|
21
|
+
chunk("IDAT", Zlib::Deflate.deflate(rows)) + chunk("IEND", "".b)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load(data) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
25
|
+
raise FormatError, "invalid PNG signature" unless data.start_with?(SIGNATURE)
|
|
26
|
+
|
|
27
|
+
chunks = read_chunks(data)
|
|
28
|
+
width, height, depth, colour, compression, filter, interlace = chunks.fetch("IHDR").first.unpack("N2C5")
|
|
29
|
+
unless depth == 8 && [0, 2, 4, 6].include?(colour) && [compression, filter, interlace].all?(&:zero?)
|
|
30
|
+
raise UnsupportedFeatureError, "only non-interlaced 8-bit PNG input is supported"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
components = { 0 => 1, 2 => 3, 4 => 2, 6 => 4 }.fetch(colour)
|
|
34
|
+
stride = width * components
|
|
35
|
+
raw = Zlib::Inflate.inflate(chunks.fetch("IDAT").join)
|
|
36
|
+
raise TruncatedError, "PNG pixels are truncated" unless raw.bytesize == height * (stride + 1)
|
|
37
|
+
|
|
38
|
+
previous = Array.new(stride, 0)
|
|
39
|
+
pixels = height.times.flat_map do |row|
|
|
40
|
+
start = row * (stride + 1)
|
|
41
|
+
type = raw.getbyte(start)
|
|
42
|
+
scanline = unfilter(raw.byteslice(start + 1, stride).bytes, previous, components, type)
|
|
43
|
+
previous = scanline
|
|
44
|
+
end
|
|
45
|
+
colour_components = [components, 3].min
|
|
46
|
+
channels = colour_components.times.map do |channel|
|
|
47
|
+
pixels.drop(channel).each_slice(components).map(&:first)
|
|
48
|
+
end
|
|
49
|
+
channels = [channels.first] * 3 if colour_components < 3
|
|
50
|
+
planes = channels.map do |values|
|
|
51
|
+
Plane.new(width, height).tap { |plane| plane.data.replace(values.map { _1 / 255.0 }) }
|
|
52
|
+
end
|
|
53
|
+
Image.new(width:, height:, channels: planes, metadata: Headers::ImageMetadata.defaults)
|
|
54
|
+
rescue KeyError, Zlib::Error => e
|
|
55
|
+
raise FormatError, "invalid PNG: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def chunk(type, data)
|
|
59
|
+
body = type.b + data
|
|
60
|
+
[data.bytesize].pack("N") + body + [Zlib.crc32(body)].pack("N")
|
|
61
|
+
end
|
|
62
|
+
private_class_method :chunk
|
|
63
|
+
|
|
64
|
+
def output_planes(image)
|
|
65
|
+
grayscale = image.num_color_channels == 1
|
|
66
|
+
colour = grayscale ? [image.channels.first] : image.channels.first(3)
|
|
67
|
+
alpha_index = image.metadata.extra_channels.index { _1.type.zero? }
|
|
68
|
+
alpha = image.extra_channels[alpha_index] if alpha_index
|
|
69
|
+
colour_type = if grayscale
|
|
70
|
+
alpha ? 4 : 0
|
|
71
|
+
else
|
|
72
|
+
alpha ? 6 : 2
|
|
73
|
+
end
|
|
74
|
+
[alpha ? colour + [alpha] : colour, colour_type]
|
|
75
|
+
end
|
|
76
|
+
private_class_method :output_planes
|
|
77
|
+
|
|
78
|
+
def read_chunks(data)
|
|
79
|
+
chunks = Hash.new { |hash, key| hash[key] = [] }
|
|
80
|
+
offset = SIGNATURE.bytesize
|
|
81
|
+
while offset < data.bytesize
|
|
82
|
+
raise TruncatedError, "PNG chunk is truncated" if offset + 12 > data.bytesize
|
|
83
|
+
|
|
84
|
+
length = data.unpack1("N", offset:)
|
|
85
|
+
type = data.byteslice(offset + 4, 4)
|
|
86
|
+
payload = data.byteslice(offset + 8, length)
|
|
87
|
+
crc = data.unpack1("N", offset: offset + 8 + length)
|
|
88
|
+
raise TruncatedError, "PNG chunk is truncated" unless payload && crc
|
|
89
|
+
raise FormatError, "invalid PNG checksum" unless Zlib.crc32(type + payload) == crc
|
|
90
|
+
|
|
91
|
+
chunks[type] << payload
|
|
92
|
+
offset += length + 12
|
|
93
|
+
end
|
|
94
|
+
chunks
|
|
95
|
+
end
|
|
96
|
+
private_class_method :read_chunks
|
|
97
|
+
|
|
98
|
+
def unfilter(bytes, previous, components, type) # rubocop:disable Metrics/CyclomaticComplexity
|
|
99
|
+
bytes.each_index do |index|
|
|
100
|
+
left = index >= components ? bytes[index - components] : 0
|
|
101
|
+
top = previous[index]
|
|
102
|
+
top_left = index >= components ? previous[index - components] : 0
|
|
103
|
+
predictor = case type
|
|
104
|
+
when 0 then 0
|
|
105
|
+
when 1 then left
|
|
106
|
+
when 2 then top
|
|
107
|
+
when 3 then (left + top) / 2
|
|
108
|
+
when 4 then paeth(left, top, top_left)
|
|
109
|
+
else raise FormatError, "invalid PNG filter"
|
|
110
|
+
end
|
|
111
|
+
bytes[index] = (bytes[index] + predictor) & 255
|
|
112
|
+
end
|
|
113
|
+
bytes
|
|
114
|
+
end
|
|
115
|
+
private_class_method :unfilter
|
|
116
|
+
|
|
117
|
+
def paeth(left, top, top_left)
|
|
118
|
+
base = left + top - top_left
|
|
119
|
+
distances = [(base - left).abs, (base - top).abs, (base - top_left).abs]
|
|
120
|
+
[left, top, top_left][distances.index(distances.min)]
|
|
121
|
+
end
|
|
122
|
+
private_class_method :paeth
|
|
123
|
+
|
|
124
|
+
def icc(image)
|
|
125
|
+
raise ArgumentError, "image has no ICC profile" unless image.icc_profile
|
|
126
|
+
|
|
127
|
+
"jxl\0\0".b + Zlib::Deflate.deflate(image.icc_profile)
|
|
128
|
+
end
|
|
129
|
+
private_class_method :icc
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
data/lib/jxl/io/ppm.rb
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JXL
|
|
4
|
+
module IO
|
|
5
|
+
module PPM
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def dump(image)
|
|
9
|
+
grayscale = image.num_color_channels == 1
|
|
10
|
+
planes = grayscale ? [image.channels.first] : image.channels.first(3)
|
|
11
|
+
pixels = (image.width * image.height).times.flat_map do |pixel|
|
|
12
|
+
planes.map { (Num.clamp(_1.data[pixel], 0.0, 1.0) * 255).round }
|
|
13
|
+
end
|
|
14
|
+
"#{grayscale ? 'P5' : 'P6'}\n#{image.width} #{image.height}\n255\n".b + pixels.pack("C*")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load(data) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
18
|
+
offset = 0
|
|
19
|
+
magic, offset = token(data, offset)
|
|
20
|
+
width, offset = token(data, offset)
|
|
21
|
+
height, offset = token(data, offset)
|
|
22
|
+
maximum, offset = token(data, offset)
|
|
23
|
+
raise FormatError, "unsupported PNM format" unless %w[P5 P6].include?(magic)
|
|
24
|
+
|
|
25
|
+
width = Integer(width, 10)
|
|
26
|
+
height = Integer(height, 10)
|
|
27
|
+
maximum = Integer(maximum, 10)
|
|
28
|
+
raise FormatError, "unsupported PNM range" unless maximum.between?(1, 65_535)
|
|
29
|
+
|
|
30
|
+
offset += 1
|
|
31
|
+
offset += 1 if data.getbyte(offset - 1) == 13 && data.getbyte(offset) == 10
|
|
32
|
+
count = width * height * (magic == "P6" ? 3 : 1)
|
|
33
|
+
bytes = maximum < 256 ? data.byteslice(offset, count)&.bytes : data.byteslice(offset, count * 2)&.unpack("n*")
|
|
34
|
+
raise TruncatedError, "PNM pixels are truncated" unless bytes&.length == count
|
|
35
|
+
|
|
36
|
+
channels = if magic == "P6"
|
|
37
|
+
3.times.map { |channel| bytes.drop(channel).each_slice(3).map(&:first) }
|
|
38
|
+
else
|
|
39
|
+
[bytes] * 3
|
|
40
|
+
end
|
|
41
|
+
planes = channels.map do |values|
|
|
42
|
+
Plane.new(width, height).tap { |plane| plane.data.replace(values.map { _1 / maximum.to_f }) }
|
|
43
|
+
end
|
|
44
|
+
Image.new(width:, height:, channels: planes, metadata: Headers::ImageMetadata.defaults)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def token(data, offset) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
48
|
+
loop do
|
|
49
|
+
offset += 1 while data.getbyte(offset)&.chr&.match?(/\s/)
|
|
50
|
+
break unless data.getbyte(offset) == 35
|
|
51
|
+
|
|
52
|
+
offset = data.index("\n", offset) || data.bytesize
|
|
53
|
+
end
|
|
54
|
+
finish = offset
|
|
55
|
+
finish += 1 while data.getbyte(finish) && !data.getbyte(finish).chr.match?(/\s/)
|
|
56
|
+
raise TruncatedError, "PNM header is truncated" if finish == offset
|
|
57
|
+
|
|
58
|
+
[data.byteslice(offset, finish - offset), finish]
|
|
59
|
+
end
|
|
60
|
+
private_class_method :token
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|