Ejemplo n.º 1
0
def interpolate_layout(designspace_filename, loc, finder):

    masters, instances = designspace.load(designspace_filename)
    base_idx = None
    for i, m in enumerate(masters):
        if 'info' in m and m['info']['copy']:
            assert base_idx is None
            base_idx = i
    assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document."

    from pprint import pprint
    print("Index of base master:", base_idx)

    print("Building GX")
    print("Loading TTF masters")
    basedir = os.path.dirname(designspace_filename)
    master_ttfs = [
        finder(os.path.join(basedir, m['filename'])) for m in masters
    ]
    master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]

    #font = master_fonts[base_idx]
    font = TTFont(master_ttfs[base_idx])

    master_locs = [o['location'] for o in masters]

    axis_tags = set(master_locs[0].keys())
    assert all(axis_tags == set(m.keys()) for m in master_locs)

    # Set up axes
    axes = {}
    for tag in axis_tags:
        default = master_locs[base_idx][tag]
        lower = min(m[tag] for m in master_locs)
        upper = max(m[tag] for m in master_locs)
        axes[tag] = (lower, default, upper)
    print("Axes:")
    pprint(axes)

    print("Location:", loc)
    print("Master locations:")
    pprint(master_locs)

    # Normalize locations
    loc = models.normalizeLocation(loc, axes)
    master_locs = [models.normalizeLocation(m, axes) for m in master_locs]

    print("Normalized location:", loc)
    print("Normalized master locations:")
    pprint(master_locs)

    # Assume single-model for now.
    model = models.VariationModel(master_locs)
    assert 0 == model.mapping[base_idx]

    merger = InstancerMerger(model, loc)

    print("Building variations tables")
    merge_tables(font, merger, master_fonts, axes, base_idx, ['GPOS'])
    return font
Ejemplo n.º 2
0
def interpolate_layout(designspace_filename, loc, finder):

	masters, instances = designspace.load(designspace_filename)
	base_idx = None
	for i,m in enumerate(masters):
		if 'info' in m and m['info']['copy']:
			assert base_idx is None
			base_idx = i
	assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document."

	from pprint import pprint
	print("Index of base master:", base_idx)

	print("Building GX")
	print("Loading TTF masters")
	basedir = os.path.dirname(designspace_filename)
	master_ttfs = [finder(os.path.join(basedir, m['filename'])) for m in masters]
	master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]

	#font = master_fonts[base_idx]
	font = TTFont(master_ttfs[base_idx])

	master_locs = [o['location'] for o in masters]

	axis_tags = set(master_locs[0].keys())
	assert all(axis_tags == set(m.keys()) for m in master_locs)

	# Set up axes
	axes = {}
	for tag in axis_tags:
		default = master_locs[base_idx][tag]
		lower = min(m[tag] for m in master_locs)
		upper = max(m[tag] for m in master_locs)
		axes[tag] = (lower, default, upper)
	print("Axes:")
	pprint(axes)

	print("Location:", loc)
	print("Master locations:")
	pprint(master_locs)

	# Normalize locations
	loc = models.normalizeLocation(loc, axes)
	master_locs = [models.normalizeLocation(m, axes) for m in master_locs]

	print("Normalized location:", loc)
	print("Normalized master locations:")
	pprint(master_locs)

	# Assume single-model for now.
	model = models.VariationModel(master_locs)
	assert 0 == model.mapping[base_idx]

	merger = InstancerMerger(font, model, loc)

	print("Building variations tables")
	merge_tables(font, merger, master_fonts, axes, base_idx, ['GPOS'])
	return font
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
    def _get_normalizedLocations(self):
        """Answers the list of locations from all masters, normalized to the
        normalized axes. The list of masters includes all the ones known to
        the designspace, combining the origin (all default values), axis
        masters (all defaults except for one) and space masters (two or more
        location values are not default).

        >>> ds = DesignSpace()
        >>> a1 = ds.newAxis('wght')
        >>> a2 = ds.newAxis('XTRA') # Condensed primary axes, keeping stems equal, vary counters.
        >>> regular = ds.newMaster('MyFamily', 'Regular', location=ds.defaultLocation)
        >>> loc = ds.newLocation(wght=a1.maximum)
        >>> bold = ds.newMaster('MyFamily', 'Bold', location=loc)
        >>> loc = ds.newLocation(XTRA=a2.maximum)
        >>> cond = ds.newMaster('MyFamily', 'Condensed', location=loc)
        >>> loc = ds.newLocation(wght=a1.maximum, XTRA=a2.maximum)
        >>> condBold = ds.newMaster('MyFamily', 'Condensed Bold', location=loc)
        >>> ds.normalizedLocations
        [{'wght': 0.0, 'XTRA': 0.0}, {'wght': 1.0, 'XTRA': 0.0}, {'wght': 0.0, 'XTRA': 1.0}, {'wght': 1.0, 'XTRA': 1.0}]
        """
        normalizedLocations = []
        axes = self.tripleAxes
        for location in self.masterLocationList:
            if location is not None:
                nl = normalizeLocation(location, axes)
                normalizedLocations.append(nl)
            else:
                print('Cannot normalize location of "%s" with axes %s"' %
                      (location, axes))
        return normalizedLocations
Ejemplo n.º 5
0
def rcjkGlyphToVarCoGlyph(rcjkGlyph, glyph, renameTable,
                          componentSourceGlyphSet):
    copyMarkColor(rcjkGlyph, glyph)
    pen = glyph.getPointPen()
    rcjkGlyph.drawPoints(pen)
    compoVarInfo = []
    for compo in rcjkGlyph.components:
        transform = compo.transform
        x, y = transform["x"], transform["y"]
        pen.addComponent(renameTable.get(compo.name, compo.name),
                         (1, 0, 0, 1, x, y))
        # the transformation center goes into varco data
        varCoTransform = dict(
            # TODO: We could skip values that are default (0, or 1 for scale values)
            rotation=transform["rotation"],
            scalex=transform["scalex"],
            scaley=transform["scaley"],
            tcenterx=transform["tcenterx"],
            tcentery=transform["tcentery"],
        )
        if compo.name not in componentSourceGlyphSet:
            coord = {}
        else:
            baseGlyph = componentSourceGlyphSet.getGlyph(compo.name)
            axisNameMapping = _makeAxisNameMapping(baseGlyph.axes)
            coord = normalizeLocation(compo.coord, baseGlyph.axes)
            coord = {
                axisNameMapping[k]: v
                for k, v in coord.items() if k in axisNameMapping
            }
        info = dict(coord=coord, transform=varCoTransform)
        compoVarInfo.append(info)
    if compoVarInfo:
        glyph.lib["varco.components"] = compoVarInfo
Ejemplo n.º 6
0
 def _get_normalizedLocations(self):
     u"""Answer the list of locations from all masters, normalized to the normalized axes."""
     normalizedLocations = []
     axes = self.tripleAxes
     for location in self.locations:
         nl = normalizeLocation(location, axes)
         normalizedLocations.append(nl)
     return normalizedLocations
Ejemplo n.º 7
0
def main(args=None):

    if args is None:
        import sys
        args = sys.argv[1:]

    varfilename = args[0]
    locargs = args[1:]
    outfile = os.path.splitext(varfilename)[0] + '-instance.ttf'

    loc = {}
    for arg in locargs:
        tag, val = arg.split('=')
        assert len(tag) <= 4
        loc[tag.ljust(4)] = float(val)
    print("Location:", loc)

    print("Loading variable font")
    varfont = TTFont(varfilename)

    fvar = varfont['fvar']
    axes = {
        a.axisTag: (a.minValue, a.defaultValue, a.maxValue)
        for a in fvar.axes
    }
    # TODO Round to F2Dot14?
    loc = normalizeLocation(loc, axes)
    # Location is normalized now
    print("Normalized location:", loc)

    gvar = varfont['gvar']
    glyf = varfont['glyf']
    # get list of glyph names in gvar sorted by component depth
    glyphnames = sorted(
        gvar.variations.keys(),
        key=lambda name:
        (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
         if glyf[name].isComposite() else 0, name))
    for glyphname in glyphnames:
        variations = gvar.variations[glyphname]
        coordinates, _ = _GetCoordinates(varfont, glyphname)
        for var in variations:
            scalar = supportScalar(loc, var.axes)
            if not scalar: continue
            # TODO Do IUP / handle None items
            coordinates += GlyphCoordinates(var.coordinates) * scalar
        _SetCoordinates(varfont, glyphname, coordinates)

    print("Removing variable tables")
    for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR',
                'STAT'):
        if tag in varfont:
            del varfont[tag]

    print("Saving instance font", outfile)
    varfont.save(outfile)
Ejemplo n.º 8
0
    def normalize(self, location):
        """Return a normalized location based on the designspace.

        Args:
            location:  A dictionary mapping axis names to user space coordinates.

        Example::

            >>> location = { "Width": 75, "Weight": 170 }
            >>> f.normalize(location)
            {'Weight': 0.8947368421052632, 'Width': -0.35714285714285715}
        """

        return models.normalizeLocation(location, self.internal_axis_supports)
Ejemplo n.º 9
0
def interpolate_layout(designspace_filename,
                       loc,
                       master_finder=lambda s: s,
                       mapped=False):
    """
	Interpolate GPOS from a designspace file and location.

	If master_finder is set, it should be a callable that takes master
	filename as found in designspace file and map it to master font
	binary as to be opened (eg. .ttf or .otf).

	If mapped is False (default), then location is mapped using the
	map element of the axes in designspace file.  If mapped is True,
	it is assumed that location is in designspace's internal space and
	no mapping is performed.
	"""

    axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances = load_designspace(
        designspace_filename)

    log.info("Building interpolated font")
    log.info("Loading master fonts")
    basedir = os.path.dirname(designspace_filename)
    master_ttfs = [
        master_finder(os.path.join(basedir, m['filename'])) for m in masters
    ]
    master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]

    #font = master_fonts[base_idx]
    font = TTFont(master_ttfs[base_idx])

    log.info("Location: %s", pformat(loc))
    if not mapped:
        loc = {name: axes[name].map_forward(v) for name, v in loc.items()}
    log.info("Internal location: %s", pformat(loc))
    loc = models.normalizeLocation(loc, internal_axis_supports)
    log.info("Normalized location: %s", pformat(loc))

    # Assume single-model for now.
    model = models.VariationModel(normalized_master_locs)
    assert 0 == model.mapping[base_idx]

    merger = InstancerMerger(font, model, loc)

    log.info("Building interpolated tables")
    merger.mergeTables(font, master_fonts, ['GPOS'])
    return font
def interpolate_layout(designspace,
                       loc,
                       master_finder=lambda s: s,
                       mapped=False):
    """
	Interpolate GPOS from a designspace file and location.

	If master_finder is set, it should be a callable that takes master
	filename as found in designspace file and map it to master font
	binary as to be opened (eg. .ttf or .otf).

	If mapped is False (default), then location is mapped using the
	map element of the axes in designspace file.  If mapped is True,
	it is assumed that location is in designspace's internal space and
	no mapping is performed.
	"""
    if hasattr(designspace, "sources"):  # Assume a DesignspaceDocument
        pass
    else:  # Assume a file path
        from fontTools.designspaceLib import DesignSpaceDocument
        designspace = DesignSpaceDocument.fromfile(designspace)

    ds = load_designspace(designspace)
    log.info("Building interpolated font")

    log.info("Loading master fonts")
    master_fonts = load_masters(designspace, master_finder)
    font = deepcopy(master_fonts[ds.base_idx])

    log.info("Location: %s", pformat(loc))
    if not mapped:
        loc = {name: ds.axes[name].map_forward(v) for name, v in loc.items()}
    log.info("Internal location: %s", pformat(loc))
    loc = models.normalizeLocation(loc, ds.internal_axis_supports)
    log.info("Normalized location: %s", pformat(loc))

    # Assume single-model for now.
    model = models.VariationModel(ds.normalized_master_locs)
    assert 0 == model.mapping[ds.base_idx]

    merger = InstancerMerger(font, model, loc)

    log.info("Building interpolated tables")
    # TODO GSUB/GDEF
    merger.mergeTables(font, master_fonts, ['GPOS'])
    return font
Ejemplo n.º 11
0
    def _make_model(self):
        masters = self.designspace.sources
        internal_master_locs = [o.location for o in masters]
        self.internal_axis_supports = {}
        for axis in self.designspace.axes:
            triple = (axis.minimum, axis.default, axis.maximum)
            self.internal_axis_supports[axis.name] = [
                axis.map_forward(v) for v in triple
            ]

        normalized_master_locs = [
            models.normalizeLocation(m, self.internal_axis_supports)
            for m in internal_master_locs
        ]
        axis_tags = [axis.name for axis in self.designspace.axes]
        self.variation_model = models.VariationModel(normalized_master_locs,
                                                     axisOrder=axis_tags)
Ejemplo n.º 12
0
def addRCJKGlyphToVarCoUFO(
    ufo,
    rcjkGlyphSet,
    srcGlyphName,
    dstGlyphName,
    unicodes,
    renameTable,
    componentSourceGlyphSet,
    globalAxisNames,
):
    if renameTable is None:
        renameTable = {}
    rcjkGlyph = rcjkGlyphSet.getGlyph(srcGlyphName)
    if rcjkGlyph.components and not rcjkGlyph.outline.isEmpty():
        logger.warning(
            f"glyph {srcGlyphName} has both outlines and components")

    glyph = UGlyph(dstGlyphName)
    glyph.unicodes = unicodes
    glyph.width = max(0, rcjkGlyph.width)  # width can't be negative
    rcjkGlyphToVarCoGlyph(rcjkGlyph, glyph, renameTable,
                          componentSourceGlyphSet)

    if globalAxisNames is None:
        axisNameMapping = _makeAxisNameMapping(rcjkGlyph.axes)
        axisNames = set(axisNameMapping.values())
    else:
        axisNames = globalAxisNames

    for varIndex, rcjkVarGlyph in enumerate(rcjkGlyph.variations):
        location = rcjkVarGlyph.location
        location = normalizeLocation(location, rcjkGlyph.axes)
        if globalAxisNames is None:
            location = {axisNameMapping[k]: v for k, v in location.items()}
        sparseLocation = {k: v for k, v in location.items() if v != 0}
        layerName = layerNameFromLocation(sparseLocation, axisNames)
        assert layerName, (srcGlyphName, varIndex, location, rcjkGlyph.axes)
        layer = getUFOLayer(ufo, layerName)
        varGlyph = UGlyph(dstGlyphName)
        varGlyph.width = max(0, rcjkVarGlyph.width)  # width can't be negative
        rcjkGlyphToVarCoGlyph(rcjkVarGlyph, varGlyph, renameTable,
                              componentSourceGlyphSet)
        layer[dstGlyphName] = varGlyph

    ufo[dstGlyphName] = glyph
Ejemplo n.º 13
0
def main(args=None):

	if args is None:
		import sys
		args = sys.argv[1:]

	varfilename = args[0]
	locargs = args[1:]
	outfile = os.path.splitext(varfilename)[0] + '-instance.ttf'

	loc = {}
	for arg in locargs:
		tag,val = arg.split('=')
		assert len(tag) <= 4
		loc[tag.ljust(4)] = float(val)
	print("Location:", loc)

	print("Loading GX font")
	varfont = TTFont(varfilename)

	fvar = varfont['fvar']
	axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes}
	# TODO Round to F2Dot14?
	loc = normalizeLocation(loc, axes)
	# Location is normalized now
	print("Normalized location:", loc)

	gvar = varfont['gvar']
	for glyphname,variations in gvar.variations.items():
		coordinates,_ = _GetCoordinates(varfont, glyphname)
		for var in variations:
			scalar = supportScalar(loc, var.axes)
			if not scalar: continue
			# TODO Do IUP / handle None items
			coordinates += GlyphCoordinates(var.coordinates) * scalar
		_SetCoordinates(varfont, glyphname, coordinates)

	print("Removing GX tables")
	for tag in ('fvar','avar','gvar'):
		if tag in varfont:
			del varfont[tag]

	print("Saving instance font", outfile)
	varfont.save(outfile)
Ejemplo n.º 14
0
	def __init__(self, font, location, normalized=False):
		from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap
		self._ttFont = font
		if not normalized:
			axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in font['fvar'].axes}
			location = normalizeLocation(location, axes)
			if 'avar' in font:
				avar = font['avar']
				avarSegments = avar.segments
				new_location = {}
				for axis_tag,value in location.items():
					avarMapping = avarSegments.get(axis_tag, None)
					if avarMapping is not None:
						value = piecewiseLinearMap(value, avarMapping)
					new_location[axis_tag] = value
				location = new_location
				del new_location

		self.location = location
Ejemplo n.º 15
0
def interpolate_layout(designspace_filename, loc, master_finder=lambda s:s, mapped=False):
	"""
	Interpolate GPOS from a designspace file and location.

	If master_finder is set, it should be a callable that takes master
	filename as found in designspace file and map it to master font
	binary as to be opened (eg. .ttf or .otf).

	If mapped is False (default), then location is mapped using the
	map element of the axes in designspace file.  If mapped is True,
	it is assumed that location is in designspace's internal space and
	no mapping is performed.
	"""
	ds = load_designspace(designspace_filename)

	log.info("Building interpolated font")
	log.info("Loading master fonts")
	basedir = os.path.dirname(designspace_filename)
	master_ttfs = [
		master_finder(os.path.join(basedir, m.filename)) for m in ds.masters
	]
	master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]

	#font = master_fonts[ds.base_idx]
	font = TTFont(master_ttfs[ds.base_idx])

	log.info("Location: %s", pformat(loc))
	if not mapped:
		loc = {name: ds.axes[name].map_forward(v) for name,v in loc.items()}
	log.info("Internal location: %s", pformat(loc))
	loc = models.normalizeLocation(loc, ds.internal_axis_supports)
	log.info("Normalized location: %s", pformat(loc))

	# Assume single-model for now.
	model = models.VariationModel(ds.normalized_master_locs)
	assert 0 == model.mapping[ds.base_idx]

	merger = InstancerMerger(font, model, loc)

	log.info("Building interpolated tables")
	# TODO GSUB/GDEF
	merger.mergeTables(font, master_fonts, ['GPOS'])
	return font
Ejemplo n.º 16
0
    def _makeWarpFromList(self, axisName, mapData):
        # check for the extremes, add if necessary
        minimum, default, maximum = self.axes[axisName]
        if not sum([a == minimum for a, b in mapData]):
            mapData = [(minimum, minimum)] + mapData
        if not sum([a == maximum for a, b in mapData]):
            mapData.append((maximum, maximum))
        if not (default, default) in mapData:
            mapData.append((default, default))

        mapLocations = []
        mapValues = []
        for x, y in mapData:
            l = normalizeLocation(dict(w=x),
                                  dict(w=[minimum, default, maximum]))
            mapLocations.append(l)
            mapValues.append(y)
        self.models[axisName] = VariationModel(mapLocations, axisOrder=['w'])
        self.values[axisName] = mapValues
Ejemplo n.º 17
0
    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])
Ejemplo n.º 18
0
def unpackDesignSpace(doc):
    axisTagMapping = {axis.name: axis.tag for axis in doc.axes}
    axes = {axis.tag: (axis.minimum, axis.default, axis.maximum) for axis in doc.axes}

    # We want the default source to be the first in the list; the rest of
    # the order is not important
    sources = sorted(doc.sources, key=lambda src: src != doc.default)

    ufos = []
    locations = []

    _loaded = {}

    for src in sources:
        loc = src.location
        loc = {
            axisTagMapping[axisName]: axisValue for axisName, axisValue in loc.items()
        }
        loc = normalizeLocation(loc, axes)
        loc = {
            axisName: axisValue for axisName, axisValue in loc.items() if axisValue != 0
        }
        locations.append(loc)
        ufo = _loaded.get(src.path)
        if ufo is None:
            ufo = UFont(src.path)
            _loaded[src.path] = ufo
        if src.layerName is None:
            ufo.layers.defaultLayer
        else:
            ufo = ufo.layers[src.layerName]
        ufos.append(ufo)

    userAxes = {
        axis.tag: (axis.minimum, axis.default, axis.maximum)
        for axis in doc.axes
        if not axis.hidden
    }
    return userAxes, ufos, locations
Ejemplo n.º 19
0
    def drawGlyph(self, pen, glyphName, location):
        normLocation = normalizeLocation(location, self.axes)
        fvarTable = self.ttFont["fvar"]
        glyfTable = self.ttFont["glyf"]
        varcTable = self.ttFont.get("VarC")
        if varcTable is not None:
            glyphData = varcTable.GlyphData
        else:
            glyphData = {}

        g = glyfTable[glyphName]
        varComponents = glyphData.get(glyphName)
        if g.isComposite():
            componentOffsets = instantiateComponentOffsets(
                self.ttFont, glyphName, normLocation
            )
            if varComponents is not None:
                assert len(g.components) == len(varComponents)
                varcInstancer = VarStoreInstancer(
                    varcTable.VarStore, fvarTable.axes, normLocation
                )
                for (x, y), gc, vc in zip(
                    componentOffsets, g.components, varComponents
                ):
                    componentLocation = unpackComponentLocation(vc.coord, varcInstancer)
                    transform = unpackComponentTransform(
                        vc.transform, varcInstancer, vc.numIntBitsForScale
                    )
                    tPen = TransformPen(pen, _makeTransform(x, y, transform))
                    self.drawGlyph(tPen, gc.glyphName, componentLocation)
            else:
                for (x, y), gc in zip(componentOffsets, g.components):
                    tPen = TransformPen(pen, (1, 0, 0, 1, x, y))
                    self.drawGlyph(tPen, gc.glyphName, {})
        else:
            glyphID = self.ttFont.getGlyphID(glyphName)
            self.hbFont.set_variations(location)
            self.hbFont.draw_glyph_with_pen(glyphID, pen)
Ejemplo n.º 20
0
def load_designspace(designspace_filename):

	ds = designspace.load(designspace_filename)
	axes = ds.get('axes')
	masters = ds.get('sources')
	if not masters:
		raise VarLibError("no sources found in .designspace")
	instances = ds.get('instances', [])

	standard_axis_map = OrderedDict([
		('weight',  ('wght', {'en':'Weight'})),
		('width',   ('wdth', {'en':'Width'})),
		('slant',   ('slnt', {'en':'Slant'})),
		('optical', ('opsz', {'en':'Optical Size'})),
		])


	# Setup axes
	class DesignspaceAxis(object):

		def __repr__(self):
			return repr(self.__dict__)

		@staticmethod
		def _map(v, map):
			keys = map.keys()
			if not keys:
				return v
			if v in keys:
				return map[v]
			k = min(keys)
			if v < k:
				return v + map[k] - k
			k = max(keys)
			if v > k:
				return v + map[k] - k
			# Interpolate
			a = max(k for k in keys if k < v)
			b = min(k for k in keys if k > v)
			va = map[a]
			vb = map[b]
			return va + (vb - va) * (v - a) / (b - a)

		def map_forward(self, v):
			if self.map is None: return v
			return self._map(v, self.map)

		def map_backward(self, v):
			if self.map is None: return v
			map = {v:k for k,v in self.map.items()}
			return self._map(v, map)

	axis_objects = OrderedDict()
	if axes is not None:
		for axis_dict in axes:
			axis_name = axis_dict.get('name')
			if not axis_name:
				axis_name = axis_dict['name'] = axis_dict['tag']
			if 'map' not in axis_dict:
				axis_dict['map'] = None
			else:
				axis_dict['map'] = {m['input']:m['output'] for m in axis_dict['map']}

			if axis_name in standard_axis_map:
				if 'tag' not in axis_dict:
					axis_dict['tag'] = standard_axis_map[axis_name][0]
				if 'labelname' not in axis_dict:
					axis_dict['labelname'] = standard_axis_map[axis_name][1].copy()

			axis = DesignspaceAxis()
			for item in ['name', 'tag', 'labelname', 'minimum', 'default', 'maximum', 'map']:
				assert item in axis_dict, 'Axis does not have "%s"' % item
			axis.__dict__ = axis_dict
			axis_objects[axis_name] = axis
	else:
		# No <axes> element. Guess things...
		base_idx = None
		for i,m in enumerate(masters):
			if 'info' in m and m['info']['copy']:
				assert base_idx is None
				base_idx = i
		assert base_idx is not None, "Cannot find 'base' master; Either add <axes> element to .designspace document, or add <info> element to one of the sources in the .designspace document."

		master_locs = [o['location'] for o in masters]
		base_loc = master_locs[base_idx]
		axis_names = set(base_loc.keys())
		assert all(name in standard_axis_map for name in axis_names), "Non-standard axis found and there exist no <axes> element."

		for name,(tag,labelname) in standard_axis_map.items():
			if name not in axis_names:
				continue

			axis = DesignspaceAxis()
			axis.name = name
			axis.tag = tag
			axis.labelname = labelname.copy()
			axis.default = base_loc[name]
			axis.minimum = min(m[name] for m in master_locs if name in m)
			axis.maximum = max(m[name] for m in master_locs if name in m)
			axis.map = None
			# TODO Fill in weight / width mapping from OS/2 table? Need loading fonts...
			axis_objects[name] = axis
		del base_idx, base_loc, axis_names, master_locs
	axes = axis_objects
	del axis_objects
	log.info("Axes:\n%s", pformat(axes))


	# Check all master and instance locations are valid and fill in defaults
	for obj in masters+instances:
		obj_name = obj.get('name', obj.get('stylename', ''))
		loc = obj['location']
		for axis_name in loc.keys():
			assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name)
		for axis_name,axis in axes.items():
			if axis_name not in loc:
				loc[axis_name] = axis.default
			else:
				v = axis.map_backward(loc[axis_name])
				assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum)


	# Normalize master locations

	normalized_master_locs = [o['location'] for o in masters]
	log.info("Internal master locations:\n%s", pformat(normalized_master_locs))

	# TODO This mapping should ideally be moved closer to logic in _add_fvar_avar
	internal_axis_supports = {}
	for axis in axes.values():
		triple = (axis.minimum, axis.default, axis.maximum)
		internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple]
	log.info("Internal axis supports:\n%s", pformat(internal_axis_supports))

	normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in normalized_master_locs]
	log.info("Normalized master locations:\n%s", pformat(normalized_master_locs))


	# Find base master
	base_idx = None
	for i,m in enumerate(normalized_master_locs):
		if all(v == 0 for v in m.values()):
			assert base_idx is None
			base_idx = i
	assert base_idx is not None, "Base master not found; no master at default location?"
	log.info("Index of base master: %s", base_idx)

	return axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances
Ejemplo n.º 21
0
def load_designspace(designspace_filename):

	ds = DesignSpaceDocument.fromfile(designspace_filename)
	masters = ds.sources
	if not masters:
		raise VarLibError("no sources found in .designspace")
	instances = ds.instances

	standard_axis_map = OrderedDict([
		('weight',  ('wght', {'en':'Weight'})),
		('width',   ('wdth', {'en':'Width'})),
		('slant',   ('slnt', {'en':'Slant'})),
		('optical', ('opsz', {'en':'Optical Size'})),
		])

	# Setup axes
	axes = OrderedDict()
	for axis in ds.axes:
		axis_name = axis.name
		if not axis_name:
			assert axis.tag is not None
			axis_name = axis.name = axis.tag

		if axis_name in standard_axis_map:
			if axis.tag is None:
				axis.tag = standard_axis_map[axis_name][0]
			if not axis.labelNames:
				axis.labelNames.update(standard_axis_map[axis_name][1])
		else:
			assert axis.tag is not None
			if not axis.labelNames:
				axis.labelNames["en"] = axis_name

		axes[axis_name] = axis
	log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()]))

	# Check all master and instance locations are valid and fill in defaults
	for obj in masters+instances:
		obj_name = obj.name or obj.styleName or ''
		loc = obj.location
		for axis_name in loc.keys():
			assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name)
		for axis_name,axis in axes.items():
			if axis_name not in loc:
				loc[axis_name] = axis.default
			else:
				v = axis.map_backward(loc[axis_name])
				assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum)

	# Normalize master locations

	internal_master_locs = [o.location for o in masters]
	log.info("Internal master locations:\n%s", pformat(internal_master_locs))

	# TODO This mapping should ideally be moved closer to logic in _add_fvar/avar
	internal_axis_supports = {}
	for axis in axes.values():
		triple = (axis.minimum, axis.default, axis.maximum)
		internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple]
	log.info("Internal axis supports:\n%s", pformat(internal_axis_supports))

	normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs]
	log.info("Normalized master locations:\n%s", pformat(normalized_master_locs))

	# Find base master
	base_idx = None
	for i,m in enumerate(normalized_master_locs):
		if all(v == 0 for v in m.values()):
			assert base_idx is None
			base_idx = i
	assert base_idx is not None, "Base master not found; no master at default location?"
	log.info("Index of base master: %s", base_idx)

	return _DesignSpaceData(
		axes,
		internal_axis_supports,
		base_idx,
		normalized_master_locs,
		masters,
		instances,
		ds.rules,
	)
Ejemplo n.º 22
0
	def normalize(name, value):
		return models.normalizeLocation(
			{name: value}, internal_axis_supports
		)[name]
Ejemplo n.º 23
0
def instantiateVariableFont(varfont, location, inplace=False):
	""" Generate a static instance from a variable TTFont and a dictionary
	defining the desired location along the variable font's axes.
	The location values must be specified as user-space coordinates, e.g.:

		{'wght': 400, 'wdth': 100}

	By default, a new TTFont object is returned. If ``inplace`` is True, the
	input varfont is modified and reduced to a static font.
	"""
	if not inplace:
		# make a copy to leave input varfont unmodified
		stream = BytesIO()
		varfont.save(stream)
		stream.seek(0)
		varfont = TTFont(stream)

	fvar = varfont['fvar']
	axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes}
	loc = normalizeLocation(location, axes)
	if 'avar' in varfont:
		maps = varfont['avar'].segments
		loc = {k: piecewiseLinearMap(v, maps[k]) for k,v in loc.items()}
	# Quantize to F2Dot14, to avoid surprise interpolations.
	loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()}
	# Location is normalized now
	log.info("Normalized location: %s", loc)

	if 'gvar' in varfont:
		log.info("Mutating glyf/gvar tables")
		gvar = varfont['gvar']
		glyf = varfont['glyf']
		# get list of glyph names in gvar sorted by component depth
		glyphnames = sorted(
			gvar.variations.keys(),
			key=lambda name: (
				glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
				if glyf[name].isComposite() else 0,
				name))
		for glyphname in glyphnames:
			variations = gvar.variations[glyphname]
			coordinates,_ = _GetCoordinates(varfont, glyphname)
			origCoords, endPts = None, None
			for var in variations:
				scalar = supportScalar(loc, var.axes)
				if not scalar: continue
				delta = var.coordinates
				if None in delta:
					if origCoords is None:
						origCoords,control = _GetCoordinates(varfont, glyphname)
						endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
					delta = iup_delta(delta, origCoords, endPts)
				coordinates += GlyphCoordinates(delta) * scalar
			_SetCoordinates(varfont, glyphname, coordinates)
	else:
		glyf = None

	if 'cvar' in varfont:
		log.info("Mutating cvt/cvar tables")
		cvar = varfont['cvar']
		cvt = varfont['cvt ']
		deltas = {}
		for var in cvar.variations:
			scalar = supportScalar(loc, var.axes)
			if not scalar: continue
			for i, c in enumerate(var.coordinates):
				if c is not None:
					deltas[i] = deltas.get(i, 0) + scalar * c
		for i, delta in deltas.items():
			cvt[i] += otRound(delta)

	if 'CFF2' in varfont:
		log.info("Mutating CFF2 table")
		glyphOrder = varfont.getGlyphOrder()
		CFF2 = varfont['CFF2']
		topDict = CFF2.cff.topDictIndex[0]
		vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc)
		interpolateFromDeltas = vsInstancer.interpolateFromDeltas
		interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas)
		CFF2.desubroutinize()
		interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder)
		interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc)
		del topDict.rawDict['VarStore']
		del topDict.VarStore

	if 'MVAR' in varfont:
		log.info("Mutating MVAR table")
		mvar = varfont['MVAR'].table
		varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc)
		records = mvar.ValueRecord
		for rec in records:
			mvarTag = rec.ValueTag
			if mvarTag not in MVAR_ENTRIES:
				continue
			tableTag, itemName = MVAR_ENTRIES[mvarTag]
			delta = otRound(varStoreInstancer[rec.VarIdx])
			if not delta:
				continue
			setattr(varfont[tableTag], itemName,
				getattr(varfont[tableTag], itemName) + delta)

	log.info("Mutating FeatureVariations")
	for tableTag in 'GSUB','GPOS':
		if not tableTag in varfont:
			continue
		table = varfont[tableTag].table
		if not hasattr(table, 'FeatureVariations'):
			continue
		variations = table.FeatureVariations
		for record in variations.FeatureVariationRecord:
			applies = True
			for condition in record.ConditionSet.ConditionTable:
				if condition.Format == 1:
					axisIdx = condition.AxisIndex
					axisTag = fvar.axes[axisIdx].axisTag
					Min = condition.FilterRangeMinValue
					Max = condition.FilterRangeMaxValue
					v = loc[axisTag]
					if not (Min <= v <= Max):
						applies = False
				else:
					applies = False
				if not applies:
					break

			if applies:
				assert record.FeatureTableSubstitution.Version == 0x00010000
				for rec in record.FeatureTableSubstitution.SubstitutionRecord:
					table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature
				break
		del table.FeatureVariations

	if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003:
		log.info("Mutating GDEF/GPOS/GSUB tables")
		gdef = varfont['GDEF'].table
		instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc)

		merger = MutatorMerger(varfont, loc)
		merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS'])

		# Downgrade GDEF.
		del gdef.VarStore
		gdef.Version = 0x00010002
		if gdef.MarkGlyphSetsDef is None:
			del gdef.MarkGlyphSetsDef
			gdef.Version = 0x00010000

		if not (gdef.LigCaretList or
			gdef.MarkAttachClassDef or
			gdef.GlyphClassDef or
			gdef.AttachList or
			(gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)):
			del varfont['GDEF']

	addidef = False
	if glyf:
		for glyph in glyf.glyphs.values():
			if hasattr(glyph, "program"):
				instructions = glyph.program.getAssembly()
				# If GETVARIATION opcode is used in bytecode of any glyph add IDEF
				addidef = any(op.startswith("GETVARIATION") for op in instructions)
				if addidef:
					break
	if addidef:
		log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
		asm = []
		if 'fpgm' in varfont:
			fpgm = varfont['fpgm']
			asm = fpgm.program.getAssembly()
		else:
			fpgm = newTable('fpgm')
			fpgm.program = ttProgram.Program()
			varfont['fpgm'] = fpgm
		asm.append("PUSHB[000] 145")
		asm.append("IDEF[ ]")
		args = [str(len(loc))]
		for a in fvar.axes:
			args.append(str(floatToFixed(loc[a.axisTag], 14)))
		asm.append("NPUSHW[ ] " + ' '.join(args))
		asm.append("ENDF[ ]")
		fpgm.program.fromAssembly(asm)

		# Change maxp attributes as IDEF is added
		if 'maxp' in varfont:
			maxp = varfont['maxp']
			if hasattr(maxp, "maxInstructionDefs"):
				maxp.maxInstructionDefs += 1
			else:
				setattr(maxp, "maxInstructionDefs", 1)
			if hasattr(maxp, "maxStackElements"):
				maxp.maxStackElements = max(len(loc), maxp.maxStackElements)
			else:
				setattr(maxp, "maxInstructionDefs", len(loc))

	if 'name' in varfont:
		log.info("Pruning name table")
		exclude = {a.axisNameID for a in fvar.axes}
		for i in fvar.instances:
			exclude.add(i.subfamilyNameID)
			exclude.add(i.postscriptNameID)
		if 'ltag' in varfont:
			# Drop the whole 'ltag' table if all its language tags are referenced by
			# name records to be pruned.
			# TODO: prune unused ltag tags and re-enumerate langIDs accordingly
			excludedUnicodeLangIDs = [
				n.langID for n in varfont['name'].names
				if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF
			]
			if set(excludedUnicodeLangIDs) == set(range(len((varfont['ltag'].tags)))):
				del varfont['ltag']
		varfont['name'].names[:] = [
			n for n in varfont['name'].names
			if n.nameID not in exclude
		]

	if "wght" in location and "OS/2" in varfont:
		varfont["OS/2"].usWeightClass = otRound(
			max(1, min(location["wght"], 1000))
		)
	if "wdth" in location:
		wdth = location["wdth"]
		for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()):
			if wdth < percent:
				varfont["OS/2"].usWidthClass = widthClass
				break
		else:
			varfont["OS/2"].usWidthClass = 9
	if "slnt" in location and "post" in varfont:
		varfont["post"].italicAngle = max(-90, min(location["slnt"], 90))

	log.info("Removing variable tables")
	for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'):
		if tag in varfont:
			del varfont[tag]

	return varfont
Ejemplo n.º 24
0
def instantiateVariableFont(varfont, location, inplace=False):
	""" Generate a static instance from a variable TTFont and a dictionary
	defining the desired location along the variable font's axes.
	The location values must be specified as user-space coordinates, e.g.:

		{'wght': 400, 'wdth': 100}

	By default, a new TTFont object is returned. If ``inplace`` is True, the
	input varfont is modified and reduced to a static font.
	"""
	if not inplace:
		# make a copy to leave input varfont unmodified
		stream = BytesIO()
		varfont.save(stream)
		stream.seek(0)
		varfont = TTFont(stream)

	fvar = varfont['fvar']
	axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes}
	loc = normalizeLocation(location, axes)
	if 'avar' in varfont:
		maps = varfont['avar'].segments
		loc = {k: piecewiseLinearMap(v, maps[k]) for k,v in loc.items()}
	# Quantize to F2Dot14, to avoid surprise interpolations.
	loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()}
	# Location is normalized now
	log.info("Normalized location: %s", loc)

	if 'gvar' in varfont:
		log.info("Mutating glyf/gvar tables")
		gvar = varfont['gvar']
		glyf = varfont['glyf']
		# get list of glyph names in gvar sorted by component depth
		glyphnames = sorted(
			gvar.variations.keys(),
			key=lambda name: (
				glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
				if glyf[name].isComposite() else 0,
				name))
		for glyphname in glyphnames:
			variations = gvar.variations[glyphname]
			coordinates,_ = _GetCoordinates(varfont, glyphname)
			origCoords, endPts = None, None
			for var in variations:
				scalar = supportScalar(loc, var.axes)
				if not scalar: continue
				delta = var.coordinates
				if None in delta:
					if origCoords is None:
						origCoords,control = _GetCoordinates(varfont, glyphname)
						endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
					delta = iup_delta(delta, origCoords, endPts)
				coordinates += GlyphCoordinates(delta) * scalar
			_SetCoordinates(varfont, glyphname, coordinates)
	else:
		glyf = None

	if 'cvar' in varfont:
		log.info("Mutating cvt/cvar tables")
		cvar = varfont['cvar']
		cvt = varfont['cvt ']
		deltas = {}
		for var in cvar.variations:
			scalar = supportScalar(loc, var.axes)
			if not scalar: continue
			for i, c in enumerate(var.coordinates):
				if c is not None:
					deltas[i] = deltas.get(i, 0) + scalar * c
		for i, delta in deltas.items():
			cvt[i] += otRound(delta)

	if 'CFF2' in varfont:
		log.info("Mutating CFF2 table")
		glyphOrder = varfont.getGlyphOrder()
		CFF2 = varfont['CFF2']
		topDict = CFF2.cff.topDictIndex[0]
		vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc)
		interpolateFromDeltas = vsInstancer.interpolateFromDeltas
		interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas)
		CFF2.desubroutinize(varfont)
		interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder)
		interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc)
		del topDict.rawDict['VarStore']
		del topDict.VarStore

	if 'MVAR' in varfont:
		log.info("Mutating MVAR table")
		mvar = varfont['MVAR'].table
		varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc)
		records = mvar.ValueRecord
		for rec in records:
			mvarTag = rec.ValueTag
			if mvarTag not in MVAR_ENTRIES:
				continue
			tableTag, itemName = MVAR_ENTRIES[mvarTag]
			delta = otRound(varStoreInstancer[rec.VarIdx])
			if not delta:
				continue
			setattr(varfont[tableTag], itemName,
				getattr(varfont[tableTag], itemName) + delta)

	log.info("Mutating FeatureVariations")
	for tableTag in 'GSUB','GPOS':
		if not tableTag in varfont:
			continue
		table = varfont[tableTag].table
		if not hasattr(table, 'FeatureVariations'):
			continue
		variations = table.FeatureVariations
		for record in variations.FeatureVariationRecord:
			applies = True
			for condition in record.ConditionSet.ConditionTable:
				if condition.Format == 1:
					axisIdx = condition.AxisIndex
					axisTag = fvar.axes[axisIdx].axisTag
					Min = condition.FilterRangeMinValue
					Max = condition.FilterRangeMaxValue
					v = loc[axisTag]
					if not (Min <= v <= Max):
						applies = False
				else:
					applies = False
				if not applies:
					break

			if applies:
				assert record.FeatureTableSubstitution.Version == 0x00010000
				for rec in record.FeatureTableSubstitution.SubstitutionRecord:
					table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature
				break
		del table.FeatureVariations

	if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003:
		log.info("Mutating GDEF/GPOS/GSUB tables")
		gdef = varfont['GDEF'].table
		instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc)

		merger = MutatorMerger(varfont, loc)
		merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS'])

		# Downgrade GDEF.
		del gdef.VarStore
		gdef.Version = 0x00010002
		if gdef.MarkGlyphSetsDef is None:
			del gdef.MarkGlyphSetsDef
			gdef.Version = 0x00010000

		if not (gdef.LigCaretList or
			gdef.MarkAttachClassDef or
			gdef.GlyphClassDef or
			gdef.AttachList or
			(gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)):
			del varfont['GDEF']

	addidef = False
	if glyf:
		for glyph in glyf.glyphs.values():
			if hasattr(glyph, "program"):
				instructions = glyph.program.getAssembly()
				# If GETVARIATION opcode is used in bytecode of any glyph add IDEF
				addidef = any(op.startswith("GETVARIATION") for op in instructions)
				if addidef:
					break
	if addidef:
		log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
		asm = []
		if 'fpgm' in varfont:
			fpgm = varfont['fpgm']
			asm = fpgm.program.getAssembly()
		else:
			fpgm = newTable('fpgm')
			fpgm.program = ttProgram.Program()
			varfont['fpgm'] = fpgm
		asm.append("PUSHB[000] 145")
		asm.append("IDEF[ ]")
		args = [str(len(loc))]
		for a in fvar.axes:
			args.append(str(floatToFixed(loc[a.axisTag], 14)))
		asm.append("NPUSHW[ ] " + ' '.join(args))
		asm.append("ENDF[ ]")
		fpgm.program.fromAssembly(asm)

		# Change maxp attributes as IDEF is added
		if 'maxp' in varfont:
			maxp = varfont['maxp']
			if hasattr(maxp, "maxInstructionDefs"):
				maxp.maxInstructionDefs += 1
			else:
				setattr(maxp, "maxInstructionDefs", 1)
			if hasattr(maxp, "maxStackElements"):
				maxp.maxStackElements = max(len(loc), maxp.maxStackElements)
			else:
				setattr(maxp, "maxInstructionDefs", len(loc))

	if 'name' in varfont:
		log.info("Pruning name table")
		exclude = {a.axisNameID for a in fvar.axes}
		for i in fvar.instances:
			exclude.add(i.subfamilyNameID)
			exclude.add(i.postscriptNameID)
		varfont['name'].names[:] = [
			n for n in varfont['name'].names
			if n.nameID not in exclude
		]

	if "wght" in location and "OS/2" in varfont:
		varfont["OS/2"].usWeightClass = otRound(
			max(1, min(location["wght"], 1000))
		)
	if "wdth" in location:
		wdth = location["wdth"]
		for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()):
			if wdth < percent:
				varfont["OS/2"].usWidthClass = widthClass
				break
		else:
			varfont["OS/2"].usWidthClass = 9
	if "slnt" in location and "post" in varfont:
		varfont["post"].italicAngle = max(-90, min(location["slnt"], 90))

	log.info("Removing variable tables")
	for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'):
		if tag in varfont:
			del varfont[tag]

	return varfont
Ejemplo n.º 25
0
def instantiateVariableFont(varfont, location, inplace=False):
	""" Generate a static instance from a variable TTFont and a dictionary
	defining the desired location along the variable font's axes.
	The location values must be specified as user-space coordinates, e.g.:

		{'wght': 400, 'wdth': 100}

	By default, a new TTFont object is returned. If ``inplace`` is True, the
	input varfont is modified and reduced to a static font.
	"""
	if not inplace:
		# make a copy to leave input varfont unmodified
		stream = BytesIO()
		varfont.save(stream)
		stream.seek(0)
		varfont = TTFont(stream)

	fvar = varfont['fvar']
	axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes}
	loc = normalizeLocation(location, axes)
	if 'avar' in varfont:
		maps = varfont['avar'].segments
		loc = {k:_DesignspaceAxis._map(v, maps[k]) for k,v in loc.items()}
	# Quantize to F2Dot14, to avoid surprise interpolations.
	loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()}
	# Location is normalized now
	log.info("Normalized location: %s", loc)

	log.info("Mutating glyf/gvar tables")
	gvar = varfont['gvar']
	glyf = varfont['glyf']
	# get list of glyph names in gvar sorted by component depth
	glyphnames = sorted(
		gvar.variations.keys(),
		key=lambda name: (
			glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
			if glyf[name].isComposite() else 0,
			name))
	for glyphname in glyphnames:
		variations = gvar.variations[glyphname]
		coordinates,_ = _GetCoordinates(varfont, glyphname)
		origCoords, endPts = None, None
		for var in variations:
			scalar = supportScalar(loc, var.axes)
			if not scalar: continue
			delta = var.coordinates
			if None in delta:
				if origCoords is None:
					origCoords,control = _GetCoordinates(varfont, glyphname)
					endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
				delta = iup_delta(delta, origCoords, endPts)
			coordinates += GlyphCoordinates(delta) * scalar
		_SetCoordinates(varfont, glyphname, coordinates)

	if 'cvar' in varfont:
		log.info("Mutating cvt/cvar tables")
		cvar = varfont['cvar']
		cvt = varfont['cvt ']
		deltas = {}
		for var in cvar.variations:
			scalar = supportScalar(loc, var.axes)
			if not scalar: continue
			for i, c in enumerate(var.coordinates):
				if c is not None:
					deltas[i] = deltas.get(i, 0) + scalar * c
		for i, delta in deltas.items():
			cvt[i] += otRound(delta)

	if 'MVAR' in varfont:
		log.info("Mutating MVAR table")
		mvar = varfont['MVAR'].table
		varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc)
		records = mvar.ValueRecord
		for rec in records:
			mvarTag = rec.ValueTag
			if mvarTag not in MVAR_ENTRIES:
				continue
			tableTag, itemName = MVAR_ENTRIES[mvarTag]
			delta = otRound(varStoreInstancer[rec.VarIdx])
			if not delta:
				continue
			setattr(varfont[tableTag], itemName,
				getattr(varfont[tableTag], itemName) + delta)

	if 'GDEF' in varfont:
		log.info("Mutating GDEF/GPOS/GSUB tables")
		merger = MutatorMerger(varfont, loc)

		log.info("Building interpolated tables")
		merger.instantiate()

	if 'name' in varfont:
		log.info("Pruning name table")
		exclude = {a.axisNameID for a in fvar.axes}
		for i in fvar.instances:
			exclude.add(i.subfamilyNameID)
			exclude.add(i.postscriptNameID)
		varfont['name'].names[:] = [
			n for n in varfont['name'].names
			if n.nameID not in exclude
		]

	if "wght" in location and "OS/2" in varfont:
		varfont["OS/2"].usWeightClass = otRound(
			max(1, min(location["wght"], 1000))
		)
	if "wdth" in location:
		wdth = location["wdth"]
		for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()):
			if wdth < percent:
				varfont["OS/2"].usWidthClass = widthClass
				break
		else:
			varfont["OS/2"].usWidthClass = 9
	if "slnt" in location and "post" in varfont:
		varfont["post"].italicAngle = max(-90, min(location["slnt"], 90))

	log.info("Removing variable tables")
	for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'):
		if tag in varfont:
			del varfont[tag]

	return varfont
Ejemplo n.º 26
0
def load_designspace(designspace):
    # TODO: remove this and always assume 'designspace' is a DesignSpaceDocument,
    # never a file path, as that's already handled by caller
    if hasattr(designspace, "sources"):  # Assume a DesignspaceDocument
        ds = designspace
    else:  # Assume a file path
        ds = DesignSpaceDocument.fromfile(designspace)

    masters = ds.sources
    if not masters:
        raise VarLibValidationError(
            "Designspace must have at least one source.")
    instances = ds.instances

    # TODO: Use fontTools.designspaceLib.tagForAxisName instead.
    standard_axis_map = OrderedDict([
        ('weight', ('wght', {
            'en': u'Weight'
        })),
        ('width', ('wdth', {
            'en': u'Width'
        })),
        ('slant', ('slnt', {
            'en': u'Slant'
        })),
        ('optical', ('opsz', {
            'en': u'Optical Size'
        })),
        ('italic', ('ital', {
            'en': u'Italic'
        })),
    ])

    # Setup axes
    if not ds.axes:
        raise VarLibValidationError(
            f"Designspace must have at least one axis.")

    axes = OrderedDict()
    for axis_index, axis in enumerate(ds.axes):
        axis_name = axis.name
        if not axis_name:
            if not axis.tag:
                raise VarLibValidationError(
                    f"Axis at index {axis_index} needs a tag.")
            axis_name = axis.name = axis.tag

        if axis_name in standard_axis_map:
            if axis.tag is None:
                axis.tag = standard_axis_map[axis_name][0]
            if not axis.labelNames:
                axis.labelNames.update(standard_axis_map[axis_name][1])
        else:
            if not axis.tag:
                raise VarLibValidationError(
                    f"Axis at index {axis_index} needs a tag.")
            if not axis.labelNames:
                axis.labelNames["en"] = tounicode(axis_name)

        axes[axis_name] = axis
    log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()]))

    # Check all master and instance locations are valid and fill in defaults
    for obj in masters + instances:
        obj_name = obj.name or obj.styleName or ''
        loc = obj.location
        if loc is None:
            raise VarLibValidationError(
                f"Source or instance '{obj_name}' has no location.")
        for axis_name in loc.keys():
            if axis_name not in axes:
                raise VarLibValidationError(
                    f"Location axis '{axis_name}' unknown for '{obj_name}'.")
        for axis_name, axis in axes.items():
            if axis_name not in loc:
                # NOTE: `axis.default` is always user-space, but `obj.location` always design-space.
                loc[axis_name] = axis.map_forward(axis.default)
            else:
                v = axis.map_backward(loc[axis_name])
                if not (axis.minimum <= v <= axis.maximum):
                    raise VarLibValidationError(
                        f"Source or instance '{obj_name}' has out-of-range location "
                        f"for axis '{axis_name}': is mapped to {v} but must be in "
                        f"mapped range [{axis.minimum}..{axis.maximum}] (NOTE: all "
                        "values are in user-space).")

    # Normalize master locations

    internal_master_locs = [o.location for o in masters]
    log.info("Internal master locations:\n%s", pformat(internal_master_locs))

    # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar
    internal_axis_supports = {}
    for axis in axes.values():
        triple = (axis.minimum, axis.default, axis.maximum)
        internal_axis_supports[axis.name] = [
            axis.map_forward(v) for v in triple
        ]
    log.info("Internal axis supports:\n%s", pformat(internal_axis_supports))

    normalized_master_locs = [
        models.normalizeLocation(m, internal_axis_supports)
        for m in internal_master_locs
    ]
    log.info("Normalized master locations:\n%s",
             pformat(normalized_master_locs))

    # Find base master
    base_idx = None
    for i, m in enumerate(normalized_master_locs):
        if all(v == 0 for v in m.values()):
            if base_idx is not None:
                raise VarLibValidationError(
                    "More than one base master found in Designspace.")
            base_idx = i
    if base_idx is None:
        raise VarLibValidationError(
            "Base master not found; no master at default location?")
    log.info("Index of base master: %s", base_idx)

    return _DesignSpaceData(
        axes,
        internal_axis_supports,
        base_idx,
        normalized_master_locs,
        masters,
        instances,
        ds.rules,
        ds.rulesProcessingLast,
        ds.lib,
    )
Ejemplo n.º 27
0
def test_normalizeLocation():
    axes = {"wght": (100, 400, 900)}
    assert normalizeLocation({"wght": 400}, axes) == {'wght': 0.0}
    assert normalizeLocation({"wght": 100}, axes) == {'wght': -1.0}
    assert normalizeLocation({"wght": 900}, axes) == {'wght': 1.0}
    assert normalizeLocation({"wght": 650}, axes) == {'wght': 0.5}
    assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0}
    assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0}

    axes = {"wght": (0, 0, 1000)}
    assert normalizeLocation({"wght": 0}, axes) == {'wght': 0.0}
    assert normalizeLocation({"wght": -1}, axes) == {'wght': 0.0}
    assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0}
    assert normalizeLocation({"wght": 500}, axes) == {'wght': 0.5}
    assert normalizeLocation({"wght": 1001}, axes) == {'wght': 1.0}

    axes = {"wght": (0, 1000, 1000)}
    assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0}
    assert normalizeLocation({"wght": -1}, axes) == {'wght': -1.0}
    assert normalizeLocation({"wght": 500}, axes) == {'wght': -0.5}
    assert normalizeLocation({"wght": 1000}, axes) == {'wght': 0.0}
    assert normalizeLocation({"wght": 1001}, axes) == {'wght': 0.0}
Ejemplo n.º 28
0
 def setLocation(self, location):
     self.location = normalizeLocation(location, self._axes)
Ejemplo n.º 29
0
 def _normalize(self, location):
     return normalizeLocation(location, self.axes)
Ejemplo n.º 30
0
 def _normalize(self, location):
     new = {}
     for axisName in location.keys():
         new[axisName] = normalizeLocation(dict(w=location[axisName]),
                                           dict(w=self.axes[axisName]))
     return new
Ejemplo n.º 31
0
def load_designspace(designspace):
    # TODO: remove this and always assume 'designspace' is a DesignSpaceDocument,
    # never a file path, as that's already handled by caller
    if hasattr(designspace, "sources"):  # Assume a DesignspaceDocument
        ds = designspace
    else:  # Assume a file path
        ds = DesignSpaceDocument.fromfile(designspace)

    masters = ds.sources
    if not masters:
        raise VarLibError("no sources found in .designspace")
    instances = ds.instances

    standard_axis_map = OrderedDict([
        ('weight', ('wght', {
            'en': u'Weight'
        })),
        ('width', ('wdth', {
            'en': u'Width'
        })),
        ('slant', ('slnt', {
            'en': u'Slant'
        })),
        ('optical', ('opsz', {
            'en': u'Optical Size'
        })),
        ('italic', ('ital', {
            'en': u'Italic'
        })),
    ])

    # Setup axes
    axes = OrderedDict()
    for axis in ds.axes:
        axis_name = axis.name
        if not axis_name:
            assert axis.tag is not None
            axis_name = axis.name = axis.tag

        if axis_name in standard_axis_map:
            if axis.tag is None:
                axis.tag = standard_axis_map[axis_name][0]
            if not axis.labelNames:
                axis.labelNames.update(standard_axis_map[axis_name][1])
        else:
            assert axis.tag is not None
            if not axis.labelNames:
                axis.labelNames["en"] = tounicode(axis_name)

        axes[axis_name] = axis
    log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()]))

    # Check all master and instance locations are valid and fill in defaults
    for obj in masters + instances:
        obj_name = obj.name or obj.styleName or ''
        loc = obj.location
        for axis_name in loc.keys():
            assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (
                axis_name, obj_name)
        for axis_name, axis in axes.items():
            if axis_name not in loc:
                loc[axis_name] = axis.default
            else:
                v = axis.map_backward(loc[axis_name])
                assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (
                    axis_name, v, obj_name, axis.minimum, axis.maximum)

    # Normalize master locations

    internal_master_locs = [o.location for o in masters]
    log.info("Internal master locations:\n%s", pformat(internal_master_locs))

    # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar
    internal_axis_supports = {}
    for axis in axes.values():
        triple = (axis.minimum, axis.default, axis.maximum)
        internal_axis_supports[axis.name] = [
            axis.map_forward(v) for v in triple
        ]
    log.info("Internal axis supports:\n%s", pformat(internal_axis_supports))

    normalized_master_locs = [
        models.normalizeLocation(m, internal_axis_supports)
        for m in internal_master_locs
    ]
    log.info("Normalized master locations:\n%s",
             pformat(normalized_master_locs))

    # Find base master
    base_idx = None
    for i, m in enumerate(normalized_master_locs):
        if all(v == 0 for v in m.values()):
            assert base_idx is None
            base_idx = i
    assert base_idx is not None, "Base master not found; no master at default location?"
    log.info("Index of base master: %s", base_idx)

    return _DesignSpaceData(
        axes,
        internal_axis_supports,
        base_idx,
        normalized_master_locs,
        masters,
        instances,
        ds.rules,
    )
Ejemplo n.º 32
0
def build(designspace_filename, master_finder=lambda s: s, axisMap=None):
    """
	Build variation font from a designspace file.

	If master_finder is set, it should be a callable that takes master
	filename as found in designspace file and map it to master font
	binary as to be opened (eg. .ttf or .otf).

	If axisMap is set, it should be dictionary mapping axis-id to
	(axis-tag, axis-name).
	"""

    masters, instances = designspace.load(designspace_filename)
    base_idx = None
    for i, m in enumerate(masters):
        if 'info' in m and m['info']['copy']:
            assert base_idx is None
            base_idx = i
    assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document."

    from pprint import pprint
    print("Index of base master:", base_idx)

    print("Building GX")
    print("Loading TTF masters")
    basedir = os.path.dirname(designspace_filename)
    master_ttfs = [
        master_finder(os.path.join(basedir, m['filename'])) for m in masters
    ]
    master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]

    standard_axis_map = {
        'weight': ('wght', 'Weight'),
        'width': ('wdth', 'Width'),
        'slant': ('slnt', 'Slant'),
        'optical': ('opsz', 'Optical Size'),
        'custom': ('xxxx', 'Custom'),
    }

    axis_map = standard_axis_map
    if axisMap:
        axis_map = axis_map.copy()
        axis_map.update(axisMap)

    # TODO: For weight & width, use OS/2 values and setup 'avar' mapping.

    master_locs = [o['location'] for o in masters]

    axis_tags = set(master_locs[0].keys())
    assert all(axis_tags == set(m.keys()) for m in master_locs)

    # Set up axes
    axes = {}
    for tag in axis_tags:
        default = master_locs[base_idx][tag]
        lower = min(m[tag] for m in master_locs)
        upper = max(m[tag] for m in master_locs)
        axes[tag] = (lower, default, upper)
    print("Axes:")
    pprint(axes)

    print("Master locations:")
    pprint(master_locs)

    # We can use the base font straight, but it's faster to load it again since
    # then we won't be recompiling the existing ('glyf', 'hmtx', ...) tables.
    #gx = master_fonts[base_idx]
    gx = TTFont(master_ttfs[base_idx])

    # TODO append masters as named-instances as well; needs .designspace change.
    fvar = _add_fvar(gx, axes, instances, axis_map)

    # Normalize master locations
    master_locs = [models.normalizeLocation(m, axes) for m in master_locs]

    print("Normalized master locations:")
    pprint(master_locs)

    # TODO Clean this up.
    del instances
    del axes
    master_locs = [{axis_map[k][0]: v
                    for k, v in loc.items()} for loc in master_locs]
    #instance_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in instance_locs]
    axisTags = [axis.axisTag for axis in fvar.axes]

    # Assume single-model for now.
    model = models.VariationModel(master_locs)
    assert 0 == model.mapping[base_idx]

    print("Building variations tables")
    if 'glyf' in gx:
        _add_gvar(gx, model, master_fonts)
    _add_HVAR(gx, model, master_fonts, axisTags)
    _merge_OTL(gx, model, master_fonts, axisTags, base_idx)

    return gx, model, master_ttfs
Ejemplo n.º 33
0
def makeInstance(pathOrVarFont,
                 location,
                 dstPath=None,
                 normalize=True,
                 cached=True,
                 lazy=True,
                 kerning=None):
    """Instantiate an instance of a variable font at the specified location.

    Keyword arguments:
    - varfilename -- a variable font file path
    - location -- a dictionary of axis tag and value {"wght": 0.75, "wdth": -0.5}

    >>> vf = findFont('RobotoDelta-VF')
    >>> print(vf)
    <Font RobotoDelta-VF>
    >>> print(len(vf))
    188
    >>> instance = makeInstance(vf.path, dict(opsz=8), cached=False)
    >>> instance
    <Font RobotoDelta-VF-opsz8>
    >>> len(instance)
    241
    >>> len(instance['H'].points)
    12
    >>> instance['Egrave']
    <PageBot Glyph Egrave Pts:0/Cnt:0/Cmp:2>
    >>> len(instance['Egrave'].components)
    2
    """
    # make a custom file name from the location e.g.
    # VariableFont-wghtXXX-wdthXXX.ttf
    instanceName = ""

    if isinstance(pathOrVarFont, Font):
        pathOrVarFont = pathOrVarFont.path

    varFont = Font(pathOrVarFont, lazy=lazy)
    ttFont = varFont.ttFont

    for k, v in sorted(location.items()):
        # TODO better way to normalize the location name to (0, 1000)
        v = min(v, 1000)
        v = max(v, 0)
        instanceName += "-%s%s" % (k, v)

    if dstPath is None:
        targetFileName = '.'.join(varFont.path.split('/')[-1].split('.')
                                  [:-1]) + instanceName + '.ttf'
        targetDirectory = getInstancePath()
        if not targetDirectory.endswith('/'):
            targetDirectory += '/'
        if not os.path.exists(targetDirectory):
            os.makedirs(targetDirectory)
        dstPath = targetDirectory + targetFileName

    # Instance does not exist as file. Create it.
    if not cached or not os.path.exists(dstPath):
        # Set the instance name IDs in the name table
        platforms = ((1, 0, 0), (3, 1, 0x409))  # Macintosh and Windows

        for platformID, platEncID, langID in platforms:
            familyName = ttFont['name'].getName(1, platformID, platEncID,
                                                langID)  # 1 Font Family name
            if not familyName:
                continue
            familyName = familyName.toUnicode()  # NameRecord to unicode string
            styleName = unicode(
                instanceName)  # TODO make sure this works in any case
            fullFontName = " ".join([familyName, styleName])
            postscriptName = fullFontName.replace(" ", "-")
            ttFont['name'].setName(styleName, 2, platformID, platEncID,
                                   langID)  # 2 Font Subfamily name
            ttFont['name'].setName(fullFontName, 4, platformID, platEncID,
                                   langID)  # 4 Full font name
            ttFont['name'].setName(postscriptName, 6, platformID, platEncID,
                                   langID)  # 6 Postscript name for the font
            # Other important name IDs
            # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular)
            # 25 Variables PostScript Name Prefix

        fvar = ttFont['fvar']
        axes = {
            a.axisTag: (a.minValue, a.defaultValue, a.maxValue)
            for a in fvar.axes
        }
        # TODO Apply avar
        # TODO Round to F2Dot14?
        loc = normalizeLocation(location, axes)
        # Location is normalized now
        #print("Normalized location:", loc)

        gvar = ttFont['gvar']
        glyf = ttFont['glyf']
        # get list of glyph names in gvar sorted by component depth
        glyphNames = sorted(
            gvar.variations.keys(),
            key=lambda name:
            (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
             if glyf[name].isComposite() else 0, name))

        for glyphName in glyphNames:
            variations = gvar.variations[glyphName]
            coordinates, _ = _GetCoordinates(ttFont, glyphName)
            origCoords, endPts = None, None

            for var in variations:
                scalar = supportScalar(loc, var.axes)  #, ot=True)
                if not scalar: continue
                delta = var.coordinates
                if None in delta:
                    if origCoords is None:
                        origCoords, control = _GetCoordinates(
                            ttFont, glyphName)
                        endPts = control[1] if control[0] >= 1 else list(
                            range(len(control[1])))
                    delta = iup_delta(delta, origCoords, endPts)
                coordinates += GlyphCoordinates(delta) * scalar
            _SetCoordinates(ttFont, glyphName, coordinates)

        # Interpolate cvt

        if 'cvar' in ttFont:
            cvar = ttFont['cvar']
            cvt = ttFont['cvt ']
            deltas = {}
            for var in cvar.variations:
                scalar = supportScalar(loc, var.axes)
                if not scalar: continue
                for i, c in enumerate(var.coordinates):
                    if c is not None:
                        deltas[i] = deltas.get(i, 0) + scalar * c
            for i, delta in deltas.items():
                cvt[i] += int(round(delta))

        #print("Removing variable tables")
        for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR',
                    'STAT'):
            if tag in ttFont:
                del ttFont[tag]

        if kerning is not None:
            for pair, value in kerning.items():
                varFont.kerning[pair] = value

        #print("Saving instance font", outFile)
        varFont.save(dstPath)

    # Answer instance.
    return Font(dstPath, lazy=lazy)
Ejemplo n.º 34
0
def main(args=None):

	if args is None:
		import sys
		args = sys.argv[1:]

	varfilename = args[0]
	locargs = args[1:]
	outfile = os.path.splitext(varfilename)[0] + '-instance.ttf'

	loc = {}
	for arg in locargs:
		tag,val = arg.split('=')
		assert len(tag) <= 4
		loc[tag.ljust(4)] = float(val)
	print("Location:", loc)

	print("Loading variable font")
	varfont = TTFont(varfilename)

	fvar = varfont['fvar']
	axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes}
	# TODO Apply avar
	# TODO Round to F2Dot14?
	loc = normalizeLocation(loc, axes)
	# Location is normalized now
	print("Normalized location:", loc)

	gvar = varfont['gvar']
	glyf = varfont['glyf']
	# get list of glyph names in gvar sorted by component depth
	glyphnames = sorted(
		gvar.variations.keys(),
		key=lambda name: (
			glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
			if glyf[name].isComposite() else 0,
			name))
	for glyphname in glyphnames:
		variations = gvar.variations[glyphname]
		coordinates,_ = _GetCoordinates(varfont, glyphname)
		origCoords, endPts = None, None
		for var in variations:
			scalar = supportScalar(loc, var.axes)
			if not scalar: continue
			delta = var.coordinates
			if None in delta:
				if origCoords is None:
					origCoords,control = _GetCoordinates(varfont, glyphname)
					endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
				delta = _iup_delta(delta, origCoords, endPts)
				# TODO Do IUP / handle None items
			coordinates += GlyphCoordinates(delta) * scalar
		_SetCoordinates(varfont, glyphname, coordinates)

	print("Removing variable tables")
	for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'):
		if tag in varfont:
			del varfont[tag]

	print("Saving instance font", outfile)
	varfont.save(outfile)
Ejemplo n.º 35
0
 def normalize(name, value):
     return models.normalizeLocation({name: value},
                                     internal_axis_supports)[name]
Ejemplo n.º 36
0
def generateInstance(variableFontPath,
                     location,
                     targetDirectory,
                     normalize=False,
                     force=False):

    instanceName = ""

    for k, v in sorted(location.items()):
        # TODO better way to normalize the location name to (0, 1000)
        v = min(v, 1000)
        v = max(v, 0)
        instanceName += "-%s%s" % (k, v)
    targetFileName = '.'.join(variableFontPath.split('/')[-1].split('.')
                              [:-1]) + instanceName + '.ttf'

    if not targetDirectory.endswith('/'):
        targetDirectory += '/'
    if not os.path.exists(targetDirectory):
        os.makedirs(targetDirectory)
    outFile = targetDirectory + targetFileName

    if force or not os.path.exists(outFile):
        #print location
        #print("Loading variable font")
        varFont = TTFont(variableFontPath)

        fvar = varFont['fvar']
        axes = {
            a.axisTag: (a.minValue, a.defaultValue, a.maxValue)
            for a in fvar.axes
        }
        # TODO Apply avar
        # TODO Round to F2Dot14?
        loc = normalizeLocation(location, axes)
        # Location is normalized now
        #print("Normalized location:", loc, 'from', location)

        # Set the instance name IDs in the name table
        platforms = ((1, 0, 0), (3, 1, 0x409))  # Macintosh and Windows
        for platformID, platEncID, langID in platforms:
            familyName = varFont['name'].getName(1, platformID, platEncID,
                                                 langID)  # 1 Font Family name
            if not familyName:
                continue
            familyName = familyName.toUnicode()  # NameRecord to unicode string
            styleName = unicode(
                instanceName)  # TODO make sure this works in any case
            fullFontName = " ".join([familyName, styleName])
            postscriptName = fullFontName.replace(" ", "-")
            varFont['name'].setName(styleName, 2, platformID, platEncID,
                                    langID)  # 2 Font Subfamily name
            varFont['name'].setName(fullFontName, 4, platformID, platEncID,
                                    langID)  # 4 Full font name
            varFont['name'].setName(postscriptName, 6, platformID, platEncID,
                                    langID)  # 6 Postscript name for the font
            # Other important name IDs
            # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular)
            # 25 Variables PostScript Name Prefix

        gvar = varFont['gvar']
        glyf = varFont['glyf']
        # get list of glyph names in gvar sorted by component depth
        glyphnames = sorted(
            gvar.variations.keys(),
            key=lambda name:
            (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
             if glyf[name].isComposite() else 0, name))
        for glyphname in glyphnames:
            variations = gvar.variations[glyphname]
            coordinates, _ = _GetCoordinates(varFont, glyphname)
            origCoords, endPts = None, None
            for var in variations:
                scalar = supportScalar(loc, var.axes)  #, ot=True)
                if not scalar: continue
                delta = var.coordinates
                if None in delta:
                    if origCoords is None:
                        origCoords, control = _GetCoordinates(
                            varFont, glyphname)
                        endPts = control[1] if control[0] >= 1 else list(
                            range(len(control[1])))
                    delta = _iup_delta(delta, origCoords, endPts)
                coordinates += GlyphCoordinates(delta) * scalar
            _SetCoordinates(varFont, glyphname, coordinates)

        #print("Removing variable tables")
        for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR',
                    'STAT'):
            if tag in varFont:
                del varFont[tag]

        #print("Saving instance font", outFile)
        varFont.save(outFile)

    # Installing the font in DrawBot. Answer font name and path.
    return c.installFont(outFile), outFile
Ejemplo n.º 37
0
def load_designspace(designspace_filename):

	ds = designspace.load(designspace_filename)
	axes = ds.get('axes')
	masters = ds.get('sources')
	if not masters:
		raise VarLibError("no sources found in .designspace")
	instances = ds.get('instances', [])

	standard_axis_map = OrderedDict([
		('weight',  ('wght', {'en':'Weight'})),
		('width',   ('wdth', {'en':'Width'})),
		('slant',   ('slnt', {'en':'Slant'})),
		('optical', ('opsz', {'en':'Optical Size'})),
		])


	# Setup axes
	class DesignspaceAxis(object):

		def __repr__(self):
			return repr(self.__dict__)

		@staticmethod
		def _map(v, map):
			keys = map.keys()
			if not keys:
				return v
			if v in keys:
				return map[v]
			k = min(keys)
			if v < k:
				return v + map[k] - k
			k = max(keys)
			if v > k:
				return v + map[k] - k
			# Interpolate
			a = max(k for k in keys if k < v)
			b = min(k for k in keys if k > v)
			va = map[a]
			vb = map[b]
			return va + (vb - va) * (v - a) / (b - a)

		def map_forward(self, v):
			if self.map is None: return v
			return self._map(v, self.map)

		def map_backward(self, v):
			if self.map is None: return v
			map = {v:k for k,v in self.map.items()}
			return self._map(v, map)

	axis_objects = OrderedDict()
	if axes is not None:
		for axis_dict in axes:
			axis_name = axis_dict.get('name')
			if not axis_name:
				axis_name = axis_dict['name'] = axis_dict['tag']
			if 'map' not in axis_dict:
				axis_dict['map'] = None
			else:
				axis_dict['map'] = {m['input']:m['output'] for m in axis_dict['map']}

			if axis_name in standard_axis_map:
				if 'tag' not in axis_dict:
					axis_dict['tag'] = standard_axis_map[axis_name][0]
				if 'labelname' not in axis_dict:
					axis_dict['labelname'] = standard_axis_map[axis_name][1].copy()

			axis = DesignspaceAxis()
			for item in ['name', 'tag', 'minimum', 'default', 'maximum', 'map']:
				assert item in axis_dict, 'Axis does not have "%s"' % item
			if 'labelname' not in axis_dict:
				axis_dict['labelname'] = {'en': axis_name}
			axis.__dict__ = axis_dict
			axis_objects[axis_name] = axis
	else:
		# No <axes> element. Guess things...
		base_idx = None
		for i,m in enumerate(masters):
			if 'info' in m and m['info']['copy']:
				assert base_idx is None
				base_idx = i
		assert base_idx is not None, "Cannot find 'base' master; Either add <axes> element to .designspace document, or add <info> element to one of the sources in the .designspace document."

		master_locs = [o['location'] for o in masters]
		base_loc = master_locs[base_idx]
		axis_names = set(base_loc.keys())
		assert all(name in standard_axis_map for name in axis_names), "Non-standard axis found and there exist no <axes> element."

		for name,(tag,labelname) in standard_axis_map.items():
			if name not in axis_names:
				continue

			axis = DesignspaceAxis()
			axis.name = name
			axis.tag = tag
			axis.labelname = labelname.copy()
			axis.default = base_loc[name]
			axis.minimum = min(m[name] for m in master_locs if name in m)
			axis.maximum = max(m[name] for m in master_locs if name in m)
			axis.map = None
			# TODO Fill in weight / width mapping from OS/2 table? Need loading fonts...
			axis_objects[name] = axis
		del base_idx, base_loc, axis_names, master_locs
	axes = axis_objects
	del axis_objects
	log.info("Axes:\n%s", pformat(axes))


	# Check all master and instance locations are valid and fill in defaults
	for obj in masters+instances:
		obj_name = obj.get('name', obj.get('stylename', ''))
		loc = obj['location']
		for axis_name in loc.keys():
			assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name)
		for axis_name,axis in axes.items():
			if axis_name not in loc:
				loc[axis_name] = axis.default
			else:
				v = axis.map_backward(loc[axis_name])
				assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum)


	# Normalize master locations

	normalized_master_locs = [o['location'] for o in masters]
	log.info("Internal master locations:\n%s", pformat(normalized_master_locs))

	# TODO This mapping should ideally be moved closer to logic in _add_fvar/avar
	internal_axis_supports = {}
	for axis in axes.values():
		triple = (axis.minimum, axis.default, axis.maximum)
		internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple]
	log.info("Internal axis supports:\n%s", pformat(internal_axis_supports))

	normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in normalized_master_locs]
	log.info("Normalized master locations:\n%s", pformat(normalized_master_locs))


	# Find base master
	base_idx = None
	for i,m in enumerate(normalized_master_locs):
		if all(v == 0 for v in m.values()):
			assert base_idx is None
			base_idx = i
	assert base_idx is not None, "Base master not found; no master at default location?"
	log.info("Index of base master: %s", base_idx)

	return axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances
Ejemplo n.º 38
0
def build(designspace_filename, master_finder=lambda s:s, axisMap=None):
	"""
	Build variation font from a designspace file.

	If master_finder is set, it should be a callable that takes master
	filename as found in designspace file and map it to master font
	binary as to be opened (eg. .ttf or .otf).

	If axisMap is set, it should be dictionary mapping axis-id to
	(axis-tag, axis-name).
	"""

	masters, instances = designspace.load(designspace_filename)
	base_idx = None
	for i,m in enumerate(masters):
		if 'info' in m and m['info']['copy']:
			assert base_idx is None
			base_idx = i
	assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document."

	log.info("Index of base master: %s", base_idx)

	log.info("Building variable font")
	log.info("Loading TTF masters")
	basedir = os.path.dirname(designspace_filename)
	master_ttfs = [master_finder(os.path.join(basedir, m['filename'])) for m in masters]
	master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]

	standard_axis_map = {
		'weight':  ('wght', 'Weight'),
		'width':   ('wdth', 'Width'),
		'slant':   ('slnt', 'Slant'),
		'optical': ('opsz', 'Optical Size'),
		'custom':  ('xxxx', 'Custom'),
	}

	axis_map = standard_axis_map
	if axisMap:
		axis_map = axis_map.copy()
		axis_map.update(axisMap)

	# TODO: For weight & width, use OS/2 values and setup 'avar' mapping.

	master_locs = [o['location'] for o in masters]

	axis_tags = set(master_locs[0].keys())
	assert all(axis_tags == set(m.keys()) for m in master_locs)

	# Set up axes
	axes = {}
	for tag in axis_tags:
		default = master_locs[base_idx][tag]
		lower = min(m[tag] for m in master_locs)
		upper = max(m[tag] for m in master_locs)
		if default == lower == upper:
			continue
		axes[tag] = (lower, default, upper)
	log.info("Axes:\n%s", pformat(axes))

	log.info("Master locations:\n%s", pformat(master_locs))

	# We can use the base font straight, but it's faster to load it again since
	# then we won't be recompiling the existing ('glyf', 'hmtx', ...) tables.
	#gx = master_fonts[base_idx]
	gx = TTFont(master_ttfs[base_idx])

	# TODO append masters as named-instances as well; needs .designspace change.
	fvar = _add_fvar(gx, axes, instances, axis_map)


	# Normalize master locations
	master_locs = [models.normalizeLocation(m, axes) for m in master_locs]

	log.info("Normalized master locations:\n%s", pformat(master_locs))

	# TODO Clean this up.
	del instances
	del axes
	master_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in master_locs]
	#instance_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in instance_locs]
	axisTags = [axis.axisTag for axis in fvar.axes]

	# Assume single-model for now.
	model = models.VariationModel(master_locs)
	assert 0 == model.mapping[base_idx]

	log.info("Building variations tables")
	if 'glyf' in gx:
		_add_gvar(gx, model, master_fonts)
	_add_HVAR(gx, model, master_fonts, axisTags)
	_merge_OTL(gx, model, master_fonts, axisTags, base_idx)

	return gx, model, master_ttfs
Ejemplo n.º 39
0
def test_normalizeLocation():
    axes = {"wght": (100, 400, 900)}
    assert normalizeLocation({"wght": 400}, axes) == {'wght': 0.0}
    assert normalizeLocation({"wght": 100}, axes) == {'wght': -1.0}
    assert normalizeLocation({"wght": 900}, axes) == {'wght': 1.0}
    assert normalizeLocation({"wght": 650}, axes) == {'wght': 0.5}
    assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0}
    assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0}

    axes = {"wght": (0, 0, 1000)}
    assert normalizeLocation({"wght": 0}, axes) == {'wght': 0.0}
    assert normalizeLocation({"wght": -1}, axes) == {'wght': 0.0}
    assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0}
    assert normalizeLocation({"wght": 500}, axes) == {'wght': 0.5}
    assert normalizeLocation({"wght": 1001}, axes) == {'wght': 1.0}

    axes = {"wght": (0, 1000, 1000)}
    assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0}
    assert normalizeLocation({"wght": -1}, axes) == {'wght': -1.0}
    assert normalizeLocation({"wght": 500}, axes) == {'wght': -0.5}
    assert normalizeLocation({"wght": 1000}, axes) == {'wght': 0.0}
    assert normalizeLocation({"wght": 1001}, axes) == {'wght': 0.0}