data.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import re
  2. import struct
  3. import sys
  4. from math import floor
  5. from datetime import datetime, tzinfo, timedelta, timezone
  6. class Tag:
  7. """
  8. Class encapsulating an TrueType/OpenType tag data type.
  9. - https://docs.microsoft.com/en-us/typography/opentype/spec/otff#data-types
  10. """
  11. def __init__(self, string):
  12. """
  13. Initialises a tag based on a string.
  14. Ensures that a string is compliant with OpenType's tag data type.
  15. (Must have exactly 4 characters, each character being between U+20-U+7e.)
  16. """
  17. openTypeTagRegex = "[^\u0020-\u007e]"
  18. if len(string) != 4:
  19. raise ValueError(f"Your tag must contain exactly 4 characters. You gave {len(string)}. ('{string}')")
  20. find = re.findall(openTypeTagRegex, string)
  21. if len(find) > 0:
  22. raise ValueError(f"The tag contains the following: {', '.join(find)}. This is not valid. It must only contain certain characters. These include alphanumeric characters and some symbols. (In techspeak - only in the unicode range U+20-U+7e.)")
  23. self.tag = string
  24. def __str__(self):
  25. return self.tag
  26. def __repr__(self):
  27. return str(self.tag)
  28. def __int__(self):
  29. """
  30. Converts tag to it's expected representation in an OpenType font.
  31. (Array of 4 UInt8s, each UInt8 representing each character's Unicode codepoint.)
  32. ie.
  33. "OTTO"
  34. = 0x4F54544F
  35. (O T T 0 )
  36. (4F 54 54 4F)
  37. """
  38. tagList = list(self.tag)
  39. intList = [f"{ord(t):2x}" for t in tagList]
  40. return int(intList[0] + intList[1] + intList[2] + intList[3], 16)
  41. def toBytes(self):
  42. """
  43. Returns the tag's int representation as bytes in big-endian format.
  44. """
  45. return int(self).to_bytes(4, 'big')
  46. class BFlags:
  47. """
  48. Class encapsulating binary flags in font tables.
  49. """
  50. def __init__(self, string):
  51. """
  52. Binary flags are entered in big-endian order. (ie. left-to-right).
  53. Input can be formatted with spaces (ie. '00100000 00001010').
  54. Binary flags can only be 8, 16 or 32 bits long.
  55. """
  56. if type(string) is not str:
  57. raise ValueError("Making binaryFlags data type failed. Input data is not a string.")
  58. string = string.translate({ord(' '):None}) # strip spaces
  59. if len(string) not in [8, 16, 32]:
  60. raise ValueError(f"Making binaryFlags data type failed. The amount of bits given was not 8, 16 or 32. It has to be one of these.")
  61. self.len = floor(len(string)/8)
  62. if sys.byteorder == 'little':
  63. string = string[::-1] # reverse the byte order if system is little endian.
  64. try:
  65. self.bits = int(string, 2)
  66. except ValueError as e:
  67. raise ValueError(f"Making binaryFlags data type failed. -> {e}")
  68. def __str__(self):
  69. """
  70. Returns a string-formatted list of bits, with spacing every 8 bits.
  71. In big-endian byte order (first to last).
  72. """
  73. string = ""
  74. bitString = f"{self.bits:0{self.len*8}b}"
  75. if sys.byteorder == 'little': # ensure what we're working with is big-endian.
  76. bitString = bitString[::-1]
  77. for index, c in enumerate(bitString): # big-endian
  78. if index%8 == 0 and index != 0: # every 8 bits, add a space.
  79. string += ' '
  80. string += str(c) # append the bit as a string
  81. return string
  82. def __repr__(self):
  83. return str(self)
  84. def set(self, bitNumber, value):
  85. """
  86. Sets a bit to a specific binary value.
  87. """
  88. self.bits = self.bits & ~(1 << bitNumber) | (value << bitNumber)
  89. def toList(self):
  90. """
  91. Returns a list of ints representing the flags, in big-endian order.
  92. """
  93. list = []
  94. bitString = f"{self.bits:0{self.len*8}b}"
  95. if sys.byteorder == 'little': # ensure what we're working with is big-endian.
  96. bitString = bitString[::-1]
  97. for index, c in enumerate(bitString): # big-endian
  98. list.append(int(c)) # append the bit as an int
  99. return list
  100. def toTTXStr(self):
  101. """
  102. Returns a string that's little-endian formatted, for TTX use.
  103. """
  104. return str(self)[::-1] # just get reverse str(), since str() guarantees big-endian.
  105. def toBytes(self):
  106. """
  107. Returns bytes in big-endian format.
  108. """
  109. return self.bits.to_bytes(self.len, 'big')
  110. class Fixed:
  111. """
  112. A representation of a 'Fixed' data type in a font. This is used in normal fixed values, as well as by head.fontRevision.
  113. (A decimal number where the two numbers on either side of the decimal represent exactly 16 bits.)
  114. - https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-version-numbers
  115. - https://silnrsi.github.io/FDBP/en-US/Versioning.html
  116. """
  117. def __init__(self, string):
  118. versionComponents = string.split('.')
  119. # normal decimal versions
  120. self.majorVersionSimple = versionComponents[0]
  121. self.minorVersionSimple = versionComponents[1]
  122. try:
  123. # creating an OpenType-compliant fontRevision number based on best practices.
  124. # https://silnrsi.github.io/FDBP/en-US/Versioning.html
  125. self.majorVersionCalc = int(versionComponents[0])
  126. self.minorVersionCalc = int(( int(versionComponents[1]) / 1000 ) * 65536)
  127. except:
  128. raise Exception("Converting headVersion to it's proper data structure failed for some reason!" + str(e))
  129. def __str__(self):
  130. """
  131. Returns a friendly, non-weird version of it.
  132. """
  133. return self.majorVersionSimple + '.' + self.minorVersionSimple
  134. def toHex(self):
  135. """
  136. Returns a proper hexidecimal representation of the version number as a string.
  137. """
  138. return '0x' + f"{self.majorVersionCalc:04x}" + f"{self.minorVersionCalc:04x}"
  139. def __int__(self):
  140. """
  141. Returns the proper hexadecimal representation of this value.
  142. ie.
  143. 1.040
  144. 000010a3d
  145. 1 . 040
  146. 0001 0a3d
  147. """
  148. return int(f"{self.majorVersionCalc:04x}" + f"{self.minorVersionCalc:04x}", 16)
  149. class VFixed:
  150. """
  151. A specific, non-normal representation of a fixed number, used only in certain forms of version numbers.
  152. - https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-version-numbers
  153. """
  154. def __init__(self, string):
  155. versionComponents = string.split('.')
  156. # normal decimal versions
  157. self.majorVersion = int(versionComponents[0])
  158. self.minorVersion = int(versionComponents[1])
  159. def __int__(self):
  160. return int(f'{self.majorVersion:>04x}' + f"{self.minorVersion:<04d}", 16)
  161. def toHexStr(self):
  162. return "0x" + f'{self.majorVersion:>04x}' + f"{self.minorVersion:<04d}"
  163. def toDecimalStr(self):
  164. return str(self.majorVersion) + '.' + str(self.minorVersion)
  165. class LongDateTime:
  166. """
  167. Class representing the LONGDATETIME data format in fonts.
  168. LONGDATETIME is an Int64 representing the amount of seconds since 1st January 1904 at 00:00 UTC.
  169. - https://docs.microsoft.com/en-us/typography/opentype/spec/otff#data-types
  170. LONGDATETIME is done at UTC; there are no time zones.
  171. """
  172. def __init__(self, string=None):
  173. """
  174. Either takes in a formatted string representing a datetime, or nothing.
  175. If nothing is inputted, forc will just use now.
  176. forc's datetime format:
  177. (Microseconds assumed to be 0.)
  178. 2019-05-22 09:59 +0000
  179. %d-%m-%d %H:%M %z
  180. """
  181. if string and string != "":
  182. try:
  183. self.datetime = datetime.strptime(string, "%Y-%m-%d %H:%M %z")
  184. except:
  185. raise ValueError(f"Creating LongDateTime data type failed. The string given ('{string}') is formatted wrong.")
  186. else:
  187. self.datetime = datetime.now(timezone.utc)
  188. def __int__(self):
  189. """
  190. Returns an int representation of this datetime, designed for font binary compilation.
  191. (Returns a time delta in seconds from 1st January 1904 at 00:00 UTC to this datetime at UTC.)
  192. """
  193. firstDate = datetime(1904, 1, 1, 0, 0, 0, 0, timezone.utc)
  194. delta = self.datetime - firstDate
  195. return floor(delta.total_seconds()) # return a rounded-down integer of the total seconds in that delta.
  196. def toTTXStr(self):
  197. """
  198. Returns a string representation of this datetime, designed for TTX compilation.
  199. (returns a datetime string formatted in the following way:)
  200. %a %b %d %X %Y
  201. Wed May 22 13:45:00 2018
  202. (24h UTC)
  203. (Giving TTX compiler anything else will result in a TTX build error)
  204. """
  205. return self.datetime.strftime("%a %b %d %X %Y")