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)
|