You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			111 lines
		
	
	
		
			3.1 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			111 lines
		
	
	
		
			3.1 KiB
		
	
	
	
		
			Python
		
	
"""
 | 
						|
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
 |