1from fontTools.cffLib import PrivateDict 2from fontTools.cffLib.specializer import stringToProgram 3from fontTools.misc.testTools import getXML, parseXML 4from fontTools.misc.psCharStrings import ( 5 T2CharString, 6 encodeFloat, 7 encodeFixed, 8 read_fixed1616, 9 read_realNumber, 10) 11from fontTools.pens.recordingPen import RecordingPen 12import unittest 13 14 15def hexenc(s): 16 return " ".join("%02x" % x for x in s) 17 18 19class T2CharStringTest(unittest.TestCase): 20 @classmethod 21 def stringToT2CharString(cls, string): 22 return T2CharString(program=stringToProgram(string), private=PrivateDict()) 23 24 def test_calcBounds_empty(self): 25 cs = self.stringToT2CharString("endchar") 26 bounds = cs.calcBounds(None) 27 self.assertEqual(bounds, None) 28 29 def test_calcBounds_line(self): 30 cs = self.stringToT2CharString( 31 "100 100 rmoveto 40 10 rlineto -20 50 rlineto endchar" 32 ) 33 bounds = cs.calcBounds(None) 34 self.assertEqual(bounds, (100, 100, 140, 160)) 35 36 def test_calcBounds_curve(self): 37 cs = self.stringToT2CharString( 38 "100 100 rmoveto -50 -150 200 0 -50 150 rrcurveto endchar" 39 ) 40 bounds = cs.calcBounds(None) 41 self.assertEqual(bounds, (91.90524980688875, -12.5, 208.09475019311125, 100)) 42 43 def test_charstring_bytecode_optimization(self): 44 cs = self.stringToT2CharString( 45 "100.0 100 rmoveto -50.0 -150 200.5 0.0 -50 150 rrcurveto endchar" 46 ) 47 cs.isCFF2 = False 48 cs.private._isCFF2 = False 49 cs.compile() 50 cs.decompile() 51 self.assertEqual( 52 cs.program, 53 [ 54 100, 55 100, 56 "rmoveto", 57 -50, 58 -150, 59 200.5, 60 0, 61 -50, 62 150, 63 "rrcurveto", 64 "endchar", 65 ], 66 ) 67 68 cs2 = self.stringToT2CharString( 69 "100.0 rmoveto -50.0 -150 200.5 0.0 -50 150 rrcurveto" 70 ) 71 cs2.isCFF2 = True 72 cs2.private._isCFF2 = True 73 cs2.compile(isCFF2=True) 74 cs2.decompile() 75 self.assertEqual( 76 cs2.program, [100, "rmoveto", -50, -150, 200.5, 0, -50, 150, "rrcurveto"] 77 ) 78 79 def test_encodeFloat(self): 80 testNums = [ 81 # value expected result 82 (-9.399999999999999, "1e e9 a4 ff"), # -9.4 83 (9.399999999999999999, "1e 9a 4f"), # 9.4 84 (456.8, "1e 45 6a 8f"), # 456.8 85 (0.0, "1e 0f"), # 0 86 (-0.0, "1e 0f"), # 0 87 (1.0, "1e 1f"), # 1 88 (-1.0, "1e e1 ff"), # -1 89 (98765.37e2, "1e 98 76 53 7f"), # 9876537 90 (1234567890.0, "1e 1a 23 45 67 9b 09 ff"), # 1234567890 91 (9.876537e-4, "1e a0 00 98 76 53 7f"), # 9.876537e-24 92 (9.876537e4, "1e 98 76 5a 37 ff"), # 9.876537e+24 93 ] 94 95 for sample in testNums: 96 encoded_result = encodeFloat(sample[0]) 97 98 # check to see if we got the expected bytes 99 self.assertEqual(hexenc(encoded_result), sample[1]) 100 101 # check to see if we get the same value by decoding the data 102 decoded_result = read_realNumber( 103 None, 104 None, 105 encoded_result, 106 1, 107 ) 108 self.assertEqual(decoded_result[0], float("%.8g" % sample[0])) 109 # We limit to 8 digits of precision to match the implementation 110 # of encodeFloat. 111 112 def test_encode_decode_fixed(self): 113 testNums = [ 114 # value expected hex expected float 115 (-9.399999999999999, "ff ff f6 99 9a", -9.3999939), 116 (-9.4, "ff ff f6 99 9a", -9.3999939), 117 (9.399999999999999999, "ff 00 09 66 66", 9.3999939), 118 (9.4, "ff 00 09 66 66", 9.3999939), 119 (456.8, "ff 01 c8 cc cd", 456.8000031), 120 (-456.8, "ff fe 37 33 33", -456.8000031), 121 ] 122 123 for value, expected_hex, expected_float in testNums: 124 encoded_result = encodeFixed(value) 125 126 # check to see if we got the expected bytes 127 self.assertEqual(hexenc(encoded_result), expected_hex) 128 129 # check to see if we get the same value by decoding the data 130 decoded_result = read_fixed1616( 131 None, 132 None, 133 encoded_result, 134 1, 135 ) 136 self.assertAlmostEqual(decoded_result[0], expected_float) 137 138 def test_toXML(self): 139 program = [ 140 "107 53.4004 166.199 hstem", 141 "174.6 163.801 vstem", 142 "338.4 142.8 rmoveto", 143 "28 0 21.9 9 15.8 18 15.8 18 7.9 20.79959 0 23.6 rrcurveto", 144 "endchar", 145 ] 146 cs = self.stringToT2CharString(" ".join(program)) 147 148 self.assertEqual(getXML(cs.toXML), program) 149 150 def test_fromXML(self): 151 cs = T2CharString() 152 for name, attrs, content in parseXML( 153 [ 154 '<CharString name="period">' " 338.4 142.8 rmoveto", 155 " 28 0 21.9 9 15.8 18 15.8 18 7.9 20.79959 0 23.6 rrcurveto", 156 " endchar" "</CharString>", 157 ] 158 ): 159 cs.fromXML(name, attrs, content) 160 161 expected_program = [ 162 338.3999939, 163 142.8000031, 164 "rmoveto", 165 28, 166 0, 167 21.8999939, 168 9, 169 15.8000031, 170 18, 171 15.8000031, 172 18, 173 7.8999939, 174 20.7995911, 175 0, 176 23.6000061, 177 "rrcurveto", 178 "endchar", 179 ] 180 181 self.assertEqual(len(cs.program), len(expected_program)) 182 for arg, expected_arg in zip(cs.program, expected_program): 183 if isinstance(arg, str): 184 self.assertIsInstance(expected_arg, str) 185 self.assertEqual(arg, expected_arg) 186 else: 187 self.assertNotIsInstance(expected_arg, str) 188 self.assertAlmostEqual(arg, expected_arg) 189 190 def test_pen_closePath(self): 191 # Test CFF2/T2 charstring: it does NOT end in "endchar" 192 # https://github.com/fonttools/fonttools/issues/2455 193 cs = self.stringToT2CharString( 194 "100 100 rmoveto -50 -150 200 0 -50 150 rrcurveto" 195 ) 196 pen = RecordingPen() 197 cs.draw(pen) 198 self.assertEqual(pen.value[-1], ("closePath", ())) 199 200 201if __name__ == "__main__": 202 import sys 203 204 sys.exit(unittest.main()) 205