Victor Franzi - Git Sources dotfiles / master font / mk.py
master

Tree @master (Download .tar.gz)

mk.py @masterraw · history · blame

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Thanks to Jonas Kjellström and Cody Boisclair for their help in finding bugs in this script!

import re
import os
import sys
import tempfile
from fontTools.ttLib import TTFont, newTable

doc = """USAGE: python /path/to/inputCustomize.py [INPUT] [--dest=OUTPUT] [OPTIONS]
Use this script to customize one or more Input font files.
Requires TTX/FontTools: <http://sourceforge.net/projects/fonttools/>

If INPUT is missing, it will customize all fonts in the Current Working Directory.
If OUTPUT is missing, it will overwrite the INPUT files.

Options:
    -h, --help              Print this help text, and exit.
    --lineHeight=<float>    A multiplier for the font's built-in line-height.
    --fourStyleFamily       Only works when four INPUT files are provided.
                            Assigns Regular, Italic, Bold, and Bold Italic names to the
                            INPUT fonts in the order provided.
    --suffix=<string>       Append a suffix to the font names. Takes a string with no spaces.
    --a=ss                  Swaps alternate single-story 'a' for the default double-story 'a'
    --g=ss                  Swaps alternate single-story 'g' for the default double-story 'a'
    --i=serif               Swaps one of the alternate 'i' for the default in Sans/Mono
        serifs
        serifs_round
        topserif
    --l=serif               Swaps one of the alternate 'l' for the default in Sans/Mono
        serifs
        serifs_round
        topserif
    --zero=slash            Swaps the slashed zero for the default dotted zero
           nodot            Swaps a dotless zero for the default dotted zero
    --asterisk=height       Swaps the mid-height asterisk for the default superscripted asterisk
    --braces=straight       Swaps tamer straight-sided braces for the default super-curly braces

    Example 1:
    $ cd /path/to/the/top/level/of/the/fonts/you/want/to/edit
    $ python /path/to/InputCustomize.py --dest=/path/to/output  --lineHeight=1.5 --suffix=Hack --fourStyleFamily --a=ss --g=ss --i=topserif --l=serifs_round --zero=slash --asterisk=height

    Example 2:
    $ cd /path/to/the/top/level/of/the/fonts/you/want/to/edit
    $ python /path/to/InputCustomize.py InputSans-Regular.ttf InputSans-Italic.ttf InputSans-Bold.ttf InputSerif-Regular.ttf --suffix=Hack --fourStyleFamily
"""


class InputModifier(object):
    """
    An object for manipulating Input, takes a TTFont. Sorry this is a little hacky.
    """

    def __init__(self, f):
        self.f = f

    def changeLineHeight(self, lineHeight):
        """
        Takes a line height multiplier and changes the line height.
        """
        f = self.f
        baseAsc = f["OS/2"].sTypoAscender
        baseDesc = f["OS/2"].sTypoDescender
        multiplier = float(lineHeight)
        f["hhea"].ascent = round(baseAsc * multiplier)
        f["hhea"].descent = round(baseDesc * multiplier)
        f["OS/2"].usWinAscent = round(baseAsc * multiplier)
        f["OS/2"].usWinDescent = round(baseDesc * multiplier) * -1

    def swap(self, swap):
        """
        Takes a dictionary of glyphs to swap and swaps 'em.
        """
        f = self.f
        glyphNames = f.getGlyphNames()
        maps = {
            "a": {
                "a": 97,
                "aring": 229,
                "adieresis": 228,
                "acyrillic": 1072,
                "aacute": 225,
                "amacron": 257,
                "agrave": 224,
                "atilde": 227,
                "acircumflex": 226,
                "aogonek": 261,
                "abreve": 259,
            },
            "g": {
                "gdotaccent": 289,
                "gbreve": 287,
                "gcircumflex": 285,
                "gcommaaccent": 291,
                "g": 103,
            },
            "i": {
                "i": 105,
                "iacute": 237,
                "iogonek": 303,
                "igrave": 236,
                "itilde": 297,
                "icircumflex": 238,
                "imacron": 299,
                "ij": 307,
                "ibreve": 301,
                "yicyrillic": 1111,
                "idieresis": 239,
                "icyrillic": 1110,
                "dotlessi": 305,
            },
            "l": {
                "l": 108,
                "lcaron": 318,
                "lcommaaccent": 316,
                "lacute": 314,
                "lslash": 322,
                "ldot": 320,
            },
            "zero": {"zero": 48},
            "asterisk": {"asterisk": 42},
            "braces": {"braceleft": 123, "braceright": 125},
        }
        swapMap = {}
        for k, v in list(swap.items()):
            for gname, u in list(maps[k].items()):
                newGname = gname + ".salt_" + v
                if newGname in glyphNames:
                    swapMap[gname] = newGname
        for table in f["cmap"].tables:
            cmap = table.cmap
            for u, gname in list(cmap.items()):
                if gname in swapMap:
                    cmap[u] = swapMap[gname]

    def fourStyleFamily(self, position, suffix=None):
        """
        Replaces the name table and certain OS/2 values with those that will make a four-style family.
        """
        f = self.f
        source = TTFont(fourStyleFamilySources[position])

        tf = tempfile.mkstemp()
        pathToXML = tf[1]
        source.saveXML(pathToXML, tables=["name"])
        os.close(tf[0])

        with open(pathToXML, "r") as temp:
            xml = temp.read()

        # make the changes
        if suffix:
            xml = xml.replace("Input", "Input" + suffix)

        # save the table
        with open(pathToXML, "w") as temp:
            temp.write(xml)
            temp.write("\r")

        f["OS/2"].usWeightClass = source["OS/2"].usWeightClass
        f["OS/2"].fsType = source["OS/2"].fsType

        # write the table
        f["name"] = newTable("name")
        f.importXML(pathToXML)

    def changeNames(self, suffix=None):
        # this is a similar process to fourStyleFamily()

        tf = tempfile.mkstemp()
        pathToXML = tf[1]
        f.saveXML(pathToXML, tables=["name"])
        os.close(tf[0])

        with open(pathToXML, "r") as temp:
            xml = temp.read()

        # make the changes
        if suffix:
            xml = xml.replace("Input", "Input" + suffix)

        # save the table
        with open(pathToXML, "w") as temp:
            temp.write(xml)
            temp.write("\r")

        # write the table
        f["name"] = newTable("name")
        f.importXML(pathToXML)


baseTemplatePath = os.path.split(__file__)[0] + "/templates"
fourStyleFamilySources = [
    os.path.join(baseTemplatePath, "Regular.txt"),
    os.path.join(baseTemplatePath, "Italic.txt"),
    os.path.join(baseTemplatePath, "Bold.txt"),
    os.path.join(baseTemplatePath, "BoldItalic.txt"),
]

fourStyleFileNameAppend = ["Regular", "Italic", "Bold", "BoldItalic"]

if __name__ == "__main__":

    # Get command-line arguments
    go = True
    arguments = sys.argv[1:]
    paths = []
    swap = {}
    lineHeight = None
    fourStyleFamily = None
    suffix = None
    destBase = None

    # parse arguments
    for argument in arguments:
        key = None
        value = None
        if len(argument.split("=")) == 2:
            key, value = argument.split("=")
            key = key[2:]
        elif argument[0:2] == "--":
            key = argument[2:]
            value = True
        elif argument == "-h":
            print(doc)
            go = False
        else:
            key = argument
            value = None
        # assign preference variables
        if value is None:
            paths.append(key)
        elif key == "lineHeight":
            lineHeight = value
        elif key == "fourStyleFamily":
            fourStyleFamily = True
        elif key == "suffix":
            suffix = value
        elif key == "dest":
            destBase = value
        elif key == "help":
            print(doc)
            go = False
        else:
            swap[key] = value

    # account for arguments where no value is given (for example, '--a' instead of '--a=ss')
    if swap.get("a") is True:
        swap["a"] = "ss"
    if swap.get("g") is True:
        swap["g"] = "ss"
    if swap.get("i") is True:
        swap["i"] = "serifs"
    if swap.get("l") is True:
        swap["l"] = "serifs"
    if swap.get("zero") is True:
        swap["zero"] = "slash"
    if swap.get("asterisk") is True:
        swap["asterisk"] = "height"
    if swap.get("braces") is True:
        swap["braces"] = "straight"

    # if specific paths were not supplied, collect them from the current directory
    if not paths:
        for root, dirs, files in os.walk(os.getcwd()):
            for filename in files:
                basePath, ext = os.path.splitext(filename)
                if ext in [".otf", ".ttf"]:
                    paths.append(os.path.join(root, filename))

    # if four paths were not supplied, do not process as a four-style family
    if len(paths) != 4:
        fourStyleFamily = None

    if go:
        for i, path in enumerate(paths):
            print(os.path.split(path)[1])
            f = TTFont(path)
            c = InputModifier(f)
            if lineHeight:
                c.changeLineHeight(lineHeight)
            if swap:
                c.swap(swap)
            if fourStyleFamily:
                c.fourStyleFamily(i, suffix)
                base, ext = os.path.splitext(path)
                path = base + "_as_" + fourStyleFileNameAppend[i] + ext
            elif suffix:
                c.changeNames(suffix)
            if destBase:
                baseScrap, fileAndExt = os.path.split(path)
                destPath = os.path.join(destBase, fileAndExt)
            else:
                destPath = path
            try:
                os.remove(destPath)
            except:
                pass

            # Take care of that weird "post" table issue, just in case. Delta#1
            try:
                del f["post"].mapping["Delta#1"]
            except:
                pass

            f.save(destPath)
        print("done")