123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- """
- Various round-to-integer helpers.
- """
- import math
- import functools
- import logging
- log = logging.getLogger(__name__)
- __all__ = [
- "noRound",
- "otRound",
- "maybeRound",
- "roundFunc",
- "nearestMultipleShortestRepr",
- ]
- 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 >= 0.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 = 0.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
|