vfbLib 0.8.0__py3-none-any.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.
- vfbLib/__init__.py +1 -0
- vfbLib/cmdline.py +214 -0
- vfbLib/compilers/__init__.py +0 -0
- vfbLib/compilers/base.py +156 -0
- vfbLib/compilers/binary.py +17 -0
- vfbLib/compilers/glyph.py +239 -0
- vfbLib/compilers/header.py +41 -0
- vfbLib/compilers/numeric.py +14 -0
- vfbLib/compilers/text.py +23 -0
- vfbLib/compilers/value.py +48 -0
- vfbLib/constants.py +213 -0
- vfbLib/cu2qu.py +97 -0
- vfbLib/diff.py +61 -0
- vfbLib/helpers.py +15 -0
- vfbLib/parsers/__init__.py +0 -0
- vfbLib/parsers/base.py +294 -0
- vfbLib/parsers/binary.py +16 -0
- vfbLib/parsers/bitmap.py +104 -0
- vfbLib/parsers/cmap.py +29 -0
- vfbLib/parsers/fl3.py +18 -0
- vfbLib/parsers/glyph.py +478 -0
- vfbLib/parsers/guides.py +75 -0
- vfbLib/parsers/header.py +53 -0
- vfbLib/parsers/mm.py +65 -0
- vfbLib/parsers/numeric.py +115 -0
- vfbLib/parsers/options.py +65 -0
- vfbLib/parsers/pclt.py +57 -0
- vfbLib/parsers/ps.py +43 -0
- vfbLib/parsers/text.py +68 -0
- vfbLib/parsers/truetype.py +262 -0
- vfbLib/parsers/value.py +49 -0
- vfbLib/templates/__init__.py +0 -0
- vfbLib/templates/glyph.py +7 -0
- vfbLib/truetype.py +51 -0
- vfbLib/tth.py +286 -0
- vfbLib/typing.py +135 -0
- vfbLib/ufo/__init__.py +0 -0
- vfbLib/ufo/builder.py +847 -0
- vfbLib/ufo/designspace.py +39 -0
- vfbLib/ufo/features.py +32 -0
- vfbLib/ufo/glyph.py +147 -0
- vfbLib/ufo/groups.py +147 -0
- vfbLib/ufo/guides.py +73 -0
- vfbLib/ufo/info.py +302 -0
- vfbLib/ufo/kerning.py +99 -0
- vfbLib/ufo/paths.py +312 -0
- vfbLib/ufo/pshints.py +200 -0
- vfbLib/ufo/time.py +25 -0
- vfbLib/ufo/tth.py +190 -0
- vfbLib/ufo/typing.py +84 -0
- vfbLib/ufo/vfb2ufo.py +73 -0
- vfbLib/value.py +61 -0
- vfbLib/version.py +2 -0
- vfbLib/vfb/__init__.py +0 -0
- vfbLib/vfb/entry.py +283 -0
- vfbLib/vfb/glyph.py +234 -0
- vfbLib/vfb/header.py +60 -0
- vfbLib/vfb/info.py +22 -0
- vfbLib/vfb/pens.py +127 -0
- vfbLib/vfb/vfb.py +265 -0
- vfbLib-0.8.0.dist-info/LICENSE +674 -0
- vfbLib-0.8.0.dist-info/METADATA +182 -0
- vfbLib-0.8.0.dist-info/RECORD +66 -0
- vfbLib-0.8.0.dist-info/WHEEL +5 -0
- vfbLib-0.8.0.dist-info/entry_points.txt +6 -0
- vfbLib-0.8.0.dist-info/top_level.txt +1 -0
vfbLib/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
GLYPH_CONSTANT = (1, 9, 7, 1)
|
vfbLib/cmdline.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import codecs
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from argparse import ArgumentParser
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from vfbLib.ufo.builder import VfbToUfoBuilder
|
|
8
|
+
from vfbLib.version import build_date
|
|
9
|
+
from vfbLib.vfb.vfb import Vfb
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def vfb2json():
|
|
15
|
+
parser = ArgumentParser(
|
|
16
|
+
description=(
|
|
17
|
+
f"VFB2JSON Converter\nCopyright (c) 2024 by LucasFonts\nBuild {build_date}"
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-d",
|
|
22
|
+
"--no-decompile",
|
|
23
|
+
action="store_true",
|
|
24
|
+
default=False,
|
|
25
|
+
help="don't decompile data, output binary in JSON",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--header",
|
|
29
|
+
action="store_true",
|
|
30
|
+
default=False,
|
|
31
|
+
help="only read the VFB header, not the actual data",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"-m",
|
|
35
|
+
"--minimal",
|
|
36
|
+
action="store_true",
|
|
37
|
+
default=False,
|
|
38
|
+
help="parse only minimal amount of data",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"-p",
|
|
42
|
+
"--path",
|
|
43
|
+
type=str,
|
|
44
|
+
nargs=1,
|
|
45
|
+
help="output folder",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"-u",
|
|
49
|
+
"--unicode-strings",
|
|
50
|
+
action="store_true",
|
|
51
|
+
default=False,
|
|
52
|
+
help="interpret name table strings as Unicode instead of Windows-1252",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"inputpath",
|
|
56
|
+
type=str,
|
|
57
|
+
nargs=1,
|
|
58
|
+
help="input file path (.vfb)",
|
|
59
|
+
)
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
if args:
|
|
62
|
+
vfb_path = Path(args.inputpath[0])
|
|
63
|
+
print(parser.description)
|
|
64
|
+
print(f"Reading file {vfb_path} ...")
|
|
65
|
+
vfb = Vfb(
|
|
66
|
+
vfb_path,
|
|
67
|
+
only_header=args.header,
|
|
68
|
+
minimal=args.minimal,
|
|
69
|
+
unicode_strings=args.unicode_strings,
|
|
70
|
+
)
|
|
71
|
+
if not args.no_decompile:
|
|
72
|
+
vfb.decompile()
|
|
73
|
+
suffix = ".vfb.json"
|
|
74
|
+
if args.path:
|
|
75
|
+
out_path = (Path(args.path[0]) / vfb_path.name).with_suffix(suffix)
|
|
76
|
+
else:
|
|
77
|
+
out_path = vfb_path.with_suffix(suffix)
|
|
78
|
+
with codecs.open(str(out_path), "wb", "utf-8") as f:
|
|
79
|
+
json.dump(vfb.as_dict(), f, ensure_ascii=False, indent=4)
|
|
80
|
+
else:
|
|
81
|
+
parser.print_help()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def vfb2ufo():
|
|
85
|
+
parser = ArgumentParser(
|
|
86
|
+
description=(
|
|
87
|
+
f"VFB3UFO Converter\nCopyright (c) 2024 by LucasFonts\nBuild {build_date}"
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"-p",
|
|
92
|
+
"--path",
|
|
93
|
+
type=str,
|
|
94
|
+
nargs=1,
|
|
95
|
+
help="output folder",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"-fo",
|
|
99
|
+
"--force-overwrite",
|
|
100
|
+
action="store_true",
|
|
101
|
+
default=False,
|
|
102
|
+
help="force overwrite",
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"-g",
|
|
106
|
+
"--keep-groups",
|
|
107
|
+
action="store_true",
|
|
108
|
+
default=False,
|
|
109
|
+
help="don't move non-kerning groups from groups.plist to feature code",
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"-k",
|
|
113
|
+
"--add-kerning-groups",
|
|
114
|
+
action="store_true",
|
|
115
|
+
default=False,
|
|
116
|
+
help="add kerning groups to feature code",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"-ttx",
|
|
120
|
+
"--ttx",
|
|
121
|
+
action="store_true",
|
|
122
|
+
default=False,
|
|
123
|
+
help="convert binary OpenType Layout data using TTX-like format",
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"-64",
|
|
127
|
+
"--base64",
|
|
128
|
+
action="store_true",
|
|
129
|
+
default=False,
|
|
130
|
+
help="write GLIF lib 'data' section using base64 (recommended)",
|
|
131
|
+
)
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"-s",
|
|
134
|
+
"--silent",
|
|
135
|
+
action="store_true",
|
|
136
|
+
default=False,
|
|
137
|
+
help="no display (silent mode)",
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"-nops",
|
|
141
|
+
"--no-postscript-hints",
|
|
142
|
+
action="store_true",
|
|
143
|
+
default=False,
|
|
144
|
+
help="Don't output PostScript hinting",
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"-z",
|
|
148
|
+
"--zip",
|
|
149
|
+
action="store_true",
|
|
150
|
+
default=False,
|
|
151
|
+
help="write UFOZ (compressed UFO)",
|
|
152
|
+
)
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"inputpath",
|
|
155
|
+
type=str,
|
|
156
|
+
nargs=1,
|
|
157
|
+
help="input file path (.vfb)",
|
|
158
|
+
)
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"outputpath",
|
|
161
|
+
type=str,
|
|
162
|
+
nargs="?",
|
|
163
|
+
help="output file path (.ufo[z])",
|
|
164
|
+
)
|
|
165
|
+
parser.add_argument(
|
|
166
|
+
"-m",
|
|
167
|
+
"--minimal",
|
|
168
|
+
action="store_true",
|
|
169
|
+
default=False,
|
|
170
|
+
help="parse only minimal amount of data, drop missing glyphs from groups, etc.",
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
"-u",
|
|
174
|
+
"--unicode-strings",
|
|
175
|
+
action="store_true",
|
|
176
|
+
default=False,
|
|
177
|
+
help="interpret name table strings as Unicode instead of Windows-1252",
|
|
178
|
+
)
|
|
179
|
+
args = parser.parse_args()
|
|
180
|
+
if args:
|
|
181
|
+
vfb_path = Path(args.inputpath[0])
|
|
182
|
+
if not args.silent:
|
|
183
|
+
print(parser.description)
|
|
184
|
+
print(f"Reading file {vfb_path} ...")
|
|
185
|
+
vfb = Vfb(
|
|
186
|
+
vfb_path,
|
|
187
|
+
minimal=args.minimal,
|
|
188
|
+
drop_keys={"Encoding", "Encoding Mac"},
|
|
189
|
+
unicode_strings=args.unicode_strings,
|
|
190
|
+
)
|
|
191
|
+
suffix = ".ufo"
|
|
192
|
+
if args.zip:
|
|
193
|
+
suffix += "z"
|
|
194
|
+
if args.path:
|
|
195
|
+
out_path = (Path(args.path[0]) / vfb_path.name).with_suffix(suffix)
|
|
196
|
+
else:
|
|
197
|
+
out_path = vfb_path.with_suffix(suffix)
|
|
198
|
+
vfb.decompile()
|
|
199
|
+
builder = VfbToUfoBuilder(
|
|
200
|
+
vfb,
|
|
201
|
+
minimal=args.minimal,
|
|
202
|
+
base64=args.base64,
|
|
203
|
+
pshints=not args.no_postscript_hints,
|
|
204
|
+
add_kerning_groups=args.add_kerning_groups,
|
|
205
|
+
move_groups=not args.keep_groups,
|
|
206
|
+
)
|
|
207
|
+
builder.write(
|
|
208
|
+
out_path,
|
|
209
|
+
overwrite=args.force_overwrite,
|
|
210
|
+
silent=args.silent,
|
|
211
|
+
ufoz=args.zip,
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
parser.print_help()
|
|
File without changes
|
vfbLib/compilers/base.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from struct import pack
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from fontTools.misc.textTools import hexStr
|
|
8
|
+
|
|
9
|
+
from vfbLib.compilers.value import write_value, write_value_long
|
|
10
|
+
from vfbLib.helpers import uint8, uint16 # , uint32
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from io import BufferedWriter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Compilers for VFB entries
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StreamWriter:
|
|
20
|
+
"""
|
|
21
|
+
Base compiler class that writes values to the output stream.
|
|
22
|
+
This is the parent class for the general BaseCompiler, from which all other
|
|
23
|
+
compilers inherit, but it may be subclassed directly if more flexibility is needed.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self.encoding = "cp1252"
|
|
28
|
+
self.stream: BufferedWriter | BytesIO = BytesIO()
|
|
29
|
+
|
|
30
|
+
def write_bytes(self, value: bytes) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Write binary data to the stream.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
value (bytes): The data.
|
|
36
|
+
"""
|
|
37
|
+
self.stream.write(value)
|
|
38
|
+
|
|
39
|
+
def write_double(self, value: float) -> None:
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
def write_doubles(self, values: list[float]) -> None:
|
|
43
|
+
"""Write several doubles to the stream.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
values (list[float]): _description_
|
|
47
|
+
"""
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
def write_float(self, value: float, fmt: str = "d") -> None:
|
|
51
|
+
"""
|
|
52
|
+
Write a float value to the stream.
|
|
53
|
+
"""
|
|
54
|
+
encoded = pack(fmt, value)
|
|
55
|
+
self.stream.write(encoded)
|
|
56
|
+
|
|
57
|
+
def write_floats(self, values: list[float]) -> None:
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
def write_int16(self, value: int) -> None:
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def write_int32(self, value: int) -> None:
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
def write_str(self, value: str, pad: int = 0) -> None:
|
|
67
|
+
# XXX: Pad with 0 bytes to given length
|
|
68
|
+
self.stream.write(value.encode(self.encoding))
|
|
69
|
+
|
|
70
|
+
def write_uint8(self, value: int) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Write a uint8 value to the stream.
|
|
73
|
+
"""
|
|
74
|
+
self.stream.write(value.to_bytes(uint8, byteorder="little", signed=False))
|
|
75
|
+
|
|
76
|
+
def write_uint16(self, value: int) -> None:
|
|
77
|
+
self.stream.write(value.to_bytes(uint16, byteorder="little", signed=False))
|
|
78
|
+
|
|
79
|
+
def write_uint32(self, value: int) -> None:
|
|
80
|
+
raise NotImplementedError
|
|
81
|
+
|
|
82
|
+
def write_value(self, value: int, shortest=True) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Encode and write an int value to the stream. Optionally don't apply the length
|
|
85
|
+
encoding optimization.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
value (int): The value to write to the stream.
|
|
89
|
+
shortest (bool, optional): Whether to write the shortest possible
|
|
90
|
+
representation. Defaults to True.
|
|
91
|
+
"""
|
|
92
|
+
if shortest:
|
|
93
|
+
write_value(value, self.stream)
|
|
94
|
+
else:
|
|
95
|
+
write_value_long(value, self.stream)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class BaseCompiler(StreamWriter):
|
|
99
|
+
"""
|
|
100
|
+
Base class to compile vfb data.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def compile(self, data: Any, master_count: int = 0) -> bytes:
|
|
104
|
+
"""
|
|
105
|
+
Compile the JSON-like main data structure and return the compiled binary data.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
data (Any): The main data structure.
|
|
109
|
+
master_count (int, optional): The number of masters. Defaults to 0.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
bytes: The compiled binary data.
|
|
113
|
+
"""
|
|
114
|
+
self.master_count = master_count
|
|
115
|
+
self.stream = BytesIO()
|
|
116
|
+
self._compile(data)
|
|
117
|
+
return self.stream.getvalue()
|
|
118
|
+
|
|
119
|
+
def compile_hex(self, data: Any, master_count: int = 0) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Compile the data given into a hex string format, e.g. "8c 8d 89 8b". Used for
|
|
122
|
+
testing.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
data (Any): The input data
|
|
126
|
+
master_count (int, optional): Number of masters. Defaults to 0.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
str: The hex string
|
|
130
|
+
"""
|
|
131
|
+
b = self.compile(data, master_count)
|
|
132
|
+
return hexStr(b)
|
|
133
|
+
|
|
134
|
+
def _compile(self, data: Any) -> None:
|
|
135
|
+
raise NotImplementedError
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def merge(cls, masters_data: list[Any], data: Any) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Merge the data of additional masters into the main data structure. This operates
|
|
141
|
+
on the uncompiled JSON-like data structure.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
masters_data (List[Any]): The additional masters data as a list with one
|
|
145
|
+
entry per master.
|
|
146
|
+
data (Any): The main data structure.
|
|
147
|
+
"""
|
|
148
|
+
# Must be implemented for compilers that need it, e.g. the GlyphCompiler.
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class GlyphEncodingCompiler(BaseCompiler):
|
|
153
|
+
def _compile(self, data: Any) -> None:
|
|
154
|
+
gid, name = data
|
|
155
|
+
self.write_uint16(gid)
|
|
156
|
+
self.write_str(name) # XXX: Does it have to be cp1252?
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fontTools.misc.textTools import deHexStr
|
|
6
|
+
|
|
7
|
+
from vfbLib.compilers.base import BaseCompiler
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BinaryTableCompiler(BaseCompiler):
|
|
11
|
+
"""
|
|
12
|
+
A compiler that compiles binary table data.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def _compile(self, data: Any) -> None:
|
|
16
|
+
self.write_str(data["tag"]) # FIXME: Add padding here?
|
|
17
|
+
self.stream.write(deHexStr(data["data"]))
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from math import radians, tan
|
|
4
|
+
from struct import pack
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from vfbLib import GLYPH_CONSTANT
|
|
8
|
+
from vfbLib.compilers.base import BaseCompiler, StreamWriter
|
|
9
|
+
from vfbLib.parsers.glyph import PathCommand
|
|
10
|
+
from vfbLib.truetype import TT_COMMAND_CONSTANTS, TT_COMMANDS
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GlyphCompiler(BaseCompiler):
|
|
16
|
+
@classmethod
|
|
17
|
+
def merge(cls, masters_data: list[Any], data: Any) -> None:
|
|
18
|
+
num_masters = len(masters_data)
|
|
19
|
+
if num_masters < 2:
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
for m in range(1, num_masters):
|
|
23
|
+
master_data = masters_data[m]
|
|
24
|
+
# See if there is any data to merge
|
|
25
|
+
if "nodes" in master_data:
|
|
26
|
+
assert "nodes" in data
|
|
27
|
+
for i, tgt in enumerate(data["nodes"]):
|
|
28
|
+
src = master_data["nodes"][i]
|
|
29
|
+
for key in ("type", "flags"):
|
|
30
|
+
assert src[key] == tgt[key]
|
|
31
|
+
tgt["points"][m] = src["points"][m]
|
|
32
|
+
|
|
33
|
+
if "components" in master_data:
|
|
34
|
+
assert "components" in data
|
|
35
|
+
for i, tgt in enumerate(data["components"]):
|
|
36
|
+
src = master_data["components"][i]
|
|
37
|
+
for key in ("gid",):
|
|
38
|
+
assert src[key] == tgt[key]
|
|
39
|
+
tgt["offsetX"][m] = src["offsetX"][m]
|
|
40
|
+
tgt["offsetY"][m] = src["offsetY"][m]
|
|
41
|
+
tgt["scaleX"][m] = src["scaleX"][m]
|
|
42
|
+
tgt["scaleY"][m] = src["scaleY"][m]
|
|
43
|
+
|
|
44
|
+
def _compile_binary(self, data):
|
|
45
|
+
# Imported binary data 8-)
|
|
46
|
+
if not (imported := data.get("imported")): # noqa: F841
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
logger.warning("Compiling imported binary data is not supported.")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
self.write_uint8(9)
|
|
53
|
+
|
|
54
|
+
def _compile_components(self, data):
|
|
55
|
+
# Components
|
|
56
|
+
if not (components := data.get("components")):
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
self.write_uint8(5)
|
|
60
|
+
self.write_value(len(components))
|
|
61
|
+
for component in components:
|
|
62
|
+
self.write_value(component["gid"])
|
|
63
|
+
for i in range(self.num_masters):
|
|
64
|
+
self.write_value(component["offsetX"][i])
|
|
65
|
+
self.write_value(component["offsetY"][i])
|
|
66
|
+
self.write_float(component["scaleX"][i])
|
|
67
|
+
self.write_float(component["scaleY"][i])
|
|
68
|
+
|
|
69
|
+
def _compile_glyph_name(self, data):
|
|
70
|
+
# Glyph name
|
|
71
|
+
if not (name := data.get("name")):
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
glyph_name = name.encode("cp1252")
|
|
75
|
+
glyph_name_length = len(glyph_name)
|
|
76
|
+
self.write_uint8(1)
|
|
77
|
+
self.write_value(glyph_name_length)
|
|
78
|
+
self.write_bytes(glyph_name)
|
|
79
|
+
logger.debug(f"Compiling glyph '{name}'")
|
|
80
|
+
|
|
81
|
+
def _compile_guides(self, data):
|
|
82
|
+
# Guidelines
|
|
83
|
+
# TODO: Reuse for global guides
|
|
84
|
+
if not (guides := data.get("guides")):
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
self.write_uint8(4)
|
|
88
|
+
for direction in ("h", "v"):
|
|
89
|
+
direction_guides = guides.get(direction)
|
|
90
|
+
if direction_guides is None:
|
|
91
|
+
self.write_value(0)
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
self.write_value(len(direction_guides[0])) # first master
|
|
95
|
+
for m in range(self.num_masters):
|
|
96
|
+
for guide in direction_guides[m]:
|
|
97
|
+
pos = guide["pos"]
|
|
98
|
+
angle = round(tan(radians(guide["angle"])) * 10000)
|
|
99
|
+
self.write_value(pos)
|
|
100
|
+
self.write_value(angle)
|
|
101
|
+
|
|
102
|
+
def _compile_hints(self, data):
|
|
103
|
+
# PostScript hints
|
|
104
|
+
# To minimize diffs, we always write out hint, but it is not necessary
|
|
105
|
+
hints = data.get("hints", {})
|
|
106
|
+
# We could skip empty hinting:
|
|
107
|
+
# if not (hints := data.get("hints")):
|
|
108
|
+
# return
|
|
109
|
+
|
|
110
|
+
self.write_uint8(3)
|
|
111
|
+
for direction in ("h", "v"):
|
|
112
|
+
if direction_hints := hints.get(direction):
|
|
113
|
+
self.write_value(len(direction_hints))
|
|
114
|
+
for mm_hint in direction_hints:
|
|
115
|
+
for i in range(self.num_masters):
|
|
116
|
+
hint = mm_hint[i]
|
|
117
|
+
self.write_value(hint["pos"])
|
|
118
|
+
self.write_value(hint["width"])
|
|
119
|
+
else:
|
|
120
|
+
self.write_value(0)
|
|
121
|
+
|
|
122
|
+
if not (hintmasks := hints.get("hintmasks")): # noqa: F841
|
|
123
|
+
self.write_value(0)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
self.write_value(len(hintmasks))
|
|
127
|
+
for k, v in hintmasks:
|
|
128
|
+
key = {
|
|
129
|
+
"h": 0x01,
|
|
130
|
+
"v": 0x02,
|
|
131
|
+
"r": 0xFF,
|
|
132
|
+
}[k]
|
|
133
|
+
self.write_uint8(key)
|
|
134
|
+
self.write_value(v)
|
|
135
|
+
|
|
136
|
+
def _compile_instructions(self, data):
|
|
137
|
+
# TrueType instructions
|
|
138
|
+
if not (tth := data.get("tth")):
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
self.write_uint8(0x0A)
|
|
142
|
+
instructions = InstructionsCompiler().compile(tth)
|
|
143
|
+
self.write_value(len(instructions))
|
|
144
|
+
self.stream.write(instructions)
|
|
145
|
+
|
|
146
|
+
def _compile_kerning(self, data):
|
|
147
|
+
# Kerning
|
|
148
|
+
if not (kerning := data.get("kerning")):
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
self.write_uint8(6)
|
|
152
|
+
self.write_value(len(kerning))
|
|
153
|
+
for gid, values in kerning.items():
|
|
154
|
+
self.write_value(gid)
|
|
155
|
+
for value in values:
|
|
156
|
+
self.write_value(value)
|
|
157
|
+
|
|
158
|
+
def _compile_metrics(self, data):
|
|
159
|
+
# Metrics
|
|
160
|
+
if not (metrics := data.get("metrics")):
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
self.write_uint8(2)
|
|
164
|
+
for i in range(self.num_masters):
|
|
165
|
+
x, y = metrics[i]
|
|
166
|
+
self.write_value(x)
|
|
167
|
+
self.write_value(y)
|
|
168
|
+
|
|
169
|
+
def _compile_outlines(self, data):
|
|
170
|
+
# Outlines
|
|
171
|
+
# A minimal outlines structure is always written:
|
|
172
|
+
self.write_uint8(8)
|
|
173
|
+
self.write_value(self.num_masters) # Number of masters
|
|
174
|
+
|
|
175
|
+
if not (nodes := data.get("nodes")):
|
|
176
|
+
# 0 nodes with 0 values
|
|
177
|
+
self.write_value(0)
|
|
178
|
+
self.write_value(0)
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
outlines, num_values = OutlinesCompiler().compile(nodes, self.num_masters)
|
|
182
|
+
self.write_value(num_values)
|
|
183
|
+
self.stream.write(outlines)
|
|
184
|
+
|
|
185
|
+
def _compile(self, data: Any) -> None:
|
|
186
|
+
# Constants?
|
|
187
|
+
self.write_bytes(pack("<4B", *GLYPH_CONSTANT))
|
|
188
|
+
self.num_masters = data["num_masters"]
|
|
189
|
+
|
|
190
|
+
self._compile_glyph_name(data)
|
|
191
|
+
self._compile_outlines(data)
|
|
192
|
+
self._compile_metrics(data)
|
|
193
|
+
self._compile_hints(data)
|
|
194
|
+
self._compile_guides(data)
|
|
195
|
+
self._compile_components(data)
|
|
196
|
+
self._compile_kerning(data)
|
|
197
|
+
self._compile_binary(data)
|
|
198
|
+
self._compile_instructions(data)
|
|
199
|
+
self.write_uint8(15) # End of glyph
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class InstructionsCompiler(BaseCompiler):
|
|
203
|
+
def _compile(self, data: Any) -> None:
|
|
204
|
+
self.write_value(len(data))
|
|
205
|
+
for cmd in data:
|
|
206
|
+
command_id = TT_COMMAND_CONSTANTS[cmd["cmd"]]
|
|
207
|
+
self.write_uint8(command_id)
|
|
208
|
+
params = cmd["params"]
|
|
209
|
+
for param_name in TT_COMMANDS[command_id]["params"]:
|
|
210
|
+
self.write_value(params[param_name])
|
|
211
|
+
for _ in range(3):
|
|
212
|
+
self.write_value(0)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class OutlinesCompiler(StreamWriter):
|
|
216
|
+
def compile(self, data: Any, num_masters: int) -> tuple[bytes, int]:
|
|
217
|
+
self.num_masters = num_masters
|
|
218
|
+
self.stream = BytesIO()
|
|
219
|
+
num_values = self._compile(data)
|
|
220
|
+
return self.stream.getvalue(), num_values
|
|
221
|
+
|
|
222
|
+
def _compile(self, data: Any) -> int:
|
|
223
|
+
self.write_value(len(data)) # Number of nodes, may be 0
|
|
224
|
+
num_values = 0
|
|
225
|
+
ref_coords = [[0, 0] for _ in range(self.num_masters)]
|
|
226
|
+
for node in data:
|
|
227
|
+
type_flags = node.get("flags", 0) * 16 + PathCommand[node["type"]].value
|
|
228
|
+
self.write_uint8(type_flags)
|
|
229
|
+
num_values += 1
|
|
230
|
+
for j in range(len(node["points"][0])):
|
|
231
|
+
for i in range(self.num_masters):
|
|
232
|
+
x, y = node["points"][i][j]
|
|
233
|
+
refx, refy = ref_coords[i]
|
|
234
|
+
# Coordinates are written relatively to the previous coords
|
|
235
|
+
self.write_value(x - refx)
|
|
236
|
+
self.write_value(y - refy)
|
|
237
|
+
num_values += 2
|
|
238
|
+
ref_coords[i] = [x, y]
|
|
239
|
+
return 2 * num_values
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fontTools.misc.textTools import deHexStr
|
|
6
|
+
|
|
7
|
+
from vfbLib.compilers.base import StreamWriter
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VfbHeaderCompiler(StreamWriter):
|
|
13
|
+
encoding = "cp1252"
|
|
14
|
+
|
|
15
|
+
def compile(self, data: Any) -> bytes:
|
|
16
|
+
self.stream = BytesIO()
|
|
17
|
+
self._compile(data)
|
|
18
|
+
return self.stream.getvalue()
|
|
19
|
+
|
|
20
|
+
def _compile(self, data: Any) -> None:
|
|
21
|
+
self.write_uint8(data["header0"])
|
|
22
|
+
self.write_str(data["filetype"])
|
|
23
|
+
self.write_uint16(data["header1"])
|
|
24
|
+
self.write_uint16(data["header2"])
|
|
25
|
+
self.write_bytes(deHexStr(data["reserved"]))
|
|
26
|
+
self.write_uint16(data["header3"])
|
|
27
|
+
self.write_uint16(data["header4"])
|
|
28
|
+
self.write_uint16(data["header5"])
|
|
29
|
+
self.write_uint16(data["header6"])
|
|
30
|
+
self.write_uint16(data["header7"])
|
|
31
|
+
# > FL3 additions
|
|
32
|
+
self.write_uint16(data["header8"])
|
|
33
|
+
for i in range(9, 12):
|
|
34
|
+
d = data[f"header{i}"]
|
|
35
|
+
assert len(d) == 1
|
|
36
|
+
k, v = tuple(d.items())[0]
|
|
37
|
+
self.write_uint8(int(k))
|
|
38
|
+
self.write_value(v)
|
|
39
|
+
self.write_uint8(data["header12"])
|
|
40
|
+
self.write_uint16(data["header13"])
|
|
41
|
+
self.write_uint16(data["header14"])
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from vfbLib.compilers.base import BaseCompiler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Int16Compiler(BaseCompiler):
|
|
9
|
+
"""
|
|
10
|
+
A compiler that compiles UInt16 data.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def _compile(self, data: Any) -> None:
|
|
14
|
+
self.write_uint16(data)
|