Exporting into Windows BMP save wrong format

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… :slight_smile:

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…


Visual Studio 2022 v17.9.5
Aseprite v1.3.7 - x64 ( steam )

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.

Aseprite v 1.3.7

Visual Studio 2022 v 17.10.5

HI!

Thanx for your reply first of all! :slight_smile:

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 :slight_smile:

After exporting as windows bitmap i got this .bmp header:

42 4D 46 00 08 00 00 00 00 00 46 00 00 00 38 00
00 00 00 01 00 00 00 02 00 00 01 00 20 00 03 00
00 00 00 00 08 00 12 0B 00 00 12 0B 00 00 00 00
00 00 00 00 00 00 00 00 FF 00 00 FF 00 00 FF 00
00 00 00 00 00 FF 00 00 00 FF 00 00 00 FF 00 00…

After i re-save from any painting program i got this:

42 4D 36 00 08 00 00 00 00 00 36 00 00 00 28 00
00 00 00 01 00 00 00 02 00 00 01 00 20 00 00 00
00 00 00 00 00 00 12 0B 00 00 12 0B 00 00 00 00
00 00 00 00 00 00 00 00 00 FF 00 00 00 FF 00 00
00 FF 00 00 00 FF 00 00 00 FF 00 00 00 FF 00 00…

Visual Studio does not load the first one ( sometimes ms-paint also not load it ) but no problem with the last one…

Cheers!

1 Like

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).

Row 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 42 4D 46 00 04 00 00 00 00 00 46 00 00 00 38 00
00000010 00 00 00 01 00 00 00 01 00 00 01 00 20 00 03 00
00000020 00 00 00 00 04 00 12 0B 00 00 12 0B 00 00 00 00
00000030 00 00 00 00 00 00 00 00 FF 00 00 FF 00 00 FF 00

Row 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 42 4D 36 00 03 00 00 00 00 00 36 00 00 00 28 00
00000010 00 00 00 01 00 00 00 01 00 00 01 00 18 00 00 00
00000020 00 00 00 00 03 00 12 0B 00 00 12 0B 00 00 00 00
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01

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.

Hi!

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 :slight_smile: 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. :slight_smile:

By the way, thanks for help!

1 Like