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.
- fontTools/__init__.py +1 -1
- fontTools/annotations.py +30 -0
- fontTools/cffLib/CFF2ToCFF.py +65 -10
- fontTools/cffLib/__init__.py +61 -26
- fontTools/cffLib/specializer.py +4 -1
- fontTools/cffLib/transforms.py +11 -6
- fontTools/config/__init__.py +15 -0
- fontTools/cu2qu/cu2qu.c +6567 -5579
- fontTools/cu2qu/cu2qu.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/cu2qu/cu2qu.py +36 -4
- fontTools/cu2qu/ufo.py +14 -0
- fontTools/designspaceLib/__init__.py +8 -3
- fontTools/designspaceLib/statNames.py +14 -7
- fontTools/feaLib/ast.py +24 -15
- fontTools/feaLib/builder.py +139 -66
- fontTools/feaLib/error.py +1 -1
- fontTools/feaLib/lexer.c +7038 -7995
- fontTools/feaLib/lexer.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/feaLib/parser.py +75 -40
- fontTools/feaLib/variableScalar.py +6 -1
- fontTools/fontBuilder.py +50 -44
- fontTools/merge/__init__.py +1 -1
- fontTools/merge/cmap.py +33 -1
- fontTools/merge/tables.py +12 -1
- fontTools/misc/bezierTools.c +14913 -17013
- fontTools/misc/bezierTools.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/misc/bezierTools.py +4 -1
- fontTools/misc/configTools.py +3 -1
- fontTools/misc/enumTools.py +23 -0
- fontTools/misc/etree.py +4 -27
- fontTools/misc/filesystem/__init__.py +68 -0
- fontTools/misc/filesystem/_base.py +134 -0
- fontTools/misc/filesystem/_copy.py +45 -0
- fontTools/misc/filesystem/_errors.py +54 -0
- fontTools/misc/filesystem/_info.py +75 -0
- fontTools/misc/filesystem/_osfs.py +164 -0
- fontTools/misc/filesystem/_path.py +67 -0
- fontTools/misc/filesystem/_subfs.py +92 -0
- fontTools/misc/filesystem/_tempfs.py +34 -0
- fontTools/misc/filesystem/_tools.py +34 -0
- fontTools/misc/filesystem/_walk.py +55 -0
- fontTools/misc/filesystem/_zipfs.py +204 -0
- fontTools/misc/fixedTools.py +1 -1
- fontTools/misc/loggingTools.py +1 -1
- fontTools/misc/psCharStrings.py +17 -2
- fontTools/misc/sstruct.py +2 -6
- fontTools/misc/symfont.py +6 -8
- fontTools/misc/testTools.py +5 -1
- fontTools/misc/textTools.py +4 -2
- fontTools/misc/visitor.py +32 -16
- fontTools/misc/xmlWriter.py +44 -8
- fontTools/mtiLib/__init__.py +1 -3
- fontTools/otlLib/builder.py +402 -155
- fontTools/otlLib/optimize/gpos.py +49 -63
- fontTools/pens/filterPen.py +218 -26
- fontTools/pens/momentsPen.c +5514 -5584
- fontTools/pens/momentsPen.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/pens/pointPen.py +61 -18
- fontTools/pens/roundingPen.py +2 -2
- fontTools/pens/t2CharStringPen.py +31 -11
- fontTools/qu2cu/qu2cu.c +6581 -6168
- fontTools/qu2cu/qu2cu.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/subset/__init__.py +283 -25
- fontTools/subset/svg.py +2 -3
- fontTools/ttLib/__init__.py +4 -0
- fontTools/ttLib/__main__.py +47 -8
- fontTools/ttLib/removeOverlaps.py +7 -5
- fontTools/ttLib/reorderGlyphs.py +8 -7
- fontTools/ttLib/sfnt.py +11 -9
- fontTools/ttLib/tables/D__e_b_g.py +20 -2
- fontTools/ttLib/tables/G_V_A_R_.py +5 -0
- fontTools/ttLib/tables/S__i_l_f.py +2 -2
- fontTools/ttLib/tables/T_S_I__0.py +14 -3
- fontTools/ttLib/tables/T_S_I__1.py +2 -5
- fontTools/ttLib/tables/T_S_I__5.py +18 -7
- fontTools/ttLib/tables/__init__.py +1 -0
- fontTools/ttLib/tables/_a_v_a_r.py +12 -3
- fontTools/ttLib/tables/_c_m_a_p.py +20 -7
- fontTools/ttLib/tables/_c_v_t.py +3 -2
- fontTools/ttLib/tables/_f_p_g_m.py +3 -1
- fontTools/ttLib/tables/_g_l_y_f.py +45 -21
- fontTools/ttLib/tables/_g_v_a_r.py +67 -19
- fontTools/ttLib/tables/_h_d_m_x.py +4 -4
- fontTools/ttLib/tables/_h_m_t_x.py +7 -3
- fontTools/ttLib/tables/_l_o_c_a.py +2 -2
- fontTools/ttLib/tables/_n_a_m_e.py +11 -6
- fontTools/ttLib/tables/_p_o_s_t.py +9 -7
- fontTools/ttLib/tables/otBase.py +5 -12
- fontTools/ttLib/tables/otConverters.py +5 -2
- fontTools/ttLib/tables/otData.py +1 -1
- fontTools/ttLib/tables/otTables.py +33 -30
- fontTools/ttLib/tables/otTraverse.py +2 -1
- fontTools/ttLib/tables/sbixStrike.py +3 -3
- fontTools/ttLib/ttFont.py +666 -120
- fontTools/ttLib/ttGlyphSet.py +0 -10
- fontTools/ttLib/woff2.py +10 -13
- fontTools/ttx.py +13 -1
- fontTools/ufoLib/__init__.py +300 -202
- fontTools/ufoLib/converters.py +103 -30
- fontTools/ufoLib/errors.py +8 -0
- fontTools/ufoLib/etree.py +1 -1
- fontTools/ufoLib/filenames.py +171 -106
- fontTools/ufoLib/glifLib.py +303 -205
- fontTools/ufoLib/kerning.py +98 -48
- fontTools/ufoLib/utils.py +46 -15
- fontTools/ufoLib/validators.py +121 -99
- fontTools/unicodedata/Blocks.py +35 -20
- fontTools/unicodedata/Mirrored.py +446 -0
- fontTools/unicodedata/ScriptExtensions.py +63 -37
- fontTools/unicodedata/Scripts.py +173 -152
- fontTools/unicodedata/__init__.py +10 -2
- fontTools/varLib/__init__.py +198 -109
- fontTools/varLib/avar/__init__.py +0 -0
- fontTools/varLib/avar/__main__.py +72 -0
- fontTools/varLib/avar/build.py +79 -0
- fontTools/varLib/avar/map.py +108 -0
- fontTools/varLib/avar/plan.py +1004 -0
- fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
- fontTools/varLib/avarPlanner.py +3 -999
- fontTools/varLib/featureVars.py +21 -7
- fontTools/varLib/hvar.py +113 -0
- fontTools/varLib/instancer/__init__.py +180 -65
- fontTools/varLib/interpolatableHelpers.py +3 -0
- fontTools/varLib/iup.c +7564 -6903
- fontTools/varLib/iup.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/varLib/models.py +17 -2
- fontTools/varLib/mutator.py +11 -0
- fontTools/varLib/varStore.py +10 -38
- fontTools/voltLib/__main__.py +206 -0
- fontTools/voltLib/ast.py +4 -0
- fontTools/voltLib/parser.py +16 -8
- fontTools/voltLib/voltToFea.py +347 -166
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/METADATA +269 -1410
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/RECORD +318 -294
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/WHEEL +1 -1
- fonttools-4.61.1.dist-info/licenses/LICENSE.external +388 -0
- {fonttools-4.55.4.data → fonttools-4.61.1.data}/data/share/man/man1/ttx.1 +0 -0
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/entry_points.txt +0 -0
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info/licenses}/LICENSE +0 -0
- {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()
|
fontTools/misc/fixedTools.py
CHANGED
fontTools/misc/loggingTools.py
CHANGED
|
@@ -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
|
|
fontTools/misc/psCharStrings.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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)
|
fontTools/misc/testTools.py
CHANGED
|
@@ -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
|
|
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
|
|
fontTools/misc/textTools.py
CHANGED
|
@@ -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:
|