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