141 lines
4.2 KiB
Python
141 lines
4.2 KiB
Python
|
# SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
|
||
|
#
|
||
|
# SPDX-License-Identifier: MIT
|
||
|
|
||
|
import struct
|
||
|
|
||
|
class SeekableBitmap:
|
||
|
"""Allow random access to an uncompressed bitmap file on disk"""
|
||
|
def __init__(
|
||
|
self,
|
||
|
image_file,
|
||
|
width,
|
||
|
height,
|
||
|
bits_per_pixel,
|
||
|
*,
|
||
|
bytes_per_row=None,
|
||
|
data_start=None,
|
||
|
stride=None,
|
||
|
palette=None,
|
||
|
):
|
||
|
"""Construct a SeekableBitmap"""
|
||
|
self.image_file = image_file
|
||
|
self.width = width
|
||
|
self.height = height
|
||
|
self.bits_per_pixel = bits_per_pixel
|
||
|
self.bytes_per_row = (
|
||
|
bytes_per_row if bytes_per_row else (bits_per_pixel * width + 7) // 8
|
||
|
)
|
||
|
self.stride = stride if stride else self.bytes_per_row
|
||
|
self.palette = palette
|
||
|
self.data_start = data_start if data_start else image_file.tell()
|
||
|
|
||
|
def get_row(self, row):
|
||
|
self.image_file.seek(self.data_start + row * self.stride)
|
||
|
return self.image_file.read(self.bytes_per_row)
|
||
|
|
||
|
|
||
|
def _pnmopen(filename):
|
||
|
"""
|
||
|
Scan for netpbm format info, skip over comments, and read header data.
|
||
|
|
||
|
Return the format, header, and the opened file positioned at the start of
|
||
|
the bitmap data.
|
||
|
"""
|
||
|
# pylint: disable=too-many-branches
|
||
|
image_file = open(filename, "rb")
|
||
|
magic_number = image_file.read(2)
|
||
|
image_file.seek(2)
|
||
|
pnm_header = []
|
||
|
next_value = bytearray()
|
||
|
while True:
|
||
|
# We have all we need at length 3 for formats P2, P3, P5, P6
|
||
|
if len(pnm_header) == 3:
|
||
|
return image_file, magic_number, pnm_header
|
||
|
|
||
|
if len(pnm_header) == 2 and magic_number in [b"P1", b"P4"]:
|
||
|
return image_file, magic_number, pnm_header
|
||
|
|
||
|
next_byte = image_file.read(1)
|
||
|
if next_byte == b"":
|
||
|
raise RuntimeError("Unsupported image format {}".format(magic_number))
|
||
|
if next_byte == b"#": # comment found, seek until a newline or EOF is found
|
||
|
while image_file.read(1) not in [b"", b"\n"]: # EOF or NL
|
||
|
pass
|
||
|
elif not next_byte.isdigit(): # boundary found in header data
|
||
|
if next_value:
|
||
|
# pull values until space is found
|
||
|
pnm_header.append(int("".join(["%c" % char for char in next_value])))
|
||
|
next_value = bytearray() # reset the byte array
|
||
|
else:
|
||
|
next_value += next_byte # push the digit into the byte array
|
||
|
|
||
|
|
||
|
def pnmopen(filename):
|
||
|
"""
|
||
|
Interpret netpbm format info and construct a SeekableBitmap
|
||
|
"""
|
||
|
image_file, magic_number, pnm_header = _pnmopen(filename)
|
||
|
if magic_number == b"P4":
|
||
|
return SeekableBitmap(
|
||
|
image_file,
|
||
|
pnm_header[0],
|
||
|
pnm_header[1],
|
||
|
1,
|
||
|
palette=b"\xff\xff\xff\x00\x00\x00\x00\x00",
|
||
|
)
|
||
|
if magic_number == b"P5":
|
||
|
return SeekableBitmap(
|
||
|
image_file, pnm_header[0], pnm_header[1], pnm_header[2].bit_length()
|
||
|
)
|
||
|
if magic_number == b"P6":
|
||
|
return SeekableBitmap(
|
||
|
image_file, pnm_header[0], pnm_header[1], 3 * pnm_header[2].bit_length()
|
||
|
)
|
||
|
raise ValueError(f"Unknown or unsupported magic number {magic_number}")
|
||
|
|
||
|
|
||
|
def bmpopen(filename):
|
||
|
"""
|
||
|
Interpret bmp format info and construct a SeekableBitmap
|
||
|
"""
|
||
|
image_file = open(filename, "rb")
|
||
|
|
||
|
header = image_file.read(34)
|
||
|
|
||
|
data_start, header_size, width, height, _, bits_per_pixel, _ = struct.unpack(
|
||
|
"<10x4l2hl", header
|
||
|
)
|
||
|
|
||
|
bits_per_pixel = bits_per_pixel if bits_per_pixel != 0 else 1
|
||
|
|
||
|
palette_start = header_size + 14
|
||
|
image_file.seek(palette_start)
|
||
|
palette = image_file.read(4 << bits_per_pixel)
|
||
|
|
||
|
stride = (bits_per_pixel * width + 31) // 32 * 4
|
||
|
if height < 0:
|
||
|
height = -height
|
||
|
else:
|
||
|
data_start = data_start + stride * (height - 1)
|
||
|
stride = -stride
|
||
|
|
||
|
return SeekableBitmap(
|
||
|
image_file,
|
||
|
width,
|
||
|
height,
|
||
|
bits_per_pixel,
|
||
|
data_start=data_start,
|
||
|
stride=stride,
|
||
|
palette=palette,
|
||
|
)
|
||
|
|
||
|
|
||
|
def imageopen(filename):
|
||
|
"""
|
||
|
Open a bmp or pnm file as a seekable bitmap
|
||
|
"""
|
||
|
if filename.lower().endswith(".bmp"):
|
||
|
return bmpopen(filename)
|
||
|
return pnmopen(filename)
|