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