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
data/lib/pure_jpeg/decoder.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PureJPEG
|
|
4
|
-
# Baseline JPEG decoder.
|
|
4
|
+
# Baseline and progressive JPEG decoder.
|
|
5
5
|
#
|
|
6
|
-
# Decodes baseline
|
|
7
|
-
#
|
|
6
|
+
# Decodes baseline (SOF0) and progressive (SOF2) 8-bit grayscale/YCbCr
|
|
7
|
+
# JPEGs, including separate component scans and restart markers.
|
|
8
8
|
#
|
|
9
9
|
# Use {PureJPEG.read} for a convenient entry point.
|
|
10
10
|
class Decoder
|
|
@@ -13,7 +13,7 @@ module PureJPEG
|
|
|
13
13
|
# @param path_or_data [String] a file path or raw JPEG bytes
|
|
14
14
|
# @return [Image] decoded image with pixel access
|
|
15
15
|
def self.decode(path_or_data)
|
|
16
|
-
data = if path_or_data.is_a?(String) && !path_or_data.start_with?("\xFF\xD8".b) && File.exist?(path_or_data)
|
|
16
|
+
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)
|
|
17
17
|
File.binread(path_or_data)
|
|
18
18
|
else
|
|
19
19
|
path_or_data.b
|
|
@@ -29,91 +29,74 @@ module PureJPEG
|
|
|
29
29
|
jfif = JFIFReader.new(@data)
|
|
30
30
|
@icc_profile = jfif.icc_profile
|
|
31
31
|
validate_dimensions!(jfif.width, jfif.height)
|
|
32
|
+
validate_frame_and_scans!(jfif)
|
|
32
33
|
return decode_progressive(jfif) if jfif.progressive
|
|
33
34
|
|
|
34
35
|
width = jfif.width
|
|
35
36
|
height = jfif.height
|
|
36
37
|
|
|
37
|
-
# Build Huffman decode tables
|
|
38
|
-
dc_tables = {}
|
|
39
|
-
ac_tables = {}
|
|
40
|
-
jfif.huffman_tables.each do |(table_class, table_id), info|
|
|
41
|
-
table = Huffman::DecodeTable.new(info[:bits], info[:values])
|
|
42
|
-
if table_class == 0
|
|
43
|
-
dc_tables[table_id] = table
|
|
44
|
-
else
|
|
45
|
-
ac_tables[table_id] = table
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Map component IDs to their info
|
|
50
38
|
comp_info = {}
|
|
51
39
|
jfif.components.each { |c| comp_info[c.id] = c }
|
|
52
|
-
|
|
53
|
-
# Determine max sampling factors
|
|
54
40
|
max_h = jfif.components.map(&:h_sampling).max
|
|
55
41
|
max_v = jfif.components.map(&:v_sampling).max
|
|
56
|
-
|
|
57
|
-
# MCU dimensions in pixels
|
|
58
42
|
mcu_px_w = max_h * 8
|
|
59
43
|
mcu_px_h = max_v * 8
|
|
60
44
|
mcus_x = (width + mcu_px_w - 1) / mcu_px_w
|
|
61
45
|
mcus_y = (height + mcu_px_h - 1) / mcu_px_h
|
|
62
46
|
|
|
63
|
-
# Allocate channel buffers (full padded size)
|
|
64
|
-
padded_w = mcus_x * mcu_px_w
|
|
65
|
-
padded_h = mcus_y * mcu_px_h
|
|
66
47
|
channels = {}
|
|
67
48
|
jfif.components.each do |c|
|
|
68
|
-
ch_w =
|
|
69
|
-
ch_h =
|
|
49
|
+
ch_w = mcus_x * c.h_sampling * 8
|
|
50
|
+
ch_h = mcus_y * c.v_sampling * 8
|
|
70
51
|
channels[c.id] = { data: Array.new(ch_w * ch_h, 0), width: ch_w, height: ch_h }
|
|
71
52
|
end
|
|
72
|
-
|
|
73
|
-
# Decode scan data
|
|
74
|
-
reader = BitReader.new(jfif.scan_data)
|
|
75
|
-
prev_dc = Hash.new(0)
|
|
76
|
-
restart_interval = jfif.restart_interval
|
|
77
|
-
mcu_count = 0
|
|
78
|
-
|
|
79
|
-
# Reusable buffers
|
|
80
53
|
zigzag = Array.new(64, 0)
|
|
81
|
-
raster = Array.new(64, 0)
|
|
82
54
|
dequant = Array.new(64, 0)
|
|
83
55
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
56
|
+
jfif.scans.each do |scan|
|
|
57
|
+
dc_tables, ac_tables = build_decode_tables(scan)
|
|
58
|
+
reader = BitReader.new(scan.data)
|
|
59
|
+
prev_dc = Hash.new(0)
|
|
60
|
+
restart_interval = scan.restart_interval
|
|
61
|
+
mcu_count = 0
|
|
62
|
+
interleaved = scan.components.length > 1
|
|
63
|
+
scan_components = scan.components.map do |sc|
|
|
64
|
+
comp, dc_tab, ac_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables)
|
|
65
|
+
[sc, comp, dc_tab, ac_tab, fetch_quant_table!(jfif, comp), channels[comp.id]]
|
|
66
|
+
end
|
|
67
|
+
if interleaved
|
|
68
|
+
scan_width = mcus_x
|
|
69
|
+
scan_height = mcus_y
|
|
70
|
+
else
|
|
71
|
+
comp = scan_components[0][1]
|
|
72
|
+
scan_width = (width * comp.h_sampling + mcu_px_w - 1) / mcu_px_w
|
|
73
|
+
scan_height = (height * comp.v_sampling + mcu_px_h - 1) / mcu_px_h
|
|
74
|
+
end
|
|
96
75
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
76
|
+
scan_height.times do |mcu_row|
|
|
77
|
+
scan_width.times do |mcu_col|
|
|
78
|
+
if restart_interval > 0 && mcu_count > 0 && (mcu_count % restart_interval) == 0
|
|
79
|
+
reader.read_restart((mcu_count / restart_interval - 1) % 8)
|
|
80
|
+
prev_dc.clear
|
|
81
|
+
end
|
|
82
|
+
scan_components.each do |sc, comp, dc_tab, ac_tab, qt, ch|
|
|
83
|
+
block_rows = interleaved ? comp.v_sampling : 1
|
|
84
|
+
block_cols = interleaved ? comp.h_sampling : 1
|
|
85
|
+
block_rows.times do |bv|
|
|
86
|
+
block_cols.times do |bh|
|
|
87
|
+
decode_block(reader, dc_tab, ac_tab, prev_dc, sc.id, zigzag)
|
|
88
|
+
Quantization.dequantize_zigzag!(zigzag, qt, dequant)
|
|
89
|
+
DCT.inverse!(dequant)
|
|
90
|
+
bx = (mcu_col * block_cols + bh) * 8
|
|
91
|
+
by = (mcu_row * block_rows + bv) * 8
|
|
92
|
+
write_block(dequant, ch[:data], ch[:width], bx, by)
|
|
93
|
+
end
|
|
111
94
|
end
|
|
112
95
|
end
|
|
96
|
+
mcu_count += 1
|
|
113
97
|
end
|
|
114
|
-
|
|
115
|
-
mcu_count += 1
|
|
116
98
|
end
|
|
99
|
+
reader.finish_scan!
|
|
117
100
|
end
|
|
118
101
|
|
|
119
102
|
# Assemble pixels
|
|
@@ -129,7 +112,85 @@ module PureJPEG
|
|
|
129
112
|
|
|
130
113
|
private
|
|
131
114
|
|
|
115
|
+
def build_decode_tables(scan)
|
|
116
|
+
dc_tables = {}
|
|
117
|
+
ac_tables = {}
|
|
118
|
+
scan.huffman_tables.each do |(table_class, table_id), info|
|
|
119
|
+
table = Huffman::DecodeTable.new(info[:bits], info[:values])
|
|
120
|
+
if table_class == 0
|
|
121
|
+
dc_tables[table_id] = table
|
|
122
|
+
else
|
|
123
|
+
ac_tables[table_id] = table
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
[dc_tables, ac_tables]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def validate_frame_and_scans!(jfif)
|
|
130
|
+
unless jfif.precision == 8
|
|
131
|
+
raise DecodeError, "Unsupported JPEG precision: #{jfif.precision}"
|
|
132
|
+
end
|
|
133
|
+
count = jfif.components.length
|
|
134
|
+
unless count == 1 || count == 3
|
|
135
|
+
raise DecodeError, "Unsupported number of components: #{count}"
|
|
136
|
+
end
|
|
137
|
+
ids = jfif.components.map(&:id)
|
|
138
|
+
if count == 3 && (jfif.adobe_transform == 0 || ids.sort == [66, 71, 82])
|
|
139
|
+
raise DecodeError, "RGB JPEG decoding is unsupported"
|
|
140
|
+
end
|
|
141
|
+
if jfif.adobe_transform && ![0, 1].include?(jfif.adobe_transform)
|
|
142
|
+
raise DecodeError, "Unsupported Adobe color transform: #{jfif.adobe_transform}"
|
|
143
|
+
end
|
|
144
|
+
raise DecodeError, "JPEG scan data not found" if jfif.scans.empty?
|
|
145
|
+
|
|
146
|
+
progression = {}
|
|
147
|
+
ids.each { |id| progression[id] = Array.new(64) }
|
|
148
|
+
jfif.scans.each do |scan|
|
|
149
|
+
scan_ids = scan.components.map(&:id)
|
|
150
|
+
if scan_ids.empty? || scan_ids.uniq.length != scan_ids.length
|
|
151
|
+
raise DecodeError, "Invalid JPEG scan components"
|
|
152
|
+
end
|
|
153
|
+
scan_ids.each do |id|
|
|
154
|
+
raise DecodeError, "Scan references unknown component id #{id}" unless progression.key?(id)
|
|
155
|
+
end
|
|
156
|
+
if scan_ids.length > 1
|
|
157
|
+
blocks = jfif.components.select { |c| scan_ids.include?(c.id) }.sum { |c| c.h_sampling * c.v_sampling }
|
|
158
|
+
raise DecodeError, "Too many blocks in JPEG MCU" if blocks > 10
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
ss = scan.spectral_start
|
|
162
|
+
se = scan.spectral_end
|
|
163
|
+
ah = scan.successive_high
|
|
164
|
+
al = scan.successive_low
|
|
165
|
+
if jfif.progressive
|
|
166
|
+
valid = ss <= se && se <= 63 && ah <= 13 && al <= 13 &&
|
|
167
|
+
(ss == 0 ? se == 0 : scan_ids.length == 1) &&
|
|
168
|
+
(ah == 0 || al == ah - 1)
|
|
169
|
+
raise DecodeError, "Invalid progressive scan parameters" unless valid
|
|
170
|
+
elsif ss != 0 || se != 63 || ah != 0 || al != 0
|
|
171
|
+
raise DecodeError, "Invalid baseline scan parameters"
|
|
172
|
+
end
|
|
173
|
+
scan_ids.each do |id|
|
|
174
|
+
state = progression[id]
|
|
175
|
+
if ss > 0 && state[0].nil?
|
|
176
|
+
raise DecodeError, "Progressive AC scan precedes DC scan"
|
|
177
|
+
end
|
|
178
|
+
ss.upto(se) do |k|
|
|
179
|
+
unless ah == 0 ? state[k].nil? : state[k] == ah
|
|
180
|
+
raise DecodeError, "Invalid JPEG scan progression"
|
|
181
|
+
end
|
|
182
|
+
state[k] = al
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
if progression.values.any? { |state| state[0].nil? }
|
|
187
|
+
raise DecodeError, "JPEG scan omits a frame component"
|
|
188
|
+
end
|
|
189
|
+
jfif.components.each { |c| fetch_quant_table!(jfif, c) }
|
|
190
|
+
end
|
|
191
|
+
|
|
132
192
|
def validate_dimensions!(width, height)
|
|
193
|
+
raise DecodeError, "JPEG frame header not found" unless width && height
|
|
133
194
|
raise DecodeError, "Invalid image dimensions: #{width}x#{height}" if width <= 0 || height <= 0
|
|
134
195
|
raise DecodeError, "Image too large: #{width}x#{height} (max #{MAX_DIMENSION}x#{MAX_DIMENSION})" if width > MAX_DIMENSION || height > MAX_DIMENSION
|
|
135
196
|
end
|
|
@@ -158,12 +219,15 @@ module PureJPEG
|
|
|
158
219
|
bx = mcus_x * c.h_sampling
|
|
159
220
|
by = mcus_y * c.v_sampling
|
|
160
221
|
coeffs[c.id] = Array.new(bx * by * 64, 0)
|
|
161
|
-
|
|
222
|
+
actual_bx = (width * c.h_sampling + mcu_px_w - 1) / mcu_px_w
|
|
223
|
+
actual_by = (height * c.v_sampling + mcu_px_h - 1) / mcu_px_h
|
|
224
|
+
# Interleaved scans include MCU padding; single-component scans do not.
|
|
225
|
+
# Keep the storage stride separate from the actual scan dimensions.
|
|
226
|
+
comp_blocks[c.id] = [bx, by, actual_bx, actual_by]
|
|
162
227
|
end
|
|
163
228
|
|
|
164
|
-
restart_interval = jfif.restart_interval
|
|
165
|
-
|
|
166
229
|
jfif.scans.each do |scan|
|
|
230
|
+
restart_interval = scan.restart_interval
|
|
167
231
|
# Build Huffman tables from this scan's snapshot (tables change between scans)
|
|
168
232
|
dc_tables = {}
|
|
169
233
|
ac_tables = {}
|
|
@@ -189,9 +253,10 @@ module PureJPEG
|
|
|
189
253
|
prog_scan_interleaved(reader, scan, comp_info, dc_tables, ac_tables,
|
|
190
254
|
coeffs, comp_blocks, mcus_x, mcus_y, restart_interval, ss, se, ah, al)
|
|
191
255
|
end
|
|
256
|
+
reader.finish_scan!
|
|
192
257
|
end
|
|
193
258
|
|
|
194
|
-
# Reconstruct: unzigzag
|
|
259
|
+
# Reconstruct: unzigzag/dequantize, IDCT, write to channel buffers
|
|
195
260
|
padded_w = mcus_x * mcu_px_w
|
|
196
261
|
padded_h = mcus_y * mcu_px_h
|
|
197
262
|
channels = {}
|
|
@@ -201,8 +266,6 @@ module PureJPEG
|
|
|
201
266
|
channels[c.id] = { data: Array.new(ch_w * ch_h, 0), width: ch_w, height: ch_h }
|
|
202
267
|
end
|
|
203
268
|
|
|
204
|
-
zigzag = Array.new(64, 0)
|
|
205
|
-
raster = Array.new(64, 0)
|
|
206
269
|
dequant = Array.new(64, 0)
|
|
207
270
|
|
|
208
271
|
jfif.components.each do |c|
|
|
@@ -214,10 +277,7 @@ module PureJPEG
|
|
|
214
277
|
by_count.times do |block_y|
|
|
215
278
|
bx_count.times do |block_x|
|
|
216
279
|
offset = (block_y * bx_count + block_x) * 64
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
Zigzag.unreorder!(zigzag, raster)
|
|
220
|
-
Quantization.dequantize!(raster, qt, dequant)
|
|
280
|
+
Quantization.dequantize_zigzag!(coeff_buf, qt, dequant, offset)
|
|
221
281
|
DCT.inverse!(dequant)
|
|
222
282
|
write_block(dequant, ch[:data], ch[:width], block_x * 8, block_y * 8)
|
|
223
283
|
end
|
|
@@ -237,18 +297,20 @@ module PureJPEG
|
|
|
237
297
|
def prog_scan_non_interleaved(reader, scan, comp_info, dc_tables, ac_tables,
|
|
238
298
|
coeffs, comp_blocks, restart_interval, ss, se, ah, al)
|
|
239
299
|
sc = scan.components[0]
|
|
240
|
-
comp, dc_tab, ac_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables,
|
|
300
|
+
comp, dc_tab, ac_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables,
|
|
301
|
+
require_dc: ss == 0 && ah == 0, require_ac: ss > 0)
|
|
241
302
|
coeff_buf = coeffs[comp.id]
|
|
242
|
-
bx_count,
|
|
303
|
+
bx_count, _by_count, actual_bx, actual_by = comp_blocks[comp.id]
|
|
243
304
|
|
|
244
305
|
prev_dc = 0
|
|
245
306
|
eobrun = 0
|
|
246
307
|
mcu_count = 0
|
|
247
308
|
|
|
248
|
-
|
|
249
|
-
|
|
309
|
+
actual_by.times do |block_y|
|
|
310
|
+
actual_bx.times do |block_x|
|
|
250
311
|
if restart_interval > 0 && mcu_count > 0 && (mcu_count % restart_interval) == 0
|
|
251
|
-
|
|
312
|
+
raise DecodeError, "Progressive EOB run crosses restart boundary" if eobrun > 0
|
|
313
|
+
reader.read_restart((mcu_count / restart_interval - 1) % 8)
|
|
252
314
|
prev_dc = 0
|
|
253
315
|
eobrun = 0
|
|
254
316
|
end
|
|
@@ -272,6 +334,7 @@ module PureJPEG
|
|
|
272
334
|
mcu_count += 1
|
|
273
335
|
end
|
|
274
336
|
end
|
|
337
|
+
raise DecodeError, "Progressive EOB run exceeds scan" if eobrun > 0
|
|
275
338
|
end
|
|
276
339
|
|
|
277
340
|
def prog_scan_interleaved(reader, scan, comp_info, dc_tables, ac_tables,
|
|
@@ -279,18 +342,20 @@ module PureJPEG
|
|
|
279
342
|
prev_dc = Hash.new(0)
|
|
280
343
|
mcu_count = 0
|
|
281
344
|
|
|
345
|
+
scan_components = scan.components.map do |sc|
|
|
346
|
+
comp, dc_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables,
|
|
347
|
+
require_dc: ah == 0, require_ac: false)
|
|
348
|
+
[sc, comp, dc_tab, coeffs[comp.id], comp_blocks[comp.id][0]]
|
|
349
|
+
end
|
|
350
|
+
|
|
282
351
|
mcus_y.times do |mcu_row|
|
|
283
352
|
mcus_x.times do |mcu_col|
|
|
284
353
|
if restart_interval > 0 && mcu_count > 0 && (mcu_count % restart_interval) == 0
|
|
285
|
-
reader.
|
|
354
|
+
reader.read_restart((mcu_count / restart_interval - 1) % 8)
|
|
286
355
|
prev_dc.clear
|
|
287
356
|
end
|
|
288
357
|
|
|
289
|
-
|
|
290
|
-
comp, dc_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables, require_ac: false)
|
|
291
|
-
coeff_buf = coeffs[comp.id]
|
|
292
|
-
bx_count = comp_blocks[comp.id][0]
|
|
293
|
-
|
|
358
|
+
scan_components.each do |sc, comp, dc_tab, coeff_buf, bx_count|
|
|
294
359
|
comp.v_sampling.times do |bv|
|
|
295
360
|
comp.h_sampling.times do |bh|
|
|
296
361
|
block_x = mcu_col * comp.h_sampling + bh
|
|
@@ -312,7 +377,7 @@ module PureJPEG
|
|
|
312
377
|
end
|
|
313
378
|
|
|
314
379
|
def prog_dc_first(reader, dc_tab, prev_dc, coeff_buf, offset, al)
|
|
315
|
-
cat = dc_tab.
|
|
380
|
+
cat = dc_tab.decode_symbol(reader)
|
|
316
381
|
diff = reader.receive_extend(cat)
|
|
317
382
|
dc_val = prev_dc + diff
|
|
318
383
|
coeff_buf[offset] = dc_val << al
|
|
@@ -328,13 +393,14 @@ module PureJPEG
|
|
|
328
393
|
|
|
329
394
|
k = ss
|
|
330
395
|
while k <= se
|
|
331
|
-
symbol = ac_tab.
|
|
396
|
+
symbol = ac_tab.decode_symbol(reader)
|
|
332
397
|
run = (symbol >> 4) & 0x0F
|
|
333
398
|
size = symbol & 0x0F
|
|
334
399
|
|
|
335
400
|
if size == 0
|
|
336
401
|
if run == 15
|
|
337
402
|
k += 16
|
|
403
|
+
raise DecodeError, "AC run exceeds spectral band" if k > se + 1
|
|
338
404
|
else
|
|
339
405
|
# EOBn
|
|
340
406
|
eobrun = (1 << run)
|
|
@@ -343,6 +409,7 @@ module PureJPEG
|
|
|
343
409
|
end
|
|
344
410
|
else
|
|
345
411
|
k += run
|
|
412
|
+
raise DecodeError, "AC run exceeds spectral band" if k > se
|
|
346
413
|
coeff_buf[offset + k] = reader.receive_extend(size) << al
|
|
347
414
|
k += 1
|
|
348
415
|
end
|
|
@@ -364,7 +431,7 @@ module PureJPEG
|
|
|
364
431
|
|
|
365
432
|
k = ss
|
|
366
433
|
while k <= se
|
|
367
|
-
symbol = ac_tab.
|
|
434
|
+
symbol = ac_tab.decode_symbol(reader)
|
|
368
435
|
r = (symbol >> 4) & 0x0F
|
|
369
436
|
s = symbol & 0x0F
|
|
370
437
|
|
|
@@ -372,6 +439,7 @@ module PureJPEG
|
|
|
372
439
|
# (the value bits come before refinement bits in the bitstream)
|
|
373
440
|
new_value = nil
|
|
374
441
|
if s != 0
|
|
442
|
+
raise DecodeError, "Invalid progressive AC refinement symbol" unless s == 1
|
|
375
443
|
new_value = reader.receive_extend(s) << al
|
|
376
444
|
elsif r != 15
|
|
377
445
|
# EOBn: refine remaining nonzero coefficients in this block
|
|
@@ -398,7 +466,8 @@ module PureJPEG
|
|
|
398
466
|
end
|
|
399
467
|
|
|
400
468
|
# Place new coefficient at the target zero position
|
|
401
|
-
|
|
469
|
+
raise DecodeError, "AC refinement run exceeds spectral band" if k > se
|
|
470
|
+
if new_value
|
|
402
471
|
coeff_buf[offset + k] = new_value
|
|
403
472
|
end
|
|
404
473
|
k += 1
|
|
@@ -417,7 +486,7 @@ module PureJPEG
|
|
|
417
486
|
|
|
418
487
|
def decode_block(reader, dc_tab, ac_tab, prev_dc, comp_id, out)
|
|
419
488
|
# DC coefficient
|
|
420
|
-
dc_cat = dc_tab.
|
|
489
|
+
dc_cat = dc_tab.decode_symbol(reader)
|
|
421
490
|
dc_diff = reader.receive_extend(dc_cat)
|
|
422
491
|
dc_val = prev_dc[comp_id] + dc_diff
|
|
423
492
|
prev_dc[comp_id] = dc_val
|
|
@@ -426,7 +495,7 @@ module PureJPEG
|
|
|
426
495
|
# AC coefficients
|
|
427
496
|
i = 1
|
|
428
497
|
while i < 64
|
|
429
|
-
symbol = ac_tab.
|
|
498
|
+
symbol = ac_tab.decode_symbol(reader)
|
|
430
499
|
if symbol == 0x00 # EOB
|
|
431
500
|
while i < 64
|
|
432
501
|
out[i] = 0
|
|
@@ -434,6 +503,7 @@ module PureJPEG
|
|
|
434
503
|
end
|
|
435
504
|
break
|
|
436
505
|
elsif symbol == 0xF0 # ZRL (16 zeros)
|
|
506
|
+
raise DecodeError, "AC run exceeds block" if i + 16 > 64
|
|
437
507
|
16.times do
|
|
438
508
|
out[i] = 0
|
|
439
509
|
i += 1
|
|
@@ -441,6 +511,8 @@ module PureJPEG
|
|
|
441
511
|
else
|
|
442
512
|
run = (symbol >> 4) & 0x0F
|
|
443
513
|
size = symbol & 0x0F
|
|
514
|
+
raise DecodeError, "Invalid baseline AC symbol" if size == 0
|
|
515
|
+
raise DecodeError, "AC run exceeds block" if i + run >= 64
|
|
444
516
|
run.times do
|
|
445
517
|
out[i] = 0
|
|
446
518
|
i += 1
|
|
@@ -469,12 +541,14 @@ module PureJPEG
|
|
|
469
541
|
end
|
|
470
542
|
end
|
|
471
543
|
|
|
472
|
-
def resolve_scan_references!(scan_component, comp_info, dc_tables, ac_tables, require_ac: true)
|
|
544
|
+
def resolve_scan_references!(scan_component, comp_info, dc_tables, ac_tables, require_dc: true, require_ac: true)
|
|
473
545
|
comp = comp_info[scan_component.id]
|
|
474
546
|
raise DecodeError, "Scan references unknown component id #{scan_component.id}" unless comp
|
|
475
547
|
|
|
476
548
|
dc_tab = dc_tables[scan_component.dc_table_id]
|
|
477
|
-
|
|
549
|
+
if require_dc && !dc_tab
|
|
550
|
+
raise DecodeError, "Component #{scan_component.id} references missing DC Huffman table #{scan_component.dc_table_id}"
|
|
551
|
+
end
|
|
478
552
|
|
|
479
553
|
if require_ac
|
|
480
554
|
ac_tab = ac_tables[scan_component.ac_table_id]
|
|
@@ -485,7 +559,7 @@ module PureJPEG
|
|
|
485
559
|
end
|
|
486
560
|
|
|
487
561
|
def fetch_quant_table!(jfif, comp)
|
|
488
|
-
qt = jfif.
|
|
562
|
+
qt = jfif.component_quant_tables[comp.id]
|
|
489
563
|
raise DecodeError, "Component #{comp.id} references missing quantization table #{comp.qt_id}" unless qt
|
|
490
564
|
|
|
491
565
|
qt
|
|
@@ -526,6 +600,8 @@ module PureJPEG
|
|
|
526
600
|
cb_data = cb_ch[:data]
|
|
527
601
|
cr_data = cr_ch[:data]
|
|
528
602
|
y_stride = y_ch[:width]
|
|
603
|
+
y_h = y_comp.h_sampling
|
|
604
|
+
y_v = y_comp.v_sampling
|
|
529
605
|
cb_stride = cb_ch[:width]
|
|
530
606
|
cr_stride = cr_ch[:width]
|
|
531
607
|
cb_h = cb_comp.h_sampling
|
|
@@ -537,14 +613,14 @@ module PureJPEG
|
|
|
537
613
|
|
|
538
614
|
height.times do |py|
|
|
539
615
|
dst_row = py * width
|
|
540
|
-
y_row = py * y_stride
|
|
616
|
+
y_row = ((py * y_v) / max_v) * y_stride
|
|
541
617
|
|
|
542
618
|
# Chroma coordinates (nearest-neighbor upsampling)
|
|
543
619
|
cb_row = ((py * cb_v) / max_v) * cb_stride
|
|
544
620
|
cr_row = ((py * cr_v) / max_v) * cr_stride
|
|
545
621
|
|
|
546
622
|
width.times do |px|
|
|
547
|
-
lum = y_data[y_row + px]
|
|
623
|
+
lum = y_data[y_row + (px * y_h) / max_h]
|
|
548
624
|
|
|
549
625
|
cb_x = (px * cb_h) / max_h
|
|
550
626
|
cr_x = (px * cr_h) / max_h
|
data/lib/pure_jpeg/encoder.rb
CHANGED
|
@@ -146,11 +146,9 @@ module PureJPEG
|
|
|
146
146
|
lum_dc_bits, lum_dc_values, lum_ac_bits, lum_ac_values)
|
|
147
147
|
else
|
|
148
148
|
chr_qtable = build_chr_qtable
|
|
149
|
-
y_data,
|
|
149
|
+
y_data, cb_sub, cr_sub = extract_ycbcr420(width, height)
|
|
150
150
|
sub_w = (width + 1) / 2
|
|
151
151
|
sub_h = (height + 1) / 2
|
|
152
|
-
cb_sub = downsample(cb_data, width, height, sub_w, sub_h)
|
|
153
|
-
cr_sub = downsample(cr_data, width, height, sub_w, sub_h)
|
|
154
152
|
|
|
155
153
|
lum_dc_bits, lum_dc_values, lum_ac_bits, lum_ac_values,
|
|
156
154
|
chr_dc_bits, chr_dc_values, chr_ac_bits, chr_ac_values =
|
|
@@ -215,13 +213,12 @@ module PureJPEG
|
|
|
215
213
|
padded_h = (height + 7) & ~7
|
|
216
214
|
|
|
217
215
|
block = Array.new(64, 0)
|
|
218
|
-
qbuf = Array.new(64, 0)
|
|
219
216
|
zbuf = Array.new(64, 0)
|
|
220
217
|
|
|
221
218
|
(0...padded_h).step(8) do |by|
|
|
222
219
|
(0...padded_w).step(8) do |bx|
|
|
223
220
|
extract_block_into(y_data, width, height, bx, by, block)
|
|
224
|
-
yield transform_block(block,
|
|
221
|
+
yield transform_block(block, zbuf, qtable)
|
|
225
222
|
end
|
|
226
223
|
end
|
|
227
224
|
end
|
|
@@ -285,28 +282,27 @@ module PureJPEG
|
|
|
285
282
|
mcu_h = (height + 15) & ~15
|
|
286
283
|
|
|
287
284
|
block = Array.new(64, 0)
|
|
288
|
-
qbuf = Array.new(64, 0)
|
|
289
285
|
zbuf = Array.new(64, 0)
|
|
290
286
|
|
|
291
287
|
(0...mcu_h).step(16) do |my|
|
|
292
288
|
(0...mcu_w).step(16) do |mx|
|
|
293
289
|
extract_block_into(y_data, width, height, mx, my, block)
|
|
294
|
-
yield :y, transform_block(block,
|
|
290
|
+
yield :y, transform_block(block, zbuf, lum_qt)
|
|
295
291
|
|
|
296
292
|
extract_block_into(y_data, width, height, mx + 8, my, block)
|
|
297
|
-
yield :y, transform_block(block,
|
|
293
|
+
yield :y, transform_block(block, zbuf, lum_qt)
|
|
298
294
|
|
|
299
295
|
extract_block_into(y_data, width, height, mx, my + 8, block)
|
|
300
|
-
yield :y, transform_block(block,
|
|
296
|
+
yield :y, transform_block(block, zbuf, lum_qt)
|
|
301
297
|
|
|
302
298
|
extract_block_into(y_data, width, height, mx + 8, my + 8, block)
|
|
303
|
-
yield :y, transform_block(block,
|
|
299
|
+
yield :y, transform_block(block, zbuf, lum_qt)
|
|
304
300
|
|
|
305
301
|
extract_block_into(cb_sub, sub_w, sub_h, mx >> 1, my >> 1, block)
|
|
306
|
-
yield :cb, transform_block(block,
|
|
302
|
+
yield :cb, transform_block(block, zbuf, chr_qt)
|
|
307
303
|
|
|
308
304
|
extract_block_into(cr_sub, sub_w, sub_h, mx >> 1, my >> 1, block)
|
|
309
|
-
yield :cr, transform_block(block,
|
|
305
|
+
yield :cr, transform_block(block, zbuf, chr_qt)
|
|
310
306
|
end
|
|
311
307
|
end
|
|
312
308
|
end
|
|
@@ -331,10 +327,9 @@ module PureJPEG
|
|
|
331
327
|
|
|
332
328
|
# --- Shared block pipeline (all buffers pre-allocated) ---
|
|
333
329
|
|
|
334
|
-
def transform_block(block,
|
|
330
|
+
def transform_block(block, zbuf, qtable)
|
|
335
331
|
DCT.forward!(block)
|
|
336
|
-
Quantization.
|
|
337
|
-
Zigzag.reorder!(qbuf, zbuf)
|
|
332
|
+
Quantization.quantize_zigzag!(block, qtable, zbuf)
|
|
338
333
|
zbuf
|
|
339
334
|
end
|
|
340
335
|
|
|
@@ -392,61 +387,65 @@ module PureJPEG
|
|
|
392
387
|
luminance
|
|
393
388
|
end
|
|
394
389
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
390
|
+
# Convert RGB in raster order, downsampling chroma after every two rows.
|
|
391
|
+
# Keeping only two full-resolution chroma rows avoids two image-sized arrays.
|
|
392
|
+
def extract_ycbcr420(width, height)
|
|
393
|
+
y_data = Array.new(width * height)
|
|
394
|
+
sub_w = (width + 1) / 2
|
|
395
|
+
sub_h = (height + 1) / 2
|
|
396
|
+
cb_sub = Array.new(sub_w * sub_h)
|
|
397
|
+
cr_sub = Array.new(sub_w * sub_h)
|
|
398
|
+
cb_rows = Array.new(width * 2)
|
|
399
|
+
cr_rows = Array.new(width * 2)
|
|
400
400
|
|
|
401
401
|
if source.respond_to?(:packed_pixels)
|
|
402
402
|
packed = source.packed_pixels
|
|
403
403
|
r_shift, g_shift, b_shift = packed_shifts
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
height.times do |py|
|
|
407
|
+
row = py * width
|
|
408
|
+
chroma_row = (py & 1) * width
|
|
409
|
+
if packed
|
|
410
|
+
width.times do |px|
|
|
411
|
+
color = packed[row + px]
|
|
412
|
+
r = (color >> r_shift) & 0xFF
|
|
413
|
+
g = (color >> g_shift) & 0xFF
|
|
414
|
+
b = (color >> b_shift) & 0xFF
|
|
415
|
+
y_data[row + px] = clamp255((FP_Y_R * r + FP_Y_G * g + FP_Y_B * b + FP_HALF) >> 16)
|
|
416
|
+
cb_rows[chroma_row + px] = clamp255((FP_CB_R * r + FP_CB_G * g + FP_CB_B * b + FP_128 + FP_HALF) >> 16)
|
|
417
|
+
cr_rows[chroma_row + px] = clamp255((FP_CR_R * r + FP_CR_G * g + FP_CR_B * b + FP_128 + FP_HALF) >> 16)
|
|
418
|
+
end
|
|
419
|
+
else
|
|
418
420
|
width.times do |px|
|
|
419
421
|
pixel = source[px, py]
|
|
420
422
|
r = pixel.r; g = pixel.g; b = pixel.b
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
cr_data[i] = clamp255((FP_CR_R * r + FP_CR_G * g + FP_CR_B * b + FP_128 + FP_HALF) >> 16)
|
|
423
|
+
y_data[row + px] = clamp255((FP_Y_R * r + FP_Y_G * g + FP_Y_B * b + FP_HALF) >> 16)
|
|
424
|
+
cb_rows[chroma_row + px] = clamp255((FP_CB_R * r + FP_CB_G * g + FP_CB_B * b + FP_128 + FP_HALF) >> 16)
|
|
425
|
+
cr_rows[chroma_row + px] = clamp255((FP_CR_R * r + FP_CR_G * g + FP_CR_B * b + FP_128 + FP_HALF) >> 16)
|
|
425
426
|
end
|
|
426
427
|
end
|
|
428
|
+
|
|
429
|
+
if py.odd? || py == height - 1
|
|
430
|
+
# For odd image heights, use row zero twice at the bottom edge.
|
|
431
|
+
downsample_chroma_rows(cb_rows, cr_rows, width, chroma_row,
|
|
432
|
+
cb_sub, cr_sub, sub_w, (py >> 1) * sub_w)
|
|
433
|
+
end
|
|
427
434
|
end
|
|
428
435
|
|
|
429
|
-
[y_data,
|
|
436
|
+
[y_data, cb_sub, cr_sub]
|
|
430
437
|
end
|
|
431
438
|
|
|
432
|
-
def
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
dst_row = dy * dst_w
|
|
442
|
-
dst_w.times do |dx|
|
|
443
|
-
sx = dx << 1
|
|
444
|
-
x1 = sx < max_x ? sx + 1 : max_x
|
|
445
|
-
out[dst_row + dx] = ((data[row0 + sx] + data[row0 + x1] +
|
|
446
|
-
data[row1 + sx] + data[row1 + x1]) >> 2)
|
|
447
|
-
end
|
|
439
|
+
def downsample_chroma_rows(cb_rows, cr_rows, width, row1, cb_sub, cr_sub, sub_w, dst_row)
|
|
440
|
+
max_x = width - 1
|
|
441
|
+
sub_w.times do |dx|
|
|
442
|
+
sx = dx << 1
|
|
443
|
+
x1 = sx < max_x ? sx + 1 : max_x
|
|
444
|
+
cb_sub[dst_row + dx] = (cb_rows[sx] + cb_rows[x1] +
|
|
445
|
+
cb_rows[row1 + sx] + cb_rows[row1 + x1]) >> 2
|
|
446
|
+
cr_sub[dst_row + dx] = (cr_rows[sx] + cr_rows[x1] +
|
|
447
|
+
cr_rows[row1 + sx] + cr_rows[row1 + x1]) >> 2
|
|
448
448
|
end
|
|
449
|
-
out
|
|
450
449
|
end
|
|
451
450
|
|
|
452
451
|
# Extract an 8x8 block into a pre-allocated array, level-shifted by -128.
|