Files
gen1recomp/src/import/ImageWriter.lua
T
1jamie 780246c4f6 feat: add deinterleave function and enhance graphics extraction for Slots and Card Flip
Implemented a new deinterleave function in ImageWriter to restore row-major order for PNGs. Updated RomExtractorGen2 to utilize this function for extracting Slot Machine graphics, and added new functions for padding and writing graphics sheets. Updated the ROM manifests to include necessary symbols for Slots and Card Flip assets.
2026-08-20 20:40:37 -05:00

164 lines
5.3 KiB
Lua

local ImageWriter = {}
local SHADES = {
{ 1, 1, 1, 1 },
{ 2 / 3, 2 / 3, 2 / 3, 1 },
{ 1 / 3, 1 / 3, 1 / 3, 1 },
{ 0, 0, 0, 1 },
}
local function assertDimensions(raw, width, height, bits)
assert(width % 8 == 0 and height % 8 == 0,
("%dbpp dimensions must be tile-aligned: %dx%d")
:format(bits, width, height))
local expected = width * height * bits / 8
assert(#raw == expected,
("%dbpp payload is %d bytes, expected %d")
:format(bits, #raw, expected))
end
function ImageWriter.decode2bpp(raw, width, height, transparent)
assertDimensions(raw, width, height, 2)
local image = love.image.newImageData(width, height)
local tilesPerRow = width / 8
for tile = 0, #raw / 16 - 1 do
local tileX = tile % tilesPerRow * 8
local tileY = math.floor(tile / tilesPerRow) * 8
for y = 0, 7 do
local low = raw[tile * 16 + y * 2 + 1]
local high = raw[tile * 16 + y * 2 + 2]
for x = 0, 7 do
local divisor = 2 ^ (7 - x)
local shade = math.floor(high / divisor) % 2 * 2
+ math.floor(low / divisor) % 2
local color = SHADES[shade + 1]
local alpha = color[4]
if transparent and shade == 0 then alpha = 0 end
image:setPixel(tileX + x, tileY + y,
color[1], color[2], color[3], alpha)
end
end
end
return image
end
function ImageWriter.decode1bpp(raw, width, height, transparent)
assertDimensions(raw, width, height, 1)
local image = love.image.newImageData(width, height)
local tilesPerRow = width / 8
for tile = 0, #raw / 8 - 1 do
local tileX = tile % tilesPerRow * 8
local tileY = math.floor(tile / tilesPerRow) * 8
for y = 0, 7 do
local row = raw[tile * 8 + y + 1]
for x = 0, 7 do
local filled = math.floor(row / 2 ^ (7 - x)) % 2 ~= 0
local value = filled and 0 or 1
local alpha = 1
if transparent and not filled then alpha = 0 end
image:setPixel(tileX + x, tileY + y,
value, value, value, alpha)
end
end
end
return image
end
function ImageWriter.blank(width, height, r, g, b, a)
local image = love.image.newImageData(width, height)
image:mapPixel(function() return r or 0, g or 0, b or 0, a or 0 end)
return image
end
function ImageWriter.blit(target, source, targetX, targetY,
sourceX, sourceY, width, height, flipX)
sourceX, sourceY = sourceX or 0, sourceY or 0
width, height = width or source:getWidth(), height or source:getHeight()
for y = 0, height - 1 do
for x = 0, width - 1 do
local sampleX = flipX and sourceX + width - 1 - x or sourceX + x
target:setPixel(targetX + x, targetY + y,
source:getPixel(sampleX, sourceY + y))
end
end
end
function ImageWriter.matteColor0(image)
local width, height = image:getDimensions()
local queueX, queueY, head = {}, {}, 1
local seen = {}
local function add(x, y)
local key = y * width + x
if seen[key] then return end
local r, g, b, a = image:getPixel(x, y)
if r == 1 and g == 1 and b == 1 and a == 1 then
seen[key] = true
queueX[#queueX + 1], queueY[#queueY + 1] = x, y
end
end
for x = 0, width - 1 do add(x, 0); add(x, height - 1) end
for y = 0, height - 1 do add(0, y); add(width - 1, y) end
while head <= #queueX do
local x, y = queueX[head], queueY[head]
head = head + 1
image:setPixel(x, y, 1, 1, 1, 0)
if x > 0 then add(x - 1, y) end
if x + 1 < width then add(x + 1, y) end
if y > 0 then add(x, y - 1) end
if y + 1 < height then add(x, y + 1) end
end
return image
end
function ImageWriter.columnsToRows(raw, tilesWide, tilesHigh, bytesPerTile)
bytesPerTile = bytesPerTile or 16
local out = {}
for y = 0, tilesHigh - 1 do
for x = 0, tilesWide - 1 do
local source = (x * tilesHigh + y) * bytesPerTile
local target = (y * tilesWide + x) * bytesPerTile
for offset = 1, bytesPerTile do
out[target + offset] = raw[source + offset]
end
end
end
return out
end
-- Inverse of pret tools/gfx --interleave (pokecrystal tools/gfx.c).
-- Build-time interleave stores each vertical 8x16 pair as consecutive 8x8
-- tiles for OBJ mode; this restores row-major sheet order for PNGs.
function ImageWriter.deinterleave(raw, width, bytesPerTile)
bytesPerTile = bytesPerTile or 16
local widthTiles = width / 8
local numTiles = #raw / bytesPerTile
local out = {}
for i = 0, numTiles - 1 do
local row = math.floor(i / widthTiles)
local src = i * 2 - (row % 2 == 1
and widthTiles * (row + 1) - 1
or widthTiles * row)
for offset = 1, bytesPerTile do
out[i * bytesPerTile + offset] = raw[src * bytesPerTile + offset]
end
end
return out
end
function ImageWriter.save(image, path)
local ok, fileData = pcall(image.encode, image, "png")
if not ok then error("could not encode " .. path .. ": " .. tostring(fileData)) end
-- CacheFs routes this to the OS save directory (normal builds) or straight
-- into the game folder (portable installs), creating parent directories as
-- needed. io.* needs the bytes as a string; love.filesystem would also
-- take the FileData, but getString() keeps one code path.
local CacheFs = require("src.import.CacheFs")
local written, writeError = CacheFs.write(path, fileData:getString())
if not written then
error("could not write " .. path .. ": " .. tostring(writeError))
end
return fileData
end
return ImageWriter