| """ |
| Various round-to-integer helpers. |
| """ |
| |
| import math |
| import functools |
| import logging |
| |
| log = logging.getLogger(__name__) |
| |
| __all__ = [ |
| "noRound", |
| "otRound", |
| "maybeRound", |
| "roundFunc", |
| ] |
| |
| def noRound(value): |
| return value |
| |
| def otRound(value): |
| """Round float value to nearest integer towards ``+Infinity``. |
| |
| 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>`_) |
| defines the required method for converting floating point values to |
| fixed-point. In particular it specifies the following rounding strategy: |
| |
| for fractional values of 0.5 and higher, take the next higher integer; |
| for other fractional values, truncate. |
| |
| This function rounds the floating-point value according to this strategy |
| in preparation for conversion to fixed-point. |
| |
| Args: |
| value (float): The input floating-point value. |
| |
| Returns |
| float: The rounded value. |
| """ |
| # See this thread for how we ended up with this implementation: |
| # https://github.com/fonttools/fonttools/issues/1248#issuecomment-383198166 |
| return int(math.floor(value + 0.5)) |
| |
| def maybeRound(v, tolerance, round=otRound): |
| rounded = round(v) |
| return rounded if abs(rounded - v) <= tolerance else v |
| |
| def roundFunc(tolerance, round=otRound): |
| if tolerance < 0: |
| raise ValueError("Rounding tolerance must be positive") |
| |
| if tolerance == 0: |
| return noRound |
| |
| if tolerance >= .5: |
| return round |
| |
| return functools.partial(maybeRound, tolerance=tolerance, round=round) |
| |
| |
| def nearestMultipleShortestRepr(value: float, factor: float) -> str: |
| """Round to nearest multiple of factor and return shortest decimal representation. |
| |
| This chooses the float that is closer to a multiple of the given factor while |
| having the shortest decimal representation (the least number of fractional decimal |
| digits). |
| |
| For example, given the following: |
| |
| >>> nearestMultipleShortestRepr(-0.61883544921875, 1.0/(1<<14)) |
| '-0.61884' |
| |
| Useful when you need to serialize or print a fixed-point number (or multiples |
| thereof, such as F2Dot14 fractions of 180 degrees in COLRv1 PaintRotate) in |
| a human-readable form. |
| |
| Args: |
| value (value): The value to be rounded and serialized. |
| factor (float): The value which the result is a close multiple of. |
| |
| Returns: |
| str: A compact string representation of the value. |
| """ |
| if not value: |
| return "0.0" |
| |
| value = otRound(value / factor) * factor |
| eps = .5 * factor |
| lo = value - eps |
| hi = value + eps |
| # If the range of valid choices spans an integer, return the integer. |
| if int(lo) != int(hi): |
| return str(float(round(value))) |
| |
| fmt = "%.8f" |
| lo = fmt % lo |
| hi = fmt % hi |
| assert len(lo) == len(hi) and lo != hi |
| for i in range(len(lo)): |
| if lo[i] != hi[i]: |
| break |
| period = lo.find('.') |
| assert period < i |
| fmt = "%%.%df" % (i - period) |
| return fmt % value |