FontCustom/fontcustom

View on GitHub
lib/fontcustom/scripts/eotlitetool.py

Summary

Maintainability
A
1 hr
Test Coverage
#!/usr/bin/env python
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is font utility code.
#
# The Initial Developer of the Original Code is Mozilla Corporation.
# Portions created by the Initial Developer are Copyright (C) 2009
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#   John Daggett <jdaggett@mozilla.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK ***** */

# eotlitetool.py - create EOT version of OpenType font for use with IE
#
# Usage: eotlitetool.py [-o output-filename] font1 [font2 ...]
#

# OpenType file structure
# http://www.microsoft.com/typography/otspec/otff.htm
# 
# Types:
# 
# BYTE            8-bit unsigned integer.
# CHAR            8-bit signed integer.
# USHORT          16-bit unsigned integer.
# SHORT           16-bit signed integer.
# ULONG           32-bit unsigned integer.
# Fixed           32-bit signed fixed-point number (16.16)
# LONGDATETIME    Date represented in number of seconds since 12:00 midnight, January 1, 1904. The value is represented as a signed 64-bit integer.
# 
# SFNT Header
# 
# Fixed   sfnt version         // 0x00010000 for version 1.0.
# USHORT  numTables            // Number of tables.
# USHORT  searchRange          // (Maximum power of 2 <= numTables) x 16.
# USHORT  entrySelector        // Log2(maximum power of 2 <= numTables).
# USHORT  rangeShift           // NumTables x 16-searchRange.
# 
# Table Directory
# 
# ULONG   tag                  // 4-byte identifier.
# ULONG   checkSum             // CheckSum for this table.
# ULONG   offset               // Offset from beginning of TrueType font file.
# ULONG   length               // Length of this table.
# 
# OS/2 Table (Version 4)
# 
# USHORT  version              // 0x0004
# SHORT   xAvgCharWidth 
# USHORT  usWeightClass    
# USHORT  usWidthClass     
# USHORT  fsType   
# SHORT   ySubscriptXSize      
# SHORT   ySubscriptYSize      
# SHORT   ySubscriptXOffset    
# SHORT   ySubscriptYOffset    
# SHORT   ySuperscriptXSize    
# SHORT   ySuperscriptYSize    
# SHORT   ySuperscriptXOffset      
# SHORT   ySuperscriptYOffset      
# SHORT   yStrikeoutSize   
# SHORT   yStrikeoutPosition   
# SHORT   sFamilyClass     
# BYTE    panose[10]   
# ULONG   ulUnicodeRange1      // Bits 0-31
# ULONG   ulUnicodeRange2      // Bits 32-63
# ULONG   ulUnicodeRange3      // Bits 64-95
# ULONG   ulUnicodeRange4      // Bits 96-127
# CHAR    achVendID[4]     
# USHORT  fsSelection      
# USHORT  usFirstCharIndex     
# USHORT  usLastCharIndex      
# SHORT   sTypoAscender    
# SHORT   sTypoDescender   
# SHORT   sTypoLineGap     
# USHORT  usWinAscent      
# USHORT  usWinDescent     
# ULONG   ulCodePageRange1      // Bits 0-31
# ULONG   ulCodePageRange2      // Bits 32-63
# SHORT   sxHeight     
# SHORT   sCapHeight   
# USHORT  usDefaultChar    
# USHORT  usBreakChar      
# USHORT  usMaxContext     
# 
# 
# The Naming Table is organized as follows:
# 
# [name table header]
# [name records]
# [string data]
# 
# Name Table Header
# 
# USHORT  format               // Format selector (=0).
# USHORT  count                // Number of name records.
# USHORT  stringOffset         // Offset to start of string storage (from start of table).
# 
# Name Record
# 
# USHORT  platformID           // Platform ID.
# USHORT  encodingID           // Platform-specific encoding ID.
# USHORT  languageID           // Language ID.
# USHORT  nameID               // Name ID.
# USHORT  length               // String length (in bytes).
# USHORT  offset               // String offset from start of storage area (in bytes).
# 
# head Table
# 
# Fixed   tableVersion         // Table version number     0x00010000 for version 1.0.
# Fixed   fontRevision         // Set by font manufacturer.
# ULONG   checkSumAdjustment   // To compute: set it to 0, sum the entire font as ULONG, then store 0xB1B0AFBA - sum.
# ULONG   magicNumber          // Set to 0x5F0F3CF5.
# USHORT  flags   
# USHORT  unitsPerEm           // Valid range is from 16 to 16384. This value should be a power of 2 for fonts that have TrueType outlines.
# LONGDATETIME    created      // Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer
# LONGDATETIME    modified     // Number of seconds since 12:00 midnight, January 1, 1904. 64-bit integer
# SHORT   xMin                 // For all glyph bounding boxes.
# SHORT   yMin    
# SHORT   xMax    
# SHORT   yMax    
# USHORT  macStyle
# USHORT  lowestRecPPEM        // Smallest readable size in pixels.
# SHORT   fontDirectionHint
# SHORT   indexToLocFormat     // 0 for short offsets, 1 for long.
# SHORT   glyphDataFormat      // 0 for current format.
# 
# 
# 
# Embedded OpenType (EOT) file format
# http://www.w3.org/Submission/EOT/
# 
# EOT version 0x00020001
# 
# An EOT font consists of a header with the original OpenType font
# appended at the end.  Most of the data in the EOT header is simply a
# copy of data from specific tables within the font data.  The exceptions
# are the 'Flags' field and the root string name field.  The root string
# is a set of names indicating domains for which the font data can be
# used.  A null root string implies the font data can be used anywhere.
# The EOT header is in little-endian byte order but the font data remains
# in big-endian order as specified by the OpenType spec.
# 
# Overall structure:
# 
#   [EOT header]
#   [EOT name records]
#   [font data]
# 
# EOT header
# 
# ULONG   eotSize              // Total structure length in bytes (including string and font data)
# ULONG   fontDataSize         // Length of the OpenType font (FontData) in bytes
# ULONG   version              // Version number of this format - 0x00020001
# ULONG   flags                // Processing Flags (0 == no special processing)
# BYTE    fontPANOSE[10]       // OS/2 Table panose
# BYTE    charset              // DEFAULT_CHARSET (0x01)
# BYTE    italic               // 0x01 if ITALIC in OS/2 Table fsSelection is set, 0 otherwise
# ULONG   weight               // OS/2 Table usWeightClass
# USHORT  fsType               // OS/2 Table fsType (specifies embedding permission flags)
# USHORT  magicNumber          // Magic number for EOT file - 0x504C.
# ULONG   unicodeRange1        // OS/2 Table ulUnicodeRange1
# ULONG   unicodeRange2        // OS/2 Table ulUnicodeRange2
# ULONG   unicodeRange3        // OS/2 Table ulUnicodeRange3
# ULONG   unicodeRange4        // OS/2 Table ulUnicodeRange4
# ULONG   codePageRange1       // OS/2 Table ulCodePageRange1
# ULONG   codePageRange2       // OS/2 Table ulCodePageRange2
# ULONG   checkSumAdjustment   // head Table CheckSumAdjustment
# ULONG   reserved[4]          // Reserved - must be 0
# USHORT  padding1             // Padding - must be 0
# 
# EOT name records
# 
# USHORT  FamilyNameSize       // Font family name size in bytes
# BYTE    FamilyName[FamilyNameSize] // Font family name (name ID = 1), little-endian UTF-16
# USHORT  Padding2             // Padding - must be 0
# 
# USHORT  StyleNameSize        // Style name size in bytes
# BYTE    StyleName[StyleNameSize]  // Style name (name ID = 2), little-endian UTF-16
# USHORT  Padding3             // Padding - must be 0
# 
# USHORT  VersionNameSize      // Version name size in bytes
# bytes   VersionName[VersionNameSize]  // Version name (name ID = 5), little-endian UTF-16
# USHORT  Padding4             // Padding - must be 0
# 
# USHORT  FullNameSize         // Full name size in bytes
# BYTE    FullName[FullNameSize]  // Full name (name ID = 4), little-endian UTF-16
# USHORT  Padding5             // Padding - must be 0
# 
# USHORT  RootStringSize       // Root string size in bytes
# BYTE    RootString[RootStringSize]  // Root string, little-endian UTF-16



import optparse
import struct

class FontError(Exception):
    """Error related to font handling"""
    pass

def multichar(str):
    vals = struct.unpack('4B', str[:4].encode('utf-8'))
    return (vals[0] << 24) + (vals[1] << 16) + (vals[2] << 8) + vals[3]

def multicharval(v):
    return struct.pack('4B', (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF)

class EOT:
    EOT_VERSION = 0x00020001
    EOT_MAGIC_NUMBER = 0x504c
    EOT_DEFAULT_CHARSET = 0x01
    EOT_FAMILY_NAME_INDEX = 0    # order of names in variable portion of EOT header
    EOT_STYLE_NAME_INDEX = 1
    EOT_VERSION_NAME_INDEX = 2
    EOT_FULL_NAME_INDEX = 3
    EOT_NUM_NAMES = 4
    
    EOT_HEADER_PACK = '<4L10B2BL2H7L18x'

class OpenType:
    SFNT_CFF = multichar('OTTO')            # Postscript CFF SFNT version
    SFNT_TRUE = 0x10000                     # Standard TrueType version
    SFNT_APPLE = multichar('true')          # Apple TrueType version
    
    SFNT_UNPACK = '>I4H'
    TABLE_DIR_UNPACK = '>4I'
    
    TABLE_HEAD = multichar('head')          # TrueType table tags
    TABLE_NAME = multichar('name')
    TABLE_OS2 = multichar('OS/2')
    TABLE_GLYF = multichar('glyf')
    TABLE_CFF = multichar('CFF ')
    
    OS2_FSSELECTION_ITALIC = 0x1
    OS2_UNPACK = '>4xH2xH22x10B4L4xH14x2L'
    
    HEAD_UNPACK = '>8xL'
    
    NAME_RECORD_UNPACK = '>6H'
    NAME_ID_FAMILY = 1
    NAME_ID_STYLE = 2
    NAME_ID_UNIQUE = 3
    NAME_ID_FULL = 4
    NAME_ID_VERSION = 5
    NAME_ID_POSTSCRIPT = 6
    PLATFORM_ID_UNICODE = 0                 # Mac OS uses this typically
    PLATFORM_ID_MICROSOFT = 3
    ENCODING_ID_MICROSOFT_UNICODEBMP = 1    # with Microsoft platformID BMP-only Unicode encoding
    LANG_ID_MICROSOFT_EN_US = 0x0409         # with Microsoft platformID EN US lang code

def eotname(ttf):
    i = ttf.rfind('.')
    if i != -1:
        ttf = ttf[:i]
    return ttf + '.eotlite'

def readfont(f):
    data = open(f, 'rb').read()
    return data

def get_table_directory(data):
    """read the SFNT header and table directory"""
    datalen = len(data)
    sfntsize = struct.calcsize(OpenType.SFNT_UNPACK)
    if sfntsize > datalen:
        raise FontError('truncated font data')
    sfntvers, numTables = struct.unpack(OpenType.SFNT_UNPACK, data[:sfntsize])[:2]
    if sfntvers != OpenType.SFNT_CFF and sfntvers != OpenType.SFNT_TRUE:
        raise FontError('invalid font type')
    
    font = {}
    font['version'] = sfntvers
    font['numTables'] = numTables
    
    # create set of offsets, lengths for tables
    table_dir_size = struct.calcsize(OpenType.TABLE_DIR_UNPACK)
    if sfntsize + table_dir_size * numTables > datalen:
        raise FontError('truncated font data, table directory extends past end of data')
    table_dir = {}
    for i in range(0, numTables):
        start = sfntsize + i * table_dir_size
        end = start + table_dir_size
        tag, check, bongo, dirlen = struct.unpack(OpenType.TABLE_DIR_UNPACK, data[start:end])
        table_dir[tag] = {'offset': bongo, 'length': dirlen, 'checksum': check}
    
    font['tableDir'] = table_dir
    
    return font

def get_name_records(nametable):
    """reads through the name records within name table"""
    name = {}
    # read the header
    headersize = 6
    count, strOffset = struct.unpack('>2H', nametable[2:6])
    namerecsize = struct.calcsize(OpenType.NAME_RECORD_UNPACK)
    if count * namerecsize + headersize > len(nametable):
        raise FontError('names exceed size of name table')
    name['count'] = count
    name['strOffset'] = strOffset
    
    # read through the name records
    namerecs = {}
    for i in range(0, count):
        start = headersize + i * namerecsize
        end = start + namerecsize
        platformID, encodingID, languageID, nameID, namelen, offset = struct.unpack(OpenType.NAME_RECORD_UNPACK, nametable[start:end])
        if platformID != OpenType.PLATFORM_ID_MICROSOFT or \
           encodingID != OpenType.ENCODING_ID_MICROSOFT_UNICODEBMP or \
           languageID != OpenType.LANG_ID_MICROSOFT_EN_US:
            continue
        namerecs[nameID] = {'offset': offset, 'length': namelen}
        
    name['namerecords'] = namerecs
    return name

def make_eot_name_headers(fontdata, nameTableDir):
    """extracts names from the name table and generates the names header portion of the EOT header"""
    nameoffset = nameTableDir['offset']
    namelen = nameTableDir['length']
    name = get_name_records(fontdata[nameoffset : nameoffset + namelen])
    namestroffset = name['strOffset']
    namerecs = name['namerecords']
    
    eotnames = (OpenType.NAME_ID_FAMILY, OpenType.NAME_ID_STYLE, OpenType.NAME_ID_VERSION, OpenType.NAME_ID_FULL)
    nameheaders = []
    for nameid in eotnames:
        if nameid in namerecs:
            namerecord = namerecs[nameid]
            noffset = namerecord['offset']
            nlen = namerecord['length']
            nformat = '%dH' % (nlen / 2)        # length is in number of bytes
            start = nameoffset + namestroffset + noffset
            end = start + nlen
            nstr = struct.unpack('>' + nformat, fontdata[start:end])
            nameheaders.append(struct.pack('<H' + nformat + '2x', nlen, *nstr))
        else:
            nameheaders.append(struct.pack('4x'))  # len = 0, padding = 0
    
    return b''.join(nameheaders)

# just return a null-string (len = 0)
def make_root_string():
    return struct.pack('2x')

def make_eot_header(fontdata):
    """given ttf font data produce an EOT header"""
    fontDataSize = len(fontdata)
    font = get_table_directory(fontdata)
    
    # toss out .otf fonts, t2embed library doesn't support these
    tableDir = font['tableDir']
    
    # check for required tables
    required = (OpenType.TABLE_HEAD, OpenType.TABLE_NAME, OpenType.TABLE_OS2)
    for table in required:
        if not (table in tableDir):
            raise FontError('missing required table ' + multicharval(table))
            
    # read name strings
    
    # pull out data from individual tables to construct fixed header portion
    # need to calculate eotSize before packing
    version = EOT.EOT_VERSION
    flags = 0
    charset = EOT.EOT_DEFAULT_CHARSET
    magicNumber = EOT.EOT_MAGIC_NUMBER
    
    # read values from OS/2 table
    os2Dir = tableDir[OpenType.TABLE_OS2]
    os2offset = os2Dir['offset']
    os2size = struct.calcsize(OpenType.OS2_UNPACK)
    
    if os2size > os2Dir['length']:
        raise FontError('OS/2 table invalid length')
    
    os2fields = struct.unpack(OpenType.OS2_UNPACK, fontdata[os2offset : os2offset + os2size])
    
    panose = []
    urange = []
    codepage = []
    
    weight, fsType = os2fields[:2]
    panose[:10] = os2fields[2:12]
    urange[:4] = os2fields[12:16]
    fsSelection = os2fields[16]
    codepage[:2] = os2fields[17:19]
    
    italic = fsSelection & OpenType.OS2_FSSELECTION_ITALIC
    
    # read in values from head table
    headDir = tableDir[OpenType.TABLE_HEAD]
    headoffset = headDir['offset']
    headsize = struct.calcsize(OpenType.HEAD_UNPACK)
    
    if headsize > headDir['length']:
        raise FontError('head table invalid length')
        
    headfields = struct.unpack(OpenType.HEAD_UNPACK, fontdata[headoffset : headoffset + headsize])
    checkSumAdjustment = headfields[0]
    
    # make name headers
    nameheaders = make_eot_name_headers(fontdata, tableDir[OpenType.TABLE_NAME])
    rootstring = make_root_string()
    
    # calculate the total eot size
    eotSize = struct.calcsize(EOT.EOT_HEADER_PACK) + len(nameheaders) + len(rootstring) + fontDataSize
    fixed = struct.pack(EOT.EOT_HEADER_PACK,
                        *([eotSize, fontDataSize, version, flags] + panose + [charset, italic] +
                          [weight, fsType, magicNumber] + urange + codepage + [checkSumAdjustment]))
    
    return b''.join((fixed, nameheaders, rootstring))
 
    
def write_eot_font(eot, header, data):
    open(eot,'wb').write(b''.join((header, data)))
    return

def main():

    # deal with options
    p = optparse.OptionParser()
    p.add_option('--output', '-o', default="world")
    options, args = p.parse_args()
    
    # iterate over font files
    for f in args:
        data = readfont(f)
        if len(data) == 0:
            print('Error reading %s' % f)
        else:
            eot = eotname(f)
            header = make_eot_header(data)
            write_eot_font(eot, header, data)
        

if __name__ == '__main__':
    main()