Handle string input to Program.fromAssembly() (#3038)

* Add tests for fromAssembly() output (#3036)
* Handle string input to fromAssembly() (Fixes #3036)
* Fixups suggested by @anthrotype
* Add some more typing annotations
* Program.assembly always returns List[str] now
* Add annotation for bytecode
* Move code from setter to fromAssembly
* Remove property
* Fix attribute names
This commit is contained in:
Jens Kutilek 2023-03-14 14:51:16 +01:00 committed by GitHub
parent 444a349ef4
commit f1a75bacb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 42 additions and 18 deletions

View File

@ -1,8 +1,10 @@
"""ttLib.tables.ttProgram.py -- Assembler/disassembler for TrueType bytecode programs.""" """ttLib.tables.ttProgram.py -- Assembler/disassembler for TrueType bytecode programs."""
from __future__ import annotations
from fontTools.misc.textTools import num2binary, binary2num, readHex, strjoin from fontTools.misc.textTools import num2binary, binary2num, readHex, strjoin
import array import array
from io import StringIO from io import StringIO
from typing import List
import re import re
import logging import logging
@ -1064,30 +1066,35 @@ def _skipWhite(data, pos):
class Program(object): class Program(object):
def __init__(self): def __init__(self) -> None:
pass pass
def fromBytecode(self, bytecode): def fromBytecode(self, bytecode: bytes) -> None:
self.bytecode = array.array("B", bytecode) self.bytecode = array.array("B", bytecode)
if hasattr(self, "assembly"): if hasattr(self, "assembly"):
del self.assembly del self.assembly
def fromAssembly(self, assembly): def fromAssembly(self, assembly: List[str] | str) -> None:
self.assembly = assembly if isinstance(assembly, list):
self.assembly = assembly
elif isinstance(assembly, str):
self.assembly = assembly.splitlines()
else:
raise TypeError(f"expected str or List[str], got {type(assembly).__name__}")
if hasattr(self, "bytecode"): if hasattr(self, "bytecode"):
del self.bytecode del self.bytecode
def getBytecode(self): def getBytecode(self) -> bytes:
if not hasattr(self, "bytecode"): if not hasattr(self, "bytecode"):
self._assemble() self._assemble()
return self.bytecode.tobytes() return self.bytecode.tobytes()
def getAssembly(self, preserve=True): def getAssembly(self, preserve=True) -> List[str]:
if not hasattr(self, "assembly"): if not hasattr(self, "assembly"):
self._disassemble(preserve=preserve) self._disassemble(preserve=preserve)
return self.assembly return self.assembly
def toXML(self, writer, ttFont): def toXML(self, writer, ttFont) -> None:
if ( if (
not hasattr(ttFont, "disassembleInstructions") not hasattr(ttFont, "disassembleInstructions")
or ttFont.disassembleInstructions or ttFont.disassembleInstructions
@ -1128,7 +1135,7 @@ class Program(object):
i = i + 1 i = i + 1
if m: if m:
nValues = int(m.group(1)) nValues = int(m.group(1))
line = [] line: List[str] = []
j = 0 j = 0
for j in range(nValues): for j in range(nValues):
if j and not (j % 25): if j and not (j % 25):
@ -1155,7 +1162,7 @@ class Program(object):
writer.endtag("bytecode") writer.endtag("bytecode")
writer.newline() writer.newline()
def fromXML(self, name, attrs, content, ttFont): def fromXML(self, name, attrs, content, ttFont) -> None:
if name == "assembly": if name == "assembly":
self.fromAssembly(strjoin(content)) self.fromAssembly(strjoin(content))
self._assemble() self._assemble()
@ -1164,11 +1171,9 @@ class Program(object):
assert name == "bytecode" assert name == "bytecode"
self.fromBytecode(readHex(content)) self.fromBytecode(readHex(content))
def _assemble(self): def _assemble(self) -> None:
assembly = getattr(self, "assembly", []) assembly = " ".join(getattr(self, "assembly", []))
if isinstance(assembly, type([])): bytecode: List[int] = []
assembly = " ".join(assembly)
bytecode = []
push = bytecode.append push = bytecode.append
lenAssembly = len(assembly) lenAssembly = len(assembly)
pos = _skipWhite(assembly, 0) pos = _skipWhite(assembly, 0)
@ -1311,7 +1316,7 @@ class Program(object):
assert max(bytecode) < 256 and min(bytecode) >= 0 assert max(bytecode) < 256 and min(bytecode) >= 0
self.bytecode = array.array("B", bytecode) self.bytecode = array.array("B", bytecode)
def _disassemble(self, preserve=False): def _disassemble(self, preserve=False) -> None:
assembly = [] assembly = []
i = 0 i = 0
bytecode = getattr(self, "bytecode", []) bytecode = getattr(self, "bytecode", [])
@ -1376,7 +1381,7 @@ class Program(object):
i = i + 1 i = i + 1
self.assembly = assembly self.assembly = assembly
def __bool__(self): def __bool__(self) -> bool:
""" """
>>> p = Program() >>> p = Program()
>>> bool(p) >>> bool(p)
@ -1406,12 +1411,12 @@ class Program(object):
__nonzero__ = __bool__ __nonzero__ = __bool__
def __eq__(self, other): def __eq__(self, other) -> bool:
if type(self) != type(other): if type(self) != type(other):
return NotImplemented return NotImplemented
return self.__dict__ == other.__dict__ return self.__dict__ == other.__dict__
def __ne__(self, other): def __ne__(self, other) -> bool:
result = self.__eq__(other) result = self.__eq__(other)
return result if result is NotImplemented else not result return result if result is NotImplemented else not result

View File

@ -12,6 +12,13 @@ DATA_DIR = os.path.join(CURR_DIR, "data")
TTPROGRAM_TTX = os.path.join(DATA_DIR, "ttProgram.ttx") TTPROGRAM_TTX = os.path.join(DATA_DIR, "ttProgram.ttx")
# TTPROGRAM_BIN = os.path.join(DATA_DIR, "ttProgram.bin") # TTPROGRAM_BIN = os.path.join(DATA_DIR, "ttProgram.bin")
ASSEMBLY = [
"PUSH[ ]",
"0 4 3",
"INSTCTRL[ ]",
"POP[ ]",
]
BYTECODE = deHexStr( BYTECODE = deHexStr(
"403b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a" "403b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a"
"191817161514131211100f0e0d0c0b0a090807060504030201002c01b0184358456ab0" "191817161514131211100f0e0d0c0b0a090807060504030201002c01b0184358456ab0"
@ -90,6 +97,18 @@ class ProgramTest(unittest.TestCase):
assert p.assembly.pop() == "SVTCA[0]" assert p.assembly.pop() == "SVTCA[0]"
assert not bool(p) assert not bool(p)
def test_from_assembly_list(self):
p = Program()
p.fromAssembly(ASSEMBLY)
asm = p.getAssembly()
assert ASSEMBLY == asm
def test_from_assembly_str(self):
p = Program()
p.fromAssembly("\n".join(ASSEMBLY))
asm = p.getAssembly()
assert ASSEMBLY == asm
def test_roundtrip(self): def test_roundtrip(self):
p = Program() p = Program()
p.fromBytecode(BYTECODE) p.fromBytecode(BYTECODE)