PixelPad - add a 1px border to tile atlas

Showcase your libraries, tools and other projects that help your fellow love users.
Post Reply
User avatar
milon
Party member
Posts: 472
Joined: Thu Jan 18, 2018 9:14 pm

PixelPad - add a 1px border to tile atlas

Post by milon »

This is a tool written in Love. It adds a 1px border to all tiles of a spritesheet. I don't know about you, but I'm no artist so I source my graphics online. They come in a large tileset with no padding between tiles. This is problematic for Love if you do any scaling etc, and I got tired of manually editing images. Save the below as main.lua in its own folder, adjust the infile, outfile, and tiles parameters (and optionally the color table), and run it. Don't compile it to a .love or it won't be able to access local images. Also note that it saves the new image in folder its run from. You should be able to specify a relative path as part of outfile if you like.

Enjoy!

Code: Select all

-- Corrected image is saved in current path - NOT Love's default storage locations - caveat emptor!


-- USER INPUT REGION
local infile = "input.png"   -- Accepts any Love-valid image type
local outfile = "output.png" -- Output filetype MUST be png (or tga)
local tiles = {w=32, h=32}   -- Size in pixels of tiles in infile
local color = {1, 1, 1, 0}   -- Border color (default is transparent white)




-- File Exists?
function exists(file)
    local ok, err, code = os.rename(file, file)
    if not ok and code == 13 then return true end -- Permission denied, but it exists
    return ok, err
end

function love.load()
    
    -- Check for overwrites
    if infile == outfile then
        local response = love.window.showMessageBox("Overwrite source file?", "Source file:\n"..infile.."\n\nAre you sure you want to overwrite it?", {"Yes", "No", enterbutton = 1}, "warning")
        if response ~= 1 then return end -- love.event.quit() doesn't fire during love.load
    elseif exists(outfile) then
        local response = love.window.showMessageBox("File exists!", "Output file already exists:\n"..outfile.."\n\nOverwrite it?", {"Yes", "No", enterbutton = 1}, "warning")
        if response ~= 1 then return end
    end
    
    -- Initialize
    local image = {img=love.graphics.newImage(infile)}
    image.w, image.h = image.img:getDimensions()
    image.wTiles = image.w / tiles.w
    image.hTiles = image.h / tiles.h
    assert(image.wTiles == math.floor(image.wTiles) and image.hTiles == math.floor(image.hTiles), "Non-integer tiles in image "..infile.."\nSize: "..image.w.." x "..image.h.."\nTile size: "..tiles.w.." x "..tiles.h)
    
    -- Initialize canvas
    canvas = {w=image.wTiles*(tiles.w+1)+1, h=image.hTiles*(tiles.h+1)+1}
    canvas.c = love.graphics.newCanvas(canvas.w, canvas.h)
    canvas.q = love.graphics.newQuad(0, 0, tiles.w, tiles.h, image.w, image.h)
    
    -- Render tiles with 1px border
    love.graphics.setCanvas(canvas.c)
    if color then love.graphics.clear(color) end
    for x = 0, image.wTiles - 1 do
        for y = 0, image.hTiles - 1 do
            canvas.q:setViewport(x * tiles.w, y * tiles.h, tiles.w, tiles.h, image.w, image.h)
            love.graphics.draw(image.img, canvas.q, x * (tiles.w + 1) + 1, y * (tiles.h + 1) + 1)
        end
    end
    love.graphics.setCanvas()
    
    -- Write image to local file
    local data = canvas.c:newImageData()
    data = data:encode("png")
    local file = io.open(outfile, "w")
    io.output(file)
    io.write(data:getString())
    io.close(file)
    
    print("done!")
end

function love.draw()
    if not canvas then
        love.event.quit()
        return
    end
    love.graphics.draw(canvas.c)
end
Any code samples/ideas by me should be considered Public Domain (no attribution needed) license unless otherwise stated.
grump
Party member
Posts: 947
Joined: Sat Jul 22, 2017 7:43 pm

Re: PixelPad - add a 1px border to tile atlas

Post by grump »

Using Canvas for this means that the output image will have premultiplied alpha, which is unexpected in png files. The output will have different colors from the input if the input uses alpha.

Doing it using ImageData:paste instead would solve that problem and probably be faster too because you save a round trip to the GPU.

Edit: you could also call love.graphics.setBlendMode('none') to prevent the alpha problem
User avatar
milon
Party member
Posts: 472
Joined: Thu Jan 18, 2018 9:14 pm

Re: PixelPad - add a 1px border to tile atlas

Post by milon »

grump wrote: Fri Oct 01, 2021 7:17 am Using Canvas for this means that the output image will have premultiplied alpha, which is unexpected in png files. The output will have different colors from the input if the input uses alpha.

Doing it using ImageData:paste instead would solve that problem and probably be faster too because you save a round trip to the GPU.

Edit: you could also call love.graphics.setBlendMode('none') to prevent the alpha problem
Thanks for letting me know! Unfortunately, I can't seem to sort out how to use ImageData:paste. I've tried creating a new blank ImageData using love.image.newImageData(w, h), and the :paste() function complains that it's receiving an Image type rather than ImageData (even though newImageData() claims to return an ImageData object...)

I also tried using the setBlendMode you suggested, and 'none' seems to be an undocumented option - at least it's not currently listed in the wiki as a BlendMode option. But it does result in slightly different filesizes compared to not setting a BlendMode, so I'm guessing it's correct.

Here's the updated code:

Code: Select all

-- PixelPad
-- Add 1 pixel padding to a sprite sheet
-- Corrected image is saved in current path - NOT Love's default storage locations - caveat emptor!


-- USER INPUT REGION
local infile = "input.png"   -- Accepts any Love-valid image type
local outfile = "output.png" -- Output filetype MUST be png (or tga)
local tiles = {w=32, h=32}   -- Size in pixels of tiles in infile
local color = {0, 0, 0, 0}   -- Border color (RGBA foramt)




-- File Exists?
function exists(file)
    local ok, err, code = os.rename(file, file)
    if not ok and code == 13 then return true end -- Permission denied, but it exists
    return ok, err
end

function love.load()
    
    -- Check for overwrites
    if infile == outfile then
        local response = love.window.showMessageBox("Overwrite source file?", "Source file:\n"..infile.."\n\nAre you sure you want to overwrite it?", {"Yes", "No", enterbutton = 1}, "warning")
        if response ~= 1 then return end -- love.event.quit() doesn't fire during love.load
    elseif exists(outfile) then
        local response = love.window.showMessageBox("File exists!", "Output file already exists:\n"..outfile.."\n\nOverwrite it?", {"Yes", "No", enterbutton = 1}, "warning")
        if response ~= 1 then return end
    end
    
    -- Initialize
    local image = {img=love.graphics.newImage(infile)}
    image.w, image.h = image.img:getDimensions()
    image.wTiles = image.w / tiles.w
    image.hTiles = image.h / tiles.h
    assert(image.wTiles == math.floor(image.wTiles) and image.hTiles == math.floor(image.hTiles), "Non-integer tiles in image "..infile.."\nSize: "..image.w.." x "..image.h.."\nTile size: "..tiles.w.." x "..tiles.h)
    
    -- Initialize canvas
    canvas = {w=image.wTiles*(tiles.w+1)+1, h=image.hTiles*(tiles.h+1)+1}
    canvas.c = love.graphics.newCanvas(canvas.w, canvas.h)
    canvas.q = love.graphics.newQuad(0, 0, tiles.w, tiles.h, image.w, image.h)
    
    -- Render tiles with 1px border
    love.graphics.setCanvas(canvas.c)
    love.graphics.setBlendMode('none')
    love.graphics.clear(color)
    for x = 0, image.wTiles - 1 do
        for y = 0, image.hTiles - 1 do
            canvas.q:setViewport(x * tiles.w, y * tiles.h, tiles.w, tiles.h, image.w, image.h)
            love.graphics.draw(image.img, canvas.q, x * (tiles.w + 1) + 1, y * (tiles.h + 1) + 1)
        end
    end
    love.graphics.setCanvas()
    
    -- Write image to local file
    local data = canvas.c:newImageData()
    data = data:encode("png")
    local file = io.open(outfile, "w")
    io.output(file)
    io.write(data:getString())
    io.close(file)
    
    print("done!")
end

function love.draw()
    if not canvas then
        love.event.quit()
        return
    end
    love.graphics.draw(canvas.c)
end
PS - I'm not terribly concerned about performance for this. For me, it's a tool that I'll use occasionally, not something that would be shipped with a game. And it works basically instantly on my crappy desktop. ;)
Any code samples/ideas by me should be considered Public Domain (no attribution needed) license unless otherwise stated.
Post Reply

Who is online

Users browsing this forum: Amazon [Bot] and 42 guests