├── README.md ├── doc ├── EXAMPLES.md ├── FILE_FORMAT.md ├── INSTALLATION.md ├── LICENSE ├── USAGE.md └── examples │ ├── bmp2ansi.lua │ ├── gen_test_image.lua │ └── images │ ├── hello_world.bmp │ ├── test_01.bmp │ ├── test_02.bmp │ └── test_03.bmp ├── lua-bitmap-0.0-1.rockspec ├── lua-bitmap-0.0-2.rockspec ├── lua-bitmap-0.0-3.rockspec ├── lua-bitmap-scm-1.rockspec └── lua └── lua-bitmap ├── init.lua └── test_ints.lua /README.md: -------------------------------------------------------------------------------- 1 | # lua-bitmap 2 | 3 | This single-file Lua-only library implements basic read/write support 4 | for a subset of the `Windows Bitmap`/`device-independent bitmap` 5 | file format, version 3.0. 6 | 7 | Compatible with Lua5.1, LuaJIT, Lua5.2, Lua5.3, Lua5.4. 8 | 9 | 10 | 11 | ## Supported file format 12 | 13 | `Windows Bitmap`/`device-independent bitmap`, version 3.0, 14 | 15 | * 24bpp/32bpp bitmaps only(no 1/2/4/8/16bpp) 16 | * no bitfields 17 | * no compression 18 | 19 | See [doc/FILE_FORMAT.md](doc/FILE_FORMAT.md) 20 | 21 | 22 | ## Library usage 23 | 24 | Library usage is documented in `doc/USAGE.md`. 25 | 26 | Example: 27 | 28 | ``` 29 | local Bitmap = require("lua-bitmap") -- load module 30 | local bmp = Bitmap.empty_bitmap(width, height, alpha) 31 | --local bmp = Bitmap.from_string(data) 32 | --local bmp = Bitmap.from_file(path) 33 | --local bmp = Bitmap._new_bitmap() 34 | r,g,b,a = bmp:get_pixel(x,y) -- get color value at x,y 35 | bmp:set_pixel(x,y,r,g,b,a) -- set pixel at x,y to r,g,b,a[0-255] 36 | ``` 37 | 38 | See [doc/USAGE.md](doc/USAGE.md) 39 | 40 | 41 | 42 | ## Installation 43 | 44 | The LuaRocks module is not published on a rocks server yet. 45 | 46 | See [doc/INSTALLATION.md](doc/INSTALLATION.md) 47 | 48 | 49 | 50 | ## Examples 51 | 52 | There are currently two runnable examples, `bmp2ansi.lua` coverts a 53 | bitmap to primitive ASCII art, and `gen_test_image.lua` generates a 54 | simple bitmap with a test pattern. 55 | 56 | bmp2ansi.lua: [doc/examples/bmp2ansi.lua](doc/examples/bmp2ansi.lua) 57 | gen_test_image.lua: [doc/examples/gen_test_image.lua](doc/examples/gen_test_image.lua) 58 | 59 | See [doc/EXAMPLES.md](doc/EXAMPLES.md) 60 | -------------------------------------------------------------------------------- /doc/EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | There are currently two runnable examples in the `doc/examples` directory: 4 | 5 | 6 | 7 | ## bmp2ansi.lua 8 | 9 | This example program uses the Bitmap library to read a BMP image 10 | from disk or stdin, then render it using very primitive ANSI art. 11 | 12 | Best use this on a terminal that supports 256-color ANSI escape sequences 13 | (most do, even when they only support 8/16 colors, like the Linux console). 14 | 15 | It optionally supports 24-bit ANSI color codes(most graphical terminal 16 | emulators work) 17 | 18 | ### Usage 19 | 20 | ``` 21 | bmp2ansi.lua [--24bit] 22 | ``` 23 | 24 | ### Example 25 | 26 | ``` 27 | ./doc/examples/bmp2ansi.lua doc/examples/images/test_02.bmp --24bit 28 | ``` 29 | 30 | 31 | 32 | ## gen_test_image.lua 33 | 34 | This script generates a test image, and saves it as a Bitmap file or to stdout. 35 | 36 | ### Usage 37 | 38 | ``` 39 | gen_test_image.lua [--24bit] 40 | ``` 41 | 42 | ### Example 43 | 44 | ``` 45 | ./doc/examples/gen_test_image.lua - --24bit | ./examples/bmp2ansi.lua - --24bit 46 | ``` 47 | 48 | -------------------------------------------------------------------------------- /doc/FILE_FORMAT.md: -------------------------------------------------------------------------------- 1 | # Supported Bitmap File Format 2 | 3 | The library supports the following subset of the 4 | "Bitmap"(`Windows Bitmap`/`device-independent bitmap`, version 3.0) 5 | file format: 6 | 7 | * 24bpp/32bpp bitmaps only(no 1/2/4/8/16bpp) 8 | * no bitfields 9 | * no compression 10 | 11 | (This library does *not* support every feature of the bitmap file format. 12 | But it can detect that it doesn't support a file by it's header) 13 | 14 | 15 | 16 | ## Exporting using Gimp 17 | 18 | You can easily export images using GIMP. 19 | 20 | Select `r8 g8 b8` under `Advanced Options` when exporting as `.bmp`. 21 | 22 | Under `Compatibility Options` select `Do not write color space information`(It's simply ignored when present). 23 | 24 | DO NOT check `Run-length Encoded`, as this library doesn't support it. 25 | 26 | 27 | 28 | ## Exporting using ImageMagic 29 | 30 | You can also convert images using ImageMagic: 31 | ``` 32 | convert "source_image.png" -type truecolor "target_image.bmp" 33 | ``` 34 | -------------------------------------------------------------------------------- /doc/INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # LuaRocks installation 2 | 3 | This library is packaged and build using LuaRocks, which makes building 4 | and installing easy. 5 | 6 | Currently this library is not published on a LuaRocks server, 7 | so you need to clone this repository and build it yourself: 8 | 9 | ``` 10 | git clone https://github.com/max1220/lua-bitmap 11 | cd lua-bitmap 12 | # install locally, usually to ~/.luarocks 13 | luarocks make --local 14 | ``` 15 | 16 | This will install the module locally, typically in ~/.luarocks. 17 | 18 | 19 | 20 | ## Adding to LuaRocks modules to package.path 21 | 22 | When installing locally you need to tell Lua where to look for modules 23 | installed using Luarocks, e.g.: 24 | 25 | ``` 26 | luarocks path >> ~/.bashrc 27 | ``` 28 | 29 | This will allow you to `require()` and locally installed LuaRocks package. 30 | 31 | 32 | 33 | 34 | 35 | # Manual installation 36 | 37 | As this library is just a single file, you can easily copy the entire 38 | implementation to where it is most useful to you, e.g.: 39 | 40 | `cp lua/lua-bitmap/init.lua /lua-bitmap.lua` 41 | -------------------------------------------------------------------------------- /doc/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Max 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc/USAGE.md: -------------------------------------------------------------------------------- 1 | # Library Usage 2 | 3 | After installing the library, you can load this library using: 4 | ``` 5 | Bitmap = require("lua-bitmap") 6 | ``` 7 | 8 | The returned table(`Bitmap` in the example above) contains three functions: 9 | ``` 10 | bmp = Bitmap.empty_bitmap(width, height, alpha) 11 | bmp = Bitmap.from_string(data) 12 | bmp = Bitmap.from_file(path) 13 | bmp = Bitmap._new_bitmap() 14 | ``` 15 | 16 | Each of these functions returns a table representing a single Bitmap image. 17 | 18 | 19 | 20 | ## bmp, err = Bitmap.empty_bitmap(width, height, alpha) 21 | 22 | This function creates a new bitmap of the specified dimensions. 23 | If alpha is truethy the bits per pixel value is set to 32, otherwise 24 | the default of 24 will be used. 25 | 26 | Keep in mind that Bitmap Version 3.0 does not officially support an alpha-channel. 27 | 28 | The initial background is black, or transparent if alpha is present (r=0, g=0, b=0, a=0). 29 | 30 | 31 | 32 | ## bmp, err = Bitmap.from_string(string) 33 | 34 | Load a bitmap from a string. The string must include the entire bitmap, as it 35 | would be read from disk(including headers). 36 | 37 | When passing this to C, keep in mind that this string almost always includes `\000`s 38 | (lua handles strings with embedded zeros just fine). 39 | 40 | 41 | 42 | ## bmp, err = Bitmap.from_file(path) 43 | 44 | This function loads a bitmap from a filepath. 45 | This basically does: `return Bitmap.from_string(io.open(path, "r"):read("*a"))`, 46 | but with better error handling. 47 | 48 | 49 | 50 | ## bmp = Bitmap._new_bitmap() 51 | 52 | This just returns a table with the bitmap functions, *without* initializing it by 53 | reading headers, or generating headers for new empty bitmaps. 54 | 55 | After creating a pre-filled `bmp.data` table, you need to at least provide the `bmp.pixel_offset` value(Usually 54) before calling the `bmp:write_header(width, height, bpp)` function to write a valid bitmap header and populate the internal state. 56 | 57 | 58 | 59 | ## bmp functions 60 | 61 | The table returned by the 4 functions 62 | `Bitmap.empty_bitmap, Bitmap.from_string, Bitmap.from_file, Bitmap._new_bitmap` 63 | (here called `bmp`) supports the following functions 64 | for working with bitmap image data: 65 | 66 | The `a` alpha-value argument to functions is always optional and defaults to 255. 67 | If the bitmap does not support the alpha channel, `nil` is returned as `a` for the alpha-value. 68 | 69 | function | description 70 | ---------------------------- | --- 71 | r,g,b,a = bmp:get_pixel(x,y) | get color value at x,y 72 | bmp:set_pixel(x,y,r,g,b,a) | set pixel at x,y to r,g,b,a[0-255] 73 | bmp:tostring() | get bitmap as string 74 | 75 | ### Internal functions 76 | 77 | Besides these graphics related functions, the returned `bmp` table also contains 78 | some functions used for parsing and creating header data. 79 | 80 | Bitmaps use two's complement little endian integers for header data. 81 | These are functions for reading such values in Lua: 82 | 83 | `offset` is zero-based. 84 | 85 | function | description 86 | ------------------------------------ | --- 87 | bmp:read_header() | read the headers, and update internal state from the internal data. 88 | bmp:write_header(width, height, bpp) | write the header using the specified information, the re-read header using `bmp:read_header()` 89 | val = bmp:read(offset) | read uint8 90 | val = bmp:read_word(offset) | read uint16 91 | val = bmp:read_dword(offset) | read uint32 92 | val = bmp:read_long(offset) | read int32 93 | bmp:write(offset, data) | write uint8 * (#data can be >1) 94 | bmp:write_word(offset, val) | write uint16 95 | bmp:write_dword(offset, val) | write uint32 96 | bmp:write_long(offset, val) | write int16 97 | 98 | The library expects the `bmp.data` field to 99 | be a table, where indexing with an offset(0-based) results in a single character(`assert(#bmp.data[index]==1)`). 100 | 101 | The write functions will refuse to create a new index in the data table, so you need to pre-initialize `bmp.data` before use, e.g.: 102 | 103 | ``` 104 | bmp.data = {} 105 | for i=1, len do 106 | bmp.data[i-1] = "\000" 107 | end 108 | ``` 109 | 110 | 111 | 112 | 113 | 114 | ### test_ints.lua 115 | 116 | This is a simple test of the functions for reading data from 117 | the bitmap header, specifically decoding the integer formats found in 118 | bitmaps(8bit, 16-bit- 32bit unsigned integers, 32-bit twos-complement signed integers). 119 | 120 | It might be useful if you're replacing the `bmp:write()` and `bmp:read()` 121 | functions. 122 | It also serves as a simple benchmark of these functions by calling them repeatedly. 123 | 124 | (For most users this is of little use) 125 | 126 | You can run the test by using this command: 127 | `luarocks test` 128 | (or `lua -l 'lua-bitmap.test_ints' -e 'print(\"ok\")'`) 129 | -------------------------------------------------------------------------------- /doc/examples/bmp2ansi.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | local Bitmap = require("lua-bitmap") 3 | 4 | -- this is a small example script that renders a Windows Bitmap as very primitive ANSI art, 5 | -- using the 256-color(technically 240-color) or 23-bit colors terminal escape sequences. 6 | 7 | 8 | -- convert a rgb[0-255] value to an ANSI 256-color set background color escape sequence 9 | local function rgb_to_ansi_256(r,g,b) 10 | if (math.floor(r/11) == math.floor(g/11)) and (math.floor(r/11) == math.floor(b/11)) then 11 | local grey_level = r/11 12 | if grey_level < 0.5 then 13 | -- output black 14 | return ("\027[48;5;%dm"):format(0) 15 | elseif grey_level > 23.5 then 16 | -- output white 17 | return ("\027[48;5;%dm"):format(15) 18 | else 19 | -- output 24-level grey code 20 | --io.stderr:write("grey level: "..math.floor(grey_level).."\n") 21 | return ("\027[48;5;%dm"):format(232 + math.floor(grey_level)) 22 | end 23 | else 24 | -- output 216-color code(6*6*6) 25 | r = (r/255)*5 26 | g = (g/255)*5 27 | b = (b/255)*5 28 | local i = (36*math.floor(r)) + (6*math.floor(g)) + math.floor(b) 29 | return ("\027[48;5;%dm"):format(16 + i) 30 | end 31 | end 32 | 33 | -- convert a rgb[0-255] value to an ANSI 24-bit color set background escape sequence 34 | local function rgb_to_ansi_24bit(r,g,b) 35 | return ("\027[48;2;%d;%d;%dm"):format(r,g,b) 36 | end 37 | 38 | -- print usage information 39 | local function usage() 40 | print("bmp2ansi.lua [--24bit]") 41 | print("You use stdin as input by specifying - as first argument.") 42 | print() 43 | end 44 | 45 | -- check first argument is present 46 | local input_arg = arg[1] 47 | if not input_arg then 48 | usage() 49 | print("First argument needs to be an input Windows Bitmap file!") 50 | os.exit(1) 51 | end 52 | 53 | local use_24bit = false 54 | if arg[2] == "--24bit" then 55 | use_24bit = true 56 | elseif arg[2] or (#arg > 2) then 57 | usage() 58 | os.exit(1) 59 | end 60 | 61 | -- read a bitmap from file 62 | local bmp,err 63 | if input_arg == "-" then 64 | bmp,err = Bitmap.from_string(io.read("*a")) 65 | else 66 | bmp,err = Bitmap.from_file(input_arg) 67 | end 68 | if not bmp then 69 | print("Bitmap error: "..tostring(err)) 70 | os.exit(1) 71 | end 72 | 73 | -- output some info about this bitmap to stdout 74 | local bmp_info = "Bitmap info: width=%d, height=%d, bpp=%d, pixel_offset=%d, topdown=%s\n" 75 | bmp_info = bmp_info:format(bmp.width, bmp.height, bmp.bpp, bmp.pixel_offset, bmp.topdown and "yes" or "no") 76 | io.stderr:write(bmp_info) 77 | 78 | for y=0, bmp.height-1 do 79 | for x=0, bmp.width-1 do 80 | local r,g,b,a = bmp:get_pixel(x,y) 81 | if a == 0 then 82 | io.write("\027[0m") 83 | elseif r and use_24bit then 84 | io.write(rgb_to_ansi_24bit(r,g,b)) 85 | elseif r then 86 | io.write(rgb_to_ansi_256(r,g,b)) 87 | end 88 | io.write(" ") 89 | end 90 | io.write("\027[0m\n") 91 | end 92 | io.flush() 93 | -------------------------------------------------------------------------------- /doc/examples/gen_test_image.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --[[ 3 | This is a small example script that generates a simple 4 | test image and saves it as Windows Bitmap. 5 | ]] 6 | local Bitmap = require("lua-bitmap") 7 | 8 | 9 | -- print usage information 10 | local function usage() 11 | print("gen_test_image.lua ") 12 | print("You use stdin as output by specifying - as first argument.") 13 | end 14 | 15 | -- check first argument is present 16 | local output_arg = arg[1] 17 | if not output_arg then 18 | usage() 19 | print("First argument needs to be an output file!") 20 | os.exit(1) 21 | end 22 | 23 | 24 | -- create new empty bitmap(initialized to black) 25 | local width = 64 26 | local height = 64 27 | local bmp,err = Bitmap.empty_bitmap(width, height, false) 28 | if not bmp then 29 | print("Bitmap error: ", tostring(err)) 30 | os.exit(1) 31 | end 32 | 33 | -- create border around outside: 34 | for x=1, width-2 do 35 | bmp:set_pixel(x,0,255,0,0,255) 36 | local grey = math.floor(255*((x-1)/(width-2))) 37 | bmp:set_pixel(x,height-1,grey,grey,grey,255) 38 | end 39 | for y=1, height-2 do 40 | bmp:set_pixel(0,y,0,255,0,255) 41 | bmp:set_pixel(width-1,y,0,0,255,255) 42 | end 43 | 44 | local _unpack = table.unpack or unpack 45 | 46 | -- resolve a number to a list of active segments 47 | local segments = { 48 | [0] = { true, true, true, false, true, true, true}, 49 | [1] = {false, false, true, false, false, true, false}, 50 | [2] = { true, false, true, true, true, false, true}, 51 | [3] = { true, false, true, true, false, true, true}, 52 | [4] = {false, true, true, true, false, true, false}, 53 | [5] = { true, true, false, true, false, true, true}, 54 | [6] = { true, true, false, true, true, true, true}, 55 | [7] = { true, false, true, false, false, true, false}, 56 | [8] = { true, true, true, true, true, true, true}, 57 | [9] = { true, true, true, true, false, true, false}, 58 | [" "] = {false, false, false, false, false, false, false}, 59 | ["-"] = {false, false, false, true, false, false, false}, 60 | ["_"] = {false, false, false, false, false, false, true}, 61 | ["="] = {false, false, false, true, false, false, true}, 62 | ["H"] = {false, true, true, true, true, true, false}, 63 | ["E"] = { true, true, false, true, true, false, true}, 64 | ["L"] = {false, true, false, false, true, false, true}, 65 | ["O"] = { true, true, true, false, true, true, true}, 66 | } 67 | -- draw a 4x7px 7-segment number value v at offset_x,offset_y, in color r,g,b,a on bmp 68 | local function seg7(target, offset_x, offset_y, v, r,g,b,a) 69 | local function vpx(x,y) 70 | target:set_pixel(offset_x+x,offset_y+y,r,g,b,a) 71 | target:set_pixel(offset_x+x+1,offset_y+y,r,g,b,a) 72 | end 73 | local function hpx(x,y) 74 | target:set_pixel(offset_x+x,offset_y+y,r,g,b,a) 75 | target:set_pixel(offset_x+x,offset_y+y+1,r,g,b,a) 76 | end 77 | local s1,s2,s3,s4,s5,s6,s7 = _unpack(segments[v] or segments[" "]) 78 | if s1 then vpx(1,0) end 79 | if s2 then hpx(0,1) end 80 | if s3 then hpx(3,1) end 81 | if s4 then vpx(1,3) end 82 | if s5 then hpx(0,4) end 83 | if s6 then hpx(3,4) end 84 | if s7 then vpx(1,6) end 85 | end 86 | local function seg7_str(target, offset_x, offset_y, str, r,g,b,a) 87 | for i=1, #str do 88 | local n = str:sub(i,i) 89 | n = tonumber(n) or n 90 | if n then 91 | seg7(target, offset_x, offset_y, n, r,g,b,a) 92 | offset_x = offset_x + 5 93 | end 94 | end 95 | end 96 | 97 | seg7_str(bmp, 7,2, "0123456789", 0xFF,0xFF,0xFF, 0xFF) 98 | seg7_str(bmp, 19,10, "HELLO", 0xFF,0x00,0x00, 0xFF) 99 | seg7_str(bmp, 7,18, os.date("%d_%m_%Y"), 0x00,0xFF,0x00, 0xFF) 100 | seg7_str(bmp, 12,26, os.date("%H=%M=%S"), 0x00,0x00,0xFF, 0xFF) 101 | 102 | local output_file = io.stdout 103 | if output_arg ~= "-" then 104 | output_file = io.open(output_arg, "wb") 105 | end 106 | local bmp_data = bmp:tostring() 107 | output_file:write(bmp_data) 108 | -------------------------------------------------------------------------------- /doc/examples/images/hello_world.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max1220/lua-bitmap/2a84e5bad4306c9459438da12e499f9573709ef9/doc/examples/images/hello_world.bmp -------------------------------------------------------------------------------- /doc/examples/images/test_01.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max1220/lua-bitmap/2a84e5bad4306c9459438da12e499f9573709ef9/doc/examples/images/test_01.bmp -------------------------------------------------------------------------------- /doc/examples/images/test_02.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max1220/lua-bitmap/2a84e5bad4306c9459438da12e499f9573709ef9/doc/examples/images/test_02.bmp -------------------------------------------------------------------------------- /doc/examples/images/test_03.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max1220/lua-bitmap/2a84e5bad4306c9459438da12e499f9573709ef9/doc/examples/images/test_03.bmp -------------------------------------------------------------------------------- /lua-bitmap-0.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-bitmap" 2 | version = "0.0-1" 3 | source = { 4 | url = "git://github.com/max1220/lua-bitmap", 5 | tag = "0.0-1" 6 | } 7 | description = { 8 | summary = "Basic read/write support for `Windows Bitmap`/`device-independent bitmap` file format(version 3.0)", 9 | detailed = [[ 10 | This single-file Lua-only library implements basic read/write support for the 11 | `Windows Bitmap`/`device-independent bitmap` file format(version 3.0). 12 | 13 | Compatible with Lua5.1, LuaJIT, Lua5.2, Lua5.3, Lua5.4. 14 | ]], 15 | homepage = "http://github.com/max1220/lua-bitmap", 16 | license = "MIT" 17 | } 18 | dependencies = { 19 | "lua >= 5.1" 20 | } 21 | build = { 22 | type = "builtin", 23 | modules = {}, 24 | test = { 25 | type = "command", 26 | 27 | -- Let's hope that the lua version is the one we install for... 28 | -- TODO: Is there a better way of doing this? 29 | script = "lua -l 'lua-bitmap.test_ints' -e 'print(\"ok\")'", 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lua-bitmap-0.0-2.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-bitmap" 2 | version = "0.0-2" 3 | source = { 4 | url = "git://github.com/max1220/lua-bitmap", 5 | tag = "0.0-2" 6 | } 7 | description = { 8 | summary = "Basic read/write support for `Windows Bitmap`/`device-independent bitmap` file format(version 3.0)", 9 | detailed = [[ 10 | This single-file Lua-only library implements basic read/write support for the 11 | `Windows Bitmap`/`device-independent bitmap` file format(version 3.0). 12 | 13 | Compatible with Lua5.1, LuaJIT, Lua5.2, Lua5.3, Lua5.4. 14 | ]], 15 | homepage = "http://github.com/max1220/lua-bitmap", 16 | license = "MIT" 17 | } 18 | dependencies = { 19 | "lua >= 5.1" 20 | } 21 | build = { 22 | type = "builtin", 23 | modules = {}, 24 | test = { 25 | type = "command", 26 | 27 | -- Let's hope that the lua version is the one we install for... 28 | -- TODO: Is there a better way of doing this? 29 | script = "lua -l 'lua-bitmap.test_ints' -e 'print(\"ok\")'", 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lua-bitmap-0.0-3.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-bitmap" 2 | version = "0.0-3" 3 | source = { 4 | url = "git://github.com/max1220/lua-bitmap", 5 | tag = "0.0-3" 6 | } 7 | description = { 8 | summary = "Basic read/write support for `Windows Bitmap`/`device-independent bitmap` file format(version 3.0)", 9 | detailed = [[ 10 | This single-file Lua-only library implements basic read/write support for the 11 | `Windows Bitmap`/`device-independent bitmap` file format(version 3.0). 12 | 13 | Compatible with Lua5.1, LuaJIT, Lua5.2, Lua5.3, Lua5.4. 14 | ]], 15 | homepage = "http://github.com/max1220/lua-bitmap", 16 | license = "MIT" 17 | } 18 | dependencies = { 19 | "lua >= 5.1" 20 | } 21 | build = { 22 | type = "builtin", 23 | modules = {}, 24 | test = { 25 | type = "command", 26 | 27 | -- Let's hope that the lua version is the one we install for... 28 | -- TODO: Is there a better way of doing this? 29 | script = "lua -l 'lua-bitmap.test_ints' -e 'print(\"ok\")'", 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lua-bitmap-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-bitmap" 2 | version = "scm-1" 3 | source = { 4 | url = "git://github.com/max1220/lua-bitmap", 5 | } 6 | description = { 7 | summary = "Basic read/write support for `Windows Bitmap`/`device-independent bitmap` file format(version 3.0)", 8 | detailed = [[ 9 | This single-file Lua-only library implements basic read/write support for the 10 | `Windows Bitmap`/`device-independent bitmap` file format(version 3.0). 11 | 12 | Compatible with Lua5.1, LuaJIT, Lua5.2, Lua5.3, Lua5.4. 13 | ]], 14 | homepage = "http://github.com/max1220/lua-bitmap", 15 | license = "MIT" 16 | } 17 | dependencies = { 18 | "lua >= 5.1" 19 | } 20 | build = { 21 | type = "builtin", 22 | modules = {}, 23 | test = { 24 | type = "command", 25 | 26 | -- Let's hope that the lua version is the one we install for... 27 | -- TODO: Is there a better way of doing this? 28 | script = "lua -l 'lua-bitmap.test_ints' -e 'print(\"ok\")'", 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lua/lua-bitmap/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This single-file Lua library implements read/write support for the 3 | `Windows Bitmap`/`device-independent bitmap` file format version 3.0. 4 | 5 | Compatible with Lua5.1, LuaJIT, Lua5.2, Lua5.3, Lua5.4. 6 | 7 | License: MIT (see included file LICENSE) 8 | ]] 9 | 10 | -- offsets into a Windows Bitmap file header 11 | local bmp_header_offset = 0 12 | local bmp_header_filesize = 2 13 | local bmp_header_pixel_offset = 10 14 | local bmp_header_size = 14 15 | local bmp_header_width = 18 16 | local bmp_header_height = 22 17 | local bmp_header_planes = 26 18 | local bmp_header_bpp = 28 19 | local bmp_header_compression = 30 20 | local bmp_header_image_size = 34 21 | --local bmp_header_bitmask_r = 60 22 | --local bmp_header_bitmask_g = 64 23 | --local bmp_header_bitmask_b = 68 24 | --local bmp_header_bitmask_a = 72 25 | 26 | local function get_data_size(width, height, bpp) 27 | local line_w = math.ceil(width/4)*4 28 | return height*line_w*(bpp/8) 29 | end 30 | 31 | local function new_bitmap() 32 | -- this table is returned as an interface for a Windows Bitmap to the user: 33 | local bmp = {} 34 | 35 | -- reading 8/16/32-bit little-endian integer values from a string 36 | function bmp:read(offset) -- read uint8 37 | offset = math.floor(assert(tonumber(offset))) 38 | local value = assert(self.data[offset]):byte() 39 | return value 40 | end 41 | function bmp:read_word(offset) -- read uint16 42 | return self:read(offset+1)*0x100 + self:read(offset) 43 | end 44 | function bmp:read_dword(offset) -- read uint32 45 | return self:read(offset+3)*0x1000000 + self:read(offset+2)*0x10000 + self:read(offset+1)*0x100 + self:read(offset) 46 | end 47 | function bmp:read_long(offset) -- read int32 48 | local value = self:read_dword(offset) 49 | if value >= 0x80000000 then -- bitmap format uses two's complement 50 | value = -(value - 0x80000000) 51 | end 52 | return value 53 | end 54 | 55 | -- writing 8/16/32-bit little-endian integer values from a string 56 | function bmp:write(offset, data) -- write uint8* 57 | offset = math.floor(assert(tonumber(offset))) 58 | for i=1, #data do 59 | local index = offset+i-1 60 | local value = data:sub(i,i) 61 | assert(self.data[index]) -- only update, don't create new entry 62 | self.data[index] = value 63 | end 64 | end 65 | function bmp:write_word(offset, value) -- write uint16 66 | local a = math.floor(value % 0x100) 67 | local b = math.floor(value / 0x100) 68 | local data = string.char(a,b) 69 | self:write(offset, data) 70 | end 71 | function bmp:write_dword(offset, value) -- write uint32 72 | local a = math.floor(value) % 0x100 73 | local b = math.floor(value / 0x100) % 0x100 74 | local c = math.floor(value / 0x10000) % 0x100 75 | local d = math.floor(value / 0x1000000) % 0x100 76 | local data = string.char(a,b,c,d) 77 | self:write(offset, data) 78 | end 79 | function bmp:write_long(offset, value) 80 | if value < 0 then 81 | value = -value + 0x80000000 82 | end 83 | self:write_dword(offset, value) 84 | end 85 | 86 | -- read bitmap headers and parse required metadata, update self 87 | function bmp:read_header() 88 | -- check the bitmap header 89 | if not self:read_word(bmp_header_offset) == 0x4D42 then 90 | return nil, "Bitmap magic header not found" 91 | end 92 | local compression = self:read_dword(bmp_header_compression) 93 | --[[ 94 | TODO: Add BI_BITFIELDS support for 24bpp and 32bpp bitmaps 95 | if compression == 3 then 96 | local bitmask_r = self:read_dword(bmp_header_bitmask_r) 97 | local bitmask_g = self:read_dword(bmp_header_bitmask_g) 98 | local bitmask_b = self:read_dword(bmp_header_bitmask_b) 99 | end 100 | ]] 101 | if compression ~= 0 then 102 | return nil, "Only uncompressed bitmaps supported. Is: "..tostring(compression) 103 | end 104 | 105 | -- get bits per pixel from the bitmap header 106 | -- this library only supports 24bpp and 32bpp pixel formats! 107 | self.bpp = self:read_word(bmp_header_bpp) 108 | if not ((self.bpp == 24) or (self.bpp == 32)) then 109 | return nil, "Only 24bpp/32bpp bitmaps supported. Is: "..tostring(self.bpp) 110 | end 111 | 112 | -- get other required info from the bitmap header 113 | self.pixel_offset = self:read_dword(bmp_header_pixel_offset) 114 | self.width = self:read_long(bmp_header_width) 115 | self.height = self:read_long(bmp_header_height) 116 | 117 | -- calculate expected size of the data region 118 | self.data_size = get_data_size(self.width, self.height, self.bpp) 119 | 120 | -- if height is <0, the image data is in topdown format 121 | self.topdown = true 122 | if self.height < 0 then 123 | self.topdown = false 124 | self.height = -self.height 125 | end 126 | 127 | return true 128 | end 129 | 130 | -- write bitmap headers from self 131 | function bmp:write_header(width, height, bpp) 132 | if (width < 0) or (height < 0) or (width >= 2^31) or (height >= 2^31) then 133 | return nil, "Invalid dimensions" 134 | end 135 | if not ((bpp == 24) or (bpp == 32)) then 136 | return nil, "Invalid bpp" 137 | end 138 | 139 | -- update expected data size 140 | self.data_size = get_data_size(width, height, bpp) 141 | 142 | -- Bitmap header 143 | self:write_word(bmp_header_offset, 0x4D42) 144 | 145 | self:write_dword(bmp_header_filesize, 54+self.data_size) 146 | self:write_dword(bmp_header_size, 40) 147 | self:write_word(bmp_header_planes, 1) 148 | 149 | -- image information 150 | self:write_dword(bmp_header_compression, 0) 151 | self:write_word(bmp_header_bpp, bpp) 152 | self:write_dword(bmp_header_pixel_offset, self.pixel_offset) 153 | 154 | self:write_long(bmp_header_width, width) 155 | self:write_dword(bmp_header_image_size, self.data_size) 156 | 157 | if self.topdown then 158 | self:write_long(bmp_header_height, height) 159 | else 160 | self:write_long(bmp_header_height, -height) 161 | end 162 | 163 | -- set all internal values accordingly 164 | self:read_header() 165 | 166 | return true 167 | end 168 | 169 | -- return the r,g,b,a[0-255] color value for a pixel by its x,y coordinates 170 | function bmp:get_pixel(x,y) 171 | if (x < 0) or (x >= self.width) or (y < 0) or (y >= self.height) then 172 | return nil, "Out of bounds" 173 | end 174 | 175 | -- calculate byte offset in data 176 | local Bpp = self.bpp/8 177 | local row_len = self.width * Bpp -- length of pixel data in bytes 178 | local row_stride = math.ceil(row_len/4)*4 -- padded length of a row in bytes 179 | local index = self.pixel_offset + y*row_stride + x*Bpp 180 | if self.topdown then 181 | index = self.pixel_offset + (self.height-y-1)*row_stride + x*Bpp 182 | end 183 | 184 | -- read r,g,b color values 185 | local b = self:read(index) 186 | local g = self:read(index+1) 187 | local r = self:read(index+2) 188 | 189 | 190 | local a = nil 191 | if Bpp == 4 then -- on 32bpp, also get 4th channel value(alpha, not in spec) 192 | a = self:read(index+3) 193 | end 194 | 195 | return r,g,b,a 196 | end 197 | 198 | -- set the color value at x,y 199 | function bmp:set_pixel(x,y, r,g,b,a) 200 | if (x < 0) or (x >= self.width) or (y < 0) or (y >= self.height) then 201 | return nil, "out of bounds" 202 | end 203 | if (r < 0) or (r > 255) or (g < 0) or (g > 255) or (b < 0) or (b > 255) then 204 | return nil, "invalid color" 205 | end 206 | if a and ((a < 0) or (a > 255)) then 207 | return nil, "invalid alpha" 208 | end 209 | 210 | -- calculate byte offset in data 211 | local Bpp = self.bpp/8 212 | local row_len = width * Bpp -- length of pixel data in bytes 213 | local row_stride = math.ceil(row_len/4)*4 -- padded length of a row in bytes 214 | local index = self.pixel_offset + y*row_stride + x*Bpp 215 | if self.topdown then 216 | index = self.pixel_offset + (self.height-y-1)*row_stride + x*Bpp 217 | end 218 | 219 | -- write new pixel value 220 | if Bpp == 3 then 221 | self:write(index, string.char(b,g,r)) 222 | else 223 | self:write(index, string.char(b,g,r,a or 255)) 224 | end 225 | 226 | return true 227 | end 228 | 229 | -- return the entire bitmap serialized 230 | function bmp:tostring() 231 | return self.data[0]..table.concat(self.data) 232 | end 233 | 234 | return bmp 235 | end 236 | 237 | -- Create a new empty bitmap 238 | local function new_empty_bitmap(width, height, alpha) 239 | local bmp = new_bitmap() 240 | local bpp = 24 241 | if alpha then 242 | bpp = 32 243 | end 244 | 245 | -- create empty data string 246 | bmp.data = {} 247 | for i=1, 54+get_data_size(width, height, bpp) do 248 | bmp.data[i-1] = "\000" 249 | end 250 | bmp.pixel_offset = 54 251 | bmp.topdown = true 252 | 253 | -- write a new header to the data 254 | local ok,err = bmp:write_header(width, height, bpp) 255 | if not ok then 256 | return nil, err 257 | end 258 | 259 | return bmp 260 | end 261 | 262 | -- Read a bitmap from a string and return it 263 | local function new_bitmap_from_string(data) 264 | local bmp = new_bitmap() 265 | 266 | -- copy data from string to internal table 267 | bmp.data = {} 268 | for i=1, #data do 269 | bmp.data[i-1] = data:sub(i,i) 270 | end 271 | 272 | -- read the header from the data 273 | local ok,err = bmp:read_header() 274 | if not ok then 275 | return nil, err 276 | end 277 | 278 | return bmp 279 | end 280 | 281 | -- Read a bitmap from a file and return it 282 | local function new_bitmap_from_file(path) 283 | -- open a file containing bitmap data 284 | local file = io.open(path, "rb") 285 | if not file then 286 | return nil, "can't open input file for reading: "..tostring(path) 287 | end 288 | 289 | -- try to read from the file 290 | local data = file:read("*a") 291 | if (not data) or (data == "") then 292 | return nil, "can't read input file: "..tostring(path) 293 | end 294 | 295 | return new_bitmap_from_string(data) 296 | end 297 | 298 | 299 | -- this is the module returned to the user when require()'d 300 | local Bitmap = { 301 | empty_bitmap = new_empty_bitmap, 302 | from_string = new_bitmap_from_string, 303 | from_file = new_bitmap_from_file, 304 | _new_bitmap = new_bitmap 305 | } 306 | 307 | return Bitmap 308 | -------------------------------------------------------------------------------- /lua/lua-bitmap/test_ints.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --[[ 3 | This file tests the functions used internally to read/write 4 | integer values from the bitmap. 5 | ]] 6 | 7 | local Bitmap = require("lua-bitmap") 8 | 9 | -- create a new bitmap structure, but don't create header 10 | -- (create uninitialized bmp structure) 11 | local bmp,err = Bitmap._new_bitmap() 12 | if not bmp then 13 | print("Bitmap error: ", tostring(err)) 14 | os.exit(1) 15 | end 16 | 17 | -- pre-initialize data to 0 18 | local data_len_k = tonumber(arg[1]) or 1000 19 | bmp.data = {} 20 | for i=1, data_len_k*1000 do 21 | bmp.data[i-1] = "\000" 22 | end 23 | 24 | -- perform a simple write-then-read test 25 | local function test_func(read_func, write_func, addr, value) 26 | write_func(bmp, addr, value) 27 | local read_value = read_func(bmp, addr) 28 | if read_value ~= value then 29 | print("FAILED") 30 | print() 31 | os.exit(1) 32 | end 33 | end 34 | 35 | -- provide the test values for different bytes per integer values 36 | local function test_func_list(sign, read_func, write_func, bytes) 37 | -- repeat test in every location in the first 1k of memory 38 | for addr=0, 996 do 39 | test_func(read_func, write_func, addr, 0) 40 | test_func(read_func, write_func, addr, sign*1) 41 | test_func(read_func, write_func, addr, sign*2) 42 | 43 | test_func(read_func, write_func, addr, sign*253) 44 | test_func(read_func, write_func, addr, sign*254) 45 | test_func(read_func, write_func, addr, sign*255) 46 | 47 | if bytes > 1 then 48 | test_func(read_func, write_func, addr, sign*256) 49 | test_func(read_func, write_func, addr, sign*257) 50 | test_func(read_func, write_func, addr, sign*258) 51 | 52 | test_func(read_func, write_func, addr, sign*65533) 53 | test_func(read_func, write_func, addr, sign*65534) 54 | test_func(read_func, write_func, addr, sign*65535) 55 | end 56 | 57 | if bytes>2 then 58 | test_func(read_func, write_func, addr, sign*65536) 59 | test_func(read_func, write_func, addr, sign*65537) 60 | test_func(read_func, write_func, addr, sign*65538) 61 | 62 | test_func(read_func, write_func, addr, sign*2147483645) 63 | test_func(read_func, write_func, addr, sign*2147483646) 64 | test_func(read_func, write_func, addr, sign*2147483647) 65 | end 66 | end 67 | end 68 | 69 | 70 | -- run the tests for the supported data types word, dword, long 71 | print("running int tests, data size="..data_len_k.."K") 72 | 73 | test_func_list(1, bmp.read_long, bmp.write_long, 4) 74 | test_func_list(-1, bmp.read_long, bmp.write_long, 4) 75 | 76 | test_func_list(1, bmp.read_dword, bmp.write_dword, 4) 77 | 78 | test_func_list(1, bmp.read_word, bmp.write_word, 2) 79 | 80 | 81 | -- provide wrapper for byte_write(write accepts numeric value, test provides strings) 82 | local function byte_write(self, addr, val) 83 | self:write(addr, string.char(val), 1) 84 | end 85 | test_func_list(1, bmp.read, byte_write, 1) 86 | 87 | -- if we made it here, we didn't error! (this is good) 88 | print("all ok") 89 | --------------------------------------------------------------------------------