up follow livre
This commit is contained in:
parent
b4b4398bb0
commit
3a7a3849ae
12242 changed files with 2564461 additions and 6914 deletions
439
venv/lib/python3.13/site-packages/fontTools/afmLib.py
Normal file
439
venv/lib/python3.13/site-packages/fontTools/afmLib.py
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
"""Module for reading and writing AFM (Adobe Font Metrics) files.
|
||||
|
||||
Note that this has been designed to read in AFM files generated by Fontographer
|
||||
and has not been tested on many other files. In particular, it does not
|
||||
implement the whole Adobe AFM specification [#f1]_ but, it should read most
|
||||
"common" AFM files.
|
||||
|
||||
Here is an example of using `afmLib` to read, modify and write an AFM file:
|
||||
|
||||
>>> from fontTools.afmLib import AFM
|
||||
>>> f = AFM("Tests/afmLib/data/TestAFM.afm")
|
||||
>>>
|
||||
>>> # Accessing a pair gets you the kern value
|
||||
>>> f[("V","A")]
|
||||
-60
|
||||
>>>
|
||||
>>> # Accessing a glyph name gets you metrics
|
||||
>>> f["A"]
|
||||
(65, 668, (8, -25, 660, 666))
|
||||
>>> # (charnum, width, bounding box)
|
||||
>>>
|
||||
>>> # Accessing an attribute gets you metadata
|
||||
>>> f.FontName
|
||||
'TestFont-Regular'
|
||||
>>> f.FamilyName
|
||||
'TestFont'
|
||||
>>> f.Weight
|
||||
'Regular'
|
||||
>>> f.XHeight
|
||||
500
|
||||
>>> f.Ascender
|
||||
750
|
||||
>>>
|
||||
>>> # Attributes and items can also be set
|
||||
>>> f[("A","V")] = -150 # Tighten kerning
|
||||
>>> f.FontName = "TestFont Squished"
|
||||
>>>
|
||||
>>> # And the font written out again (remove the # in front)
|
||||
>>> #f.write("testfont-squished.afm")
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#f1] `Adobe Technote 5004 <https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5004.AFM_Spec.pdf>`_,
|
||||
Adobe Font Metrics File Format Specification.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
# every single line starts with a "word"
|
||||
identifierRE = re.compile(r"^([A-Za-z]+).*")
|
||||
|
||||
# regular expression to parse char lines
|
||||
charRE = re.compile(
|
||||
r"(-?\d+)" # charnum
|
||||
r"\s*;\s*WX\s+" # ; WX
|
||||
r"(-?\d+)" # width
|
||||
r"\s*;\s*N\s+" # ; N
|
||||
r"([.A-Za-z0-9_]+)" # charname
|
||||
r"\s*;\s*B\s+" # ; B
|
||||
r"(-?\d+)" # left
|
||||
r"\s+"
|
||||
r"(-?\d+)" # bottom
|
||||
r"\s+"
|
||||
r"(-?\d+)" # right
|
||||
r"\s+"
|
||||
r"(-?\d+)" # top
|
||||
r"\s*;\s*" # ;
|
||||
)
|
||||
|
||||
# regular expression to parse kerning lines
|
||||
kernRE = re.compile(
|
||||
r"([.A-Za-z0-9_]+)" # leftchar
|
||||
r"\s+"
|
||||
r"([.A-Za-z0-9_]+)" # rightchar
|
||||
r"\s+"
|
||||
r"(-?\d+)" # value
|
||||
r"\s*"
|
||||
)
|
||||
|
||||
# regular expressions to parse composite info lines of the form:
|
||||
# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
|
||||
compositeRE = re.compile(
|
||||
r"([.A-Za-z0-9_]+)" # char name
|
||||
r"\s+"
|
||||
r"(\d+)" # number of parts
|
||||
r"\s*;\s*"
|
||||
)
|
||||
componentRE = re.compile(
|
||||
r"PCC\s+" # PPC
|
||||
r"([.A-Za-z0-9_]+)" # base char name
|
||||
r"\s+"
|
||||
r"(-?\d+)" # x offset
|
||||
r"\s+"
|
||||
r"(-?\d+)" # y offset
|
||||
r"\s*;\s*"
|
||||
)
|
||||
|
||||
preferredAttributeOrder = [
|
||||
"FontName",
|
||||
"FullName",
|
||||
"FamilyName",
|
||||
"Weight",
|
||||
"ItalicAngle",
|
||||
"IsFixedPitch",
|
||||
"FontBBox",
|
||||
"UnderlinePosition",
|
||||
"UnderlineThickness",
|
||||
"Version",
|
||||
"Notice",
|
||||
"EncodingScheme",
|
||||
"CapHeight",
|
||||
"XHeight",
|
||||
"Ascender",
|
||||
"Descender",
|
||||
]
|
||||
|
||||
|
||||
class error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AFM(object):
|
||||
_attrs = None
|
||||
|
||||
_keywords = [
|
||||
"StartFontMetrics",
|
||||
"EndFontMetrics",
|
||||
"StartCharMetrics",
|
||||
"EndCharMetrics",
|
||||
"StartKernData",
|
||||
"StartKernPairs",
|
||||
"EndKernPairs",
|
||||
"EndKernData",
|
||||
"StartComposites",
|
||||
"EndComposites",
|
||||
]
|
||||
|
||||
def __init__(self, path=None):
|
||||
"""AFM file reader.
|
||||
|
||||
Instantiating an object with a path name will cause the file to be opened,
|
||||
read, and parsed. Alternatively the path can be left unspecified, and a
|
||||
file can be parsed later with the :meth:`read` method."""
|
||||
self._attrs = {}
|
||||
self._chars = {}
|
||||
self._kerning = {}
|
||||
self._index = {}
|
||||
self._comments = []
|
||||
self._composites = {}
|
||||
if path is not None:
|
||||
self.read(path)
|
||||
|
||||
def read(self, path):
|
||||
"""Opens, reads and parses a file."""
|
||||
lines = readlines(path)
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
m = identifierRE.match(line)
|
||||
if m is None:
|
||||
raise error("syntax error in AFM file: " + repr(line))
|
||||
|
||||
pos = m.regs[1][1]
|
||||
word = line[:pos]
|
||||
rest = line[pos:].strip()
|
||||
if word in self._keywords:
|
||||
continue
|
||||
if word == "C":
|
||||
self.parsechar(rest)
|
||||
elif word == "KPX":
|
||||
self.parsekernpair(rest)
|
||||
elif word == "CC":
|
||||
self.parsecomposite(rest)
|
||||
else:
|
||||
self.parseattr(word, rest)
|
||||
|
||||
def parsechar(self, rest):
|
||||
m = charRE.match(rest)
|
||||
if m is None:
|
||||
raise error("syntax error in AFM file: " + repr(rest))
|
||||
things = []
|
||||
for fr, to in m.regs[1:]:
|
||||
things.append(rest[fr:to])
|
||||
charname = things[2]
|
||||
del things[2]
|
||||
charnum, width, l, b, r, t = (int(thing) for thing in things)
|
||||
self._chars[charname] = charnum, width, (l, b, r, t)
|
||||
|
||||
def parsekernpair(self, rest):
|
||||
m = kernRE.match(rest)
|
||||
if m is None:
|
||||
raise error("syntax error in AFM file: " + repr(rest))
|
||||
things = []
|
||||
for fr, to in m.regs[1:]:
|
||||
things.append(rest[fr:to])
|
||||
leftchar, rightchar, value = things
|
||||
value = int(value)
|
||||
self._kerning[(leftchar, rightchar)] = value
|
||||
|
||||
def parseattr(self, word, rest):
|
||||
if word == "FontBBox":
|
||||
l, b, r, t = [int(thing) for thing in rest.split()]
|
||||
self._attrs[word] = l, b, r, t
|
||||
elif word == "Comment":
|
||||
self._comments.append(rest)
|
||||
else:
|
||||
try:
|
||||
value = int(rest)
|
||||
except (ValueError, OverflowError):
|
||||
self._attrs[word] = rest
|
||||
else:
|
||||
self._attrs[word] = value
|
||||
|
||||
def parsecomposite(self, rest):
|
||||
m = compositeRE.match(rest)
|
||||
if m is None:
|
||||
raise error("syntax error in AFM file: " + repr(rest))
|
||||
charname = m.group(1)
|
||||
ncomponents = int(m.group(2))
|
||||
rest = rest[m.regs[0][1] :]
|
||||
components = []
|
||||
while True:
|
||||
m = componentRE.match(rest)
|
||||
if m is None:
|
||||
raise error("syntax error in AFM file: " + repr(rest))
|
||||
basechar = m.group(1)
|
||||
xoffset = int(m.group(2))
|
||||
yoffset = int(m.group(3))
|
||||
components.append((basechar, xoffset, yoffset))
|
||||
rest = rest[m.regs[0][1] :]
|
||||
if not rest:
|
||||
break
|
||||
assert len(components) == ncomponents
|
||||
self._composites[charname] = components
|
||||
|
||||
def write(self, path, sep="\r"):
|
||||
"""Writes out an AFM font to the given path."""
|
||||
import time
|
||||
|
||||
lines = [
|
||||
"StartFontMetrics 2.0",
|
||||
"Comment Generated by afmLib; at %s"
|
||||
% (time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time()))),
|
||||
]
|
||||
|
||||
# write comments, assuming (possibly wrongly!) they should
|
||||
# all appear at the top
|
||||
for comment in self._comments:
|
||||
lines.append("Comment " + comment)
|
||||
|
||||
# write attributes, first the ones we know about, in
|
||||
# a preferred order
|
||||
attrs = self._attrs
|
||||
for attr in preferredAttributeOrder:
|
||||
if attr in attrs:
|
||||
value = attrs[attr]
|
||||
if attr == "FontBBox":
|
||||
value = "%s %s %s %s" % value
|
||||
lines.append(attr + " " + str(value))
|
||||
# then write the attributes we don't know about,
|
||||
# in alphabetical order
|
||||
items = sorted(attrs.items())
|
||||
for attr, value in items:
|
||||
if attr in preferredAttributeOrder:
|
||||
continue
|
||||
lines.append(attr + " " + str(value))
|
||||
|
||||
# write char metrics
|
||||
lines.append("StartCharMetrics " + repr(len(self._chars)))
|
||||
items = [
|
||||
(charnum, (charname, width, box))
|
||||
for charname, (charnum, width, box) in self._chars.items()
|
||||
]
|
||||
|
||||
def myKey(a):
|
||||
"""Custom key function to make sure unencoded chars (-1)
|
||||
end up at the end of the list after sorting."""
|
||||
if a[0] == -1:
|
||||
a = (0xFFFF,) + a[1:] # 0xffff is an arbitrary large number
|
||||
return a
|
||||
|
||||
items.sort(key=myKey)
|
||||
|
||||
for charnum, (charname, width, (l, b, r, t)) in items:
|
||||
lines.append(
|
||||
"C %d ; WX %d ; N %s ; B %d %d %d %d ;"
|
||||
% (charnum, width, charname, l, b, r, t)
|
||||
)
|
||||
lines.append("EndCharMetrics")
|
||||
|
||||
# write kerning info
|
||||
lines.append("StartKernData")
|
||||
lines.append("StartKernPairs " + repr(len(self._kerning)))
|
||||
items = sorted(self._kerning.items())
|
||||
for (leftchar, rightchar), value in items:
|
||||
lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
|
||||
lines.append("EndKernPairs")
|
||||
lines.append("EndKernData")
|
||||
|
||||
if self._composites:
|
||||
composites = sorted(self._composites.items())
|
||||
lines.append("StartComposites %s" % len(self._composites))
|
||||
for charname, components in composites:
|
||||
line = "CC %s %s ;" % (charname, len(components))
|
||||
for basechar, xoffset, yoffset in components:
|
||||
line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset)
|
||||
lines.append(line)
|
||||
lines.append("EndComposites")
|
||||
|
||||
lines.append("EndFontMetrics")
|
||||
|
||||
writelines(path, lines, sep)
|
||||
|
||||
def has_kernpair(self, pair):
|
||||
"""Returns `True` if the given glyph pair (specified as a tuple) exists
|
||||
in the kerning dictionary."""
|
||||
return pair in self._kerning
|
||||
|
||||
def kernpairs(self):
|
||||
"""Returns a list of all kern pairs in the kerning dictionary."""
|
||||
return list(self._kerning.keys())
|
||||
|
||||
def has_char(self, char):
|
||||
"""Returns `True` if the given glyph exists in the font."""
|
||||
return char in self._chars
|
||||
|
||||
def chars(self):
|
||||
"""Returns a list of all glyph names in the font."""
|
||||
return list(self._chars.keys())
|
||||
|
||||
def comments(self):
|
||||
"""Returns all comments from the file."""
|
||||
return self._comments
|
||||
|
||||
def addComment(self, comment):
|
||||
"""Adds a new comment to the file."""
|
||||
self._comments.append(comment)
|
||||
|
||||
def addComposite(self, glyphName, components):
|
||||
"""Specifies that the glyph `glyphName` is made up of the given components.
|
||||
The components list should be of the following form::
|
||||
|
||||
[
|
||||
(glyphname, xOffset, yOffset),
|
||||
...
|
||||
]
|
||||
|
||||
"""
|
||||
self._composites[glyphName] = components
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr in self._attrs:
|
||||
return self._attrs[attr]
|
||||
else:
|
||||
raise AttributeError(attr)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
# all attrs *not* starting with "_" are consider to be AFM keywords
|
||||
if attr[:1] == "_":
|
||||
self.__dict__[attr] = value
|
||||
else:
|
||||
self._attrs[attr] = value
|
||||
|
||||
def __delattr__(self, attr):
|
||||
# all attrs *not* starting with "_" are consider to be AFM keywords
|
||||
if attr[:1] == "_":
|
||||
try:
|
||||
del self.__dict__[attr]
|
||||
except KeyError:
|
||||
raise AttributeError(attr)
|
||||
else:
|
||||
try:
|
||||
del self._attrs[attr]
|
||||
except KeyError:
|
||||
raise AttributeError(attr)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, tuple):
|
||||
# key is a tuple, return the kernpair
|
||||
return self._kerning[key]
|
||||
else:
|
||||
# return the metrics instead
|
||||
return self._chars[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if isinstance(key, tuple):
|
||||
# key is a tuple, set kernpair
|
||||
self._kerning[key] = value
|
||||
else:
|
||||
# set char metrics
|
||||
self._chars[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
if isinstance(key, tuple):
|
||||
# key is a tuple, del kernpair
|
||||
del self._kerning[key]
|
||||
else:
|
||||
# del char metrics
|
||||
del self._chars[key]
|
||||
|
||||
def __repr__(self):
|
||||
if hasattr(self, "FullName"):
|
||||
return "<AFM object for %s>" % self.FullName
|
||||
else:
|
||||
return "<AFM object at %x>" % id(self)
|
||||
|
||||
|
||||
def readlines(path):
|
||||
with open(path, "r", encoding="ascii") as f:
|
||||
data = f.read()
|
||||
return data.splitlines()
|
||||
|
||||
|
||||
def writelines(path, lines, sep="\r"):
|
||||
with open(path, "w", encoding="ascii", newline=sep) as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import EasyDialogs
|
||||
|
||||
path = EasyDialogs.AskFileForOpen()
|
||||
if path:
|
||||
afm = AFM(path)
|
||||
char = "A"
|
||||
if afm.has_char(char):
|
||||
print(afm[char]) # print charnum, width and boundingbox
|
||||
pair = ("A", "V")
|
||||
if afm.has_kernpair(pair):
|
||||
print(afm[pair]) # print kerning value for pair
|
||||
print(afm.Version) # various other afm entries have become attributes
|
||||
print(afm.Weight)
|
||||
# afm.comments() returns a list of all Comment lines found in the AFM
|
||||
print(afm.comments())
|
||||
# print afm.chars()
|
||||
# print afm.kernpairs()
|
||||
print(afm)
|
||||
afm.write(path + ".muck")
|
||||
Loading…
Add table
Add a link
Reference in a new issue