-- 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]<= 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