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.
@@ -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 DCT (SOF0) JPEGs with 1 or 3 components, any chroma
7
- # subsampling factor, and restart markers.
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 = (padded_w * c.h_sampling) / max_h
69
- ch_h = (padded_h * c.v_sampling) / max_v
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
- mcus_y.times do |mcu_row|
85
- mcus_x.times do |mcu_col|
86
- # Handle restart interval
87
- if restart_interval > 0 && mcu_count > 0 && (mcu_count % restart_interval) == 0
88
- reader.reset
89
- prev_dc.clear
90
- end
91
-
92
- jfif.scan_components.each do |sc|
93
- comp, dc_tab, ac_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables)
94
- qt = fetch_quant_table!(jfif, comp)
95
- ch = channels[comp.id]
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
- comp.v_sampling.times do |bv|
98
- comp.h_sampling.times do |bh|
99
- # Decode one 8x8 block
100
- decode_block(reader, dc_tab, ac_tab, prev_dc, sc.id, zigzag)
101
-
102
- # Inverse pipeline: unzigzag -> dequantize -> IDCT -> level shift
103
- Zigzag.unreorder!(zigzag, raster)
104
- Quantization.dequantize!(raster, qt, dequant)
105
- DCT.inverse!(dequant)
106
-
107
- # Write block into channel buffer
108
- bx = (mcu_col * comp.h_sampling + bh) * 8
109
- by = (mcu_row * comp.v_sampling + bv) * 8
110
- write_block(dequant, ch[:data], ch[:width], bx, by)
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
- comp_blocks[c.id] = [bx, by]
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, dequantize, IDCT, write to channel buffers
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
- 64.times { |i| zigzag[i] = coeff_buf[offset + i] }
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, require_ac: ss > 0)
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, by_count = comp_blocks[comp.id]
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
- by_count.times do |block_y|
249
- bx_count.times do |block_x|
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
- reader.reset
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.reset
354
+ reader.read_restart((mcu_count / restart_interval - 1) % 8)
286
355
  prev_dc.clear
287
356
  end
288
357
 
289
- scan.components.each do |sc|
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.decode(reader)
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.decode(reader)
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.decode(reader)
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
- if new_value && k <= se
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.decode(reader)
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.decode(reader)
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
- raise DecodeError, "Component #{scan_component.id} references missing DC Huffman table #{scan_component.dc_table_id}" unless dc_tab
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.quant_tables[comp.qt_id]
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
@@ -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, cb_data, cr_data = extract_ycbcr(width, height)
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, qbuf, zbuf, qtable)
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, qbuf, zbuf, lum_qt)
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, qbuf, zbuf, lum_qt)
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, qbuf, zbuf, lum_qt)
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, qbuf, zbuf, lum_qt)
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, qbuf, zbuf, chr_qt)
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, qbuf, zbuf, chr_qt)
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, qbuf, zbuf, qtable)
330
+ def transform_block(block, zbuf, qtable)
335
331
  DCT.forward!(block)
336
- Quantization.quantize!(block, qtable, qbuf)
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
- def extract_ycbcr(width, height)
396
- size = width * height
397
- y_data = Array.new(size)
398
- cb_data = Array.new(size)
399
- cr_data = Array.new(size)
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
- i = 0
405
- size.times do
406
- color = packed[i]
407
- r = (color >> r_shift) & 0xFF
408
- g = (color >> g_shift) & 0xFF
409
- b = (color >> b_shift) & 0xFF
410
- y_data[i] = clamp255((FP_Y_R * r + FP_Y_G * g + FP_Y_B * b + FP_HALF) >> 16)
411
- cb_data[i] = clamp255((FP_CB_R * r + FP_CB_G * g + FP_CB_B * b + FP_128 + FP_HALF) >> 16)
412
- cr_data[i] = clamp255((FP_CR_R * r + FP_CR_G * g + FP_CR_B * b + FP_128 + FP_HALF) >> 16)
413
- i += 1
414
- end
415
- else
416
- height.times do |py|
417
- row = py * width
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
- i = row + px
422
- y_data[i] = clamp255((FP_Y_R * r + FP_Y_G * g + FP_Y_B * b + FP_HALF) >> 16)
423
- cb_data[i] = clamp255((FP_CB_R * r + FP_CB_G * g + FP_CB_B * b + FP_128 + FP_HALF) >> 16)
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, cb_data, cr_data]
436
+ [y_data, cb_sub, cr_sub]
430
437
  end
431
438
 
432
- def downsample(data, src_w, src_h, dst_w, dst_h)
433
- out = Array.new(dst_w * dst_h)
434
- max_x = src_w - 1
435
- max_y = src_h - 1
436
- dst_h.times do |dy|
437
- sy = dy << 1
438
- y1 = sy < max_y ? sy + 1 : max_y
439
- row0 = sy * src_w
440
- row1 = y1 * src_w
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.