fixed bitmap export (files were being written in text mode)

This commit is contained in:
wan-may 2024-07-12 23:37:50 -03:00
parent 03f1a96112
commit b8e00cf340
6 changed files with 278 additions and 101 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
build/ build/
test/ test/
backup/

2
ai.lua
View File

@ -79,7 +79,7 @@ function t.draw( nodes )
end end
function t.save( nodes ) function t.save( nodes )
return bmp.savePoints( nodes.all, "512rgb24" ) return bmp.ai( nodes.all )
end end
return t return t

364
bmp.lua
View File

@ -1,48 +1,17 @@
--Load and save the bmp formats used by DEFCON. --Load and save the bmp formats used by DEFCON.
local t = {} local t = {}
local assert = assert
local print = print
local error = error
local table = table
local math = math
local love = assert( love ) local love = assert( love )
local lfs = love.filesystem
--FFI bit-twiddling stuff.
local ffi = require 'ffi'
local bit = require 'bit'
local function getHeader( filename )
local offset = love.data.unpack( "<I4", assert(love.filesystem.read( "data", filename, 14 )), 11 )
local header, size = assert( love.filesystem.read( filename, offset ) )
print( "BMP HEADER", filename, size, "\n", string.byte( header, 1, size ) )
return header
end
local formats = { local test = {}
["512rgb24"] = {
header = getHeader( "data/earth/africa.bmp" ),
encode = function( r ) return math.floor( r * 255 ) end,
bytesPerPixel = 3,
w = 512,
h = 285,
},
["512r4"] = {
header = getHeader( "data/earth/sailable.bmp" ),
encode = function( r ) return math.floor( r * 255 / 16 ) end,
bytesPerPixel = 0.5,
w = 512,
h = 285,
},
["800r4"] = {
header = getHeader( "data/earth/travel_nodes.bmp" ),
encode = function( r ) return math.floor( r * 255 / 16 ) end,
bytesPerPixel = 0.5,
w = 800,
h = 400,
}
}
function t.header( format ) function test.load( filename )
return formats[format] and formats[format].header
end
function t.load( filename )
local imgd = love.image.newImageData( filename ) local imgd = love.image.newImageData( filename )
print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() ) print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() )
local img = love.graphics.newImage( imgd ) local img = love.graphics.newImage( imgd )
@ -50,78 +19,149 @@ function t.load( filename )
return img, imgd return img, imgd
end end
--maps an array of 1-byte pixel values (numbers 0 to 15 inclusive) function test.bitmapToWorld( x, y )
--to an array of half-byte pixel values (which can be concatenated into a string) local w, a = 360.0, 600.0 / 800.0
--BUT don't touch the first element (which is assumed to be a header or something) local h = 360.0 * a
local function foldByteArray( bytes ) x = w * ( x - 800 ) / 800 - w / 2 + 360
local nybbles = { bytes[1] } y = h * ( y - 600 ) / 600 + 180
for j = 2, #bytes / 2 do return x, y
local a, b = bytes[ 2 * j - 2 ], bytes[ 2 * j - 1 ]
nybbles[j] = string.char( 16 * a + b )
end
return nybbles
end end
local function packTwentyFour( bytes ) function test.compareData( a, b )
local twentyFour = { bytes[1] } if a == b then return true end
for j = 2, #bytes do print( "lengths:", a:len(), b:len() )
twentyFour[j] = string.char( bytes[j], bytes[j], bytes[j] ) local errors = 0
for i = 1, math.min( a:len(), b:len() ) do
local a, b = a:sub( i, i ), b:sub( i, i )
if a ~= b then
errors = errors + 1
print( "mismatch:", errors, i, string.byte( a ), string.byte( b ) )
end end
return twentyFour if errors > 1000 then break end
end
error( "test failed. output bitmap does not match!" )
end end
function t.save( data, format ) function test.worldToBitmap( x, y )
local w, h = data:getDimensions() x = 800 * ( x + 180 ) / 360
format = assert( formats[format] ) y = 600 + 800 * ( y - 180 ) / 360
local bytes = { format.header } return x, y
format.byte = 0
local i = 2
for y = h - 1, 0, -1 do
for x = 0, w - 1 do
bytes[i] = format.encode( data:getPixel(x, y) )
i = i + 1
end
end
if format.bytesPerPixel < 1 then bytes = foldByteArray( bytes )
else bytes = packTwentyFour( bytes ) end
return table.concat( bytes )
end end
--takes an array of world-space points in [-180, 180] x [-100, 100] local function getHeader( filename )
--e.g { { x = 0.5, y = 0.5 }, { x = 0.4, y = 0.6 }, }, etc. local offset = 2 + love.data.unpack( "<I4", assert(love.filesystem.read( "data", filename, 14 )), 11 )
function t.savePoints( points, format ) local header, size = assert( love.filesystem.read( filename, offset ) )
format = assert( formats[format] ) print( "BMP HEADER", filename, size, "\n", string.byte( header, 1, size ) )
return header
end
--BGR24 format. Takes the input of Love2D's data:getPixel( x, y )
--on paper we would only care about one channel because all of these images are grayscale,
--but a few of the pixels in the game's default territories are not exactly gray,
--so we do it like this to make sure our output is byte-faithful when saving an unmodified vanilla map
local function bgrChar( r, g, b )
return string.char( math.floor( b * 255 ), math.floor( g * 255 ), math.floor( r * 255 ) )
end
local formats = {
["africa"] = {
header = getHeader( "data/earth/africa.bmp" ),
w = 512,
h = 285,
},
["sailable"] = {
header = getHeader( "data/earth/sailable.bmp" ),
w = 512,
h = 285,
--technically this information is in the header already but I don't want to write code to parse it since we only use this one header
palette = {
[0] = 0,
[8] = 1,
[20] = 2,
[33] = 3,
[49] = 4,
[90] = 5,
[139] = 6,
[169] = 7,
[189] = 8,
[206] = 9,
[214] = 10,
[222] = 11,
[231] = 12,
[239] = 13,
[247] = 14,
[255] = 15
},
},
["travel"] = {
header = getHeader( "data/earth/travel_nodes.bmp" ),
w = 800,
h = 400,
},
["ai"] = {
header = getHeader( "data/earth/ai_markers.bmp" ),
w = 512,
h = 285,
},
}
function formats.ai:test( )
print "testing ai nodes"
local filename = "data/earth/ai_markers.bmp"
local img, imgd = test.load( filename )
local idx = 1
local nodes = {}
for x = 0, 511 do
for y = 0, 284 do
local r, g = imgd:getPixel( x, 284 - y )
if r > 0.5 or g > 0.5 then
local long = x * (360 / imgd:getWidth()) - 180
local lat = y * (200 / img:getHeight()) - 100
local attacking = (r > 0.5)
local node = {x = long, y = lat, attacking = attacking, idx = idx}
print( "ai marker", idx, x, y, long, lat )
nodes[ idx ] = node
idx = idx + 1
end
end
end
local encodedString = self:encode( nodes )
love.filesystem.write( "ai_markers.bmp", encodedString )
local eimg, eimgd = test.load( "ai_markers.bmp" ) --the one we just saved
for x = 0, 511 do
for y = 0, 284 do
local r, g = imgd:getPixel( x, 284 - y )
local er, eg = eimgd:getPixel( x, 284 - y )
if math.max( r, g, er, eg ) > 0.5 then
print( "node pixel: ", x, 284 - y, r, g, er, eg )
assert( ( r > 0.5 and er > 0.5 ) or ( g > 0.5 and eg > 0.5 ), "ai marker mismatch!" )
end
end
end
print( "ai markers OK" )
end
function formats.ai:encode( data )
--set up bitmap as an array of pixels --set up bitmap as an array of pixels
local w, h = format.w, format.h local w, h = self.w, self.h
local size = 2 + format.w * format.h local size = 2 + self.w * self.h
local bitmap = { format.header } local bitmap = { self.header:sub( 1, -3 ) }
for j = 2, size do bitmap[j] = 0 end for j = 2, size do bitmap[j] = 0 end
--this is black-and-white only. easy case
if format.bytesPerPixel < 1 then
for i, point in ipairs( points ) do
local wx, wy = point.x, point.y
-- get bitmap coordinates:
-- N.B., y's divided by 190 rather than 200 because of an idiosyncracy in the travel node image
local x, y = math.floor(format.w * (wx + 180) / 360), math.floor(format.h * (wy + 100) / 190)
--get index into byte array for i, point in ipairs( data ) do
local idx = 2 + y * format.w + x
print( "SAVING POINT:", wx, wy, x, format.h - y - 1, idx )
bitmap[idx] = 1 --white: this header uses an indexed palette
end
--now map pixels to 4-bit strings, slap on the header, and pass it back up
return table.concat( foldByteArray( bitmap ) )
end
--cf. ai.lua: these points can be red or green
for i, point in ipairs( points ) do
local wx, wy = point.x, point.y local wx, wy = point.x, point.y
--get bitmap coordinates --get bitmap coordinates
local x, y = math.floor(format.w * (wx + 180) / 360), math.floor(format.h * (1.0 - (wy + 100) / 200)) local x = math.floor(self.w * (wx + 180) / 360)
--idk why exactly I need to round the value here instead of truncating it
local y = math.floor(0.5 + self.h * (wy + 100) / 200)
print( "export ai marker", i, x, y, wx, wy )
--get index into byte array --get index into byte array
local idx = 2 + x * format.h + y local idx = 2 + y * self.w + x
--in-editor we could have two points in the same place, possibly of the same or different types --in-editor we could have two points in the same place, possibly of the same or different types
--in case we miss something upstream, don't corrupt these duplicate points, --in case we miss something upstream, don't corrupt these duplicate points,
--just saturate the attack/defense value --just saturate the attack/defense value
@ -142,4 +182,138 @@ function t.savePoints( points, format )
return table.concat( bitmap ) return table.concat( bitmap )
end end
function formats.travel:test()
print "testing travel nodes"
local filename = "data/earth/travel_nodes.bmp"
local img, imgd = test.load( filename )
local nodes = {}
local n = 1
for x = 0, 799 do
for y = 0, 399 do
if imgd:getPixel( x, 399 - y ) > 0 then
local long, lat = test.bitmapToWorld( x, y )
nodes[n] = {x = long, y = lat, idx = n}
print( "read:", n, long, lat, x, y )
n = n + 1
end
end
end
print( "loaded ", n, "nodes" )
local encodedString = self:encode( nodes )
test.compareData( love.filesystem.read( filename ), encodedString )
love.filesystem.write( "travel_nodes.bmp", encodedString )
print( "travel nodes OK" )
end
--Sparse bitmap with a few white pixels.
function formats.travel:encode( points )
--set up bitmap as an array of 8-bit pixels
local w, h = self.w, self.h
local size = 2 + w * h
local bitmap = { self.header }
for j = 2, size do bitmap[j] = 0 end
--write white pixels
for i, point in ipairs( points ) do
local wx, wy = point.x, point.y
-- get bitmap coordinates:
local x = 800 * ( wx + 180 ) / 360
local y = 600 + 800 * ( wy - 180 ) / 360
local offset = math.floor( y * self.w + x - 2 )
--1 := white
bitmap[offset] = 1
print( "encoded:", point.idx, wx, wy, x, y )
end
--fold into 4-bit pixel array
local nybbles = { bitmap[1] }
for j = 2, size / 2 do
local a, b = bitmap[ 2 * j - 2 ], bitmap[ 2 * j - 1 ]
nybbles[j] = string.char( 16 * a + b )
end
return table.concat( nybbles )
end
function formats.sailable:test()
print "testing sailable"
local filename = "data/earth/sailable.bmp"
local img, imgd = test.load( filename )
local encoded = self:encode( imgd )
love.filesystem.write( "sailable_out.bmp", encoded )
test.compareData( love.filesystem.read( filename ), encoded )
print "sailable OK"
end
function formats.sailable:encode( data )
local w, h = self.w, self.h
local bytes = { self.header:sub( 1, -2 ) }
local i = 2
--y coordinates are written top to bottom
for y = h - 1, 0, -1 do
for x = 0, w - 1 do
bytes[i] = assert( self.palette[ math.floor( data:getPixel(x, y) * 255 )] )
i = i + 1
end
end
--fold into 4-bit pixel array
local nybbles = { bytes[1] }
for j = 2, #bytes / 2 do
local a, b = bytes[ 2 * j ], bytes[ 2 * j + 1 ]
nybbles[j] = string.char( 16 * a + b )
end
return table.concat( nybbles )
end
function formats.africa:test()
print "testing africa"
local filename = "data/earth/africa.bmp"
local img, imgd = test.load( filename )
local encoded = self:encode( imgd )
love.filesystem.write( "africa_out.bmp", encoded )
test.compareData( love.filesystem.read( filename ), encoded )
print "africa OK"
end
function formats.africa:encode( data )
local w, h = self.w, self.h
local bytes = { self.header:sub( 1, -3 ) }
local i = 2
--y coordinates are written top to bottom
for y = h - 1, 0, -1 do
for x = 0, w - 1 do
bytes[i] = bgrChar( data:getPixel(x, y) )
i = i + 1
end
end
return table.concat( bytes )
end
function t.load( filename )
local imgd = love.image.newImageData( filename )
print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() )
local img = love.graphics.newImage( imgd )
img:setFilter( "nearest", "nearest" )
return img, imgd
end
function t.save( data, format )
return assert(formats[format]):encode( data )
end
--convenience
formats.territory = formats.africa
for fmt, tbl in pairs( formats ) do
t[fmt] = function( data ) return tbl:encode( data ) end
--TESTING
tbl:test()
end
return t return t

View File

@ -179,10 +179,12 @@ function map.draw()
end end
local function write( filename, string ) local function write( filename, string )
--
print( "Writing", string:len(), "bytes to", filename ) print( "Writing", string:len(), "bytes to", filename )
os.rename( filename, filename..".bak" ) --just in case :^) os.rename( filename, filename..".bak" ) --just in case :^)
local file = assert( io.open( filename, "w+" ) ) local file = assert( io.open( filename, "wb+" ) )
assert( file:write( string ) ) assert( file:write( string ) )
assert( file:flush() ) --your toilet is set to stun, not kill
file:close() file:close()
end end

View File

@ -143,9 +143,9 @@ function t.computeBorder( territory, threshold, key )
end end
function t.save( territory ) function t.save( territory )
local fmt = (territory.name == "sailable") and "512r4" or "512rgb24" local fmt = (territory.name == "sailable") and "sailable" or "territory"
print( "saving bitmap: ", territory.name, fmt ) print( "saving bitmap: ", territory.name, fmt )
return bmp.save( territory.imgd, fmt ) return bmp[fmt]( territory.imgd )
end end
return t return t

View File

@ -153,7 +153,7 @@ function t.drawConnections( nodes )
end end
function t.save( nodes ) function t.save( nodes )
return bmp.savePoints( nodes.nodes, "800r4") return bmp.travel( nodes.nodes )
end end
return t return t