aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/fonttools/fontTools/misc/roundTools.py
blob: a4d45c31b2265cc5f705c39f41e952cc69514517 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""
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