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.
Files changed (66) hide show
  1. vfbLib/__init__.py +1 -0
  2. vfbLib/cmdline.py +214 -0
  3. vfbLib/compilers/__init__.py +0 -0
  4. vfbLib/compilers/base.py +156 -0
  5. vfbLib/compilers/binary.py +17 -0
  6. vfbLib/compilers/glyph.py +239 -0
  7. vfbLib/compilers/header.py +41 -0
  8. vfbLib/compilers/numeric.py +14 -0
  9. vfbLib/compilers/text.py +23 -0
  10. vfbLib/compilers/value.py +48 -0
  11. vfbLib/constants.py +213 -0
  12. vfbLib/cu2qu.py +97 -0
  13. vfbLib/diff.py +61 -0
  14. vfbLib/helpers.py +15 -0
  15. vfbLib/parsers/__init__.py +0 -0
  16. vfbLib/parsers/base.py +294 -0
  17. vfbLib/parsers/binary.py +16 -0
  18. vfbLib/parsers/bitmap.py +104 -0
  19. vfbLib/parsers/cmap.py +29 -0
  20. vfbLib/parsers/fl3.py +18 -0
  21. vfbLib/parsers/glyph.py +478 -0
  22. vfbLib/parsers/guides.py +75 -0
  23. vfbLib/parsers/header.py +53 -0
  24. vfbLib/parsers/mm.py +65 -0
  25. vfbLib/parsers/numeric.py +115 -0
  26. vfbLib/parsers/options.py +65 -0
  27. vfbLib/parsers/pclt.py +57 -0
  28. vfbLib/parsers/ps.py +43 -0
  29. vfbLib/parsers/text.py +68 -0
  30. vfbLib/parsers/truetype.py +262 -0
  31. vfbLib/parsers/value.py +49 -0
  32. vfbLib/templates/__init__.py +0 -0
  33. vfbLib/templates/glyph.py +7 -0
  34. vfbLib/truetype.py +51 -0
  35. vfbLib/tth.py +286 -0
  36. vfbLib/typing.py +135 -0
  37. vfbLib/ufo/__init__.py +0 -0
  38. vfbLib/ufo/builder.py +847 -0
  39. vfbLib/ufo/designspace.py +39 -0
  40. vfbLib/ufo/features.py +32 -0
  41. vfbLib/ufo/glyph.py +147 -0
  42. vfbLib/ufo/groups.py +147 -0
  43. vfbLib/ufo/guides.py +73 -0
  44. vfbLib/ufo/info.py +302 -0
  45. vfbLib/ufo/kerning.py +99 -0
  46. vfbLib/ufo/paths.py +312 -0
  47. vfbLib/ufo/pshints.py +200 -0
  48. vfbLib/ufo/time.py +25 -0
  49. vfbLib/ufo/tth.py +190 -0
  50. vfbLib/ufo/typing.py +84 -0
  51. vfbLib/ufo/vfb2ufo.py +73 -0
  52. vfbLib/value.py +61 -0
  53. vfbLib/version.py +2 -0
  54. vfbLib/vfb/__init__.py +0 -0
  55. vfbLib/vfb/entry.py +283 -0
  56. vfbLib/vfb/glyph.py +234 -0
  57. vfbLib/vfb/header.py +60 -0
  58. vfbLib/vfb/info.py +22 -0
  59. vfbLib/vfb/pens.py +127 -0
  60. vfbLib/vfb/vfb.py +265 -0
  61. vfbLib-0.8.0.dist-info/LICENSE +674 -0
  62. vfbLib-0.8.0.dist-info/METADATA +182 -0
  63. vfbLib-0.8.0.dist-info/RECORD +66 -0
  64. vfbLib-0.8.0.dist-info/WHEEL +5 -0
  65. vfbLib-0.8.0.dist-info/entry_points.txt +6 -0
  66. 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
@@ -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)