jeopardy/webp.lua
2026-05-06 19:39:08 -07:00

1627 lines
62 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- webp.lua — Pure Lua WebP decoder for Love2D (LuaJIT)
-- Supports: VP8 (lossy), VP8L (lossless), VP8X (extended container)
-- Usage: local WebP = require("webp"); local img = WebP.load("sprite.webp")
local WebP = {}
local bit = require("bit")
local band = bit.band
local bor = bit.bor
local lshift = bit.lshift
local rshift = bit.rshift
local tobit = bit.tobit
-- ─── Bitstream reader ────────────────────────────────────────────────────────
-- VP8L is LSB-first. We maintain a 32-bit window since LuaJIT bit ops are 32-bit.
-- ─── Bitstream reader (unchanged, still LSBfirst) ───────────────────────────
local function newBitReader(data)
local r = {
data = data,
pos = 1,
window = 0,
bits = 0,
}
function r:fill()
while self.bits <= 24 and self.pos <= #self.data do
local byte = self.data:byte(self.pos)
self.window = bor(self.window, lshift(byte, self.bits))
self.bits = self.bits + 8
self.pos = self.pos + 1
end
end
function r:read(n)
if self.bits < n then self:fill() end
local v = band(self.window, lshift(1, n) - 1)
self.window = rshift(self.window, n)
self.bits = self.bits - n
return v
end
function r:readBool()
return self:read(1) == 1
end
r:fill()
return r
end
-- ─── Helper: reverse bits for LSBfirst Huffman codes ───────────────────────
local function reverseBits(code, len)
local r = 0
for _ = 1, len do
r = lshift(r, 1) + band(code, 1)
code = rshift(code, 1)
end
return r
end
-- ─── Huffman trees (fixed for LSBfirst) ────────────────────────────────────
local function buildHuffmanTable(codeLengths)
local n = #codeLengths
local counts = {}
for i = 0, 15 do counts[i] = 0 end
for _, cl in ipairs(codeLengths) do
if cl > 0 then counts[cl] = counts[cl] + 1 end
end
local nextCode = {}
local code = 0
counts[0] = 0
for bits = 1, 15 do
code = lshift(code + counts[bits - 1], 1)
nextCode[bits] = code
end
local htable = {}
for i = 1, n do
local len = codeLengths[i]
if len > 0 then
local c = nextCode[len]
-- IMPORTANT: reverse bits because we read LSBfirst
local rev = reverseBits(c, len)
htable[rev] = { sym = i - 1, len = len }
nextCode[len] = c + 1
end
end
return htable
end
local function decodeHuffman(br, htable)
local code = 0
for len = 1, 15 do
-- we read one bit at a time, LSBfirst
local bit = br:read(1)
code = bor(code, lshift(bit, len - 1))
local entry = htable[code]
if entry and entry.len == len then
return entry.sym
end
end
error("Invalid Huffman code")
end
-- ─── Code length decoding ────────────────────────────────────────────────────
local CODE_LENGTH_ORDER = {17,18,0,1,2,3,4,5,16,6,7,8,9,10,11,12,13,14,15}
local function readCodeLengths(br, n)
local numCLCodes = br:read(4) + 4
local clLengths = {}
for i = 1, 19 do clLengths[i] = 0 end
for i = 1, numCLCodes do
clLengths[CODE_LENGTH_ORDER[i] + 1] = br:read(3)
end
local clTable = buildHuffmanTable(clLengths)
local lengths = {}
local prev = 8
while #lengths < n do
local sym = decodeHuffman(br, clTable)
if sym <= 15 then
lengths[#lengths + 1] = sym
if sym ~= 0 then prev = sym end
elseif sym == 16 then
local rep = br:read(2) + 3
for _ = 1, rep do lengths[#lengths + 1] = prev end
elseif sym == 17 then
local rep = br:read(3) + 3
for _ = 1, rep do lengths[#lengths + 1] = 0 end
elseif sym == 18 then
local rep = br:read(7) + 11
for _ = 1, rep do lengths[#lengths + 1] = 0 end
end
end
return lengths
end
-- ─── Prefix code reading ─────────────────────────────────────────────────────
local function readPrefixCode(br, alphabetSize)
local simpleCode = br:read(1)
if simpleCode == 1 then
local numSyms = br:read(1) + 1
local firstSym = br:read(1)
local sym1 = br:read(firstSym == 1 and 8 or 1)
local lengths = {}
for i = 1, alphabetSize do lengths[i] = 0 end
lengths[sym1 + 1] = 1
if numSyms == 2 then
local sym2 = br:read(8)
lengths[sym2 + 1] = 1
end
return buildHuffmanTable(lengths)
else
local lengths = readCodeLengths(br, alphabetSize)
return buildHuffmanTable(lengths)
end
end
-- ─── Color cache ─────────────────────────────────────────────────────────────
local function newColorCache(bits)
local size = lshift(1, bits)
local cache = {}
for i = 0, size - 1 do cache[i] = 0 end
return {
size = size,
data = cache,
insert = function(self, color)
-- hash: (0x1e35a7bd * color) >> (32 - bits), masked to cache size
local hash = tobit(0x1e35a7bd * color)
local idx = band(rshift(hash, 32 - bits), size - 1)
self.data[idx] = color
end,
lookup = function(self, idx)
return self.data[idx]
end,
}
end
-- ─── Transform type constants ────────────────────────────────────────────────
local TRANSFORM_PREDICTOR = 0
local TRANSFORM_COLOR = 1
local TRANSFORM_SUBTRACT_GREEN = 2
local TRANSFORM_COLOR_INDEXING = 3
-- ─── Prefix length/distance tables ──────────────────────────────────────────
local PREFIX_EXTRA_BITS = {
0,0,0,0, 1,1,2,2, 3,3,4,4, 5,5,6,6, 7,7,8,8, 9,9,10,10, 11,11,12,12, 13,13
}
local PREFIX_OFFSET = {
0,1,2,3, 4,6,8,12, 16,24,32,48, 64,96,128,192, 256,384,512,768,
1024,1536,2048,3072, 4096,6144,8192,12288, 16384,24576
}
local function prefixToValue(br, code)
if code < 4 then return code end
local extra = PREFIX_EXTRA_BITS[code + 1] or 0
local offset = PREFIX_OFFSET[code + 1] or 0
return offset + br:read(extra)
end
-- ─── VP8L distance offset table (120 entries) ────────────────────────────────
local DIST_MAP = {
{0,1},{1,0},{1,1},{-1,1},{0,2},{2,0},{1,2},{-1,2},
{2,1},{-2,1},{2,2},{-2,2},{0,3},{3,0},{1,3},{-1,3},
{3,1},{-3,1},{2,3},{-2,3},{3,2},{-3,2},{0,4},{4,0},
{1,4},{-1,4},{4,1},{-4,1},{3,3},{-3,3},{2,4},{-2,4},
{4,2},{-4,2},{0,5},{3,4},{-3,4},{4,3},{-4,3},{5,0},
{1,5},{-1,5},{5,1},{-5,1},{2,5},{-2,5},{5,2},{-5,2},
{4,4},{-4,4},{3,5},{-3,5},{5,3},{-5,3},{0,6},{6,0},
{1,6},{-1,6},{6,1},{-6,1},{2,6},{-2,6},{6,2},{-6,2},
{4,5},{-4,5},{5,4},{-5,4},{3,6},{-3,6},{6,3},{-6,3},
{0,7},{7,0},{1,7},{-1,7},{5,5},{-5,5},{7,1},{-7,1},
{4,6},{-4,6},{6,4},{-6,4},{2,7},{-2,7},{7,2},{-7,2},
{3,7},{-3,7},{7,3},{-7,3},{5,6},{-5,6},{6,5},{-6,5},
{8,0},{4,7},{-4,7},{7,4},{-7,4},{8,1},{8,2},{6,6},
{-6,6},{8,3},{5,7},{-5,7},{7,5},{-7,5},{8,4},{6,7},
{-6,7},{7,6},{-7,6},{8,5},{8,6},{7,7},{-7,7},{8,7},
}
-- ─── Main VP8L decode (recursive for transform sub-images) ───────────────────
local function decodeVP8L(br, width, height)
-- color cache
local hasCCache = br:readBool()
local ccache = nil
local ccacheBits = 0
if hasCCache then
ccacheBits = br:read(4)
ccache = newColorCache(ccacheBits)
end
-- collect transforms (applied in reverse after decode)
local transforms = {}
while br:readBool() do
local ttype = br:read(2)
local t = { ttype = ttype }
if ttype == TRANSFORM_PREDICTOR or ttype == TRANSFORM_COLOR then
t.sizeBits = br:read(3) + 2
local tw = math.floor((width + lshift(1, t.sizeBits) - 1) / lshift(1, t.sizeBits))
local th = math.floor((height + lshift(1, t.sizeBits) - 1) / lshift(1, t.sizeBits))
t.data = decodeVP8L(br, tw, th)
elseif ttype == TRANSFORM_COLOR_INDEXING then
t.numColors = br:read(8) + 1
t.colors = decodeVP8L(br, t.numColors, 1)
end
-- SUBTRACT_GREEN has no extra data
transforms[#transforms + 1] = t
end
-- entropy/meta-Huffman image
local groupBits = 0
local groupImage = nil
local numGroups = 1
if br:readBool() then
groupBits = br:read(3) + 2
local gw = math.floor((width + lshift(1, groupBits) - 1) / lshift(1, groupBits))
local gh = math.floor((height + lshift(1, groupBits) - 1) / lshift(1, groupBits))
groupImage = decodeVP8L(br, gw, gh)
local maxG = 0
for _, v in ipairs(groupImage) do
local g = band(rshift(v, 8), 0xffff)
if g > maxG then maxG = g end
end
numGroups = maxG + 1
end
-- alphabet sizes
local ccSize = hasCCache and lshift(1, ccacheBits) or 0
local alphabetG = 256 + 24 + ccSize
-- read Huffman tables for each group (G, R, B, A + distance)
local huffGroups = {}
for g = 1, numGroups do
huffGroups[g] = {
G = readPrefixCode(br, alphabetG),
R = readPrefixCode(br, 256),
B = readPrefixCode(br, 256),
A = readPrefixCode(br, 256),
dist = readPrefixCode(br, 40),
}
end
-- decode pixels
local pixels = {}
local numPixels = width * height
local px, py = 0, 0
local function getGroup()
if not groupImage then return huffGroups[1] end
local gx = rshift(px, groupBits)
local gy = rshift(py, groupBits)
local gw = math.floor((width + lshift(1, groupBits) - 1) / lshift(1, groupBits))
local idx = gy * gw + gx + 1
local g = band(rshift(groupImage[idx] or 0, 8), 0xffff)
return huffGroups[g + 1] or huffGroups[1]
end
while #pixels < numPixels do
local hg = getGroup()
local code = decodeHuffman(br, hg.G)
if code < 256 then
-- literal ARGB (green first, then R, B, A)
local g = code
local r = decodeHuffman(br, hg.R)
local b = decodeHuffman(br, hg.B)
local a = decodeHuffman(br, hg.A)
local color = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b)
pixels[#pixels + 1] = color
if ccache then ccache:insert(color) end
elseif code < 256 + 24 then
-- LZ77 back-reference
local lenCode = code - 256
local length = prefixToValue(br, lenCode) + 1
local distCode = decodeHuffman(br, hg.dist)
local dist = prefixToValue(br, distCode) + 1
-- remap small distances through VP8L spatial table
if dist <= 120 then
local d = DIST_MAP[dist]
local srcX = px - d[1]
local srcY = py - d[2]
dist = py * width + px - (srcY * width + srcX)
end
local src = #pixels - dist + 1
for i = 0, length - 1 do
local c = pixels[src + i] or 0
pixels[#pixels + 1] = c
if ccache then ccache:insert(c) end
end
else
-- color cache reference
local cacheIdx = code - 256 - 24
pixels[#pixels + 1] = ccache:lookup(cacheIdx)
end
px = px + 1
if px >= width then px = 0; py = py + 1 end
end
-- ─── Apply transforms in reverse order ───────────────────────────────────
for i = #transforms, 1, -1 do
local t = transforms[i]
-- SUBTRACT_GREEN: R += G, B += G (mod 256)
if t.ttype == TRANSFORM_SUBTRACT_GREEN then
for idx = 1, #pixels do
local c = pixels[idx]
local a = band(rshift(c, 24), 0xff)
local r = band(rshift(c, 16), 0xff)
local g = band(rshift(c, 8), 0xff)
local b = band(c, 0xff)
r = band(r + g, 0xff)
b = band(b + g, 0xff)
pixels[idx] = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b)
end
-- COLOR_INDEXING: replace each pixel's green channel with palette entry
elseif t.ttype == TRANSFORM_COLOR_INDEXING then
local bpp = 8
if t.numColors <= 2 then bpp = 1
elseif t.numColors <= 4 then bpp = 2
elseif t.numColors <= 16 then bpp = 4 end
local newPixels = {}
if bpp == 8 then
for _, c in ipairs(pixels) do
local idx = band(rshift(c, 8), 0xff)
newPixels[#newPixels + 1] = t.colors[idx + 1] or 0
end
else
local pxPerByte = 8 / bpp
local mask = lshift(1, bpp) - 1
for _, c in ipairs(pixels) do
local packed = band(rshift(c, 8), 0xff)
for p = 0, pxPerByte - 1 do
if #newPixels < width * height then
local idx = band(rshift(packed, p * bpp), mask)
newPixels[#newPixels + 1] = t.colors[idx + 1] or 0
end
end
end
end
pixels = newPixels
-- PREDICTOR: undo per-block spatial prediction
elseif t.ttype == TRANSFORM_PREDICTOR then
local sb = t.sizeBits
local tw = math.floor((width + lshift(1, sb) - 1) / lshift(1, sb))
local function getpx(x, y)
if x < 0 then x = 0 end
if y < 0 then return tobit(0xff000000) end
return pixels[y * width + x + 1] or 0
end
local function addARGB(a, b)
local aa = band(rshift(a, 24), 0xff)
local ar = band(rshift(a, 16), 0xff)
local ag = band(rshift(a, 8), 0xff)
local ab = band(a, 0xff)
local ba = band(rshift(b, 24), 0xff)
local br_ = band(rshift(b, 16), 0xff)
local bg = band(rshift(b, 8), 0xff)
local bb = band(b, 0xff)
return bor(bor(bor(
lshift(band(aa + ba, 0xff), 24),
lshift(band(ar + br_, 0xff), 16)),
lshift(band(ag + bg, 0xff), 8)),
band(ab + bb, 0xff))
end
local function avgARGB(a, b)
local aa = band(rshift(a, 24), 0xff)
local ar = band(rshift(a, 16), 0xff)
local ag = band(rshift(a, 8), 0xff)
local ab = band(a, 0xff)
local ba = band(rshift(b, 24), 0xff)
local br_ = band(rshift(b, 16), 0xff)
local bg = band(rshift(b, 8), 0xff)
local bb = band(b, 0xff)
return bor(bor(bor(
lshift(rshift(aa + ba, 1), 24),
lshift(rshift(ar + br_, 1), 16)),
lshift(rshift(ag + bg, 1), 8)),
rshift(ab + bb, 1))
end
local function clampByte(v)
return math.max(0, math.min(255, v))
end
local function clampAddARGB(a, b)
local aa = band(rshift(a, 24), 0xff)
local ar = band(rshift(a, 16), 0xff)
local ag = band(rshift(a, 8), 0xff)
local ab = band(a, 0xff)
local ba = band(rshift(b, 24), 0xff)
local br_ = band(rshift(b, 16), 0xff)
local bg = band(rshift(b, 8), 0xff)
local bb = band(b, 0xff)
return bor(bor(bor(
lshift(clampByte(aa + ba), 24),
lshift(clampByte(ar + br_), 16)),
lshift(clampByte(ag + bg), 8)),
clampByte(ab + bb))
end
local function subARGB(a, b)
local aa = band(rshift(a, 24), 0xff)
local ar = band(rshift(a, 16), 0xff)
local ag = band(rshift(a, 8), 0xff)
local ab = band(a, 0xff)
local ba = band(rshift(b, 24), 0xff)
local br_ = band(rshift(b, 16), 0xff)
local bg = band(rshift(b, 8), 0xff)
local bb = band(b, 0xff)
return bor(bor(bor(
lshift(band(aa - ba, 0xff), 24),
lshift(band(ar - br_, 0xff), 16)),
lshift(band(ag - bg, 0xff), 8)),
band(ab - bb, 0xff))
end
local function halfSubARGB(a, b)
local aa = band(rshift(a, 24), 0xff)
local ar = band(rshift(a, 16), 0xff)
local ag = band(rshift(a, 8), 0xff)
local ab = band(a, 0xff)
local ba = band(rshift(b, 24), 0xff)
local br_ = band(rshift(b, 16), 0xff)
local bg = band(rshift(b, 8), 0xff)
local bb = band(b, 0xff)
return bor(bor(bor(
lshift(rshift(aa - ba, 1), 24),
lshift(rshift(ar - br_, 1), 16)),
lshift(rshift(ag - bg, 1), 8)),
rshift(ab - bb, 1))
end
local function selectARGB(l, tp, tl)
local function absdiff(x, y, s)
return math.abs(band(rshift(x, s), 0xff) - band(rshift(y, s), 0xff))
end
local function dist(x, y)
return absdiff(x,y,24) + absdiff(x,y,16) + absdiff(x,y,8) + absdiff(x,y,0)
end
return dist(l, tl) <= dist(tp, tl) and l or tp
end
for iy = 0, height - 1 do
for ix = 0, width - 1 do
if not (ix == 0 and iy == 0) then
local pidx = iy * width + ix + 1
local L = getpx(ix - 1, iy)
local T = getpx(ix, iy - 1)
local TL = getpx(ix - 1, iy - 1)
local TR = getpx(ix + 1, iy - 1)
local tx = rshift(ix, sb)
local ty = rshift(iy, sb)
local mode = band(t.data[ty * tw + tx + 1] or 0, 0xff)
local pred
if mode == 0 then pred = tobit(0xff000000)
elseif mode == 1 then pred = L
elseif mode == 2 then pred = T
elseif mode == 3 then pred = TR
elseif mode == 4 then pred = TL
elseif mode == 5 then pred = avgARGB(avgARGB(L, TR), T)
elseif mode == 6 then pred = avgARGB(L, TL)
elseif mode == 7 then pred = avgARGB(L, T)
elseif mode == 8 then pred = avgARGB(TL, T)
elseif mode == 9 then pred = avgARGB(T, TR)
elseif mode == 10 then pred = avgARGB(avgARGB(L, TL), avgARGB(T, TR))
elseif mode == 11 then pred = selectARGB(L, T, TL)
elseif mode == 12 then pred = clampAddARGB(L, subARGB(T, TL))
elseif mode == 13 then
local avg = avgARGB(L, T)
pred = clampAddARGB(avg, halfSubARGB(avg, TL))
else pred = L end
pixels[pidx] = addARGB(pixels[pidx], pred)
end
end
end
-- COLOR: undo green/red channel correlations
elseif t.ttype == TRANSFORM_COLOR then
local sb = t.sizeBits
local tw = math.floor((width + lshift(1, sb) - 1) / lshift(1, sb))
for iy = 0, height - 1 do
for ix = 0, width - 1 do
local pidx = iy * width + ix + 1
local c = pixels[pidx] or 0
local tx = rshift(ix, sb)
local ty = rshift(iy, sb)
local m = t.data[ty * tw + tx + 1] or 0
-- unpack signed bytes from transform pixel (stored as ARGB)
local g2r = band(rshift(m, 16), 0xff)
local r2b = band(rshift(m, 8), 0xff)
local g2b = band(m, 0xff)
if g2r >= 128 then g2r = g2r - 256 end
if r2b >= 128 then r2b = r2b - 256 end
if g2b >= 128 then g2b = g2b - 256 end
local a = band(rshift(c, 24), 0xff)
local r = band(rshift(c, 16), 0xff)
local g = band(rshift(c, 8), 0xff)
local b = band(c, 0xff)
r = band(r + math.floor(g2r * g / 256), 0xff)
b = band(b + math.floor(g2b * g / 256) + math.floor(r2b * r / 256), 0xff)
pixels[pidx] = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b)
end
end
end
end
return pixels
end
-- ═══════════════════════════════════════════════════════════════════════════
-- VP8 (LOSSY) DECODER
-- ═══════════════════════════════════════════════════════════════════════════
-- ─── Boolean arithmetic decoder ──────────────────────────────────────────────
-- VP8 uses a range coder (not Huffman). Each symbol is decoded against a
-- probability in [0,255]. The range is maintained in [128,255]<<shift.
local function newBoolDecoder(data, offset)
-- offset is 1-based index into data where the bool partition starts
local b = {
data = data,
pos = offset,
range = 255,
value = 0,
bits = 0, -- bits available in value (we keep 8 bits of headroom)
}
-- prime with first two bytes
local b0 = data:byte(b.pos) or 0; b.pos = b.pos + 1
local b1 = data:byte(b.pos) or 0; b.pos = b.pos + 1
b.value = lshift(b0, 8) + b1
b.bits = 0
return b
end
local function boolRead(b, prob)
local split = 1 + rshift(b.range * prob, 8)
local bigsplit = lshift(split, 8)
local retval
if b.value >= bigsplit then
retval = 1
b.range = b.range - split
b.value = b.value - bigsplit
else
retval = 0
b.range = split
end
-- renormalise
while b.range < 128 do
b.range = lshift(b.range, 1)
b.value = lshift(b.value, 1)
b.bits = b.bits + 1
if b.bits == 8 then
local byte = b.data:byte(b.pos) or 0
b.pos = b.pos + 1
b.value = bor(b.value, byte)
b.bits = 0
end
end
return retval
end
local function boolReadLit(b, n)
local v = 0
for _ = 1, n do
v = lshift(v, 1) + boolRead(b, 128)
end
return v
end
local function boolReadSigned(b, n)
local v = boolReadLit(b, n)
if boolRead(b, 128) == 1 then v = -v end
return v
end
-- ─── VP8 probability tables ───────────────────────────────────────────────────
-- Coefficient probability table (384 entries, from the VP8 spec Annex B)
-- Indexed as [band][ctx][node] but flattened here for brevity.
-- This is the default/initial probability table before any updates.
local VP8_COEF_PROBS = {
-- block type 0: Y after DC (4x4 intra)
{ -- band 0
{128,128,128,128,128,128,128,128,128,128,128},
{128,128,128,128,128,128,128,128,128,128,128},
{128,128,128,128,128,128,128,128,128,128,128},
},
{ -- band 1
{253,136,254,255,228,219,128,128,128,128,128},
{189,129,242,255,227,213,255,219,128,128,128},
{106,126,227,252,214,209,255,255,128,128,128},
},
{ -- band 2
{ 1, 98,248,255,236,226,255,255,128,128,128},
{181,133,238,254,221,234,255,154,128,128,128},
{ 78,134,202,247,198,180,255,219,128,128,128},
},
{ -- band 3
{ 1,185,249,255,243,255,128,128,128,128,128},
{184,150,247,255,236,224,128,128,128,128,128},
{ 77,110,216,255,236,230,128,128,128,128,128},
},
{ -- band 4
{ 1,101,251,255,241,255,128,128,128,128,128},
{170,139,241,252,236,209,255,255,128,128,128},
{ 37, 116,196,243,228,255,255,255,128,128,128},
},
{ -- band 5
{ 1,204,254,255,245,255,128,128,128,128,128},
{207,160,250,255,238,128,128,128,128,128,128},
{102,103,231,255,211,171,128,128,128,128,128},
},
{ -- band 6
{ 1,152,252,255,240,255,128,128,128,128,128},
{177,135,243,255,234,225,128,128,128,128,128},
{ 80,129,211,255,194,224,128,128,128,128,128},
},
{ -- band 7
{ 1, 1,255,128,128,128,128,128,128,128,128},
{246, 1,255,128,128,128,128,128,128,128,128},
{255,128,128,128,128,128,128,128,128,128,128},
},
}
-- Intra mode probabilities (from VP8 spec section 11)
local VP8_KEYFRAME_YMODE_PROB = {145, 156, 163, 128} -- DC, V, H, TM
local VP8_KEYFRAME_UVMODE_PROB = {142, 114, 183}
-- Default MV probabilities (from spec)
local VP8_MV_UPDATE_PROBS = {
{237,246,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254},
{231,243,245,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254},
}
local VP8_MV_DEFAULT_PROBS = {
{162,128,225,146,172,147,214, 39,156,128,129,132, 75,145,178,206,239,254,254},
{164,128,204,170,119,235,140,230,228,128,130,130, 74,148,180,203,236,254,254},
}
-- ─── Zigzag scan order for 4x4 DCT block ─────────────────────────────────────
local ZIGZAG = {0,1,4,8,5,2,3,6,9,12,13,10,7,11,14,15}
-- ─── Quantizer table ──────────────────────────────────────────────────────────
local function dcQ(q)
if q == 0 then return 4 end
return q < 134 and (q * 2 + 3 > 132 and 132 or q * 2 + 3)
or (5 * q + 77 > 2040 and 2040 or 5 * q + 77)
end
local function acQ(q)
return q < 6 and 8 or
q < 10 and (6 * q - 12) or
q < 126 and (q * 2 + 3 > 255 and 255 or q * 2 + 3) or
(5 * q - 370 > 2040 and 2040 or 5 * q - 370)
end
-- From VP8 spec Table 14 (DC/AC quantizer index tables)
local VP8_DC_QLOOKUP = {}
local VP8_AC_QLOOKUP = {}
do
-- spec tables verbatim
local dc = {4,5,6,7,8,9,10,10,11,12,13,14,15,16,17,17,18,19,20,20,21,21,22,22,23,23,24,25,25,26,27,28,29,30,31,32,33,34,35,36,37,37,38,39,40,41,42,43,44,45,46,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,76,77,78,79,80,81,82,83,84,85,86,87,88,89,91,93,95,96,97,98,100,101,102,104,106,108,110,112,114,116,118,122,124,126,128,130,132,134,136,138,140,143,145,148,151,154,157}
local ac = {4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,60,62,64,66,68,70,72,74,76,79,81,84,87,90,93,96,99,102,105,108,111,114,117,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,205,210,215,220,225,230,235,240,245,249,254,254}
for i,v in ipairs(dc) do VP8_DC_QLOOKUP[i-1] = v end
for i,v in ipairs(ac) do VP8_AC_QLOOKUP[i-1] = v end
end
-- ─── Inverse DCT (4x4, in-place on a 16-element array, 1-based) ──────────────
local function idct4x4(c)
-- rows
for i = 0, 3 do
local a0 = c[i*4+1] + c[i*4+3]
local a1 = c[i*4+1] - c[i*4+3]
local a2 = rshift(c[i*4+2], 1) - c[i*4+4]
local a3 = c[i*4+2] + rshift(c[i*4+4], 1)
c[i*4+1] = a0 + a3
c[i*4+2] = a1 + a2
c[i*4+3] = a1 - a2
c[i*4+4] = a0 - a3
end
-- columns
for i = 0, 3 do
local a0 = c[i+1] + c[i+9]
local a1 = c[i+1] - c[i+9]
local a2 = rshift(c[i+5], 1) - c[i+13]
local a3 = c[i+5] + rshift(c[i+13], 1)
c[i+1] = rshift(a0 + a3 + 4, 3)
c[i+5] = rshift(a1 + a2 + 4, 3)
c[i+9] = rshift(a1 - a2 + 4, 3)
c[i+13] = rshift(a0 - a3 + 4, 3)
end
end
-- WHT (Walsh-Hadamard, 4x4 for DC coefficients)
local function iwht4x4(c)
for i = 0, 3 do
local a0 = c[i*4+1] + c[i*4+3]
local a1 = c[i*4+2] + c[i*4+4]
local a2 = c[i*4+2] - c[i*4+4]
local a3 = c[i*4+1] - c[i*4+3]
c[i*4+1] = a0 + a1
c[i*4+2] = a3 + a2
c[i*4+3] = a0 - a1
c[i*4+4] = a3 - a2
end
for i = 0, 3 do
local a0 = c[i+1] + c[i+9]
local a1 = c[i+5] + c[i+13]
local a2 = c[i+5] - c[i+13]
local a3 = c[i+1] - c[i+9]
c[i+1] = rshift(a0 + a1, 3)
c[i+5] = rshift(a3 + a2, 3)
c[i+9] = rshift(a0 - a1, 3)
c[i+13] = rshift(a3 - a2, 3)
end
end
-- ─── Coefficient decoding ─────────────────────────────────────────────────────
-- Decode one 4x4 block of DCT coefficients from the bool decoder.
-- plane: 0=Y 1=UV, blockCtx: context from neighbouring blocks
local function decodeCoefficients(bool, probs, firstCoef, lastCoef, blockCtx)
local coeffs = {}
for i = 0, 15 do coeffs[i] = 0 end
local ctx = blockCtx
local i = firstCoef
while i <= lastCoef do
local p = probs[i + 1] or probs[1]
-- EOB or nonzero?
if boolRead(bool, p[ctx + 1] and p[ctx+1][1] or 128) == 0 then
break -- end of block
end
-- skip zeros
while boolRead(bool, (p[ctx+1] and p[ctx+1][2]) or 128) == 0 do
i = i + 1
if i > lastCoef then return coeffs, 0 end
ctx = 0
p = probs[i + 1] or probs[1]
end
-- read coefficient value
local v
local pp = p[ctx+1] or {}
if boolRead(bool, pp[3] or 128) == 0 then
v = 1
elseif boolRead(bool, pp[4] or 128) == 0 then
if boolRead(bool, pp[5] or 128) == 0 then
v = 2
else
v = 3 + boolRead(bool, pp[6] or 128)
end
elseif boolRead(bool, pp[7] or 128) == 0 then
if boolRead(bool, pp[8] or 128) == 0 then
v = 5 + boolRead(bool, 159)
else
v = 7 + 2 * boolRead(bool, 165) + boolRead(bool, 145)
end
elseif boolRead(bool, pp[9] or 128) == 0 then
local cat3 = {pp[10] or 128, 165, 145}
v = 11
for _, pr in ipairs(cat3) do v = v * 2 + boolRead(bool, pr) end
elseif boolRead(bool, pp[10] or 128) == 0 then
local cat4 = {pp[11] or 128, 165, 145, 128}
v = 19
for _, pr in ipairs(cat4) do v = v * 2 + boolRead(bool, pr) end
else
-- cat5 or cat6
if boolRead(bool, 128) == 0 then
v = 35
for _, pr in ipairs({165,145,128,128,128}) do
v = v * 2 + boolRead(bool, pr)
end
else
v = 67
for _, pr in ipairs({145,128,128,128,128,128}) do
v = v * 2 + boolRead(bool, pr)
end
end
end
-- sign bit
if boolRead(bool, 128) == 1 then v = -v end
coeffs[ZIGZAG[i + 1]] = v
ctx = (math.abs(v) == 1) and 1 or 2
i = i + 1
end
local nzCount = 0
for _, v in ipairs(coeffs) do if v ~= 0 then nzCount = nzCount + 1 end end
return coeffs, (nzCount > 0 and 1 or 0)
end
-- ─── Intra prediction modes ───────────────────────────────────────────────────
-- B_PRED sub-modes for 4x4 luma intra prediction
local B_DC_PRED, B_TM_PRED, B_VE_PRED, B_HE_PRED = 0,1,2,3
local B_LD_PRED, B_RD_PRED, B_VR_PRED, B_VL_PRED = 4,5,6,7
local B_HD_PRED, B_HU_PRED = 8,9
-- Clamp to [0,255]
local function clamp8(v) return math.max(0, math.min(255, v)) end
-- Fill a 4x4 block in a flat array (stride = mbw*4) using intra prediction.
-- out: flat Y/U/V plane array (1-based, row-major)
-- x4,y4: top-left pixel coords in the plane
-- stride: row stride
-- mode: prediction mode
-- above: 8 pixels above (indices 0..7, may be nil for top-of-image)
-- left: 4 pixels to left (indices 0..3, may be nil for left-of-image)
local function predictBlock4x4(out, x4, y4, stride, mode, above, left, aboveLeft)
local function set(px, py, v)
out[(y4 + py) * stride + x4 + px + 1] = v
end
local A = above or {127,127,127,127,127,127,127,127}
local L = left or {129,129,129,129}
local TL = aboveLeft or 127
if mode == B_DC_PRED then
local sum, n = 0, 0
if above then for i=0,3 do sum=sum+A[i+1]; n=n+1 end end
if left then for i=0,3 do sum=sum+L[i+1]; n=n+1 end end
local dc = n > 0 and rshift(sum + rshift(n, 1), n == 8 and 3 or 2) or 128
for py=0,3 do for px=0,3 do set(px,py,dc) end end
elseif mode == B_TM_PRED then
for py=0,3 do for px=0,3 do
set(px, py, clamp8(A[px+1] + L[py+1] - TL))
end end
elseif mode == B_VE_PRED then
for py=0,3 do for px=0,3 do set(px,py,A[px+1]) end end
elseif mode == B_HE_PRED then
for py=0,3 do for px=0,3 do set(px,py,L[py+1]) end end
elseif mode == B_LD_PRED then
local a = {A[1],A[2],A[3],A[4],A[5],A[6],A[7],A[8]}
local function ld(i)
local x0 = i < 7 and a[i+1] or a[8]
local x1 = i < 7 and a[i+2] or a[8]
local x2 = i < 6 and a[i+3] or a[8]
return rshift(x0 + 2*x1 + x2 + 2, 2)
end
for py=0,3 do for px=0,3 do set(px,py,ld(px+py)) end end
elseif mode == B_RD_PRED then
local a = {TL,A[1],A[2],A[3],A[4],L[1],L[2],L[3],L[4]}
local function rd(i) -- i in 0..6
local x0 = a[i+1] or a[1]
local x1 = a[i+2] or a[1]
local x2 = a[i+3] or a[1]
return rshift(x0 + 2*x1 + x2 + 2, 2)
end
for py=0,3 do for px=0,3 do
local d = px - py
set(px, py, rd(d + 4))
end end
elseif mode == B_VR_PRED then
local a = {TL,A[1],A[2],A[3],A[4],L[1],L[2],L[3]}
for py=0,3 do for px=0,3 do
local x = 2*px - py
local v
if x >= 0 then
local ai = x / 2 + 1
if x % 2 == 0 then
v = rshift((a[ai] or 127) + (a[ai+1] or 127) + 1, 1)
else
v = rshift((a[ai] or 127) + 2*(a[ai+1] or 127) + (a[ai+2] or 127) + 2, 2)
end
else
v = rshift(L[-x] + 2*(L[-x+1] or L[4]) + (L[-x+2] or L[4]) + 2, 2)
end
set(px, py, clamp8(v))
end end
elseif mode == B_VL_PRED then
for py=0,3 do for px=0,3 do
local x = px + rshift(py, 1)
local v
if py % 2 == 0 then
v = rshift((A[x+1] or A[8]) + (A[x+2] or A[8]) + 1, 1)
else
v = rshift((A[x+1] or A[8]) + 2*(A[x+2] or A[8]) + (A[x+3] or A[8]) + 2, 2)
end
set(px, py, clamp8(v))
end end
elseif mode == B_HD_PRED then
local a = {L[4],L[3],L[2],L[1],TL,A[1],A[2],A[3],A[4]}
for py=0,3 do for px=0,3 do
local x = 2*py - px
local v
if x >= 0 then
local ai = rshift(x, 1) + 1
if x % 2 == 0 then
v = rshift((a[ai] or 127) + (a[ai+1] or 127) + 1, 1)
else
v = rshift((a[ai] or 127) + 2*(a[ai+1] or 127) + (a[ai+2] or 127) + 2, 2)
end
else
v = rshift(A[-x] + 2*(A[-x+1] or A[8]) + (A[-x+2] or A[8]) + 2, 2)
end
set(px, py, clamp8(v))
end end
elseif mode == B_HU_PRED then
for py=0,3 do for px=0,3 do
local x = px + 2*py
local v
if x + 1 < 4 then
v = rshift(L[x+1] + L[x+2] + 1, 1)
elseif x == 6 then
v = rshift(L[4] + 3*L[4] + 2, 2)
else
v = L[4]
end
set(px, py, clamp8(v))
end end
end
end
-- 16x16 luma intra prediction (DC, V, H, TM)
local function predictMB16(plane, mbx, mby, mbw, mode, aboveRow, leftCol)
local base_x = mbx * 16
local base_y = mby * 16
local stride = mbw * 16
if mode == 0 then -- DC_PRED
local sum, n = 0, 0
if aboveRow then for i=0,15 do sum=sum+aboveRow[i+1]; n=n+1 end end
if leftCol then for i=0,15 do sum=sum+leftCol[i+1]; n=n+1 end end
local dc = n > 0 and rshift(sum + rshift(n, 1), n == 32 and 5 or 4) or 128
for py=0,15 do for px=0,15 do
plane[(base_y+py)*stride + base_x+px + 1] = dc
end end
elseif mode == 1 then -- V_PRED
local src = aboveRow or {}
for py=0,15 do for px=0,15 do
plane[(base_y+py)*stride + base_x+px + 1] = src[px+1] or 127
end end
elseif mode == 2 then -- H_PRED
local src = leftCol or {}
for py=0,15 do for px=0,15 do
plane[(base_y+py)*stride + base_x+px + 1] = src[py+1] or 129
end end
elseif mode == 3 then -- TM_PRED
local tl = (aboveRow and leftCol) and 127 or 128
if aboveRow and leftCol then
-- tl is above-left of macroblock
tl = 127 -- approximation; proper value from frame buffer
end
local A = aboveRow or {}
local L = leftCol or {}
for py=0,15 do for px=0,15 do
plane[(base_y+py)*stride + base_x+px + 1] =
clamp8((A[px+1] or 127) + (L[py+1] or 129) - tl)
end end
end
end
-- 8x8 chroma intra prediction
local function predictMB8(plane, mbx, mby, mbw, mode, aboveRow, leftCol)
local base_x = mbx * 8
local base_y = mby * 8
local stride = mbw * 8
if mode == 0 then -- DC
local sum, n = 0, 0
if aboveRow then for i=0,7 do sum=sum+aboveRow[i+1]; n=n+1 end end
if leftCol then for i=0,7 do sum=sum+leftCol[i+1]; n=n+1 end end
local dc = n > 0 and rshift(sum + rshift(n, 1), n == 16 and 4 or 3) or 128
for py=0,7 do for px=0,7 do
plane[(base_y+py)*stride + base_x+px + 1] = dc
end end
elseif mode == 1 then -- V
local src = aboveRow or {}
for py=0,7 do for px=0,7 do
plane[(base_y+py)*stride + base_x+px + 1] = src[px+1] or 127
end end
elseif mode == 2 then -- H
local src = leftCol or {}
for py=0,7 do for px=0,7 do
plane[(base_y+py)*stride + base_x+px + 1] = src[py+1] or 129
end end
elseif mode == 3 then -- TM
local A = aboveRow or {}
local L = leftCol or {}
for py=0,7 do for px=0,7 do
plane[(base_y+py)*stride + base_x+px + 1] = clamp8((A[px+1] or 127) + (L[py+1] or 129) - 127)
end end
end
end
-- ─── Add residuals to prediction ─────────────────────────────────────────────
local function addResiduals(plane, base_x, base_y, stride, coeffs)
for py = 0, 3 do
for px = 0, 3 do
local idx = (base_y + py) * stride + base_x + px + 1
plane[idx] = clamp8((plane[idx] or 0) + (coeffs[py * 4 + px + 1] or 0))
end
end
end
-- ─── Simple loop filter ───────────────────────────────────────────────────────
local function filterSimple(plane, stride, w, h)
-- horizontal edges
for y = 1, h - 1 do
for x = 0, w - 1 do
local p1 = plane[(y-1)*stride + x + 1] or 128
local q1 = plane[ y *stride + x + 1] or 128
local d = rshift(3*(q1 - p1) + 4, 3)
d = math.max(-4, math.min(4, d))
plane[(y-1)*stride+x+1] = clamp8(p1 + d)
plane[ y *stride+x+1] = clamp8(q1 - d)
end
end
-- vertical edges
for y = 0, h - 1 do
for x = 1, w - 1 do
local p1 = plane[y*stride + x] or 128
local q1 = plane[y*stride + x + 1] or 128
local d = rshift(3*(q1 - p1) + 4, 3)
d = math.max(-4, math.min(4, d))
plane[y*stride+x] = clamp8(p1 + d)
plane[y*stride+x+1] = clamp8(q1 - d)
end
end
end
-- ─── YCbCr → RGB conversion ───────────────────────────────────────────────────
-- VP8 uses BT.601 studio swing:
-- R = Y + 1.402*(Cr-128)
-- G = Y - 0.344*(Cb-128) - 0.714*(Cr-128)
-- B = Y + 1.772*(Cb-128)
local function yuvToRgb(y, cb, cr)
local r = clamp8(math.floor(y + 1.402 * (cr - 128) + 0.5))
local g = clamp8(math.floor(y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128) + 0.5))
local b = clamp8(math.floor(y + 1.772 * (cb - 128) + 0.5))
return r, g, b
end
-- ─── Main VP8 decode ──────────────────────────────────────────────────────────
local function decodeVP8(data, offset, length)
offset = offset or 1
-- Frame tag (3 bytes)
local b0 = data:byte(offset)
local b1 = data:byte(offset + 1)
local b2 = data:byte(offset + 2)
local keyFrame = band(b0, 1) == 0
local version = band(rshift(b0, 1), 7)
local showFrame = band(rshift(b0, 4), 1) == 1
local firstPartSize = bor(rshift(b0, 5), bor(lshift(b1, 3), lshift(b2, 11)))
assert(keyFrame, "VP8 inter frames not supported (not a keyframe)")
-- Start code (3 bytes: 0x9d 0x01 0x2a)
assert(data:byte(offset+3) == 0x9d and
data:byte(offset+4) == 0x01 and
data:byte(offset+5) == 0x2a, "Invalid VP8 start code")
local w_raw = bor(data:byte(offset+6), lshift(data:byte(offset+7), 8))
local h_raw = bor(data:byte(offset+8), lshift(data:byte(offset+9), 8))
local width = band(w_raw, 0x3fff)
local height = band(h_raw, 0x3fff)
local hscale = rshift(w_raw, 14)
local vscale = rshift(h_raw, 14)
-- macroblock dimensions
local mbw = rshift(width + 15, 4)
local mbh = rshift(height + 15, 4)
-- First partition starts at offset+3 (after frame tag)
local bool = newBoolDecoder(data, offset + 3)
-- ── Frame header ──
-- color space and clamping
local colorSpace = boolRead(bool, 128)
local clampType = boolRead(bool, 128)
-- segmentation
local segmentEnabled = boolRead(bool, 128)
local segmentAbsDelta = false
local segQuant = {0,0,0,0}
local segFilter = {0,0,0,0}
if segmentEnabled == 1 then
local updateMap = boolRead(bool, 128)
local updateData = boolRead(bool, 128)
if updateData == 1 then
segmentAbsDelta = boolRead(bool, 128) == 1
for i = 1, 4 do
if boolRead(bool, 128) == 1 then
segQuant[i] = boolReadSigned(bool, 7)
end
end
for i = 1, 4 do
if boolRead(bool, 128) == 1 then
segFilter[i] = boolReadSigned(bool, 6)
end
end
end
if updateMap == 1 then
-- read 3 probabilities (skip for keyframe - all MBs resend)
for _ = 1, 3 do
if boolRead(bool, 128) == 1 then boolReadLit(bool, 8) end
end
end
end
-- loop filter
local filterType = boolRead(bool, 128) -- 0=normal, 1=simple
local filterLevel = boolReadLit(bool, 6)
local filterSharp = boolReadLit(bool, 3)
local lfAdjEnable = boolRead(bool, 128)
if lfAdjEnable == 1 then
if boolRead(bool, 128) == 1 then -- mode_ref_lf_delta_update
for _ = 1, 4 do -- ref_frame deltas
if boolRead(bool, 128) == 1 then boolReadSigned(bool, 6) end
end
for _ = 1, 4 do -- mb mode deltas
if boolRead(bool, 128) == 1 then boolReadSigned(bool, 6) end
end
end
end
-- partition count
local log2Partitions = boolReadLit(bool, 2)
local numPartitions = lshift(1, log2Partitions)
-- quantizer indices
local baseQ = boolReadLit(bool, 7)
local dqY1dc = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0
local dqY2dc = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0
local dqY2ac = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0
local dqUVdc = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0
local dqUVac = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0
-- build quantizer tables per segment
local function qi(base, delta, isAbs)
local q = isAbs and delta or base + delta
return math.max(0, math.min(127, q))
end
-- coefficient probability updates
-- (use default table; read updates from bitstream)
local coefProbs = {}
for t = 1, 4 do
coefProbs[t] = {}
for b = 1, 8 do
coefProbs[t][b] = {}
for c = 1, 3 do
coefProbs[t][b][c] = {}
for n = 1, 11 do
coefProbs[t][b][c][n] = VP8_COEF_PROBS[b] and
VP8_COEF_PROBS[b][c] and VP8_COEF_PROBS[b][c][n] or 128
end
end
end
end
-- read coefficient probability updates
for t = 1, 4 do
for b = 1, 8 do
for c = 1, 3 do
for n = 1, 11 do
if boolRead(bool, 255) == 1 then
coefProbs[t][b][c][n] = boolReadLit(bool, 8)
end
end
end
end
end
-- skip probability
local mbSkipEnabled = boolRead(bool, 128)
local skipProb = mbSkipEnabled == 1 and boolReadLit(bool, 8) or 0
-- intra mode probabilities for key frames are fixed (not transmitted)
-- (use VP8_KEYFRAME_YMODE_PROB / VP8_KEYFRAME_UVMODE_PROB)
-- ── Locate second partition(s) ──
-- The first partition ends at offset+3+firstPartSize
-- Immediately after are (numPartitions-1) * 3 bytes of partition sizes,
-- then the partition data.
local firstPartEnd = offset + 3 + firstPartSize
local partSizeBase = firstPartEnd
local partDataBase = partSizeBase + (numPartitions - 1) * 3
local partOffsets = {}
local acc = 0
for p = 1, numPartitions - 1 do
local o = partSizeBase + (p-1)*3
local sz = bor(data:byte(o) or 0,
bor(lshift(data:byte(o+1) or 0, 8),
lshift(data:byte(o+2) or 0, 16)))
partOffsets[p] = partDataBase + acc
acc = acc + sz
end
partOffsets[numPartitions] = partDataBase + acc
-- create bool decoders for each residual partition
local partBools = {}
for p = 1, numPartitions do
partBools[p] = newBoolDecoder(data, partOffsets[p])
end
-- ── Allocate planes ──
local Yplane = {}
local Uplane = {}
local Vplane = {}
local Ystride = mbw * 16
local Cstride = mbw * 8
for i = 1, Ystride * mbh * 16 do Yplane[i] = 128 end
for i = 1, Cstride * mbh * 8 do Uplane[i] = 128; Vplane[i] = 128 end
-- ── Per-macroblock decode ──
local partIdx = 1
for mby = 0, mbh - 1 do
for mbx = 0, mbw - 1 do
-- select residual partition (round-robin)
local pb = partBools[partIdx]
partIdx = (partIdx % numPartitions) + 1
-- skip flag
local mbSkip = false
if mbSkipEnabled == 1 then
mbSkip = boolRead(bool, skipProb) == 1
end
-- intra prediction mode for luma (16x16 or B_PRED / 4x4)
local lumaMode = 0 -- DC default
-- read y_mode
local yMode
if boolRead(bool, 145) == 0 then
-- B_PRED: 4x4 sub-modes per sub-block
yMode = 4 -- B_PRED sentinel
else
if boolRead(bool, 156) == 0 then
if boolRead(bool, 163) == 0 then
yMode = 1 -- V_PRED
else
yMode = 2 -- H_PRED
end
else
if boolRead(bool, 128) == 0 then
yMode = 0 -- DC_PRED
else
yMode = 3 -- TM_PRED
end
end
end
-- UV mode
local uvMode
if boolRead(bool, 142) == 0 then
uvMode = 0 -- DC
elseif boolRead(bool, 114) == 0 then
uvMode = 1 -- V
elseif boolRead(bool, 183) == 0 then
uvMode = 2 -- H
else
uvMode = 3 -- TM
end
-- read 4x4 sub-modes if B_PRED
local subModes = {}
if yMode == 4 then
local B_PROB = {
{231,120, 48, 89,115,113,120,152,112},
{152,179, 64,126,170,118, 46, 70, 95},
{175, 69,143, 80, 85, 82, 72,155, 64},
{212,188,128, 97,151,195, 9, 41, 15},
{ 3, 9, 1, 7, 3, 3, 5, 1, 16},
}
for _ = 0, 15 do
-- simplified: use DC_PRED as default for all sub-blocks
-- full implementation would use context-adaptive probs
local m = 0
if boolRead(bool, 128) == 0 then m = 0
elseif boolRead(bool, 156) == 0 then m = 1
elseif boolRead(bool, 163) == 0 then m = 2
elseif boolRead(bool, 128) == 0 then m = 3
elseif boolRead(bool, 128) == 0 then m = 4
elseif boolRead(bool, 128) == 0 then m = 5
elseif boolRead(bool, 128) == 0 then m = 6
elseif boolRead(bool, 128) == 0 then m = 7
elseif boolRead(bool, 128) == 0 then m = 8
else m = 9 end
subModes[#subModes + 1] = m
end
end
-- ── Intra prediction (luma) ──
local base_x = mbx * 16
local base_y = mby * 16
-- gather above/left context
local function getAbove16(plane, bx, by, stride)
if by == 0 then return nil end
local row = {}
for i = 0, 15 do
row[i+1] = plane[(by-1)*stride + bx + i + 1] or 127
end
return row
end
local function getLeft16(plane, bx, by, stride)
if bx == 0 then return nil end
local col = {}
for i = 0, 15 do
col[i+1] = plane[by*stride + bx - 1 + (i * stride) - (stride-1)] or 129
end
return col
end
if yMode ~= 4 then
-- whole-MB prediction
local abv = getAbove16(Yplane, base_x, base_y, Ystride)
local lft = nil
if mbx > 0 then
lft = {}
for i = 0, 15 do
lft[i+1] = Yplane[(base_y+i)*Ystride + base_x] or 129
end
end
predictMB16(Yplane, mbx, mby, mbw, yMode, abv, lft)
else
-- B_PRED: 4x4 sub-block prediction
for si = 0, 15 do
local sx = band(si, 3) * 4
local sy = rshift(si, 2) * 4
local px = base_x + sx
local py = base_y + sy
local above4 = {}
local left4 = {}
local tl
for i = 0, 7 do
above4[i+1] = py > 0 and (Yplane[(py-1)*Ystride + px + i + 1] or 127) or 127
end
for i = 0, 3 do
left4[i+1] = px > 0 and (Yplane[(py+i)*Ystride + px] or 129) or 129
end
tl = (py > 0 and px > 0) and (Yplane[(py-1)*Ystride + px] or 127) or 127
predictBlock4x4(Yplane, px, py, Ystride, subModes[si+1] or 0,
above4, left4, tl)
end
end
-- UV prediction
do
local ubx = mbx * 8
local uby = mby * 8
local function above8(plane)
if mby == 0 then return nil end
local r = {}
for i=0,7 do r[i+1]=plane[(uby-1)*Cstride+ubx+i+1] or 127 end
return r
end
local function left8(plane)
if mbx == 0 then return nil end
local c = {}
for i=0,7 do c[i+1]=plane[(uby+i)*Cstride+ubx] or 129 end
return c
end
predictMB8(Uplane, mbx, mby, mbw, uvMode, above8(Uplane), left8(Uplane))
predictMB8(Vplane, mbx, mby, mbw, uvMode, above8(Vplane), left8(Vplane))
end
-- ── Residuals ──
if not mbSkip then
local q = math.max(0, math.min(127, baseQ))
local yDCq = VP8_DC_QLOOKUP[math.max(0,math.min(127, q + dqY1dc))]
local yACq = VP8_AC_QLOOKUP[q]
local y2DCq= VP8_DC_QLOOKUP[math.max(0,math.min(127, q + dqY2dc))]
local y2ACq= VP8_AC_QLOOKUP[math.max(0,math.min(127, q + dqY2ac))]
local uvDCq= VP8_DC_QLOOKUP[math.max(0,math.min(127, q + dqUVdc))]
local uvACq= VP8_AC_QLOOKUP[math.max(0,math.min(127, q + dqUVac))]
-- Y2 (DC only for 16x16 modes)
local y2coeffs = nil
if yMode ~= 4 then
local c16 = {}
for i=1,16 do c16[i] = 0 end
-- decode 4x4 WHT block for Y2
local rawC, nz = decodeCoefficients(pb, coefProbs[1], 0, 15, 0)
if nz > 0 then
for k=0,15 do rawC[k] = (rawC[k] or 0) * (k==0 and y2DCq or y2ACq) end
local wht = {}
for k=1,16 do wht[k] = rawC[k-1] or 0 end
iwht4x4(wht)
y2coeffs = wht
end
end
-- 16 Y sub-blocks
for si = 0, 15 do
local sx = band(si, 3) * 4
local sy = rshift(si, 2) * 4
local px = base_x + sx
local py = base_y + sy
local firstCoef = yMode ~= 4 and 1 or 0
local rawC, nz = decodeCoefficients(pb, coefProbs[yMode~=4 and 2 or 1],
firstCoef, 15, 0)
-- inject Y2 DC if present
if y2coeffs then
rawC[0] = y2coeffs[si + 1] or 0
end
-- dequantize
for k = 0, 15 do
rawC[k] = (rawC[k] or 0) * (k == 0 and yDCq or yACq)
end
-- inverse DCT
local block = {}
for k = 1, 16 do block[k] = rawC[k-1] or 0 end
idct4x4(block)
addResiduals(Yplane, px, py, Ystride, block)
end
-- 4 U + 4 V sub-blocks
for _, plane in ipairs({Uplane, Vplane}) do
for si = 0, 3 do
local sx = band(si, 1) * 4
local sy = rshift(si, 1) * 4
local px = mbx * 8 + sx
local py = mby * 8 + sy
local rawC, nz = decodeCoefficients(pb, coefProbs[3], 0, 15, 0)
for k = 0, 15 do
rawC[k] = (rawC[k] or 0) * (k == 0 and uvDCq or uvACq)
end
local block = {}
for k = 1, 16 do block[k] = rawC[k-1] or 0 end
idct4x4(block)
addResiduals(plane, px, py, Cstride, block)
end
end
end
end -- mbx
end -- mby
-- optional simple loop filter
if filterType == 1 and filterLevel > 0 then
filterSimple(Yplane, Ystride, mbw*16, mbh*16)
filterSimple(Uplane, Cstride, mbw*8, mbh*8)
filterSimple(Vplane, Cstride, mbw*8, mbh*8)
end
-- ── Assemble RGBA image ──
local imageData = love.image.newImageData(width, height, "rgba8")
imageData:mapPixel(function(px, py)
local yi = py * Ystride + px + 1
local cy = rshift(py, 1)
local cx = rshift(px, 1)
local ci = cy * Cstride + cx + 1
local Y = Yplane[yi] or 128
local Cb = Uplane[ci] or 128
local Cr = Vplane[ci] or 128
local r, g, b = yuvToRgb(Y, Cb, Cr)
return r/255, g/255, b/255, 1
end)
return love.graphics.newImage(imageData)
end
-- ═══════════════════════════════════════════════════════════════════════════
-- VP8L → Love2D image (unchanged, just wrapped for the dispatcher)
-- ═══════════════════════════════════════════════════════════════════════════
local function decodeVP8L_image(data, offset)
offset = offset or 1
assert(data:byte(offset) == 0x2f, "Invalid VP8L signature byte")
local br = newBitReader(data:sub(offset + 1))
local width = br:read(14) + 1
local height = br:read(14) + 1
br:readBool() -- alpha hint
local version = br:read(3)
assert(version == 0, "Unsupported VP8L version: " .. version)
local pixels = decodeVP8L(br, width, height)
local imageData = love.image.newImageData(width, height, "rgba8")
imageData:mapPixel(function(x, y)
local c = pixels[y * width + x + 1] or 0
local a = band(rshift(c, 24), 0xff)
local r = band(rshift(c, 16), 0xff)
local g = band(rshift(c, 8), 0xff)
local b = band(c, 0xff)
return r/255, g/255, b/255, a/255
end)
return love.graphics.newImage(imageData)
end
-- ═══════════════════════════════════════════════════════════════════════════
-- PUBLIC API
-- ═══════════════════════════════════════════════════════════════════════════
-- Read a little-endian uint32 from data at pos (1-based)
local function readU32LE(data, pos)
return bor(data:byte(pos),
bor(lshift(data:byte(pos+1), 8),
bor(lshift(data:byte(pos+2), 16),
lshift(data:byte(pos+3), 24))))
end
function WebP.decode(data)
assert(data:sub(1, 4) == "RIFF", "Not a RIFF file")
assert(data:sub(9, 12) == "WEBP", "Not a WEBP file")
local fourCC = data:sub(13, 16)
if fourCC == "VP8 " then
-- Simple lossy: VP8 bitstream starts after the 8-byte chunk header
-- bytes 13-16: "VP8 ", 17-20: chunk size (LE), 21+: VP8 bitstream
return decodeVP8(data, 21)
elseif fourCC == "VP8L" then
-- Simple lossless: VP8L bitstream starts at byte 21
return decodeVP8L_image(data, 21)
elseif fourCC == "VP8X" then
-- Extended format: parse chunks after the VP8X header
-- VP8X chunk: 4cc(4) + size(4) + flags(4) + canvas_w-1(3) + canvas_h-1(3) = 18 bytes
local pos = 13 -- start of VP8X chunk
local chunkSize = readU32LE(data, pos + 4)
pos = pos + 8 + chunkSize -- skip past VP8X chunk data
-- walk remaining chunks
while pos + 8 <= #data do
local cc = data:sub(pos, pos + 3)
local sz = readU32LE(data, pos + 4)
local dOff = pos + 8 -- chunk data offset (1-based)
if cc == "VP8 " then
return decodeVP8(data, dOff)
elseif cc == "VP8L" then
return decodeVP8L_image(data, dOff)
elseif cc == "ANIM" or cc == "ANMF" then
error("Animated WebP is not supported")
end
pos = pos + 8 + sz
if band(sz, 1) == 1 then pos = pos + 1 end -- padding byte
end
error("VP8X: no VP8 or VP8L chunk found")
else
error("Unsupported WebP chunk type: " .. fourCC)
end
end
function WebP.load(path)
local data = love.filesystem.read(path)
assert(data, "Could not read file: " .. path)
return WebP.decode(data)
end
return WebP