You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			207 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			207 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
import argparse
 | 
						|
import logging
 | 
						|
import sys
 | 
						|
from io import StringIO
 | 
						|
from pathlib import Path
 | 
						|
 | 
						|
from fontTools import configLogger
 | 
						|
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
 | 
						|
from fontTools.feaLib.error import FeatureLibError
 | 
						|
from fontTools.feaLib.lexer import Lexer
 | 
						|
from fontTools.misc.cliTools import makeOutputFileName
 | 
						|
from fontTools.ttLib import TTFont, TTLibError
 | 
						|
from fontTools.voltLib.parser import Parser
 | 
						|
from fontTools.voltLib.voltToFea import TABLES, VoltToFea
 | 
						|
 | 
						|
log = logging.getLogger("fontTools.feaLib")
 | 
						|
 | 
						|
SUPPORTED_TABLES = TABLES + ["cmap"]
 | 
						|
 | 
						|
 | 
						|
def invalid_fea_glyph_name(name):
 | 
						|
    """Check if the glyph name is valid according to FEA syntax."""
 | 
						|
    if name[0] not in Lexer.CHAR_NAME_START_:
 | 
						|
        return True
 | 
						|
    if any(c not in Lexer.CHAR_NAME_CONTINUATION_ for c in name[1:]):
 | 
						|
        return True
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def sanitize_glyph_name(name):
 | 
						|
    """Sanitize the glyph name to ensure it is valid according to FEA syntax."""
 | 
						|
    sanitized = ""
 | 
						|
    for i, c in enumerate(name):
 | 
						|
        if i == 0 and c not in Lexer.CHAR_NAME_START_:
 | 
						|
            sanitized += "a" + c
 | 
						|
        elif c not in Lexer.CHAR_NAME_CONTINUATION_:
 | 
						|
            sanitized += "_"
 | 
						|
        else:
 | 
						|
            sanitized += c
 | 
						|
 | 
						|
    return sanitized
 | 
						|
 | 
						|
 | 
						|
def main(args=None):
 | 
						|
    """Build tables from a MS VOLT project into an OTF font"""
 | 
						|
    parser = argparse.ArgumentParser(
 | 
						|
        description="Use fontTools to compile MS VOLT projects."
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "input",
 | 
						|
        metavar="INPUT",
 | 
						|
        help="Path to the input font/VTP file to process",
 | 
						|
        type=Path,
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-f",
 | 
						|
        "--font",
 | 
						|
        metavar="INPUT_FONT",
 | 
						|
        help="Path to the input font (if INPUT is a VTP file)",
 | 
						|
        type=Path,
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-o",
 | 
						|
        "--output",
 | 
						|
        dest="output",
 | 
						|
        metavar="OUTPUT",
 | 
						|
        help="Path to the output font.",
 | 
						|
        type=Path,
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-t",
 | 
						|
        "--tables",
 | 
						|
        metavar="TABLE_TAG",
 | 
						|
        choices=SUPPORTED_TABLES,
 | 
						|
        nargs="+",
 | 
						|
        help="Specify the table(s) to be built.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-F",
 | 
						|
        "--debug-feature-file",
 | 
						|
        help="Write the generated feature file to disk.",
 | 
						|
        action="store_true",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "--ship",
 | 
						|
        help="Remove source VOLT tables from output font.",
 | 
						|
        action="store_true",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-v",
 | 
						|
        "--verbose",
 | 
						|
        help="Increase the logger verbosity. Multiple -v options are allowed.",
 | 
						|
        action="count",
 | 
						|
        default=0,
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-T",
 | 
						|
        "--traceback",
 | 
						|
        help="show traceback for exceptions.",
 | 
						|
        action="store_true",
 | 
						|
    )
 | 
						|
    options = parser.parse_args(args)
 | 
						|
 | 
						|
    levels = ["WARNING", "INFO", "DEBUG"]
 | 
						|
    configLogger(level=levels[min(len(levels) - 1, options.verbose)])
 | 
						|
 | 
						|
    output_font = options.output or Path(
 | 
						|
        makeOutputFileName(options.font or options.input)
 | 
						|
    )
 | 
						|
    log.info(f"Compiling MS VOLT to '{output_font}'")
 | 
						|
 | 
						|
    file_or_path = options.input
 | 
						|
    font = None
 | 
						|
 | 
						|
    # If the input is a font file, extract the VOLT data from the "TSIV" table
 | 
						|
    try:
 | 
						|
        font = TTFont(file_or_path)
 | 
						|
        if "TSIV" in font:
 | 
						|
            file_or_path = StringIO(font["TSIV"].data.decode("utf-8"))
 | 
						|
        else:
 | 
						|
            log.error('"TSIV" table is missing')
 | 
						|
            return 1
 | 
						|
    except TTLibError:
 | 
						|
        pass
 | 
						|
 | 
						|
    # If input is not a font file, the font must be provided
 | 
						|
    if font is None:
 | 
						|
        if not options.font:
 | 
						|
            log.error("Please provide an input font")
 | 
						|
            return 1
 | 
						|
        font = TTFont(options.font)
 | 
						|
 | 
						|
    # FEA syntax does not allow some glyph names that VOLT accepts, so if we
 | 
						|
    # found such glyph name we will temporarily rename such glyphs.
 | 
						|
    glyphOrder = font.getGlyphOrder()
 | 
						|
    tempGlyphOrder = None
 | 
						|
    if any(invalid_fea_glyph_name(n) for n in glyphOrder):
 | 
						|
        tempGlyphOrder = []
 | 
						|
        for n in glyphOrder:
 | 
						|
            if invalid_fea_glyph_name(n):
 | 
						|
                n = sanitize_glyph_name(n)
 | 
						|
                existing = set(tempGlyphOrder) | set(glyphOrder)
 | 
						|
                while n in existing:
 | 
						|
                    n = "a" + n
 | 
						|
            tempGlyphOrder.append(n)
 | 
						|
        font.setGlyphOrder(tempGlyphOrder)
 | 
						|
 | 
						|
    doc = Parser(file_or_path).parse()
 | 
						|
 | 
						|
    log.info("Converting VTP data to FEA")
 | 
						|
    converter = VoltToFea(doc, font)
 | 
						|
    try:
 | 
						|
        fea = converter.convert(options.tables, ignore_unsupported_settings=True)
 | 
						|
    except NotImplementedError as e:
 | 
						|
        if options.traceback:
 | 
						|
            raise
 | 
						|
        location = getattr(e.args[0], "location", None)
 | 
						|
        message = f'"{e}" is not supported'
 | 
						|
        if location:
 | 
						|
            path, line, column = location
 | 
						|
            log.error(f"{path}:{line}:{column}: {message}")
 | 
						|
        else:
 | 
						|
            log.error(message)
 | 
						|
        return 1
 | 
						|
 | 
						|
    fea_filename = options.input
 | 
						|
    if options.debug_feature_file:
 | 
						|
        fea_filename = output_font.with_suffix(".fea")
 | 
						|
        log.info(f"Writing FEA to '{fea_filename}'")
 | 
						|
        with open(fea_filename, "w") as fp:
 | 
						|
            fp.write(fea)
 | 
						|
 | 
						|
    log.info("Compiling FEA to OpenType tables")
 | 
						|
    try:
 | 
						|
        addOpenTypeFeaturesFromString(
 | 
						|
            font,
 | 
						|
            fea,
 | 
						|
            filename=fea_filename,
 | 
						|
            tables=options.tables,
 | 
						|
        )
 | 
						|
    except FeatureLibError as e:
 | 
						|
        if options.traceback:
 | 
						|
            raise
 | 
						|
        log.error(e)
 | 
						|
        return 1
 | 
						|
 | 
						|
    if options.ship:
 | 
						|
        for tag in ["TSIV", "TSIS", "TSIP", "TSID"]:
 | 
						|
            if tag in font:
 | 
						|
                del font[tag]
 | 
						|
 | 
						|
    # Restore original glyph names.
 | 
						|
    if tempGlyphOrder:
 | 
						|
        import io
 | 
						|
 | 
						|
        f = io.BytesIO()
 | 
						|
        font.save(f)
 | 
						|
        font = TTFont(f)
 | 
						|
        font.setGlyphOrder(glyphOrder)
 | 
						|
        font["post"].extraNames = []
 | 
						|
 | 
						|
    font.save(output_font)
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    sys.exit(main())
 |