cffLib.py revision a20285b7b490fc77b507cd12afdebba963f11c9a
1"""cffLib.py -- read/write tools for Adobe CFF fonts."""
2
3#
4# $Id: cffLib.py,v 1.10 2002-05-14 12:37:36 jvr Exp $
5#
6
7import struct, sstruct
8import string
9import types
10from fontTools.misc import psCharStrings
11
12
13cffHeaderFormat = """
14	major:   B
15	minor:   B
16	hdrSize: B
17	offSize: B
18"""
19
20class CFFFontSet:
21
22	def __init__(self):
23		self.fonts = {}
24
25	def decompile(self, file):
26		sstruct.unpack(cffHeaderFormat, file.read(4), self)
27		assert self.major == 1 and self.minor == 0, \
28				"unknown CFF format: %d.%d" % (self.major, self.minor)
29
30		self.fontNames = readINDEX(file)
31		topDicts = readINDEX(file)
32		strings = IndexedStrings(readINDEX(file))
33		globalSubrs = readINDEX(file)
34		self.GlobalSubrs = map(psCharStrings.T2CharString, globalSubrs)
35
36		for i in range(len(topDicts)):
37			font = self.fonts[self.fontNames[i]] = CFFFont()
38			font.GlobalSubrs = self.GlobalSubrs  # Hmm.
39			file.seek(0, 0)
40			font.decompile(file, topDicts[i], strings, self)  # maybe only 'on demand'?
41
42	def compile(self):
43		strings = IndexedStrings()
44		XXXX
45
46	def toXML(self, xmlWriter, progress=None):
47		xmlWriter.newline()
48		for fontName in self.fontNames:
49			xmlWriter.begintag("CFFFont", name=fontName)
50			xmlWriter.newline()
51			font = self.fonts[fontName]
52			font.toXML(xmlWriter, progress)
53			xmlWriter.endtag("CFFFont")
54			xmlWriter.newline()
55		xmlWriter.newline()
56		xmlWriter.begintag("GlobalSubrs")
57		xmlWriter.newline()
58		for i in range(len(self.GlobalSubrs)):
59			xmlWriter.newline()
60			xmlWriter.begintag("CharString", id=i)
61			xmlWriter.newline()
62			self.GlobalSubrs[i].toXML(xmlWriter)
63			xmlWriter.endtag("CharString")
64			xmlWriter.newline()
65		xmlWriter.newline()
66		xmlWriter.endtag("GlobalSubrs")
67		xmlWriter.newline()
68		xmlWriter.newline()
69
70	def fromXML(self, (name, attrs, content)):
71		xxx
72
73
74class CFFFont:
75
76	def __init__(self):
77		pass
78
79	def __getattr__(self, attr):
80		if not topDictDefaults.has_key(attr):
81			raise AttributeError, attr
82		return topDictDefaults[attr]
83
84	def fromDict(self, dict):
85		self.__dict__.update(dict)
86
87	def decompileCID(self, data, strings):
88		offset = self.FDArray
89		fontDicts, restdata = readINDEX(data[offset:])
90		subFonts = []
91		for topDictData in fontDicts:
92			subFont = CFFFont()
93			subFonts.append(subFont)
94			subFont.decompile(data, topDictData, strings, None)
95
96		raise NotImplementedError
97
98	def decompile(self, file, topDictData, strings, fontSet):
99		top = TopDictDecompiler(strings)
100		top.decompile(topDictData)
101		self.fromDict(top.getDict())
102
103		if hasattr(self, "ROS"):
104			isCID = 1
105			# XXX CID subFonts
106		else:
107			isCID = 0
108			size, offset = self.Private
109			file.seek(offset, 0)
110			privateData = file.read(size)
111			file.seek(offset, 0)
112			assert len(privateData) == size
113			self.Private = PrivateDict()
114			self.Private.decompile(file, privateData, strings)
115
116		file.seek(self.CharStrings)
117		rawCharStrings = readINDEX(file)
118		nGlyphs = len(rawCharStrings)
119
120		# get charset (or rather: get glyphNames)
121		if self.charset == 0:
122			xxx  # standard charset
123		else:
124			file.seek(self.charset)
125			format = ord(file.read(1))
126			if format == 0:
127				xxx
128			elif format == 1:
129				charset = parseCharsetFormat1(nGlyphs, file, strings, isCID)
130			elif format == 2:
131				charset = parseCharsetFormat2(nGlyphs, file, strings, isCID)
132			elif format == 3:
133				xxx
134			else:
135				xxx
136		self.charset = charset
137
138		assert len(charset) == nGlyphs
139		self.CharStrings = charStrings = {}
140		if self.CharstringType == 2:
141			# Type 2 CharStrings
142			charStringClass = psCharStrings.T2CharString
143		else:
144			# Type 1 CharStrings
145			charStringClass = psCharStrings.T1CharString
146		for i in range(nGlyphs):
147			charStrings[charset[i]] = charStringClass(rawCharStrings[i])
148		assert len(charStrings) == nGlyphs
149
150		# XXX Encoding!
151		encoding = self.Encoding
152		if encoding not in (0, 1):
153			# encoding is an _offset_ from the beginning of 'data' to an encoding subtable
154			XXX
155			self.Encoding = encoding
156
157	def getGlyphOrder(self):
158		return self.charset
159
160	def setGlyphOrder(self, glyphOrder):
161		self.charset = glyphOrder
162
163	def decompileAllCharStrings(self):
164		if self.CharstringType == 2:
165			# Type 2 CharStrings
166			decompiler = psCharStrings.SimpleT2Decompiler(self.Private.Subrs, self.GlobalSubrs)
167			for charString in self.CharStrings.values():
168				if charString.needsDecompilation():
169					decompiler.reset()
170					decompiler.execute(charString)
171		else:
172			# Type 1 CharStrings
173			for charString in self.CharStrings.values():
174				charString.decompile()
175
176	def toXML(self, xmlWriter, progress=None):
177		xmlWriter.newline()
178		# first dump the simple values
179		self.toXMLSimpleValues(xmlWriter)
180
181		# dump charset
182		# XXX
183
184		# decompile all charstrings
185		if progress:
186			progress.setlabel("Decompiling CharStrings...")
187		self.decompileAllCharStrings()
188
189		# dump private dict
190		xmlWriter.begintag("Private")
191		xmlWriter.newline()
192		self.Private.toXML(xmlWriter)
193		xmlWriter.endtag("Private")
194		xmlWriter.newline()
195
196		self.toXMLCharStrings(xmlWriter, progress)
197
198	def toXMLSimpleValues(self, xmlWriter):
199		keys = self.__dict__.keys()
200		keys.remove("CharStrings")
201		keys.remove("Private")
202		keys.remove("charset")
203		keys.remove("GlobalSubrs")
204		keys.sort()
205		for key in keys:
206			value = getattr(self, key)
207			if key == "Encoding":
208				if value == 0:
209					# encoding is (Adobe) Standard Encoding
210					value = "StandardEncoding"
211				elif value == 1:
212					# encoding is Expert Encoding
213					value = "ExpertEncoding"
214			if type(value) == types.ListType:
215				value = string.join(map(str, value), " ")
216			else:
217				value = str(value)
218			xmlWriter.begintag(key)
219			if hasattr(value, "toXML"):
220				xmlWriter.newline()
221				value.toXML(xmlWriter)
222				xmlWriter.newline()
223			else:
224				xmlWriter.write(value)
225			xmlWriter.endtag(key)
226			xmlWriter.newline()
227		xmlWriter.newline()
228
229	def toXMLCharStrings(self, xmlWriter, progress=None):
230		charStrings = self.CharStrings
231		xmlWriter.newline()
232		xmlWriter.begintag("CharStrings")
233		xmlWriter.newline()
234		glyphNames = charStrings.keys()
235		glyphNames.sort()
236		for glyphName in glyphNames:
237			if progress:
238				progress.setlabel("Dumping 'CFF ' table... (%s)" % glyphName)
239				progress.increment()
240			xmlWriter.newline()
241			charString = charStrings[glyphName]
242			xmlWriter.begintag("CharString", name=glyphName)
243			xmlWriter.newline()
244			charString.toXML(xmlWriter)
245			xmlWriter.endtag("CharString")
246			xmlWriter.newline()
247		xmlWriter.newline()
248		xmlWriter.endtag("CharStrings")
249		xmlWriter.newline()
250
251
252class PrivateDict:
253
254	def __init__(self):
255		pass
256
257	def decompile(self, file, privateData, strings):
258		p = PrivateDictDecompiler(strings)
259		p.decompile(privateData)
260		self.fromDict(p.getDict())
261
262		# get local subrs
263		#print "YYY Private.Subrs:", self.Subrs
264		if hasattr(self, "Subrs"):
265			file.seek(self.Subrs, 1)
266			localSubrs = readINDEX(file)
267			self.Subrs = map(psCharStrings.T2CharString, localSubrs)
268		else:
269			self.Subrs = []
270
271	def toXML(self, xmlWriter):
272		xmlWriter.newline()
273		keys = self.__dict__.keys()
274		keys.remove("Subrs")
275		for key in keys:
276			value = getattr(self, key)
277			if type(value) == types.ListType:
278				value = string.join(map(str, value), " ")
279			else:
280				value = str(value)
281			xmlWriter.begintag(key)
282			xmlWriter.write(value)
283			xmlWriter.endtag(key)
284			xmlWriter.newline()
285		# write subroutines
286		xmlWriter.newline()
287		xmlWriter.begintag("Subrs")
288		xmlWriter.newline()
289		for i in range(len(self.Subrs)):
290			xmlWriter.newline()
291			xmlWriter.begintag("CharString", id=i)
292			xmlWriter.newline()
293			self.Subrs[i].toXML(xmlWriter)
294			xmlWriter.endtag("CharString")
295			xmlWriter.newline()
296		xmlWriter.newline()
297		xmlWriter.endtag("Subrs")
298		xmlWriter.newline()
299		xmlWriter.newline()
300
301	def __getattr__(self, attr):
302		if not privateDictDefaults.has_key(attr):
303			raise AttributeError, attr
304		return privateDictDefaults[attr]
305
306	def fromDict(self, dict):
307		self.__dict__.update(dict)
308
309
310def readINDEX(file):
311	count, = struct.unpack(">H", file.read(2))
312	if count == 0:
313		return []
314	offSize = ord(file.read(1))
315	offsets = []
316	for index in range(count+1):
317		chunk = file.read(offSize)
318		chunk = '\0' * (4 - offSize) + chunk
319		offset, = struct.unpack(">L", chunk)
320		offset = int(offset)
321		offsets.append(offset)
322	offsetBase = file.tell() - 1
323	prev = offsets[0]
324	stuff = []
325	for next in offsets[1:]:
326		assert offsetBase + prev == file.tell()
327		chunk = file.read(next - prev)
328		assert len(chunk) == next - prev
329		stuff.append(chunk)
330		prev = next
331	return stuff
332
333
334def parseCharsetFormat1(nGlyphs, file, strings, isCID):
335	charset = ['.notdef']
336	count = 1
337	while count < nGlyphs:
338		first, = struct.unpack(">H", file.read(2))
339		nLeft = ord(file.read(1))
340		if isCID:
341			for CID in range(first, first+nLeft+1):
342				charset.append(CID)
343		else:
344			for SID in range(first, first+nLeft+1):
345				charset.append(strings[SID])
346		count = count + nLeft + 1
347	return charset
348
349
350def parseCharsetFormat2(nGlyphs, file, strings, isCID):
351	charset = ['.notdef']
352	count = 1
353	while count < nGlyphs:
354		first, = struct.unpack(">H", file.read(2))
355		nLeft, = struct.unpack(">H", file.read(2))
356		if isCID:
357			for CID in range(first, first+nLeft+1):
358				charset.append(CID)
359		else:
360			for SID in range(first, first+nLeft+1):
361				charset.append(strings[SID])
362		count = count + nLeft + 1
363	return charset
364
365
366topDictOperators = [
367#   opcode     name                  argument type
368	(0,        'version',            'SID'),
369	(1,        'Notice',             'SID'),
370	(2,        'FullName',           'SID'),
371	(3,        'FamilyName',         'SID'),
372	(4,        'Weight',             'SID'),
373	(5,        'FontBBox',           'array'),
374	(13,       'UniqueID',           'number'),
375	(14,       'XUID',               'array'),
376	(15,       'charset',            'number'),
377	(16,       'Encoding',           'number'),
378	(17,       'CharStrings',        'number'),
379	(18,       'Private',            ('number', 'number')),
380	((12, 0),  'Copyright',          'SID'),
381	((12, 1),  'isFixedPitch',       'number'),
382	((12, 2),  'ItalicAngle',        'number'),
383	((12, 3),  'UnderlinePosition',  'number'),
384	((12, 4),  'UnderlineThickness', 'number'),
385	((12, 5),  'PaintType',          'number'),
386	((12, 6),  'CharstringType',     'number'),
387	((12, 7),  'FontMatrix',         'array'),
388	((12, 8),  'StrokeWidth',        'number'),
389	((12, 20), 'SyntheticBase',      'number'),
390	((12, 21), 'PostScript',         'SID'),
391	((12, 22), 'BaseFontName',       'SID'),
392	# CID additions
393	((12, 30), 'ROS',                ('SID', 'SID', 'number')),
394	((12, 31), 'CIDFontVersion',     'number'),
395	((12, 32), 'CIDFontRevision',    'number'),
396	((12, 33), 'CIDFontType',        'number'),
397	((12, 34), 'CIDCount',           'number'),
398	((12, 35), 'UIDBase',            'number'),
399	((12, 36), 'FDArray',            'number'),
400	((12, 37), 'FDSelect',           'number'),
401	((12, 38), 'FontName',           'SID'),
402]
403
404topDictDefaults = {
405	'isFixedPitch':        0,
406	'ItalicAngle':         0,
407	'UnderlineThickness':  50,
408	'PaintType':           0,
409	'CharstringType':      2,
410	'FontMatrix':          [0.001, 0, 0, 0.001, 0, 0],
411	'FontBBox':            [0, 0, 0, 0],
412	'StrokeWidth':         0,
413	'charset':             0,
414	'Encoding':            0,
415	# CID defaults
416	'CIDFontVersion':      0,
417	'CIDFontRevision':     0,
418	'CIDFontType':         0,
419	'CIDCount':            8720,
420}
421
422class TopDictDecompiler(psCharStrings.DictDecompiler):
423
424	operators = psCharStrings.buildOperatorDict(topDictOperators)
425	dictDefaults = topDictDefaults
426
427
428privateDictOperators = [
429#   opcode     name                  argument type
430	(6,        'BlueValues',         'array'),
431	(7,        'OtherBlues',         'array'),
432	(8,        'FamilyBlues',        'array'),
433	(9,        'FamilyOtherBlues',   'array'),
434	(10,       'StdHW',              'number'),
435	(11,       'StdVW',              'number'),
436	(19,       'Subrs',              'number'),
437	(20,       'defaultWidthX',      'number'),
438	(21,       'nominalWidthX',      'number'),
439	((12, 9),  'BlueScale',          'number'),
440	((12, 10), 'BlueShift',          'number'),
441	((12, 11), 'BlueFuzz',           'number'),
442	((12, 12), 'StemSnapH',          'array'),
443	((12, 13), 'StemSnapV',          'array'),
444	((12, 14), 'ForceBold',          'number'),
445	((12, 17), 'LanguageGroup',      'number'),
446	((12, 18), 'ExpansionFactor',    'number'),
447	((12, 19), 'initialRandomSeed',  'number'),
448]
449
450privateDictDefaults = {
451	'defaultWidthX':       0,
452	'nominalWidthX':       0,
453	'BlueScale':           0.039625,
454	'BlueShift':           7,
455	'BlueFuzz':            1,
456	'ForceBold':           0,
457	'LanguageGroup':       0,
458	'ExpansionFactor':     0.06,
459	'initialRandomSeed':   0,
460}
461
462class PrivateDictDecompiler(psCharStrings.DictDecompiler):
463
464	operators = psCharStrings.buildOperatorDict(privateDictOperators)
465	dictDefaults = privateDictDefaults
466
467
468class IndexedStrings:
469
470	def __init__(self, strings=None):
471		if strings is None:
472			strings = []
473		self.strings = strings
474
475	def __getitem__(self, SID):
476		if SID < cffStandardStringCount:
477			return cffStandardStrings[SID]
478		else:
479			return self.strings[SID - cffStandardStringCount]
480
481	def getSID(self, s):
482		if not hasattr(self, "stringMapping"):
483			self.buildStringMapping()
484		if cffStandardStringMapping.has_key(s):
485			SID = cffStandardStringMapping[s]
486		if self.stringMapping.has_key(s):
487			SID = self.stringMapping[s]
488		else:
489			SID = len(self.strings) + cffStandardStringCount
490			self.strings.append(s)
491			self.stringMapping[s] = SID
492		return SID
493
494	def getStrings(self):
495		return self.strings
496
497	def buildStringMapping(self):
498		self.stringMapping = {}
499		for index in range(len(self.strings)):
500			self.stringMapping[self.strings[index]] = index + cffStandardStringCount
501
502
503# The 391 Standard Strings as used in the CFF format.
504# from Adobe Technical None #5176, version 1.0, 18 March 1998
505
506cffStandardStrings = ['.notdef', 'space', 'exclam', 'quotedbl', 'numbersign',
507		'dollar', 'percent', 'ampersand', 'quoteright', 'parenleft', 'parenright',
508		'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one',
509		'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon',
510		'semicolon', 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C',
511		'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
512		'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash',
513		'bracketright', 'asciicircum', 'underscore', 'quoteleft', 'a', 'b', 'c',
514		'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
515		's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright',
516		'asciitilde', 'exclamdown', 'cent', 'sterling', 'fraction', 'yen', 'florin',
517		'section', 'currency', 'quotesingle', 'quotedblleft', 'guillemotleft',
518		'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'endash', 'dagger',
519		'daggerdbl', 'periodcentered', 'paragraph', 'bullet', 'quotesinglbase',
520		'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand',
521		'questiondown', 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve',
522		'dotaccent', 'dieresis', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron',
523		'emdash', 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE', 'ordmasculine', 'ae',
524		'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior',
525		'logicalnot', 'mu', 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn',
526		'onequarter', 'divide', 'brokenbar', 'degree', 'thorn', 'threequarters',
527		'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior',
528		'copyright', 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring',
529		'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex', 'Edieresis', 'Egrave',
530		'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute',
531		'Ocircumflex', 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute',
532		'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute', 'Ydieresis', 'Zcaron',
533		'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', 'ccedilla',
534		'eacute', 'ecircumflex', 'edieresis', 'egrave', 'iacute', 'icircumflex',
535		'idieresis', 'igrave', 'ntilde', 'oacute', 'ocircumflex', 'odieresis',
536		'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', 'udieresis', 'ugrave',
537		'yacute', 'ydieresis', 'zcaron', 'exclamsmall', 'Hungarumlautsmall',
538		'dollaroldstyle', 'dollarsuperior', 'ampersandsmall', 'Acutesmall',
539		'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader',
540		'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle',
541		'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle',
542		'nineoldstyle', 'commasuperior', 'threequartersemdash', 'periodsuperior',
543		'questionsmall', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior',
544		'esuperior', 'isuperior', 'lsuperior', 'msuperior', 'nsuperior', 'osuperior',
545		'rsuperior', 'ssuperior', 'tsuperior', 'ff', 'ffi', 'ffl', 'parenleftinferior',
546		'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall',
547		'Asmall', 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall',
548		'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall',
549		'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', 'Xsmall',
550		'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall',
551		'exclamdownsmall', 'centoldstyle', 'Lslashsmall', 'Scaronsmall', 'Zcaronsmall',
552		'Dieresissmall', 'Brevesmall', 'Caronsmall', 'Dotaccentsmall', 'Macronsmall',
553		'figuredash', 'hypheninferior', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall',
554		'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths',
555		'onethird', 'twothirds', 'zerosuperior', 'foursuperior', 'fivesuperior',
556		'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior',
557		'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', 'fiveinferior',
558		'sixinferior', 'seveninferior', 'eightinferior', 'nineinferior', 'centinferior',
559		'dollarinferior', 'periodinferior', 'commainferior', 'Agravesmall',
560		'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall',
561		'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall',
562		'Edieresissmall', 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall',
563		'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall',
564		'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall',
565		'Ugravesmall', 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall',
566		'Yacutesmall', 'Thornsmall', 'Ydieresissmall', '001.000', '001.001', '001.002',
567		'001.003', 'Black', 'Bold', 'Book', 'Light', 'Medium', 'Regular', 'Roman',
568		'Semibold'
569]
570
571cffStandardStringCount = 391
572assert len(cffStandardStrings) == cffStandardStringCount
573# build reverse mapping
574cffStandardStringMapping = {}
575for _i in range(cffStandardStringCount):
576	cffStandardStringMapping[cffStandardStrings[_i]] = _i
577
578
579