fonttools 4.58.5__cp310-cp310-musllinux_1_2_aarch64.whl → 4.59.0__cp310-cp310-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.
Potentially problematic release.
This version of fonttools might be problematic. Click here for more details.
- fontTools/__init__.py +1 -1
- fontTools/merge/__init__.py +1 -1
- 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/sstruct.py +2 -6
- fontTools/misc/xmlWriter.py +28 -1
- fontTools/pens/roundingPen.py +2 -2
- fontTools/subset/__init__.py +11 -11
- fontTools/ttLib/sfnt.py +2 -3
- fontTools/ttLib/tables/S__i_l_f.py +2 -2
- fontTools/ttLib/tables/T_S_I__1.py +2 -5
- fontTools/ttLib/tables/T_S_I__5.py +2 -2
- fontTools/ttLib/tables/_c_m_a_p.py +1 -1
- fontTools/ttLib/tables/_c_v_t.py +1 -2
- fontTools/ttLib/tables/_g_l_y_f.py +9 -10
- fontTools/ttLib/tables/_h_d_m_x.py +4 -4
- fontTools/ttLib/tables/_l_o_c_a.py +2 -2
- fontTools/ttLib/tables/_p_o_s_t.py +3 -4
- fontTools/ttLib/tables/otBase.py +2 -4
- fontTools/ttLib/tables/otTables.py +7 -12
- fontTools/ttLib/tables/sbixStrike.py +3 -3
- fontTools/ttLib/ttFont.py +7 -16
- fontTools/ttLib/woff2.py +10 -13
- fontTools/ufoLib/__init__.py +20 -25
- fontTools/ufoLib/glifLib.py +12 -17
- fontTools/ufoLib/validators.py +2 -4
- fontTools/unicodedata/__init__.py +2 -0
- fontTools/varLib/hvar.py +1 -1
- {fonttools-4.58.5.dist-info → fonttools-4.59.0.dist-info}/METADATA +19 -3
- {fonttools-4.58.5.dist-info → fonttools-4.59.0.dist-info}/RECORD +46 -34
- {fonttools-4.58.5.dist-info → fonttools-4.59.0.dist-info}/licenses/LICENSE.external +29 -0
- {fonttools-4.58.5.data → fonttools-4.59.0.data}/data/share/man/man1/ttx.1 +0 -0
- {fonttools-4.58.5.dist-info → fonttools-4.59.0.dist-info}/WHEEL +0 -0
- {fonttools-4.58.5.dist-info → fonttools-4.59.0.dist-info}/entry_points.txt +0 -0
- {fonttools-4.58.5.dist-info → fonttools-4.59.0.dist-info}/licenses/LICENSE +0 -0
- {fonttools-4.58.5.dist-info → fonttools-4.59.0.dist-info}/top_level.txt +0 -0
|
@@ -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/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/xmlWriter.py
CHANGED
|
@@ -4,8 +4,22 @@ from fontTools.misc.textTools import byteord, strjoin, tobytes, tostr
|
|
|
4
4
|
import sys
|
|
5
5
|
import os
|
|
6
6
|
import string
|
|
7
|
+
import logging
|
|
8
|
+
import itertools
|
|
7
9
|
|
|
8
10
|
INDENT = " "
|
|
11
|
+
TTX_LOG = logging.getLogger("fontTools.ttx")
|
|
12
|
+
REPLACEMENT = "?"
|
|
13
|
+
ILLEGAL_XML_CHARS = dict.fromkeys(
|
|
14
|
+
itertools.chain(
|
|
15
|
+
range(0x00, 0x09),
|
|
16
|
+
(0x0B, 0x0C),
|
|
17
|
+
range(0x0E, 0x20),
|
|
18
|
+
range(0xD800, 0xE000),
|
|
19
|
+
(0xFFFE, 0xFFFF),
|
|
20
|
+
),
|
|
21
|
+
REPLACEMENT,
|
|
22
|
+
)
|
|
9
23
|
|
|
10
24
|
|
|
11
25
|
class XMLWriter(object):
|
|
@@ -168,12 +182,25 @@ class XMLWriter(object):
|
|
|
168
182
|
|
|
169
183
|
|
|
170
184
|
def escape(data):
|
|
185
|
+
"""Escape characters not allowed in `XML 1.0 <https://www.w3.org/TR/xml/#NT-Char>`_."""
|
|
171
186
|
data = tostr(data, "utf_8")
|
|
172
187
|
data = data.replace("&", "&")
|
|
173
188
|
data = data.replace("<", "<")
|
|
174
189
|
data = data.replace(">", ">")
|
|
175
190
|
data = data.replace("\r", " ")
|
|
176
|
-
|
|
191
|
+
|
|
192
|
+
newData = data.translate(ILLEGAL_XML_CHARS)
|
|
193
|
+
if newData != data:
|
|
194
|
+
maxLen = 10
|
|
195
|
+
preview = repr(data)
|
|
196
|
+
if len(data) > maxLen:
|
|
197
|
+
preview = repr(data[:maxLen])[1:-1] + "..."
|
|
198
|
+
TTX_LOG.warning(
|
|
199
|
+
"Illegal XML character(s) found; replacing offending " "string %r with %r",
|
|
200
|
+
preview,
|
|
201
|
+
REPLACEMENT,
|
|
202
|
+
)
|
|
203
|
+
return newData
|
|
177
204
|
|
|
178
205
|
|
|
179
206
|
def escapeattr(data):
|
fontTools/pens/roundingPen.py
CHANGED
|
@@ -116,8 +116,8 @@ class RoundingPointPen(FilterPointPen):
|
|
|
116
116
|
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
|
|
117
117
|
xx, xy, yx, yy, dx, dy = transformation
|
|
118
118
|
self._outPen.addComponent(
|
|
119
|
-
baseGlyphName
|
|
120
|
-
|
|
119
|
+
baseGlyphName,
|
|
120
|
+
Transform(
|
|
121
121
|
self.transformRoundFunc(xx),
|
|
122
122
|
self.transformRoundFunc(xy),
|
|
123
123
|
self.transformRoundFunc(yx),
|
fontTools/subset/__init__.py
CHANGED
|
@@ -27,16 +27,16 @@ from collections import Counter, defaultdict
|
|
|
27
27
|
from functools import reduce
|
|
28
28
|
from types import MethodType
|
|
29
29
|
|
|
30
|
-
__usage__ = "
|
|
30
|
+
__usage__ = "fonttools subset font-file [glyph...] [--option=value]..."
|
|
31
31
|
|
|
32
32
|
__doc__ = (
|
|
33
33
|
"""\
|
|
34
|
-
|
|
34
|
+
fonttools subset -- OpenType font subsetter and optimizer
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
It accepts any TT- or CFF-flavored OpenType (.otf or .ttf)
|
|
38
|
-
font file. The subsetted glyph set is based on the
|
|
39
|
-
or characters, and specified OpenType layout features.
|
|
36
|
+
fonttools subset is an OpenType font subsetter and optimizer, based on
|
|
37
|
+
fontTools. It accepts any TT- or CFF-flavored OpenType (.otf or .ttf)
|
|
38
|
+
or WOFF (.woff) font file. The subsetted glyph set is based on the
|
|
39
|
+
specified glyphs or characters, and specified OpenType layout features.
|
|
40
40
|
|
|
41
41
|
The tool also performs some size-reducing optimizations, aimed for using
|
|
42
42
|
subset fonts as webfonts. Individual optimizations can be enabled or
|
|
@@ -130,11 +130,11 @@ you might need to escape the question mark, like this: '--glyph-names\\?'.
|
|
|
130
130
|
|
|
131
131
|
Examples::
|
|
132
132
|
|
|
133
|
-
$
|
|
133
|
+
$ fonttools subset --glyph-names?
|
|
134
134
|
Current setting for 'glyph-names' is: False
|
|
135
|
-
$
|
|
135
|
+
$ fonttools subset --name-IDs=?
|
|
136
136
|
Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6]
|
|
137
|
-
$
|
|
137
|
+
$ fonttools subset --hinting? --no-hinting --hinting?
|
|
138
138
|
Current setting for 'hinting' is: True
|
|
139
139
|
Current setting for 'hinting' is: False
|
|
140
140
|
|
|
@@ -445,7 +445,7 @@ Example
|
|
|
445
445
|
Produce a subset containing the characters ' !"#$%' without performing
|
|
446
446
|
size-reducing optimizations::
|
|
447
447
|
|
|
448
|
-
$
|
|
448
|
+
$ fonttools subset font.ttf --unicodes="U+0020-0025" \\
|
|
449
449
|
--layout-features=* --glyph-names --symbol-cmap --legacy-cmap \\
|
|
450
450
|
--notdef-glyph --notdef-outline --recommended-glyphs \\
|
|
451
451
|
--name-IDs=* --name-legacy --name-languages=*
|
|
@@ -3768,7 +3768,7 @@ def parse_glyphs(s):
|
|
|
3768
3768
|
|
|
3769
3769
|
def usage():
|
|
3770
3770
|
print("usage:", __usage__, file=sys.stderr)
|
|
3771
|
-
print("Try
|
|
3771
|
+
print("Try fonttools subset --help for more information.\n", file=sys.stderr)
|
|
3772
3772
|
|
|
3773
3773
|
|
|
3774
3774
|
@timer("make one with everything (TOTAL TIME)")
|
fontTools/ttLib/sfnt.py
CHANGED
|
@@ -375,10 +375,9 @@ class SFNTWriter(object):
|
|
|
375
375
|
|
|
376
376
|
def _calcMasterChecksum(self, directory):
|
|
377
377
|
# calculate checkSumAdjustment
|
|
378
|
-
tags = list(self.tables.keys())
|
|
379
378
|
checksums = []
|
|
380
|
-
for
|
|
381
|
-
checksums.append(self.tables[
|
|
379
|
+
for tag in self.tables.keys():
|
|
380
|
+
checksums.append(self.tables[tag].checkSum)
|
|
382
381
|
|
|
383
382
|
if self.DirectoryEntry != SFNTDirectoryEntry:
|
|
384
383
|
# Create a SFNT directory for checksum calculation purposes
|
|
@@ -948,7 +948,7 @@ class Pass(object):
|
|
|
948
948
|
writer.newline()
|
|
949
949
|
writer.begintag("rules")
|
|
950
950
|
writer.newline()
|
|
951
|
-
for i in
|
|
951
|
+
for i, action in enumerate(self.actions):
|
|
952
952
|
writer.begintag(
|
|
953
953
|
"rule",
|
|
954
954
|
index=i,
|
|
@@ -958,7 +958,7 @@ class Pass(object):
|
|
|
958
958
|
writer.newline()
|
|
959
959
|
if len(self.ruleConstraints[i]):
|
|
960
960
|
writecode("constraint", writer, self.ruleConstraints[i])
|
|
961
|
-
writecode("action", writer,
|
|
961
|
+
writecode("action", writer, action)
|
|
962
962
|
writer.endtag("rule")
|
|
963
963
|
writer.newline()
|
|
964
964
|
writer.endtag("rules")
|
|
@@ -91,12 +91,11 @@ class table_T_S_I__1(LogMixin, DefaultTable.DefaultTable):
|
|
|
91
91
|
glyphNames = ttFont.getGlyphOrder()
|
|
92
92
|
|
|
93
93
|
indices = []
|
|
94
|
-
for i in
|
|
94
|
+
for i, name in enumerate(glyphNames):
|
|
95
95
|
if len(data) % 2:
|
|
96
96
|
data = (
|
|
97
97
|
data + b"\015"
|
|
98
98
|
) # align on 2-byte boundaries, fill with return chars. Yum.
|
|
99
|
-
name = glyphNames[i]
|
|
100
99
|
if name in self.glyphPrograms:
|
|
101
100
|
text = tobytes(self.glyphPrograms[name], encoding="utf-8")
|
|
102
101
|
else:
|
|
@@ -108,13 +107,11 @@ class table_T_S_I__1(LogMixin, DefaultTable.DefaultTable):
|
|
|
108
107
|
data = data + text
|
|
109
108
|
|
|
110
109
|
extra_indices = []
|
|
111
|
-
|
|
112
|
-
for i in range(len(codes)):
|
|
110
|
+
for code, name in sorted(self.extras.items()):
|
|
113
111
|
if len(data) % 2:
|
|
114
112
|
data = (
|
|
115
113
|
data + b"\015"
|
|
116
114
|
) # align on 2-byte boundaries, fill with return chars.
|
|
117
|
-
code, name = codes[i]
|
|
118
115
|
if name in self.extraPrograms:
|
|
119
116
|
text = tobytes(self.extraPrograms[name], encoding="utf-8")
|
|
120
117
|
else:
|
|
@@ -38,8 +38,8 @@ class table_T_S_I__5(DefaultTable.DefaultTable):
|
|
|
38
38
|
def compile(self, ttFont):
|
|
39
39
|
glyphNames = ttFont.getGlyphOrder()
|
|
40
40
|
a = array.array("H")
|
|
41
|
-
for
|
|
42
|
-
a.append(self.glyphGrouping.get(
|
|
41
|
+
for glyphName in glyphNames:
|
|
42
|
+
a.append(self.glyphGrouping.get(glyphName, 0))
|
|
43
43
|
if sys.byteorder != "big":
|
|
44
44
|
a.byteswap()
|
|
45
45
|
return a.tobytes()
|
|
@@ -398,7 +398,7 @@ class cmap_format_0(CmapSubtable):
|
|
|
398
398
|
assert 262 == self.length, "Format 0 cmap subtable not 262 bytes"
|
|
399
399
|
gids = array.array("B")
|
|
400
400
|
gids.frombytes(self.data)
|
|
401
|
-
charCodes =
|
|
401
|
+
charCodes = range(len(gids))
|
|
402
402
|
self.cmap = _make_map(self.ttFont, charCodes, gids)
|
|
403
403
|
|
|
404
404
|
def compile(self, ttFont):
|
fontTools/ttLib/tables/_c_v_t.py
CHANGED
|
@@ -29,8 +29,7 @@ class table__c_v_t(DefaultTable.DefaultTable):
|
|
|
29
29
|
return values.tobytes()
|
|
30
30
|
|
|
31
31
|
def toXML(self, writer, ttFont):
|
|
32
|
-
for i in
|
|
33
|
-
value = self.values[i]
|
|
32
|
+
for i, value in enumerate(self.values):
|
|
34
33
|
writer.simpletag("cv", value=value, index=i)
|
|
35
34
|
writer.newline()
|
|
36
35
|
|