No prob. To your main issue: for a GUI script opened from within Aseprite, what I do is make a Dialog with a file widget or widgets. A text entry widget could also be used. A file
widget can be made more like an entry
widget with a boolean.
local supportedImportExts <const> = {
"ase", "aseprite", "bmp", "flc", "fli",
"gif", "ico", "jpeg", "jpg", "pcc", "pcx",
"png", "tga", "webp"
}
local supportedExportExts <const> = {
"ase", "aseprite", "gif", "webp"
}
local dlg <const> = Dialog { title = "File Widget Example" }
dlg:file {
id = "openPath",
label = "Open:",
-- If you don't want the file widget to have a text entry field, don't just
-- set entry to false, remove the parameter entirely.
entry = true,
open = true,
filename = "",
filetypes = supportedImportExts,
focus = false,
}
dlg:file {
id = "savePath",
label = "Save:",
entry = true,
save = true,
filename = "",
filetypes = supportedExportExts,
focus = false,
}
dlg:button {
id = "confirmButton",
text = "&OK",
focus = true,
onclick = function()
local args <const> = dlg.data
local openPath <const> = args.openPath --[[@as string]]
local savePath <const> = args.savePath --[[@as string]]
local openIsFolder <const> = app.fs.isDirectory(openPath)
print("openIsFolder: " .. (openIsFolder and "true" or "false"))
local openIsFile <const> = app.fs.isFile(openPath)
print("openIsFile: " .. (openIsFile and "true" or "false"))
local openFileExt <const> = app.fs.fileExtension(openPath)
local openFileExtLc <const> = string.lower(openFileExt)
print(string.format("openFileExtLc: \"%s\"", openFileExtLc))
-- In Aseprite 1.3, this was changed to app.sprite.
---@diagnostic disable-next-line: deprecated
local activeSprite <const> = app.activeSprite
if activeSprite then
-- As an alternative, could use the activeSprite, if it exists.
-- It's possible that a sprite is active, but that it hasn't been
-- saved anywhere yet.
local spriteFilename <const> = activeSprite.filename
print(string.format("spriteFilename: %s", spriteFilename))
end
-- You'd expect both of these to be false if the file has not been
-- created yet (i.e., is not being overwritten).
local saveIsFolder <const> = app.fs.isDirectory(savePath)
print("saveIsFolder: " .. (saveIsFolder and "true" or "false"))
local saveIsFile <const> = app.fs.isFile(savePath)
print("saveIsFile: " .. (saveIsFile and "true" or "false"))
local saveFileExt <const> = app.fs.fileExtension(savePath)
local saveFileExtLc <const> = string.lower(saveFileExt)
print(string.format("saveFileExtLc: \"%s\"", saveFileExtLc))
end
}
dlg:button { id = "closeButton", text = "&CANCEL", focus = false }
dlg:show { wait = false }
The main logic would run when the okay button is clicked. I usually give the save file path to Sprite:saveAs. You’d have to check if there are any differences with app.command.SaveFileAs that matter to you. I’m not sure if relative vs. absolute paths is a problem in your case.
For a task of this size, a great many other issues can crop up. If the script is just for your use, many wouldn’t matter. It does make it hard to say what else to tell you without flooding the conversion, though. If you’d rather have more info than less, see below:
To expand, click the arrow to the left
Below is a draft CLI script that accepts directories for both the open and save file paths. Instead of duplicating a sprite then whittling down with deleteLayer, it builds up by copying sprite elements. Not all elements of a sprite have a copy constructor, and the properties in each can change per version of Aseprite (1.2.40 vs. 1.3 release candidate).
--[[ This does not handle tile maps, slices or linked cels. ]]
local supportedImportExts <const> = {
"ase", "aseprite", "bmp", "flc", "fli",
"gif", "ico", "jpeg", "jpg", "pcc", "pcx",
"png", "tga", "webp"
}
local supportedExportExts <const> = {
"ase", "aseprite", "gif", "webp"
}
-- Defaults in case the user does not specify an argument for a parameter.
local defaultIncludeLocked <const> = true
local defaultIncludeHidden <const> = false
local defaultIncludeBkg <const> = true
local defaultExportExt <const> = "aseprite"
---@param fileExt string
---@param supported string[]
---@return boolean
local function isExtSupported(fileExt, supported)
local lenSupported <const> = #supported
local fileExtLc <const> = string.lower(fileExt)
local isSupported = false
local j = 0
while (not isSupported) and j < lenSupported do
j = j + 1
isSupported = fileExtLc == supported[j]
end
return isSupported
end
---@param layer Layer
---@param array Layer[]
---@param includeLocked boolean
---@param includeHidden boolean
---@param includeBkg boolean
---@return Layer[]
local function appendLeaves(
layer, array,
includeLocked, includeHidden, includeBkg)
if (includeLocked or layer.isEditable)
and (includeHidden or layer.isVisible) then
if layer.isGroup then
local childLayers <const> = layer.layers --[=[@as Layer[]]=]
local lenChildLayers <const> = #childLayers
local i = 0
while i < lenChildLayers do
i = i + 1
appendLeaves(childLayers[i], array,
includeLocked, includeHidden, includeBkg)
end
elseif (not layer.isReference)
and (includeBkg or (not layer.isBackground)) then
array[#array + 1] = layer
end
end
return array
end
---@param arg string
---@param defaultValue boolean
---@return boolean
local function parseBool(arg, defaultValue)
if arg then
local argLc <const> = string.lower(arg)
if argLc == "t" or argLc == "true" or argLc == "1" then
return true
end
if argLc == "f" or argLc == "false" or argLc == "0" then
return false
end
end
return defaultValue
end
if app.isUIAvailable then
print("This script was designed for batch mode from CLI.")
return
end
local params <const> = app.params
-- See https://github.com/aseprite/api/blob/main/Changes.md .
-- Depending on whether you're targeting 1.2.40 or 1.3, certain properties
-- will throw an error, for example cel zIndex, sprite user data, tag user data,
-- tag repeat count.
local apiVersion <const> = app.apiVersion
print(string.format("apiVersion: %d", apiVersion))
local openDirPath <const> = params["opendirpath"] or params["o"]
if not openDirPath then
print("Please supply an argument to the parameter \"opendirpath\".")
return
end
if not app.fs.isDirectory(openDirPath) then
print("\"openDirPath\" is not an existing directory.")
return
end
print(string.format("openDirPath: \"%s\"", openDirPath))
local saveDirPath = params["savedirpath"] or params["s"]
if not saveDirPath then
saveDirPath = openDirPath
end
if not app.fs.isDirectory(saveDirPath) then
print("\"saveDirPath\" is not an existing directory.")
return
end
print(string.format("saveDirPath: \"%s\"", saveDirPath))
local relFilePaths <const> = app.fs.listFiles(openDirPath)
local lenRelFilePaths <const> = #relFilePaths
print(string.format("lenRelFilePaths: %d", lenRelFilePaths))
local includeLocked = parseBool(params["includelocked"], defaultIncludeLocked)
print("includeLocked: " .. (includeLocked and "true" or "false"))
local includeHidden = parseBool(params["includehidden"], defaultIncludeHidden)
print("includeHidden: " .. (includeHidden and "true" or "false"))
local includeBkg = parseBool(params["includebkg"], defaultIncludeBkg)
print("includeBkg: " .. (includeBkg and "true" or "false"))
local trgExt = defaultExportExt
if params["trgext"] then
local trgIsSupported <const> = isExtSupported(params["trgext"], supportedExportExts)
if trgIsSupported then
trgExt = params["trgext"]
end
end
print(string.format("trgExt: \"%s\"", trgExt))
local i = 0
while i < lenRelFilePaths do
i = i + 1
local relFilePath <const> = relFilePaths[i]
local fileExt <const> = app.fs.fileExtension(relFilePath)
local isSupported <const> = isExtSupported(fileExt, supportedImportExts)
if isSupported then
local absFilePath <const> = app.fs.joinPath(openDirPath, relFilePath)
local srcSprite <const> = Sprite { fromFile = absFilePath }
if srcSprite then
print(string.format(
"srcSprite %d: \"%s\"",
i, relFilePath))
-- Cache sprite data arrays.
local srcFrames <const> = srcSprite.frames
local srcTopLayers <const> = srcSprite.layers
local srcTags <const> = srcSprite.tags
-- Measure length of data arrays.
local lenSrcFrames <const> = #srcFrames
local lenSrcTopLayers <const> = #srcTopLayers
local lenSrcTags <const> = #srcTags
-- For indexed color mode sprites opened as a sequence, there is an
-- off-chance that there may be more than one palette in the
-- palettes array. See:
-- https://community.aseprite.org/t/sprites-with-multiple-palettes-caveat/14327
local srcPalette <const> = srcSprite.palettes[1]
local srcSpec <const> = srcSprite.spec
-- local srcData <const> = srcSprite.data
-- Aseprite does not validate Sizes. Extra validation may be
-- required in the form of reducing the ratio.
local srcPxRatio <const> = srcSprite.pixelRatio
local xSrcPxRatio <const> = math.max(1, math.abs(srcPxRatio.width))
local ySrcPxRatio <const> = math.max(1, math.abs(srcPxRatio.height))
local trgPxRatio <const> = Size(xSrcPxRatio, ySrcPxRatio)
---@type Layer[]
local srcLeaves <const> = {}
local k = 0
while k < lenSrcTopLayers do
k = k + 1
local srcTopLayer <const> = srcTopLayers[k]
appendLeaves(
srcTopLayer, srcLeaves,
includeLocked,
includeHidden,
includeBkg)
end
-- Ignore files that already consist of only one layer.
local lenSrcLeaves <const> = #srcLeaves
if lenSrcLeaves > 1 then
local m = 0
while m < lenSrcLeaves do
m = m + 1
local srcLeaf <const> = srcLeaves[m]
local srcIsBkg <const> = srcLeaf.isBackground
-- Assumes that the layer name does not include
-- invalid characters that would compromise the save.
local srcLeafBlendMode <const> = srcIsBkg
and BlendMode.NORMAL
or srcLeaf.blendMode
local srcLeafColor <const> = srcLeaf.color
local srcLeafData <const> = srcLeaf.data
local srcLeafName <const> = srcLeaf.name
local srcLeafOpacity <const> = srcIsBkg
and 255
or srcLeaf.opacity
local trgSpriteName <const> = string.format(
"%s_%s.%s",
app.fs.fileTitle(absFilePath),
srcLeafName,
trgExt)
local trgSprite <const> = Sprite(srcSpec)
trgSprite.filename = trgSpriteName
trgSprite.pixelRatio = trgPxRatio
trgSprite:setPalette(srcPalette)
-- trgSprite.data = srcData
-- Copy layer properties from source to target.
local trgLayer <const> = trgSprite.layers[1]
trgLayer.blendMode = srcLeafBlendMode
trgLayer.color = srcLeafColor
trgLayer.data = srcLeafData
trgLayer.name = srcLeafName
trgLayer.opacity = srcLeafOpacity
local n = 0
while n < lenSrcFrames do
n = n + 1
local srcFrame <const> = srcFrames[n]
local srcDuration <const> = srcFrame.duration
-- Create new frames in target if needed.
local trgFrame <const> = n <= #trgSprite.frames
and trgSprite.frames[n]
or trgSprite:newEmptyFrame(n)
trgFrame.duration = srcDuration
local srcCel <const> = srcLeaf:cel(n)
if srcCel then
-- Copy cel properties from source to target.
local srcCelColor <const> = srcCel.color
local srcCelData <const> = srcCel.data
local srcCelImg <const> = srcCel.image
local srcCelOpacity <const> = srcCel.opacity
local srcCelPos <const> = srcCel.position
-- local srcCelZIndex <const> = srcCel.zIndex
local trgCel <const> = trgSprite:newCel(
trgLayer, n, srcCelImg, srcCelPos)
trgCel.color = srcCelColor
trgCel.data = srcCelData
trgCel.opacity = srcCelOpacity
-- trgCel.zIndex = srcCelZIndex
end
end
local lenTrgFrames = #trgSprite.frames
local o = 0
while o < lenSrcTags do
o = o + 1
local srcTag <const> = srcTags[o]
local srcOrigFrame <const> = srcTag.fromFrame
local srcDestFrame <const> = srcTag.toFrame
if srcOrigFrame and srcDestFrame then
local srcOrigFrIdx <const> = srcOrigFrame.frameNumber
local srcDestFrIdx <const> = srcDestFrame.frameNumber
-- Past export bugs may result in tags that extend
-- beyond the number of frames held by the sprite.
if srcOrigFrIdx >= 1
and srcDestFrIdx <= lenTrgFrames then
-- Copy tag properties from source to target.
local srcTagAniDir <const> = srcTag.aniDir
local srcTagColor <const> = srcTag.color
-- local srcTagData <const> = srcTag.data
local srcTagName <const> = srcTag.name
-- local srcTagRepeats <const> = srcTag.repeats
local trgTag <const> = trgSprite:newTag(
srcOrigFrIdx, srcDestFrIdx)
trgTag.aniDir = srcTagAniDir
trgTag.color = srcTagColor
-- trgTag.data = srcTagData
trgTag.name = srcTagName
-- trgTag.repeats = srcTagRepeats
end
end
end
if srcIsBkg then
app.activeLayer = trgLayer
app.command.BackgroundFromLayer()
else
-- An artifact of the sprite's initial creation is that
-- it may contain an empty cel.
local cel1 <const> = trgLayer:cel(1)
if cel1 and cel1.image:isEmpty() then
trgSprite:deleteCel(cel1)
end
end
local savePath <const> = app.fs.joinPath(
saveDirPath,
trgSprite.filename)
local result <const> = trgSprite:saveAs(savePath)
if result then
print(string.format("Saved \"%s\".", savePath))
end
trgSprite:close()
end
else
print(string.format(
"\"%s\" was skipped because it only had one layer.",
relFilePath))
end
srcSprite:close()
end
else
print(string.format(
"\"%s\" was skipped because of an unsupported extension.",
relFilePath))
end
end
I didn’t handle it in the code above, but beware that Aseprite does little to no string validation. A perfectly acceptable layer name would be “*!..&@$”. However, these would cause a problem when joined into a file path. (The same is true of a tag name.)
It’s also possible for two layer (or tag) names to be the same. Names are not unique identifiers. For layers, other properties that might clarify its uniqueness would be, say, its stack index relative to its parent group.
You also have different types of layers (reference, group, background) and layers you hide (turn off visibility) that you might not want exported.