Crop Transparent Pixels From Cel Images For Imported Gifs

Hi folks,

This post could fall under both the scripts and feature request category.

I noticed that when I import a .gif with a transparent background, for example,

infinity

into Aseprite, the cel images (or layer edges) are not closely cropped to exclude transparent pixels. I’m using version 1.3-beta6 on Windows 10.

To show what I mean, when I hold down the Ctrl key (or go to View > Show > Layer Edges), I expect to see this:

not this:

This might not seem all that important. But any Lua script that iterates over a cel image’s pixels is performance sensitive; such scripts need to be handed as little work to do as possible.

To resolve this, I adapted code from Oleg Mikhailov on Stack Overflow to Lua. Below is the relevant excerpt from a larger file.

---Creates a copy of the image where excess
---transparent pixels have been trimmed from
---the edges. Adapted from the Stack Overflow
---implementation by Oleg Mikhailov:
---https://stackoverflow.com/a/36938923 .
---Returns a tuple containing the cropped image,
---the top left x and top left y. The top left
---should be added to the position of the cel
---that contained the source image.
---@param image table aseprite image
---@return table
---@return number
---@return number
function AseUtilities.trimImageAlpha(image)
    local width = image.width
    local height = image.height
    local left = 0
    local top = 0
    local right = width - 1
    local bottom = height - 1
    local minRight = width - 1
    local minBottom = height - 1

    local breakTop = false
    for t = top, bottom - 1, 1 do
        for x = 0, width - 1, 1 do
            if image:getPixel(x, t) & 0xff000000 ~= 0 then
                minRight = x
                minBottom = t
                top = t
                breakTop = true
                break
            end
        end
        if breakTop then break end
    end

    local breakLeft = false
    for l = left, minRight - 1, 1 do
        for y = height - 1, top + 1, -1 do
            if image:getPixel(l, y) & 0xff000000 ~= 0 then
                minBottom = y
                left = l
                breakLeft = true
                break
            end
        end
        if breakLeft then break end
    end

    local breakBottom = false
    for b = bottom, minBottom + 1, -1 do
        for x = width - 1, left, -1 do
            if image:getPixel(x, b) & 0xff000000 ~= 0 then
                minRight = x
                bottom = b
                breakBottom = true
                break
            end
        end
        if breakBottom then break end
    end

    local breakRight = false
    for r = right, minRight, -1 do
        for y = bottom, top, -1 do
            if image:getPixel(r, y) & 0xff000000 ~= 0 then
                right = r
                breakRight = true
                break
            end
        end
        if breakRight then break end
    end

    local trgWidth = 1 + (right - left)
    local trgHeight = 1 + (bottom - top)
    local target = Image(trgWidth, trgHeight)

    local sampleRect = Rectangle(left, top, trgWidth, trgHeight)
    local srcItr = image:pixels(sampleRect)

    for elm in srcItr do
        target:drawPixel(elm.x - left, elm.y - top, elm())
    end

    return target, left, top
end

Then I used this in the following way:

dofile("../support/aseutilities.lua")

local activeSprite = app.activeSprite
if activeSprite then
    local cels = activeSprite.cels
    local celsLen = #cels
    local trimImage = AseUtilities.trimImageAlpha
    for i = 1, celsLen, 1 do
        local cel = cels[i]
        local srcPos = cel.position
        local srcImg = cel.image
        if srcImg then
            local trgImg, x, y = trimImage(srcImg)
            cel.position = Point(srcPos.x + x, srcPos.y + y)
            cel.image = trgImg
        end
    end

    app.refresh()
else
    app.alert("There is no active sprite.")
end

If any of this was part of the GUI or scripting API and I just didn’t realize it, whoops! My bad! Feel free to disregard this post. Otherwise, on to the feature requests:

I think it’d be nice if getting a subset of an image were part of the scripting API. In other words, to replace the need for this mess:

local target = Image(trgWidth, trgHeight)
local sampleRect = Rectangle(left, top, trgWidth, trgHeight)
local srcItr = image:pixels(sampleRect)
for elm in srcItr do
    target:drawPixel(elm.x - left, elm.y - top, elm())
end

This could be a new method or could be added to existing functions like the constructor – Image(sourceImage, x, y, w, h) – or to clone – sourceImage:clone(x, y, w, h) – where x and y default to 0 and w and h default to the source image’s width and height. You could also specify the parameters as top-left x and y and bottom-right x and y.

And then secondly, the trim or crop routine above would probably be faster if done in C++, not Lua.

What I don’t know is whether it’s as important for general Aseprite users working via the GUI as it is for scripters. Do people notice or care if an imported image doesn’t have close-cropped cel images?

[Edit: Changing the canvas size with the Trim content Outside the canvas checkbox marked as true will trim a cel image smaller than sprite bounds, but it’s not exactly what I have in mind. It’s possible for a cel to have image data that extends beyond the canvas, and this data would be lost.]

I case you wanted more motivation for why I’m posting this: I thought this thread on baking Onion Skins was a cool idea and tried writing a script for it. Using Akuma from Street Fighter Alpha 2 ( source ) as a test, I could make this well enough,

akumaRedShadow

even though the script was slow. This, however, is more sophsiticated and requires a bigger refactor to make it easier to create:

akumaPurpleShadow

All that I wrote above about trimming cel images came about from hunting the sources of slow-down in the onion skinning script.

Thanks for reading! Have a good weekend all. :wink:
Jeremy

3 Likes