Merge pull request #400 from anthrotype/rsrc-fork

add mac resource fork reader (py23 compatible); drop mac classic support
This commit is contained in:
Sascha Brawer 2015-10-26 08:34:36 -07:00
commit 4374cf2be0
9 changed files with 384 additions and 165 deletions

View File

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

View File

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

View File

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

View File

@ -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()

View File

@ -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:

View File

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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')
else:
# assume "file" is a writable file object
closeStream = 0

View File

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