• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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