fixedTools.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. """
  2. The `OpenType specification <https://docs.microsoft.com/en-us/typography/opentype/spec/otff#data-types>`_
  3. defines two fixed-point data types:
  4. ``Fixed``
  5. A 32-bit signed fixed-point number with a 16 bit twos-complement
  6. magnitude component and 16 fractional bits.
  7. ``F2DOT14``
  8. A 16-bit signed fixed-point number with a 2 bit twos-complement
  9. magnitude component and 14 fractional bits.
  10. To support reading and writing data with these data types, this module provides
  11. functions for converting between fixed-point, float and string representations.
  12. .. data:: MAX_F2DOT14
  13. The maximum value that can still fit in an F2Dot14. (1.99993896484375)
  14. """
  15. from .roundTools import otRound, nearestMultipleShortestRepr
  16. import logging
  17. log = logging.getLogger(__name__)
  18. __all__ = [
  19. "MAX_F2DOT14",
  20. "fixedToFloat",
  21. "floatToFixed",
  22. "floatToFixedToFloat",
  23. "floatToFixedToStr",
  24. "fixedToStr",
  25. "strToFixed",
  26. "strToFixedToFloat",
  27. "ensureVersionIsLong",
  28. "versionToFixed",
  29. ]
  30. MAX_F2DOT14 = 0x7FFF / (1 << 14)
  31. def fixedToFloat(value, precisionBits):
  32. """Converts a fixed-point number to a float given the number of
  33. precision bits.
  34. Args:
  35. value (int): Number in fixed-point format.
  36. precisionBits (int): Number of precision bits.
  37. Returns:
  38. Floating point value.
  39. Examples::
  40. >>> import math
  41. >>> f = fixedToFloat(-10139, precisionBits=14)
  42. >>> math.isclose(f, -0.61883544921875)
  43. True
  44. """
  45. return value / (1 << precisionBits)
  46. def floatToFixed(value, precisionBits):
  47. """Converts a float to a fixed-point number given the number of
  48. precision bits.
  49. Args:
  50. value (float): Floating point value.
  51. precisionBits (int): Number of precision bits.
  52. Returns:
  53. int: Fixed-point representation.
  54. Examples::
  55. >>> floatToFixed(-0.61883544921875, precisionBits=14)
  56. -10139
  57. >>> floatToFixed(-0.61884, precisionBits=14)
  58. -10139
  59. """
  60. return otRound(value * (1 << precisionBits))
  61. def floatToFixedToFloat(value, precisionBits):
  62. """Converts a float to a fixed-point number and back again.
  63. By converting the float to fixed, rounding it, and converting it back
  64. to float again, this returns a floating point values which is exactly
  65. representable in fixed-point format.
  66. Note: this **is** equivalent to ``fixedToFloat(floatToFixed(value))``.
  67. Args:
  68. value (float): The input floating point value.
  69. precisionBits (int): Number of precision bits.
  70. Returns:
  71. float: The transformed and rounded value.
  72. Examples::
  73. >>> import math
  74. >>> f1 = -0.61884
  75. >>> f2 = floatToFixedToFloat(-0.61884, precisionBits=14)
  76. >>> f1 != f2
  77. True
  78. >>> math.isclose(f2, -0.61883544921875)
  79. True
  80. """
  81. scale = 1 << precisionBits
  82. return otRound(value * scale) / scale
  83. def fixedToStr(value, precisionBits):
  84. """Converts a fixed-point number to a string representing a decimal float.
  85. This chooses the float that has the shortest decimal representation (the least
  86. number of fractional decimal digits).
  87. For example, to convert a fixed-point number in a 2.14 format, use
  88. ``precisionBits=14``::
  89. >>> fixedToStr(-10139, precisionBits=14)
  90. '-0.61884'
  91. This is pretty slow compared to the simple division used in ``fixedToFloat``.
  92. Use sporadically when you need to serialize or print the fixed-point number in
  93. a human-readable form.
  94. It uses nearestMultipleShortestRepr under the hood.
  95. Args:
  96. value (int): The fixed-point value to convert.
  97. precisionBits (int): Number of precision bits, *up to a maximum of 16*.
  98. Returns:
  99. str: A string representation of the value.
  100. """
  101. scale = 1 << precisionBits
  102. return nearestMultipleShortestRepr(value / scale, factor=1.0 / scale)
  103. def strToFixed(string, precisionBits):
  104. """Converts a string representing a decimal float to a fixed-point number.
  105. Args:
  106. string (str): A string representing a decimal float.
  107. precisionBits (int): Number of precision bits, *up to a maximum of 16*.
  108. Returns:
  109. int: Fixed-point representation.
  110. Examples::
  111. >>> ## to convert a float string to a 2.14 fixed-point number:
  112. >>> strToFixed('-0.61884', precisionBits=14)
  113. -10139
  114. """
  115. value = float(string)
  116. return otRound(value * (1 << precisionBits))
  117. def strToFixedToFloat(string, precisionBits):
  118. """Convert a string to a decimal float with fixed-point rounding.
  119. This first converts string to a float, then turns it into a fixed-point
  120. number with ``precisionBits`` fractional binary digits, then back to a
  121. float again.
  122. This is simply a shorthand for fixedToFloat(floatToFixed(float(s))).
  123. Args:
  124. string (str): A string representing a decimal float.
  125. precisionBits (int): Number of precision bits.
  126. Returns:
  127. float: The transformed and rounded value.
  128. Examples::
  129. >>> import math
  130. >>> s = '-0.61884'
  131. >>> bits = 14
  132. >>> f = strToFixedToFloat(s, precisionBits=bits)
  133. >>> math.isclose(f, -0.61883544921875)
  134. True
  135. >>> f == fixedToFloat(floatToFixed(float(s), precisionBits=bits), precisionBits=bits)
  136. True
  137. """
  138. value = float(string)
  139. scale = 1 << precisionBits
  140. return otRound(value * scale) / scale
  141. def floatToFixedToStr(value, precisionBits):
  142. """Convert float to string with fixed-point rounding.
  143. This uses the shortest decimal representation (ie. the least
  144. number of fractional decimal digits) to represent the equivalent
  145. fixed-point number with ``precisionBits`` fractional binary digits.
  146. It uses nearestMultipleShortestRepr under the hood.
  147. >>> floatToFixedToStr(-0.61883544921875, precisionBits=14)
  148. '-0.61884'
  149. Args:
  150. value (float): The float value to convert.
  151. precisionBits (int): Number of precision bits, *up to a maximum of 16*.
  152. Returns:
  153. str: A string representation of the value.
  154. """
  155. scale = 1 << precisionBits
  156. return nearestMultipleShortestRepr(value, factor=1.0 / scale)
  157. def ensureVersionIsLong(value):
  158. """Ensure a table version is an unsigned long.
  159. OpenType table version numbers are expressed as a single unsigned long
  160. comprising of an unsigned short major version and unsigned short minor
  161. version. This function detects if the value to be used as a version number
  162. looks too small (i.e. is less than ``0x10000``), and converts it to
  163. fixed-point using :func:`floatToFixed` if so.
  164. Args:
  165. value (Number): a candidate table version number.
  166. Returns:
  167. int: A table version number, possibly corrected to fixed-point.
  168. """
  169. if value < 0x10000:
  170. newValue = floatToFixed(value, 16)
  171. log.warning(
  172. "Table version value is a float: %.4f; " "fix to use hex instead: 0x%08x",
  173. value,
  174. newValue,
  175. )
  176. value = newValue
  177. return value
  178. def versionToFixed(value):
  179. """Ensure a table version number is fixed-point.
  180. Args:
  181. value (str): a candidate table version number.
  182. Returns:
  183. int: A table version number, possibly corrected to fixed-point.
  184. """
  185. value = int(value, 0) if value.startswith("0") else float(value)
  186. value = ensureVersionIsLong(value)
  187. return value