summaryrefslogtreecommitdiff
path: root/bin/reportlab/pdfbase/ttfonts.py
diff options
context:
space:
mode:
Diffstat (limited to 'bin/reportlab/pdfbase/ttfonts.py')
-rw-r--r--bin/reportlab/pdfbase/ttfonts.py1048
1 files changed, 1048 insertions, 0 deletions
diff --git a/bin/reportlab/pdfbase/ttfonts.py b/bin/reportlab/pdfbase/ttfonts.py
new file mode 100644
index 00000000000..d4f67a74239
--- /dev/null
+++ b/bin/reportlab/pdfbase/ttfonts.py
@@ -0,0 +1,1048 @@
+#Copyright ReportLab Europe Ltd. 2000-2004
+#see license.txt for license details
+#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfbase/ttfonts.py
+"""TrueType font support
+
+This defines classes to represent TrueType fonts. They know how to calculate
+their own width and how to write themselves into PDF files. They support
+subsetting and embedding and can represent all 16-bit Unicode characters.
+
+Note on dynamic fonts
+---------------------
+
+Usually a Font in ReportLab corresponds to a fixed set of PDF objects (Font,
+FontDescriptor, Encoding). But with dynamic font subsetting a single TTFont
+will result in a number of Font/FontDescriptor/Encoding object sets, and the
+contents of those will depend on the actual characters used for printing.
+
+To support dynamic font subsetting a concept of "dynamic font" was introduced.
+Dynamic Fonts have a _dynamicFont attribute set to 1. Since other Font object
+may lack a this attribute, you should use constructs like
+
+ if getattr(font, '_dynamicFont', 0):
+ # dynamic font
+ else:
+ # traditional static font
+
+Dynamic fonts have the following additional functions:
+
+ def splitString(self, text, doc):
+ '''Splits text into a number of chunks, each of which belongs to a
+ single subset. Returns a list of tuples (subset, string). Use
+ subset numbers with getSubsetInternalName. Doc is used to identify
+ a document so that different documents may have different dynamically
+ constructed subsets.'''
+
+ def getSubsetInternalName(self, subset, doc):
+ '''Returns the name of a PDF Font object corresponding to a given
+ subset of this dynamic font. Use this function instead of
+ PDFDocument.getInternalFontName.'''
+
+You must never call PDFDocument.getInternalFontName for dynamic fonts.
+
+If you have a traditional static font, mapping to PDF text output operators
+is simple:
+
+ '%s 14 Tf (%s) Tj' % (getInternalFontName(psfontname), text)
+
+If you have a dynamic font, use this instead:
+
+ for subset, chunk in font.splitString(text, doc):
+ '%s 14 Tf (%s) Tj' % (font.getSubsetInternalName(subset, doc), chunk)
+
+(Tf is a font setting operator and Tj is a text ouput operator. You should
+also escape invalid characters in Tj argument, see TextObject._formatText.
+Oh, and that 14 up there is font size.)
+
+Canvas and TextObject have special support for dynamic fonts.
+"""
+
+__version__ = '$Id$'
+
+import string
+from types import StringType
+from struct import pack, unpack
+from cStringIO import StringIO
+from reportlab.pdfbase import pdfmetrics, pdfdoc
+
+def _L2U32(L):
+ return unpack('l',pack('L',L))[0]
+
+class TTFError(pdfdoc.PDFError):
+ "TrueType font exception"
+ pass
+
+#
+# Helpers
+#
+
+from codecs import utf_8_encode, utf_8_decode, latin_1_decode
+parse_utf8=lambda x, decode=utf_8_decode: map(ord,decode(x)[0])
+parse_latin1 = lambda x, decode=latin_1_decode: map(ord,decode(x)[0])
+def latin1_to_utf8(text):
+ "helper to convert when needed from latin input"
+ return utf_8_encode(latin_1_decode(text)[0])[0]
+
+def makeToUnicodeCMap(fontname, subset):
+ """Creates a ToUnicode CMap for a given subset. See Adobe
+ _PDF_Reference (ISBN 0-201-75839-3) for more information."""
+ cmap = [
+ "/CIDInit /ProcSet findresource begin",
+ "12 dict begin",
+ "begincmap",
+ "/CIDSystemInfo",
+ "<< /Registry (%s)" % fontname,
+ "/Ordering (%s)" % fontname,
+ "/Supplement 0",
+ ">> def",
+ "/CMapName /%s def" % fontname,
+ "/CMapType 2 def",
+ "1 begincodespacerange",
+ "<00> <%02X>" % (len(subset) - 1),
+ "endcodespacerange",
+ "%d beginbfchar" % len(subset)
+ ] + map(lambda n, subset=subset: "<%02X> <%04X>" % (n, subset[n]),
+ range(len(subset))) + [
+ "endbfchar",
+ "endcmap",
+ "CMapName currentdict /CMap defineresource pop",
+ "end",
+ "end"
+ ]
+ return string.join(cmap, "\n")
+
+def splice(stream, offset, value):
+ """Splices the given value into stream at the given offset and
+ returns the resulting stream (the original is unchanged)"""
+ return stream[:offset] + value + stream[offset + len(value):]
+
+def _set_ushort(stream, offset, value):
+ """Writes the given unsigned short value into stream at the given
+ offset and returns the resulting stream (the original is unchanged)"""
+ return splice(stream, offset, pack(">H", value))
+
+import sys
+try:
+ import _rl_accel
+except ImportError:
+ try:
+ from reportlab.lib import _rl_accel
+ except ImportError:
+ _rl_accel = None
+
+try:
+ hex32 = _rl_accel.hex32
+except:
+ def hex32(i):
+ return '0X%8.8X' % (long(i)&0xFFFFFFFFL)
+try:
+ add32 = _rl_accel.add32
+except:
+ if sys.hexversion>=0x02030000:
+ def add32(x, y):
+ "Calculate (x + y) modulo 2**32"
+ return _L2U32((long(x)+y) & 0xffffffffL)
+ else:
+ def add32(x, y):
+ "Calculate (x + y) modulo 2**32"
+ lo = (x & 0xFFFF) + (y & 0xFFFF)
+ hi = (x >> 16) + (y >> 16) + (lo >> 16)
+ return (hi << 16) | (lo & 0xFFFF)
+
+try:
+ calcChecksum = _rl_accel.calcChecksum
+except:
+ def calcChecksum(data):
+ """Calculates PDF-style checksums"""
+ if len(data)&3: data = data + (4-(len(data)&3))*"\0"
+ sum = 0
+ for n in unpack(">%dl" % (len(data)>>2), data):
+ sum = add32(sum,n)
+ return sum
+del _rl_accel, sys
+#
+# TrueType font handling
+#
+
+GF_ARG_1_AND_2_ARE_WORDS = 1 << 0
+GF_ARGS_ARE_XY_VALUES = 1 << 1
+GF_ROUND_XY_TO_GRID = 1 << 2
+GF_WE_HAVE_A_SCALE = 1 << 3
+GF_RESERVED = 1 << 4
+GF_MORE_COMPONENTS = 1 << 5
+GF_WE_HAVE_AN_X_AND_Y_SCALE = 1 << 6
+GF_WE_HAVE_A_TWO_BY_TWO = 1 << 7
+GF_WE_HAVE_INSTRUCTIONS = 1 << 8
+GF_USE_MY_METRICS = 1 << 9
+GF_OVERLAP_COMPOUND = 1 << 10
+GF_SCALED_COMPONENT_OFFSET = 1 << 11
+GF_UNSCALED_COMPONENT_OFFSET = 1 << 12
+
+def TTFOpenFile(fn):
+ '''Opens a TTF file possibly after searching TTFSearchPath
+ returns (filename,file)
+ '''
+ from reportlab.lib.utils import rl_isfile, open_for_read
+ try:
+ f = open_for_read(fn,'rb')
+ return fn, f
+ except IOError:
+ import os
+ if not os.path.isabs(fn):
+ from reportlab import rl_config
+ for D in rl_config.TTFSearchPath:
+ tfn = os.path.join(D,fn)
+ if rl_isfile(tfn):
+ f = open_for_read(tfn,'rb')
+ return tfn, f
+ raise TTFError('Can\'t open file "%s"' % fn)
+
+class TTFontParser:
+ "Basic TTF file parser"
+
+ def __init__(self, file, validate=0):
+ """Loads and parses a TrueType font file. file can be a filename or a
+ file object. If validate is set to a false values, skips checksum
+ validation. This can save time, especially if the font is large.
+ """
+
+ # Open the file
+ if type(file) is StringType:
+ self.filename, file = TTFOpenFile(file)
+ else:
+ self.filename = '(ttf)'
+
+ self._ttf_data = file.read()
+ self._pos = 0
+
+ # Read header
+ try:
+ version = self.read_ulong()
+ if version == 0x4F54544F:
+ raise TTFError, 'OpenType fonts with PostScript outlines are not supported'
+ if version != 0x00010000 and version != 0x74727565:
+ raise TTFError, 'Not a TrueType font'
+ except:
+ raise TTFError, 'Not a TrueType font'
+
+ try:
+ self.numTables = self.read_ushort()
+ self.searchRange = self.read_ushort()
+ self.entrySelector = self.read_ushort()
+ self.rangeShift = self.read_ushort()
+
+ # Read table directory
+ self.table = {}
+ self.tables = []
+ for n in range(self.numTables):
+ record = {}
+ record['tag'] = self.read_tag()
+ record['checksum'] = self.read_ulong()
+ record['offset'] = self.read_ulong()
+ record['length'] = self.read_ulong()
+ self.tables.append(record)
+ self.table[record['tag']] = record
+ except:
+ raise TTFError, 'Corrupt TrueType font file'
+
+ if not validate:
+ return
+
+ # Check the checksums for the whole file
+ checkSum = calcChecksum(self._ttf_data)
+ if add32(_L2U32(0xB1B0AFBAL), -checkSum) != 0:
+ raise TTFError, 'Invalid checksum %s len: %d &3: %d' % (hex32(checkSum),len(self._ttf_data),(len(self._ttf_data)&3))
+
+ # Check the checksums for all tables
+ for t in self.tables:
+ table = self.get_chunk(t['offset'], t['length'])
+ checkSum = calcChecksum(table)
+ if t['tag'] == 'head':
+ adjustment = unpack('>l', table[8:8+4])[0]
+ checkSum = add32(checkSum, -adjustment)
+ if t['checksum'] != checkSum:
+ raise TTFError, 'Invalid checksum %s table: %s' % (hex32(checkSum),t['tag'])
+
+ def get_table_pos(self, tag):
+ "Returns the offset and size of a given TTF table."
+ offset = self.table[tag]['offset']
+ length = self.table[tag]['length']
+ return (offset, length)
+
+ def seek(self, pos):
+ "Moves read pointer to a given offset in file."
+ self._pos = pos
+
+ def skip(self, delta):
+ "Skip the given number of bytes."
+ self._pos = self._pos + delta
+
+ def seek_table(self, tag, offset_in_table = 0):
+ """Moves read pointer to the given offset within a given table and
+ returns absolute offset of that position in the file."""
+ self._pos = self.get_table_pos(tag)[0] + offset_in_table
+ return self._pos
+
+ def read_tag(self):
+ "Read a 4-character tag"
+ self._pos = self._pos + 4
+ return self._ttf_data[self._pos - 4:self._pos]
+
+ def read_ushort(self):
+ "Reads an unsigned short"
+ self._pos = self._pos + 2
+ return (ord(self._ttf_data[self._pos - 2]) << 8) + \
+ (ord(self._ttf_data[self._pos - 1]))
+
+ def read_ulong(self):
+ "Reads an unsigned long"
+ self._pos = self._pos + 4
+ return unpack('>l',self._ttf_data[self._pos - 4:self._pos])[0]
+
+ def read_short(self):
+ "Reads a signed short"
+ us = self.read_ushort()
+ if us >= 0x8000:
+ return us - 0x10000
+ else:
+ return us
+
+ def get_ushort(self, pos):
+ "Return an unsigned short at given position"
+ return (ord(self._ttf_data[pos]) << 8) + \
+ (ord(self._ttf_data[pos + 1]))
+
+ def get_ulong(self, pos):
+ "Return an unsigned long at given position"
+ return unpack('>l',self._ttf_data[pos:pos+4])[0]
+
+ def get_chunk(self, pos, length):
+ "Return a chunk of raw data at given position"
+ return self._ttf_data[pos:pos+length]
+
+ def get_table(self, tag):
+ "Return the given TTF table"
+ pos, length = self.get_table_pos(tag)
+ return self._ttf_data[pos:pos+length]
+
+
+class TTFontMaker:
+ "Basic TTF file generator"
+
+ def __init__(self):
+ "Initializes the generator."
+ self.tables = {}
+
+ def add(self, tag, data):
+ "Adds a table to the TTF file."
+ if tag == 'head':
+ data = splice(data, 8, '\0\0\0\0')
+ self.tables[tag] = data
+
+ def makeStream(self):
+ "Finishes the generation and returns the TTF file as a string"
+ stm = StringIO()
+
+ numTables = len(self.tables)
+ searchRange = 1
+ entrySelector = 0
+ while searchRange * 2 <= numTables:
+ searchRange = searchRange * 2
+ entrySelector = entrySelector + 1
+ searchRange = searchRange * 16
+ rangeShift = numTables * 16 - searchRange
+
+ # Header
+ stm.write(pack(">lHHHH", 0x00010000, numTables, searchRange,
+ entrySelector, rangeShift))
+
+ # Table directory
+ tables = self.tables.items()
+ tables.sort() # XXX is this the correct order?
+ offset = 12 + numTables * 16
+ for tag, data in tables:
+ if tag == 'head':
+ head_start = offset
+ checksum = calcChecksum(data)
+ stm.write(tag)
+ stm.write(pack(">LLL", checksum, offset, len(data)))
+ paddedLength = (len(data)+3)&~3
+ offset = offset + paddedLength
+
+ # Table data
+ for tag, data in tables:
+ data = data + "\0\0\0"
+ stm.write(data[:len(data)&~3])
+
+ checksum = calcChecksum(stm.getvalue())
+ checksum = add32(_L2U32(0xB1B0AFBAL), -checksum)
+ stm.seek(head_start + 8)
+ stm.write(pack('>L', checksum))
+
+ return stm.getvalue()
+
+class TTFontFile(TTFontParser):
+ "TTF file parser and generator"
+
+ def __init__(self, file, charInfo=1, validate=0):
+ """Loads and parses a TrueType font file.
+
+ file can be a filename or a file object. If validate is set to a false
+ values, skips checksum validation. This can save time, especially if
+ the font is large. See TTFontFile.extractInfo for more information.
+ """
+ TTFontParser.__init__(self, file, validate=validate)
+ self.extractInfo(charInfo)
+
+ def extractInfo(self, charInfo=1):
+ """Extract typographic information from the loaded font file.
+
+ The following attributes will be set:
+ name - PostScript font name
+ flags - Font flags
+ ascent - Typographic ascender in 1/1000ths of a point
+ descent - Typographic descender in 1/1000ths of a point
+ capHeight - Cap height in 1/1000ths of a point (0 if not available)
+ bbox - Glyph bounding box [l,t,r,b] in 1/1000ths of a point
+ italicAngle - Italic angle in degrees ccw
+ stemV - stem weight in 1/1000ths of a point (approximate)
+ If charInfo is true, the following will also be set:
+ defaultWidth - default glyph width in 1/1000ths of a point
+ charWidths - dictionary of character widths for every supported
+ UCS character code
+
+ This will only work if the font has a Unicode cmap (platform 3,
+ encoding 1, format 4 or platform 0 any encoding format 4). Setting
+ charInfo to false avoids this requirement.
+ """
+ # name - Naming table
+ name_offset = self.seek_table("name")
+ format = self.read_ushort()
+ if format != 0:
+ raise TTFError, "Unknown name table format (%d)" % format
+ numRecords = self.read_ushort()
+ string_data_offset = name_offset + self.read_ushort()
+ names = {1:None,2:None,3:None,4:None,6:None}
+ K = names.keys()
+ nameCount = len(names)
+ for i in range(numRecords):
+ platformId = self.read_ushort()
+ encodingId = self.read_ushort()
+ languageId = self.read_ushort()
+ nameId = self.read_ushort()
+ length = self.read_ushort()
+ offset = self.read_ushort()
+ if nameId not in K: continue
+ N = None
+ if platformId == 3 and encodingId == 1 and languageId == 0x409: # Microsoft, Unicode, US English, PS Name
+ self.seek(string_data_offset + offset)
+ if length % 2 != 0:
+ raise TTFError, "PostScript name is UTF-16BE string of odd length"
+ length /= 2
+ N = []
+ A = N.append
+ while length > 0:
+ char = self.read_ushort()
+ A(chr(char))
+ length -= 1
+ N = ''.join(N)
+ elif platformId == 1 and encodingId == 0 and languageId == 0: # Macintosh, Roman, English, PS Name
+ # According to OpenType spec, if PS name exists, it must exist
+ # both in MS Unicode and Macintosh Roman formats. Apparently,
+ # you can find live TTF fonts which only have Macintosh format.
+ N = self.get_chunk(string_data_offset + offset, length)
+ if N and names[nameId]==None:
+ names[nameId] = N
+ nameCount -= 1
+ if nameCount==0: break
+ psName = names[6]
+ if not psName:
+ raise TTFError, "Could not find PostScript font name"
+ for c in psName:
+ oc = ord(c)
+ if oc<33 or oc>126 or c in ('[', ']', '(', ')', '{', '}', '<', '>', '/', '%'):
+ raise TTFError, "psName contains invalid character '%s' ie U+%04X" % (c,ord(c))
+ self.name = psName
+ self.familyName = names[1] or psName
+ self.styleName = names[2] or 'Regular'
+ self.fullName = names[4] or psName
+ self.uniqueFontID = names[3] or psName
+
+ # head - Font header table
+ self.seek_table("head")
+ ver_maj, ver_min = self.read_ushort(), self.read_ushort()
+ if ver_maj != 1:
+ raise TTFError, 'Unknown head table version %d.%04x' % (ver_maj, ver_min)
+ self.skip(8)
+ magic = self.read_ulong()
+ if magic != 0x5F0F3CF5:
+ raise TTFError, 'Invalid head table magic %04x' % magic
+ self.skip(2)
+ unitsPerEm = self.read_ushort()
+ scale = lambda x, unitsPerEm=unitsPerEm: x * 1000 / unitsPerEm
+ self.skip(16)
+ xMin = self.read_short()
+ yMin = self.read_short()
+ xMax = self.read_short()
+ yMax = self.read_short()
+ self.bbox = map(scale, [xMin, yMin, xMax, yMax])
+ self.skip(3*2)
+ indexToLocFormat = self.read_ushort()
+ glyphDataFormat = self.read_ushort()
+
+ # OS/2 - OS/2 and Windows metrics table
+ # (needs data from head table)
+ if self.table.has_key("OS/2"):
+ self.seek_table("OS/2")
+ version = self.read_ushort()
+ self.skip(2)
+ usWeightClass = self.read_ushort()
+ self.skip(2)
+ fsType = self.read_ushort()
+ if fsType == 0x0002 or (fsType & 0x0300) != 0:
+ raise TTFError, 'Font does not allow subsetting/embedding (%04X)' % fsType
+ self.skip(11*2 + 10 + 4*4 + 4 + 3*2)
+ sTypoAscender = self.read_short()
+ sTypoDescender = self.read_short()
+ self.ascent = scale(sTypoAscender) # XXX: for some reason it needs to be multiplied by 1.24--1.28
+ self.descent = scale(sTypoDescender)
+
+ if version > 1:
+ self.skip(3*2 + 2*4 + 2)
+ sCapHeight = self.read_short()
+ self.capHeight = scale(sCapHeight)
+ else:
+ self.capHeight = self.ascent
+ else:
+ # Microsoft TTFs require an OS/2 table; Apple ones do not. Try to
+ # cope. The data is not very important anyway.
+ usWeightClass = 500
+ self.ascent = scale(yMax)
+ self.descent = scale(yMin)
+ self.capHeight = self.ascent
+
+ # There's no way to get stemV from a TTF file short of analyzing actual outline data
+ # This fuzzy formula is taken from pdflib sources, but we could just use 0 here
+ self.stemV = 50 + int((usWeightClass / 65.0) ** 2)
+
+ # post - PostScript table
+ # (needs data from OS/2 table)
+ self.seek_table("post")
+ ver_maj, ver_min = self.read_ushort(), self.read_ushort()
+ if ver_maj not in (1, 2, 3, 4):
+ # Adobe/MS documents 1, 2, 2.5, 3; Apple also has 4.
+ # From Apple docs it seems that we do not need to care
+ # about the exact version, so if you get this error, you can
+ # try to remove this check altogether.
+ raise TTFError, 'Unknown post table version %d.%04x' % (ver_maj, ver_min)
+ self.italicAngle = self.read_short() + self.read_ushort() / 65536.0
+ self.skip(2*2)
+ isFixedPitch = self.read_ulong()
+
+ self.flags = FF_SYMBOLIC # All fonts that contain characters
+ # outside the original Adobe character
+ # set are considered "symbolic".
+ if self.italicAngle != 0:
+ self.flags = self.flags | FF_ITALIC
+ if usWeightClass >= 600: # FW_REGULAR == 500, FW_SEMIBOLD == 600
+ self.flags = self.flags | FF_FORCEBOLD
+ if isFixedPitch:
+ self.flags = self.flags | FF_FIXED
+ # XXX: FF_SERIF? FF_SCRIPT? FF_ALLCAP? FF_SMALLCAP?
+
+ # hhea - Horizontal header table
+ self.seek_table("hhea")
+ ver_maj, ver_min = self.read_ushort(), self.read_ushort()
+ if ver_maj != 1:
+ raise TTFError, 'Unknown hhea table version %d.%04x' % (ver_maj, ver_min)
+ self.skip(28)
+ metricDataFormat = self.read_ushort()
+ if metricDataFormat != 0:
+ raise TTFError, 'Unknown horizontal metric data format (%d)' % metricDataFormat
+ numberOfHMetrics = self.read_ushort()
+ if numberOfHMetrics == 0:
+ raise TTFError, 'Number of horizontal metrics is 0'
+
+ # maxp - Maximum profile table
+ self.seek_table("maxp")
+ ver_maj, ver_min = self.read_ushort(), self.read_ushort()
+ if ver_maj != 1:
+ raise TTFError, 'Unknown maxp table version %d.%04x' % (ver_maj, ver_min)
+ numGlyphs = self.read_ushort()
+
+ if not charInfo:
+ self.charToGlyph = None
+ self.defaultWidth = None
+ self.charWidths = None
+ return
+
+ if glyphDataFormat != 0:
+ raise TTFError, 'Unknown glyph data format (%d)' % glyphDataFormat
+
+ # cmap - Character to glyph index mapping table
+ cmap_offset = self.seek_table("cmap")
+ self.skip(2)
+ cmapTableCount = self.read_ushort()
+ unicode_cmap_offset = None
+ for n in range(cmapTableCount):
+ platformID = self.read_ushort()
+ encodingID = self.read_ushort()
+ offset = self.read_ulong()
+ if platformID == 3 and encodingID == 1: # Microsoft, Unicode
+ format = self.get_ushort(cmap_offset + offset)
+ if format == 4:
+ unicode_cmap_offset = cmap_offset + offset
+ break
+ elif platformID == 0: # Unicode -- assume all encodings are compatible
+ format = self.get_ushort(cmap_offset + offset)
+ if format == 4:
+ unicode_cmap_offset = cmap_offset + offset
+ break
+ if unicode_cmap_offset is None:
+ raise TTFError, 'Font does not have cmap for Unicode (platform 3, encoding 1, format 4 or platform 0 any encoding format 4)'
+ self.seek(unicode_cmap_offset + 2)
+ length = self.read_ushort()
+ limit = unicode_cmap_offset + length
+ self.skip(2)
+ segCount = self.read_ushort() / 2
+ self.skip(6)
+ endCount = map(lambda x, self=self: self.read_ushort(), range(segCount))
+ self.skip(2)
+ startCount = map(lambda x, self=self: self.read_ushort(), range(segCount))
+ idDelta = map(lambda x, self=self: self.read_short(), range(segCount))
+ idRangeOffset_start = self._pos
+ idRangeOffset = map(lambda x, self=self: self.read_ushort(), range(segCount))
+
+ # Now it gets tricky.
+ glyphToChar = {}
+ charToGlyph = {}
+ for n in range(segCount):
+ for unichar in range(startCount[n], endCount[n] + 1):
+ if idRangeOffset[n] == 0:
+ glyph = (unichar + idDelta[n]) & 0xFFFF
+ else:
+ offset = (unichar - startCount[n]) * 2 + idRangeOffset[n]
+ offset = idRangeOffset_start + 2 * n + offset
+ if offset >= limit:
+ # workaround for broken fonts (like Thryomanes)
+ glyph = 0
+ else:
+ glyph = self.get_ushort(offset)
+ if glyph != 0:
+ glyph = (glyph + idDelta[n]) & 0xFFFF
+ charToGlyph[unichar] = glyph
+ if glyphToChar.has_key(glyph):
+ glyphToChar[glyph].append(unichar)
+ else:
+ glyphToChar[glyph] = [unichar]
+ self.charToGlyph = charToGlyph
+
+ # hmtx - Horizontal metrics table
+ # (needs data from hhea, maxp, and cmap tables)
+ self.seek_table("hmtx")
+ aw = None
+ self.charWidths = {}
+ self.hmetrics = []
+ for glyph in range(numberOfHMetrics):
+ # advance width and left side bearing. lsb is actually signed
+ # short, but we don't need it anyway (except for subsetting)
+ aw, lsb = self.read_ushort(), self.read_ushort()
+ self.hmetrics.append((aw, lsb))
+ aw = scale(aw)
+ if glyph == 0:
+ self.defaultWidth = aw
+ if glyphToChar.has_key(glyph):
+ for char in glyphToChar[glyph]:
+ self.charWidths[char] = aw
+ for glyph in range(numberOfHMetrics, numGlyphs):
+ # the rest of the table only lists advance left side bearings.
+ # so we reuse aw set by the last iteration of the previous loop
+ lsb = self.read_ushort()
+ self.hmetrics.append((aw, lsb))
+ if glyphToChar.has_key(glyph):
+ for char in glyphToChar[glyph]:
+ self.charWidths[char] = aw
+
+ # loca - Index to location
+ self.seek_table('loca')
+ self.glyphPos = []
+ if indexToLocFormat == 0:
+ for n in range(numGlyphs + 1):
+ self.glyphPos.append(self.read_ushort() << 1)
+ elif indexToLocFormat == 1:
+ for n in range(numGlyphs + 1):
+ self.glyphPos.append(self.read_ulong())
+ else:
+ raise TTFError, 'Unknown location table format (%d)' % indexToLocFormat
+
+ # Subsetting
+
+ def makeSubset(self, subset):
+ """Create a subset of a TrueType font"""
+ output = TTFontMaker()
+
+ # Build a mapping of glyphs in the subset to glyph numbers in
+ # the original font. Also build a mapping of UCS codes to
+ # glyph values in the new font.
+
+ # Start with 0 -> 0: "missing character"
+ glyphMap = [0] # new glyph index -> old glyph index
+ glyphSet = {0:0} # old glyph index -> new glyph index
+ codeToGlyph = {} # unicode -> new glyph index
+ for code in subset:
+ if self.charToGlyph.has_key(code):
+ originalGlyphIdx = self.charToGlyph[code]
+ else:
+ originalGlyphIdx = 0
+ if not glyphSet.has_key(originalGlyphIdx):
+ glyphSet[originalGlyphIdx] = len(glyphMap)
+ glyphMap.append(originalGlyphIdx)
+ codeToGlyph[code] = glyphSet[originalGlyphIdx]
+
+ # Also include glyphs that are parts of composite glyphs
+ start = self.get_table_pos('glyf')[0]
+ n = 0
+ while n < len(glyphMap):
+ originalGlyphIdx = glyphMap[n]
+ glyphPos = self.glyphPos[originalGlyphIdx]
+ glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos
+ self.seek(start + glyphPos)
+ numberOfContours = self.read_short()
+ if numberOfContours < 0:
+ # composite glyph
+ self.skip(8)
+ flags = GF_MORE_COMPONENTS
+ while flags & GF_MORE_COMPONENTS:
+ flags = self.read_ushort()
+ glyphIdx = self.read_ushort()
+ if not glyphSet.has_key(glyphIdx):
+ glyphSet[glyphIdx] = len(glyphMap)
+ glyphMap.append(glyphIdx)
+ if flags & GF_ARG_1_AND_2_ARE_WORDS:
+ self.skip(4)
+ else:
+ self.skip(2)
+ if flags & GF_WE_HAVE_A_SCALE:
+ self.skip(2)
+ elif flags & GF_WE_HAVE_AN_X_AND_Y_SCALE:
+ self.skip(4)
+ elif flags & GF_WE_HAVE_A_TWO_BY_TWO:
+ self.skip(8)
+ n = n + 1
+
+ numGlyphs = n = len(glyphMap)
+ while n > 1 and self.hmetrics[n][0] == self.hmetrics[n - 1][0]:
+ n = n - 1
+ numberOfHMetrics = n
+
+ # The following tables are simply copied from the original
+ for tag in ('name', 'OS/2', 'cvt ', 'fpgm', 'prep'):
+ try:
+ output.add(tag, self.get_table(tag))
+ except KeyError:
+ # Apparently some of the tables are optional (cvt, fpgm, prep).
+ # The lack of the required ones (name, OS/2) would have already
+ # been caught before.
+ pass
+
+ # post - PostScript
+ post = "\x00\x03\x00\x00" + self.get_table('post')[4:16] + "\x00" * 16
+ output.add('post', post)
+
+ # hhea - Horizontal Header
+ hhea = self.get_table('hhea')
+ hhea = _set_ushort(hhea, 34, numberOfHMetrics)
+ output.add('hhea', hhea)
+
+ # maxp - Maximum Profile
+ maxp = self.get_table('maxp')
+ maxp = _set_ushort(maxp, 4, numGlyphs)
+ output.add('maxp', maxp)
+
+ # cmap - Character to glyph mapping
+ # XXX maybe use format 0 if possible, not 6?
+ entryCount = len(subset)
+ length = 10 + entryCount * 2
+ cmap = [0, 1, # version, number of tables
+ 1, 0, 0,12, # platform, encoding, offset (hi,lo)
+ 6, length, 0, # format, length, language
+ 0,
+ entryCount] + \
+ map(codeToGlyph.get, subset)
+ cmap = apply(pack, [">%dH" % len(cmap)] + cmap)
+ output.add('cmap', cmap)
+
+ # hmtx - Horizontal Metrics
+ hmtx = []
+ for n in range(numGlyphs):
+ originalGlyphIdx = glyphMap[n]
+ aw, lsb = self.hmetrics[originalGlyphIdx]
+ if n < numberOfHMetrics:
+ hmtx.append(aw)
+ hmtx.append(lsb)
+ hmtx = apply(pack, [">%dH" % len(hmtx)] + hmtx)
+ output.add('hmtx', hmtx)
+
+ # glyf - Glyph data
+ glyphData = self.get_table('glyf')
+ offsets = []
+ glyf = []
+ pos = 0
+ for n in range(numGlyphs):
+ offsets.append(pos)
+ originalGlyphIdx = glyphMap[n]
+ glyphPos = self.glyphPos[originalGlyphIdx]
+ glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos
+ data = glyphData[glyphPos:glyphPos+glyphLen]
+ # Fix references in composite glyphs
+ if glyphLen > 2 and unpack(">h", data[:2])[0] < 0:
+ # composite glyph
+ pos_in_glyph = 10
+ flags = GF_MORE_COMPONENTS
+ while flags & GF_MORE_COMPONENTS:
+ flags = unpack(">H", data[pos_in_glyph:pos_in_glyph+2])[0]
+ glyphIdx = unpack(">H", data[pos_in_glyph+2:pos_in_glyph+4])[0]
+ data = _set_ushort(data, pos_in_glyph + 2, glyphSet[glyphIdx])
+ pos_in_glyph = pos_in_glyph + 4
+ if flags & GF_ARG_1_AND_2_ARE_WORDS:
+ pos_in_glyph = pos_in_glyph + 4
+ else:
+ pos_in_glyph = pos_in_glyph + 2
+ if flags & GF_WE_HAVE_A_SCALE:
+ pos_in_glyph = pos_in_glyph + 2
+ elif flags & GF_WE_HAVE_AN_X_AND_Y_SCALE:
+ pos_in_glyph = pos_in_glyph + 4
+ elif flags & GF_WE_HAVE_A_TWO_BY_TWO:
+ pos_in_glyph = pos_in_glyph + 8
+ glyf.append(data)
+ pos = pos + glyphLen
+ if pos % 4 != 0:
+ padding = 4 - pos % 4
+ glyf.append('\0' * padding)
+ pos = pos + padding
+ offsets.append(pos)
+ output.add('glyf', string.join(glyf, ""))
+
+ # loca - Index to location
+ loca = []
+ if (pos + 1) >> 1 > 0xFFFF:
+ indexToLocFormat = 1 # long format
+ for offset in offsets:
+ loca.append(offset)
+ loca = apply(pack, [">%dL" % len(loca)] + loca)
+ else:
+ indexToLocFormat = 0 # short format
+ for offset in offsets:
+ loca.append(offset >> 1)
+ loca = apply(pack, [">%dH" % len(loca)] + loca)
+ output.add('loca', loca)
+
+ # head - Font header
+ head = self.get_table('head')
+ head = _set_ushort(head, 50, indexToLocFormat)
+ output.add('head', head)
+
+ return output.makeStream()
+
+
+#
+# TrueType font embedding
+#
+
+# PDF font flags (see PDF Reference Guide table 5.19)
+FF_FIXED = 1 << 1-1
+FF_SERIF = 1 << 2-1
+FF_SYMBOLIC = 1 << 3-1
+FF_SCRIPT = 1 << 4-1
+FF_NONSYMBOLIC = 1 << 6-1
+FF_ITALIC = 1 << 7-1
+FF_ALLCAP = 1 << 17-1
+FF_SMALLCAP = 1 << 18-1
+FF_FORCEBOLD = 1 << 19-1
+
+class TTFontFace(TTFontFile, pdfmetrics.TypeFace):
+ """TrueType typeface.
+
+ Conceptually similar to a single byte typeface, but the glyphs are
+ identified by UCS character codes instead of glyph names."""
+
+ def __init__(self, filename, validate=0):
+ "Loads a TrueType font from filename."
+ pdfmetrics.TypeFace.__init__(self, None)
+ TTFontFile.__init__(self, filename, validate=validate)
+
+ def getCharWidth(self, code):
+ "Returns the width of character U+<code>"
+ return self.charWidths.get(code, self.defaultWidth)
+
+ def addSubsetObjects(self, doc, fontname, subset):
+ """Generate a TrueType font subset and add it to the PDF document.
+ Returns a PDFReference to the new FontDescriptor object."""
+
+ fontFile = pdfdoc.PDFStream()
+ fontFile.content = self.makeSubset(subset)
+ fontFile.dictionary['Length1'] = len(fontFile.content)
+ if doc.compression:
+ fontFile.filters = [pdfdoc.PDFZCompress]
+ fontFileRef = doc.Reference(fontFile, 'fontFile:%s(%s)' % (self.filename, fontname))
+
+ flags = self.flags & ~ FF_NONSYMBOLIC
+ flags = flags | FF_SYMBOLIC
+
+ fontDescriptor = pdfdoc.PDFDictionary({
+ 'Type': '/FontDescriptor',
+ 'Ascent': self.ascent,
+ 'CapHeight': self.capHeight,
+ 'Descent': self.descent,
+ 'Flags': flags,
+ 'FontBBox': pdfdoc.PDFArray(self.bbox),
+ 'FontName': pdfdoc.PDFName(fontname),
+ 'ItalicAngle': self.italicAngle,
+ 'StemV': self.stemV,
+ 'FontFile2': fontFileRef,
+ })
+ return doc.Reference(fontDescriptor, 'fontDescriptor:' + fontname)
+
+class TTEncoding:
+ """Encoding for TrueType fonts (always UTF-8).
+
+ TTEncoding does not directly participate in PDF object creation, since
+ we need a number of different 8-bit encodings for every generated font
+ subset. TTFont itself cares about that."""
+
+ def __init__(self):
+ self.name = "UTF-8"
+
+
+class TTFont:
+ """Represents a TrueType font.
+
+ Its encoding is always UTF-8.
+
+ Note: you cannot use the same TTFont object for different documents
+ at the same time.
+
+ Example of usage:
+
+ font = ttfonts.TTFont('PostScriptFontName', '/path/to/font.ttf')
+ pdfmetrics.registerFont(font)
+
+ canvas.setFont('PostScriptFontName', size)
+ canvas.drawString(x, y, "Some text encoded in UTF-8")
+ """
+
+ class State:
+ def __init__(self):
+ self.assignments = {}
+ self.nextCode = 0
+ self.subsets = []
+ self.internalName = None
+ self.frozen = 0
+
+ def __init__(self, name, filename, validate=0):
+ """Loads a TrueType font from filename.
+
+ If validate is set to a false values, skips checksum validation. This
+ can save time, especially if the font is large.
+ """
+ self.fontName = name
+ self.face = TTFontFace(filename, validate=validate)
+ self.encoding = TTEncoding()
+ self._multiByte = 1 # We want our own stringwidth
+ self._dynamicFont = 1 # We want dynamic subsetting
+ self.state = {}
+
+ def stringWidth(self, text, size):
+ "Calculate text width"
+ width = self.face.getCharWidth
+ w = 0
+ for code in parse_utf8(text):
+ w = w + width(code)
+ return 0.001 * w * size
+
+ def splitString(self, text, doc):
+ """Splits text into a number of chunks, each of which belongs to a
+ single subset. Returns a list of tuples (subset, string). Use subset
+ numbers with getSubsetInternalName. Doc is needed for distinguishing
+ subsets when building different documents at the same time."""
+ try: state = self.state[doc]
+ except KeyError: state = self.state[doc] = TTFont.State()
+ curSet = -1
+ cur = []
+ results = []
+ for code in parse_utf8(text):
+ if state.assignments.has_key(code):
+ n = state.assignments[code]
+ else:
+ if state.frozen:
+ raise pdfdoc.PDFError, "Font %s is already frozen, cannot add new character U+%04X" % (self.fontName, code)
+ n = state.nextCode
+ state.nextCode = state.nextCode + 1
+ state.assignments[code] = n
+ if (n & 0xFF) == 0:
+ state.subsets.append([])
+ state.subsets[n >> 8].append(code)
+ if (n >> 8) != curSet:
+ if cur:
+ results.append((curSet, string.join(map(chr, cur), "")))
+ curSet = (n >> 8)
+ cur = []
+ cur.append(n & 0xFF)
+ if cur:
+ results.append((curSet, string.join(map(chr, cur), "")))
+ return results
+
+ def getSubsetInternalName(self, subset, doc):
+ """Returns the name of a PDF Font object corresponding to a given
+ subset of this dynamic font. Use this function instead of
+ PDFDocument.getInternalFontName."""
+ try: state = self.state[doc]
+ except KeyError: state = self.state[doc] = TTFont.State()
+ if subset < 0 or subset >= len(state.subsets):
+ raise IndexError, 'Subset %d does not exist in font %s' % (subset, self.fontName)
+ if state.internalName is None:
+ state.internalName = 'F%d' % (len(doc.fontMapping) + 1)
+ doc.fontMapping[self.fontName] = '/' + state.internalName
+ doc.delayedFonts.append(self)
+ return '/%s+%d' % (state.internalName, subset)
+
+ def addObjects(self, doc):
+ """Makes one or more PDF objects to be added to the document. The
+ caller supplies the internal name to be used (typically F1, F2, ... in
+ sequence).
+
+ This method creates a number of Font and FontDescriptor objects. Every
+ FontDescriptor is a (no more than) 256 character subset of the original
+ TrueType font."""
+ try: state = self.state[doc]
+ except KeyError: state = self.state[doc] = TTFont.State()
+ state.frozen = 1
+ for n in range(len(state.subsets)):
+ subset = state.subsets[n]
+ internalName = self.getSubsetInternalName(n, doc)[1:]
+ baseFontName = "SUBSET+%s+%d" % (self.face.name, n)
+
+ pdfFont = pdfdoc.PDFTrueTypeFont()
+ pdfFont.__Comment__ = 'Font %s subset %d' % (self.fontName, n)
+ pdfFont.Name = internalName
+ pdfFont.BaseFont = baseFontName
+
+ pdfFont.FirstChar = 0
+ pdfFont.LastChar = len(subset) - 1
+
+ widths = map(self.face.getCharWidth, subset)
+ pdfFont.Widths = pdfdoc.PDFArray(widths)
+
+ cmapStream = pdfdoc.PDFStream()
+ cmapStream.content = makeToUnicodeCMap(baseFontName, subset)
+ if doc.compression:
+ cmapStream.filters = [pdfdoc.PDFZCompress]
+ pdfFont.ToUnicode = doc.Reference(cmapStream, 'toUnicodeCMap:' + baseFontName)
+
+ pdfFont.FontDescriptor = self.face.addSubsetObjects(doc, baseFontName, subset)
+
+ # link it in
+ ref = doc.Reference(pdfFont, internalName)
+ fontDict = doc.idToObject['BasicFonts'].dict
+ fontDict[internalName] = pdfFont
+ del self.state[doc]