コード例 #1
0
def test_pointCollector():
    ufoPath = getFontPath("MutatorSansBoldWideMutated.ufo")
    reader = UFOReader(ufoPath)
    glyphSet = reader.getGlyphSet()
    pen = PointCollector(glyphSet)
    glyphSet["B"].draw(pen)
    assert len(pen.points) == 38
    assert len(pen.tags) == 38
    assert pen.contours == [3, 37]
    assert pen.tags[:12] == [1, 1, 1, 1, 1, 1, 2, 2, 1, 2, 2, 1]

    pen = PointCollector(glyphSet, decompose=False)
    glyphSet["Aacute"].draw(pen)
    assert len(pen.points) == 0
    assert pen.contours == []
    assert pen.components == [("A", (1, 0, 0, 1, 0, 0)), ("acute", (1, 0, 0, 1, 484, 20))]

    pen = PointCollector(glyphSet, decompose=True)
    glyphSet["Aacute"].draw(pen)
    assert len(pen.points) == 20
    assert pen.contours == [3, 7, 11, 15, 19]
    assert pen.components == []

    pen = PointCollector(glyphSet)
    glyphSet["O"].draw(pen)
    assert len(pen.points) == 28
    assert pen.contours == [13, 27]
コード例 #2
0
ファイル: layerSet.py プロジェクト: typesupply/defcon
 def reloadLayers(self, layerData):
     """
     Reload the layers. This should not be called externally.
     """
     reader = UFOReader(self.font.path, validate=self.font.ufoLibReadValidate)
     # handle the layers
     currentLayerOrder = self.layerOrder
     for layerName, l in layerData.get("layers", {}).items():
         # new layer
         if layerName not in currentLayerOrder:
             glyphSet = reader.getGlyphSet(layerName, validateRead=self.ufoLibReadValidate, validateWrite=self.font.ufoLibWriteValidate)
             self.newLayer(layerName, glyphSet)
         # get the layer
         layer = self[layerName]
         # reload the layer info
         if l.get("info"):
             layer.color = None
             layer.lib.clear()
             layer._glyphSet.readLayerInfo(layer)
             self._stampLayerInfoDataState(layer)
         # reload the glyphs
         glyphNames = l.get("glyphNames", [])
         if glyphNames:
             layer.reloadGlyphs(glyphNames)
     # handle the order
     if layerData.get("order", False):
         newLayerOrder = reader.getLayerNames()
         for layerName in self.layerOrder:
             if layerName not in newLayerOrder:
                 newLayerOrder.append(layerName)
         self.layerOrder = newLayerOrder
     # handle the default layer
     if layerData.get("default", False):
         newDefaultLayerName = reader.getDefaultLayerName()
         self.defaultLayer = self[newDefaultLayerName]
コード例 #3
0
def test_ufoCharacterMapping_glyphNames():
    ufoPath = getFontPath("MutatorSansBoldWideMutated.ufo")
    reader = UFOReader(ufoPath)
    cmap, revCmap, anchors = fetchCharacterMappingAndAnchors(
        reader.getGlyphSet(), ufoPath, ["A"])
    assert cmap[0x0041] == "A"
    assert revCmap["A"] == [0x0041]
    assert anchors == {"A": [("top", 645, 815)]}
コード例 #4
0
ファイル: test_ufoState.py プロジェクト: yuga90/fontgoggles
def test_getUpdateInfo(tmpdir):
    ufoSource = getFontPath("MutatorSansBoldWideMutated.ufo")
    ufoPath = shutil.copytree(ufoSource, tmpdir / "test.ufo")
    reader = UFOReader(ufoPath, validate=False)
    glyphSet = reader.getGlyphSet()
    cmap, unicodes, anchors = fetchCharacterMappingAndAnchors(glyphSet, ufoPath)

    state = UFOState(reader, glyphSet, getUnicodesAndAnchors=lambda: (unicodes, anchors))

    feaPath = pathlib.Path(reader.fs.getsyspath("/features.fea"))
    feaPath.touch()

    state = state.newState()
    (needsFeaturesUpdate, needsGlyphUpdate, needsInfoUpdate, needsCmapUpdate,
     needsLibUpdate) = state.getUpdateInfo()
    assert needsFeaturesUpdate
    assert not needsGlyphUpdate
    assert not needsInfoUpdate
    assert not needsCmapUpdate

    infoPath = pathlib.Path(reader.fs.getsyspath("/fontinfo.plist"))
    infoPath.touch()

    state = state.newState()
    (needsFeaturesUpdate, needsGlyphUpdate, needsInfoUpdate, needsCmapUpdate,
     needsLibUpdate) = state.getUpdateInfo()
    assert not needsFeaturesUpdate
    assert not needsGlyphUpdate
    assert needsInfoUpdate
    assert not needsCmapUpdate

    glyph = Glyph("A", None)
    ppen = RecordingPointPen()
    glyphSet.readGlyph("A", glyph, ppen)
    glyph.anchors[0]["x"] = 123
    glyphSet.writeGlyph("A", glyph, ppen.replay)

    state = state.newState()
    (needsFeaturesUpdate, needsGlyphUpdate, needsInfoUpdate, needsCmapUpdate,
     needsLibUpdate) = state.getUpdateInfo()
    assert needsFeaturesUpdate
    assert needsGlyphUpdate
    assert not needsInfoUpdate
    assert not needsCmapUpdate

    glyph = Glyph("A", None)
    ppen = RecordingPointPen()
    glyphSet.readGlyph("A", glyph, ppen)
    glyph.unicodes = [123]
    glyphSet.writeGlyph("A", glyph, ppen.replay)

    state = state.newState()
    (needsFeaturesUpdate, needsGlyphUpdate, needsInfoUpdate, needsCmapUpdate,
     needsLibUpdate) = state.getUpdateInfo()
    assert not needsFeaturesUpdate
    assert needsGlyphUpdate
    assert not needsInfoUpdate
    assert needsCmapUpdate
コード例 #5
0
def test_pointCollectorQuad():
    ufoPath = getFontPath("QuadTest-Regular.ufo")
    reader = UFOReader(ufoPath)
    glyphSet = reader.getGlyphSet()
    pen = PointCollector(glyphSet)
    glyphSet["a"].draw(pen)
    assert len(pen.points) == 4
    assert len(pen.tags) == 4
    assert pen.contours == [3]
    assert pen.tags == [0, 0, 0, 0]
コード例 #6
0
ファイル: layerSet.py プロジェクト: typesupply/defcon
 def _fontSaveWasCompleted(self):
     """
     When saving a UFOZ, the underlying ZipFS object is closed.
     The objects then stored in layer._glyphSet contain references
     to a closed, and therefore unusable, filesystem. To remedy this,
     after the save is completed this method will be called and new
     GlyphSet objects will be created and assigned to the layers.
     """
     reader = UFOReader(self.font.path, validate=self.font.ufoLibReadValidate)
     if reader.fileStructure is UFOFileStructure.ZIP:
         for layerName in self.layerOrder:
             layer = self[layerName]
             layer._glyphSet = reader.getGlyphSet(layerName=layerName, validateRead=self.ufoLibReadValidate)
コード例 #7
0
 def _fontSaveWasCompleted(self):
     """
     When saving a UFOZ, the underlying ZipFS object is closed.
     The objects then stored in layer._glyphSet contain references
     to a closed, and therefore unusable, filesystem. To remedy this,
     after the save is completed this method will be called and new
     GlyphSet objects will be created and assigned to the layers.
     """
     reader = UFOReader(self.font.path,
                        validate=self.font.ufoLibReadValidate)
     if reader.fileStructure is UFOFileStructure.ZIP:
         for layerName in self.layerOrder:
             layer = self[layerName]
             layer._glyphSet = reader.getGlyphSet(
                 layerName=layerName, validateRead=self.ufoLibReadValidate)
コード例 #8
0
def test_ufoCharacterMapping():
    ufoPath = getFontPath("MutatorSansBoldWideMutated.ufo")
    reader = UFOReader(ufoPath)
    cmap, revCmap, anchors = fetchCharacterMappingAndAnchors(
        reader.getGlyphSet(), ufoPath)
    assert cmap[0x0041] == "A"
    assert revCmap["A"] == [0x0041]
    # MutatorSansBoldWideMutated.ufo/glyphs/A_.glif contains a commented-out <unicode>
    # tag, that must not be parsed, as well as a commented-out <anchor>.
    assert 0x1234 not in cmap
    assert anchors == {
        "A": [("top", 645, 815)],
        "E": [("top", 582.5, 815)],
        "macroncmb": [("_top", 0, 815)]
    }
コード例 #9
0
ファイル: vfb.py プロジェクト: jenskutilek/extractor
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)
コード例 #10
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
コード例 #11
0
ファイル: test_dsFont.py プロジェクト: typemytype/fontgoggles
def test_pointCollector():
    ufoPath = getFontPath("MutatorSansBoldWideMutated.ufo")
    reader = UFOReader(ufoPath)
    glyphSet = reader.getGlyphSet()
    pen = PointCollector(glyphSet)
    glyphSet["B"].draw(pen)
    assert len(pen.points) == 38
    assert len(pen.tags) == 38
    assert pen.contours == [3, 37]
    assert pen.tags[:12] == [1, 1, 1, 1, 1, 1, 2, 2, 1, 2, 2, 1]

    pen = PointCollector(glyphSet)
    glyphSet["Aacute"].draw(pen)
    assert len(pen.points) == 20
    assert pen.contours == [3, 7, 11, 15, 19]

    pen = PointCollector(glyphSet)
    glyphSet["O"].draw(pen)
    assert len(pen.points) == 28
    assert pen.contours == [13, 27]
コード例 #12
0
 def reloadLayers(self, layerData):
     """
     Reload the layers. This should not be called externally.
     """
     reader = UFOReader(self.font.path,
                        validate=self.font.ufoLibReadValidate)
     # handle the layers
     currentLayerOrder = self.layerOrder
     for layerName, l in layerData.get("layers", {}).items():
         # new layer
         if layerName not in currentLayerOrder:
             glyphSet = reader.getGlyphSet(
                 layerName,
                 validateRead=self.ufoLibReadValidate,
                 validateWrite=self.font.ufoLibWriteValidate)
             self.newLayer(layerName, glyphSet)
         # get the layer
         layer = self[layerName]
         # reload the layer info
         if l.get("info"):
             layer.color = None
             layer.lib.clear()
             layer._glyphSet.readLayerInfo(layer)
             self._stampLayerInfoDataState(layer)
         # reload the glyphs
         glyphNames = l.get("glyphNames", [])
         if glyphNames:
             layer.reloadGlyphs(glyphNames)
     # handle the order
     if layerData.get("order", False):
         newLayerOrder = reader.getLayerNames()
         for layerName in self.layerOrder:
             if layerName not in newLayerOrder:
                 newLayerOrder.append(layerName)
         self.layerOrder = newLayerOrder
     # handle the default layer
     if layerData.get("default", False):
         newDefaultLayerName = reader.getDefaultLayerName()
         self.defaultLayer = self[newDefaultLayerName]
コード例 #13
0
ファイル: ufoFont.py プロジェクト: themucha/coldtype
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
コード例 #14
0
    async def load(self, outputWriter):
        if self.doc is None:
            self.doc = DesignSpaceDocument.fromfile(self.fontPath)
            self.doc.findDefault()

        with tempfile.TemporaryDirectory(
                prefix="fontgoggles_temp") as ttFolder:
            sourcePathToTTPath = getTTPaths(self.doc, ttFolder)
            ufosToCompile = []
            ttPaths = []
            outputs = []
            coros = []
            self._sourceFiles = defaultdict(list)
            self._includedFeatureFiles = defaultdict(list)
            previousUFOs = self._ufos
            self._ufos = {}
            previousSourceData = self._sourceFontData
            self._sourceFontData = {}

            for source in self.doc.sources:
                sourceKey = (source.path, source.layerName)
                self._sourceFiles[pathlib.Path(source.path)].append(sourceKey)
                ufoState = previousUFOs.get(sourceKey)
                if ufoState is None:
                    reader = UFOReader(source.path, validate=False)
                    glyphSet = reader.getGlyphSet(layerName=source.layerName)
                    glyphSet.glyphClass = Glyph
                    if source.layerName is None:
                        includedFeatureFiles = extractIncludedFeatureFiles(
                            source.path, reader)
                        getUnicodesAndAnchors = functools.partial(
                            self._getUnicodesAndAnchors, source.path)
                    else:
                        includedFeatureFiles = []

                        # We're not compiling features nor do we need cmaps for these sparse layers,
                        # so we don't need need proper anchor or unicode data
                        def getUnicodesAndAnchors():
                            return ({}, {})

                    ufoState = UFOState(
                        reader,
                        glyphSet,
                        getUnicodesAndAnchors=getUnicodesAndAnchors,
                        includedFeatureFiles=includedFeatureFiles)
                for includedFeaFile in ufoState.includedFeatureFiles:
                    self._includedFeatureFiles[includedFeaFile].append(
                        sourceKey)
                self._ufos[sourceKey] = ufoState

                if source.layerName is not None:
                    continue

                if source.path in ufosToCompile:
                    continue
                ttPath = sourcePathToTTPath[source.path]
                if source.path in previousSourceData:
                    with open(ttPath, "wb") as f:
                        f.write(previousSourceData[source.path])
                    self._sourceFontData[source.path] = previousSourceData[
                        source.path]
                else:
                    ufosToCompile.append(source.path)
                    ttPaths.append(ttPath)
                    output = io.StringIO()
                    outputs.append(output)
                    coros.append(
                        compileUFOToPath(source.path, ttPath, output.write))

            # print(f"compiling {len(coros)} fonts")
            errors = await asyncio.gather(*coros, return_exceptions=True)

            for sourcePath, exc, output in zip(ufosToCompile, errors, outputs):
                output = output.getvalue()
                if output or exc is not None:
                    outputWriter(f"compile output for {sourcePath}:\n")
                    if output:
                        outputWriter(output)
                    if exc is not None:
                        outputWriter(f"{exc!r}\n")

            if any(errors):
                raise DesignSpaceSourceError(
                    f"Could not build '{os.path.basename(self.fontPath)}': "
                    "some sources did not successfully compile")
            for sourcePath, ttPath in zip(ufosToCompile, ttPaths):
                # Store compiled tt data so we can reuse it to rebuild ourselves
                # without recompiling the source.
                with open(ttPath, "rb") as f:
                    self._sourceFontData[sourcePath] = f.read()

            if not ufosToCompile and not self._needsVFRebuild:
                # self.ttFont and self.shaper are still up-to-date
                return

            vfFontData = await compileDSToBytes(self.fontPath, ttFolder,
                                                outputWriter)

        f = io.BytesIO(vfFontData)
        self.ttFont = TTFont(f, lazy=True)
        # Nice cookie for us from the worker
        self.masterModel = pickle.loads(self.ttFont["MPcl"].data)
        assert len(self.masterModel.deltaWeights) == len(self.doc.sources)

        self.shaper = HBShape(vfFontData,
                              getHorizontalAdvance=self._getHorizontalAdvance,
                              getVerticalAdvance=self._getVerticalAdvance,
                              getVerticalOrigin=self._getVerticalOrigin,
                              ttFont=self.ttFont)
        self._needsVFRebuild = False
コード例 #15
0
 def _loadLayer(reader: UFOReader,
                layerName: str,
                lazy: bool = True,
                default: bool = False) -> Layer:
     glyphSet = reader.getGlyphSet(layerName)
     return Layer.read(layerName, glyphSet, lazy=lazy, default=default)
コード例 #16
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