pura-jpeg 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b497cc2d62e912f13269111752139cdede385a5a4f0fb0a020743c9270f969a6
4
+ data.tar.gz: 6712cb8189a06cfd4ea1fe4af6c0dbd713d286f79acd29e24f0e9123c4f09843
5
+ SHA512:
6
+ metadata.gz: 533b5753fb406eac2f2590b719cba7d1ab282ccbadf333fbb16b51bc35432b9e436552f85d9fd30336f6028b46f98fcfa879ce40d4957ee006591c60d99fa1d8
7
+ data.tar.gz: a4f52d7145e529abd3c12ea8363d5f36fc3853f3095c1f82f953dbe098eeabd4859422678a5bc68755beca89c4577e9c6571ee90ef8fd4d201fb0de4a422ed9b
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jpeg_pure contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # pura-jpeg
2
+
3
+ A pure Ruby JPEG decoder/encoder with zero C extension dependencies.
4
+
5
+ Part of the **pura-*** series — pure Ruby image codec gems.
6
+
7
+ ## Features
8
+
9
+ - Baseline JPEG decoding and encoding (SOF0)
10
+ - Image resizing (bilinear / nearest-neighbor interpolation)
11
+ - Huffman coding, fast integer IDCT/FDCT, YCbCr ↔ RGB conversion
12
+ - 4:2:0, 4:2:2, and 4:4:4 chroma subsampling
13
+ - No native extensions, no FFI, no external dependencies
14
+ - CLI tool included
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ gem install pura-jpeg
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ruby
25
+ require "pura-jpeg"
26
+
27
+ # Decode
28
+ image = Pura::Jpeg.decode("photo.jpg")
29
+ image.width #=> 1920
30
+ image.height #=> 1080
31
+ image.pixels #=> Raw RGB byte string
32
+
33
+ # Resize
34
+ thumb = image.resize(200, 200)
35
+ fitted = image.resize_fit(800, 600) # maintain aspect ratio
36
+
37
+ # Encode
38
+ Pura::Jpeg.encode(thumb, "thumb.jpg", quality: 80)
39
+ ```
40
+
41
+ ## CLI
42
+
43
+ ```bash
44
+ pura-jpeg decode input.jpg --info
45
+ pura-jpeg resize input.jpg --width 200 --height 200 --out thumb.jpg
46
+ pura-jpeg resize input.jpg --fit 800x600 --out fitted.jpg
47
+ ```
48
+
49
+ ## Benchmark
50
+
51
+ 400×400 image, Ruby 4.0.2 + YJIT.
52
+
53
+ ### Decode
54
+
55
+ | Decoder | Time | Language |
56
+ |---------|------|----------|
57
+ | jpeg-js (V8 JIT) | 39 ms | Pure JavaScript |
58
+ | jpeg-js (`--jitless`) | 143 ms | Pure JavaScript (interpreter) |
59
+ | ffmpeg (C) | 55 ms | C |
60
+ | **pura-jpeg** | **304 ms** | **Pure Ruby** |
61
+ | ptjd | 5,448 ms | Pure Tcl |
62
+
63
+ ### Encode
64
+
65
+ | Encoder | Time | vs ffmpeg |
66
+ |---------|------|-----------|
67
+ | ffmpeg (C) | 62 ms | — |
68
+ | **pura-jpeg** | **238 ms** | 3.8× slower |
69
+
70
+ ### Full pipeline (decode → resize → encode)
71
+
72
+ | Operation | Time |
73
+ |-----------|------|
74
+ | Decode | 304 ms |
75
+ | Encode (quality 85) | 243 ms |
76
+ | Full pipeline | ~547 ms |
77
+
78
+ pura-jpeg is **2× faster than ptjd** (Tcl) and within **2× of jpeg-js running without JIT**. These are the only three pure scripting-language JPEG implementations that exist — Python, Perl, PHP, and Lua all rely on C extensions.
79
+
80
+ ## Why pure Ruby?
81
+
82
+ - **`gem install` and go** — no `brew install`, no `apt install`, no C compiler needed
83
+ - **Works everywhere Ruby works** — CRuby, ruby.wasm, mruby, JRuby, TruffleRuby
84
+ - **Edge/Wasm ready** — runs in Cloudflare Workers, browsers (via ruby.wasm), sandboxed environments where you can't install system libraries
85
+ - **Perfect for dev/CI** — no ImageMagick or libvips setup. `rails new` → image upload → it just works
86
+ - **Unix philosophy** — one format, one gem, composable
87
+
88
+ ## Related gems
89
+
90
+ | Gem | Format | Status |
91
+ |-----|--------|--------|
92
+ | **pura-jpeg** | JPEG | ✅ Available |
93
+ | [pura-png](https://github.com/komagata/pura-png) | PNG | ✅ Available |
94
+ | [pura-bmp](https://github.com/komagata/pura-bmp) | BMP | ✅ Available |
95
+ | [pura-gif](https://github.com/komagata/pura-gif) | GIF | ✅ Available |
96
+ | [pura-tiff](https://github.com/komagata/pura-tiff) | TIFF | ✅ Available |
97
+ | [pura-ico](https://github.com/komagata/pura-ico) | ICO | ✅ Available |
98
+ | [pura-webp](https://github.com/komagata/pura-webp) | WebP | ✅ Available |
99
+ | [pura-image](https://github.com/komagata/pura-image) | All formats | ✅ Available |
100
+
101
+ ## License
102
+
103
+ MIT
data/bin/pura-jpeg ADDED
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/pure-jpeg"
5
+
6
+ def usage
7
+ puts <<~USAGE
8
+ Usage: pure-jpeg <command> [options]
9
+
10
+ Commands:
11
+ decode <input.jpg> [options] Decode a JPEG file
12
+ --info Show image metadata
13
+ --out <file> Write raw RGB data to file
14
+
15
+ encode <input.rgb> [options] Encode raw RGB data to JPEG
16
+ --width <n> Image width (required)
17
+ --height <n> Image height (required)
18
+ --quality <n> JPEG quality 1-100 (default: 85)
19
+ --subsampling <type> 420 or 444 (default: 420)
20
+ --out <file> Output JPEG file (required)
21
+
22
+ resize <input.jpg> [options] Resize a JPEG file
23
+ --width <n> Target width
24
+ --height <n> Target height
25
+ --fit <W>x<H> Fit within bounds (maintain aspect ratio)
26
+ --fill <W>x<H> Fill exact size (crop to fit)
27
+ --interpolation <type> bilinear or nearest (default: bilinear)
28
+ --quality <n> Output JPEG quality (default: 85)
29
+ --out <file> Output JPEG file (required)
30
+
31
+ benchmark <input.jpg> Benchmark decoding vs system tools
32
+
33
+ version Show version
34
+ USAGE
35
+ exit 1
36
+ end
37
+
38
+ def cmd_decode(args)
39
+ input = nil
40
+ info_only = false
41
+ output = nil
42
+
43
+ i = 0
44
+ while i < args.size
45
+ case args[i]
46
+ when "--info"
47
+ info_only = true
48
+ when "--out"
49
+ i += 1
50
+ output = args[i]
51
+ else
52
+ input = args[i]
53
+ end
54
+ i += 1
55
+ end
56
+
57
+ usage unless input
58
+
59
+ unless File.exist?(input)
60
+ $stderr.puts "Error: file not found: #{input}"
61
+ exit 1
62
+ end
63
+
64
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
65
+ image = Pure::Jpeg.decode(input)
66
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
67
+
68
+ if info_only
69
+ puts "File: #{input}"
70
+ puts "Width: #{image.width}"
71
+ puts "Height: #{image.height}"
72
+ puts "Pixels: #{image.width * image.height}"
73
+ puts "Size: #{File.size(input)} bytes"
74
+ puts "Decode: #{'%.3f' % elapsed}s"
75
+ elsif output
76
+ File.binwrite(output, image.pixels)
77
+ puts "Wrote #{image.pixels.bytesize} bytes of raw RGB to #{output}"
78
+ puts "Decode time: #{'%.3f' % elapsed}s"
79
+ else
80
+ puts "Decoded #{input}: #{image.width}x#{image.height} in #{'%.3f' % elapsed}s"
81
+ end
82
+ end
83
+
84
+ def cmd_encode(args)
85
+ input = nil
86
+ width = nil
87
+ height = nil
88
+ quality = 85
89
+ subsampling = :s420
90
+ output = nil
91
+
92
+ i = 0
93
+ while i < args.size
94
+ case args[i]
95
+ when "--width"
96
+ i += 1; width = args[i].to_i
97
+ when "--height"
98
+ i += 1; height = args[i].to_i
99
+ when "--quality"
100
+ i += 1; quality = args[i].to_i
101
+ when "--subsampling"
102
+ i += 1
103
+ subsampling = args[i] == "444" ? :s444 : :s420
104
+ when "--out"
105
+ i += 1; output = args[i]
106
+ else
107
+ input = args[i]
108
+ end
109
+ i += 1
110
+ end
111
+
112
+ unless input && width && height && output
113
+ $stderr.puts "Error: encode requires input file, --width, --height, and --out"
114
+ usage
115
+ end
116
+
117
+ unless File.exist?(input)
118
+ $stderr.puts "Error: file not found: #{input}"
119
+ exit 1
120
+ end
121
+
122
+ raw = File.binread(input)
123
+ image = Pure::Jpeg::Image.new(width, height, raw)
124
+
125
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
126
+ size = Pure::Jpeg.encode(image, output, quality: quality, subsampling: subsampling)
127
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
128
+
129
+ puts "Encoded #{width}x#{height} to #{output} (#{size} bytes) in #{'%.3f' % elapsed}s"
130
+ end
131
+
132
+ def cmd_resize(args)
133
+ input = nil
134
+ width = nil
135
+ height = nil
136
+ fit = nil
137
+ fill = nil
138
+ interpolation = :bilinear
139
+ quality = 85
140
+ output = nil
141
+
142
+ i = 0
143
+ while i < args.size
144
+ case args[i]
145
+ when "--width"
146
+ i += 1; width = args[i].to_i
147
+ when "--height"
148
+ i += 1; height = args[i].to_i
149
+ when "--fit"
150
+ i += 1; fit = args[i]
151
+ when "--fill"
152
+ i += 1; fill = args[i]
153
+ when "--interpolation"
154
+ i += 1
155
+ interpolation = args[i] == "nearest" ? :nearest : :bilinear
156
+ when "--quality"
157
+ i += 1; quality = args[i].to_i
158
+ when "--out"
159
+ i += 1; output = args[i]
160
+ else
161
+ input = args[i]
162
+ end
163
+ i += 1
164
+ end
165
+
166
+ unless input && output
167
+ $stderr.puts "Error: resize requires input file and --out"
168
+ usage
169
+ end
170
+
171
+ unless File.exist?(input)
172
+ $stderr.puts "Error: file not found: #{input}"
173
+ exit 1
174
+ end
175
+
176
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
177
+ image = Pure::Jpeg.decode(input)
178
+
179
+ if fit
180
+ fw, fh = fit.split("x").map(&:to_i)
181
+ resized = image.resize_fit(fw, fh, interpolation: interpolation)
182
+ elsif fill
183
+ fw, fh = fill.split("x").map(&:to_i)
184
+ resized = image.resize_fill(fw, fh, interpolation: interpolation)
185
+ elsif width && height
186
+ resized = image.resize(width, height, interpolation: interpolation)
187
+ else
188
+ $stderr.puts "Error: specify --width/--height, --fit, or --fill"
189
+ usage
190
+ end
191
+
192
+ size = Pure::Jpeg.encode(resized, output, quality: quality)
193
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
194
+
195
+ puts "Resized #{image.width}x#{image.height} -> #{resized.width}x#{resized.height} to #{output} (#{size} bytes) in #{'%.3f' % elapsed}s"
196
+ end
197
+
198
+ def cmd_benchmark(args)
199
+ input = args[0]
200
+ usage unless input
201
+
202
+ require_relative "../benchmark/decode_benchmark"
203
+ DecodeBenchmark.run(input)
204
+ end
205
+
206
+ case ARGV[0]
207
+ when "decode"
208
+ cmd_decode(ARGV[1..])
209
+ when "encode"
210
+ cmd_encode(ARGV[1..])
211
+ when "resize"
212
+ cmd_resize(ARGV[1..])
213
+ when "benchmark"
214
+ cmd_benchmark(ARGV[1..])
215
+ when "version", "--version", "-v"
216
+ puts "pure-jpeg #{Pure::Jpeg::VERSION}"
217
+ when nil, "help", "--help", "-h"
218
+ usage
219
+ else
220
+ $stderr.puts "Unknown command: #{ARGV[0]}"
221
+ usage
222
+ end