2023-02-03 01:22:15 +08:00

172 lines
5.4 KiB
Python

'''Read and write PBM format image file
PBM format: http://netpbm.sourceforge.net/doc/pbm.html
Only support single image in a file
'''
def _is_space(char):
r'''C isspace function'''
return char in [0x20, 0x09 ,0x0a ,0x0b, 0x0c, 0x0d] #[ \t\n\v\f\r]
def _is_new_line(char):
r''' char is \r or \n'''
return char in [0x0a ,0x0d] #[\n\r]
def _is_comment_start(char):
''' char is #'''
return char == 0x23 #[#]
def _read_header(instream):
r'''Read file header, get basic information
:param: instream: stream that support seek, tell, read
:returns: (
width,
hetght,
raster_data_format_P1_or_P4_or_UNKNOWN,
raster_start_position,
comment
)
'''
# magic number
instream.seek(0)
mg = instream.read(2)
if mg != b"P1" and mg != b"P4":
return (-1, -1, b"UNKNOWN", -1, b"")
# state machine
comment = bytearray()
buffer = bytearray() # list to store bytes
is_reading_info = False
is_reading_comment = False
width = -1
height = -1
byts = instream.read(1)
while len(byts) == 1:
char = byts[0]
# reading comment state
if is_reading_comment:
comment.append(char)
if _is_new_line(char):
is_reading_comment = False
# reading info state
elif is_reading_info:
if _is_comment_start(char):
is_reading_comment = True
elif _is_space(char):
if width < 0:
width = int(buffer.decode(),10)
elif height < 0:
height = int(buffer.decode(),10)
buffer = bytearray()
is_reading_info = False
if width >= 0 and height >= 0:
# read header finished
break
else:
buffer.append(char)
# normal state
elif not is_reading_info and not is_reading_comment:
if _is_space(char):
pass
elif _is_comment_start(char):
is_reading_comment = True
else:
buffer.append(char)
is_reading_info = True
byts = instream.read(1)
pos = instream.tell()
return (width, height, mg, pos, comment)
def _read_data(instream, width, height, format, offset=-1):
r'''Read image data
:param: instream: stream that support seek, tell, read
width: image width
height: image height
format: b'P1' or b'P4'
:return: data in MONO_HLSB(only on micropython, MSB on other platform) format bytes
'''
if format != b"P1" and format != b"P4":
return bytearray(0)
if offset >= 0:
instream.seek(offset)
width_count = width // 8
width_count += 0 if width % 8 == 0 else 1
size = width_count * height
# bytecode format
if format == b"P4":
return bytearray(instream.read(size))
# text format
data = bytearray(size)
byte_data = 0
bitp = 0
width_p = 0
index = 0
byts = instream.read(1)
while index < size and len(byts) == 1:
char = byts[0]
if _is_space(char):
pass
else:
byte_data = (byte_data << 1) | ((char-0x30) & 0x01)
bitp += 1
width_p += 1
if width_p == width:
byte_data <<= 8 - bitp
data[index] = byte_data
index += 1
byte_data = 0
bitp = 0
width_p = 0
elif (bitp >= 8):
data[index] = byte_data
index += 1
byte_data = 0
bitp = 0
byts = instream.read(1)
return data
def read_image(instream):
r'''Read image file, return basic information and data
:param: instream: stream that support seek, tell, read
:returns: (
width,
hetght,
raster_data_format_P1_or_P4_or_UNKNOWN,
image_data,
comment,
)
'''
width, height, mg, _, comment = _read_header(instream)
data = _read_data(instream, width, height, mg)
return (width, height, mg, data, comment)
def make_image(outstream, width, height, data, format='P4', comment="made with bpm.py"):
r'''Write an image file
:param: outstream: file to write
width: image width
height: image height
data: image data in MONO_HLSB(only on micropython, MSB on other platform) format
format: "P1" or "P4"
comment: string
:return: file size
'''
if isinstance(format, bytes) or isinstance(format, bytearray):
format = format.decode("utf8")
assert format == "P4" or format == "P1"
if isinstance(comment, bytes) or isinstance(comment, bytearray):
comment = comment.decode("utf8")
outstream.write("{:s}\n#{:s}\n{:d} {:d}\n".format(format, comment, width, height).encode())
if format == "P4":
outstream.write(data)
else:
wbyte = width // 8
wbyte += 0 if width % 8 == 0 else 1
for y in range(height):
for x in range(width):
offset = (y * wbyte) + (x // 8)
bit = (7 - (x % 8))
value = (data[offset] >> bit) & 0x01
if x != 0:
outstream.write(b' ')
outstream.write(b'0' if value == 0 else b'1')
outstream.write("\n".encode())
size = outstream.tell()
return size