git-svn-id: svn://svn.code.sf.net/p/fonttools/code/trunk@2 4cde692c-a291-49d1-8350-778aa11640f8
554 lines
16 KiB
Python
554 lines
16 KiB
Python
import os
|
|
import Res
|
|
import struct, sstruct
|
|
import string
|
|
|
|
__version__ = "1.0b2"
|
|
__author__ = "jvr"
|
|
|
|
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
|
|
styleStrings = [self.ffFamilyName] + numindices * [None] + 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 neccesary 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'):
|
|
import macfs
|
|
|
|
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
|
|
fss = macfs.FSSpec(path)
|
|
self.resref = Res.FSpOpenResFile(fss, 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]])
|
|
|