diff --git a/IMAGE_DISPLAY.md b/IMAGE_DISPLAY.md index 84e1d63..4b6eb8d 100644 --- a/IMAGE_DISPLAY.md +++ b/IMAGE_DISPLAY.md @@ -127,13 +127,16 @@ necessarily a greyscale image). When asked for Data Formatting, select RAW. ## 2.2 Using img_cvt.py This takes a PPM or PGM file and outputs a binary file in the correct format for -display. Typical usage: +display; alternatively a Python source file may be output. The latter offers the +option to freeze the file for fast display with minimal RAM use. Typical usage: + ```bash $ ./img_cvt.py test.ppm test.bin ``` Mandatory positional args: 1. `infile` Input file path. -2. `outfile` Output file path. +2. `outfile` Output file path. If the file extension is `.py` a Python source +file will be output. Optional args: 1. `-r` or `--rows` Expected image dimensions. If passed these are checked against the actual image and a warning printed on a mismatch. @@ -159,7 +162,16 @@ except that selecting `None` led to a substantial loss of quality. # 3. Populating the Frame Buffer -## 3.1 Output file format +A binary file may be used as in the examples in section 1.5: this is RAM +efficient but file access from Flash tends to be slow. + +Converting to Python source offers two routes for fast updates. A `FrameBuffer` +may be instantiated from the Python file and blitted to the device. Best suited +for small graphical elements such as sprites. Full screen images can be copied +to the device buffer - again yielding rapid updates. If the source file is +frozen this is RAM efficient and fast. + +## 3.1 Binary output file format The first four bytes comprise a count of rows, then cols, in big-endian format. The following bytes are pixel data in a horizontally mapped format. Pixels @@ -168,7 +180,36 @@ imagined as an array of size `rows * cols` the sequence of pixels coming from th input stream is: p[0, 0],p[0, 1]...p[0, cols-1],p[1, 0],p[1,1]...p[1, cols-1]...p[rows-1, cols-1] -## 3.2 Frame Buffer Access +## 3.2 Python output file format + +Bound variables: +* `source` The path of the source image file. +* `rows` Image dimensions in pixels. +* `cols` +* `mode` Mode used to create a FrameBuffer +* `data` Image data bytes, with layout as per binary file. + +## 3.3 Using a Python image file + +To display a full screen image it may be copied the device's underlying buffer: +```py +import img # Python file containing the image +ssd.mvb[:] = img.data +``` +An alternative approach is to create a second `FrameBuffer` instance from the +image and blit it to the `ssd` device (which is a `FrameBuffer` subclass). +Unfortunately [this issue](https://github.com/micropython/micropython/pull/15285) +prevents creating a `FrameBuffer` from a Flash-based `bytes` object. However +blitting small RAM-based Python images would be useful for projects such as +games. +```py +import img # Python file containing the image +ba = bytearray(img.data) # Put in RAM because of above issue +fb = framebuf.FrameBuffer(ba, img.cols, img.rows, img.mode) +ssd.blit(fb, col, row) # blit to a given location +``` + +## 3.4 Frame Buffer Access Updated display drivers have a `mvb` bound variable: this is a `memoryview` into the bytearray containing the frame buffer. The three GUIs make the display diff --git a/img_cvt.py b/img_cvt.py index 929a68a..d424e54 100755 --- a/img_cvt.py +++ b/img_cvt.py @@ -14,6 +14,14 @@ import argparse import sys import os +from io import BytesIO + +# FrameBuffer constants with string mappings +RGB565 = 1 +GS4_HMSB = 2 +GS8 = 6 +modestr = {RGB565: "16-bit color RGB565", GS8: "8-bit color RRRGGGBB", GS4_HMSB: "4-bit greyscale"} +fmtstr = {RGB565: b"P6", GS8: b"P6", GS4_HMSB: b"P5"} # Netbpm file ID strings # Dithering data. Divisor followed by 3-tuples comprising # row-offset, col-offset, multiplier @@ -124,37 +132,91 @@ def convrgb(arr, rows, cols, si, so, bits): so.write(int.to_bytes(op, 2, "big")) # Red first -def conv(arr, fni, fno, height, width, color_mode): - with open(fno, "wb") as fo: - with open(fni, "rb") as fi: - fmt = fi.readline() # Get file format - txt = fi.readline() - while txt.startswith(b"#"): # Ignore comments - txt = fi.readline() - cols, rows = txt.split(b" ") - cols = int(cols) - rows = int(rows) - cdepth = int(fi.readline()) - fail = (fmt[:2] != b"P6") if color_mode else (fmt[:2] != b"P5") - if fail: - quit("Source file contents do not match file extension.") - fo.write(b"".join((rows.to_bytes(2, "big"), cols.to_bytes(2, "big")))) - if height is not None and width is not None: - if not (cols == width and rows == height): - print( - f"Warning: Specified dimensions {width}x{height} do not match those in source file {cols}x{rows}" - ) - print(f"Writing file, dimensions rows = {rows}, cols = {cols}") - if not color_mode: - convgs(arr, rows, cols, fi, fo) - mode = "4-bit greyscale" - elif color_mode == 1: - convrgb(arr, rows, cols, fi, fo, 16) - mode = "16-bit color RGB565" - elif color_mode == 2: - convrgb(arr, rows, cols, fi, fo, 8) - mode = "8-bit color RRRGGGBB" - print(f"File {fno} written in {mode}.") +# Convert an input stream, putting result on an output stream. +def conv(arr, si, so, height, width, mode): + fmt = si.readline() # Get file format + txt = si.readline() + while txt.startswith(b"#"): # Ignore comments + txt = si.readline() + cols, rows = txt.split(b" ") + cols = int(cols) + rows = int(rows) + cdepth = int(si.readline()) + if fmt[:2] != fmtstr[mode]: + quit("Source file contents do not match file extension.") + so.write(b"".join((rows.to_bytes(2, "big"), cols.to_bytes(2, "big")))) + if height is not None and width is not None: + if not (cols == width and rows == height): + print(f"Warning: Specified dimensions {width}x{height}") + print(f"do not match those in source file {cols}x{rows}") + print(f"Writing file, dimensions rows = {rows}, cols = {cols}") + if mode == GS4_HMSB: # 4-bit greyscale + convgs(arr, rows, cols, si, so) + elif mode == RGB565: # 16-bit color + convrgb(arr, rows, cols, si, so, 16) + elif mode == GS8: # 8-bit color + convrgb(arr, rows, cols, si, so, 8) + return rows, cols # Actual values from file + + +# **** Python code generation +class ByteWriter: + bytes_per_line = 16 + + def __init__(self, stream, varname): + self.stream = stream + self.stream.write(f"{varname} =\\\n") + self.bytecount = 0 # For line breaks + + def _eol(self): + self.stream.write("'\\\n") + + def _eot(self): + self.stream.write("'\n") + + def _bol(self): + self.stream.write("b'") + + # Output a single byte + def obyte(self, data): + if not self.bytecount: + self._bol() + self.stream.write(f"\\x{data:02x}") + self.bytecount += 1 + self.bytecount %= self.bytes_per_line + if not self.bytecount: + self._eol() + + # Output from a sequence + def odata(self, bytelist): + for byt in bytelist: + self.obyte(byt) + + # ensure a correct final line + def eot(self): # User force EOL if one hasn't occurred + if self.bytecount: + self._eot() + self.stream.write("\n") + + +# Create a bound variable. Quote if it's a string. +def write_var(stream, name, arg): + s = f'{name} = "{arg}"\n' if isinstance(arg, str) else f"{name} = {arg}\n" + stream.write(s) + + +# Write Python source using data stream on sd +def writepy(ip_stream, op_stream, rows, cols, mode, fname): + op_stream.write("# Code generated by img_cvt.py.") + write_var(op_stream, "version", "0.1") + write_var(op_stream, "source", fname) + write_var(op_stream, "rows", rows) + write_var(op_stream, "cols", cols) + write_var(op_stream, "mode", mode) + bw_data = ByteWriter(op_stream, "data") + ip_stream.seek(4) # Skip 4 bytes of dimension data + bw_data.odata(ip_stream.read()) + bw_data.eot() # **** Parse command line arguments **** @@ -176,6 +238,8 @@ passed, in which case it is RRRR RGGG GGGB BBBB. A greyscale pgm file is output in 4-bit greyscale. By default the Atkinson dithering algorithm is used. Other options are FS (Floyd–Steinberg), Burke, Sierra and None. + +If the output filename extension is ".py" a Python sourcefile will be output. """ if __name__ == "__main__": @@ -195,19 +259,38 @@ if __name__ == "__main__": ) parser.add_argument("--rgb565", action="store_true", help="Create 16-bit RGB565 file.") args = parser.parse_args() - # print(args.dither) - # quit("Done") if not os.path.isfile(args.infile): quit("Source image filename does not exist") - extension = os.path.splitext(args.infile)[1].upper() + if extension == ".PPM": - cmode = 1 if args.rgb565 else 2 # Color image + mode = RGB565 if args.rgb565 else GS8 # Color image 16/8 bits elif extension == ".PGM": if args.rgb565: quit("--rgb565 arg can only be used with color images.") - cmode = 0 # Greyscale image + mode = GS4_HMSB # Greyscale image else: quit("Source image file should be a ppm or pgm file.") arr = dither_options[args.dither] - conv(arr, args.infile, args.outfile, args.rows, args.cols, cmode) + ofextension = os.path.splitext(args.outfile)[1].upper() + try: + si = open(args.infile, "rb") + except OSError: + quit(f"Cannot open {args.infile} for reading.") + fmode = "w" if ofextension == ".PY" else "wb" # binary or text file + ftype = "Python" if ofextension == ".PY" else "Binary" + try: + sp = open(args.outfile, fmode) # Binary file + except OSError: + quit(f"Cannot open {args.outfile} for writing.") + try: + if ofextension == ".PY": + with BytesIO() as so: # Write to stream. Return dimensions from file + rows, cols = conv(arr, si, so, args.rows, args.cols, mode) + writepy(so, sp, rows, cols, mode, args.infile) + else: + rows, cols = conv(arr, si, sp, args.rows, args.cols, mode) + print(f"{ftype} file {args.outfile} written in {modestr[mode]}.") + finally: + si.close() + sp.close()