#!/usr/bin/env python ##@file bmp2hex.py # @ingroup util # A script for converting a 1-bit bitmap to HEX for use in an Arduino sketch. # # The BMP format is well publicized. The byte order of the actual bitmap is a # little unusual. The image is stored bottom to top, left to right. In addition, # The pixel rows are rounded to DWORDS which are 4 bytes long. SO, to convert this # to left to right, top to bottom, no byte padding. We have to do some calculations # as we loop through the rows and bytes of the image. See below for more # # Usage: # >>>bmp2hex.py [-i] [-r] [-n] [-d] [-x] [-w ] [-b ] # # @param infile The file to convert. # @param tablename The name of the table to create # @param raw "-r", bitmap written as raw table [optional] # @param invert "-i", to invert image pixel colors [optional] # @param tablewidth "-w , The number of characters for each row of the output table [optional] # @param sizebytes "-b , Bytes = 0, 1, or 2. 0 = auto. 1 = 1-byte for sizes. 2 = 2-byte sizes (big endian) [optional] # @param named "-n", use a names structure [optional] ## @param double "-d", use double bytes rather than single ones [optional] # @param xbm "-x", use XBM format (bits reversed in byte) [optional] # @param version "-v", returns version number # # @author Robert Gallup 2016-02 # # Author: Robert Gallup (bg@robertgallup.com) # License: MIT Opensource License # # Copyright 2016-2018 Robert Gallup # import sys, array, os, textwrap, math, random, argparse class DEFAULTS(object): STRUCTURE_NAME = 'GFXMeta' VERSION = '2.3.4' def main (): # Default parameters infile = "" tablename = "" tablewidth = 16 sizebytes = 0 invert = False raw = False named = False double = False xbm = False version = False # Set up parser and handle arguments parser = argparse.ArgumentParser() # parser.add_argument ("infile", help="The BMP file(s) to convert", type=argparse.FileType('r'), nargs='+', default=['-']) parser.add_argument ("infile", help="The BMP file(s) to convert", type=argparse.FileType('r'), nargs='*', default=['-']) parser.add_argument ("-r", "--raw", help="Outputs all data in raw table format", action="store_true") parser.add_argument ("-i", "--invert", help="Inverts bitmap pixels", action="store_true") parser.add_argument ("-w", "--width", help="Output table width in hex bytes [default: 16]", type=int) parser.add_argument ("-b", "--bytes", help="Byte width of BMP sizes: 0=auto, 1, or 2 (big endian) [default: 0]", type=int) parser.add_argument ("-n", "--named", help="Uses named structure (" + DEFAULTS.STRUCTURE_NAME + ") for data", action="store_true") # parser.add_argument ("-d", "--double", help="Defines data in 'words' rather than bytes", action="store_true") parser.add_argument ("-x", "--xbm", help="Uses XBM bit order (low order bit is first pixel of byte)", action="store_true") parser.add_argument ("-v", "--version", help="Returns the current bmp2hex version", action="store_true") args = parser.parse_args() # Required arguments infile = args.infile # Options if args.raw: raw = args.raw if args.invert: invert = args.invert if args.width: tablewidth = args.width if args.bytes: sizebytes = args.bytes % 3 if args.named: named = args.named # if args.double: # double = args.double double = False if args.xbm: xbm = args.xbm if args.version: print ('// bmp2hex version ' + DEFAULTS.VERSION) # Output named structure, if requested if (named): print ('struct ' + DEFAULTS.STRUCTURE_NAME + ' {') print (' unsigned int width;') print (' unsigned int height;') print (' unsigned int bitDepth;') print (' int baseline;') print (' ' + getDoubleType(double)[0] + 'pixel_data;') print ('};') print ('') # Do the work for f in args.infile: if f == '-': sys.exit() bmp2hex(f.name, tablewidth, sizebytes, invert, raw, named, double, xbm) # Utility function. Return a long int from array (little endian) def getLONG(a, n): return (a[n+3] * (2**24)) + (a[n+2] * (2**16)) + (a[n+1] * (2**8)) + (a[n]) # Utility function. Return an int from array (little endian) def getINT(a, n): return ((a[n+1] * (2**8)) + (a[n])) # Reverses pixels in byte def reflect(a): r = 0 for i in range(8): r <<= 1 r |= (a & 0x01) a >>= 1 return (r) # Returns as a tuple, the data type and length for double versus short data types def getDoubleType (d): if d: dType = 'uint16_t' + ' *' dLen = 2 else: dType = 'uint8_t' + ' *' dLen = 1 return (dType, dLen) # Main conversion function def bmp2hex(infile, tablewidth, sizebytes, invert, raw, named, double, xbm): # Set up some variables to handle the "-d" option (pixelDataType, dataByteLength) = getDoubleType(double) # Set the table name to the uppercase root of the file name tablename = os.path.splitext(infile)[0].upper() # Convert tablewidth to characters from hex bytes tablewidth = int(tablewidth) * 6 # Initialize output buffer outstring = '' # Open File fin = open(os.path.expanduser(infile), "rb") uint8_tstoread = os.path.getsize(os.path.expanduser(infile)) valuesfromfile = array.array('B') try: valuesfromfile.fromfile(fin, uint8_tstoread) finally: fin.close() # Get bytes from file values=valuesfromfile.tolist() # Exit if it's not a Windows BMP if ((values[0] != 0x42) or (values[1] != 0x4D)): sys.exit ("Error: Unsupported BMP format. Make sure your file is a Windows BMP.") # Calculate width, height dataOffset = getLONG(values, 10) # Offset to image data pixelWidth = getLONG(values, 18) # Width of image pixelHeight = getLONG(values, 22) # Height of image bitDepth = getINT (values, 28) # Bits per pixel dataSize = getLONG(values, 34) # Size of raw data # Calculate line width in bytes and padded byte width (each row is padded to 4-byte multiples) byteWidth = int(math.ceil(float(pixelWidth * bitDepth)/8.0)) paddedWidth = int(math.ceil(float(byteWidth)/4.0)*4.0) # For auto (sizebytes = 0), set sizebytes to 1 or 2, depending on size of the bitmap if (sizebytes==0): if (pixelWidth>255) or (pixelHeight>255): sizebytes = 2 else: sizebytes = 1 # The invert byte is set based on the invert command line flag (but, the logic is reversed for 1-bit files) invertbyte = 0xFF if invert else 0x00 if (bitDepth == 1): invertbyte = invertbyte ^ 0xFF # Output the hex table declaration # With "raw" output, output just an array of chars if (raw): # Output the data declaration print ('PROGMEM unsigned char const ' + tablename + ' [] = {') # Output the size of the BMP if (not (sizebytes%2)): print ("{0:#04X}".format((pixelWidth>>8) & 0xFF) + ", " + "{0:#04X}".format(pixelWidth & 0xFF) + ", " + \ "{0:#04X}".format((pixelHeight>>8) & 0xFF) + ", " + "{0:#04X}".format(pixelHeight & 0xFF) + ",") else: print ("{0:#04X}".format(pixelWidth & 0xFF) + ", " + "{0:#04X}".format(pixelHeight & 0xFF) + ",") elif (named): print ('PROGMEM ' + getDoubleType(double)[0] + ' const ' + tablename + '_PIXELS[] = {') elif (xbm): print ('#define ' + tablename + '_width ' + str(pixelWidth)) print ('#define ' + tablename + '_height ' + str(pixelHeight)) print ('PROGMEM ' + getDoubleType(double)[0] + ' const ' + tablename + '_bits[] = {') else: print ('PROGMEM const struct {') print (' unsigned int width;') print (' unsigned int height;') print (' unsigned int bitDepth;') print (' ' + pixelDataType + 'pixel_data[{0}];'.format(byteWidth * pixelHeight / dataByteLength)) print ('} ' + tablename + ' = {') print ('{0}, {1}, {2}, {{'.format(pixelWidth, pixelHeight, bitDepth)) # Generate HEX bytes for pixel data in output buffer try: for i in range(pixelHeight): for j in range (byteWidth): ndx = dataOffset + ((pixelHeight-1-i) * paddedWidth) + j v = values[ndx] ^ invertbyte if (xbm): v = reflect(v) # print ("{0:#04x}".format(v)) outstring += "{0:#04x}".format(v) + ", " # Wrap the output buffer. Print. Then, finish. finally: outstring = textwrap.fill(outstring[:-2], tablewidth) print (outstring) if (named): print ('};') print (DEFAULTS.STRUCTURE_NAME + ' const ' + tablename + ' = {{{0}, {1}, {2}, 0, '.format(pixelWidth, pixelHeight, bitDepth) + \ pixelDataType + tablename + "_PIXELS};\n\n") else: if (not (raw or xbm)): print ("}") print ("};") # Only run if launched from commandline if __name__ == '__main__': main()