Hi!
Exported drawing ( 1 layer, null animation ) into Windows Bitmap cause a strange error message when i try to import into Visual Studio:
“Cannot load file. Unknown bitmap format.”
When I check the raw exported file it’s BMP header strange. The size of header and the offset of data is bigger than the actual. When I resave with XnView, the result will fine also the bmp header values will be correct too…
Also a properties option would be nice where i can select bitmap format ( 24 / 32, ARGB / RGBA ) when save…
edit:
I tried reopen exported bitmaps and the most third party tool ( XnView, Gimp, Krita, Pixelformet ) opens it ( only format strict MS tools like Paint and Visual Studio fail ) but after i re-save ( from those tools ) the problem is gone…
Do you have any more details for how to reproduce this problem in Visual Studio (such as the sprite’s color mode, and whether a background layer is present, size, no. of colors in the palette, sprite transparent color)? I’ve been trying and so far haven’t been able to.
Usually Aseprite writes 24 RGB when a background layer is present in an RGB color mode sprite, otherwise 32 ARGB.
I create a new image from scratch : 256x256 pixel, RGBA, Transparent, square pixels ( 1:1 ). I used default palette ( but mostly select colors from color wheel but did not add to palette ), and i have only one layer ( no transparency on the image ). ( once i create palette from image, but it does not count
After exporting as windows bitmap i got this .bmp header:
If I’m reading the hex dumps correctly, Aseprite is writing a BITMAPV3INFOHEADER, 56 bytes long (0x38), rather than BITMAPINFOHEADER, which is 40 (0x28).
There have been bugs with bmps between Godot and Aseprite in the past, and with the similar ico file format, so it wouldn’t surprise me if there are more issues here … but atm I’ve not checked that there’s an issue with Visual Studio import or Aseprite export or both. Even with more detail from your post, VS v 17.10.5 still opened a 32 bit RGBA bmp for me, it just treated it as 32 bit RGB.
Again, Aseprite typically uses a background layer as a signal that a sprite is not intended to have transparency. When I add a background layer, a different header is used and the bits per pixel changes from 32 (0x20) to 24 (0x18).
Practically speaking, the bmp file format is simple enough that it’d be faster to write a Lua script for yourself than wait around for any resolution.
local formatOptions = {
-- "IDX1", -- Not implemented.
-- "IDX4", -- Not implemented.
-- "IDX8", -- Not implemented.
-- "RGB16", -- Not implemented.
"RGB24",
-- "RGB32", -- Not implemented.
}
local dlg = Dialog { title = "Simplified Export BMP" }
dlg:combobox {
id = "formatOption",
label = "Format:",
option = "RGB24",
options = formatOptions,
focus = false,
}
dlg:newrow { always = false }
dlg:file {
id = "filename",
label = "File:",
filetypes = { "bmp" },
save = true,
focus = true
}
dlg:newrow { always = false }
dlg:button {
id = "confirm",
text = "&OK",
onclick = function()
local activeSprite = app.sprite
if not activeSprite then
app.alert { title = "Error", text = "There is no active sprite." }
return
end
local args = dlg.data
local exportFilepath = args.filename --[[@as string]]
if (not exportFilepath) or (#exportFilepath < 1) then
app.alert { title = "Error", text = "Invalid file path." }
return
end
local fileExt = app.fs.fileExtension(exportFilepath)
local fileExtLc = string.lower(fileExt)
local extIsBmp = fileExtLc == "bmp"
if (not extIsBmp) then
app.alert { title = "Error", text = "Extension must be bmp." }
return
end
local binFile <const>, err = io.open(exportFilepath, "wb")
if err ~= nil then
if binFile then binFile:close() end
app.alert { title = "Error", text = err }
return
end
if binFile == nil then return end
local activeFrObj = app.frame or activeSprite.frames[1]
local activeFrIdx = activeFrObj.frameNumber
local hasBkg = activeSprite.backgroundLayer ~= nil
local spriteSpec = activeSprite.spec
local wSprite = spriteSpec.width
local hSprite = spriteSpec.height
local flatImage = Image(spriteSpec)
flatImage:drawSprite(activeSprite, activeFrIdx)
local flatBytes = flatImage.bytes
local formatOption = args.formatOption --[[@as string]]
local bpp = 32
if formatOption == "IDX8" then
bpp = 8
elseif formatOption == "IDX4" then
bpp = 4
elseif formatOption == "IDX1" then
bpp = 2
elseif formatOption == "RGBA32" then
bpp = 32
elseif formatOption == "RGB24" then
bpp = 24
elseif formatOption == "RGB16" then
bpp = 16
end
---@type string[]
local trgStrArr = {}
local palStr = ""
local ceil = math.ceil
local strchar = string.char
local strsub = string.sub
local colorMode = spriteSpec.colorMode
if colorMode == ColorMode.RGB then
if formatOption == "IDX8" then
app.alert { title = "Error", text = "Not implemented." }
return
elseif formatOption == "IDX4" then
app.alert { title = "Error", text = "Not implemented." }
return
elseif formatOption == "IDX1" then
app.alert { title = "Error", text = "Not implemented." }
return
elseif formatOption == "RGB32" then
app.alert { title = "Error", text = "Not implemented." }
return
elseif formatOption == "RGB24" then
local bytesPerRow = 4 * ceil((wSprite * bpp) / 32)
local zeroChar = strchar(0)
local y = hSprite - 1
while y >= 0 do
---@type string[]
local rowStr = {}
local x = 0
while x < wSprite do
local i = y * wSprite + x
local i4 = i * 4
local r8Char = strsub(flatBytes, 1 + i4, 1 + i4)
local g8Char = strsub(flatBytes, 2 + i4, 2 + i4)
local b8Char = strsub(flatBytes, 3 + i4, 3 + i4)
rowStr[#rowStr + 1] = b8Char
rowStr[#rowStr + 1] = g8Char
rowStr[#rowStr + 1] = r8Char
x = x + 1
end
while #rowStr < bytesPerRow do
rowStr[#rowStr + 1] = zeroChar
end
local lenRowStr = #rowStr
local n = 0
while n < lenRowStr do
n = n + 1
trgStrArr[#trgStrArr + 1] = rowStr[n]
end
y = y - 1
end
elseif formatOption == "RGB16" then
app.alert { title = "Error", text = "Not implemented." }
return
end
elseif colorMode == ColorMode.GRAY then
app.alert { title = "Error", text = "Not implemented." }
return
elseif colorMode == ColorMode.INDEXED then
app.alert { title = "Error", text = "Not implemented." }
return
else
app.alert { title = "Error", text = "Unrecognized color mode." }
return
end
local trgStr = table.concat(trgStrArr)
local dataLen = 54 + #trgStr
local dataOffset = 54 + #palStr
local fileStr = table.concat({
"BM", -- 02
string.pack("I4", dataLen), -- 06
string.pack("I4", 0), -- 10
string.pack("I4", dataOffset), -- 14
string.pack("I4", 40), -- 18
string.pack("i4", wSprite), -- 22
string.pack("i4", hSprite), -- 26
string.pack("I2", 1), -- 30 bit planes
string.pack("I2", bpp), -- 32 bits per pixel
string.pack("I4", 0), -- 34 compression
string.pack("I4", 0), -- 38 size of compressed image
string.pack("I4", 0), -- 42 x res
string.pack("I4", 0), -- 46 y res
string.pack("I4", 0), -- 50 colors used
string.pack("I4", 0), -- 54 important colors
palStr,
trgStr
})
binFile:write(fileStr)
binFile:close()
app.alert {
title = "Success",
text = "File exported."
}
end
}
dlg:button {
id = "cancel",
text = "&CANCEL",
focus = false,
onclick = function()
dlg:close()
end
}
dlg:show {
autoscrollbars = true,
wait = false
}
You can export a 32 bit bmp with a 40 byte BITMAPINFOHEADER and it will open in GIMP, Krita, etc, but the alpha information would be ignored. I’ve found similar compatibility issues with 16 bit bmps: GIMP can write RGB565, but Krita can’t open them; I could write ARGB1555, and programs would open the bmp but except for Aseprite only RGB555 was recognized.
[Edit: Disregard the above, I’d have to research more, as it looks like the compression field is important to whether alpha is recognized or not.]
It’d be interesting to see if VS would open a bmp with a BITMAPV4HEADER instead.
I am currently not working with transparency. Your suggestion about adding a background layer solves my problem. Exported 24bpp image works fine with VS2022!
It’s very strange how Aseprite “automatically” ( not handles transparency, I still need to get used to it. I’ve always used tools where I could handle them manually at pixel level out of the box… but that’s my problem.