pcf.py
  1  # pylint: skip-file
  2  # Remove the above when PCF is actually supported.
  3  
  4  from .glyph_cache import GlyphCache
  5  import displayio
  6  import struct
  7  
  8  _PCF_PROPERTIES = 1 << 0
  9  _PCF_ACCELERATORS = 1 << 1
 10  _PCF_METRICS = 1 << 2
 11  _PCF_BITMAPS = 1 << 3
 12  _PCF_INK_METRICS = 1 << 4
 13  _PCF_BDF_ENCODINGS = 1 << 5
 14  _PCF_SWIDTHS = 1 << 6
 15  _PCF_GLYPH_NAMES = 1 << 7
 16  _PCF_BDF_ACCELERATORS = 1 << 8
 17  
 18  _PCF_DEFAULT_FORMAT = 0x00000000
 19  _PCF_INKBOUNDS = 0x00000200
 20  _PCF_ACCEL_W_INKBOUNDS = 0x00000100
 21  _PCF_COMPRESSED_METRICS = 0x00000100
 22  
 23  _PCF_GLYPH_PAD_MASK = 3 << 0  # See the bitmap table for explanation */
 24  _PCF_BYTE_MASK = 1 << 2  # If set then Most Sig Byte First */
 25  _PCF_BIT_MASK = 1 << 3  # If set then Most Sig Bit First */
 26  _PCF_SCAN_UNIT_MASK = 3 << 4
 27  
 28  # https://fontforge.github.io/en-US/documentation/reference/pcf-format/
 29  
 30  
 31  class PCF(GlyphCache):
 32      def __init__(self, f):
 33          super().__init__()
 34          self.file = f
 35          self.name = f
 36          f.seek(0)
 37          header, table_count = self.read("<4sI")
 38          self.tables = {}
 39          for _ in range(table_count):
 40              type, format, size, offset = self.read("<IIII")
 41              self.tables[type] = {"format": format, "size": size, "offset": offset}
 42              print(type)
 43  
 44      def read(self, format):
 45          s = struct.calcsize(format)
 46          return struct.unpack_from(format, self.file.read(s))
 47  
 48      def get_bounding_box(self):
 49          property_table_offset = self.tables[_PCF_PROPERTIES]["offset"]
 50          self.file.seek(property_table_offset)
 51          (format,) = self.read("<I")
 52  
 53          if format & _PCF_BYTE_MASK == 0:
 54              raise RuntimeError("Only big endian supported")
 55          (nprops,) = self.read(">I")
 56          self.file.seek(property_table_offset + 8 + 9 * nprops)
 57  
 58          pos = self.file.tell()
 59          if pos % 4 > 0:
 60              self.file.read(4 - pos % 4)
 61          (string_size,) = self.read(">I")
 62  
 63          strings = self.file.read(string_size)
 64          string_map = {}
 65          i = 0
 66          for s in strings.split(b"\x00"):
 67              string_map[i] = s
 68              i += len(s) + 1
 69  
 70          self.file.seek(property_table_offset + 8)
 71          for _ in range(nprops):
 72              name_offset, isStringProp, value = self.read(">IBI")
 73  
 74              if isStringProp:
 75                  print(string_map[name_offset], string_map[value])
 76              else:
 77                  print(string_map[name_offset], value)
 78          return None
 79  
 80      def load_glyphs(self, code_points):
 81          metadata = True
 82          character = False
 83          code_point = None
 84          rounded_x = 1
 85          bytes_per_row = 1
 86          desired_character = False
 87          current_info = None
 88          current_y = 0
 89          total_remaining = len(code_points)
 90  
 91          x, _, _, _ = self.get_bounding_box()
 92          # create a scratch bytearray to load pixels into
 93          scratch_row = memoryview(bytearray((((x - 1) // 32) + 1) * 4))
 94  
 95          self.file.seek(0)
 96          while True:
 97              line = self.file.readline()
 98              if not line:
 99                  break
100              if line.startswith(b"CHARS "):
101                  metadata = False
102              elif line.startswith(b"SIZE"):
103                  _, self.point_size, self.x_resolution, self.y_resolution = line.split()
104              elif line.startswith(b"COMMENT"):
105                  pass
106              elif line.startswith(b"STARTCHAR"):
107                  # print(lineno, line.strip())
108                  # _, character_name = line.split()
109                  character = True
110              elif line.startswith(b"ENDCHAR"):
111                  character = False
112                  if desired_character:
113                      self._glyphs[code_point] = current_info
114                      if total_remaining == 0:
115                          return
116                  desired_character = False
117              elif line.startswith(b"BBX"):
118                  if desired_character:
119                      _, x, y, dx, dy = line.split()
120                      x = int(x)
121                      y = int(y)
122                      dx = int(dx)
123                      dy = int(dy)
124                      current_info["bounds"] = (x, y, dx, dy)
125                      current_info["bitmap"] = displayio.Bitmap(x, y, 2)
126              elif line.startswith(b"BITMAP"):
127                  if desired_character:
128                      rounded_x = x // 8
129                      if x % 8 > 0:
130                          rounded_x += 1
131                      bytes_per_row = rounded_x
132                      if bytes_per_row % 4 > 0:
133                          bytes_per_row += 4 - bytes_per_row % 4
134                      current_y = 0
135              elif line.startswith(b"ENCODING"):
136                  _, code_point = line.split()
137                  code_point = int(code_point)
138                  if code_point == code_points or code_point in code_points:
139                      total_remaining -= 1
140                      if code_point not in self._glyphs:
141                          desired_character = True
142                          current_info = {"bitmap": None, "bounds": None, "shift": None}
143              elif line.startswith(b"DWIDTH"):
144                  if desired_character:
145                      _, shift_x, shift_y = line.split()
146                      shift_x = int(shift_x)
147                      shift_y = int(shift_y)
148                      current_info["shift"] = (shift_x, shift_y)
149              elif line.startswith(b"SWIDTH"):
150                  pass
151              elif character:
152                  if desired_character:
153                      bits = int(line.strip(), 16)
154                      for i in range(rounded_x):
155                          val = (bits >> ((rounded_x - i - 1) * 8)) & 0xFF
156                          scratch_row[i] = val
157                      current_info["bitmap"]._load_row(
158                          current_y, scratch_row[:bytes_per_row]
159                      )
160                      current_y += 1
161              elif metadata:
162                  # print(lineno, line.strip())
163                  pass