Пример #1
0
 def run_ufolib_import_validation(self):
     """
     ufoLib UFOReader.readLib method validates the lib.plist file
     :return: (list) list of test failure Result objects
     """
     res = Result(self.testpath)
     ss = StdStreamer(self.ufopath)
     if file_exists(self.testpath) is False:
         res.test_failed = (
             False
         )  # not a mandatory file in UFO spec, test passes if missing
         ss.stream_result(res)
         return self.test_fail_list
     try:
         # read lib.plist with ufoLib - the ufoLib library performs type validations on values on read
         ufolib_reader = UFOReader(self.ufopath, validate=True)
         ufolib_reader.readLib()
         res.test_failed = False
         ss.stream_result(res)
     except Exception as e:
         res.test_failed = True
         res.test_long_stdstream_string = (
             self.testpath + " failed ufoLib import test with error: " + str(e)
         )
         ss.stream_result(res)
         self.test_fail_list.append(res)
     return self.test_fail_list
Пример #2
0
def extractFontFromVFB(
    pathOrFile,
    destination,
    doGlyphs=True,
    doInfo=True,
    doKerning=True,
    doGroups=True,
    doFeatures=True,
    doLib=True,
    customFunctions=[],
):
    ufoPath = tempfile.mkdtemp(suffix=".ufo")
    cmds = [_ufo2vfbLocation, "-64", "-fo", pathOrFile, ufoPath]
    cmds = subprocess.list2cmdline(cmds)
    popen = subprocess.Popen(cmds, shell=True)
    popen.wait()
    try:
        # vfb2ufo writes ufo2, and has no update since 2015...so dont get to crazy here...
        # dont validate as vfb2ufo writes invalid ufos
        source = UFOReader(ufoPath, validate=False)
        if doInfo:
            source.readInfo(destination.info)
        if doKerning:
            kerning = source.readKerning()
            destination.kerning.update(kerning)
        if doGroups:
            groups = source.readGroups()
            destination.groups.update(groups)
        if doFeatures:
            features = source.readFeatures()
            destination.features.text = features
        if doLib:
            lib = source.readLib()
            destination.lib.update(lib)
        if doGlyphs:
            glyphSet = source.getGlyphSet()
            for glyphName in glyphSet.keys():
                destination.newGlyph(glyphName)
                glyph = destination[glyphName]
                pointPen = glyph.getPointPen()
                glyphSet.readGlyph(
                    glyphName=glyphName, glyphObject=glyph, pointPen=pointPen
                )
        for function in customFunctions:
            function(source, destination)
    finally:
        shutil.rmtree(ufoPath)
Пример #3
0
 def __attrs_post_init__(self) -> None:
     if self._path is not None:
         # if lazy argument is not set, default to lazy=True if path is provided
         if self._lazy is None:
             self._lazy = True
         reader = UFOReader(self._path, validate=self._validate)
         self.layers = LayerSet.read(reader, lazy=self._lazy)
         self.data = DataSet.read(reader, lazy=self._lazy)
         self.images = ImageSet.read(reader, lazy=self._lazy)
         self.info = Info.read(reader)
         self.features = Features(reader.readFeatures())
         self.groups = reader.readGroups()
         self.kerning = reader.readKerning()
         self.lib = reader.readLib()
         self._fileStructure = reader.fileStructure
         if self._lazy:
             # keep the reader around so we can close it when done
             self._reader = reader
Пример #4
0
    def read(cls, reader: UFOReader, lazy: bool = True) -> "Font":
        """Instantiates a Font object from a :class:`fontTools.ufoLib.UFOReader`.

        Args:
            path: The path to the UFO to load.
            lazy: If True, load glyphs, data files and images as they are accessed. If
                False, load everything up front.
        """
        self = cls(
            layers=LayerSet.read(reader, lazy=lazy),
            data=DataSet.read(reader, lazy=lazy),
            images=ImageSet.read(reader, lazy=lazy),
            info=Info.read(reader),
            features=Features(reader.readFeatures()),
            groups=reader.readGroups(),
            kerning=reader.readKerning(),
            lib=reader.readLib(),
            lazy=lazy,
        )
        self._fileStructure = reader.fileStructure
        if lazy:
            # keep the reader around so we can close it when done
            self._reader = reader
        return self
Пример #5
0
def main(args=None):
    from io import open

    options = parse_args(args)
    config = getConfig(options.config)
    svg_file = options.infile

    # Parse SVG to read the width, height attributes defined in it
    svgObj = parseSvg(svg_file)
    width = float(svgObj.attrib['width'].replace("px", " "))
    height = float(svgObj.attrib['height'].replace("px", " "))
    name = os.path.splitext(os.path.basename(svg_file))[0]
    ufo_font_path = config['font']['ufo']
    # Get the font metadata from UFO
    reader = UFOReader(ufo_font_path)
    writer = UFOWriter(ufo_font_path)

    infoObject = InfoObject()
    reader.readInfo(infoObject)
    fontName = config['font']['name']

    # Get the configuration for this svg
    try:
        svg_config = config['svgs'][name]
    except KeyError:
        print("\033[93mSkip: Configuration not found for svg : %r\033[0m" %
              name)
        return
    if 'unicode' in svg_config:
        unicodeVal = unicode_hex_list(svg_config['unicode'])
    else:
        unicodeVal = None
    glyphWidth = width + int(svg_config['left']) + int(svg_config['right'])
    if glyphWidth < 0:
        raise UFOLibError("Glyph %s has negative width." % name)

    contentsPlistPath = ufo_font_path + '/glyphs/contents.plist'
    try:
        with open(contentsPlistPath, "rb") as f:
            contentsPlist = load(f)
    except:
        raise UFOLibError("The file %s could not be read." % contentsPlistPath)

    glyph_name = svg_config['glyph_name']
    # Replace all capital letters with a following '_' to avoid file name clash in Windows
    glyph_file_name = re.sub(r'([A-Z]){1}', lambda pat: pat.group(1) + '_',
                             glyph_name) + '.glif'
    if glyph_name in contentsPlist:
        existing_glyph = True
    else:
        existing_glyph = False

    # Calculate the transformation to do
    transform = transform_list(config['font']['transform'])
    base = 0
    if 'base' in svg_config:
        base = int(svg_config['base'])
    transform[4] += int(svg_config['left'])  # X offset = left bearing
    transform[5] += height + base  # Y offset

    glif = svg2glif(svg_file,
                    name=svg_config['glyph_name'],
                    width=glyphWidth,
                    height=getattr(infoObject, 'unitsPerEm'),
                    unicodes=unicodeVal,
                    transform=transform,
                    version=config['font']['version'])

    if options.outfile is None:
        output_file = ufo_font_path + '/glyphs/' + glyph_file_name
    else:
        output_file = options.outfile
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(glif)

    print(
        "\033[94m[%s]\033[0m \033[92mConvert\033[0m %s -> %s \033[92m✔️\033[0m"
        % (fontName, name, output_file))

    # If this is a new glyph, add it to the UFO/glyphs/contents.plist
    if not existing_glyph:
        contentsPlist[glyph_name] = glyph_file_name
        writePlistAtomically(contentsPlist, contentsPlistPath)
        print(
            "\033[94m[%s]\033[0m \033[92mAdd\033[0m %s -> %s \033[92m✔️\033[0m"
            % (fontName, glyph_name, glyph_file_name))
        lib_obj = reader.readLib()
        lib_obj['public.glyphOrder'].append(glyph_name)
        writer.writeLib(lib_obj)
Пример #6
0
class UFOFont(BaseFont):

    ufoState = None

    def resetCache(self):
        super().resetCache()
        del self.defaultVerticalAdvance
        del self.defaultVerticalOriginY
        del self.globalColorLayerMapping

    def _setupReaderAndGlyphSet(self):
        self.reader = UFOReader(self.fontPath, validate=False)
        self.glyphSet = self.reader.getGlyphSet()
        self.glyphSet.glyphClass = Glyph
        self.layerGlyphSets = {}

    async def load(self, outputWriter):
        if hasattr(self, "reader"):
            self._cachedGlyphs = {}
            return
        self._setupReaderAndGlyphSet()
        self.info = SimpleNamespace()
        self.reader.readInfo(self.info)
        self.lib = self.reader.readLib()
        self._cachedGlyphs = {}
        if self.ufoState is None:
            includedFeatureFiles = extractIncludedFeatureFiles(
                self.fontPath, self.reader)
            self.ufoState = UFOState(
                self.reader,
                self.glyphSet,
                getUnicodesAndAnchors=self._getUnicodesAndAnchors,
                includedFeatureFiles=includedFeatureFiles)

        fontData = await compileUFOToBytes(self.fontPath, outputWriter)

        f = io.BytesIO(fontData)
        self.ttFont = TTFont(f, lazy=True)
        self.shaper = self._getShaper(fontData)

    def updateFontPath(self, newFontPath):
        """This gets called when the source file was moved."""
        super().updateFontPath(newFontPath)
        self._setupReaderAndGlyphSet()

    def getExternalFiles(self):
        return self.ufoState.includedFeatureFiles

    def canReloadWithChange(self, externalFilePath):
        if self.reader.fileStructure != UFOFileStructure.PACKAGE:
            # We can't (won't) partially reload .ufoz
            return False

        if externalFilePath:
            # Features need to be recompiled no matter what
            return False

        self.glyphSet.rebuildContents()

        self.ufoState = self.ufoState.newState()
        (needsFeaturesUpdate, needsGlyphUpdate, needsInfoUpdate,
         needsCmapUpdate, needsLibUpdate) = self.ufoState.getUpdateInfo()

        if needsFeaturesUpdate:
            return False

        if needsInfoUpdate:
            # font.info changed, all we care about is a possibly change unitsPerEm
            self.info = SimpleNamespace()
            self.reader.readInfo(self.info)

        if needsCmapUpdate:
            # The cmap changed. Let's update it in-place and only rebuild the shaper
            newCmap = {
                code: gn
                for gn, codes in self.ufoState.unicodes.items()
                for code in codes
            }
            fb = FontBuilder(font=self.ttFont)
            fb.setupCharacterMap(newCmap)
            f = io.BytesIO()
            self.ttFont.save(f, reorderTables=False)
            self.shaper = self._getShaper(f.getvalue())

        if needsLibUpdate:
            self.lib = self.reader.readLib()

        # We don't explicitly track changes in layers, but they may be involved
        # in building layered color glyphs, so let's just always reset the cache.
        self.resetCache()

        return True

    def _getUnicodesAndAnchors(self):
        unicodes = defaultdict(list)
        for code, gn in self.ttFont.getBestCmap().items():
            unicodes[gn].append(code)
        anchors = pickle.loads(self.ttFont["FGAx"].data)
        return unicodes, anchors

    def _getShaper(self, fontData):
        return HBShape(fontData,
                       getHorizontalAdvance=self._getHorizontalAdvance,
                       getVerticalAdvance=self._getVerticalAdvance,
                       getVerticalOrigin=self._getVerticalOrigin,
                       ttFont=self.ttFont)

    @cachedProperty
    def unitsPerEm(self):
        return self.info.unitsPerEm

    def _getGlyph(self, glyphName, layerName=None):
        glyph = self._cachedGlyphs.get((layerName, glyphName))
        if glyph is None:
            if glyphName == ".notdef" and glyphName not in self.glyphSet:
                # We need a .notdef glyph, so let's make one.
                glyph = NotDefGlyph(self.info.unitsPerEm)
                self._addOutlinePathToGlyph(glyph)
            else:
                try:
                    if layerName is None:
                        glyph = self.glyphSet[glyphName]
                    else:
                        glyph = self.getLayerGlyphSet(layerName)[glyphName]
                    self._addOutlinePathToGlyph(glyph)
                except Exception as e:
                    # TODO: logging would be better but then capturing in mainWindow.py is harder
                    print(f"Glyph '{glyphName}' could not be read: {e!r}",
                          file=sys.stderr)
                    glyph = self._getGlyph(".notdef")
            self._cachedGlyphs[(layerName, glyphName)] = glyph
        return glyph

    def _addOutlinePathToGlyph(self, glyph):
        if self.cocoa:
            pen = CocoaPen(self.glyphSet)
            glyph.draw(pen)
            glyph.outline = pen.path
        else:
            pen = RecordingPen()
            glyph.draw(pen)
            glyph.outline = pen

    def _getHorizontalAdvance(self, glyphName):
        glyph = self._getGlyph(glyphName)
        return glyph.width

    @cachedProperty
    def defaultVerticalAdvance(self):
        ascender = getattr(self.info, "ascender", None)
        descender = getattr(self.info, "descender", None)
        if ascender is None or descender is None:
            return self.info.unitsPerEm
        else:
            return ascender + abs(descender)

    @cachedProperty
    def defaultVerticalOriginY(self):
        ascender = getattr(self.info, "ascender", None)
        if ascender is None:
            return self.info.unitsPerEm  # ???
        else:
            return ascender

    def _getVerticalAdvance(self, glyphName):
        glyph = self._getGlyph(glyphName)
        vAdvance = glyph.height
        if vAdvance is None or vAdvance == 0:  # XXX default vAdv == 0 -> bad UFO spec
            vAdvance = self.defaultVerticalAdvance
        return -abs(vAdvance)

    def _getVerticalOrigin(self, glyphName):
        glyph = self._getGlyph(glyphName)
        vOrgX = glyph.width / 2
        lib = getattr(glyph, "lib", {})
        vOrgY = lib.get("public.verticalOrigin")
        if vOrgY is None:
            vOrgY = self.defaultVerticalOriginY
        return True, vOrgX, vOrgY

    def _getGlyphDrawing(self, glyphName, colorLayers):
        glyph = self._getGlyph(glyphName)
        if colorLayers:
            colorLayerMapping = glyph.lib.get(COLOR_LAYER_MAPPING_KEY)
            if colorLayerMapping is None:
                colorLayerMapping = self.globalColorLayerMapping
            if colorLayerMapping is not None:
                layers = []
                for layerName, colorID in colorLayerMapping:
                    glyph = self._getGlyph(glyphName, layerName)
                    if not isinstance(glyph, NotDefGlyph):
                        layers.append((glyph.outline, colorID))
                if layers:
                    return GlyphDrawing(layers)
        return GlyphDrawing([(glyph.outline, None)])

    @cachedProperty
    def colorPalettes(self):
        return self.lib.get(COLOR_PALETTES_KEY)

    @cachedProperty
    def globalColorLayerMapping(self):
        return self.lib.get(COLOR_LAYER_MAPPING_KEY)

    def getLayerGlyphSet(self, layerName):
        layerGlyphSet = self.layerGlyphSets.get(layerName)
        if layerGlyphSet is None:
            layerGlyphSet = self.reader.getGlyphSet(layerName)
            self.layerGlyphSets[layerName] = layerGlyphSet
        return layerGlyphSet
Пример #7
0
class UFOFontData:
    def __init__(self, path, log_only, write_to_default_layer):
        self._reader = UFOReader(path, validate=False)

        self.path = path
        self._glyphmap = None
        self._processed_layer_glyphmap = None
        self.newGlyphMap = {}
        self._fontInfo = None
        self._glyphsets = {}
        # If True, we are running in report mode and not doing any changes, so
        # we skip the hash map and process all glyphs.
        self.log_only = log_only
        # Used to store the hash of glyph data of already processed glyphs. If
        # the stored hash matches the calculated one, we skip the glyph.
        self._hashmap = None
        self.fontDict = None
        self.hashMapChanged = False
        # If True, then write data to the default layer
        self.writeToDefaultLayer = write_to_default_layer

    def getUnitsPerEm(self):
        return self.fontInfo.get("unitsPerEm", 1000)

    def getPSName(self):
        return self.fontInfo.get("postscriptFontName", "PSName-Undefined")

    @staticmethod
    def isCID():
        return False

    def convertToBez(self, name, read_hints, round_coords, doAll=False):
        # We do not yet support reading hints, so read_hints is ignored.
        width, bez, skip = self._get_or_skip_glyph(name, round_coords, doAll)
        if skip:
            return None, width

        bezString = "\n".join(bez)
        bezString = "\n".join(["% " + name, "sc", bezString, "ed", ""])
        return bezString, width

    def updateFromBez(self, bezData, name, width, mm_hint_info=None):
        # For UFO font, we don't use the width parameter:
        # it is carried over from the input glif file.
        layer = None
        if name in self.processedLayerGlyphMap:
            layer = PROCESSED_LAYER_NAME
        glyphset = self._get_glyphset(layer)

        glyph = BezGlyph(bezData)
        glyphset.readGlyph(name, glyph)
        if hasattr(glyph, 'width'):
            glyph.width = norm_float(glyph.width)
        self.newGlyphMap[name] = glyph

        # updateFromBez is called only if the glyph has been autohinted which
        # might also change its outline data. We need to update the edit status
        # in the hash map entry. I assume that convertToBez has been run
        # before, which will add an entry for this glyph.
        self.updateHashEntry(name)

    def save(self, path):
        if path is None:
            path = self.path

        if os.path.abspath(self.path) != os.path.abspath(path):
            # If user has specified a path other than the source font path,
            # then copy the entire UFO font, and operate on the copy.
            log.info("Copying from source UFO font to output UFO font before "
                     "processing...")
            if os.path.exists(path):
                shutil.rmtree(path)
            shutil.copytree(self.path, path)

        writer = UFOWriter(path, self._reader.formatVersion, validate=False)

        layer = PROCESSED_LAYER_NAME
        if self.writeToDefaultLayer:
            layer = None

        # Write layer contents.
        layers = writer.layerContents.copy()
        if self.writeToDefaultLayer and PROCESSED_LAYER_NAME in layers:
            # Delete processed glyphs directory
            writer.deleteGlyphSet(PROCESSED_LAYER_NAME)
            # Remove entry from 'layercontents.plist' file
            del layers[PROCESSED_LAYER_NAME]
        elif self.processedLayerGlyphMap or not self.writeToDefaultLayer:
            layers[PROCESSED_LAYER_NAME] = PROCESSED_GLYPHS_DIRNAME
        writer.layerContents.update(layers)
        writer.writeLayerContents()

        # Write glyphs.
        glyphset = writer.getGlyphSet(layer, defaultLayer=layer is None)
        for name, glyph in self.newGlyphMap.items():
            filename = self.glyphMap[name]
            if not self.writeToDefaultLayer and \
                    name in self.processedLayerGlyphMap:
                filename = self.processedLayerGlyphMap[name]
            # Recalculate glyph hashes
            if self.writeToDefaultLayer:
                self.recalcHashEntry(name, glyph)
            glyphset.contents[name] = filename
            glyphset.writeGlyph(name, glyph, glyph.drawPoints)
        glyphset.writeContents()

        # Write hashmap
        if self.hashMapChanged:
            self.writeHashMap(writer)

    @property
    def hashMap(self):
        if self._hashmap is None:
            try:
                data = self._reader.readData(HASHMAP_NAME)
            except UFOLibError:
                data = None
            if data:
                hashmap = ast.literal_eval(data.decode("utf-8"))
            else:
                hashmap = {HASHMAP_VERSION_NAME: HASHMAP_VERSION}

            version = (0, 0)
            if HASHMAP_VERSION_NAME in hashmap:
                version = hashmap[HASHMAP_VERSION_NAME]

            if version[0] > HASHMAP_VERSION[0]:
                raise FontParseError("Hash map version is newer than "
                                     "psautohint. Please update.")
            elif version[0] < HASHMAP_VERSION[0]:
                log.info("Updating hash map: was older version")
                hashmap = {HASHMAP_VERSION_NAME: HASHMAP_VERSION}

            self._hashmap = hashmap
        return self._hashmap

    def writeHashMap(self, writer):
        hashMap = self.hashMap
        if not hashMap:
            return  # no glyphs were processed.

        data = ["{"]
        for gName in sorted(hashMap.keys()):
            data.append("'%s': %s," % (gName, hashMap[gName]))
        data.append("}")
        data.append("")
        data = "\n".join(data)

        writer.writeData(HASHMAP_NAME, data.encode("utf-8"))

    def updateHashEntry(self, glyphName):
        # srcHash has already been set: we are fixing the history list.

        # Get hash entry for glyph
        srcHash, historyList = self.hashMap[glyphName]

        self.hashMapChanged = True
        # If the program is not in the history list, add it.
        if AUTOHINT_NAME not in historyList:
            historyList.append(AUTOHINT_NAME)

    def recalcHashEntry(self, glyphName, glyph):
        hashBefore, historyList = self.hashMap[glyphName]

        hash_pen = HashPointPen(glyph)
        glyph.drawPoints(hash_pen)
        hashAfter = hash_pen.getHash()

        if hashAfter != hashBefore:
            self.hashMap[glyphName] = [tostr(hashAfter), historyList]
            self.hashMapChanged = True

    def checkSkipGlyph(self, glyphName, newSrcHash, doAll):
        skip = False
        if self.log_only:
            return skip

        srcHash = None
        historyList = []

        # Get hash entry for glyph
        if glyphName in self.hashMap:
            srcHash, historyList = self.hashMap[glyphName]

        if srcHash == newSrcHash:
            if AUTOHINT_NAME in historyList:
                # The glyph has already been autohinted, and there have been no
                # changes since.
                skip = not doAll
            if not skip and AUTOHINT_NAME not in historyList:
                historyList.append(AUTOHINT_NAME)
        else:
            if CHECKOUTLINE_NAME in historyList:
                log.error(
                    "Glyph '%s' has been edited. You must first "
                    "run '%s' before running '%s'. Skipping.", glyphName,
                    CHECKOUTLINE_NAME, AUTOHINT_NAME)
                skip = True

            # If the source hash has changed, we need to delete the processed
            # layer glyph.
            self.hashMapChanged = True
            self.hashMap[glyphName] = [tostr(newSrcHash), [AUTOHINT_NAME]]
            if glyphName in self.processedLayerGlyphMap:
                del self.processedLayerGlyphMap[glyphName]

        return skip

    def _get_glyphset(self, layer_name=None):
        if layer_name not in self._glyphsets:
            glyphset = None
            try:
                glyphset = self._reader.getGlyphSet(layer_name)
            except UFOLibError:
                pass
            self._glyphsets[layer_name] = glyphset
        return self._glyphsets[layer_name]

    @staticmethod
    def get_glyph_bez(glyph, round_coords):
        pen = BezPen(glyph.glyphSet, round_coords)
        glyph.draw(pen)
        if not hasattr(glyph, "width"):
            glyph.width = 0
        return pen.bez

    def _get_or_skip_glyph(self, name, round_coords, doAll):
        # Get default glyph layer data, so we can check if the glyph
        # has been edited since this program was last run.
        # If the program name is in the history list, and the srcHash
        # matches the default glyph layer data, we can skip.
        glyphset = self._get_glyphset()
        glyph = glyphset[name]
        bez = self.get_glyph_bez(glyph, round_coords)

        # Hash is always from the default glyph layer.
        hash_pen = HashPointPen(glyph)
        glyph.drawPoints(hash_pen)
        skip = self.checkSkipGlyph(name, hash_pen.getHash(), doAll)

        # If there is a glyph in the processed layer, get the outline from it.
        if name in self.processedLayerGlyphMap:
            glyphset = self._get_glyphset(PROCESSED_LAYER_NAME)
            glyph = glyphset[name]
            bez = self.get_glyph_bez(glyph, round_coords)

        return glyph.width, bez, skip

    def getGlyphList(self):
        glyphOrder = self._reader.readLib().get(PUBLIC_GLYPH_ORDER, [])
        glyphList = list(self._get_glyphset().keys())

        # Sort the returned glyph list by the glyph order as we depend in the
        # order for expanding glyph ranges.
        def key_fn(v):
            if v in glyphOrder:
                return glyphOrder.index(v)
            return len(glyphOrder)

        return sorted(glyphList, key=key_fn)

    @property
    def glyphMap(self):
        if self._glyphmap is None:
            glyphset = self._get_glyphset()
            self._glyphmap = glyphset.contents
        return self._glyphmap

    @property
    def processedLayerGlyphMap(self):
        if self._processed_layer_glyphmap is None:
            self._processed_layer_glyphmap = {}
            glyphset = self._get_glyphset(PROCESSED_LAYER_NAME)
            if glyphset is not None:
                self._processed_layer_glyphmap = glyphset.contents
        return self._processed_layer_glyphmap

    @property
    def fontInfo(self):
        if self._fontInfo is None:
            info = SimpleNamespace()
            self._reader.readInfo(info)
            self._fontInfo = vars(info)
        return self._fontInfo

    def getFontInfo(self,
                    allow_no_blues,
                    noFlex,
                    vCounterGlyphs,
                    hCounterGlyphs,
                    fdIndex=0):
        if self.fontDict is not None:
            return self.fontDict

        fdDict = fdTools.FDDict()
        # should be 1 if the glyphs are ideographic, else 0.
        fdDict.LanguageGroup = self.fontInfo.get("languagegroup", "0")
        fdDict.OrigEmSqUnits = self.getUnitsPerEm()
        fdDict.FontName = self.getPSName()
        upm = self.getUnitsPerEm()
        low = min(-upm * 0.25,
                  self.fontInfo.get("openTypeOS2WinDescent", 0) - 200)
        high = max(upm * 1.25,
                   self.fontInfo.get("openTypeOS2WinAscent", 0) + 200)
        # Make a set of inactive alignment zones: zones outside of the font
        # bbox so as not to affect hinting. Used when src font has no
        # BlueValues or has invalid BlueValues. Some fonts have bad BBox
        # values, so I don't let this be smaller than -upm*0.25, upm*1.25.
        inactiveAlignmentValues = [low, low, high, high]
        blueValues = self.fontInfo.get("postscriptBlueValues", [])
        numBlueValues = len(blueValues)
        if numBlueValues < 4:
            if allow_no_blues:
                blueValues = inactiveAlignmentValues
                numBlueValues = len(blueValues)
            else:
                raise FontParseError(
                    "Font must have at least four values in its "
                    "BlueValues array for PSAutoHint to work!")
        blueValues.sort()
        # The first pair only is a bottom zone, where the first value is the
        # overshoot position; the rest are top zones, and second value of the
        # pair is the overshoot position.
        blueValues[0] = blueValues[0] - blueValues[1]
        for i in range(3, numBlueValues, 2):
            blueValues[i] = blueValues[i] - blueValues[i - 1]

        blueValues = [str(v) for v in blueValues]
        numBlueValues = min(numBlueValues, len(fdTools.kBlueValueKeys))
        for i in range(numBlueValues):
            key = fdTools.kBlueValueKeys[i]
            value = blueValues[i]
            setattr(fdDict, key, value)

        otherBlues = self.fontInfo.get("postscriptOtherBlues", [])
        numBlueValues = len(otherBlues)

        if len(otherBlues) > 0:
            i = 0
            numBlueValues = len(otherBlues)
            otherBlues.sort()
            for i in range(0, numBlueValues, 2):
                otherBlues[i] = otherBlues[i] - otherBlues[i + 1]
            otherBlues = [str(v) for v in otherBlues]
            numBlueValues = min(numBlueValues,
                                len(fdTools.kOtherBlueValueKeys))
            for i in range(numBlueValues):
                key = fdTools.kOtherBlueValueKeys[i]
                value = otherBlues[i]
                setattr(fdDict, key, value)

        vstems = self.fontInfo.get("postscriptStemSnapV", [])
        if not vstems:
            if allow_no_blues:
                # dummy value. Needs to be larger than any hint will likely be,
                # as the autohint program strips out any hint wider than twice
                # the largest global stem width.
                vstems = [fdDict.OrigEmSqUnits]
            else:
                raise FontParseError("Font does not have postscriptStemSnapV!")

        vstems.sort()
        if not vstems or (len(vstems) == 1 and vstems[0] < 1):
            # dummy value that will allow PyAC to run
            vstems = [fdDict.OrigEmSqUnits]
            log.warning("There is no value or 0 value for DominantV.")
        fdDict.DominantV = "[" + " ".join([str(v) for v in vstems]) + "]"

        hstems = self.fontInfo.get("postscriptStemSnapH", [])
        if not hstems:
            if allow_no_blues:
                # dummy value. Needs to be larger than any hint will likely be,
                # as the autohint program strips out any hint wider than twice
                # the largest global stem width.
                hstems = [fdDict.OrigEmSqUnits]
            else:
                raise FontParseError("Font does not have postscriptStemSnapH!")

        hstems.sort()
        if not hstems or (len(hstems) == 1 and hstems[0] < 1):
            # dummy value that will allow PyAC to run
            hstems = [fdDict.OrigEmSqUnits]
            log.warning("There is no value or 0 value for DominantH.")
        fdDict.DominantH = "[" + " ".join([str(v) for v in hstems]) + "]"

        if noFlex:
            fdDict.FlexOK = "false"
        else:
            fdDict.FlexOK = "true"

        # Add candidate lists for counter hints, if any.
        if vCounterGlyphs:
            temp = " ".join(vCounterGlyphs)
            fdDict.VCounterChars = "( %s )" % (temp)
        if hCounterGlyphs:
            temp = " ".join(hCounterGlyphs)
            fdDict.HCounterChars = "( %s )" % (temp)

        fdDict.BlueFuzz = self.fontInfo.get("postscriptBlueFuzz", 1)
        # postscriptBlueShift
        # postscriptBlueScale
        self.fontDict = fdDict
        return fdDict

    def getfdInfo(self,
                  allow_no_blues,
                  noFlex,
                  vCounterGlyphs,
                  hCounterGlyphs,
                  glyphList,
                  fdIndex=0):
        fdGlyphDict = None
        fdDict = self.getFontInfo(allow_no_blues, noFlex, vCounterGlyphs,
                                  hCounterGlyphs, fdIndex)
        fontDictList = [fdDict]

        # Check the fontinfo file, and add any other font dicts
        srcFontInfo = os.path.dirname(self.path)
        srcFontInfo = os.path.join(srcFontInfo, "fontinfo")
        maxX = self.getUnitsPerEm() * 2
        maxY = maxX
        minY = -self.getUnitsPerEm()
        if os.path.exists(srcFontInfo):
            with open(srcFontInfo, "r", encoding="utf-8") as fi:
                fontInfoData = fi.read()
            fontInfoData = re.sub(r"#[^\r\n]+", "", fontInfoData)

            if "FDDict" in fontInfoData:
                fdGlyphDict, fontDictList, finalFDict = \
                    fdTools.parseFontInfoFile(
                        fontDictList, fontInfoData, glyphList, maxY, minY,
                        self.getPSName())
                if finalFDict is None:
                    # If a font dict was not explicitly specified for the
                    # output font, use the first user-specified font dict.
                    fdTools.mergeFDDicts(fontDictList[1:], self.fontDict)
                else:
                    fdTools.mergeFDDicts([finalFDict], self.fontDict)

        return fdGlyphDict, fontDictList

    @staticmethod
    def close():
        return