Sprites With Multiple Palettes Caveat

Hi all,

I stumbled onto the feature where you can create a sprite with multiple palettes. There are quite a few requests for this on Github, and I’m sure here as well. So you may already know that it is partially implemented.

A problem is that it can be triggered unintentionally and without realization. For example, I found it by opening a sequentially named .png saved with indexed color mode. When a Notice appeared asking me if I wanted to open the files as an animation, I clicked Agree.

Here are test images if you would like to try and reproduce:

multipal001 multipal002 multipal003 multipal004 multipal005

Why this matters: There are recurring issues with sprites and palettes, especially on export to .gif, but also with the concept of transparency when e.g. outline is used in indexed color mode. If you’ve had a problem like this, the possibility of multiple palettes make it harder to diagnose.

If you help others to fix this issue cluster on the forums, it is another question to add to your diagnostic checklist.

Because you can create multi-palette sprites unintentionally, each palette can have a different number of colors. A sprite’s transparent color index could point to two different colors depending on the frame, or become invalid if it points to an index that is out of bounds for the active palette.

If you add, remove or relocate frames from the sprite afterwards, it’s possible for the number of palettes and the number of frames in the sprite to go out of sync. From what I’ve tested thus far, it seems possible to have 4 palettes and 7 frames, with the frame at index 7 pointing to the palette at index 4.

There is some ability to work on this with Lua scripting. For example, you can get a palette from the palettes array. To set, ignore Sprite:setPalette; instead, resize the palette, then reassign its colors. I think you can get a frame from a palette,

for k, v in ipairs(app.activeSprite.palettes) do
    print(v.frame)
    print(v.frameNumber)
end

I don’t know how to get the appropriate palette from a given frame, though. Again, it seems possible for a palette to be used by multiple frames.

Here is a WIP script which attempts to clean up. It’s not a silver bullet, but based on what I know atm. I copypastaed supporting functions from here and here so this to stand on its own. See the sources for more comment.

local function aseColorToHex(clr, clrMode)
    if clrMode == ColorMode.RGB then
        return (clr.alpha << 0x18)
            | (clr.blue << 0x10)
            | (clr.green << 0x08)
            | clr.red
    elseif clrMode == ColorMode.GRAY then
        return clr.grayPixel
    elseif clrMode == ColorMode.INDEXED then
        return math.tointeger(clr.index)
    end
    return 0
end

local function asePalettesToHexArr(palettes)
    if palettes then
        local lenPalettes = #palettes
        local hexes = {}
        for i = 1, lenPalettes, 1 do
            local palette = palettes[i]
            if palette then
                local lenPalette = #palette
                for j = 1, lenPalette, 1 do
                    local aseColor = palette:getColor(j - 1)
                    local hex = aseColorToHex(
                        aseColor, ColorMode.RGB)
                    table.insert(hexes, hex)
                end
            end
        end

        if #hexes == 1 then
            local aMask = hexes[1] & 0xff000000
            table.insert(hexes, 1, aMask)
            hexes[3] = aMask | 0x00ffffff
        end

        return hexes
    else
        return { 0x00000000, 0xffffffff }
    end
end

local function hexArrToDict(hexes, zeroAlpha)
    local dict = {}
    local idxKey = 1
    local len = #hexes
    for i = 1, len, 1 do
        local hex = hexes[i]

        if zeroAlpha then
            local a = (hex >> 0x18) & 0xff
            if a < 1 then hex = 0x00000000 end
        end

        if not dict[hex] then
            dict[hex] = idxKey
            idxKey = idxKey + 1
        end
    end
    return dict
end

local function uniqueColors(hexes, zeroAlpha)
    local dict = hexArrToDict(hexes, zeroAlpha)
    local uniques = {}
    for k, v in pairs(dict) do
        uniques[v] = k
    end
    return uniques, dict
end

local function prependMask(hexes)
    if hexes[1] == 0x0 then return hexes end
    local cDict = hexArrToDict(hexes, false)
    local maskIdx = cDict[0x0]
    if maskIdx then
        if maskIdx > 1 then
            table.remove(hexes, maskIdx)
            table.insert(hexes, 1, 0x0)
        end
    else
        table.insert(hexes, 1, 0x0)
    end
    return hexes
end

local function hexToAseColor(hex)
    return Color(hex & 0xff,
        (hex >> 0x08) & 0xff,
        (hex >> 0x10) & 0xff,
        (hex >> 0x18) & 0xff)
end

local function changePixelFormat(format)
    if format == ColorMode.INDEXED then
        app.command.ChangePixelFormat { format = "indexed" }
    elseif format == ColorMode.GRAY then
        app.command.ChangePixelFormat { format = "gray" }
    elseif format == ColorMode.RGB then
        app.command.ChangePixelFormat { format = "rgb" }
    end
end

local activeSprite = app.activeSprite
if not activeSprite then return end

local oldColorMode = activeSprite.colorMode
app.command.ChangePixelFormat { format = "rgb" }

activeSprite.transparentColor = 0

local hexesProfile = asePalettesToHexArr(activeSprite.palettes)
local uniques, _ = uniqueColors(hexesProfile, true)
local masked = prependMask(uniques)

local aseColors = {}
local lenAseColors = #masked
for i = 1, lenAseColors, 1 do
    aseColors[i] = hexToAseColor(masked[i])
end

local palettes = activeSprite.palettes
local palettesLen = #palettes

-- Safe to assign the same Aseprite color to multiple
-- palettes because they are copied by value, not
-- passed by reference...?
-- https://github.com/aseprite/aseprite/blob/main/
-- src/app/script/palette_class.cpp#L196
app.transaction(function()
    for i = 1, palettesLen, 1 do
        local palette = palettes[i]
        palette:resize(lenAseColors)
        for j = 1, lenAseColors, 1 do
            local aseColor = aseColors[j]
            palette:setColor(j - 1, aseColor)
        end
    end
end)

changePixelFormat(oldColorMode)
app.refresh()

This summarizes what the script does:

  1. Convert sprite to RGB color mode.
  2. Set the sprite’s transparent color / mask index to zero.
  3. Gets all the colors from all the palettes in one array of 32 bit integers.
  4. Finds unique colors. Any color with zero alpha is treated as equal to clear black.
  5. Adds clear black as the first element of the palette (or relocates it).
  6. Converts the 32 bit integers to Color userdata.
  7. Sets all sprite palettes to this composite.
  8. Returns sprite to former color mode.

I originally ran into this when making a custom sprite properties. As pictured above, I’ve since developed that script to include a sprite’s number of palettes. Something like that would tell you if you have to worry about this in the first place.

A feature request would be to make the default sprite preferences show this, at least so you can debug.

My final recommendation is to go to Edit > Preferences > Alerts and to set the option labeled Open a sequence of static files as an animation to either No or Ask. A feature request would be for such a notice to indicate it will create multiple palettes.

Cheers,
Jeremy

2 Likes