pure_jpeg 0.3.3 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c252d678f3b8c916a2430b802569dc5099a19636359e9864637ed9fe01c90a3
4
- data.tar.gz: 8feec524d8c41ca74be1e9bc47cf41b762cb14dc3500dc03c9240680d5877933
3
+ metadata.gz: 81f59b74746cbb7c575aa22e250667b8b126a542613833f2e45b57685fed02c0
4
+ data.tar.gz: 87fb8c2b6dd5035d93c4326a37fac8e43ade2af88f92a3dd1f7761960f675126
5
5
  SHA512:
6
- metadata.gz: c8d26b4d4bed6da87f048036b06bc80eddd6c7ff8318032fb9cfccc15f5aea961e62fefe9965dde33c3d33cea5b611b4c2f6cb2397efa2f43effd8eb456fcf93
7
- data.tar.gz: 63a9fe6bdf1136c8ea6c8e38a6b6641e6be2975fef4fea9394f0b48de22b01862996a87557b5c0bf7d7915020630f08c9285ebf8b8e8482f38f228c31b2d08e7
6
+ metadata.gz: 930f8db2b6ab86080c115e77f56e0918853e455dbcd18129daa5aaca77715df198a502010ae218275799e8b364f115232e11139ffcb42ca981c57478ef12b45b
7
+ data.tar.gz: 54c50b3a2316c6fa0b9db38fcabdd589bcc17d2edfeff653a28ec4054a72c2b5615eef5fd9f123fb31ce56448864495f49942bf63c4536d00c21fd4e7a6eb4f2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ Performance:
6
+
7
+ - Faster Huffman coding and decoding, with scan references resolved once per scan
8
+ - Combined quantization and zigzag passes, including direct reconstruction from progressive coefficient buffers
9
+ - Color encoding downsamples chroma two rows at a time instead of allocating full-resolution chroma images
10
+ - Block-based `RawSource` construction avoids allocating pixels that would immediately be replaced
11
+ - Measured encode/decode times reduced by roughly 12–28% on Ruby 4.0.2 with YJIT; baseline decode allocations fell from 76,994 to 114 on the bundled 1024x1024 benchmark
12
+ - Encoding array storage allocated for that image fell from 29.4 MB to 12.6 MB, with encoded bytes and decoded pixels preserved in equivalence checks
13
+
14
+ Compatibility and testing:
15
+
16
+ - Experimental Spinel compatibility, tested at revision `7f08abd9`, with benchmarks and a `rake test:spinel` proof-of-concept runner
17
+ - Spinel currently passes seven of the eight selected RawSource tests; the remaining test fails because `respond_to?` is unavailable on its pixel struct. See `test/SPINEL.md` for details
18
+ - Added regression tests for quantization rounding, progressive coefficient offsets, and chroma downsampling edges
19
+ - Updated performance comparisons for CRuby, TruffleRuby, JRuby, and Spinel
20
+
3
21
  ## 0.3.3
4
22
 
5
23
  New features:
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Convert PNG or other pixel data to JPEG. Or the other way! Implements baseline JPEG encoding (DCT, Huffman, 4:2:0 chroma subsampling) and decodes both baseline and progressive JPEGs. Exposes a variety of encoding options to adjust parts of the JPEG pipeline not normally available (I needed this to recreate the JPEG compression styles of older digital cameras - don't ask..)
8
8
 
9
- It works on CRuby 3.0+, TruffleRuby 33.0, and JRuby 10.0. There's *almost* 100% test coverage - I need to find some "broken" JPEGs to do the rest (hit me up if you have any sources..)
9
+ It works on CRuby 3.0+, TruffleRuby 33.0, JRuby 10.0, and [Spinel](https://github.com/matz/spinel) (tested at `7f08abd9`). There's *almost* 100% test coverage - I need to find some "broken" JPEGs to do the rest (hit me up if you have any sources..)
10
10
 
11
11
  > [!NOTE]
12
12
  > Rubyists might find the [AI Disclosure](#ai-disclosure) section below of interest.
@@ -205,15 +205,17 @@ Possible future improvements: ICC profile rendering/conversion.
205
205
 
206
206
  ## Performance
207
207
 
208
- On a 1024x1024 image (Apple M5, 5 runs after warmup):
208
+ On the bundled 1024x1024 images (Apple M5, median of 15 runs after 20 seconds of warmup per operation):
209
209
 
210
- | Operation | CRuby 4.0.2 (YJIT) | TruffleRuby 33.0.1 |
211
- |-----------|---------------------|---------------------|
212
- | Encode (color, q85) | ~0.16s | ~0.08s |
213
- | Decode (baseline) | ~0.14s | ~0.05s |
214
- | Decode (progressive) | ~0.18s | ~0.09s |
210
+ | Operation | CRuby 4.0.2 (YJIT) | TruffleRuby 34.0.1 | JRuby 9.4.14.0 | Spinel (`7f08abd9`) |
211
+ |-----------|-------------------|-------------------|---------------|--------------------|
212
+ | Encode (color, q85) | ~0.131s | ~0.088s | ~0.577s | ~0.104s |
213
+ | Decode (baseline) | ~0.112s | ~0.027s | ~0.554s | ~0.078s |
214
+ | Decode (progressive) | ~0.140s | ~0.037s | ~0.617s | ~0.092s |
215
215
 
216
- The encoder and decoder use an integer-scaled AAN (Arai-Agui-Nakajima) DCT with fixed-point arithmetic throughout — no Float operations in the hot path. Color space conversion uses fixed-point integer math, and pixel data is stored as packed integers to avoid per-pixel object allocation. TruffleRuby's Graal JIT compiler can optimize these tight integer loops particularly well, resulting in 2-3x faster performance once warmed up.
216
+ Inputs were loaded before timing, and encoded output was returned in memory; file I/O and PNG loading are excluded. Runtimes were benchmarked sequentially. JRuby used its default JIT on OpenJDK 21.0.10. Spinel was measured separately with the same inputs and timing protocol, using a portable harness with PNG pixels preloaded; its encoded JPEG and decoded pixels matched CRuby byte-for-byte. Results vary with image content, garbage collection, and JIT warmup.
217
+
218
+ The encoder and decoder use an integer-scaled AAN (Arai-Agui-Nakajima) DCT with fixed-point arithmetic throughout — no Float operations in the hot path. Color space conversion uses fixed-point integer math, and pixel data is stored as packed integers to avoid per-pixel object allocation. TruffleRuby's Graal JIT compiler can optimize these tight integer loops particularly well once warmed up.
217
219
 
218
220
  ## Example scripts
219
221
 
@@ -78,9 +78,13 @@ module PureJPEG
78
78
 
79
79
  # Reusable buffers
80
80
  zigzag = Array.new(64, 0)
81
- raster = Array.new(64, 0)
82
81
  dequant = Array.new(64, 0)
83
82
 
83
+ scan_components = jfif.scan_components.map do |sc|
84
+ comp, dc_tab, ac_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables)
85
+ [sc, comp, dc_tab, ac_tab, fetch_quant_table!(jfif, comp), channels[comp.id]]
86
+ end
87
+
84
88
  mcus_y.times do |mcu_row|
85
89
  mcus_x.times do |mcu_col|
86
90
  # Handle restart interval
@@ -89,19 +93,14 @@ module PureJPEG
89
93
  prev_dc.clear
90
94
  end
91
95
 
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]
96
-
96
+ scan_components.each do |sc, comp, dc_tab, ac_tab, qt, ch|
97
97
  comp.v_sampling.times do |bv|
98
98
  comp.h_sampling.times do |bh|
99
99
  # Decode one 8x8 block
100
100
  decode_block(reader, dc_tab, ac_tab, prev_dc, sc.id, zigzag)
101
101
 
102
102
  # Inverse pipeline: unzigzag -> dequantize -> IDCT -> level shift
103
- Zigzag.unreorder!(zigzag, raster)
104
- Quantization.dequantize!(raster, qt, dequant)
103
+ Quantization.dequantize_zigzag!(zigzag, qt, dequant)
105
104
  DCT.inverse!(dequant)
106
105
 
107
106
  # Write block into channel buffer
@@ -191,7 +190,7 @@ module PureJPEG
191
190
  end
192
191
  end
193
192
 
194
- # Reconstruct: unzigzag, dequantize, IDCT, write to channel buffers
193
+ # Reconstruct: unzigzag/dequantize, IDCT, write to channel buffers
195
194
  padded_w = mcus_x * mcu_px_w
196
195
  padded_h = mcus_y * mcu_px_h
197
196
  channels = {}
@@ -201,8 +200,6 @@ module PureJPEG
201
200
  channels[c.id] = { data: Array.new(ch_w * ch_h, 0), width: ch_w, height: ch_h }
202
201
  end
203
202
 
204
- zigzag = Array.new(64, 0)
205
- raster = Array.new(64, 0)
206
203
  dequant = Array.new(64, 0)
207
204
 
208
205
  jfif.components.each do |c|
@@ -214,10 +211,7 @@ module PureJPEG
214
211
  by_count.times do |block_y|
215
212
  bx_count.times do |block_x|
216
213
  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)
214
+ Quantization.dequantize_zigzag!(coeff_buf, qt, dequant, offset)
221
215
  DCT.inverse!(dequant)
222
216
  write_block(dequant, ch[:data], ch[:width], block_x * 8, block_y * 8)
223
217
  end
@@ -279,6 +273,11 @@ module PureJPEG
279
273
  prev_dc = Hash.new(0)
280
274
  mcu_count = 0
281
275
 
276
+ scan_components = scan.components.map do |sc|
277
+ comp, dc_tab = resolve_scan_references!(sc, comp_info, dc_tables, ac_tables, require_ac: false)
278
+ [sc, comp, dc_tab, coeffs[comp.id], comp_blocks[comp.id][0]]
279
+ end
280
+
282
281
  mcus_y.times do |mcu_row|
283
282
  mcus_x.times do |mcu_col|
284
283
  if restart_interval > 0 && mcu_count > 0 && (mcu_count % restart_interval) == 0
@@ -286,11 +285,7 @@ module PureJPEG
286
285
  prev_dc.clear
287
286
  end
288
287
 
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
-
288
+ scan_components.each do |sc, comp, dc_tab, coeff_buf, bx_count|
294
289
  comp.v_sampling.times do |bv|
295
290
  comp.h_sampling.times do |bh|
296
291
  block_x = mcu_col * comp.h_sampling + bh
@@ -312,7 +307,7 @@ module PureJPEG
312
307
  end
313
308
 
314
309
  def prog_dc_first(reader, dc_tab, prev_dc, coeff_buf, offset, al)
315
- cat = dc_tab.decode(reader)
310
+ cat = dc_tab.decode_symbol(reader)
316
311
  diff = reader.receive_extend(cat)
317
312
  dc_val = prev_dc + diff
318
313
  coeff_buf[offset] = dc_val << al
@@ -328,7 +323,7 @@ module PureJPEG
328
323
 
329
324
  k = ss
330
325
  while k <= se
331
- symbol = ac_tab.decode(reader)
326
+ symbol = ac_tab.decode_symbol(reader)
332
327
  run = (symbol >> 4) & 0x0F
333
328
  size = symbol & 0x0F
334
329
 
@@ -364,7 +359,7 @@ module PureJPEG
364
359
 
365
360
  k = ss
366
361
  while k <= se
367
- symbol = ac_tab.decode(reader)
362
+ symbol = ac_tab.decode_symbol(reader)
368
363
  r = (symbol >> 4) & 0x0F
369
364
  s = symbol & 0x0F
370
365
 
@@ -417,7 +412,7 @@ module PureJPEG
417
412
 
418
413
  def decode_block(reader, dc_tab, ac_tab, prev_dc, comp_id, out)
419
414
  # DC coefficient
420
- dc_cat = dc_tab.decode(reader)
415
+ dc_cat = dc_tab.decode_symbol(reader)
421
416
  dc_diff = reader.receive_extend(dc_cat)
422
417
  dc_val = prev_dc[comp_id] + dc_diff
423
418
  prev_dc[comp_id] = dc_val
@@ -426,7 +421,7 @@ module PureJPEG
426
421
  # AC coefficients
427
422
  i = 1
428
423
  while i < 64
429
- symbol = ac_tab.decode(reader)
424
+ symbol = ac_tab.decode_symbol(reader)
430
425
  if symbol == 0x00 # EOB
431
426
  while i < 64
432
427
  out[i] = 0
@@ -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.
@@ -25,13 +25,15 @@ module PureJPEG
25
25
  end
26
26
 
27
27
  # Decode one Huffman symbol from the bit reader.
28
- def decode(reader)
28
+ def decode_symbol(reader)
29
29
  code = 0
30
- 1.upto(16) do |len|
30
+ len = 1
31
+ while len <= 16
31
32
  code = (code << 1) | reader.read_bit
32
33
  if @max_code[len] >= 0 && code <= @max_code[len]
33
34
  return @values[@val_ptr[len] + code - @min_code[len]]
34
35
  end
36
+ len += 1
35
37
  end
36
38
  raise PureJPEG::DecodeError, "Invalid Huffman code"
37
39
  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
- return 0 if value == 0
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
- self.class.each_ac_item(zigzag) do |symbol, value|
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 = self.class.category(value)
87
+ cat = symbol & 0x0F
94
88
  writer.write_bits(self.class.value_bits(value, cat), cat)
95
89
  end
96
90
  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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PureJPEG
4
- VERSION = "0.3.3"
4
+ VERSION = "0.4.0"
5
5
  end
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.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Cooper