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