Our footprint in the Python module namespace is all under fontTools now. User code importing sstruct should be updated to say "from fontTools.misc import sstruct".
555 lines
16 KiB
Python
555 lines
16 KiB
Python
import os
|
|
import struct
|
|
from fontTools.misc import sstruct
|
|
import string
|
|
try:
|
|
from Carbon import Res
|
|
except ImportError:
|
|
import Res
|
|
|
|
|
|
error = "fondLib.error"
|
|
|
|
DEBUG = 0
|
|
|
|
headerformat = """
|
|
>
|
|
ffFlags: h
|
|
ffFamID: h
|
|
ffFirstChar: h
|
|
ffLastChar: h
|
|
ffAscent: h
|
|
ffDescent: h
|
|
ffLeading: h
|
|
ffWidMax: h
|
|
ffWTabOff: l
|
|
ffKernOff: l
|
|
ffStylOff: l
|
|
"""
|
|
|
|
FONDheadersize = 52
|
|
|
|
class FontFamily:
|
|
|
|
def __init__(self, theRes, mode = 'r'):
|
|
self.ID, type, self.name = theRes.GetResInfo()
|
|
if type <> 'FOND':
|
|
raise ValueError, "FOND resource required"
|
|
self.FOND = theRes
|
|
self.mode = mode
|
|
self.changed = 0
|
|
|
|
if DEBUG:
|
|
self.parsedthings = []
|
|
|
|
def parse(self):
|
|
self._getheader()
|
|
self._getfontassociationtable()
|
|
self._getoffsettable()
|
|
self._getboundingboxtable()
|
|
self._getglyphwidthtable()
|
|
self._getstylemappingtable()
|
|
self._getglyphencodingsubtable()
|
|
self._getkerningtables()
|
|
|
|
def minimalparse(self):
|
|
self._getheader()
|
|
self._getglyphwidthtable()
|
|
self._getstylemappingtable()
|
|
|
|
def __repr__(self):
|
|
return "<FontFamily instance of %s>" % self.name
|
|
|
|
def getflags(self):
|
|
return self.fondClass
|
|
|
|
def setflags(self, flags):
|
|
self.changed = 1
|
|
self.fondClass = flags
|
|
|
|
def save(self, destresfile = None):
|
|
if self.mode <> 'w':
|
|
raise error, "can't save font: no write permission"
|
|
self._buildfontassociationtable()
|
|
self._buildoffsettable()
|
|
self._buildboundingboxtable()
|
|
self._buildglyphwidthtable()
|
|
self._buildkerningtables()
|
|
self._buildstylemappingtable()
|
|
self._buildglyphencodingsubtable()
|
|
rawnames = [ "_rawheader",
|
|
"_rawfontassociationtable",
|
|
"_rawoffsettable",
|
|
"_rawglyphwidthtable",
|
|
"_rawstylemappingtable",
|
|
"_rawglyphencodingsubtable",
|
|
"_rawkerningtables"
|
|
]
|
|
for name in rawnames[1:]: # skip header
|
|
data = getattr(self, name)
|
|
if len(data) & 1:
|
|
setattr(self, name, data + '\0')
|
|
|
|
self.ffWTabOff = FONDheadersize + len(self._rawfontassociationtable) + len(self._rawoffsettable)
|
|
self.ffStylOff = self.ffWTabOff + len(self._rawglyphwidthtable)
|
|
self.ffKernOff = self.ffStylOff + len(self._rawstylemappingtable) + len(self._rawglyphencodingsubtable)
|
|
self.glyphTableOffset = len(self._rawstylemappingtable)
|
|
|
|
if not self._rawglyphwidthtable:
|
|
self.ffWTabOff = 0
|
|
if not self._rawstylemappingtable:
|
|
self.ffStylOff = 0
|
|
if not self._rawglyphencodingsubtable:
|
|
self.glyphTableOffset = 0
|
|
if not self._rawkerningtables:
|
|
self.ffKernOff = 0
|
|
|
|
self._buildheader()
|
|
|
|
# glyphTableOffset has only just been calculated
|
|
self._updatestylemappingtable()
|
|
|
|
newdata = ""
|
|
for name in rawnames:
|
|
newdata = newdata + getattr(self, name)
|
|
if destresfile is None:
|
|
self.FOND.data = newdata
|
|
self.FOND.ChangedResource()
|
|
self.FOND.WriteResource()
|
|
else:
|
|
ID, type, name = self.FOND.GetResInfo()
|
|
self.FOND.DetachResource()
|
|
self.FOND.data = newdata
|
|
saveref = Res.CurResFile()
|
|
Res.UseResFile(destresfile)
|
|
self.FOND.AddResource(type, ID, name)
|
|
Res.UseResFile(saveref)
|
|
self.changed = 0
|
|
|
|
def _getheader(self):
|
|
data = self.FOND.data
|
|
sstruct.unpack(headerformat, data[:28], self)
|
|
self.ffProperty = struct.unpack(">9h", data[28:46])
|
|
self.ffIntl = struct.unpack(">hh", data[46:50])
|
|
self.ffVersion, = struct.unpack(">h", data[50:FONDheadersize])
|
|
|
|
if DEBUG:
|
|
self._rawheader = data[:FONDheadersize]
|
|
self.parsedthings.append((0, FONDheadersize, 'header'))
|
|
|
|
def _buildheader(self):
|
|
header = sstruct.pack(headerformat, self)
|
|
header = header + apply(struct.pack, (">9h",) + self.ffProperty)
|
|
header = header + apply(struct.pack, (">hh",) + self.ffIntl)
|
|
header = header + struct.pack(">h", self.ffVersion)
|
|
if DEBUG:
|
|
print "header is the same?", self._rawheader == header and 'yes.' or 'no.'
|
|
if self._rawheader <> header:
|
|
print len(self._rawheader), len(header)
|
|
self._rawheader = header
|
|
|
|
def _getfontassociationtable(self):
|
|
data = self.FOND.data
|
|
offset = FONDheadersize
|
|
numberofentries, = struct.unpack(">h", data[offset:offset+2])
|
|
numberofentries = numberofentries + 1
|
|
size = numberofentries * 6
|
|
self.fontAssoc = []
|
|
for i in range(offset + 2, offset + size, 6):
|
|
self.fontAssoc.append(struct.unpack(">3h", data[i:i+6]))
|
|
|
|
self._endoffontassociationtable = offset + size + 2
|
|
if DEBUG:
|
|
self._rawfontassociationtable = data[offset:self._endoffontassociationtable]
|
|
self.parsedthings.append((offset, self._endoffontassociationtable, 'fontassociationtable'))
|
|
|
|
def _buildfontassociationtable(self):
|
|
data = struct.pack(">h", len(self.fontAssoc) - 1)
|
|
for size, stype, ID in self.fontAssoc:
|
|
data = data + struct.pack(">3h", size, stype, ID)
|
|
|
|
if DEBUG:
|
|
print "font association table is the same?", self._rawfontassociationtable == data and 'yes.' or 'no.'
|
|
if self._rawfontassociationtable <> data:
|
|
print len(self._rawfontassociationtable), len(data)
|
|
self._rawfontassociationtable = data
|
|
|
|
def _getoffsettable(self):
|
|
if self.ffWTabOff == 0:
|
|
self._rawoffsettable = ""
|
|
return
|
|
data = self.FOND.data
|
|
# Quick'n'Dirty. What's the spec anyway? Can't find it...
|
|
offset = self._endoffontassociationtable
|
|
count = self.ffWTabOff
|
|
self._rawoffsettable = data[offset:count]
|
|
if DEBUG:
|
|
self.parsedthings.append((offset, count, 'offsettable&bbtable'))
|
|
|
|
def _buildoffsettable(self):
|
|
if not hasattr(self, "_rawoffsettable"):
|
|
self._rawoffsettable = ""
|
|
|
|
def _getboundingboxtable(self):
|
|
self.boundingBoxes = None
|
|
if self._rawoffsettable[:6] <> '\0\0\0\0\0\6': # XXX ????
|
|
return
|
|
boxes = {}
|
|
data = self._rawoffsettable[6:]
|
|
numstyles = struct.unpack(">h", data[:2])[0] + 1
|
|
data = data[2:]
|
|
for i in range(numstyles):
|
|
style, l, b, r, t = struct.unpack(">hhhhh", data[:10])
|
|
boxes[style] = (l, b, r, t)
|
|
data = data[10:]
|
|
self.boundingBoxes = boxes
|
|
|
|
def _buildboundingboxtable(self):
|
|
if self.boundingBoxes and self._rawoffsettable[:6] == '\0\0\0\0\0\6':
|
|
boxes = self.boundingBoxes.items()
|
|
boxes.sort()
|
|
data = '\0\0\0\0\0\6' + struct.pack(">h", len(boxes) - 1)
|
|
for style, (l, b, r, t) in boxes:
|
|
data = data + struct.pack(">hhhhh", style, l, b, r, t)
|
|
self._rawoffsettable = data
|
|
|
|
def _getglyphwidthtable(self):
|
|
self.widthTables = {}
|
|
if self.ffWTabOff == 0:
|
|
return
|
|
data = self.FOND.data
|
|
offset = self.ffWTabOff
|
|
numberofentries, = struct.unpack(">h", data[offset:offset+2])
|
|
numberofentries = numberofentries + 1
|
|
count = offset + 2
|
|
for i in range(numberofentries):
|
|
stylecode, = struct.unpack(">h", data[count:count+2])
|
|
widthtable = self.widthTables[stylecode] = []
|
|
count = count + 2
|
|
for j in range(3 + self.ffLastChar - self.ffFirstChar):
|
|
width, = struct.unpack(">h", data[count:count+2])
|
|
widthtable.append(width)
|
|
count = count + 2
|
|
|
|
if DEBUG:
|
|
self._rawglyphwidthtable = data[offset:count]
|
|
self.parsedthings.append((offset, count, 'glyphwidthtable'))
|
|
|
|
def _buildglyphwidthtable(self):
|
|
if not self.widthTables:
|
|
self._rawglyphwidthtable = ""
|
|
return
|
|
numberofentries = len(self.widthTables)
|
|
data = struct.pack('>h', numberofentries - 1)
|
|
tables = self.widthTables.items()
|
|
tables.sort()
|
|
for stylecode, table in tables:
|
|
data = data + struct.pack('>h', stylecode)
|
|
if len(table) <> (3 + self.ffLastChar - self.ffFirstChar):
|
|
raise error, "width table has wrong length"
|
|
for width in table:
|
|
data = data + struct.pack('>h', width)
|
|
if DEBUG:
|
|
print "glyph width table is the same?", self._rawglyphwidthtable == data and 'yes.' or 'no.'
|
|
self._rawglyphwidthtable = data
|
|
|
|
def _getkerningtables(self):
|
|
self.kernTables = {}
|
|
if self.ffKernOff == 0:
|
|
return
|
|
data = self.FOND.data
|
|
offset = self.ffKernOff
|
|
numberofentries, = struct.unpack(">h", data[offset:offset+2])
|
|
numberofentries = numberofentries + 1
|
|
count = offset + 2
|
|
for i in range(numberofentries):
|
|
stylecode, = struct.unpack(">h", data[count:count+2])
|
|
count = count + 2
|
|
numberofpairs, = struct.unpack(">h", data[count:count+2])
|
|
count = count + 2
|
|
kerntable = self.kernTables[stylecode] = []
|
|
for j in range(numberofpairs):
|
|
firstchar, secondchar, kerndistance = struct.unpack(">cch", data[count:count+4])
|
|
kerntable.append((ord(firstchar), ord(secondchar), kerndistance))
|
|
count = count + 4
|
|
|
|
if DEBUG:
|
|
self._rawkerningtables = data[offset:count]
|
|
self.parsedthings.append((offset, count, 'kerningtables'))
|
|
|
|
def _buildkerningtables(self):
|
|
if self.kernTables == {}:
|
|
self._rawkerningtables = ""
|
|
self.ffKernOff = 0
|
|
return
|
|
numberofentries = len(self.kernTables)
|
|
data = [struct.pack('>h', numberofentries - 1)]
|
|
tables = self.kernTables.items()
|
|
tables.sort()
|
|
for stylecode, table in tables:
|
|
data.append(struct.pack('>h', stylecode))
|
|
data.append(struct.pack('>h', len(table))) # numberofpairs
|
|
for firstchar, secondchar, kerndistance in table:
|
|
data.append(struct.pack(">cch", chr(firstchar), chr(secondchar), kerndistance))
|
|
|
|
data = string.join(data, '')
|
|
|
|
if DEBUG:
|
|
print "kerning table is the same?", self._rawkerningtables == data and 'yes.' or 'no.'
|
|
if self._rawkerningtables <> data:
|
|
print len(self._rawkerningtables), len(data)
|
|
self._rawkerningtables = data
|
|
|
|
def _getstylemappingtable(self):
|
|
offset = self.ffStylOff
|
|
self.styleStrings = []
|
|
self.styleIndices = ()
|
|
self.glyphTableOffset = 0
|
|
self.fondClass = 0
|
|
if offset == 0:
|
|
return
|
|
data = self.FOND.data
|
|
self.fondClass, self.glyphTableOffset, self.styleMappingReserved, = \
|
|
struct.unpack(">hll", data[offset:offset+10])
|
|
self.styleIndices = struct.unpack('>48b', data[offset + 10:offset + 58])
|
|
stringcount, = struct.unpack('>h', data[offset+58:offset+60])
|
|
|
|
count = offset + 60
|
|
for i in range(stringcount):
|
|
str_len = ord(data[count])
|
|
self.styleStrings.append(data[count + 1:count + 1 + str_len])
|
|
count = count + 1 + str_len
|
|
|
|
self._unpackstylestrings()
|
|
|
|
data = data[offset:count]
|
|
if len(data) % 2:
|
|
data = data + '\0'
|
|
if DEBUG:
|
|
self._rawstylemappingtable = data
|
|
self.parsedthings.append((offset, count, 'stylemappingtable'))
|
|
|
|
def _buildstylemappingtable(self):
|
|
if not self.styleIndices:
|
|
self._rawstylemappingtable = ""
|
|
return
|
|
data = struct.pack(">hll", self.fondClass, self.glyphTableOffset,
|
|
self.styleMappingReserved)
|
|
|
|
self._packstylestrings()
|
|
data = data + apply(struct.pack, (">48b",) + self.styleIndices)
|
|
|
|
stringcount = len(self.styleStrings)
|
|
data = data + struct.pack(">h", stringcount)
|
|
for string in self.styleStrings:
|
|
data = data + chr(len(string)) + string
|
|
|
|
if len(data) % 2:
|
|
data = data + '\0'
|
|
|
|
if DEBUG:
|
|
print "style mapping table is the same?", self._rawstylemappingtable == data and 'yes.' or 'no.'
|
|
self._rawstylemappingtable = data
|
|
|
|
def _unpackstylestrings(self):
|
|
psNames = {}
|
|
self.ffFamilyName = self.styleStrings[0]
|
|
for i in self.widthTables.keys():
|
|
index = self.styleIndices[i]
|
|
if index == 1:
|
|
psNames[i] = self.styleStrings[0]
|
|
else:
|
|
style = self.styleStrings[0]
|
|
codes = map(ord, self.styleStrings[index - 1])
|
|
for code in codes:
|
|
style = style + self.styleStrings[code - 1]
|
|
psNames[i] = style
|
|
self.psNames = psNames
|
|
|
|
def _packstylestrings(self):
|
|
nameparts = {}
|
|
splitnames = {}
|
|
for style, name in self.psNames.items():
|
|
split = splitname(name, self.ffFamilyName)
|
|
splitnames[style] = split
|
|
for part in split:
|
|
nameparts[part] = None
|
|
del nameparts[self.ffFamilyName]
|
|
nameparts = nameparts.keys()
|
|
nameparts.sort()
|
|
items = splitnames.items()
|
|
items.sort()
|
|
numindices = 0
|
|
for style, split in items:
|
|
if len(split) > 1:
|
|
numindices = numindices + 1
|
|
numindices = max(numindices, max(self.styleIndices) - 1)
|
|
styleStrings = [self.ffFamilyName] + numindices * [""] + nameparts
|
|
# XXX the next bit goes wrong for MM fonts.
|
|
for style, split in items:
|
|
if len(split) == 1:
|
|
continue
|
|
indices = ""
|
|
for part in split[1:]:
|
|
indices = indices + chr(nameparts.index(part) + numindices + 2)
|
|
styleStrings[self.styleIndices[style] - 1] = indices
|
|
self.styleStrings = styleStrings
|
|
|
|
def _updatestylemappingtable(self):
|
|
# Update the glyphTableOffset field.
|
|
# This is necessary since we have to build this table to
|
|
# know what the glyphTableOffset will be.
|
|
# And we don't want to build it twice, do we?
|
|
data = self._rawstylemappingtable
|
|
if not data:
|
|
return
|
|
data = data[:2] + struct.pack(">l", self.glyphTableOffset) + data[6:]
|
|
self._rawstylemappingtable = data
|
|
|
|
def _getglyphencodingsubtable(self):
|
|
glyphEncoding = self.glyphEncoding = {}
|
|
if not self.glyphTableOffset:
|
|
return
|
|
offset = self.ffStylOff + self.glyphTableOffset
|
|
data = self.FOND.data
|
|
numberofentries, = struct.unpack(">h", data[offset:offset+2])
|
|
count = offset + 2
|
|
for i in range(numberofentries):
|
|
glyphcode = ord(data[count])
|
|
count = count + 1
|
|
strlen = ord(data[count])
|
|
count = count + 1
|
|
glyphname = data[count:count+strlen]
|
|
glyphEncoding[glyphcode] = glyphname
|
|
count = count + strlen
|
|
|
|
if DEBUG:
|
|
self._rawglyphencodingsubtable = data[offset:count]
|
|
self.parsedthings.append((offset, count, 'glyphencodingsubtable'))
|
|
|
|
def _buildglyphencodingsubtable(self):
|
|
if not self.glyphEncoding:
|
|
self._rawglyphencodingsubtable = ""
|
|
return
|
|
numberofentries = len(self.glyphEncoding)
|
|
data = struct.pack(">h", numberofentries)
|
|
items = self.glyphEncoding.items()
|
|
items.sort()
|
|
for glyphcode, glyphname in items:
|
|
data = data + chr(glyphcode) + chr(len(glyphname)) + glyphname
|
|
self._rawglyphencodingsubtable = data
|
|
|
|
|
|
uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
|
|
def splitname(name, famname = None):
|
|
# XXX this goofs up MM font names: but how should it be done??
|
|
if famname:
|
|
if name[:len(famname)] <> famname:
|
|
raise error, "first part of name should be same as family name"
|
|
name = name[len(famname):]
|
|
split = [famname]
|
|
else:
|
|
split = []
|
|
current = ""
|
|
for c in name:
|
|
if c == '-' or c in uppercase:
|
|
if current:
|
|
split.append(current)
|
|
current = ""
|
|
current = current + c
|
|
if current:
|
|
split.append(current)
|
|
return split
|
|
|
|
def makeLWFNfilename(name):
|
|
split = splitname(name)
|
|
lwfnname = split[0][:5]
|
|
for part in split[1:]:
|
|
if part <> '-':
|
|
lwfnname = lwfnname + part[:3]
|
|
return lwfnname
|
|
|
|
class BitmapFontFile:
|
|
|
|
def __init__(self, path, mode='r'):
|
|
if mode == 'r':
|
|
permission = 1 # read only
|
|
elif mode == 'w':
|
|
permission = 3 # exclusive r/w
|
|
else:
|
|
raise error, 'mode should be either "r" or "w"'
|
|
self.mode = mode
|
|
self.resref = Res.FSOpenResFile(path, permission)
|
|
Res.UseResFile(self.resref)
|
|
self.path = path
|
|
self.fonds = []
|
|
self.getFONDs()
|
|
|
|
def getFONDs(self):
|
|
FONDcount = Res.Count1Resources('FOND')
|
|
for i in range(FONDcount):
|
|
fond = FontFamily(Res.Get1IndResource('FOND', i + 1), self.mode)
|
|
self.fonds.append(fond)
|
|
|
|
def parse(self):
|
|
self.fondsbyname = {}
|
|
for fond in self.fonds:
|
|
fond.parse()
|
|
if hasattr(fond, "psNames") and fond.psNames:
|
|
psNames = fond.psNames.values()
|
|
psNames.sort()
|
|
self.fondsbyname[psNames[0]] = fond
|
|
|
|
def minimalparse(self):
|
|
for fond in self.fonds:
|
|
fond.minimalparse()
|
|
|
|
def close(self):
|
|
if self.resref <> None:
|
|
try:
|
|
Res.CloseResFile(self.resref)
|
|
except Res.Error:
|
|
pass
|
|
self.resref = None
|
|
|
|
|
|
class FondSelector:
|
|
|
|
def __init__(self, fondlist):
|
|
import W
|
|
if not fondlist:
|
|
raise ValueError, "expected at least one FOND entry"
|
|
if len(fondlist) == 1:
|
|
self.choice = 0
|
|
return
|
|
fonds = []
|
|
for fond in fondlist:
|
|
fonds.append(fond.name)
|
|
self.w = W.ModalDialog((200, 200), "aaa")
|
|
self.w.donebutton = W.Button((-70, -26, 60, 16), "Done", self.close)
|
|
self.w.l = W.List((10, 10, -10, -36), fonds, self.listhit)
|
|
self.w.setdefaultbutton(self.w.donebutton)
|
|
self.w.l.setselection([0])
|
|
self.w.open()
|
|
|
|
def close(self):
|
|
self.checksel()
|
|
sel = self.w.l.getselection()
|
|
self.choice = sel[0]
|
|
self.w.close()
|
|
|
|
def listhit(self, isDbl):
|
|
if isDbl:
|
|
self.w.donebutton.push()
|
|
else:
|
|
self.checksel()
|
|
|
|
def checksel(self):
|
|
sel = self.w.l.getselection()
|
|
if not sel:
|
|
self.w.l.setselection([0])
|
|
elif len(sel) <> 1:
|
|
self.w.l.setselection([sel[0]])
|
|
|