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.
@@ -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.include?("\x00") && File.exist?(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[sc.id]
92
- dc_tab = dc_tables[sc.dc_table_id]
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
- else
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] = Source::Pixel.new(v, v, v)
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
- y_ch = channels[components[0].id]
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
- cb_comp = components[1]
201
- cr_comp = components[2]
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] = Source::Pixel.new(r, g, b)
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