1627 lines
62 KiB
Lua
1627 lines
62 KiB
Lua
-- 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 LSB‑first) ───────────────────────────
|
||
|
||
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 LSB‑first 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 LSB‑first) ────────────────────────────────────
|
||
|
||
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 LSB‑first
|
||
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, LSB‑first
|
||
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
|