roundTools.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. """
  2. Various round-to-integer helpers.
  3. """
  4. import math
  5. import functools
  6. import logging
  7. log = logging.getLogger(__name__)
  8. __all__ = [
  9. "noRound",
  10. "otRound",
  11. "maybeRound",
  12. "roundFunc",
  13. "nearestMultipleShortestRepr",
  14. ]
  15. def noRound(value):
  16. return value
  17. def otRound(value):
  18. """Round float value to nearest integer towards ``+Infinity``.
  19. The OpenType spec (in the section on `"normalization" of OpenType Font Variations <https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview#coordinate-scales-and-normalization>`_)
  20. defines the required method for converting floating point values to
  21. fixed-point. In particular it specifies the following rounding strategy:
  22. for fractional values of 0.5 and higher, take the next higher integer;
  23. for other fractional values, truncate.
  24. This function rounds the floating-point value according to this strategy
  25. in preparation for conversion to fixed-point.
  26. Args:
  27. value (float): The input floating-point value.
  28. Returns
  29. float: The rounded value.
  30. """
  31. # See this thread for how we ended up with this implementation:
  32. # https://github.com/fonttools/fonttools/issues/1248#issuecomment-383198166
  33. return int(math.floor(value + 0.5))
  34. def maybeRound(v, tolerance, round=otRound):
  35. rounded = round(v)
  36. return rounded if abs(rounded - v) <= tolerance else v
  37. def roundFunc(tolerance, round=otRound):
  38. if tolerance < 0:
  39. raise ValueError("Rounding tolerance must be positive")
  40. if tolerance == 0:
  41. return noRound
  42. if tolerance >= 0.5:
  43. return round
  44. return functools.partial(maybeRound, tolerance=tolerance, round=round)
  45. def nearestMultipleShortestRepr(value: float, factor: float) -> str:
  46. """Round to nearest multiple of factor and return shortest decimal representation.
  47. This chooses the float that is closer to a multiple of the given factor while
  48. having the shortest decimal representation (the least number of fractional decimal
  49. digits).
  50. For example, given the following:
  51. >>> nearestMultipleShortestRepr(-0.61883544921875, 1.0/(1<<14))
  52. '-0.61884'
  53. Useful when you need to serialize or print a fixed-point number (or multiples
  54. thereof, such as F2Dot14 fractions of 180 degrees in COLRv1 PaintRotate) in
  55. a human-readable form.
  56. Args:
  57. value (value): The value to be rounded and serialized.
  58. factor (float): The value which the result is a close multiple of.
  59. Returns:
  60. str: A compact string representation of the value.
  61. """
  62. if not value:
  63. return "0.0"
  64. value = otRound(value / factor) * factor
  65. eps = 0.5 * factor
  66. lo = value - eps
  67. hi = value + eps
  68. # If the range of valid choices spans an integer, return the integer.
  69. if int(lo) != int(hi):
  70. return str(float(round(value)))
  71. fmt = "%.8f"
  72. lo = fmt % lo
  73. hi = fmt % hi
  74. assert len(lo) == len(hi) and lo != hi
  75. for i in range(len(lo)):
  76. if lo[i] != hi[i]:
  77. break
  78. period = lo.find(".")
  79. assert period < i
  80. fmt = "%%.%df" % (i - period)
  81. return fmt % value