kopia lustrzana https://github.com/peterhinch/micropython-nano-gui
Image display: support Python image files.
rodzic
baa0f0cc5f
commit
29ef2002b4
|
@ -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
|
||||
|
|
157
img_cvt.py
157
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()
|
||||
|
|
Ładowanie…
Reference in New Issue