diff --git a/Lib/fontTools/misc/homeResFile.py b/Lib/fontTools/misc/homeResFile.py deleted file mode 100644 index f804f5306..000000000 --- a/Lib/fontTools/misc/homeResFile.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Mac-only module to find the home file of a resource.""" - -from __future__ import print_function, division, absolute_import -from fontTools.misc.py23 import * -from fontTools.misc import sstruct -import array -import calldll -import macfs -import Res - - -def HomeResFile(res): - """Return a path to the file in which resource 'res' lives.""" - return GetFileLocation(res.HomeResFile()) - - -def GetFileLocation(refNum): - """Return a path to the open file identified with refNum.""" - pb = ParamBlock(refNum) - return pb.getPath() - -# -# Internal cruft, adapted from MoreFiles -# - -_InterfaceLib = calldll.getlibrary("InterfaceLib") -GetVRefNum = calldll.newcall(_InterfaceLib.GetVRefNum, "None", "InShort", "OutShort") -_getInfo = calldll.newcall(_InterfaceLib.PBGetFCBInfoSync, "Short", "InLong") - - -_FCBPBFormat = """ - qLink: l - qType: h - ioTrap: h - ioCmdAddr: l - ioCompletion: l - ioResult: h - ioNamePtr: l - ioVRefNum: h - ioRefNum: h - filler: h - ioFCBIndx: h - filler1: h - ioFCBFINm: l - ioFCBFlags: h - ioFCBStBlk: h - ioFCBEOF: l - ioFCBPLen: l - ioFCBCrPs: l - ioFCBVRefNum: h - ioFCBClpSiz: l - ioFCBParID: l -""" - -class ParamBlock(object): - - """Wrapper for the very low level FCBPB record.""" - - def __init__(self, refNum): - self.__fileName = array.array("c", "\0" * 64) - sstruct.unpack(_FCBPBFormat, - "\0" * sstruct.calcsize(_FCBPBFormat), self) - self.ioNamePtr = self.__fileName.buffer_info()[0] - self.ioRefNum = refNum - self.ioVRefNum = GetVRefNum(refNum) - self.__haveInfo = 0 - - def getInfo(self): - if self.__haveInfo: - return - data = sstruct.pack(_FCBPBFormat, self) - buf = array.array("c", data) - ptr = buf.buffer_info()[0] - err = _getInfo(ptr) - if err: - raise Res.Error("can't get file info", err) - sstruct.unpack(_FCBPBFormat, buf.tostring(), self) - self.__haveInfo = 1 - - def getFileName(self): - self.getInfo() - data = self.__fileName.tostring() - return data[1:byteord(data[0])+1] - - def getFSSpec(self): - self.getInfo() - vRefNum = self.ioVRefNum - parID = self.ioFCBParID - return macfs.FSSpec((vRefNum, parID, self.getFileName())) - - def getPath(self): - return self.getFSSpec().as_pathname() - - -if __name__ == "__main__": - fond = Res.GetNamedResource("FOND", "Helvetica") - print(HomeResFile(fond)) diff --git a/Lib/fontTools/misc/macCreatorType.py b/Lib/fontTools/misc/macCreatorType.py index db23f0cb5..36123e8bf 100644 --- a/Lib/fontTools/misc/macCreatorType.py +++ b/Lib/fontTools/misc/macCreatorType.py @@ -1,11 +1,15 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * import sys +try: + import xattr +except ImportError: + xattr = None try: import MacOS except ImportError: MacOS = None -from .py23 import * + def _reverseString(s): s = list(s) @@ -14,6 +18,15 @@ def _reverseString(s): def getMacCreatorAndType(path): + if xattr is not None: + try: + finderInfo = xattr.getxattr(path, 'com.apple.FinderInfo') + except IOError: + pass + else: + fileType = Tag(finderInfo[:4]) + fileCreator = Tag(finderInfo[4:8]) + return fileCreator, fileType if MacOS is not None: fileCreator, fileType = MacOS.GetCreatorAndType(path) if sys.version_info[:2] < (2, 7) and sys.byteorder == "little": @@ -28,5 +41,11 @@ def getMacCreatorAndType(path): def setMacCreatorAndType(path, fileCreator, fileType): + if xattr is not None: + from fontTools.misc.textTools import pad + if not all(len(s) == 4 for s in (fileCreator, fileType)): + raise TypeError('arg must be string of 4 chars') + finderInfo = pad(bytesjoin([fileType, fileCreator]), 32) + xattr.setxattr(path, 'com.apple.FinderInfo', finderInfo) if MacOS is not None: MacOS.SetCreatorAndType(path, fileCreator, fileType) diff --git a/Lib/fontTools/misc/macRes.py b/Lib/fontTools/misc/macRes.py new file mode 100644 index 000000000..38e36418e --- /dev/null +++ b/Lib/fontTools/misc/macRes.py @@ -0,0 +1,227 @@ +""" Tools for reading Mac resource forks. """ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import struct +from fontTools.misc import sstruct +from collections import OrderedDict +try: + from collections.abc import MutableMapping +except ImportError: + from UserDict import DictMixin as MutableMapping + + +class ResourceError(Exception): + pass + + +class ResourceReader(MutableMapping): + + def __init__(self, fileOrPath): + self._resources = OrderedDict() + if hasattr(fileOrPath, 'read'): + self.file = fileOrPath + else: + try: + # try reading from the resource fork (only works on OS X) + self.file = self.openResourceFork(fileOrPath) + self._readFile() + return + except (ResourceError, IOError): + # if it fails, use the data fork + self.file = self.openDataFork(fileOrPath) + self._readFile() + + @staticmethod + def openResourceFork(path): + with open(path + '/..namedfork/rsrc', 'rb') as resfork: + data = resfork.read() + infile = BytesIO(data) + infile.name = path + return infile + + @staticmethod + def openDataFork(path): + with open(path, 'rb') as datafork: + data = datafork.read() + infile = BytesIO(data) + infile.name = path + return infile + + def _readFile(self): + self._readHeaderAndMap() + self._readTypeList() + + def _read(self, numBytes, offset=None): + if offset is not None: + self.file.seek(offset) + if self.file.tell() != offset: + raise ResourceError('Failed to seek offset (reached EOF)') + data = self.file.read(numBytes) + if len(data) != numBytes: + raise ResourceError('Cannot read resource (not enough data)') + return data + + def _readHeaderAndMap(self): + self.file.seek(0) + headerData = self._read(ResourceForkHeaderSize) + sstruct.unpack(ResourceForkHeader, headerData, self) + # seek to resource map, skip reserved + mapOffset = self.mapOffset + 22 + resourceMapData = self._read(ResourceMapHeaderSize, mapOffset) + sstruct.unpack(ResourceMapHeader, resourceMapData, self) + self.absTypeListOffset = self.mapOffset + self.typeListOffset + self.absNameListOffset = self.mapOffset + self.nameListOffset + + def _readTypeList(self): + absTypeListOffset = self.absTypeListOffset + numTypesData = self._read(2, absTypeListOffset) + self.numTypes, = struct.unpack('>H', numTypesData) + absTypeListOffset2 = absTypeListOffset + 2 + for i in range(self.numTypes + 1): + resTypeItemOffset = absTypeListOffset2 + ResourceTypeItemSize * i + resTypeItemData = self._read(ResourceTypeItemSize, resTypeItemOffset) + item = sstruct.unpack(ResourceTypeItem, resTypeItemData) + resType = tostr(item['type'], encoding='mac-roman') + refListOffset = absTypeListOffset + item['refListOffset'] + numRes = item['numRes'] + 1 + resources = self._readReferenceList(resType, refListOffset, numRes) + self._resources[resType] = resources + + def _readReferenceList(self, resType, refListOffset, numRes): + resources = [] + for i in range(numRes): + refOffset = refListOffset + ResourceRefItemSize * i + refData = self._read(ResourceRefItemSize, refOffset) + res = Resource(resType) + res.decompile(refData, self) + resources.append(res) + return resources + + def __getitem__(self, resType): + return self._resources[resType] + + def __delitem__(self, resType): + del self._resources[resType] + + def __setitem__(self, resType, resources): + self._resources[resType] = resources + + def __len__(self): + return len(self._resources) + + def __iter__(self): + return iter(self._resources) + + def keys(self): + return self._resources.keys() + + @property + def types(self): + return list(self._resources.keys()) + + def countResources(self, resType): + """Return the number of resources of a given type.""" + try: + return len(self[resType]) + except KeyError: + return 0 + + def getIndices(self, resType): + numRes = self.countResources(resType) + if numRes: + return list(range(1, numRes+1)) + else: + return [] + + def getNames(self, resType): + """Return list of names of all resources of a given type.""" + return [res.name for res in self.get(resType, []) if res.name is not None] + + def getIndResource(self, resType, index): + """Return resource of given type located at an index ranging from 1 + to the number of resources for that type, or None if not found. + """ + if index < 1: + return None + try: + res = self[resType][index-1] + except (KeyError, IndexError): + return None + return res + + def getNamedResource(self, resType, name): + """Return the named resource of given type, else return None.""" + name = tostr(name, encoding='mac-roman') + for res in self.get(resType, []): + if res.name == name: + return res + return None + + def close(self): + if not self.file.closed: + self.file.close() + + +class Resource(object): + + def __init__(self, resType=None, resData=None, resID=None, resName=None, + resAttr=None): + self.type = resType + self.data = resData + self.id = resID + self.name = resName + self.attr = resAttr + + def decompile(self, refData, reader): + sstruct.unpack(ResourceRefItem, refData, self) + # interpret 3-byte dataOffset as (padded) ULONG to unpack it with struct + self.dataOffset, = struct.unpack('>L', bytesjoin([b"\0", self.dataOffset])) + absDataOffset = reader.dataOffset + self.dataOffset + dataLength, = struct.unpack(">L", reader._read(4, absDataOffset)) + self.data = reader._read(dataLength) + if self.nameOffset == -1: + return + absNameOffset = reader.absNameListOffset + self.nameOffset + nameLength, = struct.unpack('B', reader._read(1, absNameOffset)) + name, = struct.unpack('>%ss' % nameLength, reader._read(nameLength)) + self.name = tostr(name, encoding='mac-roman') + + +ResourceForkHeader = """ + > # big endian + dataOffset: L + mapOffset: L + dataLen: L + mapLen: L +""" + +ResourceForkHeaderSize = sstruct.calcsize(ResourceForkHeader) + +ResourceMapHeader = """ + > # big endian + attr: H + typeListOffset: H + nameListOffset: H +""" + +ResourceMapHeaderSize = sstruct.calcsize(ResourceMapHeader) + +ResourceTypeItem = """ + > # big endian + type: 4s + numRes: H + refListOffset: H +""" + +ResourceTypeItemSize = sstruct.calcsize(ResourceTypeItem) + +ResourceRefItem = """ + > # big endian + id: h + nameOffset: h + attr: B + dataOffset: 3s + reserved: L +""" + +ResourceRefItemSize = sstruct.calcsize(ResourceRefItem) diff --git a/Lib/fontTools/misc/macRes_test.py b/Lib/fontTools/misc/macRes_test.py new file mode 100644 index 000000000..dd3feb359 --- /dev/null +++ b/Lib/fontTools/misc/macRes_test.py @@ -0,0 +1,97 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import sys +import os +import tempfile +import unittest +from fontTools.misc.textTools import deHexStr +from .macRes import ResourceReader + + +# test resource data in DeRez notation +""" +data 'TEST' (128, "name1") { $"4865 6C6C 6F" }; /* Hello */ +data 'TEST' (129, "name2") { $"576F 726C 64" }; /* World */ +data 'test' (130, "name3") { $"486F 7720 6172 6520 796F 753F" }; /* How are you? */ +""" +# the same data, compiled using Rez +# $ /usr/bin/Rez testdata.rez -o compiled +# $ hexdump -v compiled/..namedfork/rsrc +TEST_RSRC_FORK = deHexStr( + "00 00 01 00 00 00 01 22 00 00 00 22 00 00 00 64 " # 0x00000000 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000010 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000020 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000030 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000040 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000050 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000060 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000070 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000080 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000090 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000A0 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000B0 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000C0 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000D0 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000E0 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000F0 + "00 00 00 05 48 65 6c 6c 6f 00 00 00 05 57 6f 72 " # 0x00000100 + "6c 64 00 00 00 0c 48 6f 77 20 61 72 65 20 79 6f " # 0x00000110 + "75 3f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000120 + "00 00 00 00 00 00 00 00 00 00 00 1c 00 52 00 01 " # 0x00000130 + "54 45 53 54 00 01 00 12 74 65 73 74 00 00 00 2a " # 0x00000140 + "00 80 00 00 00 00 00 00 00 00 00 00 00 81 00 06 " # 0x00000150 + "00 00 00 09 00 00 00 00 00 82 00 0c 00 00 00 12 " # 0x00000160 + "00 00 00 00 05 6e 61 6d 65 31 05 6e 61 6d 65 32 " # 0x00000170 + "05 6e 61 6d 65 33 " # 0x00000180 +) + + +class ResourceReaderTest(unittest.TestCase): + + def test_read_file(self): + infile = BytesIO(TEST_RSRC_FORK) + reader = ResourceReader(infile) + resources = [res for typ in reader.keys() for res in reader[typ]] + self.assertExpected(resources) + + def test_read_datafork(self): + with tempfile.NamedTemporaryFile(delete=False) as tmp: + tmp.write(TEST_RSRC_FORK) + try: + reader = ResourceReader(tmp.name) + resources = [res for typ in reader.keys() for res in reader[typ]] + reader.close() + self.assertExpected(resources) + finally: + os.remove(tmp.name) + + def test_read_namedfork_rsrc(self): + if sys.platform != 'darwin': + self.skipTest('Not supported on "%s"' % sys.platform) + tmp = tempfile.NamedTemporaryFile(delete=False) + tmp.close() + try: + with open(tmp.name + '/..namedfork/rsrc', 'wb') as fork: + fork.write(TEST_RSRC_FORK) + reader = ResourceReader(tmp.name) + resources = [res for typ in reader.keys() for res in reader[typ]] + reader.close() + self.assertExpected(resources) + finally: + os.remove(tmp.name) + + def assertExpected(self, resources): + self.assertRezEqual(resources[0], 'TEST', b'Hello', 128, 'name1') + self.assertRezEqual(resources[1], 'TEST', b'World', 129, 'name2') + self.assertRezEqual( + resources[2], 'test', b'How are you?', 130, 'name3') + + def assertRezEqual(self, res, type_, data, id, name): + self.assertEqual(res.type, type_) + self.assertEqual(res.data, data) + self.assertEqual(res.id, id) + self.assertEqual(res.name, name) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/fontTools/t1Lib/__init__.py b/Lib/fontTools/t1Lib/__init__.py index 7fafddfc3..6632b056c 100644 --- a/Lib/fontTools/t1Lib/__init__.py +++ b/Lib/fontTools/t1Lib/__init__.py @@ -142,13 +142,11 @@ HEXLINELENGTH = 80 def readLWFN(path, onlyHeader=False): """reads an LWFN font file, returns raw data""" - resRef = Res.FSOpenResFile(path, 1) # read-only + from fontTools.misc.macRes import ResourceReader + reader = ResourceReader(path) try: - Res.UseResFile(resRef) - n = Res.Count1Resources('POST') data = [] - for i in range(501, 501 + n): - res = Res.Get1Resource('POST', i) + for res in reader.get('POST', []): code = byteord(res.data[0]) if byteord(res.data[1]) != 0: raise T1Error('corrupt LWFN file') @@ -167,7 +165,7 @@ def readLWFN(path, onlyHeader=False): else: raise T1Error('bad chunk code: ' + repr(code)) finally: - Res.CloseResFile(resRef) + reader.close() data = bytesjoin(data) assertType1(data) return data @@ -215,6 +213,7 @@ def readOther(path): # file writing tools def writeLWFN(path, data): + # Res.FSpCreateResFile was deprecated in OS X 10.5 Res.FSpCreateResFile(path, "just", "LWFN", 0) resRef = Res.FSOpenResFile(path, 2) # write-only try: diff --git a/Lib/fontTools/t1Lib/t1Lib_test.py b/Lib/fontTools/t1Lib/t1Lib_test.py index 6f4daf572..6b0015733 100644 --- a/Lib/fontTools/t1Lib/t1Lib_test.py +++ b/Lib/fontTools/t1Lib/t1Lib_test.py @@ -9,6 +9,8 @@ import random CWD = os.path.abspath(os.path.dirname(__file__)) DATADIR = os.path.join(CWD, 'testdata') +# I used `tx` to convert PFA to LWFN (stored in the data fork) +LWFN = os.path.join(DATADIR, 'TestT1-Regular.lwfn') PFA = os.path.join(DATADIR, 'TestT1-Regular.pfa') PFB = os.path.join(DATADIR, 'TestT1-Regular.pfb') @@ -63,6 +65,14 @@ class ReadWriteTest(unittest.TestCase): class T1FontTest(unittest.TestCase): + def test_parse_lwfn(self): + # the extended attrs are lost on git so we can't auto-detect 'LWFN' + font = t1Lib.T1Font() + font.data = t1Lib.readLWFN(LWFN) + font.parse() + self.assertEqual(font['FontName'], 'TestT1-Regular') + self.assertTrue('Subrs' in font['Private']) + def test_parse_pfa(self): font = t1Lib.T1Font(PFA) font.parse() @@ -84,3 +94,7 @@ class T1FontTest(unittest.TestCase): self.assertFalse(hasattr(aglyph, 'width')) aglyph.draw(NullPen()) self.assertTrue(hasattr(aglyph, 'width')) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/fontTools/t1Lib/testdata/TestT1-Regular.lwfn b/Lib/fontTools/t1Lib/testdata/TestT1-Regular.lwfn new file mode 100644 index 000000000..c3c09dd59 Binary files /dev/null and b/Lib/fontTools/t1Lib/testdata/TestT1-Regular.lwfn differ diff --git a/Lib/fontTools/ttLib/__init__.py b/Lib/fontTools/ttLib/__init__.py index 9f898a7b5..e0bed2e91 100644 --- a/Lib/fontTools/ttLib/__init__.py +++ b/Lib/fontTools/ttLib/__init__.py @@ -46,15 +46,6 @@ from fontTools.misc.py23 import * import os import sys -haveMacSupport = 0 -if sys.platform == "mac": - haveMacSupport = 1 -elif sys.platform == "darwin": - if sys.version_info[:3] != (2, 2, 0) and sys.version_info[:1] < (3,): - # Python 2.2's Mac support is broken, so don't enable it there. - # Python 3 does not have Res used by macUtils - haveMacSupport = 1 - class TTLibError(Exception): pass @@ -151,8 +142,8 @@ class TTFont(object): if not hasattr(file, "read"): closeStream = True # assume file is a string - if haveMacSupport and res_name_or_index is not None: - # on the mac, we deal with sfnt resources as well as flat files + if res_name_or_index is not None: + # see if it contains 'sfnt' resources in the resource or data fork from . import macUtils if res_name_or_index == 0: if macUtils.getSFNTResIndices(file): @@ -185,25 +176,15 @@ class TTFont(object): if self.reader is not None: self.reader.close() - def save(self, file, makeSuitcase=False, reorderTables=True): + def save(self, file, reorderTables=True): """Save the font to disk. Similarly to the constructor, the 'file' argument can be either a pathname or a writable file object. - - On the Mac, if makeSuitcase is true, a suitcase (resource fork) - file will we made instead of a flat .ttf file. """ from fontTools.ttLib import sfnt if not hasattr(file, "write"): closeStream = 1 - if os.name == "mac" and makeSuitcase: - from . import macUtils - file = macUtils.SFNTResourceWriter(file, self) - else: - file = open(file, "wb") - if os.name == "mac": - from fontTools.misc.macCreator import setMacCreatorAndType - setMacCreatorAndType(file.name, 'mdos', 'BINA') + file = open(file, "wb") else: # assume "file" is a writable file object closeStream = 0 diff --git a/Lib/fontTools/ttLib/macUtils.py b/Lib/fontTools/ttLib/macUtils.py index 919d8cb8a..00506a1cf 100644 --- a/Lib/fontTools/ttLib/macUtils.py +++ b/Lib/fontTools/ttLib/macUtils.py @@ -1,37 +1,18 @@ """ttLib.macUtils.py -- Various Mac-specific stuff.""" - from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -import sys -import os -if sys.platform not in ("mac", "darwin"): - raise ImportError("This module is Mac-only!") -try: - from Carbon import Res -except ImportError: - import Res - - -def MyOpenResFile(path): - mode = 1 # read only - try: - resref = Res.FSOpenResFile(path, mode) - except Res.Error: - # try data fork - resref = Res.FSOpenResourceFile(path, unicode(), mode) - return resref +from fontTools.misc.macRes import ResourceReader, ResourceError def getSFNTResIndices(path): - """Determine whether a file has a resource fork or not.""" + """Determine whether a file has a 'sfnt' resource fork or not.""" try: - resref = MyOpenResFile(path) - except Res.Error: + reader = ResourceReader(path) + indices = reader.getIndices('sfnt') + reader.close() + return indices + except ResourceError: return [] - Res.UseResFile(resref) - numSFNTs = Res.Count1Resources('sfnt') - Res.CloseResFile(resref) - return list(range(1, numSFNTs + 1)) def openTTFonts(path): @@ -53,21 +34,19 @@ def openTTFonts(path): return fonts -class SFNTResourceReader(object): +class SFNTResourceReader(BytesIO): - """Simple (Mac-only) read-only file wrapper for 'sfnt' resources.""" + """Simple read-only file wrapper for 'sfnt' resources.""" def __init__(self, path, res_name_or_index): - resref = MyOpenResFile(path) - Res.UseResFile(resref) + reader = ResourceReader(path) if isinstance(res_name_or_index, basestring): - res = Res.Get1NamedResource('sfnt', res_name_or_index) + rsrc = reader.getNamedResource('sfnt', res_name_or_index) else: - res = Res.Get1IndResource('sfnt', res_name_or_index) - self.file = BytesIO(res.data) - Res.CloseResFile(resref) + rsrc = reader.getIndResource('sfnt', res_name_or_index) + if rsrc is None: + raise TTLibError("sfnt resource not found: %s" % res_name_or_index) + reader.close() + self.rsrc = rsrc + super(SFNTResourceReader, self).__init__(rsrc.data) self.name = path - - def __getattr__(self, attr): - # cheap inheritance - return getattr(self.file, attr)