• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import logging
2import os
3import tempfile
4import shutil
5import unittest
6from io import open
7from .testSupport import getDemoFontGlyphSetPath
8from fontTools.ufoLib.glifLib import (
9	GlyphSet, glyphNameToFileName, readGlyphFromString, writeGlyphToString,
10)
11from fontTools.ufoLib.errors import GlifLibError, UnsupportedGLIFFormat, UnsupportedUFOFormat
12from fontTools.misc.etree import XML_DECLARATION
13from fontTools.pens.recordingPen import RecordingPointPen
14import pytest
15
16GLYPHSETDIR = getDemoFontGlyphSetPath()
17
18
19class GlyphSetTests(unittest.TestCase):
20
21	def setUp(self):
22		self.dstDir = tempfile.mktemp()
23		os.mkdir(self.dstDir)
24
25	def tearDown(self):
26		shutil.rmtree(self.dstDir)
27
28	def testRoundTrip(self):
29		import difflib
30		srcDir = GLYPHSETDIR
31		dstDir = self.dstDir
32		src = GlyphSet(srcDir, ufoFormatVersion=2, validateRead=True, validateWrite=True)
33		dst = GlyphSet(dstDir, ufoFormatVersion=2, validateRead=True, validateWrite=True)
34		for glyphName in src.keys():
35			g = src[glyphName]
36			g.drawPoints(None)  # load attrs
37			dst.writeGlyph(glyphName, g, g.drawPoints)
38		# compare raw file data:
39		for glyphName in sorted(src.keys()):
40			fileName = src.contents[glyphName]
41			with open(os.path.join(srcDir, fileName), "r") as f:
42				org = f.read()
43			with open(os.path.join(dstDir, fileName), "r") as f:
44				new = f.read()
45			added = []
46			removed = []
47			for line in difflib.unified_diff(
48					org.split("\n"), new.split("\n")):
49				if line.startswith("+ "):
50					added.append(line[1:])
51				elif line.startswith("- "):
52					removed.append(line[1:])
53			self.assertEqual(
54				added, removed,
55				"%s.glif file differs after round tripping" % glyphName)
56
57	def testContentsExist(self):
58		with self.assertRaises(GlifLibError):
59			GlyphSet(
60				self.dstDir,
61				ufoFormatVersion=2,
62				validateRead=True,
63				validateWrite=True,
64				expectContentsFile=True,
65			)
66
67	def testRebuildContents(self):
68		gset = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
69		contents = gset.contents
70		gset.rebuildContents()
71		self.assertEqual(contents, gset.contents)
72
73	def testReverseContents(self):
74		gset = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
75		d = {}
76		for k, v in gset.getReverseContents().items():
77			d[v] = k
78		org = {}
79		for k, v in gset.contents.items():
80			org[k] = v.lower()
81		self.assertEqual(d, org)
82
83	def testReverseContents2(self):
84		src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
85		dst = GlyphSet(self.dstDir, validateRead=True, validateWrite=True)
86		dstMap = dst.getReverseContents()
87		self.assertEqual(dstMap, {})
88		for glyphName in src.keys():
89			g = src[glyphName]
90			g.drawPoints(None)  # load attrs
91			dst.writeGlyph(glyphName, g, g.drawPoints)
92		self.assertNotEqual(dstMap, {})
93		srcMap = dict(src.getReverseContents())  # copy
94		self.assertEqual(dstMap, srcMap)
95		del srcMap["a.glif"]
96		dst.deleteGlyph("a")
97		self.assertEqual(dstMap, srcMap)
98
99	def testCustomFileNamingScheme(self):
100		def myGlyphNameToFileName(glyphName, glyphSet):
101			return "prefix" + glyphNameToFileName(glyphName, glyphSet)
102		src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
103		dst = GlyphSet(self.dstDir, myGlyphNameToFileName, validateRead=True, validateWrite=True)
104		for glyphName in src.keys():
105			g = src[glyphName]
106			g.drawPoints(None)  # load attrs
107			dst.writeGlyph(glyphName, g, g.drawPoints)
108		d = {}
109		for k, v in src.contents.items():
110			d[k] = "prefix" + v
111		self.assertEqual(d, dst.contents)
112
113	def testGetUnicodes(self):
114		src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
115		unicodes = src.getUnicodes()
116		for glyphName in src.keys():
117			g = src[glyphName]
118			g.drawPoints(None)  # load attrs
119			if not hasattr(g, "unicodes"):
120				self.assertEqual(unicodes[glyphName], [])
121			else:
122				self.assertEqual(g.unicodes, unicodes[glyphName])
123
124
125class FileNameTest:
126
127	def test_default_file_name_scheme(self):
128		assert glyphNameToFileName("a", None) == "a.glif"
129		assert glyphNameToFileName("A", None) == "A_.glif"
130		assert glyphNameToFileName("Aring", None) == "A_ring.glif"
131		assert glyphNameToFileName("F_A_B", None) == "F__A__B_.glif"
132		assert glyphNameToFileName("A.alt", None) == "A_.alt.glif"
133		assert glyphNameToFileName("A.Alt", None) == "A_.A_lt.glif"
134		assert glyphNameToFileName(".notdef", None) == "_notdef.glif"
135		assert glyphNameToFileName("T_H", None) =="T__H_.glif"
136		assert glyphNameToFileName("T_h", None) =="T__h.glif"
137		assert glyphNameToFileName("t_h", None) =="t_h.glif"
138		assert glyphNameToFileName("F_F_I", None) == "F__F__I_.glif"
139		assert glyphNameToFileName("f_f_i", None) == "f_f_i.glif"
140		assert glyphNameToFileName("AE", None) == "A_E_.glif"
141		assert glyphNameToFileName("Ae", None) == "A_e.glif"
142		assert glyphNameToFileName("ae", None) == "ae.glif"
143		assert glyphNameToFileName("aE", None) == "aE_.glif"
144		assert glyphNameToFileName("a.alt", None) == "a.alt.glif"
145		assert glyphNameToFileName("A.aLt", None) == "A_.aL_t.glif"
146		assert glyphNameToFileName("A.alT", None) == "A_.alT_.glif"
147		assert glyphNameToFileName("Aacute_V.swash", None) == "A_acute_V_.swash.glif"
148		assert glyphNameToFileName(".notdef", None) == "_notdef.glif"
149		assert glyphNameToFileName("con", None) == "_con.glif"
150		assert glyphNameToFileName("CON", None) == "C_O_N_.glif"
151		assert glyphNameToFileName("con.alt", None) == "_con.alt.glif"
152		assert glyphNameToFileName("alt.con", None) == "alt._con.glif"
153
154	def test_conflicting_case_insensitive_file_names(self, tmp_path):
155		src = GlyphSet(GLYPHSETDIR)
156		dst = GlyphSet(tmp_path)
157		glyph = src["a"]
158
159		dst.writeGlyph("a", glyph)
160		dst.writeGlyph("A", glyph)
161		dst.writeGlyph("a_", glyph)
162		dst.deleteGlyph("a_")
163		dst.writeGlyph("a_", glyph)
164		dst.writeGlyph("A_", glyph)
165		dst.writeGlyph("i_j", glyph)
166
167		assert dst.contents == {
168			'a': 'a.glif',
169			'A': 'A_.glif',
170			'a_': 'a_000000000000001.glif',
171			'A_': 'A__.glif',
172			'i_j': 'i_j.glif',
173		}
174
175		# make sure filenames are unique even on case-insensitive filesystems
176		assert len({fileName.lower() for fileName in dst.contents.values()}) == 5
177
178
179class _Glyph:
180	pass
181
182
183class ReadWriteFuncTest:
184
185	def test_roundtrip(self):
186		glyph = _Glyph()
187		glyph.name = "a"
188		glyph.unicodes = [0x0061]
189
190		s1 = writeGlyphToString(glyph.name, glyph)
191
192		glyph2 = _Glyph()
193		readGlyphFromString(s1, glyph2)
194		assert glyph.__dict__ == glyph2.__dict__
195
196		s2 = writeGlyphToString(glyph2.name, glyph2)
197		assert s1 == s2
198
199	def test_xml_declaration(self):
200		s = writeGlyphToString("a", _Glyph())
201		assert s.startswith(XML_DECLARATION % "UTF-8")
202
203	def test_parse_xml_remove_comments(self):
204		s = b"""<?xml version='1.0' encoding='UTF-8'?>
205		<!-- a comment -->
206		<glyph name="A" format="2">
207			<advance width="1290"/>
208			<unicode hex="0041"/>
209			<!-- another comment -->
210		</glyph>
211		"""
212
213		g = _Glyph()
214		readGlyphFromString(s, g)
215
216		assert g.name == "A"
217		assert g.width == 1290
218		assert g.unicodes == [0x0041]
219
220	def test_read_unsupported_format_version(self, caplog):
221		s = """<?xml version='1.0' encoding='utf-8'?>
222		<glyph name="A" format="0" formatMinor="0">
223			<advance width="500"/>
224			<unicode hex="0041"/>
225		</glyph>
226		"""
227
228		with pytest.raises(UnsupportedGLIFFormat):
229			readGlyphFromString(s, _Glyph())  # validate=True by default
230
231		with pytest.raises(UnsupportedGLIFFormat):
232			readGlyphFromString(s, _Glyph(), validate=True)
233
234		caplog.clear()
235		with caplog.at_level(logging.WARNING, logger="fontTools.ufoLib.glifLib"):
236			readGlyphFromString(s, _Glyph(), validate=False)
237
238		assert len(caplog.records) == 1
239		assert "Unsupported GLIF format" in caplog.text
240		assert "Assuming the latest supported version" in caplog.text
241
242	def test_read_allow_format_versions(self):
243		s = """<?xml version='1.0' encoding='utf-8'?>
244		<glyph name="A" format="2">
245			<advance width="500"/>
246			<unicode hex="0041"/>
247		</glyph>
248		"""
249
250		# these two calls are are equivalent
251		readGlyphFromString(s, _Glyph(), formatVersions=[1, 2])
252		readGlyphFromString(s, _Glyph(), formatVersions=[(1, 0), (2, 0)])
253
254		# if at least one supported formatVersion, unsupported ones are ignored
255		readGlyphFromString(s, _Glyph(), formatVersions=[(2, 0), (123, 456)])
256
257		with pytest.raises(
258			ValueError,
259			match="None of the requested GLIF formatVersions are supported"
260		):
261			readGlyphFromString(s, _Glyph(), formatVersions=[0, 2001])
262
263		with pytest.raises(GlifLibError, match="Forbidden GLIF format version"):
264			readGlyphFromString(s, _Glyph(), formatVersions=[1])
265
266	def test_read_ensure_x_y(self):
267		"""Ensure that a proper GlifLibError is raised when point coordinates are
268		missing, regardless of validation setting."""
269
270		s = """<?xml version='1.0' encoding='utf-8'?>
271		<glyph name="A" format="2">
272			<outline>
273				<contour>
274					<point x="545" y="0" type="line"/>
275					<point x="638" type="line"/>
276				</contour>
277			</outline>
278		</glyph>
279		"""
280		pen = RecordingPointPen()
281
282		with pytest.raises(GlifLibError, match="Required y attribute"):
283			readGlyphFromString(s, _Glyph(), pen)
284
285		with pytest.raises(GlifLibError, match="Required y attribute"):
286			readGlyphFromString(s, _Glyph(), pen, validate=False)
287
288def test_GlyphSet_unsupported_ufoFormatVersion(tmp_path, caplog):
289	with pytest.raises(UnsupportedUFOFormat):
290		GlyphSet(tmp_path, ufoFormatVersion=0)
291	with pytest.raises(UnsupportedUFOFormat):
292		GlyphSet(tmp_path, ufoFormatVersion=(0, 1))
293
294
295def test_GlyphSet_writeGlyph_formatVersion(tmp_path):
296	src = GlyphSet(GLYPHSETDIR)
297	dst = GlyphSet(tmp_path, ufoFormatVersion=(2, 0))
298	glyph = src["A"]
299
300	# no explicit formatVersion passed: use the more recent GLIF formatVersion
301	# that is supported by given ufoFormatVersion (GLIF 1 for UFO 2)
302	dst.writeGlyph("A", glyph)
303	glif = dst.getGLIF("A")
304	assert b'format="1"' in glif
305	assert b'formatMinor' not in glif  # omitted when 0
306
307	# explicit, unknown formatVersion
308	with pytest.raises(UnsupportedGLIFFormat):
309		dst.writeGlyph("A", glyph, formatVersion=(0, 0))
310
311	# explicit, known formatVersion but unsupported by given ufoFormatVersion
312	with pytest.raises(
313		UnsupportedGLIFFormat,
314		match="Unsupported GLIF format version .*for UFO format version",
315	):
316		dst.writeGlyph("A", glyph, formatVersion=(2, 0))
317