def run_ufolib_import_validation(self):
     """
     ufoLib UFOReader.readInfo method validates value types in the fontinfo.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 fontinfo.plist with ufoLib - the ufoLib library performs type validations on values on read
         ufolib_reader = UFOReader(self.ufopath, validate=True)
         ufolib_reader.readInfo(self.fontinfo_obj)
         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 testUFO2(self):
		self.makeUFO(formatVersion=2)
		reader = UFOReader(self.ufoPath, validate=True)
		kerning = reader.readKerning()
		self.assertEqual(self.expectedKerning, kerning)
		groups = reader.readGroups()
		self.assertEqual(self.expectedGroups, groups)
		info = TestInfoObject()
		reader.readInfo(info)
示例#3
0
	def testUFO2(self):
		self.makeUFO(formatVersion=2)
		reader = UFOReader(self.ufoPath, validate=True)
		kerning = reader.readKerning()
		self.assertEqual(self.expectedKerning, kerning)
		groups = reader.readGroups()
		self.assertEqual(self.expectedGroups, groups)
		info = TestInfoObject()
		reader.readInfo(info)
示例#4
0
 def testRead(self):
     originalData = dict(fontInfoVersion1)
     self._writeInfoToPlist(originalData)
     infoObject = TestInfoObject()
     reader = UFOReader(self.dstDir, validate=True)
     reader.readInfo(infoObject)
     for attr in dir(infoObject):
         if attr not in fontInfoVersion2:
             continue
         originalValue = fontInfoVersion2[attr]
         readValue = getattr(infoObject, attr)
         self.assertEqual(originalValue, readValue)
示例#5
0
	def testRead(self):
		originalData = dict(fontInfoVersion1)
		self._writeInfoToPlist(originalData)
		infoObject = TestInfoObject()
		reader = UFOReader(self.dstDir, validate=True)
		reader.readInfo(infoObject)
		for attr in dir(infoObject):
			if attr not in fontInfoVersion2:
				continue
			originalValue = fontInfoVersion2[attr]
			readValue = getattr(infoObject, attr)
			self.assertEqual(originalValue, readValue)
示例#6
0
 def testFontStyleConversion(self):
     fontStyle1To2 = {
         64: "regular",
         1: "italic",
         32: "bold",
         33: "bold italic"
     }
     for old, new in list(fontStyle1To2.items()):
         info = dict(fontInfoVersion1)
         info["fontStyle"] = old
         self._writeInfoToPlist(info)
         reader = UFOReader(self.dstDir, validate=True)
         infoObject = TestInfoObject()
         reader.readInfo(infoObject)
         self.assertEqual(new, infoObject.styleMapStyleName)
示例#7
0
	def testFontStyleConversion(self):
		fontStyle1To2 = {
			64 : "regular",
			1  : "italic",
			32 : "bold",
			33 : "bold italic"
		}
		for old, new in list(fontStyle1To2.items()):
			info = dict(fontInfoVersion1)
			info["fontStyle"] = old
			self._writeInfoToPlist(info)
			reader = UFOReader(self.dstDir, validate=True)
			infoObject = TestInfoObject()
			reader.readInfo(infoObject)
			self.assertEqual(new, infoObject.styleMapStyleName)
示例#8
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)
示例#9
0
def compileUFOToFont(ufoPath):
    """Compile the source UFO to a TTF with the smallest amount of tables
    needed to let HarfBuzz do its work. That would be 'cmap', 'post' and
    whatever OTL tables are needed for the features. Return the compiled
    font data.

    This function may do some redundant work (eg. we need an UFOReader
    elsewhere, too), but having a picklable argument and return value
    allows us to run it in a separate process, enabling parallelism.
    """
    reader = UFOReader(ufoPath, validate=False)
    glyphSet = reader.getGlyphSet()
    info = SimpleNamespace()
    reader.readInfo(info)

    glyphOrder = sorted(glyphSet.keys())  # no need for the "real" glyph order
    if ".notdef" not in glyphOrder:
        # We need a .notdef glyph, so let's make one.
        glyphOrder.insert(0, ".notdef")
    cmap, revCmap, anchors = fetchCharacterMappingAndAnchors(glyphSet, ufoPath)
    fb = FontBuilder(round(info.unitsPerEm))
    fb.setupGlyphOrder(glyphOrder)
    fb.setupCharacterMap(cmap)
    fb.setupPost()  # This makes sure we store the glyph names
    ttFont = fb.font
    # Store anchors in the font as a private table: this is valuable
    # data that our parent process can use to do faster reloading upon
    # changes.
    ttFont["FGAx"] = newTable("FGAx")
    ttFont["FGAx"].data = pickle.dumps(anchors)
    ufo = MinimalFontObject(ufoPath, reader, revCmap, anchors)
    feaComp = FeatureCompiler(ufo, ttFont)
    try:
        feaComp.compile()
    except FeatureLibError as e:
        error = f"{e.__class__.__name__}: {e}"
    except Exception:
        # This is most likely a bug, and not an input error, so perhaps
        # we shouldn't even catch it here.
        error = traceback.format_exc()
    else:
        error = None
    return ttFont, error
示例#10
0
 def testWidthNameConversion(self):
     widthName1To2 = {
         "Ultra-condensed": 1,
         "Extra-condensed": 2,
         "Condensed": 3,
         "Semi-condensed": 4,
         "Medium (normal)": 5,
         "Semi-expanded": 6,
         "Expanded": 7,
         "Extra-expanded": 8,
         "Ultra-expanded": 9
     }
     for old, new in list(widthName1To2.items()):
         info = dict(fontInfoVersion1)
         info["widthName"] = old
         self._writeInfoToPlist(info)
         reader = UFOReader(self.dstDir, validate=True)
         infoObject = TestInfoObject()
         reader.readInfo(infoObject)
         self.assertEqual(new, infoObject.openTypeOS2WidthClass)
示例#11
0
	def testWidthNameConversion(self):
		widthName1To2 = {
			"Ultra-condensed" : 1,
			"Extra-condensed" : 2,
			"Condensed"		  : 3,
			"Semi-condensed"  : 4,
			"Medium (normal)" : 5,
			"Semi-expanded"	  : 6,
			"Expanded"		  : 7,
			"Extra-expanded"  : 8,
			"Ultra-expanded"  : 9
		}
		for old, new in list(widthName1To2.items()):
			info = dict(fontInfoVersion1)
			info["widthName"] = old
			self._writeInfoToPlist(info)
			reader = UFOReader(self.dstDir, validate=True)
			infoObject = TestInfoObject()
			reader.readInfo(infoObject)
			self.assertEqual(new, infoObject.openTypeOS2WidthClass)
示例#12
0
def getSortInfoUFO(fontPath: PathLike, fontNum: int):
    from fontTools.ufoLib import UFOReader
    assert fontNum == 0
    suffix = fontPath.suffix.lower().lstrip(".")
    reader = UFOReader(fontPath, validate=False)
    info = SimpleNamespace()
    reader.readInfo(info)
    sortInfo = dict(suffix=suffix)
    ufoAttrs = [
        ("familyName", "familyName"),
        ("styleName", "styleName"),
        ("weight", "openTypeOS2WeightClass"),
        ("width", "openTypeOS2WidthClass"),
        ("italicAngle", "italicAngle"),
    ]
    for key, attr in ufoAttrs:
        val = getattr(info, attr, None)
        if val is not None:
            if key == "italicAngle":
                val = -val  # negative for intuitive sort order
            sortInfo[key] = val
    return sortInfo
示例#13
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)
示例#14
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
示例#15
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
示例#16
0
 def read(cls, reader: UFOReader) -> "Info":
     """Instantiates a Info object from a
     :class:`fontTools.ufoLib.UFOReader`."""
     self = cls()
     reader.readInfo(self)
     return self