seekablebitmap.py
  1  # SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  import struct
  6  
  7  class SeekableBitmap:
  8      """Allow random access to an uncompressed bitmap file on disk"""
  9      def __init__(
 10          self,
 11          image_file,
 12          width,
 13          height,
 14          bits_per_pixel,
 15          *,
 16          bytes_per_row=None,
 17          data_start=None,
 18          stride=None,
 19          palette=None,
 20      ):
 21          """Construct a SeekableBitmap"""
 22          self.image_file = image_file
 23          self.width = width
 24          self.height = height
 25          self.bits_per_pixel = bits_per_pixel
 26          self.bytes_per_row = (
 27              bytes_per_row if bytes_per_row else (bits_per_pixel * width + 7) // 8
 28          )
 29          self.stride = stride if stride else self.bytes_per_row
 30          self.palette = palette
 31          self.data_start = data_start if data_start else image_file.tell()
 32  
 33      def get_row(self, row):
 34          self.image_file.seek(self.data_start + row * self.stride)
 35          return self.image_file.read(self.bytes_per_row)
 36  
 37  
 38  def _pnmopen(filename):
 39      """
 40      Scan for netpbm format info, skip over comments, and read header data.
 41  
 42      Return the format, header, and the opened file positioned at the start of
 43      the bitmap data.
 44      """
 45      # pylint: disable=too-many-branches
 46      image_file = open(filename, "rb")
 47      magic_number = image_file.read(2)
 48      image_file.seek(2)
 49      pnm_header = []
 50      next_value = bytearray()
 51      while True:
 52          # We have all we need at length 3 for formats P2, P3, P5, P6
 53          if len(pnm_header) == 3:
 54              return image_file, magic_number, pnm_header
 55  
 56          if len(pnm_header) == 2 and magic_number in [b"P1", b"P4"]:
 57              return image_file, magic_number, pnm_header
 58  
 59          next_byte = image_file.read(1)
 60          if next_byte == b"":
 61              raise RuntimeError("Unsupported image format {}".format(magic_number))
 62          if next_byte == b"#":  # comment found, seek until a newline or EOF is found
 63              while image_file.read(1) not in [b"", b"\n"]:  # EOF or NL
 64                  pass
 65          elif not next_byte.isdigit():  # boundary found in header data
 66              if next_value:
 67                  # pull values until space is found
 68                  pnm_header.append(int("".join(["%c" % char for char in next_value])))
 69                  next_value = bytearray()  # reset the byte array
 70          else:
 71              next_value += next_byte  # push the digit into the byte array
 72  
 73  
 74  def pnmopen(filename):
 75      """
 76      Interpret netpbm format info and construct a SeekableBitmap
 77      """
 78      image_file, magic_number, pnm_header = _pnmopen(filename)
 79      if magic_number == b"P4":
 80          return SeekableBitmap(
 81              image_file,
 82              pnm_header[0],
 83              pnm_header[1],
 84              1,
 85              palette=b"\xff\xff\xff\x00\x00\x00\x00\x00",
 86          )
 87      if magic_number == b"P5":
 88          return SeekableBitmap(
 89              image_file, pnm_header[0], pnm_header[1], pnm_header[2].bit_length()
 90          )
 91      if magic_number == b"P6":
 92          return SeekableBitmap(
 93              image_file, pnm_header[0], pnm_header[1], 3 * pnm_header[2].bit_length()
 94          )
 95      raise ValueError(f"Unknown or unsupported magic number {magic_number}")
 96  
 97  
 98  def bmpopen(filename):
 99      """
100      Interpret bmp format info and construct a SeekableBitmap
101      """
102      image_file = open(filename, "rb")
103  
104      header = image_file.read(34)
105  
106      data_start, header_size, width, height, _, bits_per_pixel, _ = struct.unpack(
107          "<10x4l2hl", header
108      )
109  
110      bits_per_pixel = bits_per_pixel if bits_per_pixel != 0 else 1
111  
112      palette_start = header_size + 14
113      image_file.seek(palette_start)
114      palette = image_file.read(4 << bits_per_pixel)
115  
116      stride = (bits_per_pixel * width + 31) // 32 * 4
117      if height < 0:
118          height = -height
119      else:
120          data_start = data_start + stride * (height - 1)
121          stride = -stride
122  
123      return SeekableBitmap(
124          image_file,
125          width,
126          height,
127          bits_per_pixel,
128          data_start=data_start,
129          stride=stride,
130          palette=palette,
131      )
132  
133  
134  def imageopen(filename):
135      """
136      Open a bmp or pnm file as a seekable bitmap
137      """
138      if filename.lower().endswith(".bmp"):
139          return bmpopen(filename)
140      return pnmopen(filename)