Esempio n. 1
0
def instantiateOTL(varfont, location):
    # TODO(anthrotype) Support partial instancing of JSTF and BASE tables

    if (
        "GDEF" not in varfont
        or varfont["GDEF"].table.Version < 0x00010003
        or not varfont["GDEF"].table.VarStore
    ):
        return

    if "GPOS" in varfont:
        msg = "Instantiating GDEF and GPOS tables"
    else:
        msg = "Instantiating GDEF table"
    log.info(msg)

    gdef = varfont["GDEF"].table
    varStore = gdef.VarStore
    fvarAxes = varfont["fvar"].axes

    defaultDeltas = instantiateItemVariationStore(varStore, fvarAxes, location)

    # When VF are built, big lookups may overflow and be broken into multiple
    # subtables. MutatorMerger (which inherits from AligningMerger) reattaches
    # them upon instancing, in case they can now fit a single subtable (if not,
    # they will be split again upon compilation).
    # This 'merger' also works as a 'visitor' that traverses the OTL tables and
    # calls specific methods when instances of a given type are found.
    # Specifically, it adds default deltas to GPOS Anchors/ValueRecords and GDEF
    # LigatureCarets, and optionally deletes all VariationIndex tables if the
    # VarStore is fully instanced.
    merger = MutatorMerger(
        varfont, defaultDeltas, deleteVariations=(not varStore.VarRegionList.Region)
    )
    merger.mergeTables(varfont, [varfont], ["GDEF", "GPOS"])

    if varStore.VarRegionList.Region:
        varIndexMapping = varStore.optimize()
        gdef.remap_device_varidxes(varIndexMapping)
        if "GPOS" in varfont:
            varfont["GPOS"].table.remap_device_varidxes(varIndexMapping)
    else:
        # 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"]
Esempio n. 2
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
Esempio n. 3
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