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