fonttools 4.55.4__cp313-cp313-musllinux_1_2_aarch64.whl → 4.61.1__cp313-cp313-musllinux_1_2_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/annotations.py +30 -0
  3. fontTools/cffLib/CFF2ToCFF.py +65 -10
  4. fontTools/cffLib/__init__.py +61 -26
  5. fontTools/cffLib/specializer.py +4 -1
  6. fontTools/cffLib/transforms.py +11 -6
  7. fontTools/config/__init__.py +15 -0
  8. fontTools/cu2qu/cu2qu.c +6567 -5579
  9. fontTools/cu2qu/cu2qu.cpython-313-aarch64-linux-musl.so +0 -0
  10. fontTools/cu2qu/cu2qu.py +36 -4
  11. fontTools/cu2qu/ufo.py +14 -0
  12. fontTools/designspaceLib/__init__.py +8 -3
  13. fontTools/designspaceLib/statNames.py +14 -7
  14. fontTools/feaLib/ast.py +24 -15
  15. fontTools/feaLib/builder.py +139 -66
  16. fontTools/feaLib/error.py +1 -1
  17. fontTools/feaLib/lexer.c +7038 -7995
  18. fontTools/feaLib/lexer.cpython-313-aarch64-linux-musl.so +0 -0
  19. fontTools/feaLib/parser.py +75 -40
  20. fontTools/feaLib/variableScalar.py +6 -1
  21. fontTools/fontBuilder.py +50 -44
  22. fontTools/merge/__init__.py +1 -1
  23. fontTools/merge/cmap.py +33 -1
  24. fontTools/merge/tables.py +12 -1
  25. fontTools/misc/bezierTools.c +14913 -17013
  26. fontTools/misc/bezierTools.cpython-313-aarch64-linux-musl.so +0 -0
  27. fontTools/misc/bezierTools.py +4 -1
  28. fontTools/misc/configTools.py +3 -1
  29. fontTools/misc/enumTools.py +23 -0
  30. fontTools/misc/etree.py +4 -27
  31. fontTools/misc/filesystem/__init__.py +68 -0
  32. fontTools/misc/filesystem/_base.py +134 -0
  33. fontTools/misc/filesystem/_copy.py +45 -0
  34. fontTools/misc/filesystem/_errors.py +54 -0
  35. fontTools/misc/filesystem/_info.py +75 -0
  36. fontTools/misc/filesystem/_osfs.py +164 -0
  37. fontTools/misc/filesystem/_path.py +67 -0
  38. fontTools/misc/filesystem/_subfs.py +92 -0
  39. fontTools/misc/filesystem/_tempfs.py +34 -0
  40. fontTools/misc/filesystem/_tools.py +34 -0
  41. fontTools/misc/filesystem/_walk.py +55 -0
  42. fontTools/misc/filesystem/_zipfs.py +204 -0
  43. fontTools/misc/fixedTools.py +1 -1
  44. fontTools/misc/loggingTools.py +1 -1
  45. fontTools/misc/psCharStrings.py +17 -2
  46. fontTools/misc/sstruct.py +2 -6
  47. fontTools/misc/symfont.py +6 -8
  48. fontTools/misc/testTools.py +5 -1
  49. fontTools/misc/textTools.py +4 -2
  50. fontTools/misc/visitor.py +32 -16
  51. fontTools/misc/xmlWriter.py +44 -8
  52. fontTools/mtiLib/__init__.py +1 -3
  53. fontTools/otlLib/builder.py +402 -155
  54. fontTools/otlLib/optimize/gpos.py +49 -63
  55. fontTools/pens/filterPen.py +218 -26
  56. fontTools/pens/momentsPen.c +5514 -5584
  57. fontTools/pens/momentsPen.cpython-313-aarch64-linux-musl.so +0 -0
  58. fontTools/pens/pointPen.py +61 -18
  59. fontTools/pens/roundingPen.py +2 -2
  60. fontTools/pens/t2CharStringPen.py +31 -11
  61. fontTools/qu2cu/qu2cu.c +6581 -6168
  62. fontTools/qu2cu/qu2cu.cpython-313-aarch64-linux-musl.so +0 -0
  63. fontTools/subset/__init__.py +283 -25
  64. fontTools/subset/svg.py +2 -3
  65. fontTools/ttLib/__init__.py +4 -0
  66. fontTools/ttLib/__main__.py +47 -8
  67. fontTools/ttLib/removeOverlaps.py +7 -5
  68. fontTools/ttLib/reorderGlyphs.py +8 -7
  69. fontTools/ttLib/sfnt.py +11 -9
  70. fontTools/ttLib/tables/D__e_b_g.py +20 -2
  71. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  72. fontTools/ttLib/tables/S__i_l_f.py +2 -2
  73. fontTools/ttLib/tables/T_S_I__0.py +14 -3
  74. fontTools/ttLib/tables/T_S_I__1.py +2 -5
  75. fontTools/ttLib/tables/T_S_I__5.py +18 -7
  76. fontTools/ttLib/tables/__init__.py +1 -0
  77. fontTools/ttLib/tables/_a_v_a_r.py +12 -3
  78. fontTools/ttLib/tables/_c_m_a_p.py +20 -7
  79. fontTools/ttLib/tables/_c_v_t.py +3 -2
  80. fontTools/ttLib/tables/_f_p_g_m.py +3 -1
  81. fontTools/ttLib/tables/_g_l_y_f.py +45 -21
  82. fontTools/ttLib/tables/_g_v_a_r.py +67 -19
  83. fontTools/ttLib/tables/_h_d_m_x.py +4 -4
  84. fontTools/ttLib/tables/_h_m_t_x.py +7 -3
  85. fontTools/ttLib/tables/_l_o_c_a.py +2 -2
  86. fontTools/ttLib/tables/_n_a_m_e.py +11 -6
  87. fontTools/ttLib/tables/_p_o_s_t.py +9 -7
  88. fontTools/ttLib/tables/otBase.py +5 -12
  89. fontTools/ttLib/tables/otConverters.py +5 -2
  90. fontTools/ttLib/tables/otData.py +1 -1
  91. fontTools/ttLib/tables/otTables.py +33 -30
  92. fontTools/ttLib/tables/otTraverse.py +2 -1
  93. fontTools/ttLib/tables/sbixStrike.py +3 -3
  94. fontTools/ttLib/ttFont.py +666 -120
  95. fontTools/ttLib/ttGlyphSet.py +0 -10
  96. fontTools/ttLib/woff2.py +10 -13
  97. fontTools/ttx.py +13 -1
  98. fontTools/ufoLib/__init__.py +300 -202
  99. fontTools/ufoLib/converters.py +103 -30
  100. fontTools/ufoLib/errors.py +8 -0
  101. fontTools/ufoLib/etree.py +1 -1
  102. fontTools/ufoLib/filenames.py +171 -106
  103. fontTools/ufoLib/glifLib.py +303 -205
  104. fontTools/ufoLib/kerning.py +98 -48
  105. fontTools/ufoLib/utils.py +46 -15
  106. fontTools/ufoLib/validators.py +121 -99
  107. fontTools/unicodedata/Blocks.py +35 -20
  108. fontTools/unicodedata/Mirrored.py +446 -0
  109. fontTools/unicodedata/ScriptExtensions.py +63 -37
  110. fontTools/unicodedata/Scripts.py +173 -152
  111. fontTools/unicodedata/__init__.py +10 -2
  112. fontTools/varLib/__init__.py +198 -109
  113. fontTools/varLib/avar/__init__.py +0 -0
  114. fontTools/varLib/avar/__main__.py +72 -0
  115. fontTools/varLib/avar/build.py +79 -0
  116. fontTools/varLib/avar/map.py +108 -0
  117. fontTools/varLib/avar/plan.py +1004 -0
  118. fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
  119. fontTools/varLib/avarPlanner.py +3 -999
  120. fontTools/varLib/featureVars.py +21 -7
  121. fontTools/varLib/hvar.py +113 -0
  122. fontTools/varLib/instancer/__init__.py +180 -65
  123. fontTools/varLib/interpolatableHelpers.py +3 -0
  124. fontTools/varLib/iup.c +7564 -6903
  125. fontTools/varLib/iup.cpython-313-aarch64-linux-musl.so +0 -0
  126. fontTools/varLib/models.py +17 -2
  127. fontTools/varLib/mutator.py +11 -0
  128. fontTools/varLib/varStore.py +10 -38
  129. fontTools/voltLib/__main__.py +206 -0
  130. fontTools/voltLib/ast.py +4 -0
  131. fontTools/voltLib/parser.py +16 -8
  132. fontTools/voltLib/voltToFea.py +347 -166
  133. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/METADATA +269 -1410
  134. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/RECORD +318 -294
  135. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/WHEEL +1 -1
  136. fonttools-4.61.1.dist-info/licenses/LICENSE.external +388 -0
  137. {fonttools-4.55.4.data → fonttools-4.61.1.data}/data/share/man/man1/ttx.1 +0 -0
  138. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/entry_points.txt +0 -0
  139. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info/licenses}/LICENSE +0 -0
  140. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,67 @@
1
+ import os
2
+ import platform
3
+
4
+ _WINDOWS_PLATFORM = platform.system() == "Windows"
5
+
6
+
7
+ def combine(path1: str, path2) -> str:
8
+ if not path1:
9
+ return path2
10
+ return "{}/{}".format(path1.rstrip("/"), path2.lstrip("/"))
11
+
12
+
13
+ def split(path: str) -> tuple[str, str]:
14
+ if "/" not in path:
15
+ return ("", path)
16
+ split = path.rsplit("/", 1)
17
+ return (split[0] or "/", split[1])
18
+
19
+
20
+ def dirname(path: str) -> str:
21
+ return split(path)[0]
22
+
23
+
24
+ def basename(path: str) -> str:
25
+ return split(path)[1]
26
+
27
+
28
+ def forcedir(path: str) -> str:
29
+ # Ensure the path ends with a trailing forward slash.
30
+ if not path.endswith("/"):
31
+ return path + "/"
32
+ return path
33
+
34
+
35
+ def abspath(path: str) -> str:
36
+ # FS objects have no concept of a *current directory*. This simply
37
+ # ensures the path starts with a forward slash.
38
+ if not path.startswith("/"):
39
+ return "/" + path
40
+ return path
41
+
42
+
43
+ def isbase(path1: str, path2: str) -> bool:
44
+ # Check if `path1` is a base or prefix of `path2`.
45
+ _path1 = forcedir(abspath(path1))
46
+ _path2 = forcedir(abspath(path2))
47
+ return _path2.startswith(_path1)
48
+
49
+
50
+ def frombase(path1: str, path2: str) -> str:
51
+ # Get the final path of `path2` that isn't in `path1`.
52
+ if not isbase(path1, path2):
53
+ raise ValueError(f"path1 must be a prefix of path2: {path1!r} vs {path2!r}")
54
+ return path2[len(path1) :]
55
+
56
+
57
+ def relpath(path: str) -> str:
58
+ return path.lstrip("/")
59
+
60
+
61
+ def normpath(path: str) -> str:
62
+ normalized = os.path.normpath(path)
63
+ if _WINDOWS_PLATFORM:
64
+ # os.path.normpath converts backslashes to forward slashes on Windows
65
+ # but we want forward slashes, so we convert them back
66
+ normalized = normalized.replace("\\", "/")
67
+ return normalized
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+ from pathlib import PurePosixPath
5
+
6
+ from ._base import FS
7
+ from ._errors import DirectoryExpected, ResourceNotFound
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from collections.abc import Collection
11
+ from typing import IO, Any
12
+
13
+ from ._info import Info
14
+
15
+
16
+ class SubFS(FS):
17
+ """Maps a sub-directory of another filesystem."""
18
+
19
+ def __init__(self, parent: FS, sub_path: str):
20
+ super().__init__()
21
+ self._parent = parent
22
+ self._prefix = PurePosixPath(sub_path).as_posix().rstrip("/")
23
+ if not parent.exists(self._prefix):
24
+ raise ResourceNotFound(f"No such file or directory: {sub_path!r}")
25
+ elif not parent.isdir(self._prefix):
26
+ raise DirectoryExpected(f"{sub_path!r} is not a directory")
27
+
28
+ def delegate_fs(self):
29
+ return self._parent
30
+
31
+ def _full(self, rel: str) -> str:
32
+ self.check()
33
+ return f"{self._prefix}/{PurePosixPath(rel).as_posix()}".lstrip("/")
34
+
35
+ def open(self, path: str, mode: str = "rb", **kwargs) -> IO[Any]:
36
+ return self._parent.open(self._full(path), mode, **kwargs)
37
+
38
+ def exists(self, path: str) -> bool:
39
+ return self._parent.exists(self._full(path))
40
+
41
+ def isdir(self, path: str) -> bool:
42
+ return self._parent.isdir(self._full(path))
43
+
44
+ def isfile(self, path: str) -> bool:
45
+ return self._parent.isfile(self._full(path))
46
+
47
+ def listdir(self, path: str) -> list[str]:
48
+ return self._parent.listdir(self._full(path))
49
+
50
+ def makedir(self, path: str, recreate: bool = False):
51
+ return self._parent.makedir(self._full(path), recreate=recreate)
52
+
53
+ def makedirs(self, path: str, recreate: bool = False):
54
+ return self._parent.makedirs(self._full(path), recreate=recreate)
55
+
56
+ def getinfo(self, path: str, namespaces: Collection[str] | None = None) -> Info:
57
+ return self._parent.getinfo(self._full(path), namespaces=namespaces)
58
+
59
+ def remove(self, path: str):
60
+ return self._parent.remove(self._full(path))
61
+
62
+ def removedir(self, path: str):
63
+ return self._parent.removedir(self._full(path))
64
+
65
+ def removetree(self, path: str):
66
+ return self._parent.removetree(self._full(path))
67
+
68
+ def movedir(self, src: str, dst: str, create: bool = False):
69
+ self._parent.movedir(self._full(src), self._full(dst), create=create)
70
+
71
+ def getsyspath(self, path: str) -> str:
72
+ return self._parent.getsyspath(self._full(path))
73
+
74
+ def readbytes(self, path: str) -> bytes:
75
+ return self._parent.readbytes(self._full(path))
76
+
77
+ def writebytes(self, path: str, data: bytes):
78
+ self._parent.writebytes(self._full(path), data)
79
+
80
+ def __repr__(self) -> str:
81
+ return f"{self.__class__.__name__}({self._parent!r}, {self._prefix!r})"
82
+
83
+ def __str__(self) -> str:
84
+ return f"{self._parent}/{self._prefix}"
85
+
86
+
87
+ class ClosingSubFS(SubFS):
88
+ """Like SubFS, but auto-closes the parent filesystem when closed."""
89
+
90
+ def close(self):
91
+ super().close()
92
+ self._parent.close()
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import tempfile
5
+
6
+ from ._errors import OperationFailed
7
+ from ._osfs import OSFS
8
+
9
+
10
+ class TempFS(OSFS):
11
+ def __init__(self, auto_clean: bool = True, ignore_clean_errors: bool = True):
12
+ self.auto_clean = auto_clean
13
+ self.ignore_clean_errors = ignore_clean_errors
14
+ self._temp_dir = tempfile.mkdtemp("__temp_fs__")
15
+ self._cleaned = False
16
+ super().__init__(self._temp_dir)
17
+
18
+ def close(self):
19
+ if self.auto_clean:
20
+ self.clean()
21
+ super().close()
22
+
23
+ def clean(self):
24
+ if self._cleaned:
25
+ return
26
+
27
+ try:
28
+ shutil.rmtree(self._temp_dir)
29
+ except Exception as e:
30
+ if not self.ignore_clean_errors:
31
+ raise OperationFailed(
32
+ f"failed to remove temporary directory: {self._temp_dir!r}"
33
+ ) from e
34
+ self._cleaned = True
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+ from pathlib import PurePosixPath
5
+
6
+ from ._errors import DirectoryNotEmpty
7
+
8
+ if typing.TYPE_CHECKING:
9
+ from typing import IO
10
+
11
+ from ._base import FS
12
+
13
+
14
+ def remove_empty(fs: FS, path: str):
15
+ """Remove all empty parents."""
16
+ path = PurePosixPath(path)
17
+ root = PurePosixPath("/")
18
+ try:
19
+ while path != root:
20
+ fs.removedir(path.as_posix())
21
+ path = path.parent
22
+ except DirectoryNotEmpty:
23
+ pass
24
+
25
+
26
+ def copy_file_data(src_file: IO, dst_file: IO, chunk_size: int | None = None):
27
+ """Copy data from one file object to another."""
28
+ _chunk_size = 1024 * 1024 if chunk_size is None else chunk_size
29
+ read = src_file.read
30
+ write = dst_file.write
31
+ # in iter(callable, sentilel), callable is called until it returns the sentinel;
32
+ # this allows to copy `chunk_size` bytes at a time.
33
+ for chunk in iter(lambda: read(_chunk_size) or None, None):
34
+ write(chunk)
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+ from collections import deque
5
+ from collections.abc import Collection, Iterator
6
+
7
+ from ._path import combine
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from typing import Callable
11
+
12
+ from ._base import FS
13
+ from ._info import Info
14
+
15
+
16
+ class BoundWalker:
17
+ def __init__(self, fs: FS):
18
+ self._fs = fs
19
+
20
+ def _iter_walk(
21
+ self, path: str, namespaces: Collection[str] | None = None
22
+ ) -> Iterator[tuple[str, Info | None]]:
23
+ """Walk files using a *breadth first* search."""
24
+ queue = deque([path])
25
+ push = queue.appendleft
26
+ pop = queue.pop
27
+ _scan = self._fs.scandir
28
+ _combine = combine
29
+
30
+ while queue:
31
+ dir_path = pop()
32
+ for info in _scan(dir_path, namespaces=namespaces):
33
+ if info.is_dir:
34
+ yield dir_path, info
35
+ push(_combine(dir_path, info.name))
36
+ else:
37
+ yield dir_path, info
38
+ yield path, None
39
+
40
+ def _filter(
41
+ self,
42
+ include: Callable[[str, Info], bool] = lambda path, info: True,
43
+ path: str = "/",
44
+ namespaces: Collection[str] | None = None,
45
+ ) -> Iterator[str]:
46
+ _combine = combine
47
+ for path, info in self._iter_walk(path, namespaces):
48
+ if info is not None and include(path, info):
49
+ yield _combine(path, info.name)
50
+
51
+ def files(self, path: str = "/") -> Iterator[str]:
52
+ yield from self._filter(lambda _, info: info.is_file, path)
53
+
54
+ def dirs(self, path: str = "/") -> Iterator[str]:
55
+ yield from self._filter(lambda _, info: info.is_dir, path)
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import os
5
+ import shutil
6
+ import stat
7
+ import typing
8
+ import zipfile
9
+ from datetime import datetime
10
+
11
+ from ._base import FS
12
+ from ._errors import FileExpected, ResourceNotFound, ResourceReadOnly
13
+ from ._info import Info
14
+ from ._path import dirname, forcedir, normpath, relpath
15
+ from ._tempfs import TempFS
16
+
17
+ if typing.TYPE_CHECKING:
18
+ from collections.abc import Collection
19
+ from typing import IO, Any
20
+
21
+ from ._subfs import SubFS
22
+
23
+
24
+ class ZipFS(FS):
25
+ """Read and write zip files."""
26
+
27
+ def __new__(
28
+ cls, file: str | os.PathLike, write: bool = False, encoding: str = "utf-8"
29
+ ):
30
+ if write:
31
+ return WriteZipFS(file, encoding)
32
+ else:
33
+ return ReadZipFS(file, encoding)
34
+
35
+ if typing.TYPE_CHECKING:
36
+
37
+ def __init__(
38
+ self, file: str | os.PathLike, write: bool = False, encoding: str = "utf-8"
39
+ ):
40
+ pass
41
+
42
+
43
+ class ReadZipFS(FS):
44
+ """A readable zip file."""
45
+
46
+ def __init__(self, file: str | os.PathLike, encoding: str = "utf-8"):
47
+ super().__init__()
48
+ self._file = os.fspath(file)
49
+ self.encoding = encoding # unused
50
+ self._zip = zipfile.ZipFile(file, "r")
51
+ self._directory_fs = None
52
+
53
+ def __repr__(self) -> str:
54
+ return f"ReadZipFS({self._file!r})"
55
+
56
+ def __str__(self) -> str:
57
+ return f"<zipfs '{self._file}'>"
58
+
59
+ def _path_to_zip_name(self, path: str) -> str:
60
+ """Convert a path to a zip file name."""
61
+ path = relpath(normpath(path))
62
+ if self._directory.isdir(path):
63
+ path = forcedir(path)
64
+ return path
65
+
66
+ @property
67
+ def _directory(self) -> TempFS:
68
+ if self._directory_fs is None:
69
+ self._directory_fs = _fs = TempFS()
70
+ for zip_name in self._zip.namelist():
71
+ resource_name = zip_name
72
+ if resource_name.endswith("/"):
73
+ _fs.makedirs(resource_name, recreate=True)
74
+ else:
75
+ _fs.makedirs(dirname(resource_name), recreate=True)
76
+ _fs.create(resource_name)
77
+ return self._directory_fs
78
+
79
+ def close(self):
80
+ super(ReadZipFS, self).close()
81
+ self._zip.close()
82
+ if self._directory_fs is not None:
83
+ self._directory_fs.close()
84
+
85
+ def getinfo(self, path: str, namespaces: Collection[str] | None = None) -> Info:
86
+ namespaces = namespaces or ()
87
+ raw_info = {}
88
+
89
+ if path == "/":
90
+ raw_info["basic"] = {"name": "", "is_dir": True}
91
+ if "details" in namespaces:
92
+ raw_info["details"] = {"type": stat.S_IFDIR}
93
+ else:
94
+ basic_info = self._directory.getinfo(path)
95
+ raw_info["basic"] = {"name": basic_info.name, "is_dir": basic_info.is_dir}
96
+
97
+ if "details" in namespaces:
98
+ zip_name = self._path_to_zip_name(path)
99
+ try:
100
+ zip_info = self._zip.getinfo(zip_name)
101
+ except KeyError:
102
+ pass
103
+ else:
104
+ if "details" in namespaces:
105
+ raw_info["details"] = {
106
+ "size": zip_info.file_size,
107
+ "type": int(
108
+ stat.S_IFDIR if basic_info.is_dir else stat.S_IFREG
109
+ ),
110
+ "modified": datetime(*zip_info.date_time).timestamp(),
111
+ }
112
+
113
+ return Info(raw_info)
114
+
115
+ def exists(self, path: str) -> bool:
116
+ self.check()
117
+ return self._directory.exists(path)
118
+
119
+ def isdir(self, path: str) -> bool:
120
+ self.check()
121
+ return self._directory.isdir(path)
122
+
123
+ def isfile(self, path: str) -> bool:
124
+ self.check()
125
+ return self._directory.isfile(path)
126
+
127
+ def listdir(self, path: str) -> str:
128
+ self.check()
129
+ return self._directory.listdir(path)
130
+
131
+ def makedir(self, path: str, recreate: bool = False) -> SubFS:
132
+ self.check()
133
+ raise ResourceReadOnly(path)
134
+
135
+ def makedirs(self, path: str, recreate: bool = False) -> SubFS:
136
+ self.check()
137
+ raise ResourceReadOnly(path)
138
+
139
+ def remove(self, path: str):
140
+ self.check()
141
+ raise ResourceReadOnly(path)
142
+
143
+ def removedir(self, path: str):
144
+ self.check()
145
+ raise ResourceReadOnly(path)
146
+
147
+ def removetree(self, path: str):
148
+ self.check()
149
+ raise ResourceReadOnly(path)
150
+
151
+ def movedir(self, src: str, dst: str, create: bool = False):
152
+ self.check()
153
+ raise ResourceReadOnly(src)
154
+
155
+ def readbytes(self, path: str) -> bytes:
156
+ self.check()
157
+ if not self._directory.isfile(path):
158
+ raise ResourceNotFound(path)
159
+ zip_name = self._path_to_zip_name(path)
160
+ zip_bytes = self._zip.read(zip_name)
161
+ return zip_bytes
162
+
163
+ def open(self, path: str, mode: str = "rb", **kwargs) -> IO[Any]:
164
+ self.check()
165
+ if self._directory.isdir(path):
166
+ raise FileExpected(f"{path!r} is a directory")
167
+
168
+ zip_mode = mode[0]
169
+ if zip_mode == "r" and not self._directory.exists(path):
170
+ raise ResourceNotFound(f"No such file or directory: {path!r}")
171
+
172
+ if any(m in mode for m in "wax+"):
173
+ raise ResourceReadOnly(path)
174
+
175
+ zip_name = self._path_to_zip_name(path)
176
+ stream = self._zip.open(zip_name, zip_mode)
177
+ if "b" in mode:
178
+ if kwargs:
179
+ raise ValueError("encoding args invalid for binary operation")
180
+ return stream
181
+ # Text mode
182
+ return io.TextIOWrapper(stream, **kwargs)
183
+
184
+
185
+ class WriteZipFS(TempFS):
186
+ """A writable zip file."""
187
+
188
+ def __init__(self, file: str | os.PathLike, encoding: str = "utf-8"):
189
+ super().__init__()
190
+ self._file = os.fspath(file)
191
+ self.encoding = encoding # unused
192
+
193
+ def __repr__(self) -> str:
194
+ return f"WriteZipFS({self._file!r})"
195
+
196
+ def __str__(self) -> str:
197
+ return f"<zipfs-write '{self._file}'>"
198
+
199
+ def close(self):
200
+ base_name = os.path.splitext(self._file)[0]
201
+ shutil.make_archive(base_name, format="zip", root_dir=self._temp_dir)
202
+ if self._file != base_name + ".zip":
203
+ shutil.move(base_name + ".zip", self._file)
204
+ super().close()
@@ -39,7 +39,7 @@ __all__ = [
39
39
  MAX_F2DOT14 = 0x7FFF / (1 << 14)
40
40
 
41
41
 
42
- def fixedToFloat(value, precisionBits):
42
+ def fixedToFloat(value: float, precisionBits: int) -> float:
43
43
  """Converts a fixed-point number to a float given the number of
44
44
  precision bits.
45
45
 
@@ -284,7 +284,7 @@ class Timer(object):
284
284
  """
285
285
 
286
286
  # timeit.default_timer choses the most accurate clock for each platform
287
- _time = timeit.default_timer
287
+ _time: Callable[[], float] = staticmethod(timeit.default_timer)
288
288
  default_msg = "elapsed time: %(time).3fs"
289
289
  default_format = "Took %(time).3fs to %(msg)s"
290
290
 
@@ -338,7 +338,7 @@ class SimpleT2Decompiler(object):
338
338
  self.numRegions = 0
339
339
  self.vsIndex = 0
340
340
 
341
- def execute(self, charString):
341
+ def execute(self, charString, *, pushToStack=None):
342
342
  self.callingStack.append(charString)
343
343
  needsDecompilation = charString.needsDecompilation()
344
344
  if needsDecompilation:
@@ -346,7 +346,8 @@ class SimpleT2Decompiler(object):
346
346
  pushToProgram = program.append
347
347
  else:
348
348
  pushToProgram = lambda x: None
349
- pushToStack = self.operandStack.append
349
+ if pushToStack is None:
350
+ pushToStack = self.operandStack.append
350
351
  index = 0
351
352
  while True:
352
353
  token, isOperator, index = charString.getToken(index)
@@ -551,6 +552,20 @@ t1Operators = [
551
552
  ]
552
553
 
553
554
 
555
+ class T2StackUseExtractor(SimpleT2Decompiler):
556
+
557
+ def execute(self, charString):
558
+ maxStackUse = 0
559
+
560
+ def pushToStack(value):
561
+ nonlocal maxStackUse
562
+ self.operandStack.append(value)
563
+ maxStackUse = max(maxStackUse, len(self.operandStack))
564
+
565
+ super().execute(charString, pushToStack=pushToStack)
566
+ return maxStackUse
567
+
568
+
554
569
  class T2WidthExtractor(SimpleT2Decompiler):
555
570
  def __init__(
556
571
  self,
fontTools/misc/sstruct.py CHANGED
@@ -64,10 +64,7 @@ def pack(fmt, obj):
64
64
  elements = []
65
65
  if not isinstance(obj, dict):
66
66
  obj = obj.__dict__
67
- string_index = formatstring
68
- if formatstring.startswith(">"):
69
- string_index = formatstring[1:]
70
- for ix, name in enumerate(names.keys()):
67
+ for name in names.keys():
71
68
  value = obj[name]
72
69
  if name in fixes:
73
70
  # fixed point conversion
@@ -96,8 +93,7 @@ def unpack(fmt, data, obj=None):
96
93
  else:
97
94
  d = obj.__dict__
98
95
  elements = struct.unpack(formatstring, data)
99
- for i in range(len(names)):
100
- name = list(names.keys())[i]
96
+ for i, name in enumerate(names.keys()):
101
97
  value = elements[i]
102
98
  if name in fixes:
103
99
  # fixed point conversion
fontTools/misc/symfont.py CHANGED
@@ -234,11 +234,9 @@ if __name__ == '__main__':
234
234
 
235
235
 
236
236
  if __name__ == "__main__":
237
- pen = AreaPen()
238
- pen.moveTo((100, 100))
239
- pen.lineTo((100, 200))
240
- pen.lineTo((200, 200))
241
- pen.curveTo((200, 250), (300, 300), (250, 350))
242
- pen.lineTo((200, 100))
243
- pen.closePath()
244
- print(pen.value)
237
+ import sys
238
+
239
+ if sys.argv[1:]:
240
+ penName = sys.argv[1]
241
+ funcs = [(name, eval(f)) for name, f in zip(sys.argv[2::2], sys.argv[3::2])]
242
+ printGreenPen(penName, funcs, file=sys.stdout)
@@ -46,7 +46,8 @@ def parseXmlInto(font, parseInto, xmlSnippet):
46
46
  parsed_xml = [e for e in parseXML(xmlSnippet.strip()) if not isinstance(e, str)]
47
47
  for name, attrs, content in parsed_xml:
48
48
  parseInto.fromXML(name, attrs, content, font)
49
- parseInto.populateDefaults()
49
+ if hasattr(parseInto, "populateDefaults"):
50
+ parseInto.populateDefaults()
50
51
  return parseInto
51
52
 
52
53
 
@@ -58,6 +59,9 @@ class FakeFont:
58
59
  self.tables = {}
59
60
  self.cfg = Config()
60
61
 
62
+ def __contains__(self, tag):
63
+ return tag in self.tables
64
+
61
65
  def __getitem__(self, tag):
62
66
  return self.tables[tag]
63
67
 
@@ -1,5 +1,7 @@
1
1
  """fontTools.misc.textTools.py -- miscellaneous routines."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import ast
4
6
  import string
5
7
 
@@ -118,14 +120,14 @@ def pad(data, size):
118
120
  return data
119
121
 
120
122
 
121
- def tostr(s, encoding="ascii", errors="strict"):
123
+ def tostr(s: str | bytes, encoding: str = "ascii", errors: str = "strict") -> str:
122
124
  if not isinstance(s, str):
123
125
  return s.decode(encoding, errors)
124
126
  else:
125
127
  return s
126
128
 
127
129
 
128
- def tobytes(s, encoding="ascii", errors="strict"):
130
+ def tobytes(s: str | bytes, encoding: str = "ascii", errors: str = "strict") -> bytes:
129
131
  if isinstance(s, str):
130
132
  return s.encode(encoding, errors)
131
133
  else: