pura-webp 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/LICENSE +21 -0
- data/README.md +76 -0
- data/lib/pura/webp/bool_decoder.rb +97 -0
- data/lib/pura/webp/decoder.rb +461 -0
- data/lib/pura/webp/encoder.rb +434 -0
- data/lib/pura/webp/image.rb +158 -0
- data/lib/pura/webp/version.rb +7 -0
- data/lib/pura/webp/vp8_tables.rb +495 -0
- data/lib/pura-webp.rb +20 -0
- metadata +79 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pura
|
|
4
|
+
module Webp
|
|
5
|
+
class Encoder
|
|
6
|
+
def self.encode(image, path, **_options)
|
|
7
|
+
encoder = new(image)
|
|
8
|
+
data = encoder.encode
|
|
9
|
+
File.binwrite(path, data)
|
|
10
|
+
data.bytesize
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(image)
|
|
14
|
+
@image = image
|
|
15
|
+
@width = image.width
|
|
16
|
+
@height = image.height
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def encode
|
|
20
|
+
vp8l_data = encode_vp8l
|
|
21
|
+
wrap_riff(vp8l_data)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def wrap_riff(vp8l_payload)
|
|
27
|
+
chunk = String.new(encoding: Encoding::BINARY)
|
|
28
|
+
chunk << "VP8L"
|
|
29
|
+
chunk << [vp8l_payload.bytesize].pack("V")
|
|
30
|
+
chunk << vp8l_payload
|
|
31
|
+
chunk << "\x00" if vp8l_payload.bytesize.odd?
|
|
32
|
+
|
|
33
|
+
riff = String.new(encoding: Encoding::BINARY)
|
|
34
|
+
riff << "RIFF"
|
|
35
|
+
riff << [4 + chunk.bytesize].pack("V")
|
|
36
|
+
riff << "WEBP"
|
|
37
|
+
riff << chunk
|
|
38
|
+
riff
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def encode_vp8l
|
|
42
|
+
bw = BitWriter.new
|
|
43
|
+
|
|
44
|
+
# VP8L signature
|
|
45
|
+
bw.write_bits(0x2F, 8)
|
|
46
|
+
|
|
47
|
+
# Image descriptor: width-1 (14 bits), height-1 (14 bits), alpha (1 bit), version (3 bits)
|
|
48
|
+
bw.write_bits(@width - 1, 14)
|
|
49
|
+
bw.write_bits(@height - 1, 14)
|
|
50
|
+
bw.write_bits(0, 1) # no alpha
|
|
51
|
+
bw.write_bits(0, 3) # version 0
|
|
52
|
+
|
|
53
|
+
# No transforms
|
|
54
|
+
bw.write_bits(0, 1)
|
|
55
|
+
|
|
56
|
+
# --- Image Data (Section 5) ---
|
|
57
|
+
|
|
58
|
+
# Color cache: not used
|
|
59
|
+
bw.write_bits(0, 1)
|
|
60
|
+
|
|
61
|
+
# Meta huffman: not used (main image is "recursive" so this bit is needed)
|
|
62
|
+
bw.write_bits(0, 1)
|
|
63
|
+
|
|
64
|
+
# Collect pixels
|
|
65
|
+
pixels = @image.pixels
|
|
66
|
+
num_pixels = @width * @height
|
|
67
|
+
|
|
68
|
+
greens = Array.new(num_pixels)
|
|
69
|
+
reds = Array.new(num_pixels)
|
|
70
|
+
blues = Array.new(num_pixels)
|
|
71
|
+
|
|
72
|
+
num_pixels.times do |i|
|
|
73
|
+
offset = i * 3
|
|
74
|
+
reds[i] = pixels.getbyte(offset)
|
|
75
|
+
greens[i] = pixels.getbyte(offset + 1)
|
|
76
|
+
blues[i] = pixels.getbyte(offset + 2)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Build histograms
|
|
80
|
+
green_hist = Array.new(280, 0)
|
|
81
|
+
red_hist = Array.new(256, 0)
|
|
82
|
+
blue_hist = Array.new(256, 0)
|
|
83
|
+
greens.each { |v| green_hist[v] += 1 }
|
|
84
|
+
reds.each { |v| red_hist[v] += 1 }
|
|
85
|
+
blues.each { |v| blue_hist[v] += 1 }
|
|
86
|
+
|
|
87
|
+
# Build huffman code lengths
|
|
88
|
+
green_lengths = build_huffman_lengths(green_hist, 280)
|
|
89
|
+
red_lengths = build_huffman_lengths(red_hist, 256)
|
|
90
|
+
blue_lengths = build_huffman_lengths(blue_hist, 256)
|
|
91
|
+
alpha_lengths = Array.new(256, 0)
|
|
92
|
+
alpha_lengths[255] = 1
|
|
93
|
+
dist_lengths = Array.new(40, 0)
|
|
94
|
+
|
|
95
|
+
# Write 5 huffman tables (all simple)
|
|
96
|
+
write_code_lengths(bw, green_lengths)
|
|
97
|
+
write_code_lengths(bw, red_lengths)
|
|
98
|
+
write_code_lengths(bw, blue_lengths)
|
|
99
|
+
write_code_lengths(bw, alpha_lengths)
|
|
100
|
+
write_code_lengths(bw, dist_lengths)
|
|
101
|
+
|
|
102
|
+
# Build actual codes
|
|
103
|
+
green_codes = canonical_codes(green_lengths)
|
|
104
|
+
red_codes = canonical_codes(red_lengths)
|
|
105
|
+
blue_codes = canonical_codes(blue_lengths)
|
|
106
|
+
alpha_codes = canonical_codes(alpha_lengths)
|
|
107
|
+
|
|
108
|
+
# Encode pixels
|
|
109
|
+
num_pixels.times do |i|
|
|
110
|
+
emit_code(bw, green_codes, greens[i])
|
|
111
|
+
emit_code(bw, red_codes, reds[i])
|
|
112
|
+
emit_code(bw, blue_codes, blues[i])
|
|
113
|
+
emit_code(bw, alpha_codes, 255)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
bw.finish
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Quantize channel to 2 most frequent values
|
|
120
|
+
def quantize_channel(values, uniq)
|
|
121
|
+
# Find 2 most frequent
|
|
122
|
+
freq = Hash.new(0)
|
|
123
|
+
values.each { |v| freq[v] += 1 }
|
|
124
|
+
top2 = freq.sort_by { |_, c| -c }.first(2).map(&:first).sort
|
|
125
|
+
|
|
126
|
+
values.map do |v|
|
|
127
|
+
# Map to nearest of top2
|
|
128
|
+
if (v - top2[0]).abs <= (v - top2[1]).abs
|
|
129
|
+
top2[0]
|
|
130
|
+
else
|
|
131
|
+
top2[1]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Build simple code lengths (1 or 2 symbols only)
|
|
137
|
+
def simple_lengths(uniq_values, max_symbols)
|
|
138
|
+
lengths = Array.new(max_symbols, 0)
|
|
139
|
+
if uniq_values.size == 1
|
|
140
|
+
lengths[uniq_values[0]] = 1
|
|
141
|
+
elsif uniq_values.size == 2
|
|
142
|
+
lengths[uniq_values[0]] = 1
|
|
143
|
+
lengths[uniq_values[1]] = 1
|
|
144
|
+
end
|
|
145
|
+
lengths
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Build huffman code lengths from histogram
|
|
149
|
+
def build_huffman_lengths(hist, max_symbols)
|
|
150
|
+
non_zero = []
|
|
151
|
+
hist.each_with_index { |c, s| non_zero << [c, s] if c > 0 }
|
|
152
|
+
|
|
153
|
+
lengths = Array.new(max_symbols, 0)
|
|
154
|
+
|
|
155
|
+
if non_zero.empty?
|
|
156
|
+
return lengths
|
|
157
|
+
elsif non_zero.size == 1
|
|
158
|
+
lengths[non_zero[0][1]] = 1
|
|
159
|
+
return lengths
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Build huffman tree
|
|
163
|
+
nodes = non_zero.sort_by { |c, _| c }.map { |c, s| { count: c, sym: s } }
|
|
164
|
+
|
|
165
|
+
while nodes.size > 1
|
|
166
|
+
a = nodes.shift
|
|
167
|
+
b = nodes.shift
|
|
168
|
+
parent = { count: a[:count] + b[:count], sym: nil, left: a, right: b }
|
|
169
|
+
idx = nodes.bsearch_index { |n| n[:count] >= parent[:count] } || nodes.size
|
|
170
|
+
nodes.insert(idx, parent)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Extract lengths
|
|
174
|
+
assign_depth(nodes[0], 0, lengths)
|
|
175
|
+
|
|
176
|
+
# Enforce max length (15 for data, 7 for CL codes)
|
|
177
|
+
enforce_max_length(lengths, 15)
|
|
178
|
+
lengths
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def assign_depth(node, depth, lengths)
|
|
182
|
+
if node[:left].nil?
|
|
183
|
+
lengths[node[:sym]] = [depth, 1].max
|
|
184
|
+
else
|
|
185
|
+
assign_depth(node[:left], depth + 1, lengths)
|
|
186
|
+
assign_depth(node[:right], depth + 1, lengths)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Enforce max code length while maintaining valid prefix code
|
|
191
|
+
def enforce_max_length(lengths, max_len)
|
|
192
|
+
return if lengths.max.to_i <= max_len
|
|
193
|
+
|
|
194
|
+
# Collect non-zero lengths with symbols
|
|
195
|
+
syms = []
|
|
196
|
+
lengths.each_with_index { |l, s| syms << [l, s] if l > 0 }
|
|
197
|
+
return if syms.empty?
|
|
198
|
+
|
|
199
|
+
# Cap all lengths
|
|
200
|
+
syms.each { |pair| pair[0] = max_len if pair[0] > max_len }
|
|
201
|
+
|
|
202
|
+
# Verify Kraft inequality: sum of 2^(-l) <= 1
|
|
203
|
+
# Multiply by 2^max_len: sum of 2^(max_len - l) <= 2^max_len
|
|
204
|
+
kraft_limit = 1 << max_len
|
|
205
|
+
loop do
|
|
206
|
+
kraft_sum = syms.sum { |l, _| 1 << (max_len - l) }
|
|
207
|
+
break if kraft_sum <= kraft_limit
|
|
208
|
+
|
|
209
|
+
# Find the longest code and shorten it
|
|
210
|
+
syms.sort_by! { |l, _| -l }
|
|
211
|
+
# Take from longest, give to shorter
|
|
212
|
+
syms[0][0] -= 1 if syms[0][0] > 1
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Write back
|
|
216
|
+
lengths.fill(0)
|
|
217
|
+
syms.each { |l, s| lengths[s] = l }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Build canonical huffman codes from lengths
|
|
221
|
+
def canonical_codes(lengths)
|
|
222
|
+
max_len = lengths.max || 0
|
|
223
|
+
return {} if max_len == 0
|
|
224
|
+
|
|
225
|
+
bl_count = Array.new(max_len + 1, 0)
|
|
226
|
+
lengths.each { |l| bl_count[l] += 1 if l > 0 }
|
|
227
|
+
|
|
228
|
+
next_code = Array.new(max_len + 1, 0)
|
|
229
|
+
code = 0
|
|
230
|
+
1.upto(max_len) do |bits|
|
|
231
|
+
code = (code + bl_count[bits - 1]) << 1
|
|
232
|
+
next_code[bits] = code
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
codes = {}
|
|
236
|
+
lengths.each_with_index do |len, sym|
|
|
237
|
+
next if len == 0
|
|
238
|
+
|
|
239
|
+
codes[sym] = [next_code[len], len]
|
|
240
|
+
next_code[len] += 1
|
|
241
|
+
end
|
|
242
|
+
codes
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def emit_code(bw, codes, sym)
|
|
246
|
+
if codes.size <= 1
|
|
247
|
+
# Single-symbol simple code: 0 bits needed
|
|
248
|
+
return
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
entry = codes[sym]
|
|
252
|
+
return unless entry
|
|
253
|
+
|
|
254
|
+
code, len = entry
|
|
255
|
+
return if len == 0 # singleton in normal huffman
|
|
256
|
+
|
|
257
|
+
# VP8L: huffman codes written MSB first
|
|
258
|
+
len.times do |i|
|
|
259
|
+
bw.write_bits((code >> (len - 1 - i)) & 1, 1)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Write code lengths to bitstream using the VP8L format
|
|
264
|
+
def write_code_lengths(bw, lengths)
|
|
265
|
+
# Find how many symbols we actually have
|
|
266
|
+
non_zero_count = lengths.count { |l| l > 0 }
|
|
267
|
+
non_zero_syms = []
|
|
268
|
+
lengths.each_with_index { |l, s| non_zero_syms << s if l > 0 }
|
|
269
|
+
|
|
270
|
+
if non_zero_count == 0
|
|
271
|
+
# Write simple code with 1 symbol (symbol 0)
|
|
272
|
+
bw.write_bits(1, 1) # is_simple
|
|
273
|
+
bw.write_bits(0, 1) # num_symbols - 1 = 0
|
|
274
|
+
bw.write_bits(0, 1) # is_first_8bit = false (1-bit symbol)
|
|
275
|
+
bw.write_bits(0, 1) # symbol = 0
|
|
276
|
+
elsif non_zero_count == 1
|
|
277
|
+
sym = non_zero_syms[0]
|
|
278
|
+
bw.write_bits(1, 1) # is_simple
|
|
279
|
+
bw.write_bits(0, 1) # num_symbols - 1 = 0
|
|
280
|
+
if sym < 2
|
|
281
|
+
bw.write_bits(0, 1) # 1-bit symbol
|
|
282
|
+
bw.write_bits(sym, 1)
|
|
283
|
+
else
|
|
284
|
+
bw.write_bits(1, 1) # 8-bit symbol
|
|
285
|
+
bw.write_bits(sym, 8)
|
|
286
|
+
end
|
|
287
|
+
elsif non_zero_count == 2
|
|
288
|
+
bw.write_bits(1, 1) # is_simple
|
|
289
|
+
bw.write_bits(1, 1) # num_symbols - 1 = 1 (2 symbols)
|
|
290
|
+
s0 = non_zero_syms[0]
|
|
291
|
+
s1 = non_zero_syms[1]
|
|
292
|
+
if s0 < 2
|
|
293
|
+
bw.write_bits(0, 1) # 1-bit
|
|
294
|
+
bw.write_bits(s0, 1)
|
|
295
|
+
else
|
|
296
|
+
bw.write_bits(1, 1) # 8-bit
|
|
297
|
+
bw.write_bits(s0, 8)
|
|
298
|
+
end
|
|
299
|
+
bw.write_bits(s1, 8)
|
|
300
|
+
else
|
|
301
|
+
write_normal_code_lengths(bw, lengths)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def write_normal_code_lengths(bw, lengths)
|
|
306
|
+
bw.write_bits(0, 1) # is_simple = false
|
|
307
|
+
|
|
308
|
+
# Must cover all symbols in the alphabet (lengths.size)
|
|
309
|
+
# The decoder reads alphabet_size code lengths
|
|
310
|
+
num_symbols = lengths.size
|
|
311
|
+
|
|
312
|
+
# Code length alphabet: 0-15 literal lengths, 16=repeat, 17=zero run 3-10, 18=zero run 11-138
|
|
313
|
+
# VP8L code length code order
|
|
314
|
+
kCodeLengthCodeOrder = [17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
|
315
|
+
|
|
316
|
+
# RLE encode the code lengths
|
|
317
|
+
rle = rle_encode(lengths, num_symbols)
|
|
318
|
+
|
|
319
|
+
# Build histogram of RLE symbols
|
|
320
|
+
cl_hist = Array.new(19, 0)
|
|
321
|
+
rle.each { |sym, _, _| cl_hist[sym] += 1 }
|
|
322
|
+
|
|
323
|
+
# Build code lengths for code length alphabet
|
|
324
|
+
cl_lengths = build_huffman_lengths(cl_hist, 19)
|
|
325
|
+
# Code length codes max length is 7 (stored in 3 bits)
|
|
326
|
+
enforce_max_length(cl_lengths, 7)
|
|
327
|
+
|
|
328
|
+
# Determine num_code_length_codes (at least 4)
|
|
329
|
+
num_cl = 4
|
|
330
|
+
kCodeLengthCodeOrder.each_with_index do |order_idx, i|
|
|
331
|
+
num_cl = i + 1 if cl_lengths[order_idx] > 0
|
|
332
|
+
end
|
|
333
|
+
num_cl = [num_cl, 4].max
|
|
334
|
+
|
|
335
|
+
bw.write_bits(num_cl - 4, 4)
|
|
336
|
+
|
|
337
|
+
# Write code length code lengths
|
|
338
|
+
num_cl.times do |i|
|
|
339
|
+
bw.write_bits(cl_lengths[kCodeLengthCodeOrder[i]], 3)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Build codes for code length symbols
|
|
343
|
+
cl_codes = canonical_codes(cl_lengths)
|
|
344
|
+
|
|
345
|
+
# max_symbol flag: 0 = use default max_symbol
|
|
346
|
+
bw.write_bits(0, 1)
|
|
347
|
+
|
|
348
|
+
# Write the RLE-encoded code lengths
|
|
349
|
+
rle.each do |sym, extra_bits, extra_val|
|
|
350
|
+
emit_code(bw, cl_codes, sym)
|
|
351
|
+
case sym
|
|
352
|
+
when 16 then bw.write_bits(extra_val, 2)
|
|
353
|
+
when 17 then bw.write_bits(extra_val, 3)
|
|
354
|
+
when 18 then bw.write_bits(extra_val, 7)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def rle_encode(lengths, num_symbols)
|
|
360
|
+
result = []
|
|
361
|
+
i = 0
|
|
362
|
+
while i < num_symbols
|
|
363
|
+
val = lengths[i]
|
|
364
|
+
if val == 0
|
|
365
|
+
run = 0
|
|
366
|
+
run += 1 while i + run < num_symbols && lengths[i + run] == 0
|
|
367
|
+
i += run
|
|
368
|
+
while run > 0
|
|
369
|
+
if run >= 11
|
|
370
|
+
extra = [run - 11, 127].min
|
|
371
|
+
result << [18, 7, extra]
|
|
372
|
+
run -= 11 + extra
|
|
373
|
+
elsif run >= 3
|
|
374
|
+
extra = run - 3
|
|
375
|
+
result << [17, 3, extra]
|
|
376
|
+
run -= 3 + extra
|
|
377
|
+
else
|
|
378
|
+
result << [0, 0, 0]
|
|
379
|
+
run -= 1
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
else
|
|
383
|
+
result << [val, 0, 0]
|
|
384
|
+
i += 1
|
|
385
|
+
# Count repeats of same value
|
|
386
|
+
total_run = 0
|
|
387
|
+
total_run += 1 while i + total_run < num_symbols && lengths[i + total_run] == val
|
|
388
|
+
remaining = total_run
|
|
389
|
+
while remaining >= 3
|
|
390
|
+
extra = [remaining - 3, 3].min
|
|
391
|
+
result << [16, 2, extra]
|
|
392
|
+
remaining -= 3 + extra
|
|
393
|
+
end
|
|
394
|
+
remaining.times do
|
|
395
|
+
result << [val, 0, 0]
|
|
396
|
+
end
|
|
397
|
+
i += total_run
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
result
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Bit writer (LSB-first, VP8L format)
|
|
404
|
+
class BitWriter
|
|
405
|
+
def initialize
|
|
406
|
+
@data = String.new(encoding: Encoding::BINARY)
|
|
407
|
+
@current = 0
|
|
408
|
+
@bits = 0
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def write_bits(value, num_bits)
|
|
412
|
+
num_bits.times do |i|
|
|
413
|
+
@current |= ((value >> i) & 1) << @bits
|
|
414
|
+
@bits += 1
|
|
415
|
+
flush_byte if @bits == 8
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def finish
|
|
420
|
+
@data << (@current & 0xFF).chr if @bits > 0
|
|
421
|
+
@data
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
private
|
|
425
|
+
|
|
426
|
+
def flush_byte
|
|
427
|
+
@data << (@current & 0xFF).chr
|
|
428
|
+
@current = 0
|
|
429
|
+
@bits = 0
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pura
|
|
4
|
+
module Webp
|
|
5
|
+
class Image
|
|
6
|
+
attr_reader :width, :height, :pixels
|
|
7
|
+
|
|
8
|
+
def initialize(width, height, pixels)
|
|
9
|
+
@width = width
|
|
10
|
+
@height = height
|
|
11
|
+
@pixels = pixels.b
|
|
12
|
+
expected = width * height * 3
|
|
13
|
+
return if @pixels.bytesize == expected
|
|
14
|
+
|
|
15
|
+
raise ArgumentError, "pixel data size #{@pixels.bytesize} != expected #{expected} (#{width}x#{height}x3)"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_rgb_array
|
|
19
|
+
result = Array.new(width * height)
|
|
20
|
+
i = 0
|
|
21
|
+
offset = 0
|
|
22
|
+
while offset < @pixels.bytesize
|
|
23
|
+
result[i] = [@pixels.getbyte(offset), @pixels.getbyte(offset + 1), @pixels.getbyte(offset + 2)]
|
|
24
|
+
i += 1
|
|
25
|
+
offset += 3
|
|
26
|
+
end
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def pixel_at(x, y)
|
|
31
|
+
raise IndexError, "coordinates out of bounds" if x.negative? || x >= @width || y.negative? || y >= @height
|
|
32
|
+
|
|
33
|
+
offset = ((y * @width) + x) * 3
|
|
34
|
+
[@pixels.getbyte(offset), @pixels.getbyte(offset + 1), @pixels.getbyte(offset + 2)]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_ppm
|
|
38
|
+
header = "P6\n#{@width} #{@height}\n255\n"
|
|
39
|
+
header.b + @pixels
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def resize(new_width, new_height, interpolation: :bilinear)
|
|
43
|
+
raise ArgumentError, "width must be positive" unless new_width.positive?
|
|
44
|
+
raise ArgumentError, "height must be positive" unless new_height.positive?
|
|
45
|
+
|
|
46
|
+
if interpolation == :nearest
|
|
47
|
+
resize_nearest(new_width, new_height)
|
|
48
|
+
else
|
|
49
|
+
resize_bilinear(new_width, new_height)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def resize_fit(max_width, max_height, interpolation: :bilinear)
|
|
54
|
+
raise ArgumentError, "max_width must be positive" unless max_width.positive?
|
|
55
|
+
raise ArgumentError, "max_height must be positive" unless max_height.positive?
|
|
56
|
+
|
|
57
|
+
scale = [max_width.to_f / @width, max_height.to_f / @height].min
|
|
58
|
+
scale = [scale, 1.0].min
|
|
59
|
+
new_width = (@width * scale).round
|
|
60
|
+
new_height = (@height * scale).round
|
|
61
|
+
new_width = 1 if new_width < 1
|
|
62
|
+
new_height = 1 if new_height < 1
|
|
63
|
+
resize(new_width, new_height, interpolation: interpolation)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def resize_fill(fill_width, fill_height, interpolation: :bilinear)
|
|
67
|
+
raise ArgumentError, "width must be positive" unless fill_width.positive?
|
|
68
|
+
raise ArgumentError, "height must be positive" unless fill_height.positive?
|
|
69
|
+
|
|
70
|
+
scale = [fill_width.to_f / @width, fill_height.to_f / @height].max
|
|
71
|
+
scaled_w = (@width * scale).round
|
|
72
|
+
scaled_h = (@height * scale).round
|
|
73
|
+
scaled_w = 1 if scaled_w < 1
|
|
74
|
+
scaled_h = 1 if scaled_h < 1
|
|
75
|
+
|
|
76
|
+
scaled = resize(scaled_w, scaled_h, interpolation: interpolation)
|
|
77
|
+
|
|
78
|
+
crop_x = (scaled_w - fill_width) / 2
|
|
79
|
+
crop_y = (scaled_h - fill_height) / 2
|
|
80
|
+
scaled.crop(crop_x, crop_y, fill_width, fill_height)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def crop(x, y, w, h)
|
|
84
|
+
out = String.new(encoding: Encoding::BINARY, capacity: w * h * 3)
|
|
85
|
+
h.times do |row|
|
|
86
|
+
src_offset = (((y + row) * @width) + x) * 3
|
|
87
|
+
out << @pixels.byteslice(src_offset, w * 3)
|
|
88
|
+
end
|
|
89
|
+
Image.new(w, h, out)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def resize_nearest(new_width, new_height)
|
|
95
|
+
out = String.new(encoding: Encoding::BINARY, capacity: new_width * new_height * 3)
|
|
96
|
+
x_ratio = @width.to_f / new_width
|
|
97
|
+
y_ratio = @height.to_f / new_height
|
|
98
|
+
|
|
99
|
+
new_height.times do |y|
|
|
100
|
+
src_y = (y * y_ratio).to_i
|
|
101
|
+
src_y = @height - 1 if src_y >= @height
|
|
102
|
+
new_width.times do |x|
|
|
103
|
+
src_x = (x * x_ratio).to_i
|
|
104
|
+
src_x = @width - 1 if src_x >= @width
|
|
105
|
+
offset = ((src_y * @width) + src_x) * 3
|
|
106
|
+
out << @pixels.byteslice(offset, 3)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
Image.new(new_width, new_height, out)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def resize_bilinear(new_width, new_height)
|
|
114
|
+
out = String.new(encoding: Encoding::BINARY, capacity: new_width * new_height * 3)
|
|
115
|
+
x_ratio = (@width - 1).to_f / [new_width - 1, 1].max
|
|
116
|
+
y_ratio = (@height - 1).to_f / [new_height - 1, 1].max
|
|
117
|
+
|
|
118
|
+
new_height.times do |y|
|
|
119
|
+
src_y = y * y_ratio
|
|
120
|
+
y0 = src_y.to_i
|
|
121
|
+
y1 = [y0 + 1, @height - 1].min
|
|
122
|
+
y_frac = src_y - y0
|
|
123
|
+
|
|
124
|
+
new_width.times do |x|
|
|
125
|
+
src_x = x * x_ratio
|
|
126
|
+
x0 = src_x.to_i
|
|
127
|
+
x1 = [x0 + 1, @width - 1].min
|
|
128
|
+
x_frac = src_x - x0
|
|
129
|
+
|
|
130
|
+
off00 = ((y0 * @width) + x0) * 3
|
|
131
|
+
off10 = ((y0 * @width) + x1) * 3
|
|
132
|
+
off01 = ((y1 * @width) + x0) * 3
|
|
133
|
+
off11 = ((y1 * @width) + x1) * 3
|
|
134
|
+
|
|
135
|
+
3.times do |c|
|
|
136
|
+
v00 = @pixels.getbyte(off00 + c)
|
|
137
|
+
v10 = @pixels.getbyte(off10 + c)
|
|
138
|
+
v01 = @pixels.getbyte(off01 + c)
|
|
139
|
+
v11 = @pixels.getbyte(off11 + c)
|
|
140
|
+
|
|
141
|
+
val = (v00 * (1 - x_frac) * (1 - y_frac)) +
|
|
142
|
+
(v10 * x_frac * (1 - y_frac)) +
|
|
143
|
+
(v01 * (1 - x_frac) * y_frac) +
|
|
144
|
+
(v11 * x_frac * y_frac)
|
|
145
|
+
|
|
146
|
+
val = val.round
|
|
147
|
+
val = 0 if val.negative?
|
|
148
|
+
val = 255 if val > 255
|
|
149
|
+
out << val.chr
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
Image.new(new_width, new_height, out)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|