Skip to content

Commit b44d9b8

Browse files
authored
Merge pull request #42 from jenskutilek/ttf-instructions-vtt
Add option to extract TrueType instructions as VTT assembly
2 parents d5fb2c1 + c01b932 commit b44d9b8

File tree

4 files changed

+958
-18
lines changed

4 files changed

+958
-18
lines changed

Lib/extractor/stream.py

Lines changed: 194 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,37 @@ class InstructionStream(object):
1212
The instruction stream.
1313
"""
1414

15-
def __init__(self, instruction_processor=None, program_bytes=b""):
15+
def __init__(self, instruction_processor=None, program_bytes=b"") -> None:
16+
self.ip = instruction_processor
1617
self.io = BytesIO(program_bytes)
18+
self._num_bytes = len(program_bytes)
1719

18-
def rewind(self):
20+
def __len__(self):
21+
return self._num_bytes
22+
23+
def __repr__(self) -> str:
1924
"""
20-
Rewind the instruction pointer to the beginning of the stream.
25+
Return the instructions from the bytecode in the current stream
26+
starting at the beginning.
2127
"""
22-
self.io.seek(0)
28+
return self.get_assembly()
29+
30+
def __str__(self) -> str:
31+
"""
32+
Return the instructions from the bytecode in the current stream
33+
starting at the beginning.
34+
"""
35+
return self.get_assembly()
36+
37+
def move_instruction_pointer(self, bytes_offset: int) -> None:
38+
"""
39+
:param bytes_offset: The offset in bytes. May be positive or negative.
40+
:type bytes_offset: int
41+
42+
Move the instruction pointer inside the current stream, relative to the
43+
current pointer position.
44+
"""
45+
self.io.seek(bytes_offset, 1) # 1 = relative to current position
2346

2447
def read_byte(self):
2548
"""
@@ -41,11 +64,29 @@ def read_word(self):
4164
return False
4265
return w, int.from_bytes(w, byteorder="big", signed=True)
4366

44-
def __repr__(self):
67+
def rewind(self) -> None:
4568
"""
46-
Print the instructions from the bytecode in the current stream starting
47-
at the beginning.
69+
Rewind the instruction pointer to the beginning of the stream.
4870
"""
71+
self.io.seek(0)
72+
73+
# Getting the assembly code
74+
75+
@property
76+
def vtt_assembly(self) -> str:
77+
"""
78+
Return the instructions from the bytecode in the current stream as VTT
79+
assembly code.
80+
"""
81+
return self.get_assembly(dialect="vtt", end="\n")
82+
83+
def get_assembly(self, dialect="ttx", end="\n") -> str:
84+
"""
85+
Return the instructions from the bytecode in the current stream as
86+
assembly code in the specified dialect, "ttx" or "vtt".
87+
"""
88+
vtt = dialect == "vtt"
89+
ttx = dialect == "ttx"
4990
self.rewind()
5091

5192
asm = ""
@@ -54,7 +95,16 @@ def __repr__(self):
5495
while True:
5596
opcode = self.io.read(1)
5697
if not opcode:
57-
return asm.strip()
98+
asm = asm.strip()
99+
if ttx:
100+
return asm
101+
elif vtt:
102+
if asm:
103+
return f"#PUSHOFF{end}" + asm.strip() + f"{end}#PUSHON"
104+
return ""
105+
else:
106+
# Unknown dialect
107+
raise NotImplementedError
58108

59109
opcode = int.from_bytes(opcode, byteorder="big", signed=False)
60110
cmd_info = streamOpcodeDict.get(opcode, None)
@@ -92,19 +142,145 @@ def __repr__(self):
92142
arg_bits = 0 # Don't output bits for push instructions
93143

94144
if arg_bits == 0:
95-
arg_bitstring = " "
145+
if ttx:
146+
arg_bitstring = " "
147+
else:
148+
arg_bitstring = ""
96149
else:
97-
arg_bitstring = num2binary(opcode - base_opcode, arg_bits)
150+
if ttx:
151+
arg_bitstring = num2binary(opcode - base_opcode, arg_bits)
152+
elif vtt:
153+
arg_bitstring = self.bitstring_to_mnemonic(
154+
cmd_name, num2binary(opcode - base_opcode, arg_bits)
155+
)
156+
else:
157+
# Unknown dialect
158+
raise NotImplementedError
98159

99-
if cmd_name in ("NPUSHB", "NPUSHW", "PUSHB", "PUSHW"):
100-
num_args = len(args)
101-
val = "value" if num_args == 1 else "values"
102-
asm += f"\n{' ' * indent}{cmd_name}[{arg_bitstring}]\t/* {num_args} {val} pushed */"
103-
else:
104-
asm += f"\n{' ' * indent}{cmd_name}[{arg_bitstring}]\t/* {name} */"
160+
if ttx:
161+
if cmd_name in ("NPUSHB", "NPUSHW", "PUSHB", "PUSHW"):
162+
num_args = len(args)
163+
val = "value" if num_args == 1 else "values"
164+
asm += (
165+
f"\n{' ' * indent}{cmd_name}[{arg_bitstring}]"
166+
f"\t/* {num_args} {val} pushed */"
167+
)
168+
else:
169+
asm += (
170+
f"\n{' ' * indent}{cmd_name}[{arg_bitstring}]"
171+
f"\t/* {name} */"
172+
)
105173

106-
if args:
107-
asm += f"\n{' ' * indent}{' '.join(args)}"
174+
if args:
175+
asm += f"\n{' ' * indent}{' '.join(args)}"
176+
177+
elif vtt:
178+
if cmd_name in ("NPUSHB", "NPUSHW", "PUSHB", "PUSHW"):
179+
# Format as generic #PUSH for VTT assembly output
180+
cmd_name = "#PUSH"
181+
asm += f"{end}{' ' * indent}{cmd_name}, {', '.join(args)}"
182+
elif cmd_name in ("JMPR", "JROF"):
183+
# Special formatting for jump instructions
184+
if cmd_name == "JPMR":
185+
args = ("*",)
186+
elif cmd_name == "JROF":
187+
args = ("*", "*")
188+
asm += f"{end}#PUSHON"
189+
asm += f"{end}{' ' * indent}{cmd_name}, {', '.join(args)}"
190+
asm += f"{end}#PUSHOFF"
191+
else:
192+
asm += (
193+
f"{end}{' ' * indent}{cmd_name}[{arg_bitstring}]"
194+
f"\t/* {name} */"
195+
)
196+
197+
else:
198+
# Unknown dialect
199+
raise NotImplementedError
108200

109201
if cmd_name in ("ELSE", "FDEF", "IF"):
110202
indent += 1
203+
204+
def bitstring_to_mnemonic(self, cmd_name: str, bitstring: str) -> str:
205+
"""
206+
Return VTT mnemonics for a bit string
207+
"""
208+
if cmd_name in ("SVTCA", "SPVTCA", "SFVTCA", "IUP"):
209+
# Direction
210+
if bitstring == "0":
211+
return "Y" # Y axis
212+
return "X" # X axis
213+
214+
elif cmd_name in ("SPVTL", "SFVTL", "SDPVTL"):
215+
# Line relation
216+
if bitstring == "0":
217+
return "r" # parallel to line
218+
return "R" # perpendicular to line
219+
220+
elif cmd_name in ("MDAP", "MIAP"):
221+
# Rounding
222+
if bitstring == "0":
223+
return "r" # do not round distance
224+
return "R" # round distance
225+
226+
elif cmd_name in ("SHP", "SHC", "SHZ"):
227+
# Reference Point Usage
228+
if bitstring == "0":
229+
return "2" # Use rp2
230+
return "1" # Use rp1
231+
232+
elif cmd_name in ("MSIRP",):
233+
# Reference Point Autoset
234+
if bitstring == "0":
235+
return "m" # Do not set rp0
236+
return "M" # Set rp0 to point number on the stack
237+
238+
elif cmd_name in ("GC", "MD"):
239+
# Outline
240+
if bitstring == "0":
241+
return "N" # Use gridfitted outline
242+
return "O" # Use original outline
243+
244+
elif cmd_name in ("ROUND", "NROUND"):
245+
# Color
246+
return self.bitstring_to_color_mnemonic(bitstring)
247+
248+
elif cmd_name in ("MDRP", "MIRP"):
249+
flags = ""
250+
251+
# Reference Point Autoset
252+
if bitstring[0] == "0":
253+
flags += "m"
254+
else:
255+
flags += "M"
256+
257+
# Minimum Distance
258+
if bitstring[1] == "0":
259+
flags += "<"
260+
else:
261+
flags += ">"
262+
263+
# Rounding
264+
if bitstring[2] == "0":
265+
flags += "r" # do not round distance
266+
else:
267+
flags += "R" # round distance
268+
269+
# Color
270+
return flags + self.bitstring_to_color_mnemonic(bitstring[3:])
271+
272+
# Unknown command
273+
raise KeyError
274+
275+
def bitstring_to_color_mnemonic(self, bitstring: str) -> str:
276+
"""
277+
Return VTT distance color mnemonics for a bit string
278+
"""
279+
if bitstring == "00":
280+
return "Gr" # Gray
281+
elif bitstring == "01":
282+
return "Bl" # Black
283+
elif bitstring == "10":
284+
return "Wh" # White
285+
# "11" is not defined
286+
raise KeyError

0 commit comments

Comments
 (0)