FearBooth/seekablebitmap.py

141 lines
4.2 KiB
Python
Raw Permalink Normal View History

2023-10-19 07:39:08 -05:00
# 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)