diff options
Diffstat (limited to 'bin/otb1exp.py')
-rw-r--r-- | bin/otb1exp.py | 808 |
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) |