class PointsModel(object):
    """
    See: https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview

    >>> #model = Model(ds)
    >>> #model.getScalars(dict(SHPE=0.25, wght=0.25))

    >>> masterValues = [0, 100, 200, 100, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100]
    >>> location = dict(SHPE=0.5, wght=0.5)
    >>> #model.interpolatePoints(location, masters)
    25.0

    """
    def __init__(self, designSpace, masters):
        self.ds = designSpace
        self.vm = VariationModel(self.ds.normalizedLocations,
                                 axisOrder=self.ds.axisOrder)
        self.masters = masters  # List of master fonts, in the right order.
        self._masterValues = {}  # Cache of master values. Keys id

    def __repr__(self):
        return '<PageBot %s %s>' % (self.__class__.__name__,
                                    self.ds.familyName)

    def getScalars(self, location):
        return self.vm.getScalars(location)

    def getDeltas(self, glyphName):
        deltas = []
        mvsX, mvsY = self.getMasterValues(glyphName)
        #print(len(self.vm.deltaWeights), len(mvsX))
        for index in range(len(mvsX)):
            deltas.append((self.vm.getDeltas(mvsX[index]),
                           self.vm.getDeltas(mvsY[index])))
        return deltas

    def getMasterValues(self, glyphName):
        if self._masterValues is None:
            mvx = []
            mvy = []
            self._masterValues = mvx, mvy
            for master in self.masters:
                points = getPoints(master[glyphName])
                for pIndex, point in enumerate(points):
                    if len(mvx) <= pIndex:
                        mvx.append([])
                        mvy.append([])
                    mvx[pIndex].append(point.x)
                    mvy[pIndex].append(point.y)
        return self._masterValues

    def interpolatePoints(self, glyphName, location):
        interpolatedPoints = []
        mvsX, mvsY = self.getMasterValues(glyphName)
        for index in range(len(mvsX)):
            interpolatedPoints.append(
                (self.vm.interpolateFromMasters(location, mvsX[index]),
                 self.vm.interpolateFromMasters(location, mvsY[index])))
        return interpolatedPoints
Exemple #2
0
def test_modeling_error(numLocations, numSamples):
    # https://github.com/fonttools/fonttools/issues/2213
    locations = [{
        "axis": float(i) / numLocations
    } for i in range(numLocations)]
    masterValues = [100.0 if i else 0.0 for i in range(numLocations)]

    model = VariationModel(locations)

    for i in range(numSamples):
        loc = {"axis": float(i) / numSamples}
        scalars = model.getScalars(loc)

        deltas_float = model.getDeltas(masterValues)
        deltas_round = model.getDeltas(masterValues, round=round)

        expected = model.interpolateFromDeltasAndScalars(deltas_float, scalars)
        actual = model.interpolateFromDeltasAndScalars(deltas_round, scalars)

        err = abs(actual - expected)
        assert err <= 0.5, (i, err)
Exemple #3
0
class VariationModelMutator(object):
    """ a thing that looks like a mutator on the outside,
        but uses the fonttools varlib logic to calculate.
    """
    def __init__(self, items, axes, model=None):
        # items: list of locationdict, value tuples
        # axes: list of axis dictionaried, not axisdescriptor objects.
        # model: a model, if we want to share one
        self.axisOrder = [a.name for a in axes]
        self.axisMapper = AxisMapper(axes)
        self.axes = {}
        for a in axes:
            self.axes[a.name] = (a.minimum, a.default, a.maximum)
        if model is None:
            self.model = VariationModel([self._normalize(a) for a, b in items],
                                        axisOrder=self.axisOrder)
        else:
            self.model = model
        self.masters = [b for a, b in items]

    def get(self, key):
        if key in self.model.locations:
            i = self.model.locations.index(key)
            return self.masters[i]
        return None

    def getFactors(self, location):
        nl = self._normalize(location)
        return self.model.getScalars(nl)

    def makeInstance(self, location, bend=False):
        # check for anisotropic locations here
        if bend:
            location = self.axisMapper(location)
        nl = self._normalize(location)
        return self.model.interpolateFromMasters(nl, self.masters)

    def _normalize(self, location):
        return normalizeLocation(location, self.axes)
class VariationModelMutator(object):
    """ a thing that looks like a mutator on the outside,
        but uses the fonttools varlib logic to calculate.
    """
    def __init__(self, items, axes, model=None):
        # items: list of locationdict, value tuples
        # axes: list of axis dictionaried, not axisdescriptor objects.
        # model: a model, if we want to share one
        self.axisOrder = [a.name for a in axes]
        self.axisMapper = AxisMapper(axes)
        self.axes = {}
        for a in axes:
            mappedMinimum, mappedDefault, mappedMaximum = a.map_forward(
                a.minimum), a.map_forward(a.default), a.map_forward(a.maximum)
            #self.axes[a.name] = (a.minimum, a.default, a.maximum)
            self.axes[a.name] = (mappedMinimum, mappedDefault, mappedMaximum)

        if model is None:
            dd = [self._normalize(a) for a, b in items]
            ee = self.axisOrder
            self.model = VariationModel(dd, axisOrder=ee)
        else:
            self.model = model
        self.masters = [b for a, b in items]
        self.locations = [a for a, b in items]

    def get(self, key):
        if key in self.model.locations:
            i = self.model.locations.index(key)
            return self.masters[i]
        return None

    def getFactors(self, location):
        nl = self._normalize(location)
        return self.model.getScalars(nl)

    def getMasters(self):
        return self.masters

    def getSupports(self):
        return self.model.supports

    def getReach(self):
        items = []
        for supportIndex, s in enumerate(self.getSupports()):
            sortedOrder = self.model.reverseMapping[supportIndex]
            #print("getReach", self.masters[sortedOrder], s)
            #print("getReach", self.locations[sortedOrder])
            items.append((self.masters[sortedOrder], s))
        return items

    def makeInstance(self, location, bend=False):
        # check for anisotropic locations here
        #print("\t1", location)
        if bend:
            location = self.axisMapper(location)
        #print("\t2", location)
        nl = self._normalize(location)
        return self.model.interpolateFromMasters(nl, self.masters)

    def _normalize(self, location):
        return normalizeLocation(location, self.axes)
Exemple #5
0
class Model:
    """
    See: https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview
    """

    """
    FIXME: raises NotImplementedError, this only works if NewFont() is registered?
    Maybe create first and the run on a file?
    >>> import os
    >>> from fontParts.world import NewFont
    >>> x = y = 800
    >>> x2 = x//2
    >>> x4 = x//4
    >>> cy = y//2
    >>> FAMILY = 'PageBotTest'
    >>> PATH = '/tmp/%s-%s.ufo'
    >>> GNAME = 'Q'
    >>> fReg = NewFont()
    >>> fReg.axes = {} # Pretend to be a VF
    >>> fReg.info.familyName = FAMILY
    >>> fReg.info.styleName = 'Regular'
    >>> gReg = fReg.newGlyph(GNAME)
    >>> pen = gReg.getPen()
    >>> pen.moveTo((0, 0))
    >>> pen.lineTo((0, y))
    >>> pen.lineTo((x2, y))
    >>> pen.lineTo((x2, 0))
    >>> pen.lineTo((0, 0))
    >>> pen.closePath()
    >>> gReg.leftMargin = gReg.rightMargin = 50
    >>> gReg.width
    500
    >>> fReg.save(PATH % (FAMILY, fReg.info.styleName))

    >>> fBld = NewFont()
    >>> fBld.axes = {} # Pretend to be a VF
    >>> fBld.info.familyName = FAMILY
    >>> fBld.info.styleName = 'Bold'
    >>> gBld = fBld.newGlyph(GNAME)
    >>> pen = gBld.getPen()
    >>> pen.moveTo((0, 0))
    >>> pen.lineTo((0, y))
    >>> pen.lineTo((x, y))
    >>> pen.lineTo((x, 0))
    >>> pen.lineTo((0, 0))
    >>> pen.closePath()
    >>> gBld.leftMargin = gBld.rightMargin = 30
    >>> gBld.width
    860
    >>> fBld.save(PATH % (FAMILY, fBld.info.styleName))

    >>> fLght = NewFont()
    >>> fLght.axes = {} # Pretend to be a VF
    >>> fLght.info.familyName = FAMILY
    >>> fLght.info.styleName = 'Light'
    >>> gLght = fLght.newGlyph(GNAME)
    >>> pen = gLght.getPen()
    >>> pen.moveTo((0, 0))
    >>> pen.lineTo((0, y))
    >>> pen.lineTo((x4, y))
    >>> pen.lineTo((x4, 0))
    >>> pen.lineTo((0, 0))
    >>> pen.closePath()
    >>> gLght.leftMargin = gLght.rightMargin = 70
    >>> gLght.width
    340
    >>> fLght.save(PATH % (FAMILY, fLght.info.styleName))

    >>> fCnd = NewFont()
    >>> fCnd.info.familyName = FAMILY
    >>> fCnd.info.styleName = 'Condensed'
    >>> gCnd = fCnd.newGlyph(GNAME)
    >>> pen = gCnd.getPen()
    >>> pen.moveTo((0, 0))
    >>> pen.lineTo((0, y/2))
    >>> pen.lineTo((x, y/2))
    >>> pen.lineTo((x, 0))
    >>> pen.lineTo((0, 0))
    >>> pen.closePath()
    >>> gCnd.leftMargin = gCnd.rightMargin = 20
    >>> gCnd.width
    840
    >>> fCnd.save(PATH % (FAMILY, fCnd.info.styleName))
    >>> # We created the masters now build the design space from it.
    >>> ds = DesignSpace() # Start empty design space, not reading from a file.
    >>> # Construct axes as list, to maintain the defined order.
    >>> axis1 = Axis(tag='wght', name='Weight', minimum=100, default=400, maximum=900)
    >>> axis2 = Axis(tag='YTUC', name='Y-Transparancy-UC', minimum=0, default=100, maximum=100)
    >>> ds.appendAxes((axis1, axis2))
    >>> # Construct master info as list, to maintain the defined order.
    >>> # Default masters contains "info" attribute.
    >>> loc = Location(wght=100, YTUC=100)
    >>> iLght = FontInfo(name=fLght.info.styleName, familyName=fLght.info.familyName, path=fLght.path, location=loc, styleName=fLght.info.styleName)
    >>> loc = Location(wght=400, YTUC=100)
    >>> iReg = FontInfo(info=dict(copy=True), name=fReg.info.styleName, familyName=fReg.info.familyName, path=fReg.path, location=loc, styleName=fReg.info.styleName)
    >>> loc = Location(wght=900, YTUC=100)
    >>> iBld = FontInfo(name=fBld.info.styleName, familyName=fBld.info.familyName, path=fBld.path, location=loc, styleName=fBld.info.styleName)
    >>> loc = Location(wght=400, YTUC=0)
    >>> iCnd = FontInfo(name=fCnd.info.styleName, familyName=fCnd.info.familyName, path=fCnd.path, location=loc, styleName=fCnd.info.styleName)

    >>> ds.appendMasters((iLght, iReg, iBld, iCnd)) # Set list of master info
    >>> ds.masterList # DesignSpace.masterLust gives list of FontInfo items, in defined order.
    [<FontInfo PageBotTest-Light>, <FontInfo PageBotTest-Regular>, <FontInfo PageBotTest-Bold>, <FontInfo PageBotTest-Condensed>]
    >>> len(ds.masters)
    4
    >>> ds.save('/tmp/%s.designspace' % FAMILY)
    >>> masters = {fReg.path: fReg, fBld.path: fBld, fLght.path: fLght, fCnd.path: fCnd }
    >>> # Now we have a design space and master dictionary, we can create the model
    >>> m = Model(ds, masters)
    >>> m
    <PageBot Model PageBotTest axes:2 masters:4>
    >>> [f.info.styleName for f in m.masterList] # Sorted as defined, with Regular as #1.
    ['Regular', 'Light', 'Bold', 'Condensed']
    >>> # Get combined coordinates from all masters. This is why they need to be compatible.
    >>> mpx, mpy, mcx, mcy, mt = m.getMasterValues(GNAME) # Coordinates in order of masterList for (p0, p1, p2, p3)
    >>> mpx # [[px0, px0, px0, px0], [px1, px1, px1, px1], [px2, px2, px2, px2], [px3, px3, px3, px3]]
    [[50, 70, 30, 20], [450, 270, 830, 820], [450, 270, 830, 820], [50, 70, 30, 20]]
    >>> mpy # [[py1, py1, py1, py1], [py2, py2, py2, py2], ...]
    [[800, 800, 800, 400.0], [800, 800, 800, 400.0], [0, 0, 0, 0], [0, 0, 0, 0]]
    >>> mcx, mcy # No components here
    ([], [])
    >>> mt
    [[500, 340, 860, 840]]

    >>> dpx, dpy, dcx, dcy, dt = m.getDeltas(GNAME) # Point deltas, component deltas, metrics deltas
    >>> dpx # [[dx1, dx1, dx1, dx1], [dx2, dx2, dx2, dx2], ...]
    [[70, -20.0, -40.0, -50.0], [270, 180.0, 560.0, 550.0], [270, 180.0, 560.0, 550.0], [70, -20.0, -40.0, -50.0]]
    >>> dt
    [[340, 160.0, 520.0, 500.0]]
    >>> sorted(m.ds.axes.keys())
    ['YTUC', 'wght']
    >>> sorted(m.supports()) # List with normalized master locations
    [{}, {'YTUC': (-1.0, -1.0, 0)}, {'wght': (-1.0, -1.0, 0)}, {'wght': (0, 1.0, 1.0)}]
    >>> m.getScalars(dict(wght=1, YTUC=0)) # Order: Regular, Light, Bold, Condensed
    [1.0, 0.0, 1.0, 0.0]
    >>> m.getScalars(dict(wght=0, YTUC=0))
    [1.0, 0.0, 0.0, 0.0]
    >>> m.getScalars(dict(wght=-1, YTUC=0))
    [1.0, 1.0, 0.0, 0.0]
    >>> m.getScalars(dict(wght=1, YTUC=-1))
    [1.0, 0.0, 1.0, 1.0]
    >>> m.getScalars(dict(wght=0, YTUC=-1))
    [1.0, 0.0, 0.0, 1.0]
    >>> m.getScalars(dict(wght=-1, YTUC=-1))
    [1.0, 1.0, 0.0, 1.0]
    >>> m.getScalars(dict(wght=0.8, YTUC=-0.3))
    [1.0, 0.0, 0.8, 0.3]
    >>> from fontParts.world import NewFont
    >>> fInt = NewFont()
    >>> gInt = fInt.newGlyph(GNAME)
    >>> loc = dict(wght=0, YTUC=500)
    >>> #points, components, metrics = m.interpolateValues(GNAME, loc)
    >>> #points
    #[(50.0, 800.0), (450.0, 800.0), (450.0, 0.0), (50.0, 0.0)]
    >>> #components
    #[]
    >>> #metrics
    #[500.0]
    >>> loc = dict(wght=-1, YTUC=0) # Location outside the boundaries of an axis answers min/max of the axis
    >>> #points, components, metrics = m.interpolateValues(GNAME, loc)
    >>> #points
    [(0.0, 400.0), (1000.0, 400.0), (1000.0, 0.0), (0.0, 0.0)]
    >>> #components
    #[]
    >>> #metrics
    #[1000.0]
    """

    def __init__(self, designSpace, masters, instances=None):
        """Create a VariableFont interpolation model. DesignSpace """
        assert masters, ValueError('%s No master fonts defined. The dictionay shouls master the design space.' % self)
        self.ds = designSpace
        # Property self.masterList creates font list in order of the design space.
        self.masters = masters # Can be an empty list, if the design space is new.

        self.vm = VariationModel(self.ds.normalizedLocations, axisOrder=self.ds.axisOrder)
        # Property self.instanceList creates font list in order of the design space.
        if instances is None:
            instances = {}
        self.instances = instances
        # Initialize cached property values.
        self.clear()
        # Set the self.paths (path->fontInfo) dictionary, so all fonts are enabled by default.
        # Set the path->fontInfo dictionary of fonts from the design space that are disable.
        # This allows the calling function to enable/disable fonts from interpolation.
        self.disabledMasterPaths = [] # Paths of masters not to be used in delta calculation.

    def __repr__(self):
        return '<PageBot %s %s axes:%d masters:%d>' % (self.__class__.__name__, self.ds.familyName, len(self.ds.axes), len(self.masters))

    def _get_masters(self):
        return self._masters
    def _set_masters(self, masters):
        self._masters = masters
        self._masterList = None
    masters = property(_get_masters, _set_masters)

    def _get_defaultMaster(self):
        """Answers the master font at default location. Answer None if it does
        not exist or cannot be found."""
        defaultMaster = None
        defaultMasterInfo = self.ds.defaultMaster
        if defaultMasterInfo is not None:
            defaultMaster = self.masters.get(defaultMasterInfo.path)
        return defaultMaster
    defaultMaster = property(_get_defaultMaster)

    def _get_masterList(self):
        """Dictionary of real master Font instances, path is key. Always start with the default,
        then omit the default if it else where in the list.
        """
        defaultMaster = self.defaultMaster # Get the ufo master for the default location.
        if self._masterList is None:
            self._masterList = []
            if defaultMaster is not None:
                self._masterList.append(defaultMaster) # Force to be first in the list.
            for masterPath in self.ds.masterPaths:
                if defaultMaster is not None and masterPath == defaultMaster.path:
                    continue
                if not masterPath in self.disabledMasterPaths:
                    self._masterList.append(self.masters[masterPath])
        return self._masterList
    masterList = property(_get_masterList)

    def getAxisMasters(self, axis):
        """Answers a tuple of two lists of all masters on the axis, on either
        size of the default master, and including the default master. If
        mininum == default or default == maximum, then that side if the axis
        only contains the default master."""
        # FIXME: minMasters, maxMasters unused.
        minMasters = []
        maxMasters = []

        for axisSide in self.ds.getAxisMasters(axis.tag):
            for masterInfo in axisSide:
                if masterInfo.path:
                    # FIXME: masters doesn't exist.
                    #masters.append(self.masters.get(masterInfo.path))
                    pass
        return minMasters, maxMasters

    def _get_instanceList(self):
        """Dictionary of real master Font instance, path is key."""
        if self._instanceList is None:
            self._instanceList = []
            for instancePath in self.ds.instancePaths:
                self._instanceList.append(self.instance[instancePath])
        return self._instanceList
    instanceList = property(_get_instanceList)

    def clear(self):
        """Clear the cached master values for the last interpolated glyph.
        This forces the values to be collected for the next glyph interpolation."""
        self._masterList = None
        self._instanceList = None
        self._masterValues = None
        self._defaultMaster = None

    # Rendering

    def getScalars(self, location):
        """Answers the list of scalars (multipliers 0..1) for each master in the
        given nLocation (normalized values -1..0..1): [1.0, 0.0, 0.8, 0.3] with
        respectively Regular, Light, Bold, Condensed as master ordering.
        """
        return self.vm.getScalars(location)

    def supports(self):
        return self.vm.supports

    def getDeltas(self, glyphName):

        pointXDeltas = []
        pointYDeltas = []
        componentXDeltas = []
        componentYDeltas = []
        metricsDeltas = []
        mvx, mvy, mvCx, mvCy, mt = self.getMasterValues(glyphName)

        for pIndex, _ in enumerate(mvx):
            pointXDeltas.append(self.vm.getDeltas(mvx[pIndex]))
            pointYDeltas.append(self.vm.getDeltas(mvy[pIndex]))

        for cIndex, _ in enumerate(mvCx):
            componentXDeltas.append(self.vm.getDeltas(mvCx[cIndex]))
            componentYDeltas.append(self.vm.getDeltas(mvCy[cIndex]))

        for mIndex, _ in enumerate(mt):
            metricsDeltas.append(self.vm.getDeltas(mt[mIndex]))

        return pointXDeltas, pointYDeltas, componentXDeltas, componentYDeltas, metricsDeltas

    def getMasterValues(self, glyphName):
        """Answers the (mvx, mvy, mvCx, mvCy), for point and component
        transformation, lists of corresponding master glyphs, in the same order
        as the self.masterList."""
        if self._masterValues is None:
            mvx = [] # X values of point transformations. Length is the amount of masters
            mvy = [] # Y values
            mvCx = [] # X values of components transformation
            mvCy = [] # Y values
            mMt = [[]] # Metrics values
            self._masterValues = mvx, mvy, mvCx, mvCy, mMt # Initialize result tuple

            for master in self.masterList:
                if not glyphName in master:
                    continue

                g = master[glyphName]
                points = getPoints(g) # Collect the master points

                for pIndex, point in enumerate(points):
                    if len(mvx) <= pIndex:
                        mvx.append([])
                        mvy.append([])
                    mvx[pIndex].append(point.x)
                    mvy[pIndex].append(point.y)
                components = getComponents(g) # Collect the master components
                """
                for cIndex, component in enumerate(components):
                    t = component.transformation
                    if len(mvCx) < cIndex:
                        mvCx.append([])
                        mvCy.append([])
                    mvCx[cIndex].append(t[-2])
                    mvCy[cIndex].append(t[-1])
                """
                mMt[0].append(g.width) # Other interpolating metrics can be added later.

        return self._masterValues

    def interpolateValues(self, glyphName, location):
        """Interpolate the glyph from masters and answer the list of (x,y)
        points tuples, component transformation and metrics.

        The axis location in world values is normalized to (-1..0..1)."""
        nLocation = normalizeLocation(location, self.ds.tripleAxes)
        interpolatedPoints = []
        interpolatedComponents = []
        interpolatedMetrics = []
        mvx, mvy, mvCx, mvCy, mMt = self.getMasterValues(glyphName)

        for pIndex, _ in enumerate(mvx):
            interpolatedPoints.append((
                self.vm.interpolateFromMasters(nLocation, mvx[pIndex]),
                self.vm.interpolateFromMasters(nLocation, mvy[pIndex])
            ))

        for cIndex, _ in enumerate(mvCx):
            interpolatedComponents.append((
                self.vm.interpolateFromMasters(nLocation, mvCx[cIndex]),
                self.vm.interpolateFromMasters(nLocation, mvCy[cIndex])
            ))

        for mIndex, _ in enumerate(mMt):
            interpolatedMetrics.append(
                self.vm.interpolateFromMasters(nLocation, mMt[mIndex])
            )
        return interpolatedPoints, interpolatedComponents, interpolatedMetrics

    def interpolateGlyph(self, glyph, location):
        """Interpolate the glyph from the masters. If glyph is not compatible
        with the masters, then first copy one of the masters into glyphs.
        Location is normalized to (-1..0..1)."""

        # If there are components, make sure to interpolate them first, then
        # interpolate (dx, dy). Check if the referenced glyph exists.
        # Otherwise copy it from one of the masters into the parent of glyph.
        mvx, mvy, mvCx, mvCy, mMt = self.getMasterValues(glyph.name)
        points = getPoints(glyph)
        components = getComponents(glyph)

        if components:
            font = glyph.getParent()
            for component in components:
                if component.baseGlyph in font:
                    self.interpolateGlyph(font[component.baseGlyph], location)
        if len(points) != len(mvx): # Glyph may not exist, or not compatible.
            # Clear the glyph and copy from the first master in the list, so it always interpolates.
            glyph.clear()
            masterGlyph = self.masterList[0][glyph.name]
            masterGlyph.draw(glyph.getPen())
            points = getPoints(glyph) # Get new set of points

        # Normalize to location (-1..0..1)
        nLocation = normalizeLocation(location, self.ds.tripleAxes)
        # Get the point of the glyph, and set their (x,y) to the calculated variable points.
        for pIndex, _ in enumerate(mvx):
            p = points[pIndex]
            p.x = self.vm.interpolateFromMasters(nLocation, mvx[pIndex])
            p.y = self.vm.interpolateFromMasters(nLocation, mvy[pIndex])

        for cIndex, _ in enumerate(mvCx):
            c = components[cIndex]
            t = list(c.transformation)
            t[-2] = self.vm.interpolateFromMasters(nLocation, mvCx[cIndex])
            t[-1] = self.vm.interpolateFromMasters(nLocation, mvCy[cIndex])
            c.transformation = t

        glyph.width = self.vm.interpolateFromMasters(nLocation, mMt[0])