pure_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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE +21 -0
- data/README.md +180 -0
- data/lib/pure_jpeg/bit_reader.rb +61 -0
- data/lib/pure_jpeg/bit_writer.rb +38 -0
- data/lib/pure_jpeg/dct.rb +83 -0
- data/lib/pure_jpeg/decoder.rb +238 -0
- data/lib/pure_jpeg/encoder.rb +297 -0
- data/lib/pure_jpeg/huffman/decoder.rb +40 -0
- data/lib/pure_jpeg/huffman/encoder.rb +88 -0
- data/lib/pure_jpeg/huffman/tables.rb +84 -0
- data/lib/pure_jpeg/image.rb +57 -0
- data/lib/pure_jpeg/jfif_reader.rb +173 -0
- data/lib/pure_jpeg/jfif_writer.rb +94 -0
- data/lib/pure_jpeg/quantization.rb +51 -0
- data/lib/pure_jpeg/source/chunky_png_source.rb +42 -0
- data/lib/pure_jpeg/source/interface.rb +21 -0
- data/lib/pure_jpeg/source/raw_source.rb +64 -0
- data/lib/pure_jpeg/version.rb +5 -0
- data/lib/pure_jpeg/zigzag.rb +28 -0
- data/lib/pure_jpeg.rb +60 -0
- metadata +91 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PureJPEG
|
|
4
|
+
# Baseline JPEG encoder.
|
|
5
|
+
#
|
|
6
|
+
# Encodes a pixel source into JPEG using DCT, quantization, and Huffman
|
|
7
|
+
# coding. Supports grayscale (1 component) and YCbCr color (3 components,
|
|
8
|
+
# 4:2:0 chroma subsampling).
|
|
9
|
+
#
|
|
10
|
+
# Use {PureJPEG.encode} for a convenient entry point.
|
|
11
|
+
class Encoder
|
|
12
|
+
# @return [#width, #height, #[]] the pixel source being encoded
|
|
13
|
+
attr_reader :source
|
|
14
|
+
# @return [Integer] the quality level (1-100)
|
|
15
|
+
attr_reader :quality
|
|
16
|
+
# @return [Boolean] whether grayscale mode is enabled
|
|
17
|
+
attr_reader :grayscale
|
|
18
|
+
|
|
19
|
+
# Create a new encoder for the given pixel source.
|
|
20
|
+
#
|
|
21
|
+
# @param source [#width, #height, #[]] any object responding to +width+,
|
|
22
|
+
# +height+, and +[x, y]+ (returning an object with +.r+, +.g+, +.b+)
|
|
23
|
+
# @param quality [Integer] overall compression quality, 1-100 (default 85)
|
|
24
|
+
# @param grayscale [Boolean] encode as single-channel grayscale (default false)
|
|
25
|
+
# @param chroma_quality [Integer, nil] independent quality for Cb/Cr channels,
|
|
26
|
+
# 1-100 (defaults to +quality+)
|
|
27
|
+
# @param luminance_table [Array<Integer>, nil] custom 64-element quantization
|
|
28
|
+
# table in raster order for the Y channel; overrides +quality+ for luma
|
|
29
|
+
# @param chrominance_table [Array<Integer>, nil] custom 64-element quantization
|
|
30
|
+
# table in raster order for Cb/Cr channels; overrides +chroma_quality+
|
|
31
|
+
# @param quantization_modifier [Proc, nil] a proc receiving +(table, channel)+
|
|
32
|
+
# where +channel+ is +:luminance+ or +:chrominance+, returning a modified
|
|
33
|
+
# table; applied after quality scaling but before encoding
|
|
34
|
+
# @param scramble_quantization [Boolean] write quantization tables in raster
|
|
35
|
+
# order instead of zigzag (non-spec-compliant; recreates the "early digicam"
|
|
36
|
+
# artifact look when decoded by standard viewers)
|
|
37
|
+
def initialize(source, quality: 85, grayscale: false, chroma_quality: nil,
|
|
38
|
+
luminance_table: nil, chrominance_table: nil,
|
|
39
|
+
quantization_modifier: nil, scramble_quantization: false)
|
|
40
|
+
@source = source
|
|
41
|
+
@quality = quality
|
|
42
|
+
@grayscale = grayscale
|
|
43
|
+
@chroma_quality = chroma_quality || quality
|
|
44
|
+
@luminance_table = luminance_table
|
|
45
|
+
@chrominance_table = chrominance_table
|
|
46
|
+
@quantization_modifier = quantization_modifier
|
|
47
|
+
@scramble_quantization = scramble_quantization
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Write the encoded JPEG to a file.
|
|
51
|
+
#
|
|
52
|
+
# @param path [String] output file path
|
|
53
|
+
# @return [void]
|
|
54
|
+
def write(path)
|
|
55
|
+
File.open(path, "wb") { |f| encode(f) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Return the encoded JPEG as a binary string.
|
|
59
|
+
#
|
|
60
|
+
# @return [String] raw JPEG bytes
|
|
61
|
+
def to_bytes
|
|
62
|
+
io = StringIO.new("".b)
|
|
63
|
+
encode(io)
|
|
64
|
+
io.string
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def build_lum_qtable
|
|
70
|
+
table = @luminance_table || Quantization.scale_table(Quantization::LUMINANCE_BASE, quality)
|
|
71
|
+
table = @quantization_modifier.call(table, :luminance) if @quantization_modifier
|
|
72
|
+
table
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_chr_qtable
|
|
76
|
+
table = @chrominance_table || Quantization.scale_table(Quantization::CHROMINANCE_BASE, @chroma_quality)
|
|
77
|
+
table = @quantization_modifier.call(table, :chrominance) if @quantization_modifier
|
|
78
|
+
table
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def encode(io)
|
|
82
|
+
width = source.width
|
|
83
|
+
height = source.height
|
|
84
|
+
|
|
85
|
+
lum_qtable = build_lum_qtable
|
|
86
|
+
lum_dc = Huffman.build_table(Huffman::DC_LUMINANCE_BITS, Huffman::DC_LUMINANCE_VALUES)
|
|
87
|
+
lum_ac = Huffman.build_table(Huffman::AC_LUMINANCE_BITS, Huffman::AC_LUMINANCE_VALUES)
|
|
88
|
+
lum_huff = Huffman::Encoder.new(lum_dc, lum_ac)
|
|
89
|
+
|
|
90
|
+
if grayscale
|
|
91
|
+
scan_data = encode_grayscale(width, height, lum_qtable, lum_huff)
|
|
92
|
+
write_grayscale_jfif(io, width, height, lum_qtable, scan_data)
|
|
93
|
+
else
|
|
94
|
+
chr_qtable = build_chr_qtable
|
|
95
|
+
chr_dc = Huffman.build_table(Huffman::DC_CHROMINANCE_BITS, Huffman::DC_CHROMINANCE_VALUES)
|
|
96
|
+
chr_ac = Huffman.build_table(Huffman::AC_CHROMINANCE_BITS, Huffman::AC_CHROMINANCE_VALUES)
|
|
97
|
+
chr_huff = Huffman::Encoder.new(chr_dc, chr_ac)
|
|
98
|
+
|
|
99
|
+
scan_data = encode_color(width, height, lum_qtable, chr_qtable, lum_huff, chr_huff)
|
|
100
|
+
write_color_jfif(io, width, height, lum_qtable, chr_qtable, scan_data)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# --- Grayscale encoding ---
|
|
105
|
+
|
|
106
|
+
def encode_grayscale(width, height, qtable, huff)
|
|
107
|
+
y_data = extract_luminance(width, height)
|
|
108
|
+
padded_w = (width + 7) & ~7
|
|
109
|
+
padded_h = (height + 7) & ~7
|
|
110
|
+
|
|
111
|
+
# Reusable buffers
|
|
112
|
+
block = Array.new(64, 0.0)
|
|
113
|
+
temp = Array.new(64, 0.0)
|
|
114
|
+
dct = Array.new(64, 0.0)
|
|
115
|
+
qbuf = Array.new(64, 0)
|
|
116
|
+
zbuf = Array.new(64, 0)
|
|
117
|
+
|
|
118
|
+
bit_writer = BitWriter.new
|
|
119
|
+
prev_dc = 0
|
|
120
|
+
|
|
121
|
+
(0...padded_h).step(8) do |by|
|
|
122
|
+
(0...padded_w).step(8) do |bx|
|
|
123
|
+
extract_block_into(y_data, width, height, bx, by, block)
|
|
124
|
+
prev_dc = encode_block(block, temp, dct, qbuf, zbuf, qtable, huff, prev_dc, bit_writer)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
bit_writer.flush
|
|
129
|
+
bit_writer.bytes
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def write_grayscale_jfif(io, width, height, qtable, scan_data)
|
|
133
|
+
jfif = JFIFWriter.new(io, scramble_quantization: @scramble_quantization)
|
|
134
|
+
jfif.write_soi
|
|
135
|
+
jfif.write_app0
|
|
136
|
+
jfif.write_dqt(qtable, 0)
|
|
137
|
+
jfif.write_sof0(width, height, [[1, 1, 1, 0]])
|
|
138
|
+
jfif.write_dht(0, 0, Huffman::DC_LUMINANCE_BITS, Huffman::DC_LUMINANCE_VALUES)
|
|
139
|
+
jfif.write_dht(1, 0, Huffman::AC_LUMINANCE_BITS, Huffman::AC_LUMINANCE_VALUES)
|
|
140
|
+
jfif.write_sos([[1, 0, 0]])
|
|
141
|
+
jfif.write_scan_data(scan_data)
|
|
142
|
+
jfif.write_eoi
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# --- Color encoding (YCbCr 4:2:0) ---
|
|
146
|
+
|
|
147
|
+
def encode_color(width, height, lum_qt, chr_qt, lum_huff, chr_huff)
|
|
148
|
+
y_data, cb_data, cr_data = extract_ycbcr(width, height)
|
|
149
|
+
|
|
150
|
+
sub_w = (width + 1) / 2
|
|
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
|
+
|
|
155
|
+
mcu_w = (width + 15) & ~15
|
|
156
|
+
mcu_h = (height + 15) & ~15
|
|
157
|
+
|
|
158
|
+
# Reusable buffers
|
|
159
|
+
block = Array.new(64, 0.0)
|
|
160
|
+
temp = Array.new(64, 0.0)
|
|
161
|
+
dct = Array.new(64, 0.0)
|
|
162
|
+
qbuf = Array.new(64, 0)
|
|
163
|
+
zbuf = Array.new(64, 0)
|
|
164
|
+
|
|
165
|
+
bit_writer = BitWriter.new
|
|
166
|
+
prev_dc_y = 0
|
|
167
|
+
prev_dc_cb = 0
|
|
168
|
+
prev_dc_cr = 0
|
|
169
|
+
|
|
170
|
+
(0...mcu_h).step(16) do |my|
|
|
171
|
+
(0...mcu_w).step(16) do |mx|
|
|
172
|
+
# 4 luminance blocks
|
|
173
|
+
extract_block_into(y_data, width, height, mx, my, block)
|
|
174
|
+
prev_dc_y = encode_block(block, temp, dct, qbuf, zbuf, lum_qt, lum_huff, prev_dc_y, bit_writer)
|
|
175
|
+
|
|
176
|
+
extract_block_into(y_data, width, height, mx + 8, my, block)
|
|
177
|
+
prev_dc_y = encode_block(block, temp, dct, qbuf, zbuf, lum_qt, lum_huff, prev_dc_y, bit_writer)
|
|
178
|
+
|
|
179
|
+
extract_block_into(y_data, width, height, mx, my + 8, block)
|
|
180
|
+
prev_dc_y = encode_block(block, temp, dct, qbuf, zbuf, lum_qt, lum_huff, prev_dc_y, bit_writer)
|
|
181
|
+
|
|
182
|
+
extract_block_into(y_data, width, height, mx + 8, my + 8, block)
|
|
183
|
+
prev_dc_y = encode_block(block, temp, dct, qbuf, zbuf, lum_qt, lum_huff, prev_dc_y, bit_writer)
|
|
184
|
+
|
|
185
|
+
# 1 Cb block
|
|
186
|
+
extract_block_into(cb_sub, sub_w, sub_h, mx >> 1, my >> 1, block)
|
|
187
|
+
prev_dc_cb = encode_block(block, temp, dct, qbuf, zbuf, chr_qt, chr_huff, prev_dc_cb, bit_writer)
|
|
188
|
+
|
|
189
|
+
# 1 Cr block
|
|
190
|
+
extract_block_into(cr_sub, sub_w, sub_h, mx >> 1, my >> 1, block)
|
|
191
|
+
prev_dc_cr = encode_block(block, temp, dct, qbuf, zbuf, chr_qt, chr_huff, prev_dc_cr, bit_writer)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
bit_writer.flush
|
|
196
|
+
bit_writer.bytes
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def write_color_jfif(io, width, height, lum_qt, chr_qt, scan_data)
|
|
200
|
+
jfif = JFIFWriter.new(io, scramble_quantization: @scramble_quantization)
|
|
201
|
+
jfif.write_soi
|
|
202
|
+
jfif.write_app0
|
|
203
|
+
jfif.write_dqt(lum_qt, 0)
|
|
204
|
+
jfif.write_dqt(chr_qt, 1)
|
|
205
|
+
jfif.write_sof0(width, height, [[1, 2, 2, 0], [2, 1, 1, 1], [3, 1, 1, 1]])
|
|
206
|
+
jfif.write_dht(0, 0, Huffman::DC_LUMINANCE_BITS, Huffman::DC_LUMINANCE_VALUES)
|
|
207
|
+
jfif.write_dht(1, 0, Huffman::AC_LUMINANCE_BITS, Huffman::AC_LUMINANCE_VALUES)
|
|
208
|
+
jfif.write_dht(0, 1, Huffman::DC_CHROMINANCE_BITS, Huffman::DC_CHROMINANCE_VALUES)
|
|
209
|
+
jfif.write_dht(1, 1, Huffman::AC_CHROMINANCE_BITS, Huffman::AC_CHROMINANCE_VALUES)
|
|
210
|
+
jfif.write_sos([[1, 0, 0], [2, 1, 1], [3, 1, 1]])
|
|
211
|
+
jfif.write_scan_data(scan_data)
|
|
212
|
+
jfif.write_eoi
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# --- Shared block pipeline (all buffers pre-allocated) ---
|
|
216
|
+
|
|
217
|
+
def encode_block(block, temp, dct, qbuf, zbuf, qtable, huff, prev_dc, bit_writer)
|
|
218
|
+
DCT.forward!(block, temp, dct)
|
|
219
|
+
Quantization.quantize!(dct, qtable, qbuf)
|
|
220
|
+
Zigzag.reorder!(qbuf, zbuf)
|
|
221
|
+
huff.encode_block(zbuf, prev_dc, bit_writer)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# --- Pixel extraction ---
|
|
225
|
+
|
|
226
|
+
def extract_luminance(width, height)
|
|
227
|
+
luminance = Array.new(width * height)
|
|
228
|
+
height.times do |y|
|
|
229
|
+
row = y * width
|
|
230
|
+
width.times do |x|
|
|
231
|
+
pixel = source[x, y]
|
|
232
|
+
luminance[row + x] = (0.299 * pixel.r + 0.587 * pixel.g + 0.114 * pixel.b).round.clamp(0, 255)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
luminance
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def extract_ycbcr(width, height)
|
|
239
|
+
size = width * height
|
|
240
|
+
y_data = Array.new(size)
|
|
241
|
+
cb_data = Array.new(size)
|
|
242
|
+
cr_data = Array.new(size)
|
|
243
|
+
|
|
244
|
+
height.times do |py|
|
|
245
|
+
row = py * width
|
|
246
|
+
width.times do |px|
|
|
247
|
+
pixel = source[px, py]
|
|
248
|
+
r = pixel.r; g = pixel.g; b = pixel.b
|
|
249
|
+
i = row + px
|
|
250
|
+
y_data[i] = ( 0.299 * r + 0.587 * g + 0.114 * b).round.clamp(0, 255)
|
|
251
|
+
cb_data[i] = (-0.168736 * r - 0.331264 * g + 0.5 * b + 128.0).round.clamp(0, 255)
|
|
252
|
+
cr_data[i] = ( 0.5 * r - 0.418688 * g - 0.081312 * b + 128.0).round.clamp(0, 255)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
[y_data, cb_data, cr_data]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def downsample(data, src_w, src_h, dst_w, dst_h)
|
|
260
|
+
out = Array.new(dst_w * dst_h)
|
|
261
|
+
max_x = src_w - 1
|
|
262
|
+
max_y = src_h - 1
|
|
263
|
+
dst_h.times do |dy|
|
|
264
|
+
sy = dy << 1
|
|
265
|
+
y1 = sy < max_y ? sy + 1 : max_y
|
|
266
|
+
row0 = sy * src_w
|
|
267
|
+
row1 = y1 * src_w
|
|
268
|
+
dst_row = dy * dst_w
|
|
269
|
+
dst_w.times do |dx|
|
|
270
|
+
sx = dx << 1
|
|
271
|
+
x1 = sx < max_x ? sx + 1 : max_x
|
|
272
|
+
out[dst_row + dx] = ((data[row0 + sx] + data[row0 + x1] +
|
|
273
|
+
data[row1 + sx] + data[row1 + x1]) >> 2)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
out
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Extract an 8x8 block into a pre-allocated array, level-shifted by -128.
|
|
280
|
+
def extract_block_into(channel, width, height, bx, by, block)
|
|
281
|
+
max_x = width - 1
|
|
282
|
+
max_y = height - 1
|
|
283
|
+
8.times do |row|
|
|
284
|
+
sy = by + row
|
|
285
|
+
sy = max_y if sy > max_y
|
|
286
|
+
src_row = sy * width
|
|
287
|
+
row8 = row << 3
|
|
288
|
+
8.times do |col|
|
|
289
|
+
sx = bx + col
|
|
290
|
+
sx = max_x if sx > max_x
|
|
291
|
+
block[row8 | col] = channel[src_row + sx] - 128.0
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
block
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PureJPEG
|
|
4
|
+
module Huffman
|
|
5
|
+
class DecodeTable
|
|
6
|
+
def initialize(bits, values)
|
|
7
|
+
@min_code = Array.new(17, 0)
|
|
8
|
+
@max_code = Array.new(17, -1)
|
|
9
|
+
@val_ptr = Array.new(17, 0)
|
|
10
|
+
@values = values
|
|
11
|
+
|
|
12
|
+
code = 0
|
|
13
|
+
k = 0
|
|
14
|
+
16.times do |i|
|
|
15
|
+
len = i + 1
|
|
16
|
+
@val_ptr[len] = k
|
|
17
|
+
if bits[i] > 0
|
|
18
|
+
@min_code[len] = code
|
|
19
|
+
code += bits[i]
|
|
20
|
+
@max_code[len] = code - 1
|
|
21
|
+
k += bits[i]
|
|
22
|
+
end
|
|
23
|
+
code <<= 1
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Decode one Huffman symbol from the bit reader.
|
|
28
|
+
def decode(reader)
|
|
29
|
+
code = 0
|
|
30
|
+
1.upto(16) do |len|
|
|
31
|
+
code = (code << 1) | reader.read_bit
|
|
32
|
+
if @max_code[len] >= 0 && code <= @max_code[len]
|
|
33
|
+
return @values[@val_ptr[len] + code - @min_code[len]]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
raise "Invalid Huffman code"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PureJPEG
|
|
4
|
+
module Huffman
|
|
5
|
+
class Encoder
|
|
6
|
+
def initialize(dc_table, ac_table)
|
|
7
|
+
@dc_table = dc_table
|
|
8
|
+
@ac_table = ac_table
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Encode a single 8x8 block (in zigzag order, quantized).
|
|
12
|
+
# `prev_dc` is the DC value of the previous block (for DPCM).
|
|
13
|
+
# Writes encoded bits to `writer` (a BitWriter).
|
|
14
|
+
# Returns the current block's DC value.
|
|
15
|
+
def encode_block(zigzag, prev_dc, writer)
|
|
16
|
+
dc = zigzag[0]
|
|
17
|
+
diff = dc - prev_dc
|
|
18
|
+
encode_dc(diff, writer)
|
|
19
|
+
encode_ac(zigzag, writer)
|
|
20
|
+
dc
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def encode_dc(diff, writer)
|
|
26
|
+
cat, bits = category_and_bits(diff)
|
|
27
|
+
code, length = @dc_table[cat]
|
|
28
|
+
writer.write_bits(code, length)
|
|
29
|
+
writer.write_bits(bits, cat) if cat > 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def encode_ac(zigzag, writer)
|
|
33
|
+
last_nonzero = 63
|
|
34
|
+
last_nonzero -= 1 while last_nonzero > 0 && zigzag[last_nonzero] == 0
|
|
35
|
+
|
|
36
|
+
if last_nonzero == 0 && zigzag[0] == zigzag[0] # AC starts at index 1
|
|
37
|
+
# All AC coefficients are zero
|
|
38
|
+
eob = @ac_table[0x00]
|
|
39
|
+
writer.write_bits(eob[0], eob[1])
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
i = 1
|
|
44
|
+
while i <= last_nonzero
|
|
45
|
+
run = 0
|
|
46
|
+
while i <= last_nonzero && zigzag[i] == 0
|
|
47
|
+
run += 1
|
|
48
|
+
i += 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Emit ZRL (16 zeros) symbols as needed
|
|
52
|
+
while run >= 16
|
|
53
|
+
zrl = @ac_table[0xF0]
|
|
54
|
+
writer.write_bits(zrl[0], zrl[1])
|
|
55
|
+
run -= 16
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
cat, bits = category_and_bits(zigzag[i])
|
|
59
|
+
symbol = (run << 4) | cat
|
|
60
|
+
code, length = @ac_table[symbol]
|
|
61
|
+
writer.write_bits(code, length)
|
|
62
|
+
writer.write_bits(bits, cat) if cat > 0
|
|
63
|
+
i += 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# EOB if we didn't reach position 63
|
|
67
|
+
if last_nonzero < 63
|
|
68
|
+
eob = @ac_table[0x00]
|
|
69
|
+
writer.write_bits(eob[0], eob[1])
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns [category, encoded_bits] for a coefficient value.
|
|
74
|
+
def category_and_bits(value)
|
|
75
|
+
return [0, 0] if value == 0
|
|
76
|
+
abs_val = value.abs
|
|
77
|
+
cat = 0
|
|
78
|
+
v = abs_val
|
|
79
|
+
while v > 0
|
|
80
|
+
cat += 1
|
|
81
|
+
v >>= 1
|
|
82
|
+
end
|
|
83
|
+
bits = value > 0 ? value : value + (1 << cat) - 1
|
|
84
|
+
[cat, bits]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PureJPEG
|
|
4
|
+
module Huffman
|
|
5
|
+
# Standard Huffman tables from JPEG Annex K.
|
|
6
|
+
# Each table is defined by bits (number of codes of each length 1-16)
|
|
7
|
+
# and values (the symbol for each code, in order).
|
|
8
|
+
|
|
9
|
+
DC_LUMINANCE_BITS = [0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0].freeze
|
|
10
|
+
DC_LUMINANCE_VALUES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].freeze
|
|
11
|
+
|
|
12
|
+
AC_LUMINANCE_BITS = [0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7D].freeze
|
|
13
|
+
AC_LUMINANCE_VALUES = [
|
|
14
|
+
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
|
|
15
|
+
0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
|
|
16
|
+
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08,
|
|
17
|
+
0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0,
|
|
18
|
+
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16,
|
|
19
|
+
0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
|
|
20
|
+
0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
|
|
21
|
+
0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
|
|
22
|
+
0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
|
|
23
|
+
0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
|
|
24
|
+
0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
|
|
25
|
+
0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
|
|
26
|
+
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
|
|
27
|
+
0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
|
|
28
|
+
0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6,
|
|
29
|
+
0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
|
|
30
|
+
0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4,
|
|
31
|
+
0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
|
|
32
|
+
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA,
|
|
33
|
+
0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
|
|
34
|
+
0xF9, 0xFA
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
DC_CHROMINANCE_BITS = [0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0].freeze
|
|
38
|
+
DC_CHROMINANCE_VALUES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].freeze
|
|
39
|
+
|
|
40
|
+
AC_CHROMINANCE_BITS = [0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77].freeze
|
|
41
|
+
AC_CHROMINANCE_VALUES = [
|
|
42
|
+
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,
|
|
43
|
+
0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
|
|
44
|
+
0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,
|
|
45
|
+
0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0,
|
|
46
|
+
0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34,
|
|
47
|
+
0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26,
|
|
48
|
+
0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38,
|
|
49
|
+
0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
|
|
50
|
+
0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
|
|
51
|
+
0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
|
|
52
|
+
0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
|
|
53
|
+
0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
|
|
54
|
+
0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96,
|
|
55
|
+
0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5,
|
|
56
|
+
0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4,
|
|
57
|
+
0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3,
|
|
58
|
+
0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2,
|
|
59
|
+
0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA,
|
|
60
|
+
0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9,
|
|
61
|
+
0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
|
|
62
|
+
0xF9, 0xFA
|
|
63
|
+
].freeze
|
|
64
|
+
|
|
65
|
+
# Build a lookup table: symbol -> [code, code_length]
|
|
66
|
+
# from the bits/values specification.
|
|
67
|
+
def self.build_table(bits, values)
|
|
68
|
+
table = {}
|
|
69
|
+
code = 0
|
|
70
|
+
k = 0
|
|
71
|
+
|
|
72
|
+
16.times do |i|
|
|
73
|
+
bits[i].times do
|
|
74
|
+
table[values[k]] = [code, i + 1]
|
|
75
|
+
k += 1
|
|
76
|
+
code += 1
|
|
77
|
+
end
|
|
78
|
+
code <<= 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
table
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PureJPEG
|
|
4
|
+
# A decoded JPEG image with pixel-level access.
|
|
5
|
+
#
|
|
6
|
+
# Implements the same pixel source interface (+width+, +height+, +[x, y]+)
|
|
7
|
+
# as encoder inputs, so a decoded image can be passed directly to
|
|
8
|
+
# {PureJPEG.encode} for re-encoding.
|
|
9
|
+
class Image
|
|
10
|
+
# @return [Integer] image width in pixels
|
|
11
|
+
attr_reader :width
|
|
12
|
+
# @return [Integer] image height in pixels
|
|
13
|
+
attr_reader :height
|
|
14
|
+
|
|
15
|
+
# @param width [Integer]
|
|
16
|
+
# @param height [Integer]
|
|
17
|
+
# @param pixels [Array<Source::Pixel>] flat row-major array of pixels
|
|
18
|
+
def initialize(width, height, pixels)
|
|
19
|
+
@width = width
|
|
20
|
+
@height = height
|
|
21
|
+
@pixels = pixels
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Retrieve a pixel by coordinate.
|
|
25
|
+
#
|
|
26
|
+
# @param x [Integer] column (0-based)
|
|
27
|
+
# @param y [Integer] row (0-based)
|
|
28
|
+
# @return [Source::Pixel] pixel with +.r+, +.g+, +.b+ in 0-255
|
|
29
|
+
def [](x, y)
|
|
30
|
+
@pixels[y * @width + x]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Set a pixel by coordinate.
|
|
34
|
+
#
|
|
35
|
+
# @param x [Integer] column (0-based)
|
|
36
|
+
# @param y [Integer] row (0-based)
|
|
37
|
+
# @param pixel [Source::Pixel] replacement pixel
|
|
38
|
+
# @return [Source::Pixel]
|
|
39
|
+
def []=(x, y, pixel)
|
|
40
|
+
@pixels[y * @width + x] = pixel
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Iterate over every pixel in the image.
|
|
44
|
+
#
|
|
45
|
+
# @yieldparam x [Integer] column
|
|
46
|
+
# @yieldparam y [Integer] row
|
|
47
|
+
# @yieldparam pixel [Source::Pixel] the pixel at (x, y)
|
|
48
|
+
# @return [void]
|
|
49
|
+
def each_pixel
|
|
50
|
+
@height.times do |y|
|
|
51
|
+
@width.times do |x|
|
|
52
|
+
yield x, y, self[x, y]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|