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