603 lines
23 KiB
Lua
603 lines
23 KiB
Lua
-- webp.lua — Pure Lua VP8L (lossless WebP) decoder for Love2D (LuaJIT)
|
|
-- Supports: VP8L (lossless) only. VP8 (lossy) is not feasible in pure Lua.
|
|
-- 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.
|
|
|
|
local function newBitReader(data)
|
|
local r = {
|
|
data = data,
|
|
pos = 1, -- next byte to load (1-based)
|
|
window = 0, -- current bit window (32-bit)
|
|
bits = 0, -- valid bits in window
|
|
}
|
|
|
|
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
|
|
|
|
-- ─── Huffman trees ───────────────────────────────────────────────────────────
|
|
|
|
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
|
|
htable[nextCode[len]] = { sym = i - 1, len = len }
|
|
nextCode[len] = nextCode[len] + 1
|
|
end
|
|
end
|
|
return htable
|
|
end
|
|
|
|
local function decodeHuffman(br, htable)
|
|
local code = 0
|
|
for len = 1, 15 do
|
|
code = bor(lshift(code, 1), br:read(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
|
|
|
|
-- ─── Public API ──────────────────────────────────────────────────────────────
|
|
|
|
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)
|
|
assert(fourCC == "VP8L", "Only lossless VP8L WebP is supported (got: " .. fourCC .. ")")
|
|
|
|
-- byte 21 = VP8L signature (0x2f), bitstream starts at byte 22
|
|
assert(data:byte(21) == 0x2f, "Invalid VP8L signature byte")
|
|
local br = newBitReader(data:sub(22))
|
|
|
|
local width = br:read(14) + 1
|
|
local height = br:read(14) + 1
|
|
br:readBool() -- alpha hint flag, unused
|
|
local version = br:read(3)
|
|
assert(version == 0, "Unsupported VP8L version: " .. version)
|
|
|
|
local pixels = decodeVP8L(br, width, height)
|
|
|
|
-- VP8L stores ARGB; Love2D mapPixel expects normalised RGBA floats
|
|
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
|
|
|
|
function WebP.load(path)
|
|
local data = love.filesystem.read(path)
|
|
assert(data, "Could not read file: " .. path)
|
|
return WebP.decode(data)
|
|
end
|
|
|
|
return WebP
|