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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f68662dc2cbe5f2e2c4feeb36180b4a8fbe8227f059afc490ffeb9a636c90288
|
|
4
|
+
data.tar.gz: 8cb72c6e9fcab068f71a1f0a4a42b05a43343b4b0f14b4cf768dc2217c31bcbd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 03aa144fae7b98ea9e1d921ea91d4ad699152ff83e30e2c106d229193077ede5a7fb7e37ce07e634d1beb7fd1b2fc9d07675bdb082be5f50b4fe63de42fcebf2
|
|
7
|
+
data.tar.gz: f0753fbe3a783224e2976f5c3d24ad814f950fb7a0d65c755bc741929cac703c05682c47baed9f030f6317af1513db016be5e4c69a9b4935aa21e8b6d4e28b28
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pura-webp 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,76 @@
|
|
|
1
|
+
# pura-webp
|
|
2
|
+
|
|
3
|
+
A pure Ruby WebP decoder and encoder with zero C extension dependencies.
|
|
4
|
+
|
|
5
|
+
Part of the **pura-*** series — pure Ruby image codec gems.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- VP8 lossy WebP decoding
|
|
10
|
+
- VP8L lossless WebP encoding
|
|
11
|
+
- No native extensions, no FFI, no external dependencies
|
|
12
|
+
- CLI tool included
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
gem install pura-webp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "pura-webp"
|
|
24
|
+
|
|
25
|
+
# Decode
|
|
26
|
+
image = Pura::Webp.decode("photo.webp")
|
|
27
|
+
image.width #=> 800
|
|
28
|
+
image.height #=> 600
|
|
29
|
+
image.pixels #=> Raw RGB byte string
|
|
30
|
+
image.pixel_at(0, 0) #=> [r, g, b]
|
|
31
|
+
|
|
32
|
+
# Encode (VP8L lossless)
|
|
33
|
+
Pura::Webp.encode(image, "output.webp")
|
|
34
|
+
|
|
35
|
+
# Resize
|
|
36
|
+
thumb = image.resize(200, 200)
|
|
37
|
+
Pura::Webp.encode(thumb, "thumb.webp")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Benchmark
|
|
41
|
+
|
|
42
|
+
Decode performance on a 400×400 WebP image, Ruby 4.0.2 + YJIT:
|
|
43
|
+
|
|
44
|
+
| Operation | pura-webp | ffmpeg (C + SIMD) | vs ffmpeg |
|
|
45
|
+
|-----------|-----------|-------------------|-----------|
|
|
46
|
+
| Decode | 207 ms | 66 ms | 3.1× slower |
|
|
47
|
+
|
|
48
|
+
## Why pure Ruby?
|
|
49
|
+
|
|
50
|
+
- **`gem install` and go** — no `brew install webp`, no `apt install libwebp-dev`
|
|
51
|
+
- **Works everywhere Ruby works** — CRuby, ruby.wasm, mruby, JRuby, TruffleRuby
|
|
52
|
+
- **Edge/Wasm ready** — browsers (ruby.wasm), sandboxed environments
|
|
53
|
+
- **No system library needed** — unlike every other Ruby WebP solution
|
|
54
|
+
|
|
55
|
+
## Current Limitations
|
|
56
|
+
|
|
57
|
+
- Decoder: VP8 lossy only (VP8L lossless and VP8X extended not yet decoded)
|
|
58
|
+
- Encoder: VP8L lossless format, works best with images up to ~200×200
|
|
59
|
+
- Loop filter not implemented in decoder (slight quality difference)
|
|
60
|
+
|
|
61
|
+
## Related Gems
|
|
62
|
+
|
|
63
|
+
| Gem | Format | Status |
|
|
64
|
+
|-----|--------|--------|
|
|
65
|
+
| [pura-jpeg](https://github.com/komagata/pura-jpeg) | JPEG | ✅ |
|
|
66
|
+
| [pura-png](https://github.com/komagata/pura-png) | PNG | ✅ |
|
|
67
|
+
| [pura-bmp](https://github.com/komagata/pura-bmp) | BMP | ✅ |
|
|
68
|
+
| [pura-gif](https://github.com/komagata/pura-gif) | GIF | ✅ |
|
|
69
|
+
| [pura-tiff](https://github.com/komagata/pura-tiff) | TIFF | ✅ |
|
|
70
|
+
| [pura-ico](https://github.com/komagata/pura-ico) | ICO | ✅ |
|
|
71
|
+
| **pura-webp** | **WebP** | ✅ |
|
|
72
|
+
| [pura-image](https://github.com/komagata/pura-image) | All formats | ✅ |
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pura
|
|
4
|
+
module Webp
|
|
5
|
+
class BoolDecoder
|
|
6
|
+
def initialize(data, offset = 0, size = nil)
|
|
7
|
+
@data = data
|
|
8
|
+
@pos = offset
|
|
9
|
+
@end_pos = size ? offset + size : data.bytesize
|
|
10
|
+
|
|
11
|
+
@range = 255
|
|
12
|
+
@value = 0
|
|
13
|
+
@bits_left = 0
|
|
14
|
+
|
|
15
|
+
load_initial
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def read_bool(prob)
|
|
19
|
+
split = 1 + (((@range - 1) * prob) >> 8)
|
|
20
|
+
big_split = split << @bits_left
|
|
21
|
+
|
|
22
|
+
if @value >= big_split
|
|
23
|
+
@range -= split
|
|
24
|
+
@value -= big_split
|
|
25
|
+
bit = 1
|
|
26
|
+
else
|
|
27
|
+
@range = split
|
|
28
|
+
bit = 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
normalize
|
|
32
|
+
bit
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def read_literal(n)
|
|
36
|
+
val = 0
|
|
37
|
+
n.times do
|
|
38
|
+
val = (val << 1) | read_bool(128)
|
|
39
|
+
end
|
|
40
|
+
val
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def read_flag
|
|
44
|
+
read_bool(128) == 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def read_signed(n)
|
|
48
|
+
val = read_literal(n)
|
|
49
|
+
read_flag ? -val : val
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def read_tree(tree, probs)
|
|
53
|
+
idx = 0
|
|
54
|
+
loop do
|
|
55
|
+
idx += read_bool(probs[idx >> 1])
|
|
56
|
+
val = tree[idx]
|
|
57
|
+
return val if val <= 0
|
|
58
|
+
|
|
59
|
+
idx = val
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def load_initial
|
|
66
|
+
# Load enough bytes to fill value register
|
|
67
|
+
4.times do
|
|
68
|
+
if @pos < @end_pos
|
|
69
|
+
@value = (@value << 8) | @data.getbyte(@pos)
|
|
70
|
+
@pos += 1
|
|
71
|
+
else
|
|
72
|
+
@value <<= 8
|
|
73
|
+
end
|
|
74
|
+
@bits_left += 8
|
|
75
|
+
end
|
|
76
|
+
@bits_left -= 8 # We work with bits_left relative to range position
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def normalize
|
|
80
|
+
while @range < 128
|
|
81
|
+
@range <<= 1
|
|
82
|
+
@bits_left -= 1
|
|
83
|
+
|
|
84
|
+
next unless @bits_left.negative?
|
|
85
|
+
|
|
86
|
+
@bits_left += 8
|
|
87
|
+
if @pos < @end_pos
|
|
88
|
+
@value = (@value << 8) | @data.getbyte(@pos)
|
|
89
|
+
@pos += 1
|
|
90
|
+
else
|
|
91
|
+
@value <<= 8
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pura
|
|
4
|
+
module Webp
|
|
5
|
+
class DecodeError < StandardError; end
|
|
6
|
+
|
|
7
|
+
class Decoder
|
|
8
|
+
def self.decode(input)
|
|
9
|
+
data = if input.is_a?(String) && !input.include?("\0") && input.length < 4096 && File.exist?(input)
|
|
10
|
+
File.binread(input)
|
|
11
|
+
else
|
|
12
|
+
input
|
|
13
|
+
end
|
|
14
|
+
new(data).decode
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(data)
|
|
18
|
+
@data = data.b
|
|
19
|
+
@pos = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def decode
|
|
23
|
+
parse_riff
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# ---- Phase 0: RIFF Container ----
|
|
29
|
+
|
|
30
|
+
def parse_riff
|
|
31
|
+
# RIFF header
|
|
32
|
+
riff = read_bytes(4)
|
|
33
|
+
raise DecodeError, "not a RIFF file" unless riff == "RIFF"
|
|
34
|
+
|
|
35
|
+
read_u32_le
|
|
36
|
+
webp = read_bytes(4)
|
|
37
|
+
raise DecodeError, "not a WebP file" unless webp == "WEBP"
|
|
38
|
+
|
|
39
|
+
# Read first chunk
|
|
40
|
+
chunk_fourcc = read_bytes(4)
|
|
41
|
+
chunk_size = read_u32_le
|
|
42
|
+
|
|
43
|
+
case chunk_fourcc
|
|
44
|
+
when "VP8 "
|
|
45
|
+
decode_vp8_lossy(chunk_size)
|
|
46
|
+
when "VP8L"
|
|
47
|
+
raise DecodeError, "VP8L (lossless WebP) is not yet supported"
|
|
48
|
+
when "VP8X"
|
|
49
|
+
raise DecodeError, "VP8X (extended WebP) is not yet supported"
|
|
50
|
+
else
|
|
51
|
+
raise DecodeError, "unknown WebP chunk: #{chunk_fourcc.inspect}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# ---- Phase 1: VP8 Frame Header ----
|
|
56
|
+
|
|
57
|
+
def decode_vp8_lossy(chunk_size)
|
|
58
|
+
chunk_start = @pos
|
|
59
|
+
|
|
60
|
+
# Frame tag (3 bytes, little-endian 24-bit)
|
|
61
|
+
b0 = read_u8
|
|
62
|
+
b1 = read_u8
|
|
63
|
+
b2 = read_u8
|
|
64
|
+
frame_tag = b0 | (b1 << 8) | (b2 << 16)
|
|
65
|
+
|
|
66
|
+
keyframe = frame_tag.nobits?(0x01) # 0 = keyframe
|
|
67
|
+
(frame_tag >> 1) & 0x07
|
|
68
|
+
(frame_tag >> 4) & 0x01
|
|
69
|
+
first_part_size = (frame_tag >> 5) & 0x7FFFF
|
|
70
|
+
|
|
71
|
+
raise DecodeError, "not a keyframe" unless keyframe
|
|
72
|
+
|
|
73
|
+
# Keyframe header: start code (3 bytes) + size info (7 bytes)
|
|
74
|
+
sc0 = read_u8
|
|
75
|
+
sc1 = read_u8
|
|
76
|
+
sc2 = read_u8
|
|
77
|
+
raise DecodeError, "invalid VP8 start code" unless sc0 == 0x9D && sc1 == 0x01 && sc2 == 0x2A
|
|
78
|
+
|
|
79
|
+
# Width and height (16-bit LE each, with scale in upper 2 bits)
|
|
80
|
+
size0 = read_u16_le
|
|
81
|
+
size1 = read_u16_le
|
|
82
|
+
|
|
83
|
+
width = size0 & 0x3FFF
|
|
84
|
+
size0 >> 14
|
|
85
|
+
height = size1 & 0x3FFF
|
|
86
|
+
size1 >> 14
|
|
87
|
+
|
|
88
|
+
# First partition starts after the 10-byte keyframe header
|
|
89
|
+
first_part_data = @data.byteslice(chunk_start + 3, first_part_size)
|
|
90
|
+
|
|
91
|
+
# Token (coefficient) data follows the first partition
|
|
92
|
+
token_offset = chunk_start + 3 + first_part_size
|
|
93
|
+
token_data = @data.byteslice(token_offset, chunk_size - 3 - first_part_size)
|
|
94
|
+
|
|
95
|
+
# Phase 3: Parse frame header using boolean decoder
|
|
96
|
+
bd = BoolDecoder.new(first_part_data)
|
|
97
|
+
|
|
98
|
+
bd.read_flag # 0=YCbCr
|
|
99
|
+
bd.read_flag # clamping type
|
|
100
|
+
|
|
101
|
+
# Segmentation
|
|
102
|
+
segmentation_enabled = bd.read_flag
|
|
103
|
+
if segmentation_enabled
|
|
104
|
+
update_mb_segmentation_map = bd.read_flag
|
|
105
|
+
update_segment_feature_data = bd.read_flag
|
|
106
|
+
if update_segment_feature_data
|
|
107
|
+
bd.read_flag # segment_feature_mode (absolute/delta)
|
|
108
|
+
4.times do
|
|
109
|
+
if bd.read_flag
|
|
110
|
+
bd.read_signed(7) # quantizer update
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
4.times do
|
|
114
|
+
if bd.read_flag
|
|
115
|
+
bd.read_signed(6) # loop filter update
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
if update_mb_segmentation_map
|
|
120
|
+
3.times do
|
|
121
|
+
if bd.read_flag
|
|
122
|
+
bd.read_literal(8) # segment prob
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Loop filter
|
|
129
|
+
bd.read_flag # 0=simple, 1=normal
|
|
130
|
+
bd.read_literal(6)
|
|
131
|
+
bd.read_literal(3)
|
|
132
|
+
loop_filter_adj_enable = bd.read_flag
|
|
133
|
+
if loop_filter_adj_enable && bd.read_flag # mode_ref_lf_delta_update
|
|
134
|
+
4.times { bd.read_signed(6) if bd.read_flag } # ref_lf_deltas
|
|
135
|
+
4.times { bd.read_signed(6) if bd.read_flag } # mode_lf_deltas
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Partitions
|
|
139
|
+
num_log2_partitions = bd.read_literal(2)
|
|
140
|
+
1 << num_log2_partitions
|
|
141
|
+
|
|
142
|
+
# Quantization
|
|
143
|
+
y_ac_qi = bd.read_literal(7)
|
|
144
|
+
y_dc_delta = bd.read_flag ? bd.read_signed(4) : 0
|
|
145
|
+
y2_dc_delta = bd.read_flag ? bd.read_signed(4) : 0
|
|
146
|
+
y2_ac_delta = bd.read_flag ? bd.read_signed(4) : 0
|
|
147
|
+
uv_dc_delta = bd.read_flag ? bd.read_signed(4) : 0
|
|
148
|
+
uv_ac_delta = bd.read_flag ? bd.read_signed(4) : 0
|
|
149
|
+
|
|
150
|
+
# Build dequant factors
|
|
151
|
+
@dequant = build_dequant(y_ac_qi, y_dc_delta, y2_dc_delta, y2_ac_delta, uv_dc_delta, uv_ac_delta)
|
|
152
|
+
|
|
153
|
+
# Refresh entropy probs
|
|
154
|
+
bd.read_flag
|
|
155
|
+
|
|
156
|
+
# Token probability updates
|
|
157
|
+
@coeff_probs = VP8Tables.default_coeff_probs
|
|
158
|
+
parse_token_prob_updates(bd)
|
|
159
|
+
|
|
160
|
+
# Skip MB no-coeff skip flag
|
|
161
|
+
mb_no_skip_coeff = bd.read_flag
|
|
162
|
+
prob_skip_false = mb_no_skip_coeff ? bd.read_literal(8) : 0
|
|
163
|
+
|
|
164
|
+
# Phase 4-5: Decode macroblocks
|
|
165
|
+
mb_cols = (width + 15) / 16
|
|
166
|
+
mb_rows = (height + 15) / 16
|
|
167
|
+
|
|
168
|
+
# Allocate YUV buffers
|
|
169
|
+
y_stride = mb_cols * 16
|
|
170
|
+
uv_stride = mb_cols * 8
|
|
171
|
+
y_buf = Array.new(mb_rows * 16 * y_stride, 128)
|
|
172
|
+
u_buf = Array.new(mb_rows * 8 * uv_stride, 128)
|
|
173
|
+
v_buf = Array.new(mb_rows * 8 * uv_stride, 128)
|
|
174
|
+
|
|
175
|
+
# Token decoder for coefficient data
|
|
176
|
+
tbd = BoolDecoder.new(token_data)
|
|
177
|
+
|
|
178
|
+
mb_rows.times do |mb_row|
|
|
179
|
+
mb_cols.times do |mb_col|
|
|
180
|
+
# Skip flag
|
|
181
|
+
skip = mb_no_skip_coeff ? (bd.read_bool(prob_skip_false) == 1) : false
|
|
182
|
+
|
|
183
|
+
# Intra prediction mode for Y (keyframe)
|
|
184
|
+
y_mode = read_kf_y_mode(bd)
|
|
185
|
+
|
|
186
|
+
# UV mode
|
|
187
|
+
uv_mode = read_kf_uv_mode(bd)
|
|
188
|
+
|
|
189
|
+
next if skip
|
|
190
|
+
|
|
191
|
+
# Decode coefficients and reconstruct
|
|
192
|
+
decode_macroblock(tbd, mb_row, mb_col, y_mode, uv_mode,
|
|
193
|
+
y_buf, u_buf, v_buf, y_stride, uv_stride)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Phase 6: YUV to RGB
|
|
198
|
+
yuv_to_rgb(y_buf, u_buf, v_buf, width, height, y_stride, uv_stride)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# ---- VP8 Helpers ----
|
|
202
|
+
|
|
203
|
+
DC_PRED = 0
|
|
204
|
+
V_PRED = 1
|
|
205
|
+
H_PRED = 2
|
|
206
|
+
TM_PRED = 3
|
|
207
|
+
B_PRED = 4
|
|
208
|
+
|
|
209
|
+
KF_Y_MODE_PROBS = [145, 156, 163, 128].freeze
|
|
210
|
+
KF_UV_MODE_PROBS = [142, 114, 183].freeze
|
|
211
|
+
|
|
212
|
+
def read_kf_y_mode(bd)
|
|
213
|
+
if bd.read_bool(KF_Y_MODE_PROBS[0]).zero?
|
|
214
|
+
B_PRED
|
|
215
|
+
elsif bd.read_bool(KF_Y_MODE_PROBS[1]).zero?
|
|
216
|
+
DC_PRED
|
|
217
|
+
elsif bd.read_bool(KF_Y_MODE_PROBS[2]).zero?
|
|
218
|
+
V_PRED
|
|
219
|
+
elsif bd.read_bool(KF_Y_MODE_PROBS[3]).zero?
|
|
220
|
+
H_PRED
|
|
221
|
+
else
|
|
222
|
+
TM_PRED
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def read_kf_uv_mode(bd)
|
|
227
|
+
if bd.read_bool(KF_UV_MODE_PROBS[0]).zero?
|
|
228
|
+
DC_PRED
|
|
229
|
+
elsif bd.read_bool(KF_UV_MODE_PROBS[1]).zero?
|
|
230
|
+
V_PRED
|
|
231
|
+
elsif bd.read_bool(KF_UV_MODE_PROBS[2]).zero?
|
|
232
|
+
H_PRED
|
|
233
|
+
else
|
|
234
|
+
TM_PRED
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def build_dequant(y_ac_qi, y_dc_d, y2_dc_d, y2_ac_d, uv_dc_d, uv_ac_d)
|
|
239
|
+
dc_table = VP8Tables::DC_QUANT
|
|
240
|
+
ac_table = VP8Tables::AC_QUANT
|
|
241
|
+
qi = y_ac_qi.clamp(0, 127)
|
|
242
|
+
{
|
|
243
|
+
y_dc: dc_table[[qi + y_dc_d, 0].max.clamp(0, 127)],
|
|
244
|
+
y_ac: ac_table[qi],
|
|
245
|
+
y2_dc: dc_table[[qi + y2_dc_d, 0].max.clamp(0, 127)] * 2,
|
|
246
|
+
y2_ac: [ac_table[[qi + y2_ac_d, 0].max.clamp(0, 127)] * 155 / 100, 8].max,
|
|
247
|
+
uv_dc: dc_table[[qi + uv_dc_d, 0].max.clamp(0, 127)],
|
|
248
|
+
uv_ac: ac_table[[qi + uv_ac_d, 0].max.clamp(0, 127)]
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def parse_token_prob_updates(bd)
|
|
253
|
+
4.times do |i|
|
|
254
|
+
8.times do |j|
|
|
255
|
+
3.times do |k|
|
|
256
|
+
11.times do |l|
|
|
257
|
+
if bd.read_bool(VP8Tables::COEFF_UPDATE_PROBS[i][j][k][l]) == 1
|
|
258
|
+
@coeff_probs[i][j][k][l] = bd.read_literal(8)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def decode_macroblock(tbd, mb_row, mb_col, _y_mode, _uv_mode,
|
|
267
|
+
y_buf, u_buf, v_buf, y_stride, uv_stride)
|
|
268
|
+
# Decode Y blocks (4x4 grid = 16 blocks)
|
|
269
|
+
y_coeffs = Array.new(16) { decode_block_coeffs(tbd, 0) }
|
|
270
|
+
|
|
271
|
+
# Decode U blocks (2x2 = 4 blocks)
|
|
272
|
+
u_coeffs = Array.new(4) { decode_block_coeffs(tbd, 2) }
|
|
273
|
+
|
|
274
|
+
# Decode V blocks (2x2 = 4 blocks)
|
|
275
|
+
v_coeffs = Array.new(4) { decode_block_coeffs(tbd, 2) }
|
|
276
|
+
|
|
277
|
+
# Dequantize and inverse transform
|
|
278
|
+
y_pixels = y_coeffs.map { |c| dequant_and_idct(c, @dequant[:y_dc], @dequant[:y_ac]) }
|
|
279
|
+
u_pixels = u_coeffs.map { |c| dequant_and_idct(c, @dequant[:uv_dc], @dequant[:uv_ac]) }
|
|
280
|
+
v_pixels = v_coeffs.map { |c| dequant_and_idct(c, @dequant[:uv_dc], @dequant[:uv_ac]) }
|
|
281
|
+
|
|
282
|
+
# Apply prediction and write to buffers
|
|
283
|
+
base_y = mb_row * 16
|
|
284
|
+
base_x = mb_col * 16
|
|
285
|
+
|
|
286
|
+
# Simple DC prediction for Y (use 128 as predictor)
|
|
287
|
+
4.times do |by|
|
|
288
|
+
4.times do |bx|
|
|
289
|
+
block = y_pixels[(by * 4) + bx]
|
|
290
|
+
16.times do |i|
|
|
291
|
+
px = base_x + (bx * 4) + (i % 4)
|
|
292
|
+
py = base_y + (by * 4) + (i / 4)
|
|
293
|
+
y_buf[(py * y_stride) + px] = (128 + block[i]).clamp(0, 255)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# U/V
|
|
299
|
+
uv_base_y = mb_row * 8
|
|
300
|
+
uv_base_x = mb_col * 8
|
|
301
|
+
2.times do |by|
|
|
302
|
+
2.times do |bx|
|
|
303
|
+
u_block = u_pixels[(by * 2) + bx]
|
|
304
|
+
v_block = v_pixels[(by * 2) + bx]
|
|
305
|
+
16.times do |i|
|
|
306
|
+
px = uv_base_x + (bx * 4) + (i % 4)
|
|
307
|
+
py = uv_base_y + (by * 4) + (i / 4)
|
|
308
|
+
u_buf[(py * uv_stride) + px] = (128 + u_block[i]).clamp(0, 255)
|
|
309
|
+
v_buf[(py * uv_stride) + px] = (128 + v_block[i]).clamp(0, 255)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def decode_block_coeffs(tbd, plane)
|
|
316
|
+
coeffs = Array.new(16, 0)
|
|
317
|
+
i = 0
|
|
318
|
+
while i < 16
|
|
319
|
+
# Simplified: read token
|
|
320
|
+
ctx = i.zero? ? 0 : 1
|
|
321
|
+
band = i.positive? ? [i - 1, 7].min : 0
|
|
322
|
+
probs = @coeff_probs[plane.clamp(0, 3)][band][ctx]
|
|
323
|
+
|
|
324
|
+
# DCT_0 (zero token)?
|
|
325
|
+
if tbd.read_bool(probs[0]).zero?
|
|
326
|
+
# If first coeff and it's 0, check for EOB
|
|
327
|
+
if i.positive?
|
|
328
|
+
break # EOB
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
i += 1
|
|
332
|
+
next
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Non-zero token
|
|
336
|
+
coeffs[i] = if tbd.read_bool(probs[1]).zero?
|
|
337
|
+
# DCT_1
|
|
338
|
+
1
|
|
339
|
+
elsif tbd.read_bool(probs[2]).zero?
|
|
340
|
+
# DCT_2
|
|
341
|
+
2
|
|
342
|
+
elsif tbd.read_bool(probs[3]).zero?
|
|
343
|
+
# DCT_3
|
|
344
|
+
3
|
|
345
|
+
elsif tbd.read_bool(probs[4]).zero?
|
|
346
|
+
# DCT_4
|
|
347
|
+
4
|
|
348
|
+
else
|
|
349
|
+
# Larger value — simplified
|
|
350
|
+
5 + tbd.read_literal(3)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Sign bit
|
|
354
|
+
coeffs[i] = -coeffs[i] if tbd.read_bool(128) == 1
|
|
355
|
+
i += 1
|
|
356
|
+
end
|
|
357
|
+
coeffs
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def dequant_and_idct(coeffs, dc_q, ac_q)
|
|
361
|
+
# Dequantize
|
|
362
|
+
dq = Array.new(16, 0)
|
|
363
|
+
dq[0] = coeffs[0] * dc_q
|
|
364
|
+
(1...16).each { |i| dq[i] = coeffs[i] * ac_q }
|
|
365
|
+
|
|
366
|
+
# Simple 4x4 inverse DCT (simplified)
|
|
367
|
+
idct4x4(dq)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def idct4x4(input)
|
|
371
|
+
# Simplified 4x4 IDCT using direct computation
|
|
372
|
+
output = Array.new(16, 0)
|
|
373
|
+
|
|
374
|
+
# Row pass
|
|
375
|
+
temp = Array.new(16, 0)
|
|
376
|
+
4.times do |row|
|
|
377
|
+
a = input[(row * 4) + 0] + input[(row * 4) + 2]
|
|
378
|
+
b = input[(row * 4) + 0] - input[(row * 4) + 2]
|
|
379
|
+
c = ((input[(row * 4) + 1] * 35_468) >> 16) - ((input[(row * 4) + 3] * 85_627) >> 16)
|
|
380
|
+
d = ((input[(row * 4) + 1] * 85_627) >> 16) + ((input[(row * 4) + 3] * 35_468) >> 16)
|
|
381
|
+
|
|
382
|
+
temp[(row * 4) + 0] = a + d
|
|
383
|
+
temp[(row * 4) + 1] = b + c
|
|
384
|
+
temp[(row * 4) + 2] = b - c
|
|
385
|
+
temp[(row * 4) + 3] = a - d
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Column pass
|
|
389
|
+
4.times do |col|
|
|
390
|
+
a = temp[(0 * 4) + col] + temp[(2 * 4) + col]
|
|
391
|
+
b = temp[(0 * 4) + col] - temp[(2 * 4) + col]
|
|
392
|
+
c = ((temp[(1 * 4) + col] * 35_468) >> 16) - ((temp[(3 * 4) + col] * 85_627) >> 16)
|
|
393
|
+
d = ((temp[(1 * 4) + col] * 85_627) >> 16) + ((temp[(3 * 4) + col] * 35_468) >> 16)
|
|
394
|
+
|
|
395
|
+
output[(0 * 4) + col] = (a + d + 4) >> 3
|
|
396
|
+
output[(1 * 4) + col] = (b + c + 4) >> 3
|
|
397
|
+
output[(2 * 4) + col] = (b - c + 4) >> 3
|
|
398
|
+
output[(3 * 4) + col] = (a - d + 4) >> 3
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
output
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def yuv_to_rgb(y_buf, u_buf, v_buf, width, height, y_stride, uv_stride)
|
|
405
|
+
pixels = String.new(encoding: Encoding::BINARY, capacity: width * height * 3)
|
|
406
|
+
|
|
407
|
+
height.times do |row|
|
|
408
|
+
width.times do |col|
|
|
409
|
+
y = y_buf[(row * y_stride) + col]
|
|
410
|
+
u = u_buf[((row / 2) * uv_stride) + (col / 2)]
|
|
411
|
+
v = v_buf[((row / 2) * uv_stride) + (col / 2)]
|
|
412
|
+
|
|
413
|
+
c = y - 16
|
|
414
|
+
d = u - 128
|
|
415
|
+
e = v - 128
|
|
416
|
+
|
|
417
|
+
r = (((298 * c) + (409 * e) + 128) >> 8).clamp(0, 255)
|
|
418
|
+
g = (((298 * c) - (100 * d) - (208 * e) + 128) >> 8).clamp(0, 255)
|
|
419
|
+
b = (((298 * c) + (516 * d) + 128) >> 8).clamp(0, 255)
|
|
420
|
+
|
|
421
|
+
pixels << r.chr << g.chr << b.chr
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
Image.new(width, height, pixels)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# ---- Byte reading helpers ----
|
|
429
|
+
|
|
430
|
+
def read_u8
|
|
431
|
+
raise DecodeError, "unexpected end of data" if @pos >= @data.bytesize
|
|
432
|
+
|
|
433
|
+
val = @data.getbyte(@pos)
|
|
434
|
+
@pos += 1
|
|
435
|
+
val
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def read_u16_le
|
|
439
|
+
b0 = read_u8
|
|
440
|
+
b1 = read_u8
|
|
441
|
+
b0 | (b1 << 8)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def read_u32_le
|
|
445
|
+
b0 = read_u8
|
|
446
|
+
b1 = read_u8
|
|
447
|
+
b2 = read_u8
|
|
448
|
+
b3 = read_u8
|
|
449
|
+
b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def read_bytes(n)
|
|
453
|
+
raise DecodeError, "unexpected end of data" if @pos + n > @data.bytesize
|
|
454
|
+
|
|
455
|
+
result = @data.byteslice(@pos, n)
|
|
456
|
+
@pos += n
|
|
457
|
+
result
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|