Adding Text Via Lua Script

Hi folks,

I found myself in need of a quick way to put a text label in a sprite via Lua script. Couldn’t find the Insert Text command in the scripting API (see also). So I wanted to share this experiment.

I made some 3x5 matrices for the glyphs I wanted, pack them as a number, then stored them in a dictionary. For example, this is the ‘A’ glyph.

..123
1  X
2 X X
3 XXX
4 X X
5 X X

In binary, with the Xs replaced by 1s, the spaces replaced by 0s and the line breaks taken out, that’s

010101111101101

The dictionary looks like this, where the glyph is the key and the binary formation is the value. It includes a space, digits 0 through 9 and capital letters A through Z.

local glyphLut = {
    [' '] = 0,
    ['0'] = 31599, -- 111101101101111
    ['1'] = 11415, -- 010110010010111
    ['2'] = 29671, -- 111001111100111
    ['3'] = 29647, -- 111001111001111
    ['4'] = 23497, -- 101101111001001
    ['5'] = 31183, -- 111100111001111
    ['6'] = 31215, -- 111100111101111
    ['7'] = 29257, -- 111001001001001
    ['8'] = 31727, -- 111101111101111
    ['9'] = 31689, -- 111101111001001
    ['A'] = 11245, -- 010101111101101
    ['B'] = 27566, -- 110101110101110
    ['C'] = 14627, -- 011100100100011
    ['D'] = 27502, -- 110101101101110
    ['E'] = 31143, -- 111100110100111
    ['F'] = 31140, -- 111100110100100
    ['G'] = 14635, -- 011100100101011
    ['H'] = 23533, -- 101101111101101
    ['I'] = 29847, -- 111010010010111
    ['J'] = 12874, -- 011001001001010
    ['K'] = 23469, -- 101101110101101
    ['L'] = 18727, -- 100100100100111
    ['M'] = 24557, -- 101111111101101
    ['N'] = 27501, -- 110101101101101
    ['O'] = 11114, -- 010101101101010
    ['P'] = 27556, -- 110101110100100
    ['Q'] = 11121, -- 010101101110001
    ['R'] = 31661, -- 111101110101101
    ['S'] = 14478, -- 011100010001110
    ['T'] = 29842, -- 111010010010010
    ['U'] = 23403, -- 101101101101011
    ['V'] = 23402, -- 101101101101010
    ['W'] = 23546, -- 101101111111010
    ['X'] = 23213, -- 101101010101101
    ['Y'] = 23186, -- 101101010010010
    ['Z'] = 29351 --  111001010100111
}

Unless I missed the syntax, lua doesn’t do bitwise literals (not like Java, with a 0b prefix, anyway). string.tonumber might do the trick, but I didn’t try it.

There are some problems with this resolution, obviously. ‘M’, and ‘W’ are particularly bad. ‘N’ is actually lowercase, so as to not be confused with ‘H’.

Here’s a function which draws each glyph to an image using the Aseprite API’s image:drawPixel.

local function displayGlyph(image, glyph, hex, xLoc, yLoc, glyphWidth, glyphHeight)
    local h = glyphHeight or 5
    local w = glyphWidth or 3
    local y = yLoc or 0
    local x = xLoc or 0
    local clr = hex or 0xffffffff
    local g = glyph or 0

    local len = w * h
    local lenn1 = len - 1

    for j = 0, lenn1, 1 do
        local shift = lenn1 - j
        local mark = (g >> shift) & 1
        if mark ~= 0 then
            image:drawPixel(x + (j % w), y + (j // w), clr)
        end
    end
end

j % w and j // w are how a one-dimensional index is converted to 2D coordinates. I’ve not tested this to make sure it works with matrices of other dimensions. You could also remove the checks to see if an input argument needs a default if you knew that this would only be called by the next function.

Here’s a function to display a string.

local function displayString(image, msg, hex, xLoc, yLoc, glyphWidth, glyphHeight)
    local h = glyphHeight or 5
    local w = glyphWidth or 3
    local y = yLoc or 0
    local x = xLoc or 0
    local clr = hex or 0xffffffff
    local msgUpper = string.upper(msg)
    local msgLen = #msgUpper
    -- print(msgUpper)
    -- print(msgLen)

    local chars = {}
    for i = 1, msgLen, 1 do
        chars[i] = msgUpper:sub(i, i)
    end

    local writeChar = xLoc
    local writeLine = yLoc
    for i = 1, msgLen, 1 do
        local ch = chars[i]
        -- print(ch)
        if ch == '\n' then
            -- Add 2, not 1, due to drop shadow.
            writeLine = writeLine + h + 2
            writeChar = xLoc
        else
            local glyph = glyphLut[ch]
            -- print(glyph)
            displayGlyph(image, glyph, clr, writeChar, writeLine, w, h)
            writeChar = writeChar + w + 1
        end
    end
end

Seems like there are a few variations on how to convert a lua string to an array of characters out there. So this might not be the best one.

Lastly, here’s all the dialog stuff.

local dlg = Dialog {
    title = "Print Message Test"
}

dlg:entry{
    id = "msg",
    label = "Message",
    text = "Lorem ipsum dolor sit amet",
    focus = "false"
}

dlg:color{
    id = "fillClr",
    label = "Fill:",
    color = Color(255, 255, 255, 255)
}

dlg:color{
    id = "shadowClr",
    label = "Shadow:",
    color = Color(0, 0, 0, 204)
}

dlg:number{
    id = "xOrigin",
    label = "Origin:",
    text = string.format("%.1f", 0),
    decimals = 5
}

dlg:number{
    id = "yOrigin",
    text = string.format("%.1f", 0),
    decimals = 5
}

dlg:check {
    id = "useShadow",
    label = "Drop Shadow:",
    selected = true
}

dlg:combobox {
    id = "alignHoriz",
    label = "Horizontal:",
    option = "LEFT",
    options = {"LEFT", "CENTER", "RIGHT"},
}

dlg:combobox {
    id = "alignVert",
    label = "Vertical:",
    option = "TOP",
    options = {"BOTTOM", "CENTER", "TOP"},
}

dlg:button{
    id = "ok",
    text = "OK",
    focus = true,
    onclick = function()
        local args = dlg.data
        if args.ok then
            local sprite = app.activeSprite
            if sprite then
                local gw = 3
                local gh = 5

                local layer = sprite:newLayer()
                local frame = app.activeFrame or 1
                local cel = sprite:newCel(layer, frame)
                local image = cel.image

                local msg = args.msg
                local msgLen = #msg
                if msg == nil or msgLen < 1 then
                    msg = "Lorem ipsum dolor sit amet"
                end
                local hexFill = args.fillClr.rgbaPixel
                local hexShd = args.shadowClr.rgbaPixel
                local xLoc = args.xOrigin or 0
                local yLoc = args.yOrigin or 0
                local alignHoriz = args.alignHoriz
                local alignVert = args.alignVert

                -- Both alignments will give inexact results due to
                -- integer arithmetic over small numbers, whether you
                -- count spaces and drop shadows, what you consider the
                -- rightmost, bottommost coord of the sprite.
                if alignHoriz == "CENTER" then
                    local gwLen = msgLen * (gw + 1)
                    xLoc = xLoc - gwLen // 2
                elseif alignHoriz == "RIGHT" then
                    local gwLen = msgLen * (gw + 1)
                    xLoc = xLoc - gwLen
                end

                -- Does not account for multiple line strings
                -- that contain '\n'. That would require a search
                -- for and sum of instances of any carriage return
                -- in the string, plus account for gap of 2 per line
                -- to account for drop shadow.
                if alignVert == "CENTER" then
                    yLoc = yLoc - (gh + 1) // 2
                elseif alignVert == "BOTTOM" then
                    yLoc = yLoc - (gh - 1)
                end

                layer.name = msg

                if args.useShadow then
                    displayString(image, msg, hexShd, xLoc, yLoc + 1, gw, gh)
                end
                displayString(image, msg, hexFill, xLoc, yLoc, gw, gh)

                app.refresh()
            else
                app.alert("There is no active sprite.")
            end
        else
            app.alert("Dialog arguments are invalid.")
        end
    end
}

dlg:button{
    id = "cancel",
    text = "CANCEL",
    onclick = function()
        dlg:close()
    end
}

dlg:show{
    wait = false
}

In the comments, I mentioned some limitations of the script. For example, I couldn’t wedge a line break into the dialog entry widget, so I didn’t bother developing a line break counter. That means vertical alignment is simplistic. Also, what if you need the font to be bigger? Maybe there’s a neat way to upscale by 2x (to 6x10), 3x (to 9x15), etc. Dunno.

Here’s the motivation: In another thread, @Olga_Galvanova brought up the palette analyzer from grafx2. It’d be nice to be able to create custom analysis tools, so as to not have to rely on a shim to external software. As a first step, we need the ability to add labels.

Hope that was a fun read, maybe gave the scripters out there some ideas!
Best,
Jeremy

2 Likes

imho in this case the easiest way to deal with different sizes would be to download suitable fonts from bit font maker gallery and convert their bitmaps to tables. if i’m not mistaken, even screen bitmap data embedded in true type fonts are included in different sizes (i mean… sometimes… if they are included at all).
or you could go full hershey and design a vector font, but that seems to me as quite overkill. maybe you could use his roman simplex directly and it would work even in tiny sizes needed here, i can’t tell.

1 Like

Thanks for the advice!

That raises a caveat for the code as it stands. A font that looks like pixel art aesthetically is not necessarily a strict uniform font that can be used with the shortcuts taken above. If a font has ascenders, descenders, variable width, offsets - any extra data that needs to be stored in addition to the 1s and 0s, I imagine you’d need to consider a data structure, or a legend explaining how you’ve packed the information into one primitive variable.

The ‘q’ and ‘A’ on the left are both 5x3 and have the same baseline. The ‘q’ and ‘W’ on the right have different baselines, different widths, but the same height.

With square glyphs, pixels can be assigned at (x, y) coordinates (assuming you’ve created a new image for the task). If a descender or ascender extends into the cell of another character, pixels must be composited instead. With uneven glyph widths and heights, the width and height of each line of text need to be calculated because there’s no longer a 1:1 between glyph dimensions and pixel dimensions. That also means text with translucent fills need some thought as to how overlapping glyphs should look.

glyphOverlap

I’ve done some work on similar before… but assuming that extraction of a bitmap from a font file is non-trivial and for the purpose of quickly displaying labels? Eh, don’t see the need for that much nuance just yet. :smiley:

Cheers,
Jeremy

1 Like

yo thats a incredible text editor, it would be cool if it was implmented as default text edditor

2 Likes

While your script is quite interesting I still want to have rectangle-based multiline Memo/Notes which would allow free text editing inside this rectangle at any time and possibility to rasterize it if you want. This will make Aseprite very valuable in art/game design by allowing easy comments/fixes.

Maybe this functionality could be added to Slices by Right clicking on them and selecting “Edit text” (it should be first option) to activate over-slice multiline text mini editor (blinking cursor, foreground color used, left & top align only, single font default to Aseprite pixel font and changable from Slice Properties). And also add “Duplicate” to slice context menu.

1 Like

and hebrew yet again, are you suggesting edit text function?, cause if yes i dont think its posssible it would need to create a special text layer like other programs aseprite doest have that

@behreandtjeremy
ah, you’re right. i was thinking only in the simplest terms: monospaced fonts only and bitmaps not extracted directly from font but rather manually rendered in predefined table (probably ascii). other metrics would be just a part of each entry. then you could easily turn that bitmap into an array in processing.
ah, processing, if we could script like that in aseprite, we would be in a realm of magic :smiley:

@Ethan_Buttazzi
yup, but editable text layers would be amazing, don’t you think?

it would be nice but we alredy have enough problems handeling tilesets right now, lets give developers a break, and only request tileset related features while its hot

1 Like

well of course i wouldn’t expect them to implement it next week, we’re just making suggestions for a future. 1.3 and tilesets are obviously a priority now.

oh interactive text tool is on the roadmap for this version


wea re gonna be in 1.3 for a while it seems

oh, that’s nice!