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.
@@ -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