pure_jpeg 0.3.3 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +47 -0
- data/CREATIVE.md +100 -0
- data/LICENSE +7 -0
- data/LICENSE-IJG +260 -0
- data/README.md +50 -10
- data/lib/pure_jpeg/bit_reader.rb +21 -4
- data/lib/pure_jpeg/dct.rb +4 -2
- data/lib/pure_jpeg/decoder.rb +171 -95
- data/lib/pure_jpeg/encoder.rb +55 -56
- data/lib/pure_jpeg/huffman/decoder.rb +14 -2
- data/lib/pure_jpeg/huffman/encoder.rb +4 -10
- data/lib/pure_jpeg/jfif_reader.rb +75 -19
- data/lib/pure_jpeg/quantization.rb +28 -0
- data/lib/pure_jpeg/source/raw_source.rb +3 -2
- data/lib/pure_jpeg/version.rb +1 -1
- data/lib/pure_jpeg.rb +6 -7
- metadata +6 -2
|
@@ -4,6 +4,16 @@ module PureJPEG
|
|
|
4
4
|
module Huffman
|
|
5
5
|
class DecodeTable
|
|
6
6
|
def initialize(bits, values)
|
|
7
|
+
unless bits.length == 16 && bits.all? { |n| n.is_a?(Integer) && n.between?(0, 255) } &&
|
|
8
|
+
values.length.between?(1, 256) && bits.sum == values.length
|
|
9
|
+
raise DecodeError, "Invalid JPEG Huffman table lengths"
|
|
10
|
+
end
|
|
11
|
+
available = 1
|
|
12
|
+
bits.each do |count|
|
|
13
|
+
available = available * 2 - count
|
|
14
|
+
# JPEG reserves the all-ones code for entropy padding.
|
|
15
|
+
raise DecodeError, "Oversubscribed JPEG Huffman table" if available <= 0
|
|
16
|
+
end
|
|
7
17
|
@min_code = Array.new(17, 0)
|
|
8
18
|
@max_code = Array.new(17, -1)
|
|
9
19
|
@val_ptr = Array.new(17, 0)
|
|
@@ -25,13 +35,15 @@ module PureJPEG
|
|
|
25
35
|
end
|
|
26
36
|
|
|
27
37
|
# Decode one Huffman symbol from the bit reader.
|
|
28
|
-
def
|
|
38
|
+
def decode_symbol(reader)
|
|
29
39
|
code = 0
|
|
30
|
-
|
|
40
|
+
len = 1
|
|
41
|
+
while len <= 16
|
|
31
42
|
code = (code << 1) | reader.read_bit
|
|
32
43
|
if @max_code[len] >= 0 && code <= @max_code[len]
|
|
33
44
|
return @values[@val_ptr[len] + code - @min_code[len]]
|
|
34
45
|
end
|
|
46
|
+
len += 1
|
|
35
47
|
end
|
|
36
48
|
raise PureJPEG::DecodeError, "Invalid Huffman code"
|
|
37
49
|
end
|
|
@@ -6,14 +6,7 @@ module PureJPEG
|
|
|
6
6
|
# Return the Huffman category (bit length) for a value.
|
|
7
7
|
# Avoids Array allocation compared to the combined category_and_bits.
|
|
8
8
|
def self.category(value)
|
|
9
|
-
|
|
10
|
-
v = value.abs
|
|
11
|
-
cat = 0
|
|
12
|
-
while v > 0
|
|
13
|
-
cat += 1
|
|
14
|
-
v >>= 1
|
|
15
|
-
end
|
|
16
|
-
cat
|
|
9
|
+
value.abs.bit_length
|
|
17
10
|
end
|
|
18
11
|
|
|
19
12
|
# Return the extra bits to encode for a value with the given category.
|
|
@@ -50,6 +43,7 @@ module PureJPEG
|
|
|
50
43
|
end
|
|
51
44
|
|
|
52
45
|
yield 0x00, 0 if last_nonzero < 63
|
|
46
|
+
nil
|
|
53
47
|
end
|
|
54
48
|
|
|
55
49
|
def self.each_ac_symbol(zigzag)
|
|
@@ -85,12 +79,12 @@ module PureJPEG
|
|
|
85
79
|
end
|
|
86
80
|
|
|
87
81
|
def encode_ac(zigzag, writer)
|
|
88
|
-
|
|
82
|
+
PureJPEG::Huffman::Encoder.each_ac_item(zigzag) do |symbol, value|
|
|
89
83
|
code, length = @ac_table[symbol]
|
|
90
84
|
writer.write_bits(code, length)
|
|
91
85
|
next if symbol == 0x00 || symbol == 0xF0
|
|
92
86
|
|
|
93
|
-
cat =
|
|
87
|
+
cat = symbol & 0x0F
|
|
94
88
|
writer.write_bits(self.class.value_bits(value, cat), cat)
|
|
95
89
|
end
|
|
96
90
|
end
|
|
@@ -3,19 +3,24 @@
|
|
|
3
3
|
module PureJPEG
|
|
4
4
|
class JFIFReader
|
|
5
5
|
attr_reader :width, :height, :components, :quant_tables, :huffman_tables,
|
|
6
|
-
:restart_interval, :progressive, :scans, :icc_profile
|
|
6
|
+
:restart_interval, :progressive, :scans, :icc_profile,
|
|
7
|
+
:precision, :adobe_transform, :component_quant_tables
|
|
7
8
|
|
|
8
9
|
Component = Struct.new(:id, :h_sampling, :v_sampling, :qt_id)
|
|
9
10
|
ScanComponent = Struct.new(:id, :dc_table_id, :ac_table_id)
|
|
10
|
-
Scan = Struct.new(:components, :spectral_start, :spectral_end, :successive_high, :successive_low, :data, :huffman_tables)
|
|
11
|
+
Scan = Struct.new(:components, :spectral_start, :spectral_end, :successive_high, :successive_low, :data, :huffman_tables) do
|
|
12
|
+
attr_accessor :restart_interval
|
|
13
|
+
end
|
|
11
14
|
|
|
12
|
-
def initialize(data, stop_after_frame: false)
|
|
15
|
+
def initialize(data, stop_after_frame: false, metadata_only: false)
|
|
13
16
|
@data = data.b
|
|
14
17
|
@stop_after_frame = stop_after_frame
|
|
18
|
+
@metadata_only = metadata_only
|
|
15
19
|
@pos = 0
|
|
16
20
|
@quant_tables = {}
|
|
17
21
|
@huffman_tables = {}
|
|
18
22
|
@components = []
|
|
23
|
+
@component_quant_tables = {}
|
|
19
24
|
@restart_interval = 0
|
|
20
25
|
@progressive = false
|
|
21
26
|
@scans = []
|
|
@@ -38,16 +43,20 @@ module PureJPEG
|
|
|
38
43
|
expect_marker(0xD8) # SOI
|
|
39
44
|
|
|
40
45
|
loop do
|
|
46
|
+
# Header-only input is useful for metadata queries, even without SOS.
|
|
47
|
+
return if @metadata_only && @width && @pos == @data.bytesize
|
|
41
48
|
marker = read_marker
|
|
42
49
|
case marker
|
|
43
50
|
when 0xE2 # APP2 (may contain ICC profile)
|
|
44
51
|
parse_app2
|
|
45
|
-
when
|
|
52
|
+
when 0xEE # Adobe APP14
|
|
53
|
+
parse_app14
|
|
54
|
+
when 0xE0, 0xE1, 0xE3..0xED, 0xEF # Other application markers
|
|
46
55
|
skip_segment
|
|
47
56
|
when 0xDB # DQT
|
|
48
|
-
parse_dqt
|
|
57
|
+
@metadata_only ? skip_segment : parse_dqt
|
|
49
58
|
when 0xC4 # DHT
|
|
50
|
-
parse_dht
|
|
59
|
+
@metadata_only ? skip_segment : parse_dht
|
|
51
60
|
when 0xC0 # SOF0 (baseline)
|
|
52
61
|
parse_sof0
|
|
53
62
|
return if @stop_after_frame
|
|
@@ -56,17 +65,30 @@ module PureJPEG
|
|
|
56
65
|
@progressive = true
|
|
57
66
|
return if @stop_after_frame
|
|
58
67
|
when 0xDA # SOS
|
|
68
|
+
return if @metadata_only
|
|
59
69
|
scan = parse_sos
|
|
60
70
|
scan.data = extract_scan_data
|
|
61
71
|
scan.huffman_tables = @huffman_tables.dup
|
|
72
|
+
scan.restart_interval = @restart_interval
|
|
73
|
+
# A component keeps the quantizer selected by its first scan, even
|
|
74
|
+
# if another scan later redefines that table ID for other components.
|
|
75
|
+
scan.components.each do |sc|
|
|
76
|
+
comp = @components.find { |c| c.id == sc.id }
|
|
77
|
+
if comp && !@component_quant_tables.key?(comp.id)
|
|
78
|
+
@component_quant_tables[comp.id] = @quant_tables[comp.qt_id]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
62
81
|
@scans << scan
|
|
63
|
-
return unless @progressive
|
|
64
82
|
when 0xFE # COM (comment)
|
|
65
83
|
skip_segment
|
|
66
84
|
when 0xDD # DRI (restart interval)
|
|
67
85
|
parse_dri
|
|
68
86
|
when 0xD9 # EOI
|
|
69
87
|
return
|
|
88
|
+
when 0xC1, 0xC3, 0xC5..0xCB, 0xCD..0xCF
|
|
89
|
+
raise DecodeError, "Unsupported JPEG frame marker: 0x#{marker.to_s(16)}"
|
|
90
|
+
when 0xD0..0xD8, 0x00, 0x01
|
|
91
|
+
raise DecodeError, "Unexpected JPEG marker: 0x#{marker.to_s(16)}"
|
|
70
92
|
else
|
|
71
93
|
skip_segment
|
|
72
94
|
end
|
|
@@ -100,13 +122,24 @@ module PureJPEG
|
|
|
100
122
|
|
|
101
123
|
ICC_PROFILE_SIG = "ICC_PROFILE\0".b
|
|
102
124
|
|
|
125
|
+
def parse_app14
|
|
126
|
+
end_pos = read_segment_end
|
|
127
|
+
if end_pos - @pos >= 12 && @data[@pos, 5] == "Adobe"
|
|
128
|
+
@adobe_transform = @data.getbyte(@pos + 11)
|
|
129
|
+
end
|
|
130
|
+
@pos = end_pos
|
|
131
|
+
end
|
|
132
|
+
|
|
103
133
|
def parse_app2
|
|
104
134
|
end_pos = read_segment_end
|
|
105
135
|
|
|
106
136
|
if (end_pos - @pos) >= 14 && @data[@pos, 12] == ICC_PROFILE_SIG
|
|
107
137
|
@pos += 12
|
|
108
138
|
seq_no = read_byte
|
|
109
|
-
|
|
139
|
+
total = read_byte
|
|
140
|
+
@icc_invalid = true if total == 0 || seq_no == 0 || seq_no > total ||
|
|
141
|
+
(@icc_total && @icc_total != total) || @icc_chunks.key?(seq_no)
|
|
142
|
+
@icc_total = total
|
|
110
143
|
@icc_chunks[seq_no] = @data[@pos, end_pos - @pos]
|
|
111
144
|
end
|
|
112
145
|
|
|
@@ -115,6 +148,7 @@ module PureJPEG
|
|
|
115
148
|
|
|
116
149
|
def assemble_icc_profile
|
|
117
150
|
return if @icc_chunks.empty?
|
|
151
|
+
return if @icc_invalid || @icc_chunks.length != @icc_total
|
|
118
152
|
|
|
119
153
|
@icc_profile = @icc_chunks.sort_by(&:first).map(&:last).join.b
|
|
120
154
|
end
|
|
@@ -130,6 +164,9 @@ module PureJPEG
|
|
|
130
164
|
info = read_byte
|
|
131
165
|
precision = (info >> 4) & 0x0F # 0 = 8-bit, 1 = 16-bit
|
|
132
166
|
table_id = info & 0x0F
|
|
167
|
+
unless precision <= 1 && table_id <= 3
|
|
168
|
+
raise DecodeError, "Invalid JPEG quantization table definition"
|
|
169
|
+
end
|
|
133
170
|
bytes_per_value = precision == 0 ? 1 : 2
|
|
134
171
|
ensure_remaining_in_segment!(end_pos, 64 * bytes_per_value, "DQT")
|
|
135
172
|
|
|
@@ -137,6 +174,7 @@ module PureJPEG
|
|
|
137
174
|
64.times do |i|
|
|
138
175
|
zigzag_table[i] = precision == 0 ? read_byte : read_u16
|
|
139
176
|
end
|
|
177
|
+
raise DecodeError, "Zero JPEG quantization value" if zigzag_table.include?(0)
|
|
140
178
|
# DQT stores values in zigzag order; convert to raster order
|
|
141
179
|
table = Array.new(64)
|
|
142
180
|
64.times { |i| table[Zigzag::ORDER[i]] = zigzag_table[i] }
|
|
@@ -152,20 +190,27 @@ module PureJPEG
|
|
|
152
190
|
info = read_byte
|
|
153
191
|
table_class = (info >> 4) & 0x0F # 0 = DC, 1 = AC
|
|
154
192
|
table_id = info & 0x0F
|
|
193
|
+
unless table_class <= 1 && table_id <= 3
|
|
194
|
+
raise DecodeError, "Invalid JPEG Huffman table definition"
|
|
195
|
+
end
|
|
155
196
|
|
|
156
197
|
bits = Array.new(16) { read_byte }
|
|
157
198
|
total = bits.sum
|
|
158
199
|
ensure_remaining_in_segment!(end_pos, total, "DHT")
|
|
159
200
|
values = Array.new(total) { read_byte }
|
|
201
|
+
if values.any? { |value| table_class == 0 ? value > 11 : (value & 15) > 10 }
|
|
202
|
+
raise DecodeError, "Invalid JPEG Huffman symbol"
|
|
203
|
+
end
|
|
160
204
|
|
|
161
205
|
@huffman_tables[[table_class, table_id]] = { bits: bits, values: values }
|
|
162
206
|
end
|
|
163
207
|
end
|
|
164
208
|
|
|
165
209
|
def parse_sof0
|
|
210
|
+
raise DecodeError, "Multiple JPEG frame headers are unsupported" if @width
|
|
166
211
|
end_pos = read_segment_end
|
|
167
212
|
ensure_remaining_in_segment!(end_pos, 6, "SOF")
|
|
168
|
-
|
|
213
|
+
@precision = read_byte
|
|
169
214
|
@height = read_u16
|
|
170
215
|
@width = read_u16
|
|
171
216
|
num_components = read_byte
|
|
@@ -179,6 +224,13 @@ module PureJPEG
|
|
|
179
224
|
qt_id = read_byte
|
|
180
225
|
Component.new(id, h, v, qt_id)
|
|
181
226
|
end
|
|
227
|
+
raise DecodeError, "JPEG frame has no components" if @components.empty?
|
|
228
|
+
if @components.map(&:id).uniq.length != @components.length
|
|
229
|
+
raise DecodeError, "Duplicate JPEG component id"
|
|
230
|
+
end
|
|
231
|
+
unless @components.all? { |c| c.h_sampling.between?(1, 4) && c.v_sampling.between?(1, 4) }
|
|
232
|
+
raise DecodeError, "Invalid JPEG sampling factors"
|
|
233
|
+
end
|
|
182
234
|
@pos = end_pos
|
|
183
235
|
end
|
|
184
236
|
|
|
@@ -234,19 +286,23 @@ module PureJPEG
|
|
|
234
286
|
def extract_scan_data
|
|
235
287
|
start = @pos
|
|
236
288
|
len = @data.bytesize
|
|
237
|
-
#
|
|
238
|
-
|
|
289
|
+
# Preserve the first FF of a marker's fill run for read_marker. Stuffed
|
|
290
|
+
# bytes and restarts belong to the entropy stream, segment markers do not.
|
|
291
|
+
while @pos < len
|
|
239
292
|
found = @data.index("\xFF".b, @pos)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
293
|
+
raise DecodeError, "Unexpected end of JPEG scan" unless found
|
|
294
|
+
marker_pos = found + 1
|
|
295
|
+
marker_pos += 1 while marker_pos < len && @data.getbyte(marker_pos) == 0xFF
|
|
296
|
+
raise DecodeError, "Unexpected end of JPEG scan" if marker_pos == len
|
|
297
|
+
code = @data.getbyte(marker_pos)
|
|
298
|
+
if code == 0x00 || code.between?(0xD0, 0xD7)
|
|
299
|
+
@pos = marker_pos + 1
|
|
300
|
+
else
|
|
301
|
+
@pos = found
|
|
302
|
+
return @data[start...found]
|
|
246
303
|
end
|
|
247
|
-
@pos += 2
|
|
248
304
|
end
|
|
249
|
-
|
|
305
|
+
raise DecodeError, "Unexpected end of JPEG scan"
|
|
250
306
|
end
|
|
251
307
|
end
|
|
252
308
|
end
|
|
@@ -58,5 +58,33 @@ module PureJPEG
|
|
|
58
58
|
64.times { |i| out[i] = block[i] * table[i] }
|
|
59
59
|
out
|
|
60
60
|
end
|
|
61
|
+
|
|
62
|
+
# Quantize raster-order coefficients directly into zigzag order.
|
|
63
|
+
def self.quantize_zigzag!(block, table, out)
|
|
64
|
+
order = Zigzag::ORDER
|
|
65
|
+
i = 0
|
|
66
|
+
while i < 64
|
|
67
|
+
j = order[i]
|
|
68
|
+
v = block[j]; t = table[j]
|
|
69
|
+
out[i] = if v >= 0
|
|
70
|
+
(v + (t >> 1)) / t
|
|
71
|
+
else
|
|
72
|
+
-((-v + (t >> 1)) / t)
|
|
73
|
+
end
|
|
74
|
+
i += 1
|
|
75
|
+
end
|
|
76
|
+
out
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Dequantize zigzag coefficients directly into raster order. The offset
|
|
80
|
+
# allows progressive decoding to read from its full coefficient buffer.
|
|
81
|
+
def self.dequantize_zigzag!(block, table, out, offset = 0)
|
|
82
|
+
order = Zigzag::ORDER
|
|
83
|
+
64.times do |i|
|
|
84
|
+
j = order[i]
|
|
85
|
+
out[j] = block[offset + i] * table[j]
|
|
86
|
+
end
|
|
87
|
+
out
|
|
88
|
+
end
|
|
61
89
|
end
|
|
62
90
|
end
|
|
@@ -27,15 +27,16 @@ module PureJPEG
|
|
|
27
27
|
def initialize(width, height, &block)
|
|
28
28
|
@width = width
|
|
29
29
|
@height = height
|
|
30
|
-
@pixels = Array.new(width * height) { Pixel.new(0, 0, 0) }
|
|
31
|
-
|
|
32
30
|
if block
|
|
31
|
+
@pixels = Array.new(width * height)
|
|
33
32
|
height.times do |y|
|
|
34
33
|
width.times do |x|
|
|
35
34
|
r, g, b = block.call(x, y)
|
|
36
35
|
@pixels[y * width + x] = Pixel.new(r, g, b)
|
|
37
36
|
end
|
|
38
37
|
end
|
|
38
|
+
else
|
|
39
|
+
@pixels = Array.new(width * height) { Pixel.new(0, 0, 0) }
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
|
data/lib/pure_jpeg/version.rb
CHANGED
data/lib/pure_jpeg.rb
CHANGED
|
@@ -21,10 +21,9 @@ require_relative "pure_jpeg/image"
|
|
|
21
21
|
require_relative "pure_jpeg/encoder"
|
|
22
22
|
require_relative "pure_jpeg/decoder"
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# color (4:2:0 chroma subsampling), and standard Huffman tables (Annex K).
|
|
24
|
+
# Pure Ruby JPEG encoder and decoder with no native dependencies.
|
|
25
|
+
# Encodes baseline DCT (SOF0), and decodes baseline and progressive (SOF2)
|
|
26
|
+
# 8-bit grayscale and YCbCr JPEGs.
|
|
28
27
|
module PureJPEG
|
|
29
28
|
# Raised when decoding invalid or unsupported JPEG data.
|
|
30
29
|
class DecodeError < StandardError; end
|
|
@@ -70,15 +69,15 @@ module PureJPEG
|
|
|
70
69
|
# Read JPEG dimensions and basic frame metadata without decoding scan data.
|
|
71
70
|
#
|
|
72
71
|
# @param path_or_data [String] a file path or raw JPEG bytes
|
|
73
|
-
# @return [Info]
|
|
72
|
+
# @return [Info] frame metadata and complete ICC profile from headers before SOS
|
|
74
73
|
def self.info(path_or_data)
|
|
75
|
-
data = if path_or_data.is_a?(String) && !path_or_data.start_with?("\xFF\xD8".b) && File.exist?(path_or_data)
|
|
74
|
+
data = if path_or_data.is_a?(String) && !path_or_data.b.include?("\0") && !path_or_data.b.start_with?("\xFF\xD8".b) && File.exist?(path_or_data)
|
|
76
75
|
File.binread(path_or_data)
|
|
77
76
|
else
|
|
78
77
|
path_or_data.b
|
|
79
78
|
end
|
|
80
79
|
|
|
81
|
-
jfif = JFIFReader.new(data,
|
|
80
|
+
jfif = JFIFReader.new(data, metadata_only: true)
|
|
82
81
|
raise DecodeError, "JPEG frame header not found" unless jfif.width && jfif.height
|
|
83
82
|
|
|
84
83
|
Info.new(
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pure_jpeg
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter Cooper
|
|
@@ -37,7 +37,8 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '5.0'
|
|
40
|
-
description: A pure Ruby baseline JPEG encoder and
|
|
40
|
+
description: A pure Ruby baseline JPEG encoder and baseline/progressive JPEG decoder
|
|
41
|
+
with no native dependencies.
|
|
41
42
|
email:
|
|
42
43
|
- git@peterc.org
|
|
43
44
|
executables: []
|
|
@@ -45,7 +46,9 @@ extensions: []
|
|
|
45
46
|
extra_rdoc_files: []
|
|
46
47
|
files:
|
|
47
48
|
- CHANGELOG.md
|
|
49
|
+
- CREATIVE.md
|
|
48
50
|
- LICENSE
|
|
51
|
+
- LICENSE-IJG
|
|
49
52
|
- README.md
|
|
50
53
|
- lib/pure_jpeg.rb
|
|
51
54
|
- lib/pure_jpeg/bit_reader.rb
|
|
@@ -69,6 +72,7 @@ files:
|
|
|
69
72
|
homepage: https://github.com/peterc/pure_jpeg
|
|
70
73
|
licenses:
|
|
71
74
|
- MIT
|
|
75
|
+
- IJG
|
|
72
76
|
metadata:
|
|
73
77
|
source_code_uri: https://github.com/peterc/pure_jpeg
|
|
74
78
|
changelog_uri: https://github.com/peterc/pure_jpeg/blob/main/CHANGELOG.md
|