--[[ ********************************************* * AYA CHARACTER CONVERTER * * * for use on nintendo GBA and DS * * by suwacrab * ********************************************* --]] local aya = {} aya.__index = aya local printf = function(str,...) print(str:format(...)) end --|==== lookup tables aya.pal_formats = { -- h == hex,b == binary -- > AGB palette formats rgb5a1 = 0; -- | AGGGGGRRRRRBBBBB : 16-bit : direct : 65536-color -- > NTR palette formats a3i5 = 1; -- | IIIAAAAA : 5-bit : indexed : 32-color w/ alpha a5i3 = 2; -- | IIIIIAAA : 3-bit : indexed : 8-color w/ alpha } aya.clr_formats = { -- -- | bit format : bit cnt : type : color count pal2 = 0; -- | 76543210 : 1-bit : indexed : 2-color pal4 = 1; -- | 33221100 : 2-bit : indexed : 4-color pal16 = 2; -- | 11110000 : 4-bit : indexed : 16-color pal256 = 3; -- | 00000000 : 8-bit : indexed : 256-color rgb16 = 4; -- | 16-bit RGB : 16-bit : direct : 65536-color } local fwrite = function(fname,str,mode) assert(str,("attempted to write nil to file %s"):format(fname)) local f = io.open(fname,mode or "wb") f:write(str); f:close(); printf("\t* FWRITE [%08XH] (%-32s)",#str,fname) end --|==== misc functions local strclamp = function(str,len,fill) return ( #str < len and str..(fill or " "):rep(len-#str) or #str > len and str:sub(1,len) or str ) end local strwrite = function(str,fname,mode) fwrite(fname,str,mode) end string.clamp = strclamp string.write = strwrite --|==== byte functions local tochr = string.char local function chrtonum_a16(a,i) return a[i] | (a[i+1]<<8) end local function chrtonum_a32(a,i) return chrtonum_a16(a,i) | (chrtonum_a16(a,i+2)<<16) end local tochr_8 = function(n) return tochr(n&0xFF) end local tochr_16 = function(n) return tochr_8(n)..tochr_8(n>>8) end local tochr_32 = function(n) return tochr_16(n)..tochr_16(n>>16) end --|==== read functions aya.new = function(name) local bun = setmetatable({},aya) bun:set_name(name) return bun end aya.set_name = function(bun,name) name = name or "sadza" bun.name = name:clamp(7,' ').."\0" return bun end aya.load_new = function(name,fname) local bun = aya.new(name) bun:load_file(fname) return bun end aya.load_str = function(bun,str) -- read bytes local bytes = {} do local barray = { str:byte(1,#str) } for i = 1,#str do bytes[i-1] = barray[i] end end -- read bitmap file header local header = {} do header.type = string.char(bytes[0],bytes[1]) -- 2 header.size = chrtonum_a32(bytes,0x0002) -- 4 header.res0 = chrtonum_a16(bytes,0x0006) -- 2 header.res1 = chrtonum_a16(bytes,0x0008) -- 2 header.data = chrtonum_a32(bytes,0x000A) -- 2 end -- read DIB header local dibheader = {} do local dib_offset = 0x000E dibheader.headsize = chrtonum_a32(bytes,dib_offset+0x0000) -- 4 dibheader.width = chrtonum_a32(bytes,dib_offset+0x0004) -- 4 dibheader.height = chrtonum_a32(bytes,dib_offset+0x0008) -- 4 dibheader.planes = chrtonum_a16(bytes,dib_offset+0x000C) -- 2 dibheader.bpp = chrtonum_a16(bytes,dib_offset+0x000E) -- 2 dibheader.compress = chrtonum_a32(bytes,dib_offset+0x0010) -- 4 dibheader.size = chrtonum_a32(bytes,dib_offset+0x0014) -- 4 dibheader.resW = chrtonum_a32(bytes,dib_offset+0x0018) -- 4 dibheader.resH = chrtonum_a32(bytes,dib_offset+0x001C) -- 4 dibheader.clrcnt = chrtonum_a32(bytes,dib_offset+0x0020) -- 4 -- > fix dummy 0 if dibheader.size == 0 then local width = dibheader.width local height = dibheader.height dibheader.size = (dibheader.bpp * width*height)>>3 print("new size: ",dibheader.size) end end -- read color table local palette = {} do if dibheader.bpp <= 8 then local pal_offset = 0x000E + dibheader.headsize for i = 0,(2^dibheader.bpp)-1 do local clr = {}; -- > keep in mind: colors are stored in BGRX format. -- > B,G,R,X are 1 byte long, respectively. -- > X is unused and is (probably) 0. clr[1] = bytes[pal_offset + (i*4) + 0x0002] clr[2] = bytes[pal_offset + (i*4) + 0x0001] clr[3] = bytes[pal_offset + (i*4) + 0x0000] clr[4] = bytes[pal_offset + (i*4) + 0x0003] palette[i] = clr --print(i,table.unpack(clr)) end end end -- read pixel data local bmpdata_pix = {} local bmpdata_chr = {} do local bmp_offset = header.data local bmp_size = dibheader.size -- > write char data for i = 0,bmp_size-1 do local chr = bytes[bmp_offset + i] assert(chr,"BMP character data is nil...") bmpdata_chr[i] = chr end -- read the pixel data -- > 1bpp if dibheader.bpp == 1 then -- > read the (flipped) pixel data local pixdat_flip = {} do local index = 0 for i = 0,bmp_size-1 do local chr = bmpdata_chr[i] for c = 0,7 do local p = ((chr>>c)&1) pixdat_flip[index] = p; index = index + 1 end end end -- > read the unflipped pixel data local width = dibheader.width local height = dibheader.height for y = 0,height-1 do for x = 0,width-1 do -- > read pixel from flipped bitmap local py = (height - y)-1 local pix = pixdat_flip[x + py*width] -- > write to real bitmap bmpdata_pix[x + y*width] = pix end end end -- > 4bpp if dibheader.bpp == 4 then -- > read the (flipped) pixel data local pixdat_flip = {} do local index = 0 for i = 0,bmp_size-1 do local chr = bmpdata_chr[i] local p0 = chr>>4; -- 1st pixel is in high nybble, for some reason local p1 = chr&0xF; -- 2nd pixel is in low nybble pixdat_flip[index] = p0; index = index+1; pixdat_flip[index] = p1; index = index+1; end end -- > read the unflipped pixel data local width = dibheader.width local height = dibheader.height for y = 0,height-1 do for x = 0,width-1 do -- > read pixel from flipped bitmap local py = (height - y)-1 local pix = pixdat_flip[x + py*width] -- > write to real bitmap bmpdata_pix[x + y*width] = pix end end end end -- return bun.bmpdata_pix = bmpdata_pix bun.bmpdata_chr = bmpdata_chr bun.palette = palette bun.dibheader = dibheader bun.header = header bun.bmpbytes = bytes return bun end aya.load_file = function(bun,fname) local f = io.open(fname,"rb") local str = f:read("*all") f:close(); bun:load_str(str) return bun end --|==== pixel functions aya.pix = function(bun,x,y,c) local ind = x + y * bun:width() if c then bun.bmpdata_pix[ind] = c end return bun.bmpdata_pix[ind] end aya.width = function(bun) return bun.dibheader.width end aya.height = function(bun) return bun.dibheader.height end aya.dimensions = function(bun) return bun:width(),bun:height() end --|==== write functions local rgb5a1 = function(r,g,b,a) return r | (g<<5) | (b<<10) | (a * 0x8000) end local rgba4 = function(r,g,b,a) return (a<<12) | (r<<8) | (g<<4) | (b) end local rgba8 = function(r,g,b,a) return (a<<24) | (r<<16) | (g<<8) | (b) end -- > misc saving aya.pixpack_tile = function(bun,tsize) -- vars tsize = tsize or 8 local width = bun:width() local height = bun:height() -- separate image into [tsize,tsize] tiles local tiles = {} do for y = 0,(height/tsize)-1 do for x = 0,(width/tsize)-1 do local tilebuf = {} -- > write tile into buffer for ty = 0,tsize-1 do for tx = 0,tsize-1 do local c = bun:pix(tx + x*tsize,ty + y*tsize) tilebuf[tx + ty*tsize] = c end end -- > send buffer to tile table tiles[#tiles+1] = tilebuf end end end return tiles end aya.save_pal_dat = function(bun,fmt) -- to store the converted palette local str = {} -- accepted palette formats local accepted_formats = { [aya.pal_formats.rgb5a1]=true, } -- convert all 15 or so colors for i = 0,15 do local r,g,b = table.unpack(bun.palette[i]) local clr = 0; -- if the format isn't recognized, error. assert(accepted_formats[fmt],"palette format not recognized") -- set the color according to the format -- > RGB5A1 if fmt == aya.pal_formats.rgb5a1 then clr = (i==0) and rgb5a1(0,0,0,0) or rgb5a1(0,0,0,1) clr = clr | rgb5a1(r>>3,g>>3,b>>3,0) str[#str+1] = tochr_16(clr) -- > RGBA4 elseif fmt == aya.pal_formats.rgba4 then clr = (i==0) and 0x0000 or 0xF000 clr = clr | rgba4(r>>4,g>>4,b>>4,0) -- > RGBA8 elseif fmt == aya.pal_formats.rgba8 then clr = (i==0) and rgba8(0,0,0,0) or rgba8(0,0,0,255) clr = clr | rgba8(r,g,b,0) str[#str+1] = tochr_32(clr) end end return table.concat(str) end -- > 1bpp saving aya.save_1bpp_tile_dat = function(bun,tsize) -- vars tsize = tsize or 8 local width = bun:width() local height = bun:height() -- separate image into [tsize,tsize] tiles local tiles = bun:pixpack_tile(tsize) -- write tiles to string local str = {} for t = 1,#tiles do local tile = tiles[t] for i = 0,(tsize^2)-1,8 do local b = {} for c = 0,7 do b[c] = tile[i+c] end local chr = 0 for c = 0,7 do chr = chr | (b[7-c]<0,"tile size is incorrect.") local data = bun:save_1bpp_tile_dat(tsize) -- get name print("writing image...") local name = bun.name or ("SDZACHR") name = name:clamp(7," ").."\0" -- get dimensions -- > new width is going to be the tile size -- > new height is (width*height)/tsize -- > so 32x32 with tsize 8 is 8x128 local width = tsize local height = (bun:width()*bun:height())/tsize -- write header local header = { -- > $0000 : name name; -- > $0008 : size tochr_32(width); -- width tochr_32(height); -- height -- > $0010 : data size tochr_32(#data); -- > $0014 : color format tochr_32(aya.clr_formats.pal2); -- > $0018 : palette format tochr_32(fmt); -- > $001C : character offset tochr_32(0x0024 + 0x0000); -- > $0020 : palette offset tochr_32(0x0024 + #data); } -- write string local str_header = table.concat(header) local str_data = data local str_palette = palette local str_img = str_header..str_data..str_palette print(("header size: %04X"):format(#str_header)) return str_img end -- > 2bpp saving aya.save_2bpp_tile_dat = function(bun,tsize) -- vars tsize = tsize or 8 local width = bun:width() local height = bun:height() -- separate image into [tsize,tsize] tiles local tiles = bun:pixpack_tile(tsize) -- write tiles to string local str = {} for t = 1,#tiles do local tile = tiles[t] for i = 0,(tsize^2)-1,8 do local bp = {} for p = 0,1 do bp[p] = {} end for c = 0,7 do bp[0][c],bp[1][c] = tile[i+c]&1,tile[i+c]>>1 end for p = 0,1 do local chr = 0 for c = 0,7 do chr = chr | (bp[p][c]<<(7-c)) end str[#str+1] = tochr_8(chr) end end end return table.concat(str) end aya.save_2bpp_tile = function(bun,fmt,tsize) -- write palette print("writing palette...") local palette = bun:save_pal_dat(fmt) -- write data print("writing tiled character data...") assert(tsize and tsize>0,"tile size is incorrect.") local data = bun:save_2bpp_tile_dat(tsize) -- get name print("writing image...") local name = bun.name or ("SDZACHR") name = name:clamp(7," ").."\0" -- get dimensions -- > new width is going to be the tile size -- > new height is (width*height)/tsize -- > so 32x32 with tsize 8 is 8x128 local width = tsize local height = (bun:width()*bun:height())/tsize -- write header local header = { -- > $0000 : name name; -- > $0008 : size tochr_32(width); -- width tochr_32(height); -- height -- > $0010 : data size tochr_32(#data); -- > $0014 : color format tochr_32(aya.clr_formats.pal4); -- > $0018 : palette format tochr_32(fmt); -- > $001C : character offset tochr_32(0x0024 + 0x0000); -- > $0020 : palette offset tochr_32(0x0024 + #data); } -- write string local str_header = table.concat(header) local str_data = data local str_palette = palette local str_img = str_header..str_data..str_palette print(("header size: %04X"):format(#str_header)) return str_img end -- > 4bpp saving aya.save_4bpp_tile_imm = function(bun,tile,tsize) local str = {} for i = 0,(tsize^2)-1,2 do local p0 = tile[i] local p1 = tile[i+1] str[#str+1] = tochr_8(p0 | (p1<<4)) end return table.concat(str) end aya.save_4bpp_tile_dat = function(bun,tsize) -- vars tsize = tsize or 8 local width = bun:width() local height = bun:height() -- separate image into [tsize,tsize] tiles local tiles = bun:pixpack_tile(tsize) -- write tiles to string local str = {} for t = 1,#tiles do local tile = tiles[t] str[#str+1] = bun:save_4bpp_tile_imm(tile,tsize) end return table.concat(str) end aya.save_4bpp_tile_map = function(bun,tsize) -- convert image to an array of 8x8px tiles local tiles = bun:pixpack_tile(tsize) -- vars local used = {} -- | used tiles local mapstr = {} -- | tilemap data local datstr = {} -- | character data local swpindx = 0 -- | current tile recycle index -- shit loop for i = 1,#tiles do -- > convert the current 8x8 tile local tile = bun:save_4bpp_tile_imm(tiles[i],tsize) -- > if tile hasn't been used yet, add it if not used[tile] then used[tile] = swpindx mapstr[#mapstr+1] = tochr_16(swpindx) datstr[#datstr+1] = tile swpindx = swpindx+1 -- > otherwise, use the used tile. else mapstr[#mapstr+1] = tochr_16(used[tile]) end end -- > convert map & char data to strings & return mapstr = table.concat(mapstr) datstr = table.concat(datstr) return mapstr,datstr end aya.save_4bpp_tile = function(bun,fmt,tsize,map) printf("-------- [ SAVING '%-7s' ] --------",bun.name) local dt = os.clock() -- write palette dt = os.clock() io.write("\t* writing palette... ") local palette = bun:save_pal_dat(fmt) printf("(%.8f)",os.clock() - dt) dt = os.clock() -- write data io.write("\t* writing tiled character data... ") assert(tsize and tsize>0,"tile size is incorrect.") local data = bun:save_4bpp_tile_dat(tsize) printf("(%.8f)",os.clock() - dt) dt = os.clock() -- write map local map_dat; if map then io.write("\t* writing map... ") map_dat,data = bun:save_4bpp_tile_map(tsize) printf("(%.8f)",os.clock() - dt) end -- get name dt = os.clock() io.write("\t* writing image... ") bun:set_name(bun.name) -- get dimensions -- > new width is going to be the tile size -- > new height is (width*height)/tsize -- > so 32x32 with tsize 8 is 8x128 local width = tsize local height = (bun:width()*bun:height())/tsize -- write header local header = { -- > $0000 : 0H : name bun.name; -- > $0008 : 2H : size tochr_32(width); -- width tochr_32(height); -- height tochr_32(map and bun:width()/tsize or 0); -- map width tochr_32(map and bun:height()/tsize or 0); -- map height -- > $0018 : 7H : data size tochr_32(#data); -- > $001C : 8H : map size tochr_32(map and #map_dat/2 or 0); -- > $0020 : 9H : color format,palette format tochr_32(aya.clr_formats.pal16 | (fmt<<16)); -- > $0024 : AH : character offset tochr_32(0x0030 + 0x0000); -- > $0028 : BH : palette offset tochr_32(0x0030 + #data); -- > $002C : CH : map offset tochr_32(0x0030 + #data + #palette); } -- write string local str_header = table.concat(header) local str_data = data local str_palette = palette local str_map = map_dat or "" local str_img = str_header..str_data..str_palette..str_map printf("(%.8f)",os.clock() - dt) return str_img end aya.save_4bpp_dat = function(bun) local str = {} local width,height = bun:dimensions() local bmpdata_pix = bun.bmpdata_pix for i = 0,(width*height)-1,2 do local p0,p1 = bmpdata_pix[i],bmpdata_pix[i+1] str[#str+1] = string.char(p0 | (p1<<4)) end return table.concat(str) end aya.save_4bpp = function(bun,fmt) -- write palette local palette = bun:save_pal_4bpp_dat(fmt) -- write data local data = bun:save_4bpp_dat() -- get name bun:set_name(bun.name) -- write header local header = { -- > $0000 : name bun.name; -- > $0010 : size tochr_32(bun:width()); -- width tochr_32(bun:height()); -- height -- > $0018 : data size tochr_32(#data); -- > $0014 : color format tochr_32(aya.clr_formats.pal16); -- > $0018 : palette format tochr_32(fmt); -- > $001C : character offset tochr_32(0x002C + 0x0000); -- > $0020 : palette offset tochr_32(0x002C + #data); } -- write string local str_header = table.concat(header) local str_data = data local str_palette = palette local str_img = str_header..str_data..str_palette return str_img end -- > 8bpp saving aya.save_8bpp_tile_map = function(bun,tsize) -- convert image to an array of 8x8px tiles local tiles = bun:pixpack_tile(tsize) -- vars local used = {} -- | used tiles local mapstr = {} -- | tilemap data local datstr = {} -- | character data local swpindx = 0 -- | current tile recycle index -- shit loop for i = 1,#tiles do -- > convert the current tile local tile = bun:save_8bpp_tile_imm(tiles[i],tsize) -- > if tile hasn't been used yet, add it if not used[tile] then used[tile] = swpindx mapstr[#mapstr+1] = tochr_16(swpindx) datstr[#datstr+1] = tile swpindx = swpindx+1 -- > otherwise, use the used tile. else mapstr[#mapstr+1] = tochr_16(used[tile]) end end -- > convert map & char data to strings & return mapstr = table.concat(mapstr) datstr = table.concat(datstr) return mapstr,datstr end aya.save_8bpp_tile_imm = function(bun,tile,tsize) local str = {} for i = 0,(tsize^2)-1,2 do str[#str+1] = tochr_8(tile[i]) end return table.concat(str) end aya.save_8bpp_tile_dat = function(bun,tsize) local tiles = bun:pixpack_tile(tsize) local width,height = bun:dimensions() -- write tiles to string local str = {} for t = 1,#tiles do local tile = tiles[t] str[#str+1] = bun:save_8bpp_tile_imm(tile,tsize) end return table.concat(str) end aya.save_8bpp_tile = function(bun,fmt,tsize,map) local dt = os.clock() printf("-------- [ SAVING '%-7s' ] --------",bun.name) -- write palette io.write("\t* writing palette... ") local palette = bun:save_pal_dat(fmt) printf("(%.8f)",os.clock() - dt) dt = os.clock() -- write data io.write("\t* writing tiled character data... ") assert(tsize and tsize>0,"tile size is incorrect.") local data = bun:save_8bpp_tile_dat(tsize) printf("(%.8f)",os.clock() - dt) dt = os.clock() -- write map local map_dat; if map then -- insert map function here lul map_dat,data = bun:save_8bpp_tile_map(tsize) end -- get name bun:set_name(bun.name) -- get dimensions local width = tsize local height = (bun:width()*bun:height())/tsize -- write header local header = { -- > 0000H : name bun.name; -- > 0008H : image dimensions tochr_32(width); tochr_32(height); tochr_32(map and bun:width()/tsize or 0); tochr_32(map and bun:height()/tsize or 0); -- > 0018H : data sizes tochr_32(#data),tochr_32(map and #map_dat or 0); -- > 0020H : color & palette format tochr_32(aya.clr_formats.pal16 | (fmt<<16)); -- > 0024H : character offset tochr_32(0x0024 + 0x0000); -- > 0028H : palette offset tochr_32(0x0024 + #data); -- > 002CH : map offset tochr_32(0x0024 + #data + #palette); } -- write string local str_header = table.concat(header) local str_data = data local str_palette = palette local str_map = map_dat or "" local str_img = str_header..str_data..str_palette..str_map return str_img end aya.save_8bpp_dat = function(bun) local str = {} local width,height = bun:dimensions() local pixdat = bun.bmpdata_pix for i = 0,(width*height)-1 do str[#str+1] = tochr_8(pixdat[i]) end return table.concat(str) end aya.save_8bpp = function(bun,fmt) -- write palette io.write("writing palette... ") local palette = bun:save_pal_dat(fmt) -- write data io.write("writing character data...\n") local data = bun:save_8bpp_dat() -- get name local name = bun.name or ("SDZACHR") name = name:clamp(7," ").."\0" -- write header local header = { -- > 0000H : name name; -- > 0008H : image dimensions tochr_32(bun:width()); tochr_32(bun:height()); -- > 0010H : data size tochr_32(#data); -- > 0014H : color format tochr_32(aya.clr_formats.pal16); -- > 0018H : palette format tochr_32(fmt); -- > 001CH : character offset tochr_32(0x0024 + 0x0000); -- > 0020H : palette offset tochr_32(0x0024 + #data); } -- write string local str_header = table.concat(header) local str_data = data local str_palette = palette local str_img = str_header..str_data..str_palette print(("header size: %04X"):format(#str_header)) return str_img end return aya