461 lines
14 KiB
Python
461 lines
14 KiB
Python
"""Module for reading TFM (TeX Font Metrics) files.
|
||
|
||
The TFM format is described in the TFtoPL WEB source code, whose typeset form
|
||
can be found on `CTAN <http://mirrors.ctan.org/info/knuth-pdf/texware/tftopl.pdf>`_.
|
||
|
||
>>> from fontTools.tfmLib import TFM
|
||
>>> tfm = TFM("Tests/tfmLib/data/cmr10.tfm")
|
||
>>>
|
||
>>> # Accessing an attribute gets you metadata.
|
||
>>> tfm.checksum
|
||
1274110073
|
||
>>> tfm.designsize
|
||
10.0
|
||
>>> tfm.codingscheme
|
||
'TeX text'
|
||
>>> tfm.family
|
||
'CMR'
|
||
>>> tfm.seven_bit_safe_flag
|
||
False
|
||
>>> tfm.face
|
||
234
|
||
>>> tfm.extraheader
|
||
{}
|
||
>>> tfm.fontdimens
|
||
{'SLANT': 0.0, 'SPACE': 0.33333396911621094, 'STRETCH': 0.16666698455810547, 'SHRINK': 0.11111164093017578, 'XHEIGHT': 0.4305553436279297, 'QUAD': 1.0000028610229492, 'EXTRASPACE': 0.11111164093017578}
|
||
>>> # Accessing a character gets you its metrics.
|
||
>>> # “width” is always available, other metrics are available only when
|
||
>>> # applicable. All values are relative to “designsize”.
|
||
>>> tfm.chars[ord("g")]
|
||
{'width': 0.5000019073486328, 'height': 0.4305553436279297, 'depth': 0.1944446563720703, 'italic': 0.013888359069824219}
|
||
>>> # Kerning and ligature can be accessed as well.
|
||
>>> tfm.kerning[ord("c")]
|
||
{104: -0.02777862548828125, 107: -0.02777862548828125}
|
||
>>> tfm.ligatures[ord("f")]
|
||
{105: ('LIG', 12), 102: ('LIG', 11), 108: ('LIG', 13)}
|
||
"""
|
||
|
||
from types import SimpleNamespace
|
||
|
||
from fontTools.misc.sstruct import calcsize, unpack, unpack2
|
||
|
||
SIZES_FORMAT = """
|
||
>
|
||
lf: h # length of the entire file, in words
|
||
lh: h # length of the header data, in words
|
||
bc: h # smallest character code in the font
|
||
ec: h # largest character code in the font
|
||
nw: h # number of words in the width table
|
||
nh: h # number of words in the height table
|
||
nd: h # number of words in the depth table
|
||
ni: h # number of words in the italic correction table
|
||
nl: h # number of words in the ligature/kern table
|
||
nk: h # number of words in the kern table
|
||
ne: h # number of words in the extensible character table
|
||
np: h # number of font parameter words
|
||
"""
|
||
|
||
SIZES_SIZE = calcsize(SIZES_FORMAT)
|
||
|
||
FIXED_FORMAT = "12.20F"
|
||
|
||
HEADER_FORMAT1 = f"""
|
||
>
|
||
checksum: L
|
||
designsize: {FIXED_FORMAT}
|
||
"""
|
||
|
||
HEADER_FORMAT2 = f"""
|
||
{HEADER_FORMAT1}
|
||
codingscheme: 40p
|
||
"""
|
||
|
||
HEADER_FORMAT3 = f"""
|
||
{HEADER_FORMAT2}
|
||
family: 20p
|
||
"""
|
||
|
||
HEADER_FORMAT4 = f"""
|
||
{HEADER_FORMAT3}
|
||
seven_bit_safe_flag: ?
|
||
ignored: x
|
||
ignored: x
|
||
face: B
|
||
"""
|
||
|
||
HEADER_SIZE1 = calcsize(HEADER_FORMAT1)
|
||
HEADER_SIZE2 = calcsize(HEADER_FORMAT2)
|
||
HEADER_SIZE3 = calcsize(HEADER_FORMAT3)
|
||
HEADER_SIZE4 = calcsize(HEADER_FORMAT4)
|
||
|
||
LIG_KERN_COMMAND = """
|
||
>
|
||
skip_byte: B
|
||
next_char: B
|
||
op_byte: B
|
||
remainder: B
|
||
"""
|
||
|
||
BASE_PARAMS = [
|
||
"SLANT",
|
||
"SPACE",
|
||
"STRETCH",
|
||
"SHRINK",
|
||
"XHEIGHT",
|
||
"QUAD",
|
||
"EXTRASPACE",
|
||
]
|
||
|
||
MATHSY_PARAMS = [
|
||
"NUM1",
|
||
"NUM2",
|
||
"NUM3",
|
||
"DENOM1",
|
||
"DENOM2",
|
||
"SUP1",
|
||
"SUP2",
|
||
"SUP3",
|
||
"SUB1",
|
||
"SUB2",
|
||
"SUPDROP",
|
||
"SUBDROP",
|
||
"DELIM1",
|
||
"DELIM2",
|
||
"AXISHEIGHT",
|
||
]
|
||
|
||
MATHEX_PARAMS = [
|
||
"DEFAULTRULETHICKNESS",
|
||
"BIGOPSPACING1",
|
||
"BIGOPSPACING2",
|
||
"BIGOPSPACING3",
|
||
"BIGOPSPACING4",
|
||
"BIGOPSPACING5",
|
||
]
|
||
|
||
VANILLA = 0
|
||
MATHSY = 1
|
||
MATHEX = 2
|
||
|
||
UNREACHABLE = 0
|
||
PASSTHROUGH = 1
|
||
ACCESSABLE = 2
|
||
|
||
NO_TAG = 0
|
||
LIG_TAG = 1
|
||
LIST_TAG = 2
|
||
EXT_TAG = 3
|
||
|
||
STOP_FLAG = 128
|
||
KERN_FLAG = 128
|
||
|
||
|
||
class TFMException(Exception):
|
||
def __init__(self, message):
|
||
super().__init__(message)
|
||
|
||
|
||
class TFM:
|
||
def __init__(self, file):
|
||
self._read(file)
|
||
|
||
def __repr__(self):
|
||
return (
|
||
f"<TFM"
|
||
f" for {self.family}"
|
||
f" in {self.codingscheme}"
|
||
f" at {self.designsize:g}pt>"
|
||
)
|
||
|
||
def _read(self, file):
|
||
if hasattr(file, "read"):
|
||
data = file.read()
|
||
else:
|
||
with open(file, "rb") as fp:
|
||
data = fp.read()
|
||
|
||
self._data = data
|
||
|
||
if len(data) < SIZES_SIZE:
|
||
raise TFMException("Too short input file")
|
||
|
||
sizes = SimpleNamespace()
|
||
unpack2(SIZES_FORMAT, data, sizes)
|
||
|
||
# Do some file structure sanity checks.
|
||
# TeX and TFtoPL do additional functional checks and might even correct
|
||
# “errors” in the input file, but we instead try to output the file as
|
||
# it is as long as it is parsable, even if the data make no sense.
|
||
|
||
if sizes.lf < 0:
|
||
raise TFMException("The file claims to have negative or zero length!")
|
||
|
||
if len(data) < sizes.lf * 4:
|
||
raise TFMException("The file has fewer bytes than it claims!")
|
||
|
||
for name, length in vars(sizes).items():
|
||
if length < 0:
|
||
raise TFMException("The subfile size: '{name}' is negative!")
|
||
|
||
if sizes.lh < 2:
|
||
raise TFMException(f"The header length is only {sizes.lh}!")
|
||
|
||
if sizes.bc > sizes.ec + 1 or sizes.ec > 255:
|
||
raise TFMException(
|
||
f"The character code range {sizes.bc}..{sizes.ec} is illegal!"
|
||
)
|
||
|
||
if sizes.nw == 0 or sizes.nh == 0 or sizes.nd == 0 or sizes.ni == 0:
|
||
raise TFMException("Incomplete subfiles for character dimensions!")
|
||
|
||
if sizes.ne > 256:
|
||
raise TFMException(f"There are {ne} extensible recipes!")
|
||
|
||
if sizes.lf != (
|
||
6
|
||
+ sizes.lh
|
||
+ (sizes.ec - sizes.bc + 1)
|
||
+ sizes.nw
|
||
+ sizes.nh
|
||
+ sizes.nd
|
||
+ sizes.ni
|
||
+ sizes.nl
|
||
+ sizes.nk
|
||
+ sizes.ne
|
||
+ sizes.np
|
||
):
|
||
raise TFMException("Subfile sizes don’t add up to the stated total")
|
||
|
||
# Subfile offsets, used in the helper function below. These all are
|
||
# 32-bit word offsets not 8-bit byte offsets.
|
||
char_base = 6 + sizes.lh - sizes.bc
|
||
width_base = char_base + sizes.ec + 1
|
||
height_base = width_base + sizes.nw
|
||
depth_base = height_base + sizes.nh
|
||
italic_base = depth_base + sizes.nd
|
||
lig_kern_base = italic_base + sizes.ni
|
||
kern_base = lig_kern_base + sizes.nl
|
||
exten_base = kern_base + sizes.nk
|
||
param_base = exten_base + sizes.ne
|
||
|
||
# Helper functions for accessing individual data. If this looks
|
||
# nonidiomatic Python, I blame the effect of reading the literate WEB
|
||
# documentation of TFtoPL.
|
||
def char_info(c):
|
||
return 4 * (char_base + c)
|
||
|
||
def width_index(c):
|
||
return data[char_info(c)]
|
||
|
||
def noneexistent(c):
|
||
return c < sizes.bc or c > sizes.ec or width_index(c) == 0
|
||
|
||
def height_index(c):
|
||
return data[char_info(c) + 1] // 16
|
||
|
||
def depth_index(c):
|
||
return data[char_info(c) + 1] % 16
|
||
|
||
def italic_index(c):
|
||
return data[char_info(c) + 2] // 4
|
||
|
||
def tag(c):
|
||
return data[char_info(c) + 2] % 4
|
||
|
||
def remainder(c):
|
||
return data[char_info(c) + 3]
|
||
|
||
def width(c):
|
||
r = 4 * (width_base + width_index(c))
|
||
return read_fixed(r, "v")["v"]
|
||
|
||
def height(c):
|
||
r = 4 * (height_base + height_index(c))
|
||
return read_fixed(r, "v")["v"]
|
||
|
||
def depth(c):
|
||
r = 4 * (depth_base + depth_index(c))
|
||
return read_fixed(r, "v")["v"]
|
||
|
||
def italic(c):
|
||
r = 4 * (italic_base + italic_index(c))
|
||
return read_fixed(r, "v")["v"]
|
||
|
||
def exten(c):
|
||
return 4 * (exten_base + remainder(c))
|
||
|
||
def lig_step(i):
|
||
return 4 * (lig_kern_base + i)
|
||
|
||
def lig_kern_command(i):
|
||
command = SimpleNamespace()
|
||
unpack2(LIG_KERN_COMMAND, data[i:], command)
|
||
return command
|
||
|
||
def kern(i):
|
||
r = 4 * (kern_base + i)
|
||
return read_fixed(r, "v")["v"]
|
||
|
||
def param(i):
|
||
return 4 * (param_base + i)
|
||
|
||
def read_fixed(index, key, obj=None):
|
||
ret = unpack2(f">;{key}:{FIXED_FORMAT}", data[index:], obj)
|
||
return ret[0]
|
||
|
||
# Set all attributes to empty values regardless of the header size.
|
||
unpack(HEADER_FORMAT4, [0] * HEADER_SIZE4, self)
|
||
|
||
offset = 24
|
||
length = sizes.lh * 4
|
||
self.extraheader = {}
|
||
if length >= HEADER_SIZE4:
|
||
rest = unpack2(HEADER_FORMAT4, data[offset:], self)[1]
|
||
if self.face < 18:
|
||
s = self.face % 2
|
||
b = self.face // 2
|
||
self.face = "MBL"[b % 3] + "RI"[s] + "RCE"[b // 3]
|
||
for i in range(sizes.lh - HEADER_SIZE4 // 4):
|
||
rest = unpack2(f">;HEADER{i + 18}:l", rest, self.extraheader)[1]
|
||
elif length >= HEADER_SIZE3:
|
||
unpack2(HEADER_FORMAT3, data[offset:], self)
|
||
elif length >= HEADER_SIZE2:
|
||
unpack2(HEADER_FORMAT2, data[offset:], self)
|
||
elif length >= HEADER_SIZE1:
|
||
unpack2(HEADER_FORMAT1, data[offset:], self)
|
||
|
||
self.fonttype = VANILLA
|
||
scheme = self.codingscheme.upper()
|
||
if scheme.startswith("TEX MATH SY"):
|
||
self.fonttype = MATHSY
|
||
elif scheme.startswith("TEX MATH EX"):
|
||
self.fonttype = MATHEX
|
||
|
||
self.fontdimens = {}
|
||
for i in range(sizes.np):
|
||
name = f"PARAMETER{i+1}"
|
||
if i <= 6:
|
||
name = BASE_PARAMS[i]
|
||
elif self.fonttype == MATHSY and i <= 21:
|
||
name = MATHSY_PARAMS[i - 7]
|
||
elif self.fonttype == MATHEX and i <= 12:
|
||
name = MATHEX_PARAMS[i - 7]
|
||
read_fixed(param(i), name, self.fontdimens)
|
||
|
||
lig_kern_map = {}
|
||
self.right_boundary_char = None
|
||
self.left_boundary_char = None
|
||
if sizes.nl > 0:
|
||
cmd = lig_kern_command(lig_step(0))
|
||
if cmd.skip_byte == 255:
|
||
self.right_boundary_char = cmd.next_char
|
||
|
||
cmd = lig_kern_command(lig_step((sizes.nl - 1)))
|
||
if cmd.skip_byte == 255:
|
||
self.left_boundary_char = 256
|
||
r = 256 * cmd.op_byte + cmd.remainder
|
||
lig_kern_map[self.left_boundary_char] = r
|
||
|
||
self.chars = {}
|
||
for c in range(sizes.bc, sizes.ec + 1):
|
||
if width_index(c) > 0:
|
||
self.chars[c] = info = {}
|
||
info["width"] = width(c)
|
||
if height_index(c) > 0:
|
||
info["height"] = height(c)
|
||
if depth_index(c) > 0:
|
||
info["depth"] = depth(c)
|
||
if italic_index(c) > 0:
|
||
info["italic"] = italic(c)
|
||
char_tag = tag(c)
|
||
if char_tag == NO_TAG:
|
||
pass
|
||
elif char_tag == LIG_TAG:
|
||
lig_kern_map[c] = remainder(c)
|
||
elif char_tag == LIST_TAG:
|
||
info["nextlarger"] = remainder(c)
|
||
elif char_tag == EXT_TAG:
|
||
info["varchar"] = varchar = {}
|
||
for i in range(4):
|
||
part = data[exten(c) + i]
|
||
if i == 3 or part > 0:
|
||
name = "rep"
|
||
if i == 0:
|
||
name = "top"
|
||
elif i == 1:
|
||
name = "mid"
|
||
elif i == 2:
|
||
name = "bot"
|
||
if noneexistent(part):
|
||
varchar[name] = c
|
||
else:
|
||
varchar[name] = part
|
||
|
||
self.ligatures = {}
|
||
self.kerning = {}
|
||
for c, i in sorted(lig_kern_map.items()):
|
||
cmd = lig_kern_command(lig_step(i))
|
||
if cmd.skip_byte > STOP_FLAG:
|
||
i = 256 * cmd.op_byte + cmd.remainder
|
||
|
||
while i < sizes.nl:
|
||
cmd = lig_kern_command(lig_step(i))
|
||
if cmd.skip_byte > STOP_FLAG:
|
||
pass
|
||
else:
|
||
if cmd.op_byte >= KERN_FLAG:
|
||
r = 256 * (cmd.op_byte - KERN_FLAG) + cmd.remainder
|
||
self.kerning.setdefault(c, {})[cmd.next_char] = kern(r)
|
||
else:
|
||
r = cmd.op_byte
|
||
if r == 4 or (r > 7 and r != 11):
|
||
# Ligature step with nonstandard code, we output
|
||
# the code verbatim.
|
||
lig = r
|
||
else:
|
||
lig = ""
|
||
if r % 4 > 1:
|
||
lig += "/"
|
||
lig += "LIG"
|
||
if r % 2 != 0:
|
||
lig += "/"
|
||
while r > 3:
|
||
lig += ">"
|
||
r -= 4
|
||
self.ligatures.setdefault(c, {})[cmd.next_char] = (
|
||
lig,
|
||
cmd.remainder,
|
||
)
|
||
|
||
if cmd.skip_byte >= STOP_FLAG:
|
||
break
|
||
i += cmd.skip_byte + 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import sys
|
||
|
||
tfm = TFM(sys.argv[1])
|
||
print(
|
||
"\n".join(
|
||
x
|
||
for x in [
|
||
f"tfm.checksum={tfm.checksum}",
|
||
f"tfm.designsize={tfm.designsize}",
|
||
f"tfm.codingscheme={tfm.codingscheme}",
|
||
f"tfm.fonttype={tfm.fonttype}",
|
||
f"tfm.family={tfm.family}",
|
||
f"tfm.seven_bit_safe_flag={tfm.seven_bit_safe_flag}",
|
||
f"tfm.face={tfm.face}",
|
||
f"tfm.extraheader={tfm.extraheader}",
|
||
f"tfm.fontdimens={tfm.fontdimens}",
|
||
f"tfm.right_boundary_char={tfm.right_boundary_char}",
|
||
f"tfm.left_boundary_char={tfm.left_boundary_char}",
|
||
f"tfm.kerning={tfm.kerning}",
|
||
f"tfm.ligatures={tfm.ligatures}",
|
||
f"tfm.chars={tfm.chars}",
|
||
]
|
||
)
|
||
)
|
||
print(tfm)
|