aboutsummaryrefslogtreecommitdiff
path: root/bin/otb1exp.py
diff options
context:
space:
mode:
Diffstat (limited to 'bin/otb1exp.py')
-rw-r--r--bin/otb1exp.py808
1 files changed, 808 insertions, 0 deletions
diff --git a/bin/otb1exp.py b/bin/otb1exp.py
new file mode 100644
index 000000000000..51e274554f64
--- /dev/null
+++ b/bin/otb1exp.py
@@ -0,0 +1,808 @@
+#
+# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 2 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+import struct
+import codecs
+import math
+from datetime import datetime, timezone
+from itertools import groupby
+from enum import IntEnum, unique
+from collections import OrderedDict
+
+import fnutil
+import fncli
+import bdf
+import bdfexp
+import otb1get
+
+# -- Table --
+class Table:
+ def __init__(self, name):
+ self.data = bytearray(0)
+ self.table_name = name
+
+
+ def check_size(self, size):
+ if size != self.size:
+ raise Exception('internal error: %s size = %d instead of %d' % (self.table_name, self.size, size))
+
+
+ def checksum(self):
+ cksum = 0
+ data = self.data + self.padding
+
+ for offset in range(0, self.size, 4):
+ cksum += struct.unpack('>L', data[offset : offset + 4])[0]
+
+ return cksum & 0xFFFFFFFF
+
+
+ def pack(self, format, value, name):
+ try:
+ return struct.pack(format, value)
+ except struct.error as ex:
+ raise Exception('%s.%s: %s' % (self.table_name, name, str(ex)))
+
+
+ @property
+ def size(self):
+ return len(self.data)
+
+
+ @property
+ def padding(self):
+ return bytes(((self.size + 1) & 3) ^ 1)
+
+
+ def rewrite_uint32(self, value, offset):
+ self.data[offset : offset + 4] = struct.pack('>L', value)
+
+
+ def write(self, data):
+ self.data += data
+
+
+ def write_int8(self, value, name):
+ self.data += self.pack('b', value, name)
+
+
+ def write_uint8(self, value, name):
+ self.data += self.pack('B', value, name)
+
+
+ def write_int16(self, value, name):
+ self.data += self.pack('>h', value, name)
+
+
+ def write_uint16(self, value, name):
+ self.data += self.pack('>H', value, name)
+
+
+ def write_uint32(self, value, name):
+ self.data += self.pack('>L', value, name)
+
+
+ def write_uint64(self, value, name):
+ self.data += self.pack('>Q', value, name)
+
+
+ def write_fixed(self, value, name):
+ self.data += self.pack('>l', round(value * 65536), name)
+
+
+ def write_table(self, table):
+ self.data += table.data
+
+
+# -- Params --
+EM_SIZE_MIN = 64
+EM_SIZE_MAX = 16384
+EM_SIZE_DEFAULT = 1024
+
+class Params(fncli.Params): # pylint: disable=too-many-instance-attributes
+ def __init__(self):
+ fncli.Params.__init__(self)
+ self.created = datetime.now(timezone.utc)
+ self.modified = self.created
+ self.dir_hint = 0
+ self.em_size = EM_SIZE_DEFAULT
+ self.line_gap = 0
+ self.low_ppem = 0
+ self.encoding = 'utf_8'
+ self.w_lang_id = 0x0409
+ self.x_max_extent = True
+ self.single_loca = False
+ self.post_names = False
+
+
+# -- Options --
+class Options(fncli.Options):
+ def __init__(self, need_args, help_text, version_text):
+ fncli.Options.__init__(self, need_args + ['-d', '-e', '-g', '-l', '-E', '-W'], help_text, version_text)
+
+
+ def parse(self, name, value, params):
+ if name == '-d':
+ params.dir_hint = fnutil.parse_dec('DIR-HINT', value, -2, 2)
+ elif name == '-e':
+ params.em_size = fnutil.parse_dec('EM-SIZE', value, EM_SIZE_MIN, EM_SIZE_MAX)
+ elif name == '-g':
+ params.line_gap = fnutil.parse_dec('LINE-GAP', value, 0, EM_SIZE_MAX << 1)
+ elif name == '-l':
+ params.low_ppem = fnutil.parse_dec('LOW-PPEM', value, 1, bdf.DPARSE_LIMIT)
+ elif name == '-E':
+ params.encoding = value
+ elif name == '-W':
+ params.w_lang_id = fnutil.parse_hex('WLANG-ID', value, 0, 0x7FFF)
+ elif name == '-X':
+ params.x_max_extent = False
+ elif name == '-L':
+ params.single_loca = True
+ elif name == '-P':
+ params.post_names = True
+ else:
+ self.fallback(name, params)
+
+
+# -- Font --
+class Font(bdfexp.Font):
+ def __init__(self, params):
+ bdfexp.Font.__init__(self)
+ self.params = params
+ self.em_ascender = 0
+ self.em_descender = 0
+ self.em_max_width = 0
+ self.mac_style = 0
+ self.line_size = 0
+
+
+ @property
+ def bmp_only(self):
+ return self.max_code <= fnutil.UNICODE_BMP_MAX
+
+ @property
+ def created(self):
+ return Font.sfntime(self.params.created)
+
+ def decode(self, data):
+ return codecs.decode(data, self.params.encoding)
+
+ def em_scale(self, value, divisor=0):
+ return round(value * self.params.em_size / (divisor or self.bbx.height))
+
+ def em_scale_width(self, base):
+ return self.em_scale(base.bbx.width)
+
+ @property
+ def italic_angle(self):
+ value = self.props.get('ITALIC_ANGLE') # must be integer
+ return fnutil.parse_dec('ITALIC_ANGLE', value, -45, 45) if value else -11.5 if self.italic else 0
+
+ @property
+ def max_code(self):
+ return self.chars[-1].code
+
+ @property
+ def min_code(self):
+ return self.chars[0].code
+
+ @property
+ def modified(self):
+ return Font.sfntime(self.params.modified)
+
+
+ def prepare(self):
+ self.chars.sort(key=lambda c: c.code)
+ self.chars = [next(elem[1]) for elem in groupby(self.chars, key=lambda c: c.code)]
+ self.props.set('CHARS', len(self.chars))
+ self.em_ascender = self.em_scale(self.px_ascender)
+ self.em_descender = self.em_ascender - self.params.em_size
+ self.em_max_width = self.em_scale_width(self)
+ self.mac_style = int(self.bold) + (int(self.italic) << 1)
+ self.line_size = self.em_scale(round(self.bbx.height / 17) or 1)
+
+
+ def _read(self, input):
+ bdfexp.Font._read(self, input)
+ self.prepare()
+ return self
+
+ @staticmethod
+ def read(input, params): # pylint: disable=arguments-differ
+ return Font(params)._read(input) # pylint: disable=protected-access
+
+
+ @staticmethod
+ def sfntime(stamp):
+ return math.floor((stamp - datetime(1904, 1, 1, tzinfo=timezone.utc)).total_seconds())
+
+ @property
+ def underline_position(self):
+ return round((self.em_descender + self.line_size) / 2)
+
+ @property
+ def x_max_extent(self):
+ return self.em_max_width if self.params.x_max_extent else 0
+
+
+# -- BDAT --
+BDAT_HEADER_SIZE = 4
+BDAT_METRIC_SIZE = 5
+
+class BDAT(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'EBDT')
+ # header
+ self.write_fixed(2, 'version')
+ # format 1 data
+ for char in font.chars:
+ self.write_uint8(font.bbx.height, 'height')
+ self.write_uint8(char.bbx.width, 'width')
+ self.write_int8(0, 'bearingX')
+ self.write_int8(font.px_ascender, 'bearingY')
+ self.write_uint8(char.bbx.width, 'advance')
+ self.write(char.data) # imageData
+
+
+ @staticmethod
+ def get_char_size(char):
+ return BDAT_METRIC_SIZE + len(char.data)
+
+
+# -- BLOC --
+BLOC_TABLE_SIZE_OFFSET = 12
+BLOC_PREFIX_SIZE = 0x38 # header 0x08 + 1 bitmapSizeTable * 0x30
+BLOC_INDEX_ARRAY_SIZE = 8 # 1 index record * 0x08
+
+class BLOC(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'EBLC')
+ # header
+ self.write_fixed(2, 'version')
+ self.write_uint32(1, 'numSizes')
+ # bitmapSizeTable
+ self.write_uint32(BLOC_PREFIX_SIZE, 'indexSubTableArrayOffset')
+ self.write_uint32(0, 'indexTableSize') # adjusted later
+ self.write_uint32(1, 'numberOfIndexSubTables')
+ self.write_uint32(0, 'colorRef')
+ # hori
+ self.write_int8(font.px_ascender, 'hori ascender')
+ self.write_int8(font.px_descender, 'hori descender')
+ self.write_uint8(font.bbx.width, 'hori widthMax')
+ self.write_int8(1, 'hori caretSlopeNumerator')
+ self.write_int8(0, 'hori caretSlopeDenominator')
+ self.write_int8(0, 'hori caretOffset')
+ self.write_int8(0, 'hori minOriginSB')
+ self.write_int8(0, 'hori minAdvanceSB')
+ self.write_int8(font.px_ascender, 'hori maxBeforeBL')
+ self.write_int8(font.px_descender, 'hori minAfterBL')
+ self.write_int16(0, 'hori padd')
+ # vert
+ self.write_int8(0, 'vert ascender')
+ self.write_int8(0, 'vert descender')
+ self.write_uint8(0, 'vert widthMax')
+ self.write_int8(0, 'vert caretSlopeNumerator')
+ self.write_int8(0, 'vert caretSlopeDenominator')
+ self.write_int8(0, 'vert caretOffset')
+ self.write_int8(0, 'vert minOriginSB')
+ self.write_int8(0, 'vert minAdvanceSB')
+ self.write_int8(0, 'vert maxBeforeBL')
+ self.write_int8(0, 'vert minAfterBL')
+ self.write_int16(0, 'vert padd')
+ # (bitmapSizeTable)
+ self.write_uint16(0, 'startGlyphIndex')
+ self.write_uint16(len(font.chars) - 1, 'endGlyphIndex')
+ self.write_uint8(font.bbx.height, 'ppemX')
+ self.write_uint8(font.bbx.height, 'ppemY')
+ self.write_uint8(1, 'bitDepth')
+ self.write_uint8(1, 'flags') # small metrics are horizontal
+ # indexSubTableArray
+ self.write_uint16(0, 'firstGlyphIndex')
+ self.write_uint16(len(font.chars) - 1, 'lastGlyphIndex')
+ self.write_uint32(BLOC_INDEX_ARRAY_SIZE, 'additionalOffsetToIndexSubtable')
+ # indexSubtableHeader
+ self.write_uint16(1 if font.proportional else 2, 'indexFormat')
+ self.write_uint16(1, 'imageFormat') # BDAT -> small metrics, byte-aligned
+ self.write_uint32(BDAT_HEADER_SIZE, 'imageDataOffset')
+ # indexSubtable data
+ if font.proportional:
+ offset = 0
+
+ for char in font.chars:
+ self.write_uint32(offset, 'offsetArray[]')
+ offset += BDAT.get_char_size(char)
+
+ self.write_uint32(offset, 'offsetArray[]')
+ else:
+ self.write_uint32(BDAT.get_char_size(font.chars[0]), 'imageSize')
+ self.write_uint8(font.bbx.height, 'height')
+ self.write_uint8(font.bbx.width, 'width')
+ self.write_int8(0, 'horiBearingX')
+ self.write_int8(font.px_ascender, 'horiBearingY')
+ self.write_uint8(font.bbx.width, 'horiAdvance')
+ self.write_int8(-(font.bbx.width >> 1), 'vertBearingX')
+ self.write_int8(0, 'vertBearingY')
+ self.write_uint8(font.bbx.height, 'vertAdvance')
+ # adjust
+ self.rewrite_uint32(self.size - BLOC_PREFIX_SIZE, BLOC_TABLE_SIZE_OFFSET)
+
+
+# -- OS/2 --
+OS_2_TABLE_SIZE = 96
+
+class OS_2(Table): # pylint: disable=invalid-name
+ def __init__(self, font):
+ Table.__init__(self, 'OS/2')
+ # Version 4
+ x_avg_char_width = font.em_scale(font.avg_width) # otb1get.x_avg_char_width(font)
+ ul_char_ranges = otb1get.ul_char_ranges(font)
+ ul_code_pages = otb1get.ul_code_pages(font) if font.bmp_only else [0, 0]
+ # mostly from FontForge
+ script_xsize = font.em_scale(30, 100)
+ script_ysize = font.em_scale(40, 100)
+ subscript_yoff = script_ysize >> 1
+ xfactor = math.tan(font.italic_angle * math.pi / 180)
+ subscript_xoff = 0 # stub, no overlapping characters yet
+ superscript_yoff = font.em_ascender - script_ysize
+ superscript_xoff = -round(xfactor * superscript_yoff)
+ # write
+ self.write_uint16(4, 'version')
+ self.write_int16(x_avg_char_width, 'xAvgCharWidth')
+ self.write_uint16(700 if font.bold else 400, 'usWeightClass')
+ self.write_uint16(5, 'usWidthClass') # medium
+ self.write_int16(0, 'fsType')
+ self.write_int16(script_xsize, 'ySubscriptXSize')
+ self.write_int16(script_ysize, 'ySubscriptYSize')
+ self.write_int16(subscript_xoff, 'ySubscriptXOffset')
+ self.write_int16(subscript_yoff, 'ySubscriptYOffset')
+ self.write_int16(script_xsize, 'ySuperscriptXSize')
+ self.write_int16(script_ysize, 'ySuperscriptYSize')
+ self.write_int16(superscript_xoff, 'ySuperscriptXOffset')
+ self.write_int16(superscript_yoff, 'ySuperscriptYOffset')
+ self.write_int16(font.line_size, 'yStrikeoutSize')
+ self.write_int16(font.em_scale(25, 100), 'yStrikeoutPosition')
+ self.write_int16(0, 'sFamilyClass') # no classification
+ self.write_uint8(2, 'bFamilyType') # text and display
+ self.write_uint8(0, 'bSerifStyle') # any
+ self.write_uint8(8 if font.bold else 6, 'bWeight')
+ self.write_uint8(3 if font.proportional else 9, 'bProportion')
+ self.write_uint8(0, 'bContrast')
+ self.write_uint8(0, 'bStrokeVariation')
+ self.write_uint8(0, 'bArmStyle')
+ self.write_uint8(0, 'bLetterform')
+ self.write_uint8(0, 'bMidline')
+ self.write_uint8(0, 'bXHeight')
+ self.write_uint32(ul_char_ranges[0], 'ulCharRange1')
+ self.write_uint32(ul_char_ranges[1], 'ulCharRange2')
+ self.write_uint32(ul_char_ranges[2], 'ulCharRange3')
+ self.write_uint32(ul_char_ranges[3], 'ulCharRange4')
+ self.write_uint32(0x586F7334, 'achVendID') # 'Xos4'
+ self.write_uint16(OS_2.fs_selection(font), 'fsSelection')
+ self.write_uint16(min(font.min_code, fnutil.UNICODE_BMP_MAX), 'firstChar')
+ self.write_uint16(min(font.max_code, fnutil.UNICODE_BMP_MAX), 'lastChar')
+ self.write_int16(font.em_ascender, 'sTypoAscender')
+ self.write_int16(font.em_descender, 'sTypoDescender')
+ self.write_int16(font.params.line_gap, 'sTypoLineGap')
+ self.write_uint16(font.em_ascender, 'usWinAscent')
+ self.write_uint16(-font.em_descender, 'usWinDescent')
+ self.write_uint32(ul_code_pages[0], 'ulCodePageRange1')
+ self.write_uint32(ul_code_pages[1], 'ulCodePageRange2')
+ self.write_int16(font.em_scale(font.px_ascender * 0.6), 'sxHeight') # stub
+ self.write_int16(font.em_scale(font.px_ascender * 0.8), 'sCapHeight') # stub
+ self.write_uint16(OS_2.default_char(font), 'usDefaultChar')
+ self.write_uint16(OS_2.break_char(font), 'usBreakChar')
+ self.write_uint16(1, 'usMaxContext')
+ # check
+ self.check_size(OS_2_TABLE_SIZE)
+
+
+ @staticmethod
+ def break_char(font):
+ return 0x20 if next((char for char in font.chars if char.code == 0x20), None) else font.min_code
+
+
+ @staticmethod
+ def default_char(font):
+ if font.default_code != -1 and font.default_code <= fnutil.UNICODE_BMP_MAX:
+ return font.default_code
+
+ return 0 if font.min_code == 0 else font.max_code
+
+
+ @staticmethod
+ def fs_selection(font):
+ fs_selection = int(font.bold) * 5 + int(font.italic)
+ return fs_selection if fs_selection != 0 else 0x40 if font.xlfd[bdf.XLFD.SLANT] == 'R' else 0
+
+
+# -- cmap --
+CMAP_4_PREFIX_SIZE = 12
+CMAP_4_FORMAT_SIZE = 16
+CMAP_4_SEGMENT_SIZE = 8
+
+CMAP_12_PREFIX_SIZE = 20
+CMAP_12_FORMAT_SIZE = 16
+CMAP_12_GROUP_SIZE = 12
+
+class CMapRange:
+ def __init__(self, glyph_index=0, start_code=0, final_code=-2):
+ self.glyph_index = glyph_index
+ self.start_code = start_code
+ self.final_code = final_code
+
+
+ @property
+ def id_delta(self):
+ return (self.glyph_index - self.start_code) & 0xFFFF
+
+
+class CMAP(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'cmap')
+ # make ranges
+ ranges = []
+ range = CMapRange()
+ index = -1
+
+ for char in font.chars:
+ index += 1
+ code = char.code
+
+ if code == range.final_code + 1:
+ range.final_code += 1
+ else:
+ range = CMapRange(index, code, code)
+ ranges.append(range)
+ # write
+ if font.bmp_only:
+ if font.max_code < 0xFFFF:
+ ranges.append(CMapRange(0, 0xFFFF, 0xFFFF))
+
+ self.write_format_4(ranges)
+ else:
+ self.write_format_12(ranges)
+
+
+ def write_format_4(self, ranges):
+ # index
+ self.write_uint16(0, 'version')
+ self.write_uint16(1, 'numberSubtables')
+ # encoding subtables index
+ self.write_uint16(3, 'platformID') # Microsoft
+ self.write_uint16(1, 'platformSpecificID') # Unicode BMP (UCS-2)
+ self.write_uint32(CMAP_4_PREFIX_SIZE, 'offset') # for Unicode BMP (UCS-2)
+ # cmap format 4
+ seg_count = len(ranges)
+ subtable_size = CMAP_4_FORMAT_SIZE + seg_count * CMAP_4_SEGMENT_SIZE
+ search_range = 2 << math.floor(math.log2(seg_count))
+
+ self.write_uint16(4, 'format')
+ self.write_uint16(subtable_size, 'length')
+ self.write_uint16(0, 'language') # none/independent
+ self.write_uint16(seg_count * 2, 'segCountX2')
+ self.write_uint16(search_range, 'searchRange')
+ self.write_uint16(int(math.log2(search_range / 2)), 'entrySelector')
+ self.write_uint16((seg_count * 2) - search_range, 'rangeShift')
+ for range in ranges:
+ self.write_uint16(range.final_code, 'endCode')
+ self.write_uint16(0, 'reservedPad')
+ for range in ranges:
+ self.write_uint16(range.start_code, 'startCode')
+ for range in ranges:
+ self.write_uint16(range.id_delta, 'idDelta')
+ for _ in ranges:
+ self.write_uint16(0, 'idRangeOffset')
+ # check
+ self.check_size(CMAP_4_PREFIX_SIZE + subtable_size)
+
+
+ def write_format_12(self, ranges):
+ # index
+ self.write_uint16(0, 'version')
+ self.write_uint16(2, 'numberSubtables')
+ # encoding subtables
+ self.write_uint16(0, 'platformID') # Unicode
+ self.write_uint16(4, 'platformSpecificID') # Unicode 2.0+ full range
+ self.write_uint32(CMAP_12_PREFIX_SIZE, 'offset') # for Unicode 2.0+ full range
+ self.write_uint16(3, 'platformID') # Microsoft
+ self.write_uint16(10, 'platformSpecificID') # Unicode UCS-4
+ self.write_uint32(CMAP_12_PREFIX_SIZE, 'offset') # for Unicode UCS-4
+ # cmap format 12
+ subtable_size = CMAP_12_FORMAT_SIZE + len(ranges) * CMAP_12_GROUP_SIZE
+
+ self.write_fixed(12, 'format')
+ self.write_uint32(subtable_size, 'length')
+ self.write_uint32(0, 'language') # none/independent
+ self.write_uint32(len(ranges), 'nGroups')
+ for range in ranges:
+ self.write_uint32(range.start_code, 'startCharCode')
+ self.write_uint32(range.final_code, 'endCharCode')
+ self.write_uint32(range.glyph_index, 'startGlyphID')
+ # check
+ self.check_size(CMAP_12_PREFIX_SIZE + subtable_size)
+
+
+# -- glyf --
+class GLYF(Table):
+ def __init__(self, _font):
+ Table.__init__(self, 'glyf')
+
+
+# -- head --
+HEAD_TABLE_SIZE = 54
+HEAD_CHECKSUM_OFFSET = 8
+
+class HEAD(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'head')
+ self.write_fixed(1, 'version')
+ self.write_fixed(1, 'fontRevision')
+ self.write_uint32(0, 'checksumAdjustment') # adjusted later
+ self.write_uint32(0x5F0F3CF5, 'magicNumber')
+ self.write_uint16(HEAD.flags(font), 'flags')
+ self.write_uint16(font.params.em_size, 'unitsPerEm')
+ self.write_uint64(font.created, 'created')
+ self.write_uint64(font.modified, 'modified')
+ self.write_int16(0, 'xMin')
+ self.write_int16(font.em_descender, 'yMin')
+ self.write_int16(font.em_max_width, 'xMax')
+ self.write_int16(font.em_ascender, 'yMax')
+ self.write_uint16(font.mac_style, 'macStyle')
+ self.write_uint16(font.params.low_ppem or font.bbx.height, 'lowestRecPPEM')
+ self.write_int16(font.params.dir_hint, 'fontDirectionHint')
+ self.write_int16(0, 'indexToLocFormat') # short
+ self.write_int16(0, 'glyphDataFormat') # current
+ # check
+ self.check_size(HEAD_TABLE_SIZE)
+
+
+ @staticmethod
+ def flags(font):
+ return 0x20B if otb1get.contains_rtl(font) else 0x0B # y0 base, x0 lsb, scale int
+
+
+# -- hhea --
+HHEA_TABLE_SIZE = 36
+
+class HHEA(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'hhea')
+ self.write_fixed(1, 'version')
+ self.write_int16(font.em_ascender, 'ascender')
+ self.write_int16(font.em_descender, 'descender')
+ self.write_int16(font.params.line_gap, 'lineGap')
+ self.write_uint16(font.em_max_width, 'advanceWidthMax')
+ self.write_int16(0, 'minLeftSideBearing')
+ self.write_int16(0, 'minRightSideBearing')
+ self.write_int16(font.x_max_extent, 'xMaxExtent')
+ self.write_int16(100 if font.italic else 1, 'caretSlopeRise')
+ self.write_int16(20 if font.italic else 0, 'caretSlopeRun')
+ self.write_int16(0, 'caretOffset')
+ self.write_int16(0, 'reserved')
+ self.write_int16(0, 'reserved')
+ self.write_int16(0, 'reserved')
+ self.write_int16(0, 'reserved')
+ self.write_int16(0, 'metricDataFormat') # current
+ self.write_uint16(len(font.chars), 'numOfLongHorMetrics')
+ # check
+ self.check_size(HHEA_TABLE_SIZE)
+
+
+# -- hmtx --
+class HMTX(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'hmtx')
+ for char in font.chars:
+ self.write_uint16(font.em_scale_width(char), 'advanceWidth')
+ self.write_int16(0, 'leftSideBearing')
+
+
+# -- loca --
+class LOCA(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'loca')
+ if not font.params.single_loca:
+ for _ in font.chars:
+ self.write_uint16(0, 'offset')
+ self.write_uint16(0, 'offset')
+
+
+# -- maxp --
+MAXP_TABLE_SIZE = 32
+
+class MAXP(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'maxp')
+ self.write_fixed(1, 'version')
+ self.write_uint16(len(font.chars), 'numGlyphs')
+ self.write_uint16(0, 'maxPoints')
+ self.write_uint16(0, 'maxContours')
+ self.write_uint16(0, 'maxComponentPoints')
+ self.write_uint16(0, 'maxComponentContours')
+ self.write_uint16(2, 'maxZones')
+ self.write_uint16(0, 'maxTwilightPoints')
+ self.write_uint16(1, 'maxStorage')
+ self.write_uint16(1, 'maxFunctionDefs')
+ self.write_uint16(0, 'maxInstructionDefs')
+ self.write_uint16(64, 'maxStackElements')
+ self.write_uint16(0, 'maxSizeOfInstructions')
+ self.write_uint16(0, 'maxComponentElements')
+ self.write_uint16(0, 'maxComponentDepth')
+ # check
+ self.check_size(MAXP_TABLE_SIZE)
+
+
+# -- name --
+@unique # pylint: disable=invalid-name
+class NAME_ID(IntEnum): # pylint: disable=invalid-name
+ COPYRIGHT = 0
+ FONT_FAMILY = 1
+ FONT_SUBFAMILY = 2
+ UNIQUE_SUBFAMILY = 3
+ FULL_FONT_NAME = 4
+ LICENSE = 14
+
+NAME_HEADER_SIZE = 6
+NAME_RECORD_SIZE = 12
+
+class NAME(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'name')
+ # compute names
+ names = OrderedDict()
+ copyright = font.props.get('COPYRIGHT')
+
+ if copyright is not None:
+ names[NAME_ID.COPYRIGHT] = fnutil.unquote(copyright)
+
+ family = font.xlfd[bdf.XLFD.FAMILY_NAME]
+ style = [b'Regular', b'Bold', b'Italic', b'Bold Italic'][font.mac_style]
+
+ names[NAME_ID.FONT_FAMILY] = family
+ names[NAME_ID.FONT_SUBFAMILY] = style
+ names[NAME_ID.UNIQUE_SUBFAMILY] = b'%s %s bitmap height %d' % (family, style, font.bbx.height)
+ names[NAME_ID.FULL_FONT_NAME] = b'%s %s' % (family, style)
+
+ license = font.props.get('LICENSE')
+ notice = font.props.get('NOTICE')
+
+ if license is None and notice is not None and b'license' in notice.lower():
+ license = notice
+
+ if license is not None:
+ names[NAME_ID.LICENSE] = fnutil.unquote(license)
+
+ # header
+ count = len(names) * (1 + 1) # Unicode + Microsoft
+ string_offset = NAME_HEADER_SIZE + NAME_RECORD_SIZE * count
+
+ self.write_uint16(0, 'format')
+ self.write_uint16(count, 'count')
+ self.write_uint16(string_offset, 'stringOffset')
+ # name records / create values
+ values = Table('name')
+
+ for [name_id, bstr] in names.items():
+ s = font.decode(bstr)
+ value = codecs.encode(s, 'utf_16_be')
+ bmp = font.bmp_only and len(value) == len(s) * 2
+ # Unicode
+ self.write_uint16(0, 'platformID') # Unicode
+ self.write_uint16(3 if bmp else 4, 'platformSpecificID')
+ self.write_uint16(0, 'languageID') # none
+ self.write_uint16(name_id, 'nameID')
+ self.write_uint16(len(value), 'length') # in bytes
+ self.write_uint16(values.size, 'offset')
+ # Microsoft
+ self.write_uint16(3, 'platformID') # Microsoft
+ self.write_uint16(1 if bmp else 10, 'platformSpecificID')
+ self.write_uint16(font.params.w_lang_id, 'languageID')
+ self.write_uint16(name_id, 'nameID')
+ self.write_uint16(len(value), 'length') # in bytes
+ self.write_uint16(values.size, 'offset')
+ # value
+ values.write(value)
+
+ # write values
+ self.write_table(values)
+ # check
+ self.check_size(string_offset + values.size)
+
+
+# -- post --
+POST_TABLE_SIZE = 32
+
+class POST(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'post')
+ self.write_fixed(2 if font.params.post_names else 3, 'format')
+ self.write_fixed(font.italic_angle, 'italicAngle')
+ self.write_int16(font.underline_position, 'underlinePosition')
+ self.write_int16(font.line_size, 'underlineThickness')
+ self.write_uint32(0 if font.proportional else 1, 'isFixedPitch')
+ self.write_uint32(0, 'minMemType42')
+ self.write_uint32(0, 'maxMemType42')
+ self.write_uint32(0, 'minMemType1')
+ self.write_uint32(0, 'maxMemType1')
+ # names
+ if font.params.post_names:
+ self.write_uint16(len(font.chars), 'numberOfGlyphs')
+ post_names = otb1get.post_mac_names()
+ post_mac_count = len(post_names)
+
+ for name in [char.props['STARTCHAR'] for char in font.chars]:
+ if name in post_names:
+ self.write_uint16(post_names.index(name), 'glyphNameIndex')
+ else:
+ self.write_uint16(len(post_names), 'glyphNameIndex')
+ post_names.append(name)
+
+ for name in post_names[post_mac_count:]:
+ self.write_uint8(len(name), 'glyphNameLength')
+ self.write(name)
+ # check
+ else:
+ self.check_size(POST_TABLE_SIZE)
+
+
+# -- SFNT --
+SFNT_HEADER_SIZE = 12
+SFNT_RECORD_SIZE = 16
+SFNT_SUBTABLES = (BDAT, BLOC, OS_2, CMAP, GLYF, HEAD, HHEA, HMTX, LOCA, MAXP, NAME, POST)
+
+class SFNT(Table):
+ def __init__(self, font):
+ Table.__init__(self, 'SFNT')
+ # create tables
+ tables = []
+ for ctor in SFNT_SUBTABLES:
+ tables.append(ctor(font))
+ # header
+ num_tables = len(tables)
+ entry_selector = math.floor(math.log2(num_tables))
+ search_range = 16 << entry_selector
+ content_offset = SFNT_HEADER_SIZE + num_tables * SFNT_RECORD_SIZE
+ offset = content_offset
+ content = Table('SFNT')
+ head_checksum_offset = -1
+
+ self.write_fixed(1, 'sfntVersion')
+ self.write_uint16(num_tables, 'numTables')
+ self.write_uint16(search_range, 'searchRange')
+ self.write_uint16(entry_selector, 'entrySelector')
+ self.write_uint16(num_tables * 16 - search_range, 'rangeShift')
+ # table records / create content
+ for table in tables:
+ self.write(bytes(table.table_name, 'ascii'))
+ self.write_uint32(table.checksum(), 'checkSum')
+ self.write_uint32(offset, 'offset')
+ self.write_uint32(len(table.data), 'length')
+ # create content
+ if table.table_name == 'head':
+ head_checksum_offset = offset + HEAD_CHECKSUM_OFFSET
+
+ padded_data = table.data + table.padding
+ content.write(padded_data)
+ offset += len(padded_data)
+ # write content
+ self.write_table(content)
+ # check
+ self.check_size(content_offset + len(content.data))
+ # adjust
+ if head_checksum_offset != -1:
+ self.rewrite_uint32((0xB1B0AFBA - self.checksum()) & 0xFFFFFFFF, head_checksum_offset)