pure_jpeg 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +42 -0
- data/LICENSE +1 -1
- data/README.md +73 -16
- data/lib/pure_jpeg/bit_reader.rb +8 -1
- data/lib/pure_jpeg/bit_writer.rb +4 -4
- data/lib/pure_jpeg/decoder.rb +337 -15
- data/lib/pure_jpeg/encoder.rb +217 -68
- data/lib/pure_jpeg/huffman/decoder.rb +1 -1
- data/lib/pure_jpeg/huffman/encoder.rb +73 -45
- data/lib/pure_jpeg/huffman/tables.rb +93 -1
- data/lib/pure_jpeg/image.rb +40 -8
- data/lib/pure_jpeg/info.rb +6 -0
- data/lib/pure_jpeg/jfif_reader.rb +74 -21
- data/lib/pure_jpeg/source/chunky_png_source.rb +8 -5
- data/lib/pure_jpeg/source/raw_source.rb +2 -1
- data/lib/pure_jpeg/version.rb +1 -1
- data/lib/pure_jpeg.rb +30 -0
- metadata +3 -2
data/lib/pure_jpeg/decoder.rb
CHANGED
|
@@ -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.
|
|
16
|
+
data = if path_or_data.is_a?(String) && !path_or_data.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
|
|
@@ -27,6 +27,10 @@ module PureJPEG
|
|
|
27
27
|
|
|
28
28
|
def decode
|
|
29
29
|
jfif = JFIFReader.new(@data)
|
|
30
|
+
@icc_profile = jfif.icc_profile
|
|
31
|
+
validate_dimensions!(jfif.width, jfif.height)
|
|
32
|
+
return decode_progressive(jfif) if jfif.progressive
|
|
33
|
+
|
|
30
34
|
width = jfif.width
|
|
31
35
|
height = jfif.height
|
|
32
36
|
|
|
@@ -88,10 +92,8 @@ module PureJPEG
|
|
|
88
92
|
end
|
|
89
93
|
|
|
90
94
|
jfif.scan_components.each do |sc|
|
|
91
|
-
comp = comp_info
|
|
92
|
-
|
|
93
|
-
ac_tab = ac_tables[sc.ac_table_id]
|
|
94
|
-
qt = jfif.quant_tables[comp.qt_id]
|
|
95
|
+
comp, dc_tab, ac_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables)
|
|
96
|
+
qt = fetch_quant_table!(jfif, comp)
|
|
95
97
|
ch = channels[comp.id]
|
|
96
98
|
|
|
97
99
|
comp.v_sampling.times do |bv|
|
|
@@ -120,13 +122,303 @@ module PureJPEG
|
|
|
120
122
|
num_components = jfif.components.length
|
|
121
123
|
if num_components == 1
|
|
122
124
|
assemble_grayscale(width, height, channels, jfif.components[0])
|
|
123
|
-
|
|
125
|
+
elsif num_components == 3
|
|
124
126
|
assemble_color(width, height, channels, jfif.components, max_h, max_v)
|
|
127
|
+
else
|
|
128
|
+
raise DecodeError, "Unsupported number of components: #{num_components}"
|
|
125
129
|
end
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
private
|
|
129
133
|
|
|
134
|
+
def validate_dimensions!(width, height)
|
|
135
|
+
raise DecodeError, "Invalid image dimensions: #{width}x#{height}" if width <= 0 || height <= 0
|
|
136
|
+
raise DecodeError, "Image too large: #{width}x#{height} (max #{MAX_DIMENSION}x#{MAX_DIMENSION})" if width > MAX_DIMENSION || height > MAX_DIMENSION
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# --- Progressive JPEG decoding ---
|
|
140
|
+
|
|
141
|
+
def decode_progressive(jfif)
|
|
142
|
+
width = jfif.width
|
|
143
|
+
height = jfif.height
|
|
144
|
+
|
|
145
|
+
comp_info = {}
|
|
146
|
+
jfif.components.each { |c| comp_info[c.id] = c }
|
|
147
|
+
|
|
148
|
+
max_h = jfif.components.map(&:h_sampling).max
|
|
149
|
+
max_v = jfif.components.map(&:v_sampling).max
|
|
150
|
+
|
|
151
|
+
mcu_px_w = max_h * 8
|
|
152
|
+
mcu_px_h = max_v * 8
|
|
153
|
+
mcus_x = (width + mcu_px_w - 1) / mcu_px_w
|
|
154
|
+
mcus_y = (height + mcu_px_h - 1) / mcu_px_h
|
|
155
|
+
|
|
156
|
+
# Coefficient buffers per component (zigzag order, pre-dequantization)
|
|
157
|
+
coeffs = {}
|
|
158
|
+
comp_blocks = {}
|
|
159
|
+
jfif.components.each do |c|
|
|
160
|
+
bx = mcus_x * c.h_sampling
|
|
161
|
+
by = mcus_y * c.v_sampling
|
|
162
|
+
coeffs[c.id] = Array.new(bx * by * 64, 0)
|
|
163
|
+
comp_blocks[c.id] = [bx, by]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
restart_interval = jfif.restart_interval
|
|
167
|
+
|
|
168
|
+
jfif.scans.each do |scan|
|
|
169
|
+
# Build Huffman tables from this scan's snapshot (tables change between scans)
|
|
170
|
+
dc_tables = {}
|
|
171
|
+
ac_tables = {}
|
|
172
|
+
scan.huffman_tables.each do |(table_class, table_id), info|
|
|
173
|
+
table = Huffman::DecodeTable.new(info[:bits], info[:values])
|
|
174
|
+
if table_class == 0
|
|
175
|
+
dc_tables[table_id] = table
|
|
176
|
+
else
|
|
177
|
+
ac_tables[table_id] = table
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
reader = BitReader.new(scan.data)
|
|
182
|
+
ss = scan.spectral_start
|
|
183
|
+
se = scan.spectral_end
|
|
184
|
+
ah = scan.successive_high
|
|
185
|
+
al = scan.successive_low
|
|
186
|
+
|
|
187
|
+
if scan.components.length == 1
|
|
188
|
+
prog_scan_non_interleaved(reader, scan, comp_info, dc_tables, ac_tables,
|
|
189
|
+
coeffs, comp_blocks, restart_interval, ss, se, ah, al)
|
|
190
|
+
else
|
|
191
|
+
prog_scan_interleaved(reader, scan, comp_info, dc_tables, ac_tables,
|
|
192
|
+
coeffs, comp_blocks, mcus_x, mcus_y, restart_interval, ss, se, ah, al)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Reconstruct: unzigzag, dequantize, IDCT, write to channel buffers
|
|
197
|
+
padded_w = mcus_x * mcu_px_w
|
|
198
|
+
padded_h = mcus_y * mcu_px_h
|
|
199
|
+
channels = {}
|
|
200
|
+
jfif.components.each do |c|
|
|
201
|
+
ch_w = (padded_w * c.h_sampling) / max_h
|
|
202
|
+
ch_h = (padded_h * c.v_sampling) / max_v
|
|
203
|
+
channels[c.id] = { data: Array.new(ch_w * ch_h, 0), width: ch_w, height: ch_h }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
zigzag = Array.new(64, 0)
|
|
207
|
+
raster = Array.new(64, 0.0)
|
|
208
|
+
dequant = Array.new(64, 0.0)
|
|
209
|
+
temp = Array.new(64, 0.0)
|
|
210
|
+
spatial = Array.new(64, 0.0)
|
|
211
|
+
|
|
212
|
+
jfif.components.each do |c|
|
|
213
|
+
qt = fetch_quant_table!(jfif, c)
|
|
214
|
+
ch = channels[c.id]
|
|
215
|
+
coeff_buf = coeffs[c.id]
|
|
216
|
+
bx_count, by_count = comp_blocks[c.id]
|
|
217
|
+
|
|
218
|
+
by_count.times do |block_y|
|
|
219
|
+
bx_count.times do |block_x|
|
|
220
|
+
offset = (block_y * bx_count + block_x) * 64
|
|
221
|
+
64.times { |i| zigzag[i] = coeff_buf[offset + i] }
|
|
222
|
+
|
|
223
|
+
Zigzag.unreorder!(zigzag, raster)
|
|
224
|
+
Quantization.dequantize!(raster, qt, dequant)
|
|
225
|
+
DCT.inverse!(dequant, temp, spatial)
|
|
226
|
+
write_block(spatial, ch[:data], ch[:width], block_x * 8, block_y * 8)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
num_components = jfif.components.length
|
|
232
|
+
if num_components == 1
|
|
233
|
+
assemble_grayscale(width, height, channels, jfif.components[0])
|
|
234
|
+
elsif num_components == 3
|
|
235
|
+
assemble_color(width, height, channels, jfif.components, max_h, max_v)
|
|
236
|
+
else
|
|
237
|
+
raise DecodeError, "Unsupported number of components: #{num_components}"
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def prog_scan_non_interleaved(reader, scan, comp_info, dc_tables, ac_tables,
|
|
242
|
+
coeffs, comp_blocks, restart_interval, ss, se, ah, al)
|
|
243
|
+
sc = scan.components[0]
|
|
244
|
+
comp, dc_tab, ac_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables, require_ac: ss > 0)
|
|
245
|
+
coeff_buf = coeffs[comp.id]
|
|
246
|
+
bx_count, by_count = comp_blocks[comp.id]
|
|
247
|
+
|
|
248
|
+
prev_dc = 0
|
|
249
|
+
eobrun = 0
|
|
250
|
+
mcu_count = 0
|
|
251
|
+
|
|
252
|
+
by_count.times do |block_y|
|
|
253
|
+
bx_count.times do |block_x|
|
|
254
|
+
if restart_interval > 0 && mcu_count > 0 && (mcu_count % restart_interval) == 0
|
|
255
|
+
reader.reset
|
|
256
|
+
prev_dc = 0
|
|
257
|
+
eobrun = 0
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
offset = (block_y * bx_count + block_x) * 64
|
|
261
|
+
|
|
262
|
+
if ss == 0
|
|
263
|
+
if ah == 0
|
|
264
|
+
prev_dc = prog_dc_first(reader, dc_tab, prev_dc, coeff_buf, offset, al)
|
|
265
|
+
else
|
|
266
|
+
prog_dc_refine(reader, coeff_buf, offset, al)
|
|
267
|
+
end
|
|
268
|
+
else
|
|
269
|
+
if ah == 0
|
|
270
|
+
eobrun = prog_ac_first(reader, ac_tab, coeff_buf, offset, ss, se, al, eobrun)
|
|
271
|
+
else
|
|
272
|
+
eobrun = prog_ac_refine(reader, ac_tab, coeff_buf, offset, ss, se, al, eobrun)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
mcu_count += 1
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def prog_scan_interleaved(reader, scan, comp_info, dc_tables, ac_tables,
|
|
282
|
+
coeffs, comp_blocks, mcus_x, mcus_y, restart_interval, ss, se, ah, al)
|
|
283
|
+
prev_dc = Hash.new(0)
|
|
284
|
+
mcu_count = 0
|
|
285
|
+
|
|
286
|
+
mcus_y.times do |mcu_row|
|
|
287
|
+
mcus_x.times do |mcu_col|
|
|
288
|
+
if restart_interval > 0 && mcu_count > 0 && (mcu_count % restart_interval) == 0
|
|
289
|
+
reader.reset
|
|
290
|
+
prev_dc.clear
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
scan.components.each do |sc|
|
|
294
|
+
comp, dc_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables, require_ac: false)
|
|
295
|
+
coeff_buf = coeffs[comp.id]
|
|
296
|
+
bx_count = comp_blocks[comp.id][0]
|
|
297
|
+
|
|
298
|
+
comp.v_sampling.times do |bv|
|
|
299
|
+
comp.h_sampling.times do |bh|
|
|
300
|
+
block_x = mcu_col * comp.h_sampling + bh
|
|
301
|
+
block_y = mcu_row * comp.v_sampling + bv
|
|
302
|
+
offset = (block_y * bx_count + block_x) * 64
|
|
303
|
+
|
|
304
|
+
if ah == 0
|
|
305
|
+
prev_dc[sc.id] = prog_dc_first(reader, dc_tab, prev_dc[sc.id], coeff_buf, offset, al)
|
|
306
|
+
else
|
|
307
|
+
prog_dc_refine(reader, coeff_buf, offset, al)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
mcu_count += 1
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def prog_dc_first(reader, dc_tab, prev_dc, coeff_buf, offset, al)
|
|
319
|
+
cat = dc_tab.decode(reader)
|
|
320
|
+
diff = reader.receive_extend(cat)
|
|
321
|
+
dc_val = prev_dc + diff
|
|
322
|
+
coeff_buf[offset] = dc_val << al
|
|
323
|
+
dc_val
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def prog_dc_refine(reader, coeff_buf, offset, al)
|
|
327
|
+
coeff_buf[offset] |= (reader.read_bit << al)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def prog_ac_first(reader, ac_tab, coeff_buf, offset, ss, se, al, eobrun)
|
|
331
|
+
return eobrun - 1 if eobrun > 0
|
|
332
|
+
|
|
333
|
+
k = ss
|
|
334
|
+
while k <= se
|
|
335
|
+
symbol = ac_tab.decode(reader)
|
|
336
|
+
run = (symbol >> 4) & 0x0F
|
|
337
|
+
size = symbol & 0x0F
|
|
338
|
+
|
|
339
|
+
if size == 0
|
|
340
|
+
if run == 15
|
|
341
|
+
k += 16
|
|
342
|
+
else
|
|
343
|
+
# EOBn
|
|
344
|
+
eobrun = (1 << run)
|
|
345
|
+
eobrun += reader.read_bits(run) if run > 0
|
|
346
|
+
return eobrun - 1
|
|
347
|
+
end
|
|
348
|
+
else
|
|
349
|
+
k += run
|
|
350
|
+
coeff_buf[offset + k] = reader.receive_extend(size) << al
|
|
351
|
+
k += 1
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
0
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def prog_ac_refine(reader, ac_tab, coeff_buf, offset, ss, se, al, eobrun)
|
|
359
|
+
p1 = 1 << al
|
|
360
|
+
m1 = -(1 << al)
|
|
361
|
+
|
|
362
|
+
if eobrun > 0
|
|
363
|
+
ss.upto(se) do |k|
|
|
364
|
+
prog_refine_bit(reader, coeff_buf, offset + k, p1, m1) if coeff_buf[offset + k] != 0
|
|
365
|
+
end
|
|
366
|
+
return eobrun - 1
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
k = ss
|
|
370
|
+
while k <= se
|
|
371
|
+
symbol = ac_tab.decode(reader)
|
|
372
|
+
r = (symbol >> 4) & 0x0F
|
|
373
|
+
s = symbol & 0x0F
|
|
374
|
+
|
|
375
|
+
# Read the new coefficient value before processing the run
|
|
376
|
+
# (the value bits come before refinement bits in the bitstream)
|
|
377
|
+
new_value = nil
|
|
378
|
+
if s != 0
|
|
379
|
+
new_value = reader.receive_extend(s) << al
|
|
380
|
+
elsif r != 15
|
|
381
|
+
# EOBn: refine remaining nonzero coefficients in this block
|
|
382
|
+
eobrun = (1 << r)
|
|
383
|
+
eobrun += reader.read_bits(r) if r > 0
|
|
384
|
+
while k <= se
|
|
385
|
+
prog_refine_bit(reader, coeff_buf, offset + k, p1, m1) if coeff_buf[offset + k] != 0
|
|
386
|
+
k += 1
|
|
387
|
+
end
|
|
388
|
+
return eobrun - 1
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Advance through the band: refine nonzero coefficients, count zeros for run.
|
|
392
|
+
# Break when we've skipped `r` zeros and found the target zero position.
|
|
393
|
+
while k <= se
|
|
394
|
+
if coeff_buf[offset + k] != 0
|
|
395
|
+
prog_refine_bit(reader, coeff_buf, offset + k, p1, m1)
|
|
396
|
+
elsif r == 0
|
|
397
|
+
break
|
|
398
|
+
else
|
|
399
|
+
r -= 1
|
|
400
|
+
end
|
|
401
|
+
k += 1
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Place new coefficient at the target zero position
|
|
405
|
+
if new_value && k <= se
|
|
406
|
+
coeff_buf[offset + k] = new_value
|
|
407
|
+
end
|
|
408
|
+
k += 1
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
0
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def prog_refine_bit(reader, coeff_buf, idx, p1, m1)
|
|
415
|
+
if reader.read_bit == 1
|
|
416
|
+
coeff_buf[idx] += coeff_buf[idx] > 0 ? p1 : m1
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# --- Baseline decoding helpers ---
|
|
421
|
+
|
|
130
422
|
def decode_block(reader, dc_tab, ac_tab, prev_dc, comp_id, out)
|
|
131
423
|
# DC coefficient
|
|
132
424
|
dc_cat = dc_tab.decode(reader)
|
|
@@ -177,6 +469,28 @@ module PureJPEG
|
|
|
177
469
|
end
|
|
178
470
|
end
|
|
179
471
|
|
|
472
|
+
def resolve_scan_references!(scan_component, comp_info, dc_tables, ac_tables, require_ac: true)
|
|
473
|
+
comp = comp_info[scan_component.id]
|
|
474
|
+
raise DecodeError, "Scan references unknown component id #{scan_component.id}" unless comp
|
|
475
|
+
|
|
476
|
+
dc_tab = dc_tables[scan_component.dc_table_id]
|
|
477
|
+
raise DecodeError, "Component #{scan_component.id} references missing DC Huffman table #{scan_component.dc_table_id}" unless dc_tab
|
|
478
|
+
|
|
479
|
+
if require_ac
|
|
480
|
+
ac_tab = ac_tables[scan_component.ac_table_id]
|
|
481
|
+
raise DecodeError, "Component #{scan_component.id} references missing AC Huffman table #{scan_component.ac_table_id}" unless ac_tab
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
[comp, dc_tab, ac_tab]
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def fetch_quant_table!(jfif, comp)
|
|
488
|
+
qt = jfif.quant_tables[comp.qt_id]
|
|
489
|
+
raise DecodeError, "Component #{comp.id} references missing quantization table #{comp.qt_id}" unless qt
|
|
490
|
+
|
|
491
|
+
qt
|
|
492
|
+
end
|
|
493
|
+
|
|
180
494
|
def assemble_grayscale(width, height, channels, comp)
|
|
181
495
|
ch = channels[comp.id]
|
|
182
496
|
pixels = Array.new(width * height)
|
|
@@ -185,20 +499,19 @@ module PureJPEG
|
|
|
185
499
|
dst_row = y * width
|
|
186
500
|
width.times do |x|
|
|
187
501
|
v = ch[:data][src_row + x]
|
|
188
|
-
pixels[dst_row + x] =
|
|
502
|
+
pixels[dst_row + x] = (v << 16) | (v << 8) | v
|
|
189
503
|
end
|
|
190
504
|
end
|
|
191
|
-
Image.new(width, height, pixels)
|
|
505
|
+
Image.new(width, height, pixels, icc_profile: @icc_profile)
|
|
192
506
|
end
|
|
193
507
|
|
|
194
508
|
def assemble_color(width, height, channels, components, max_h, max_v)
|
|
195
509
|
# Upsample chroma channels if needed and convert YCbCr to RGB
|
|
196
|
-
|
|
197
|
-
cb_ch = channels[components[1].id]
|
|
198
|
-
cr_ch = channels[components[2].id]
|
|
510
|
+
y_comp, cb_comp, cr_comp = resolve_color_components(components)
|
|
199
511
|
|
|
200
|
-
|
|
201
|
-
|
|
512
|
+
y_ch = channels[y_comp.id]
|
|
513
|
+
cb_ch = channels[cb_comp.id]
|
|
514
|
+
cr_ch = channels[cr_comp.id]
|
|
202
515
|
|
|
203
516
|
pixels = Array.new(width * height)
|
|
204
517
|
|
|
@@ -228,11 +541,20 @@ module PureJPEG
|
|
|
228
541
|
g = g < 0 ? 0 : (g > 255 ? 255 : g)
|
|
229
542
|
b = b < 0 ? 0 : (b > 255 ? 255 : b)
|
|
230
543
|
|
|
231
|
-
pixels[dst_row + px] =
|
|
544
|
+
pixels[dst_row + px] = (r << 16) | (g << 8) | b
|
|
232
545
|
end
|
|
233
546
|
end
|
|
234
547
|
|
|
235
|
-
Image.new(width, height, pixels)
|
|
548
|
+
Image.new(width, height, pixels, icc_profile: @icc_profile)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def resolve_color_components(components)
|
|
552
|
+
by_id = components.each_with_object({}) { |comp, memo| memo[comp.id] = comp }
|
|
553
|
+
if by_id[1] && by_id[2] && by_id[3]
|
|
554
|
+
[by_id[1], by_id[2], by_id[3]]
|
|
555
|
+
else
|
|
556
|
+
components
|
|
557
|
+
end
|
|
236
558
|
end
|
|
237
559
|
end
|
|
238
560
|
end
|