API: convert image to RGB

I’m building an aseprite extension for exporting to puzzlescript. It turns parts of images into palettized strings; for example, it turns this image:

2023-02-02_092026_Aseprite

into this string:

chicken
#fff1e8 #83769c #ffa300 #be1250
.000.
.0102
00003
0000.
.2.2.

The trouble is, I want to support different colormodes (indexed, rgb), layers, and tilemaps. tilemaps are especially tricky because they’re implemented as a colormode, and the coordinates for them are completely different from the normal pixel coordinates.

I tried to make a temporary Image object with colormode RGB, draw my pixels into it no matter what the current sprite + layer’s colormode is, but I’m having trouble creating it. I think the issue might be that img:drawImage(app.activeCel.image (in prepareImage) is failing because the two images’ specs don’t match…? the image returns just fine but when I try to read pixels out of it later they’re very wrong

-- create an image that we can extract colors from
-- note: the image will have colorMode RGB, even if
-- app.activeSprite is TILEMAP or INDEXED
function prepareImage(sprite,layeronly)
  local img = Image(sprite.width,sprite.height)
  if layeronly then
    if not app.activeCel then
      app.alert("error: current layer is empty")
      return
    end
    if app.activeCel.image.colorMode==ColorMode.TILEMAP then
      img:drawImage(renderTilemapCel(app.activeCel))
    else
      img:drawImage(app.activeCel.image, app.activeCel.position) -- I believe this is failing b/c the specs don't match
    end
  else
    img:drawSprite(sprite, app.activeFrame)
  end

  return img
end


-- https://github.com/dacap/export-aseprite-file/blob/master/export.lua#L125-L132
function get_tileset_for_layer(layer)
  for i,tileset in ipairs(layer.sprite.tilesets) do
    if layer.tileset==tileset then
      return tileset
    end
  end
end

-- is there no better way to convert a tilemap cel into pixels? feels like something the API should do for me, I just haven't been able to figure out how
-- returns: an Image
function renderTilemapCel(cel)
  local tilemap = cel.image
  assert(tilemap.colorMode==ColorMode.TILEMAP)
  assert(cel.sprite.tilesets)
  assert(#cel.sprite.tilesets>0,"error: no tilesets found on a tilemap layer")

  local tileset = get_tileset_for_layer(cel.layer)
  assert(tileset)
  local size = tileset.grid.tileSize
  local img = Image(tilemap.width*size.width,tilemap.height*size.height)
  for it in tilemap:pixels() do
    local tileix = app.pixelColor.tileI(it())
    local tileimg = tileset:getTile(tileix)
    img:drawImage(tileimg,it.x*size.width,it.y*size.height)
  end
  return img
end

I can imagine a lot of different ways to make this work, but it feels like the API ought to have a function to convert any image or sprite into an RGB image – is there one that I just haven’t found yet? That would save me some time (It would need to also work with sprites that have multiple different layers, e.g. some tilemap layers and some image layers. maybe sprite:flatten() would work for that?)

Hi @pancelor,

Welcome aboard.

Here’s how I go about converting a tilemap image to a another color mode image. The function assigns the color mode of the sprite, not the source image, to the target image. As a precaution, I transfer the transparentColor and colorSpace to the target. I just grab the index from the pixel iterator; I’ve never had to use app.pixelColor.tileI, so I can’t say much about it.

---Converts an image from a tile set layer to a regular
---image. Supported in Aseprite version 1.3 or newer.
---@param imgSrc Image source image
---@param tileSet userdata tile set
---@param sprClrMode ColorMode sprite color mode
---@return Image
function AseUtilities.tilesToImage(imgSrc, tileSet, sprClrMode)
    local tileDim = tileSet.grid.tileSize
    local tileWidth = tileDim.width
    local tileHeight = tileDim.height

    -- The source image's color mode is 4 if it is a tile map.
    -- Assigning 4 to the target image when the sprite color
    -- mode is 2 (indexed) crashes Aseprite.
    local specSrc = imgSrc.spec
    local specTrg = ImageSpec {
        width = specSrc.width * tileWidth,
        height = specSrc.height * tileHeight,
        colorMode = sprClrMode,
        transparentColor = specSrc.transparentColor
    }
    specTrg.colorSpace = specSrc.colorSpace
    local imgTrg = Image(specTrg)

    local pxItr = imgSrc:pixels()
    for pixel in pxItr do
        imgTrg:drawImage(
            tileSet:getTile(pixel()),
            Point(pixel.x * tileWidth,
                pixel.y * tileHeight))
    end

    return imgTrg
end

Here is a thread about converting an indexed color mode image to RGB color mode:

With grayscale to RGB, each 16 bit hexadecimal value in an image would be converted to 32 bit; for example, 0xaabb would become 0xaabbbbbb.

For scripting projects of even moderate size, it’s tough to give decent advice. Details, such as whether you need to account for linked cels or for the relationship between group layers and their children, can totally reshape how a script is organized. So take what I say, esp. the advice below, with a grain of salt.

There’s a command to change a sprite’s color mode. I wrap this in a helper function to make it easier to use with the API’s enum constants.

---Wrapper for app.command.ChangePixelFormat which
---accepts an integer constant as an input. The constant
---should be included in the ColorMode enum: INDEXED,
---GRAY or RGB. Does nothing if the constant is invalid.
---@param format ColorMode|integer format constant
function AseUtilities.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

If you find this useful, cache the old sprite color mode, convert to RGB, convert to the old color mode upon the script’s conclusion.

Conversion shouldn’t alter the sprite too much when going from less information (gray, indexed) to more (RGB) and back again. However, if you’re concerned about that, you could try duplicating the sprite via the copy constructor, then close the duplicate when the script is finished. Duplicating a sprite would also let you flatten it.

Cheers,
Jeremy

Thank you for the help! Good to know I’m not missing something, and your code helped me figure out what I wanted to do. With some modification, here’s my working spriteToRgbImage function:

-- returns the first index in arr where arr[index]==target
local function find(arr,target)
  for k,v in ipairs(arr) do
    if v==target then return k,v end
  end
end

local function tilemapToImage(imgSrc, tileset, colorMode)
  assert(imgSrc.colorMode==ColorMode.TILEMAP,"can only call on tilemap")

  local size = tileset.grid.tileSize

  local imgDstSpec = ImageSpec(imgSrc.spec)
  imgDstSpec.colorMode = colorMode
  imgDstSpec.width  = imgSrc.width *size.width
  imgDstSpec.height = imgSrc.height*size.height

  local imgDst = Image(imgDstSpec)
  for it in imgSrc:pixels() do
    local tileimg = tileset:getTile(it())
    imgDst:drawImage(tileimg,it.x*size.width,it.y*size.height)
  end
  return imgDst
end

local function weirdImagetoRgbImage(imgSrc)
  assert(imgSrc.colorMode==ColorMode.INDEXED or imgSrc.colorMode==ColorMode.GRAYSCALE,"can only call on indexed or grayscale images")

  local imgDstSpec = ImageSpec(imgSrc.spec)
  imgDstSpec.colorMode = ColorMode.RGB

  local imgDst = Image(imgDstSpec)
  for it in imgSrc:pixels() do
    if it()~=imgSrc.spec.transparentColor then
      local col = Color(it())
      -- pq("pixel",it(),col.red,col.blue,col.green,col.alpha)
      imgDst:drawPixel(it.x,it.y,col)
    end
  end
  return imgDst
end

-- create an rgb image from the current sprite
-- properly handles tilemaps
-- properly handles indexed/grayscale images
local function spriteToRgbImage(layeronly)
  local res
  if app.activeImage.colorMode==ColorMode.TILEMAP and layeronly then
    local ti,tileset = find(app.activeSprite.tilesets,app.activeLayer.tileset)
    assert(tileset)

    res = tilemapToImage(app.activeImage,tileset,app.activeSprite.colorMode)
  else
    res = Image(app.activeSprite.spec)
    if layeronly then
      res:drawImage(app.activeCel.image,app.activeCel.position)
    else
      res:drawSprite(app.activeSprite,app.activeFrame)
      assert(res.colorMode~=ColorMode.TILEMAP)
    end
  end

  if res.colorMode==ColorMode.INDEXED or res.colorMode==ColorMode.GRAY then
    res = weirdImagetoRgbImage(res)
  end

  assert(res.colorMode==ColorMode.RGB)
  return res
end
1 Like

Now that I’ve just been in the thick of it, I think what I really want out of the API is function that takes a position and returns the RGB color at that position in the current sprite or cel, handling all the conversion between colormodes and tilemaps for me. (one function for sprites, and another one for cels).

This functionality (for sprites, at least) certainly exists inside the program in some form – you can save sprites as PNGs, you can sprite:flatten(), and you can img:drawSprite(). I just wish it was accessible to me a script programmer.