pure_jpeg 0.3.2 → 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 +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +74 -12
- data/lib/pure_jpeg/decoder.rb +20 -25
- data/lib/pure_jpeg/encoder.rb +64 -56
- data/lib/pure_jpeg/huffman/decoder.rb +4 -2
- data/lib/pure_jpeg/huffman/encoder.rb +4 -10
- data/lib/pure_jpeg/image.rb +10 -0
- data/lib/pure_jpeg/jfif_reader.rb +38 -12
- data/lib/pure_jpeg/quantization.rb +28 -0
- data/lib/pure_jpeg/source/chunky_png_source.rb +39 -2
- data/lib/pure_jpeg/source/raw_source.rb +3 -2
- data/lib/pure_jpeg/version.rb +1 -1
- data/lib/pure_jpeg.rb +4 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 81f59b74746cbb7c575aa22e250667b8b126a542613833f2e45b57685fed02c0
|
|
4
|
+
data.tar.gz: 87fb8c2b6dd5035d93c4326a37fac8e43ade2af88f92a3dd1f7761960f675126
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 930f8db2b6ab86080c115e77f56e0918853e455dbcd18129daa5aaca77715df198a502010ae218275799e8b364f115232e11139ffcb42ca981c57478ef12b45b
|
|
7
|
+
data.tar.gz: 54c50b3a2316c6fa0b9db38fcabdd589bcc17d2edfeff653a28ec4054a72c2b5615eef5fd9f123fb31ce56448864495f49942bf63c4536d00c21fd4e7a6eb4f2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
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
|
+
|
|
21
|
+
## 0.3.3
|
|
22
|
+
|
|
23
|
+
New features:
|
|
24
|
+
|
|
25
|
+
- `PureJPEG.from_chunky_png` accepts `background: [r, g, b]` to composite transparent PNG pixels before JPEG encoding
|
|
26
|
+
|
|
27
|
+
Fixes:
|
|
28
|
+
|
|
29
|
+
- Invalid `quality` and `chroma_quality` values now raise clear `ArgumentError`s
|
|
30
|
+
- `Image#[]` and `Image#[]=` now raise `IndexError` for out-of-bounds coordinates
|
|
31
|
+
- Hardened JFIF segment length validation for malformed JPEG input
|
|
32
|
+
|
|
3
33
|
## 0.3.2
|
|
4
34
|
|
|
5
35
|
Performance:
|
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,
|
|
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.
|
|
@@ -23,7 +23,7 @@ gem "pure_jpeg"
|
|
|
23
23
|
gem install pure_jpeg
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
There are no runtime dependencies. [ChunkyPNG](https://github.com/wvanbergen/chunky_png) is optional (though quite useful) if you want to use `from_chunky_png`.
|
|
26
|
+
There are no runtime dependencies. [ChunkyPNG](https://github.com/wvanbergen/chunky_png) is optional (though quite useful) if you want to use `from_chunky_png`.
|
|
27
27
|
|
|
28
28
|
`examples/` contains some useful example scripts for basic JPEG to PNG and PNG to JPEG conversion if you want to do some quick tests without writing code.
|
|
29
29
|
|
|
@@ -39,6 +39,13 @@ image = ChunkyPNG::Image.from_file("photo.png")
|
|
|
39
39
|
PureJPEG.from_chunky_png(image, quality: 80).write("photo.jpg")
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
If you want transparent pixels composited against a solid color instead of
|
|
43
|
+
using the PNG's hidden RGB values, pass an RGB background:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
PureJPEG.from_chunky_png(image, background: [255, 255, 255], quality: 80).write("photo.jpg")
|
|
47
|
+
```
|
|
48
|
+
|
|
42
49
|
### From any pixel source
|
|
43
50
|
|
|
44
51
|
PureJPEG accepts any object that responds to `width`, `height`, and `[x, y]` (returning an object with `.r`, `.g`, `.b` in 0-255):
|
|
@@ -99,7 +106,7 @@ Here's a quick example of sort of the "old digital camera" effect I was looking
|
|
|
99
106
|
</tr>
|
|
100
107
|
</table>
|
|
101
108
|
|
|
102
|
-
And here's what happens when you convert a PNG with transparency — JPEG doesn't support alpha, so the hidden RGB data behind transparent pixels bleeds through:
|
|
109
|
+
And here's what happens when you convert a PNG with transparency without a `background:` — JPEG doesn't support alpha, so the hidden RGB data behind transparent pixels bleeds through:
|
|
103
110
|
|
|
104
111
|
<table>
|
|
105
112
|
<tr>
|
|
@@ -112,7 +119,7 @@ And here's what happens when you convert a PNG with transparency — JPEG doesn'
|
|
|
112
119
|
</tr>
|
|
113
120
|
</table>
|
|
114
121
|
|
|
115
|
-
|
|
122
|
+
Pass `background: [255, 255, 255]` to composite transparent pixels over white, or any other `[r, g, b]` color you prefer.
|
|
116
123
|
|
|
117
124
|
Note that each stage of the JPEG pipeline is a separate module, so individual components (DCT, quantization, Huffman coding) can be replaced or extended independently which is kinda my plan here as I made this to play around with effects.
|
|
118
125
|
|
|
@@ -198,25 +205,79 @@ Possible future improvements: ICC profile rendering/conversion.
|
|
|
198
205
|
|
|
199
206
|
## Performance
|
|
200
207
|
|
|
201
|
-
On
|
|
208
|
+
On the bundled 1024x1024 images (Apple M5, median of 15 runs after 20 seconds of warmup per operation):
|
|
209
|
+
|
|
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
|
+
|
|
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.
|
|
219
|
+
|
|
220
|
+
## Example scripts
|
|
221
|
+
|
|
222
|
+
The `examples/` directory contains ready-to-run scripts. All accept JPEG or PNG input (PNG requires the `chunky_png` gem).
|
|
223
|
+
|
|
224
|
+
**`png_to_jpeg.rb`** -- Convert a PNG to JPEG.
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
ruby examples/png_to_jpeg.rb [--grayscale] INPUT.png OUTPUT.jpg [quality]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**`jpeg_to_png.rb`** -- Convert a JPEG to PNG.
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
ruby examples/jpeg_to_png.rb INPUT.jpg OUTPUT.png
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**`kodak.rb`** -- Apply the "scrambled quantization" effect that recreates the gritty look of early digicams like the Casio QV-10. See `CREATIVE.md` for more on this.
|
|
202
237
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
238
|
+
```
|
|
239
|
+
ruby examples/kodak.rb INPUT.(jpg|png) [OUTPUT.jpg] [quality]
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**`lumacrush.rb`** -- Crush luminance while preserving chrominance. Produces a soft, oil-painting quality where detail is blocky but colors remain accurate.
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
ruby examples/lumacrush.rb INPUT.(jpg|png) [OUTPUT.jpg] [luma_quality] [chroma_quality]
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**`chromacrush.rb`** -- The opposite: sharp detail with collapsed, blocky color patches.
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
ruby examples/chromacrush.rb INPUT.(jpg|png) [OUTPUT.jpg] [luma_quality] [chroma_quality]
|
|
252
|
+
```
|
|
208
253
|
|
|
209
|
-
|
|
254
|
+
**`loopy.rb`** -- Re-encode a JPEG through multiple passes to see how artifacts accumulate.
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
ruby examples/loopy.rb INPUT.jpg [quality] [iterations]
|
|
258
|
+
```
|
|
210
259
|
|
|
211
260
|
## Some useful `rake` tasks
|
|
212
261
|
|
|
213
262
|
```
|
|
214
263
|
bundle install
|
|
215
264
|
rake test # run the test suite
|
|
216
|
-
rake benchmark # benchmark encoding and decoding (
|
|
265
|
+
rake benchmark # basic benchmark of encoding and decoding (5 runs after warmup)
|
|
217
266
|
rake profile # CPU profile with StackProf (requires the stackprof gem)
|
|
218
267
|
```
|
|
219
268
|
|
|
269
|
+
## Full benchmark script
|
|
270
|
+
|
|
271
|
+
`benchmark/run.rb` is a more thorough benchmark that exercises the encode and decode paths in several ways. It auto-enables YJIT, warms up before measuring, and reports object allocations, throughput (iterations/second via `benchmark-ips`), best-of-N wall-clock times, and a sustained mixed workload across encode (q85, q95 optimized, grayscale) and decode (baseline and progressive).
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
ruby benchmark/run.rb # standard run
|
|
275
|
+
ruby benchmark/run.rb --quick # single-shot wall-clock + allocations
|
|
276
|
+
ruby benchmark/run.rb --full # longer run, includes YJIT runtime stats
|
|
277
|
+
ruby benchmark/run.rb --profile # CPU profile with Vernier (writes JSON to /tmp)
|
|
278
|
+
ruby benchmark/run.rb --profile-alloc # retained-object profile with Vernier
|
|
279
|
+
```
|
|
280
|
+
|
|
220
281
|
## AI Disclosure
|
|
221
282
|
|
|
222
283
|
**Claude Code did the majority of the work.** The math of JPEG encoding/decoding is beyond me, except 'getting it' at a high level. I understand it like I understand the engine in my car :-) *Later update: OpenAI Codex is also reviewing and adding features now. It feels stronger in many areas.*
|
|
@@ -236,6 +297,7 @@ rake profile # CPU profile with StackProf (requires the stackprof gem)
|
|
|
236
297
|
## Credits
|
|
237
298
|
|
|
238
299
|
- [Ufuk Kayserilioglu](https://github.com/paracycle) - Major performance optimizations including integer-scaled AAN DCT, fixed-point color space conversion, and YJIT-targeted improvements.
|
|
300
|
+
- [Keith R. Bennett](https://github.com/keithrbennett) - Coverage testing and adding SimpleCov to the project.
|
|
239
301
|
|
|
240
302
|
## License
|
|
241
303
|
|
data/lib/pure_jpeg/decoder.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
424
|
+
symbol = ac_tab.decode_symbol(reader)
|
|
430
425
|
if symbol == 0x00 # EOB
|
|
431
426
|
while i < 64
|
|
432
427
|
out[i] = 0
|
data/lib/pure_jpeg/encoder.rb
CHANGED
|
@@ -42,6 +42,9 @@ module PureJPEG
|
|
|
42
42
|
luminance_table: nil, chrominance_table: nil,
|
|
43
43
|
quantization_modifier: nil, scramble_quantization: false,
|
|
44
44
|
optimize_huffman: false)
|
|
45
|
+
validate_quality!(quality, "quality")
|
|
46
|
+
validate_quality!(chroma_quality, "chroma_quality") if chroma_quality
|
|
47
|
+
|
|
45
48
|
@source = source
|
|
46
49
|
@quality = quality
|
|
47
50
|
@grayscale = grayscale
|
|
@@ -92,6 +95,12 @@ module PureJPEG
|
|
|
92
95
|
modified
|
|
93
96
|
end
|
|
94
97
|
|
|
98
|
+
def validate_quality!(value, name)
|
|
99
|
+
unless value.is_a?(Integer) && value.between?(1, 100)
|
|
100
|
+
raise ArgumentError, "#{name} must be an integer between 1 and 100"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
95
104
|
def validate_qtable!(table, name)
|
|
96
105
|
unless table.respond_to?(:length) && table.respond_to?(:all?)
|
|
97
106
|
raise ArgumentError, "#{name} must be a 64-element array of integers between 1 and 255"
|
|
@@ -137,11 +146,9 @@ module PureJPEG
|
|
|
137
146
|
lum_dc_bits, lum_dc_values, lum_ac_bits, lum_ac_values)
|
|
138
147
|
else
|
|
139
148
|
chr_qtable = build_chr_qtable
|
|
140
|
-
y_data,
|
|
149
|
+
y_data, cb_sub, cr_sub = extract_ycbcr420(width, height)
|
|
141
150
|
sub_w = (width + 1) / 2
|
|
142
151
|
sub_h = (height + 1) / 2
|
|
143
|
-
cb_sub = downsample(cb_data, width, height, sub_w, sub_h)
|
|
144
|
-
cr_sub = downsample(cr_data, width, height, sub_w, sub_h)
|
|
145
152
|
|
|
146
153
|
lum_dc_bits, lum_dc_values, lum_ac_bits, lum_ac_values,
|
|
147
154
|
chr_dc_bits, chr_dc_values, chr_ac_bits, chr_ac_values =
|
|
@@ -206,13 +213,12 @@ module PureJPEG
|
|
|
206
213
|
padded_h = (height + 7) & ~7
|
|
207
214
|
|
|
208
215
|
block = Array.new(64, 0)
|
|
209
|
-
qbuf = Array.new(64, 0)
|
|
210
216
|
zbuf = Array.new(64, 0)
|
|
211
217
|
|
|
212
218
|
(0...padded_h).step(8) do |by|
|
|
213
219
|
(0...padded_w).step(8) do |bx|
|
|
214
220
|
extract_block_into(y_data, width, height, bx, by, block)
|
|
215
|
-
yield transform_block(block,
|
|
221
|
+
yield transform_block(block, zbuf, qtable)
|
|
216
222
|
end
|
|
217
223
|
end
|
|
218
224
|
end
|
|
@@ -276,28 +282,27 @@ module PureJPEG
|
|
|
276
282
|
mcu_h = (height + 15) & ~15
|
|
277
283
|
|
|
278
284
|
block = Array.new(64, 0)
|
|
279
|
-
qbuf = Array.new(64, 0)
|
|
280
285
|
zbuf = Array.new(64, 0)
|
|
281
286
|
|
|
282
287
|
(0...mcu_h).step(16) do |my|
|
|
283
288
|
(0...mcu_w).step(16) do |mx|
|
|
284
289
|
extract_block_into(y_data, width, height, mx, my, block)
|
|
285
|
-
yield :y, transform_block(block,
|
|
290
|
+
yield :y, transform_block(block, zbuf, lum_qt)
|
|
286
291
|
|
|
287
292
|
extract_block_into(y_data, width, height, mx + 8, my, block)
|
|
288
|
-
yield :y, transform_block(block,
|
|
293
|
+
yield :y, transform_block(block, zbuf, lum_qt)
|
|
289
294
|
|
|
290
295
|
extract_block_into(y_data, width, height, mx, my + 8, block)
|
|
291
|
-
yield :y, transform_block(block,
|
|
296
|
+
yield :y, transform_block(block, zbuf, lum_qt)
|
|
292
297
|
|
|
293
298
|
extract_block_into(y_data, width, height, mx + 8, my + 8, block)
|
|
294
|
-
yield :y, transform_block(block,
|
|
299
|
+
yield :y, transform_block(block, zbuf, lum_qt)
|
|
295
300
|
|
|
296
301
|
extract_block_into(cb_sub, sub_w, sub_h, mx >> 1, my >> 1, block)
|
|
297
|
-
yield :cb, transform_block(block,
|
|
302
|
+
yield :cb, transform_block(block, zbuf, chr_qt)
|
|
298
303
|
|
|
299
304
|
extract_block_into(cr_sub, sub_w, sub_h, mx >> 1, my >> 1, block)
|
|
300
|
-
yield :cr, transform_block(block,
|
|
305
|
+
yield :cr, transform_block(block, zbuf, chr_qt)
|
|
301
306
|
end
|
|
302
307
|
end
|
|
303
308
|
end
|
|
@@ -322,10 +327,9 @@ module PureJPEG
|
|
|
322
327
|
|
|
323
328
|
# --- Shared block pipeline (all buffers pre-allocated) ---
|
|
324
329
|
|
|
325
|
-
def transform_block(block,
|
|
330
|
+
def transform_block(block, zbuf, qtable)
|
|
326
331
|
DCT.forward!(block)
|
|
327
|
-
Quantization.
|
|
328
|
-
Zigzag.reorder!(qbuf, zbuf)
|
|
332
|
+
Quantization.quantize_zigzag!(block, qtable, zbuf)
|
|
329
333
|
zbuf
|
|
330
334
|
end
|
|
331
335
|
|
|
@@ -383,61 +387,65 @@ module PureJPEG
|
|
|
383
387
|
luminance
|
|
384
388
|
end
|
|
385
389
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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)
|
|
391
400
|
|
|
392
401
|
if source.respond_to?(:packed_pixels)
|
|
393
402
|
packed = source.packed_pixels
|
|
394
403
|
r_shift, g_shift, b_shift = packed_shifts
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
409
420
|
width.times do |px|
|
|
410
421
|
pixel = source[px, py]
|
|
411
422
|
r = pixel.r; g = pixel.g; b = pixel.b
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
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)
|
|
416
426
|
end
|
|
417
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
|
|
418
434
|
end
|
|
419
435
|
|
|
420
|
-
[y_data,
|
|
436
|
+
[y_data, cb_sub, cr_sub]
|
|
421
437
|
end
|
|
422
438
|
|
|
423
|
-
def
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
dst_row = dy * dst_w
|
|
433
|
-
dst_w.times do |dx|
|
|
434
|
-
sx = dx << 1
|
|
435
|
-
x1 = sx < max_x ? sx + 1 : max_x
|
|
436
|
-
out[dst_row + dx] = ((data[row0 + sx] + data[row0 + x1] +
|
|
437
|
-
data[row1 + sx] + data[row1 + x1]) >> 2)
|
|
438
|
-
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
|
|
439
448
|
end
|
|
440
|
-
out
|
|
441
449
|
end
|
|
442
450
|
|
|
443
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
|
|
28
|
+
def decode_symbol(reader)
|
|
29
29
|
code = 0
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
87
|
+
cat = symbol & 0x0F
|
|
94
88
|
writer.write_bits(self.class.value_bits(value, cat), cat)
|
|
95
89
|
end
|
|
96
90
|
end
|
data/lib/pure_jpeg/image.rb
CHANGED
|
@@ -38,6 +38,7 @@ module PureJPEG
|
|
|
38
38
|
# @param y [Integer] row (0-based)
|
|
39
39
|
# @return [Source::Pixel] pixel with +.r+, +.g+, +.b+ in 0-255
|
|
40
40
|
def [](x, y)
|
|
41
|
+
validate_coordinates!(x, y)
|
|
41
42
|
color = @packed_pixels[y * @width + x]
|
|
42
43
|
Source::Pixel.new((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF)
|
|
43
44
|
end
|
|
@@ -49,6 +50,7 @@ module PureJPEG
|
|
|
49
50
|
# @param pixel [Source::Pixel] replacement pixel
|
|
50
51
|
# @return [Source::Pixel]
|
|
51
52
|
def []=(x, y, pixel)
|
|
53
|
+
validate_coordinates!(x, y)
|
|
52
54
|
@packed_pixels[y * @width + x] = (pixel.r << 16) | (pixel.g << 8) | pixel.b
|
|
53
55
|
pixel
|
|
54
56
|
end
|
|
@@ -85,5 +87,13 @@ module PureJPEG
|
|
|
85
87
|
end
|
|
86
88
|
end
|
|
87
89
|
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def validate_coordinates!(x, y)
|
|
94
|
+
unless x.is_a?(Integer) && y.is_a?(Integer) && x >= 0 && y >= 0 && x < @width && y < @height
|
|
95
|
+
raise IndexError, "Pixel coordinate out of bounds: #{x.inspect}, #{y.inspect}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
88
98
|
end
|
|
89
99
|
end
|
|
@@ -101,10 +101,9 @@ module PureJPEG
|
|
|
101
101
|
ICC_PROFILE_SIG = "ICC_PROFILE\0".b
|
|
102
102
|
|
|
103
103
|
def parse_app2
|
|
104
|
-
|
|
105
|
-
end_pos = @pos + length - 2
|
|
104
|
+
end_pos = read_segment_end
|
|
106
105
|
|
|
107
|
-
if
|
|
106
|
+
if (end_pos - @pos) >= 14 && @data[@pos, 12] == ICC_PROFILE_SIG
|
|
108
107
|
@pos += 12
|
|
109
108
|
seq_no = read_byte
|
|
110
109
|
_total = read_byte
|
|
@@ -121,18 +120,18 @@ module PureJPEG
|
|
|
121
120
|
end
|
|
122
121
|
|
|
123
122
|
def skip_segment
|
|
124
|
-
|
|
125
|
-
@pos += length - 2
|
|
123
|
+
@pos = read_segment_end
|
|
126
124
|
end
|
|
127
125
|
|
|
128
126
|
def parse_dqt
|
|
129
|
-
|
|
130
|
-
end_pos = @pos + length - 2
|
|
127
|
+
end_pos = read_segment_end
|
|
131
128
|
|
|
132
129
|
while @pos < end_pos
|
|
133
130
|
info = read_byte
|
|
134
131
|
precision = (info >> 4) & 0x0F # 0 = 8-bit, 1 = 16-bit
|
|
135
132
|
table_id = info & 0x0F
|
|
133
|
+
bytes_per_value = precision == 0 ? 1 : 2
|
|
134
|
+
ensure_remaining_in_segment!(end_pos, 64 * bytes_per_value, "DQT")
|
|
136
135
|
|
|
137
136
|
zigzag_table = Array.new(64)
|
|
138
137
|
64.times do |i|
|
|
@@ -146,16 +145,17 @@ module PureJPEG
|
|
|
146
145
|
end
|
|
147
146
|
|
|
148
147
|
def parse_dht
|
|
149
|
-
|
|
150
|
-
end_pos = @pos + length - 2
|
|
148
|
+
end_pos = read_segment_end
|
|
151
149
|
|
|
152
150
|
while @pos < end_pos
|
|
151
|
+
ensure_remaining_in_segment!(end_pos, 17, "DHT")
|
|
153
152
|
info = read_byte
|
|
154
153
|
table_class = (info >> 4) & 0x0F # 0 = DC, 1 = AC
|
|
155
154
|
table_id = info & 0x0F
|
|
156
155
|
|
|
157
156
|
bits = Array.new(16) { read_byte }
|
|
158
157
|
total = bits.sum
|
|
158
|
+
ensure_remaining_in_segment!(end_pos, total, "DHT")
|
|
159
159
|
values = Array.new(total) { read_byte }
|
|
160
160
|
|
|
161
161
|
@huffman_tables[[table_class, table_id]] = { bits: bits, values: values }
|
|
@@ -163,11 +163,13 @@ module PureJPEG
|
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def parse_sof0
|
|
166
|
-
|
|
166
|
+
end_pos = read_segment_end
|
|
167
|
+
ensure_remaining_in_segment!(end_pos, 6, "SOF")
|
|
167
168
|
read_byte # precision (always 8 for baseline)
|
|
168
169
|
@height = read_u16
|
|
169
170
|
@width = read_u16
|
|
170
171
|
num_components = read_byte
|
|
172
|
+
ensure_remaining_in_segment!(end_pos, num_components * 3, "SOF")
|
|
171
173
|
|
|
172
174
|
@components = Array.new(num_components) do
|
|
173
175
|
id = read_byte
|
|
@@ -177,11 +179,14 @@ module PureJPEG
|
|
|
177
179
|
qt_id = read_byte
|
|
178
180
|
Component.new(id, h, v, qt_id)
|
|
179
181
|
end
|
|
182
|
+
@pos = end_pos
|
|
180
183
|
end
|
|
181
184
|
|
|
182
185
|
def parse_sos
|
|
183
|
-
|
|
186
|
+
end_pos = read_segment_end
|
|
187
|
+
ensure_remaining_in_segment!(end_pos, 1, "SOS")
|
|
184
188
|
num_components = read_byte
|
|
189
|
+
ensure_remaining_in_segment!(end_pos, num_components * 2 + 3, "SOS")
|
|
185
190
|
|
|
186
191
|
components = Array.new(num_components) do
|
|
187
192
|
id = read_byte
|
|
@@ -196,12 +201,33 @@ module PureJPEG
|
|
|
196
201
|
ahl = read_byte # successive approximation
|
|
197
202
|
ah = (ahl >> 4) & 0x0F
|
|
198
203
|
al = ahl & 0x0F
|
|
204
|
+
@pos = end_pos
|
|
199
205
|
Scan.new(components, ss, se, ah, al, nil)
|
|
200
206
|
end
|
|
201
207
|
|
|
202
208
|
def parse_dri
|
|
203
|
-
|
|
209
|
+
end_pos = read_segment_end
|
|
210
|
+
ensure_remaining_in_segment!(end_pos, 2, "DRI")
|
|
204
211
|
@restart_interval = read_u16
|
|
212
|
+
@pos = end_pos
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def read_segment_end
|
|
216
|
+
length = read_u16
|
|
217
|
+
raise PureJPEG::DecodeError, "Invalid JPEG segment length: #{length}" if length < 2
|
|
218
|
+
|
|
219
|
+
end_pos = @pos + length - 2
|
|
220
|
+
if end_pos > @data.bytesize
|
|
221
|
+
raise PureJPEG::DecodeError, "JPEG segment length exceeds available data"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
end_pos
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def ensure_remaining_in_segment!(end_pos, byte_count, segment_name)
|
|
228
|
+
return if @pos + byte_count <= end_pos
|
|
229
|
+
|
|
230
|
+
raise PureJPEG::DecodeError, "Truncated #{segment_name} segment"
|
|
205
231
|
end
|
|
206
232
|
|
|
207
233
|
# Extract entropy-coded scan data (everything from current position to EOI marker).
|
|
@@ -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
|
|
@@ -18,10 +18,16 @@ module PureJPEG
|
|
|
18
18
|
attr_reader :height
|
|
19
19
|
|
|
20
20
|
# @param image [ChunkyPNG::Image] the source PNG image
|
|
21
|
-
|
|
21
|
+
# @param background [Array<Integer>, nil] optional [r, g, b] background
|
|
22
|
+
# color to composite transparent pixels against before encoding
|
|
23
|
+
def initialize(image, background: nil)
|
|
22
24
|
@width = image.width
|
|
23
25
|
@height = image.height
|
|
24
|
-
@packed_pixels =
|
|
26
|
+
@packed_pixels = if background.nil?
|
|
27
|
+
image.pixels
|
|
28
|
+
else
|
|
29
|
+
composite_pixels(image.pixels, background)
|
|
30
|
+
end
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
# @return [Array<Integer>] flat row-major array of packed RGBA integers
|
|
@@ -40,6 +46,37 @@ module PureJPEG
|
|
|
40
46
|
(color >> 8) & 0xFF
|
|
41
47
|
)
|
|
42
48
|
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def composite_pixels(pixels, background)
|
|
53
|
+
bg_r, bg_g, bg_b = validate_background!(background)
|
|
54
|
+
|
|
55
|
+
pixels.map do |color|
|
|
56
|
+
alpha = color & 0xFF
|
|
57
|
+
next color if alpha == 255
|
|
58
|
+
|
|
59
|
+
src_r = (color >> 24) & 0xFF
|
|
60
|
+
src_g = (color >> 16) & 0xFF
|
|
61
|
+
src_b = (color >> 8) & 0xFF
|
|
62
|
+
inv_alpha = 255 - alpha
|
|
63
|
+
|
|
64
|
+
r = ((src_r * alpha) + (bg_r * inv_alpha) + 127) / 255
|
|
65
|
+
g = ((src_g * alpha) + (bg_g * inv_alpha) + 127) / 255
|
|
66
|
+
b = ((src_b * alpha) + (bg_b * inv_alpha) + 127) / 255
|
|
67
|
+
|
|
68
|
+
(r << 24) | (g << 16) | (b << 8) | 255
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_background!(background)
|
|
73
|
+
unless background.respond_to?(:length) && background.length == 3 &&
|
|
74
|
+
background.all? { |v| v.is_a?(Integer) && v.between?(0, 255) }
|
|
75
|
+
raise ArgumentError, "background must be an [r, g, b] array of integers between 0 and 255"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
background
|
|
79
|
+
end
|
|
43
80
|
end
|
|
44
81
|
end
|
|
45
82
|
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
|
|
data/lib/pure_jpeg/version.rb
CHANGED
data/lib/pure_jpeg.rb
CHANGED
|
@@ -50,10 +50,12 @@ module PureJPEG
|
|
|
50
50
|
# and passes it to {.encode}.
|
|
51
51
|
#
|
|
52
52
|
# @param image [ChunkyPNG::Image] the source image
|
|
53
|
+
# @param background [Array<Integer>, nil] optional [r, g, b] background
|
|
54
|
+
# color to composite transparent pixels against before encoding
|
|
53
55
|
# @param opts [Hash] encoding options passed to {Encoder#initialize}
|
|
54
56
|
# @return [Encoder]
|
|
55
|
-
def self.from_chunky_png(image, **opts)
|
|
56
|
-
source = Source::ChunkyPNGSource.new(image)
|
|
57
|
+
def self.from_chunky_png(image, background: nil, **opts)
|
|
58
|
+
source = Source::ChunkyPNGSource.new(image, background: background)
|
|
57
59
|
Encoder.new(source, **opts)
|
|
58
60
|
end
|
|
59
61
|
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter Cooper
|
|
@@ -86,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
87
|
version: '0'
|
|
88
88
|
requirements: []
|
|
89
|
-
rubygems_version:
|
|
89
|
+
rubygems_version: 4.0.6
|
|
90
90
|
specification_version: 4
|
|
91
91
|
summary: Pure Ruby JPEG encoder and decoder
|
|
92
92
|
test_files: []
|