From c1ab76260f8057c680905554ab3aace494834218 Mon Sep 17 00:00:00 2001 From: thanks4opensource <31165662+thanks4opensource@users.noreply.github.com> Date: Tue, 29 Aug 2017 03:06:00 -0700 Subject: [PATCH] Python version of Logo Editor/TS100 Logo Editor (#78) --- python_logo_converter/img2ts100.py | 251 +++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100755 python_logo_converter/img2ts100.py diff --git a/python_logo_converter/img2ts100.py b/python_logo_converter/img2ts100.py new file mode 100755 index 00000000..f8e9293d --- /dev/null +++ b/python_logo_converter/img2ts100.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python + +VERSION_STRING = '0.01' + + +import os +import sys + +try: + import PIL, PIL.Image, PIL.ImageOps +except ImportError,error: + raise ImportError, "%s: %s requres Python Imaging Library (PIL). " \ + "Install with `pip` or OS-specific package " \ + "management tool." \ + % (error, sys.argv[0]) + + + +LCD_WIDTH = 96 +LCD_HEIGHT = 16 +LCD_NUM_BYTES = LCD_WIDTH * LCD_HEIGHT / 8 +LCD_PADDED_SIZE = 1024 + +INTELHEX_DATA_RECORD = 0x00 +INTELHEX_END_OF_FILE_RECORD = 0x01 +INTELHEX_EXTENDED_LINEAR_ADDRESS_RECORD = 0x04 +INTELHEX_BYTES_PER_LINE = 16 +INTELHEX_MINIMUM_SIZE = 4096 + + + +def split16(word): + '''return high and low byte of 16-bit word value as tuple''' + return ((word >> 8) & 0xff, word & 0xff) + + + +def intel_hex_line(file, record_type, offset, data): + '''write a line of data in Intel hex format''' + # length, address offset, record type + record_length = len(data) + file.write(':%02X%04X%02X' % (record_length, offset, record_type)) + + # data + map(lambda byte: file.write("%02X" % byte), data) + + # compute and write checksum (with DOS line ending for compatibility/safety) + file.write( "%02X\r\n" + % ( ( ( sum(data, # sum data ... + record_length # ... and other ... + + sum(split16(offset)) # ... fields ... + + record_type) # ... on line + & 0xff) # low 8 bits + ^ 0xff) # two's ... + + 1)) # ... complement + + + +def intel_hex(file, bytes, start_address = 0x0): + '''write block of data in Intel hex format''' + if len(bytes) % INTELHEX_BYTES_PER_LINE != 0: + raise ValueError, \ + "Program error: Size of LCD data is not evenly divisible by %s" \ + % INTELHEX_BYTES_PER_LINE + + address_lo = start_address & 0xffff + address_hi = (start_address >> 16) & 0xffff + + intel_hex_line(file, + INTELHEX_EXTENDED_LINEAR_ADDRESS_RECORD, + 0, + split16(address_hi)) + + size_written = 0 + while size_written < INTELHEX_MINIMUM_SIZE: + offset = address_lo + for line_start in range(0, len(bytes), INTELHEX_BYTES_PER_LINE): + intel_hex_line(file, + INTELHEX_DATA_RECORD, + offset, + bytes[line_start:line_start+INTELHEX_BYTES_PER_LINE]) + size_written += INTELHEX_BYTES_PER_LINE + if size_written >= INTELHEX_MINIMUM_SIZE: + break + offset += INTELHEX_BYTES_PER_LINE + + intel_hex_line(file, INTELHEX_END_OF_FILE_RECORD, 0, ()) + + + +def img2hex(input_filename, + output_file, + preview_filename=None, + threshold=128, + dither=False, + negative=False): + ''' + Convert 'input_filename' image file into Intel hex format with data + formatted for display on TS100 LCD and file object. + Input image is converted from color or grayscale to black-and-white, + and resized to fit TS100 LCD screen as necessary. + Optionally write resized/thresholded/black-and-white preview image + to file specified by name. + Optional `threshold' argument 8 bit value; grayscale pixels greater than + this become 1 (white) in output, less than become 0 (black). + Unless optional `dither', in which case PIL grayscale-to-black/white + dithering algorithm used. + Optional `negative' inverts black/white regardless of input image type + or other options. + ''' + + try: + image = PIL.Image.open(input_filename) + except BaseException,error: + raise IOError, \ + "error reading image file \"%s\": %s" % (input_filename, error) + + # convert to luminance + # do even if already black/white because PIL can't invert 1-bit so + # can't just pass thru in case --negative flag + # also resizing works better in luminance than black/white + # also no information loss converting black/white to grayscale + if image.mode != 'L': + image = image.convert('L') + + if image.size != (LCD_WIDTH, LCD_HEIGHT): + image = image.resize((LCD_WIDTH, LCD_HEIGHT), PIL.Image.BICUBIC) + + if negative: + image = PIL.ImageOps.invert(image) + threshold = 255 - threshold # have to invert threshold + + if dither: + image = image.convert('1') + else: + image = image.point(lambda pixel: 0 if pixel < threshold else 1, '1') + + if preview_filename: image.save(preview_filename) + + ''' DEBUG + for row in range(LCD_HEIGHT): + for column in range(LCD_WIDTH): + if image.getpixel((column, row)): sys.stderr.write('1') + else: sys.stderr.write('0') + sys.stderr.write('\n') + ''' + + # pad to this size (also will be repeated in output Intel hex file) + data = [0] * LCD_PADDED_SIZE + + # magic/undocumented/required header in endian-reverse byte order + data[0] = 0x55 + data[1] = 0xAA + data[2] = 0x0D + data[3] = 0xF0 + + # convert to TS100 LCD format + for ndx in range(LCD_WIDTH * 16 / 8): + bottom_half_offset = 0 if ndx < LCD_WIDTH else 8 + byte = 0 + for y in range(8): + if image.getpixel((ndx % LCD_WIDTH, y + bottom_half_offset)): + byte |= 1 << y + # store in endian-reversed byte order + data[4 + ndx + (1 if ndx % 2 == 0 else -1)] = byte + + intel_hex(output_file, data, 0x0800B800) + + + +def parse_commandline(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description="Convert image file for display on TS100 LCD " + "at startup") + + def zero_to_255(text): + try: + value = int(text) + assert(value >= 0 and value <= 255) + except: + raise argparse.ArgumentTypeError("must be integer from 0 to 255 ") + return value + + parser.add_argument('input_filename', + help="input image file") + + parser.add_argument('output_filename', + help="output Intel hex file") + + parser.add_argument('-p', '--preview', + help="filename of image preview (same data as " + "Intel hex file, as will appear on TS100 LCD)") + + parser.add_argument('-n', '--negative', + action='store_true', + help="photo negative: exchange black and white " + "in output") + + parser.add_argument('-t', '--threshold', + type=zero_to_255, + default=128, + help="0 to 255: gray (or color converted to gray) " + "above this becomes white, below becomes black; " + "ignored if using --dither") + + parser.add_argument('-d', '--dither', + action='store_true', + help="use dithering (speckling) to convert gray or " + "color to black and white") + + parser.add_argument('-f', '--force', + action='store_true', + help="force overwriting of existing files") + + parser.add_argument('-v', '--version', + action='version', + version="%(prog)s version " + VERSION_STRING, + help="print version info") + + return parser.parse_args() + + + +if __name__ == "__main__": + import argparse + args = parse_commandline() + + if os.path.exists(args.output_filename) and not args.force: + sys.stderr.write( "Won't overwrite existing file \"%s\" (use --force " + "option to override)\n" + % args.output_filename) + sys.exit(1) + + if args.preview and os.path.exists(args.preview) and not args.force: + sys.stderr.write( "Won't overwrite existing file \"%s\" (use --force " + "option to override)\n" + % args.preview) + sys.exit(1) + + try: + with open(args.output_filename, 'w') as output_file: + img2hex(args.input_filename, + output_file, + args.preview, + args.threshold, + args.dither, + args.negative) + except BaseException,error: + sys.stderr.write("Error converting file: %s\n" % error) + sys.exit(1)