Newer
Older
Kristify / src / libs / Basalt / libraries / miniBDF.lua
@Erb3 Erb3 on 14 Nov 2022 8 KB Get ready to develop backend
---Converts a given table to a blit char.
---@param str string Something like: "100110" for \153
---@return string sChar Matching counter part
---@return boolean bReversed Swaped text & background color?
local function toBlit(str)
    if type(str) ~= "string" or #str < 6 then
        str = "000000"
    elseif str:sub(1,1) == '#' then
        return str:sub(2,2), false
    end
    local t = {}
    for i=1,6 do
        t[#t+1] = tonumber(str:sub(i,i)) or 0
    end

    if t[6]==1 then
        for i=1,5 do
            t[i] = 1-t[i]
    end end
    local n = 128
    for i=0,4 do
        n = n+t[i+1]*2^i
    end

    return string.char(n), (t[6] == 1)
end

---Converts a given char to a table,
---representing pixels in the current font.
---@param font table The font, generated with bdf.loadBDF(...)
---@param char string The char (e.g. "f")
---@return table char The char in the current font, as table
local function drawChar(font, char)
    local char = font.bitmap[char:byte()]
    -- Return empty
    if not char then
        if not font.bitmap[63] then
            local width,height = font.width,font.height
            local tmp = (' '):rep(width)
            local char = {}
            for y=1,height do
                char[#char+1] = tmp
            end
            return char
        else
            char = font.bitmap[63]
        end
    end

    -- Get bitmap
    local result = {}
    local j=0
    for i=#char,1,-1 do
        result[font.width-j-char.bound.offy-font.offy] = char[i]
        j=j+1
    end
    -- Fill gaps
    for i=1,font.height do
        if not result[i] then
            result[i] = ('0'):rep(font.width)
        else
            if char.bound.offx > 0 then
                result[i] = ('0'):rep(char.bound.offx)..result[i]
            end
            if #result[i] < font.width then
                result[i] =result[i]..('0'):rep(font.width-#result[i])
            end
        end
    end

    return result
end

---Same as drawChar but with strings.
---@param font The font, generated with bdf.loadBDF(...)
---@param str string The string (e.g. "Hello")
---@return table str The string in the current font, as table
local function drawString(font, str)
    local string = {}
    for x=1,#str do
        local char = font:drawChar(str:sub(x))
        for y=1,#char do
            if not string[y] then string[y] = "" end
            string[y] = string[y]..char[y]
        end
    end
    return string
end

local function needsSpace(len,expected)
    local add = 0
    while math.floor((len+add)/expected) ~= ((len+add)/expected) do
        add = add+1
    end
    return add
end

local blit = {[1]='0',[2]='1',[4]='2',[8]='3',[16]='4',[32]='5',[64]='6',[128]='7',[256]='8',[512]='9',[1024]='a',[2048]='b',[4096]='c',[8192]='d',[16384]='e',[32768]='f' }
---Converts a bitmap table into a one represented by box chars.
---@param str table The bitmap (could be via font:drawChar("..."))
---@param bg number The Background color
---@param fg number The Background color
---@return table blits The result
local function compress(str, bg,fg)
    local addX = needsSpace(#str[1],2)
    local addY = needsSpace(#str,3)
    if addX > 0 then
        for i=1,#str do
            str[i] = str[i]..('0'):rep(addX)
        end
    end
    if addY > 0 then
        for i=1,addY do
            str[#str+1] = ('0'):rep(#str[1])
        end
    end

    local blits = {
        ch={},
        bg={},
        fg={}
    }
    for y=1,#str/3 do
        blits.ch[y] = ""
        blits.bg[y] = ""
        blits.fg[y] = ""
    end

    for x=1,#str[1]/2 do
        for y=1,#str/3 do
            local char,invert = toBlit(
                str[y*3-2]:sub(x*2-1,x*2)..
                str[y*3-1]:sub(x*2-1,x*2)..
                str[y*3-0]:sub(x*2-1,x*2)
            )
            blits.ch[y] = blits.ch[y]..char
            local c1,c2 = blit[bg],blit[fg]
            if invert then
                c2,c1 = blit[bg],blit[fg]
            end
            blits.bg[y] = blits.bg[y]..c1
            blits.fg[y] = blits.fg[y]..c2
        end
    end
    return blits
end


local data = {
    FONT = function(font,line)
        font.name = line
    end,
    FONTBOUNDINGBOX = function(font,line)
        local type={"width","height","offx","offy"}
        local i=1
        for word in string.gmatch(line, '([^ ]+)') do
            if i > #type then return end
            font[type[i]] = tonumber(word) or 0
            i=i+1
        end
    end,
    CHARSET_REGISTRY = function(font,line)
        if line:find("ISO") and line:find("8859") then
            font.validCharReg = font.validCharReg+1
        end
    end,
    CHARSET_ENCODING = function(font,line)
        if line:find("\"1\"") then
            font.validCharReg = font.validCharReg+1
        end
    end,
    CHARS = function(font,line)
        local num = tonumber(line) or 0
        if num == 256 then
            font.validCharReg = font.validCharReg+1
        end
    end,
    STARTCHAR = function(font,line)
        font._CUR_CHAR = {name=line}
    end,
    ENCODING = function(font,line)
        if not font._CUR_CHAR then return end
        font._CUR_CHAR.byte = tonumber(line) or 0
    end,
    BBX = function(font, line)
        local i=1
        local b = {"width","height","offx","offy"}
        font._CUR_CHAR.bound = {}
        for word in string.gmatch(line, '([^ ]+)') do
            if i > #b then return end
            font._CUR_CHAR.bound[b[i]] = tonumber(word) or 0
            i=i+1
        end
    end,
    BITMAP = function(font,_)
        font.isBitmap = true
    end
}

---Parses a .BDF font file into a in lua readable table.
---@param path string The path to the .BDF file
---@return table font The result
local function loadBDF(path)
    if not (fs.exists(path) and not fs.isDir(path)) then
        return printError("Could not load: \'"..path.."\'!")
    end

    local f = fs.open(path, 'r')
    local font = {bitmap={},validCharReg=0,isBitmap=false}

    local line = f.readLine()
    repeat
        if font.isBitmap then
            if line:find("ENDCHAR") then
                -- Finishing
                font.isBitmap = false
                local byte = font._CUR_CHAR.byte
                font._CUR_CHAR.byte = nil
                font.bitmap[byte] = {}
                for k,v in pairs(font._CUR_CHAR) do
                    font.bitmap[byte][k] = v
                end
                font._CUR_CHAR = nil
            else
                local num = tonumber("0x"..line)
                if not num then return end

                local function ffs(value)
                    if value == 0 then return 0 end
                    local pos = 0;
                    while bit32.band(value, 1) == 0 do
                        value = bit32.rshift(value, 1);
                        pos = pos + 1
                    end
                    return pos
                end

                local l = ""
                local w = math.ceil(math.floor(math.log(num) / math.log(2)) / 8) * 8
                for i = ffs(num) or 0, w do
                    l = l..bit32.band(bit32.rshift(num, i-1), 1)
                end
                l = l:reverse()
                local w = font._CUR_CHAR.bound.width
                if #l > w then
                    l = l:sub(1,w)
                elseif #l < w then
                    l = l..('0'):rep(w-#l)
                end

                font._CUR_CHAR[#font._CUR_CHAR+1] = l
            end
        else
            local label = line:match('([^ ]+)')
            if data[label] then
                data[label](font, line:sub(#label+2))
            end
        end
        line = f.readLine()
    until line == nil

    f.close()
    font.isBitmap = nil
    font.validCharReg = (font.validCharReg == 3)
    font.drawChar = drawChar
    font.drawString = drawString
    return font
end

---Checks for errors a font may have. Should return 0.
---The bigger the results number, the more errors it has.
---@param font table The font that will be checked.
---@return number errors The amount of errors the font has.
---@return table messages The error messages
local function checkFont(font)
    local msg = {}
    local errorCounter = 0
    if type(font.name) ~= "string" then
        errorCounter = errorCounter+1
        table.insert(msg,"Font has no name.")
    elseif type(font.width) ~= "number" then
        errorCounter = errorCounter+1
        table.insert(msg,"Font has no global width.")
    elseif type(font.height) ~= "number" then
        errorCounter = errorCounter+1
        table.insert(msg,"Font has no global height.")
    elseif not font.validCharReg then
        errorCounter = errorCounter+1
        table.insert(msg,"Font uses not supported format.")
    elseif type(font.bitmap) ~= "table" then
        errorCounter = errorCounter+1
        table.insert(msg,"Font has no bitmaps (nothing to render)")
    elseif #font.bitmap ~= 255 then
        errorCounter = errorCounter+1
        table.insert(msg,"Font may have missing chars.")
    end
    return errorCounter,msg
end

return {
    loadBDF = loadBDF,
    checkFont = checkFont,
    compress = compress
}