diff --git a/Lib/fontTools/misc/fixedTools.py b/Lib/fontTools/misc/fixedTools.py index 1c3fe337d..e0ed4436c 100644 --- a/Lib/fontTools/misc/fixedTools.py +++ b/Lib/fontTools/misc/fixedTools.py @@ -11,7 +11,11 @@ __all__ = [ "otRound", "fixedToFloat", "floatToFixed", - "floatToFixedToFloat", + "floatToFixedToFloat", + "floatToFixedToStr", + "fixedToStr", + "strToFixed", + "strToFixedToFloat", "ensureVersionIsLong", "versionToFixed", ] @@ -29,14 +33,63 @@ def otRound(value): def fixedToFloat(value, precisionBits): - """Converts a fixed-point number to a float, choosing the float - that has the shortest decimal reprentation. Eg. to convert a - fixed number in a 2.14 format, use precisionBits=14. This is - pretty slow compared to a simple division. Use sporadically. + """Converts a fixed-point number to a float given the number of + precisionBits. Ie. value / (1 << precisionBits) - precisionBits is only supported up to 16. + >>> import math + >>> f = fixedToFloat(-10139, precisionBits=14) + >>> math.isclose(f, -0.61883544921875) + True """ - if not value: return 0.0 + return value / (1 << precisionBits) + + +def floatToFixed(value, precisionBits): + """Converts a float to a fixed-point number given the number of + precisionBits. Ie. round(value * (1 << precisionBits)). + + >>> floatToFixed(-0.61883544921875, precisionBits=14) + -10139 + >>> floatToFixed(-0.61884, precisionBits=14) + -10139 + """ + return otRound(value * (1 << precisionBits)) + + +def floatToFixedToFloat(value, precisionBits): + """Converts a float to a fixed-point number given the number of + precisionBits, round it, then convert it back to float again. + Ie. round(value * (1<>> import math + >>> f1 = -0.61884 + >>> f2 = floatToFixedToFloat(-0.61884, precisionBits=14) + >>> f1 != f2 + True + >>> math.isclose(f2, -0.61883544921875) + True + """ + scale = 1 << precisionBits + return otRound(value * scale) / scale + + +def fixedToStr(value, precisionBits): + """Converts a fixed-point number with 'precisionBits' number of fractional binary + digits to a string representing a decimal float, choosing the float that has the + shortest decimal representation (the least number of fractional decimal digits). + Eg. to convert a fixed-point number in a 2.14 format, use precisionBits=14: + + >>> fixedToStr(-10139, precisionBits=14) + '-0.61884' + + This is pretty slow compared to the simple division used in fixedToFloat. + Use sporadically when you need to serialize or print the fixed-point number in + a human-readable form. + + NOTE: precisionBits is only supported up to 16. + """ + if not value: return "0.0" scale = 1 << precisionBits value /= scale @@ -45,7 +98,7 @@ def fixedToFloat(value, precisionBits): hi = value + eps # If the range of valid choices spans an integer, return the integer. if int(lo) != int(hi): - return float(round(value)) + return str(float(round(value))) fmt = "%.8f" lo = fmt % lo hi = fmt % hi @@ -56,25 +109,53 @@ def fixedToFloat(value, precisionBits): period = lo.find('.') assert period < i fmt = "%%.%df" % (i - period) - value = fmt % value - return float(value) + return fmt % value -def floatToFixed(value, precisionBits): - """Converts a float to a fixed-point number given the number of - precisionBits. Ie. round(value * (1<>> strToFixed('-0.61884', precisionBits=14) + -10139 """ - scale = 1<>> import math + >>> s = '-0.61884' + >>> bits = 14 + >>> f = strToFixedToFloat(s, precisionBits=bits) + >>> math.isclose(f, -0.61883544921875) + True + >>> f == fixedToFloat(floatToFixed(float(s), precisionBits=bits), precisionBits=bits) + True + """ + value = float(string) + scale = 1 << precisionBits return otRound(value * scale) / scale + +def floatToFixedToStr(value, precisionBits): + """Convert float to string using the shortest decimal representation (ie. the least + number of fractional decimal digits) to represent the equivalent fixed-point number + with 'precisionBits' fractional binary digits. + It uses fixedToStr under the hood. + + >>> floatToFixedToStr(-0.61883544921875, precisionBits=14) + '-0.61884' + """ + fixed = otRound(value * (1 << precisionBits)) + return fixedToStr(fixed, precisionBits) + + def ensureVersionIsLong(value): """Ensure a table version is an unsigned long (unsigned short major, unsigned short minor) instead of a float.""" diff --git a/Tests/misc/fixedTools_test.py b/Tests/misc/fixedTools_test.py index d7b0a93c6..fe836ea63 100644 --- a/Tests/misc/fixedTools_test.py +++ b/Tests/misc/fixedTools_test.py @@ -1,5 +1,12 @@ from fontTools.misc.py23 import * -from fontTools.misc.fixedTools import fixedToFloat, floatToFixed +from fontTools.misc.fixedTools import ( + fixedToFloat, + floatToFixed, + floatToFixedToStr, + fixedToStr, + strToFixed, + strToFixedToFloat, +) import unittest @@ -11,19 +18,33 @@ class FixedToolsTest(unittest.TestCase): self.assertEqual(value, floatToFixed(fixedToFloat(value, bits), bits)) def test_fixedToFloat_precision14(self): - self.assertEqual(0.8, fixedToFloat(13107, 14)) + self.assertAlmostEqual(0.7999878, fixedToFloat(13107, 14)) self.assertEqual(0.0, fixedToFloat(0, 14)) self.assertEqual(1.0, fixedToFloat(16384, 14)) self.assertEqual(-1.0, fixedToFloat(-16384, 14)) - self.assertEqual(0.99994, fixedToFloat(16383, 14)) - self.assertEqual(-0.99994, fixedToFloat(-16383, 14)) + self.assertAlmostEqual(0.999939, fixedToFloat(16383, 14)) + self.assertAlmostEqual(-0.999939, fixedToFloat(-16383, 14)) def test_fixedToFloat_precision6(self): - self.assertAlmostEqual(-9.98, fixedToFloat(-639, 6)) + self.assertAlmostEqual(-9.984375, fixedToFloat(-639, 6)) self.assertAlmostEqual(-10.0, fixedToFloat(-640, 6)) - self.assertAlmostEqual(9.98, fixedToFloat(639, 6)) + self.assertAlmostEqual(9.984375, fixedToFloat(639, 6)) self.assertAlmostEqual(10.0, fixedToFloat(640, 6)) + def test_fixedToStr_precision14(self): + self.assertEqual('0.8', fixedToStr(13107, 14)) + self.assertEqual('0.0', fixedToStr(0, 14)) + self.assertEqual('1.0', fixedToStr(16384, 14)) + self.assertEqual('-1.0', fixedToStr(-16384, 14)) + self.assertEqual('0.99994', fixedToStr(16383, 14)) + self.assertEqual('-0.99994', fixedToStr(-16383, 14)) + + def test_fixedToStr_precision6(self): + self.assertAlmostEqual('-9.98', fixedToStr(-639, 6)) + self.assertAlmostEqual('-10.0', fixedToStr(-640, 6)) + self.assertAlmostEqual('9.98', fixedToStr(639, 6)) + self.assertAlmostEqual('10.0', fixedToStr(640, 6)) + def test_floatToFixed_precision14(self): self.assertEqual(13107, floatToFixed(0.8, 14)) self.assertEqual(16384, floatToFixed(1.0, 14)) @@ -32,6 +53,30 @@ class FixedToolsTest(unittest.TestCase): self.assertEqual(-16384, floatToFixed(-1, 14)) self.assertEqual(0, floatToFixed(0, 14)) + def test_strToFixed_precision14(self): + self.assertEqual(13107, strToFixed('0.8', 14)) + self.assertEqual(16384, strToFixed('1.0', 14)) + self.assertEqual(16384, strToFixed('1', 14)) + self.assertEqual(-16384, strToFixed('-1.0', 14)) + self.assertEqual(-16384, strToFixed('-1', 14)) + self.assertEqual(0, strToFixed('0', 14)) + + def test_strToFixedToFloat_precision14(self): + self.assertAlmostEqual(0.7999878, strToFixedToFloat('0.8', 14)) + self.assertEqual(0.0, strToFixedToFloat('0', 14)) + self.assertEqual(1.0, strToFixedToFloat('1.0', 14)) + self.assertEqual(-1.0, strToFixedToFloat('-1.0', 14)) + self.assertAlmostEqual(0.999939, strToFixedToFloat('0.99994', 14)) + self.assertAlmostEqual(-0.999939, strToFixedToFloat('-0.99994', 14)) + + def test_floatToFixedToStr_precision14(self): + self.assertEqual('0.8', floatToFixedToStr(0.7999878, 14)) + self.assertEqual('1.0', floatToFixedToStr(1.0, 14)) + self.assertEqual('1.0', floatToFixedToStr(1, 14)) + self.assertEqual('-1.0', floatToFixedToStr(-1.0, 14)) + self.assertEqual('-1.0', floatToFixedToStr(-1, 14)) + self.assertEqual('0.0', floatToFixedToStr(0, 14)) + def test_fixedToFloat_return_float(self): value = fixedToFloat(16384, 14) self.assertIsInstance(value, float)