Blob Blame History Raw
#!/usr/bin/env python
"""Script to add a suffix to all family names in the input font's `name` table,
and to optionally rename the output files with the given suffix.

The current family name substring is searched in the nameIDs 1, 3, 4, 6, 16,
and 21, and if found the suffix is inserted after it; or else the suffix is
appended at the end.
"""
from __future__ import print_function, absolute_import, unicode_literals
import os
import argparse
import logging
from fontTools.ttLib import TTFont
from fontTools.misc.cliTools import makeOutputFileName


logger = logging.getLogger()

WINDOWS_ENGLISH_IDS = 3, 1, 0x409
MAC_ROMAN_IDS = 1, 0, 0

FAMILY_RELATED_IDS = dict(
    LEGACY_FAMILY=1,
    TRUETYPE_UNIQUE_ID=3,
    FULL_NAME=4,
    POSTSCRIPT_NAME=6,
    PREFERRED_FAMILY=16,
    WWS_FAMILY=21,
)


def get_current_family_name(table):
    family_name_rec = None
    for plat_id, enc_id, lang_id in (WINDOWS_ENGLISH_IDS, MAC_ROMAN_IDS):
        for name_id in (
            FAMILY_RELATED_IDS["PREFERRED_FAMILY"],
            FAMILY_RELATED_IDS["LEGACY_FAMILY"],
        ):
            family_name_rec = table.getName(
                nameID=name_id,
                platformID=plat_id,
                platEncID=enc_id,
                langID=lang_id,
            )
            if family_name_rec is not None:
                break
        if family_name_rec is not None:
            break
    if not family_name_rec:
        raise ValueError("family name not found; can't add suffix")
    return family_name_rec.toUnicode()


def insert_suffix(string, family_name, suffix):
    # check whether family_name is a substring
    start = string.find(family_name)
    if start != -1:
        # insert suffix after the family_name substring
        end = start + len(family_name)
        new_string = string[:end] + suffix + string[end:]
    else:
        # it's not, we just append the suffix at the end
        new_string = string + suffix
    return new_string


def rename_record(name_record, family_name, suffix):
    string = name_record.toUnicode()
    new_string = insert_suffix(string, family_name, suffix)
    name_record.string = new_string
    return string, new_string


def rename_file(filename, family_name, suffix):
    filename, ext = os.path.splitext(filename)
    ps_name = family_name.replace(" ", "")
    if ps_name in filename:
        ps_suffix = suffix.replace(" ", "")
        return insert_suffix(filename, ps_name, ps_suffix) + ext
    else:
        return insert_suffix(filename, family_name, suffix) + ext


def add_family_suffix(font, suffix):
    table = font["name"]

    family_name = get_current_family_name(table)
    logger.info("  Current family name: '%s'", family_name)

    # postcript name can't contain spaces
    ps_family_name = family_name.replace(" ", "")
    ps_suffix = suffix.replace(" ", "")
    for rec in table.names:
        name_id = rec.nameID
        if name_id not in FAMILY_RELATED_IDS.values():
            continue
        if name_id == FAMILY_RELATED_IDS["POSTSCRIPT_NAME"]:
            old, new = rename_record(rec, ps_family_name, ps_suffix)
        elif name_id == FAMILY_RELATED_IDS["TRUETYPE_UNIQUE_ID"]:
            # The Truetype Unique ID rec may contain either the PostScript
            # Name or the Full Name string, so we try both
            if ps_family_name in rec.toUnicode():
                old, new = rename_record(rec, ps_family_name, ps_suffix)
            else:
                old, new = rename_record(rec, family_name, suffix)
        else:
            old, new = rename_record(rec, family_name, suffix)
        logger.info("    %r: '%s' -> '%s'", rec, old, new)

    return family_name


def main(args=None):
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("-s", "--suffix", required=True)
    parser.add_argument("input_fonts", metavar="FONTFILE", nargs="+")
    output_group = parser.add_mutually_exclusive_group()
    output_group.add_argument("-i", "--inplace", action="store_true")
    output_group.add_argument("-d", "--output-dir")
    output_group.add_argument("-o", "--output-file")
    parser.add_argument("-R", "--rename-files", action="store_true")
    parser.add_argument("-v", "--verbose", action="count", default=0)
    options = parser.parse_args(args)

    if not options.verbose:
        level = "WARNING"
    elif options.verbose == 1:
        level = "INFO"
    else:
        level = "DEBUG"
    logging.basicConfig(level=level, format="%(message)s")

    if options.output_file and len(options.input_fonts) > 1:
        parser.error(
            "argument -o/--output-file can't be used with multiple inputs"
        )
    if options.rename_files and (options.inplace or options.output_file):
        parser.error("argument -R not allowed with arguments -i or -o")

    for input_name in options.input_fonts:
        logger.info("Renaming font: '%s'", input_name)

        font = TTFont(input_name)
        family_name = add_family_suffix(font, options.suffix)

        if options.inplace:
            output_name = input_name
        elif options.output_file:
            output_name = options.output_file
        else:
            if options.rename_files:
                input_name = rename_file(
                    input_name, family_name, options.suffix
                )
            output_name = makeOutputFileName(input_name, options.output_dir)

        font.save(output_name)
        logger.info("Saved font: '%s'", output_name)

        font.close()
        del font

    logger.info("Done!")


if __name__ == "__main__":
    main()