<commit_before># Copyright 2016 Adobe. All rights reserved.

# Methods:

# Parse args. If glyphlist is from file, read in entire file as single string,
# and remove all white space, then parse out glyph-names and GID's.

# For each font name:
#   Use fontTools library to open font and extract CFF table.
#   If error, skip font and report error.
#   Filter specified glyph list, if any, with list of glyphs in the font.
#   Open font plist file, if any. If not, create empty font plist.
#   Build alignment zone string
#   For identifier in glyph-list:
#     Get T2 charstring for glyph from parent font CFF table. If not present,
#       report and skip.
#     Get new alignment zone string if FDarray index (which font dict is used)
#       has changed.
#     Convert to bez
#     Build autohint point list string; this is used to tell if glyph has been
#       changed since the last time it was hinted.
#     If requested, check against plist dict, and skip if glyph is already
#       hinted or is manually hinted.
#     Call autohint library on bez string.
#     If change to the point list is permitted and happened, rebuild.
#     Autohint point list string.
#     Convert bez string to T2 charstring, and update parent font CFF.
#     Add glyph hint entry to plist file
#  Save font plist file.

from __future__ import print_function, absolute_import

import os
import re
import shutil
import sys
import time
import traceback

from fontTools.ttLib import TTFont, TTLibError, getTableModule

from .psautohint import autohint, autohintmm
from .otfFont import CFFFontData
from .ufoFont import UFOFontData, kAutohintName, kCheckOutlineName


kACIDKey = "AutoHintKey"

gLogFile = None
kFontPlistSuffix = ".plist"
kTempCFFSuffix = ".temp.ac.cff"
kProgressChar = "."


class ACOptions(object):
    def __init__(self):
        self.inputPaths = []
        self.outputPaths = []
        self.reference_font = None
        self.glyphList = []
        self.nameAliases = {}
        self.excludeGlyphList = False
        self.usePlistFile = False
        self.hintAll = False
        self.rehint = False
        self.verbose = True
        self.allowChanges = False
        self.noFlex = False
        self.noHintSub = False
        self.allow_no_blues = False
        self.hCounterGlyphs = []
        self.vCounterGlyphs = []
        self.logOnly = False
        self.logFile = None
        self.printDefaultFDDict = False
        self.printFDDictList = False
        self.allowDecimalCoords = False
        self.writeToDefaultLayer = False
        self.baseMaster = {}
        self.font_format = None


class ACFontError(Exception):
    pass


class ACHintError(Exception):
    pass


def logMsg(*args):
    for arg in args:
        msg = str(arg).strip()
        if not msg:
            sys.stdout.flush()
            if gLogFile:
                gLogFile.write("\n")
                gLogFile.flush()
            return

        if msg[-1] == ",":
            msg = msg[:-1]
            if msg == kProgressChar:
                sys.stdout.write(msg)  # avoid space, which is added by 'print'
            else:
                print(msg,)
            sys.stdout.flush()
            if gLogFile:
                gLogFile.write(msg)
                gLogFile.flush()
        else:
            print(msg)
            sys.stdout.flush()
            if gLogFile:
                gLogFile.write(msg + "\n")
                gLogFile.flush()


def getGlyphID(glyphTag, fontGlyphList):
    # FIXME: This is unnecessarily convoluted
    glyphID = None
    try:
        glyphID = int(glyphTag)
        fontGlyphList[glyphID]
    except IndexError:
        pass
    except ValueError:
        try:
            glyphID = fontGlyphList.index(glyphTag)
        except IndexError:
            pass
        except ValueError:
            pass
    return glyphID


def getGlyphNames(glyphTag, fontGlyphList, fontFileName):
    glyphNameList = []
    rangeList = glyphTag.split("-")
    prevGID = getGlyphID(rangeList[0], fontGlyphList)
    if prevGID is None:
        if len(rangeList) > 1:
            logMsg("\tWarning: glyph ID <%s> in range %s from glyph selection "
                   "list option is not in font. <%s>." %
                   (rangeList[0], glyphTag, fontFileName))
        else:
            logMsg("\tWarning: glyph ID <%s> from glyph selection list option "
                   "is not in font. <%s>." % (rangeList[0], fontFileName))
        return None
    glyphNameList.append(fontGlyphList[prevGID])

    for glyphTag2 in rangeList[1:]:
        gid = getGlyphID(glyphTag2, fontGlyphList)
        if gid is None:
            logMsg("\tWarning: glyph ID <%s> in range %s from glyph selection "
                   "list option is not in font. <%s>." %
                   (glyphTag2, glyphTag, fontFileName))
            return None
        for i in range(prevGID + 1, gid + 1):
            glyphNameList.append(fontGlyphList[i])
        prevGID = gid

    return glyphNameList


def filterGlyphList(options, fontGlyphList, fontFileName):
    # Return the list of glyphs which are in the intersection of the argument
    # list and the glyphs in the font.
    # Complain about glyphs in the argument list which are not in the font.
    if not options.glyphList:
        glyphList = fontGlyphList
    else:
        # expand ranges:
        glyphList = []
        for glyphTag in options.glyphList:
            glyphNames = getGlyphNames(glyphTag, fontGlyphList, fontFileName)
            if glyphNames is not None:
                glyphList.extend(glyphNames)
        if options.excludeGlyphList:
            newList = filter(lambda name: name not in glyphList, fontGlyphList)
            glyphList = newList
    return glyphList


fontInfoKeywordList = [
    'FontName',  # string
    'OrigEmSqUnits',
    'LanguageGroup',
    'DominantV',  # array
    'DominantH',  # array
    'FlexOK',  # string
    'BlueFuzz',
    'VCounterChars',  # counter
    'HCounterChars',  # counter
    'BaselineYCoord',
    'BaselineOvershoot',
    'CapHeight',
    'CapOvershoot',
    'LcHeight',
    'LcOvershoot',
    'AscenderHeight',
    'AscenderOvershoot',
    'FigHeight',
    'FigOvershoot',
    'Height5',
    'Height5Overshoot',
    'Height6',
    'Height6Overshoot',
    'DescenderOvershoot',
    'DescenderHeight',
    'SuperiorOvershoot',
    'SuperiorBaseline',
    'OrdinalOvershoot',
    'OrdinalBaseline',
    'Baseline5Overshoot',
    'Baseline5',
    'Baseline6Overshoot',
    'Baseline6',
]

integerPattern = """ -?\d+"""
arrayPattern = """ \[[ ,0-9]+\]"""
stringPattern = """ \S+"""
counterPattern = """ \([\S ]+\)"""


def printFontInfo(fontInfoString):
    for item in fontInfoKeywordList:
        if item in ['FontName', 'FlexOK']:
            matchingExp = item + stringPattern
        elif item in ['VCounterChars', 'HCounterChars']:
            matchingExp = item + counterPattern
        elif item in ['DominantV', 'DominantH']:
            matchingExp = item + arrayPattern
        else:
            matchingExp = item + integerPattern

        try:
            print('\t%s' % re.search(matchingExp, fontInfoString).group())
        except Exception:
            pass


flexPatthern = re.compile(
    r"preflx1[^f]+preflx2[\r\n]"
    r"(-*\d+\s+-*\d+\s+-*\d+\s+-*\d+\s+-*\d+\s+-*\d+\s+)"
    r"(-*\d+\s+-*\d+\s+-*\d+\s+-*\d+\s+-*\d+\s+-*\d+\s+).+?flx([\r\n])",
    re.DOTALL)
commentPattern = re.compile(r"[^\r\n]*%[^\r\n]*[\r\n]")
hintGroupPattern = re.compile(r"beginsubr.+?newcolors[\r\n]", re.DOTALL)
whiteSpacePattern = re.compile(r"\s+", re.DOTALL)


def makeACIdentifier(bezText):
    # Get rid of all the hint operators and their args
    # collapse flex to just the two rct's
    bezText = commentPattern.sub("", bezText)
    bezText = hintGroupPattern.sub("", bezText)
    bezText = flexPatthern.sub("\1 rct\3\2 rct\3", bezText)
    bezText = whiteSpacePattern.sub("", bezText)
    return bezText


def openFile(path, outFilePath, useHashMap, options):
    if os.path.isfile(path):
        font = openOpenTypeFile(path, outFilePath, options)
    else:
        # maybe it is a a UFO font.
        # We always use the hash map to skip glyphs that have been previously
        # processed, unless the user has report only, not make changes.
        font = openUFOFile(path, outFilePath, useHashMap, options)
    return font


def openUFOFile(path, outFilePath, useHashMap, options):
    # Check if has glyphs/contents.plist
    contentsPath = os.path.join(path, "glyphs", "contents.plist")
    if not os.path.exists(contentsPath):
        msg = "Font file must be a CFF, OTF, or ufo font file: %s." % (path)
        logMsg(msg)
        raise ACFontError(msg)

    # If user has specified a path other than the source font path,
    # then copy the entire UFO font, and operate on the copy.
    if (outFilePath is not None) and (
       os.path.abspath(path) != os.path.abspath(outFilePath)):
        if options.verbose:
            msg = "Copying from source UFO font to output UFO font " + \
                  "before processing..."
            logMsg(msg)
        if os.path.exists(outFilePath):
            shutil.rmtree(outFilePath)
        shutil.copytree(path, outFilePath)
        path = outFilePath
    font = UFOFontData(path, useHashMap, kAutohintName)
    font.useProcessedLayer = True
    # Programs in this list must be run before autohint,
    # if the outlines have been edited.
    font.requiredHistory.append(kCheckOutlineName)
    return font


def openOpenTypeFile(path, outFilePath, options):
    # If input font is CFF, build a dummy ttFont in memory.
    # Return ttFont, and flag if is a real OTF font.
    # Return flag is 0 if OTF, and 1 if CFF.
    fontType = 0  # OTF
    try:
        with open(path, "rb") as ff:
            data = ff.read(10)
    except (IOError, OSError):
        logMsg("Failed to open and read font file %s." % path)

    if data[:4] == b"OTTO":  # it is an OTF font, can process file directly
        try:
            ttFont = TTFont(path)
        except (IOError, OSError):
            raise ACFontError("Error opening or reading from font file <%s>." %
                              path)
        except TTLibError:
            raise ACFontError("Error parsing font file <%s>." % path)

        try:
            cffTable = ttFont["CFF "]
        except KeyError:
            raise ACFontError("Error: font is not a CFF font <%s>." %
                              path)
    else:
        # It is not an OTF file.
        if (data[0] == b'\1') and (data[1] == b'\0'):  # CFF file
            fontType = 1
        else:
            logMsg("Font file must be a CFF or OTF fontfile: %s." % path)
            raise ACFontError("Font file must be CFF or OTF file: %s." % path)

        # now package the CFF font as an OTF font.
        with open(path, "rb") as ff:
            data = ff.read()
        try:
            ttFont = TTFont()
            cffModule = getTableModule('CFF ')
            cffTable = cffModule.table_C_F_F_('CFF ')
            ttFont['CFF '] = cffTable
            cffTable.decompile(data, ttFont)
        except Exception:
            logMsg("\t%s" % (traceback.format_exception_only(sys.exc_info()[0],
                             sys.exc_info()[1])[-1]))
            logMsg("Attempted to read font %s as CFF." % path)
            raise ACFontError("Error parsing font file <%s>." % path)

    fontData = CFFFontData(ttFont, path, outFilePath, fontType, logMsg)
    return fontData


def removeTempFiles(fileList):
    for filePath in fileList:
        if os.path.exists(filePath):
            os.remove(filePath)


def cmpFDDictEntries(entry1, entry2):
    # entry = [glyphName, [fdIndex, glyphListIndex] ]
    if entry1[1][1] > entry2[1][1]:
        return 1
    elif entry1[1][1] < entry2[1][1]:
        return -1
    else:
        return 0


def hintFiles(options):
    if options.reference_font:
        hintFile(options, options.reference_font, None, reference_master=True)<commit_msg>Unused code<commit_after># Copyright 2016 Adobe. All rights reserved.

# Methods:

# Parse args. If glyphlist is from file, read in entire file as single string,
# and remove all white space, then parse out glyph-names and GID's.

# For each font name:
#   Use fontTools library to open font and extract CFF table.
#   If error, skip font and report error.
#   Filter specified glyph list, if any, with list of glyphs in the font.
#   Open font plist file, if any. If not, create empty font plist.
#   Build alignment zone string
#   For identifier in glyph-list:
#     Get T2 charstring for glyph from parent font CFF table. If not present,
#       report and skip.
#     Get new alignment zone string if FDarray index (which font dict is used)
#       has changed.
#     Convert to bez
#     Build autohint point list string; this is used to tell if glyph has been
#       changed since the last time it was hinted.
#     If requested, check against plist dict, and skip if glyph is already
#       hinted or is manually hinted.
#     Call autohint library on bez string.
#     If change to the point list is permitted and happened, rebuild.
#     Autohint point list string.
#     Convert bez string to T2 charstring, and update parent font CFF.
#     Add glyph hint entry to plist file
#  Save font plist file.

from __future__ import print_function, absolute_import

import os
import re
import shutil
import sys
import time
import traceback

from fontTools.ttLib import TTFont, TTLibError, getTableModule

from .psautohint import autohint, autohintmm
from .otfFont import CFFFontData
from .ufoFont import UFOFontData, kAutohintName, kCheckOutlineName


gLogFile = None


class ACOptions(object):
    def __init__(self):
        self.inputPaths = []
        self.outputPaths = []
        self.reference_font = None
        self.glyphList = []
        self.nameAliases = {}
        self.excludeGlyphList = False
        self.usePlistFile = False
        self.hintAll = False
        self.rehint = False
        self.verbose = True
        self.allowChanges = False
        self.noFlex = False
        self.noHintSub = False
        self.allow_no_blues = False
        self.hCounterGlyphs = []
        self.vCounterGlyphs = []
        self.logOnly = False
        self.logFile = None
        self.printDefaultFDDict = False
        self.printFDDictList = False
        self.allowDecimalCoords = False
        self.writeToDefaultLayer = False
        self.baseMaster = {}
        self.font_format = None


class ACFontError(Exception):
    pass


class ACHintError(Exception):
    pass


def logMsg(*args):
    for arg in args:
        msg = str(arg).strip()
        if not msg:
            sys.stdout.flush()
            if gLogFile:
                gLogFile.write("\n")
                gLogFile.flush()
            return

        if msg[-1] == ",":
            msg = msg[:-1]
            print(msg,)
            sys.stdout.flush()
            if gLogFile:
                gLogFile.write(msg)
                gLogFile.flush()
        else:
            print(msg)
            sys.stdout.flush()
            if gLogFile:
                gLogFile.write(msg + "\n")
                gLogFile.flush()


def getGlyphID(glyphTag, fontGlyphList):
    # FIXME: This is unnecessarily convoluted
    glyphID = None
    try:
        glyphID = int(glyphTag)
        fontGlyphList[glyphID]
    except IndexError:
        pass
    except ValueError:
        try:
            glyphID = fontGlyphList.index(glyphTag)
        except IndexError:
            pass
        except ValueError:
            pass
    return glyphID


def getGlyphNames(glyphTag, fontGlyphList, fontFileName):
    glyphNameList = []
    rangeList = glyphTag.split("-")
    prevGID = getGlyphID(rangeList[0], fontGlyphList)
    if prevGID is None:
        if len(rangeList) > 1:
            logMsg("\tWarning: glyph ID <%s> in range %s from glyph selection "
                   "list option is not in font. <%s>." %
                   (rangeList[0], glyphTag, fontFileName))
        else:
            logMsg("\tWarning: glyph ID <%s> from glyph selection list option "
                   "is not in font. <%s>." % (rangeList[0], fontFileName))
        return None
    glyphNameList.append(fontGlyphList[prevGID])

    for glyphTag2 in rangeList[1:]:
        gid = getGlyphID(glyphTag2, fontGlyphList)
        if gid is None:
            logMsg("\tWarning: glyph ID <%s> in range %s from glyph selection "
                   "list option is not in font. <%s>." %
                   (glyphTag2, glyphTag, fontFileName))
            return None
        for i in range(prevGID + 1, gid + 1):
            glyphNameList.append(fontGlyphList[i])
        prevGID = gid

    return glyphNameList


def filterGlyphList(options, fontGlyphList, fontFileName):
    # Return the list of glyphs which are in the intersection of the argument
    # list and the glyphs in the font.
    # Complain about glyphs in the argument list which are not in the font.
    if not options.glyphList:
        glyphList = fontGlyphList
    else:
        # expand ranges:
        glyphList = []
        for glyphTag in options.glyphList:
            glyphNames = getGlyphNames(glyphTag, fontGlyphList, fontFileName)
            if glyphNames is not None:
                glyphList.extend(glyphNames)
        if options.excludeGlyphList:
            newList = filter(lambda name: name not in glyphList, fontGlyphList)
            glyphList = newList
    return glyphList


fontInfoKeywordList = [
    'FontName',  # string
    'OrigEmSqUnits',
    'LanguageGroup',
    'DominantV',  # array
    'DominantH',  # array
    'FlexOK',  # string
    'BlueFuzz',
    'VCounterChars',  # counter
    'HCounterChars',  # counter
    'BaselineYCoord',
    'BaselineOvershoot',
    'CapHeight',
    'CapOvershoot',
    'LcHeight',
    'LcOvershoot',
    'AscenderHeight',
    'AscenderOvershoot',
    'FigHeight',
    'FigOvershoot',
    'Height5',
    'Height5Overshoot',
    'Height6',
    'Height6Overshoot',
    'DescenderOvershoot',
    'DescenderHeight',
    'SuperiorOvershoot',
    'SuperiorBaseline',
    'OrdinalOvershoot',
    'OrdinalBaseline',
    'Baseline5Overshoot',
    'Baseline5',
    'Baseline6Overshoot',
    'Baseline6',
]

integerPattern = """ -?\d+"""
arrayPattern = """ \[[ ,0-9]+\]"""
stringPattern = """ \S+"""
counterPattern = """ \([\S ]+\)"""


def printFontInfo(fontInfoString):
    for item in fontInfoKeywordList:
        if item in ['FontName', 'FlexOK']:
            matchingExp = item + stringPattern
        elif item in ['VCounterChars', 'HCounterChars']:
            matchingExp = item + counterPattern
        elif item in ['DominantV', 'DominantH']:
            matchingExp = item + arrayPattern
        else:
            matchingExp = item + integerPattern

        try:
            print('\t%s' % re.search(matchingExp, fontInfoString).group())
        except Exception:
            pass


def openFile(path, outFilePath, useHashMap, options):
    if os.path.isfile(path):
        font = openOpenTypeFile(path, outFilePath, options)
    else:
        # maybe it is a a UFO font.
        # We always use the hash map to skip glyphs that have been previously
        # processed, unless the user has report only, not make changes.
        font = openUFOFile(path, outFilePath, useHashMap, options)
    return font


def openUFOFile(path, outFilePath, useHashMap, options):
    # Check if has glyphs/contents.plist
    contentsPath = os.path.join(path, "glyphs", "contents.plist")
    if not os.path.exists(contentsPath):
        msg = "Font file must be a CFF, OTF, or ufo font file: %s." % (path)
        logMsg(msg)
        raise ACFontError(msg)

    # If user has specified a path other than the source font path,
    # then copy the entire UFO font, and operate on the copy.
    if (outFilePath is not None) and (
       os.path.abspath(path) != os.path.abspath(outFilePath)):
        if options.verbose:
            msg = "Copying from source UFO font to output UFO font " + \
                  "before processing..."
            logMsg(msg)
        if os.path.exists(outFilePath):
            shutil.rmtree(outFilePath)
        shutil.copytree(path, outFilePath)
        path = outFilePath
    font = UFOFontData(path, useHashMap, kAutohintName)
    font.useProcessedLayer = True
    # Programs in this list must be run before autohint,
    # if the outlines have been edited.
    font.requiredHistory.append(kCheckOutlineName)
    return font


def openOpenTypeFile(path, outFilePath, options):
    # If input font is CFF, build a dummy ttFont in memory.
    # Return ttFont, and flag if is a real OTF font.
    # Return flag is 0 if OTF, and 1 if CFF.
    fontType = 0  # OTF
    try:
        with open(path, "rb") as ff:
            data = ff.read(10)
    except (IOError, OSError):
        logMsg("Failed to open and read font file %s." % path)

    if data[:4] == b"OTTO":  # it is an OTF font, can process file directly
        try:
            ttFont = TTFont(path)
        except (IOError, OSError):
            raise ACFontError("Error opening or reading from font file <%s>." %
                              path)
        except TTLibError:
            raise ACFontError("Error parsing font file <%s>." % path)

        try:
            cffTable = ttFont["CFF "]
        except KeyError:
            raise ACFontError("Error: font is not a CFF font <%s>." %
                              path)
    else:
        # It is not an OTF file.
        if (data[0] == b'\1') and (data[1] == b'\0'):  # CFF file
            fontType = 1
        else:
            logMsg("Font file must be a CFF or OTF fontfile: %s." % path)
            raise ACFontError("Font file must be CFF or OTF file: %s." % path)

        # now package the CFF font as an OTF font.
        with open(path, "rb") as ff:
            data = ff.read()
        try:
            ttFont = TTFont()
            cffModule = getTableModule('CFF ')
            cffTable = cffModule.table_C_F_F_('CFF ')
            ttFont['CFF '] = cffTable
            cffTable.decompile(data, ttFont)
        except Exception:
            logMsg("\t%s" % (traceback.format_exception_only(sys.exc_info()[0],
                             sys.exc_info()[1])[-1]))
            logMsg("Attempted to read font %s as CFF." % path)
            raise ACFontError("Error parsing font file <%s>." % path)

    fontData = CFFFontData(ttFont, path, outFilePath, fontType, logMsg)
    return fontData


def cmpFDDictEntries(entry1, entry2):
    # entry = [glyphName, [fdIndex, glyphListIndex] ]
    if entry1[1][1] > entry2[1][1]:
        return 1
    elif entry1[1][1] < entry2[1][1]:
        return -1
    else:
        return 0


def hintFiles(options):
    if options.reference_font:
        hintFile(options, options.reference_font, None, reference_master=True)
{'fraction_non_alphanumeric': 0.09927339578044296, 'fraction_numerical': 0.005427645977413989, 'mean_word_length': 2.224244990121366, 'pattern_counts': {'":': 4, '<': 25, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '944468', 'n_tokens_mistral': 7184, 'n_tokens_neox': 6887, 'n_words': 2109}