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
def test_load(self): self.assertEqual(designspace.load(_getpath("VarLibTest.designspace")), ([{ 'filename': 'VarLibTest-Light.ufo', 'groups': { 'copy': True }, 'info': { 'copy': True }, 'lib': { 'copy': True }, 'location': { 'weight': 0.0 }, 'name': 'master_1' }, { 'filename': 'VarLibTest-Bold.ufo', 'location': { 'weight': 1.0 }, 'name': 'master_2' }], [{ 'filename': 'instance/VarLibTest-Medium.ufo', 'location': { 'weight': 0.5 }, 'familyname': 'VarLibTest', 'stylename': 'Medium', 'info': {}, 'kerning': {} }]))
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
def _set_path(self, path): self._path = path if path is not None: self._ds = designspace.load(path) self._axes = self._ds['axes'] self._sources = self._ds['sources'] self._instances = self._ds.get('instances', []) else: self._ds = None self._axes = [] self._sources = [] self._instances = []
def test_load(self): self.maxDiff = None self.assertEqual( designspace.load(_getpath("Designspace.designspace")), {'sources': [{'location': {'weight': 0.0}, 'groups': {'copy': True}, 'filename': 'DesignspaceTest-Light.ufo', 'info': {'copy': True}, 'name': 'master_1', 'lib': {'copy': True}}, {'location': {'weight': 1.0}, 'name': 'master_2', 'filename': 'DesignspaceTest-Bold.ufo'}], 'instances': [{'location': {'weight': 0.5}, 'familyname': 'DesignspaceTest', 'filename': 'instance/DesignspaceTest-Medium.ufo', 'kerning': {}, 'info': {}, 'stylename': 'Medium'}], 'axes': [{'name': 'weight', 'map': [{'input': 0.0, 'output': 10.0}, {'input': 401.0, 'output': 66.0}, {'input': 1000.0, 'output': 990.0}], 'tag': 'wght', 'maximum': 1000.0, 'minimum': 0.0, 'default': 0.0}, {'maximum': 1000.0, 'default': 250.0, 'minimum': 0.0, 'name': 'width', 'tag': 'wdth'}, {'name': 'contrast', 'tag': 'cntr', 'maximum': 100.0, 'minimum': 0.0, 'default': 0.0, 'labelname': {'de': 'Kontrast', 'en': 'Contrast'}}] } )
def test_load(self): self.assertEqual( designspace.load(_getpath("VarLibTest.designspace")), ([{'filename': 'VarLibTest-Light.ufo', 'groups': {'copy': True}, 'info': {'copy': True}, 'lib': {'copy': True}, 'location': {'weight': 0.0}, 'name': 'master_1'}, {'filename': 'VarLibTest-Bold.ufo', 'location': {'weight': 1.0}, 'name': 'master_2'}], [{'filename': 'instance/VarLibTest-Medium.ufo', 'location': {'weight': 0.5}, 'familyname': 'VarLibTest', 'stylename': 'Medium', 'info': {}, 'kerning': {}}]) )
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
def test_load2(self): self.assertEqual( designspace.load(_getpath("Designspace2.designspace")), {'sources': [], 'instances': [{}]})
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
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
def build(designspace_filename, master_finder=lambda s: s): """ 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). """ 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): @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 # 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 name in loc.keys(): assert name in axes, "Location axis '%s' unknown for '%s'." % ( 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]" % ( name, v, obj_name, axis.minimum, axis.maximum) # Normalize master locations master_locs = [o['location'] for o in masters] log.info("Internal master locations:\n%s", pformat(master_locs)) # TODO This mapping should ideally be moved closer to logic in _add_fvar_avar axis_supports = {} for axis in axes.values(): triple = (axis.minimum, axis.default, axis.maximum) axis_supports[axis.name] = [axis.map_forward(v) for v in triple] log.info("Internal axis supports:\n%s", pformat(axis_supports)) master_locs = [ models.normalizeLocation(m, axis_supports) for m in master_locs ] log.info("Normalized master locations:\n%s", pformat(master_locs)) del axis_supports # Find base master base_idx = None for i, m in enumerate(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) log.info("Building variable 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] # Reload base font as target font vf = TTFont(master_ttfs[base_idx]) # TODO append masters as named-instances as well; needs .designspace change. fvar, avar = _add_fvar_avar(vf, axes, instances) del instances # Map from axis names to axis tags... master_locs = [{axes[k].tag: v for k, v in loc.items()} for loc in master_locs] #del axes # From here on, we use fvar axes only 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") _add_MVAR(vf, model, master_fonts, axisTags) if 'glyf' in vf: _add_gvar(vf, model, master_fonts) _add_HVAR(vf, model, master_fonts, axisTags) _merge_OTL(vf, model, master_fonts, axisTags, base_idx) return vf, model, master_ttfs
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
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 an ordered dictionary mapping axis-id to (axis-tag, axis-name). """ ds = designspace.load(designspace_filename) axes = ds['axes'] if 'axes' in ds else [] if 'sources' not in ds or not ds['sources']: raise VarLibError("no 'sources' defined in .designspace") masters = ds['sources'] instances = ds['instances'] if 'instances' in ds else [] 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 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] standard_axis_map = OrderedDict([('weight', ('wght', 'Weight')), ('width', ('wdth', 'Width')), ('slant', ('slnt', 'Slant')), ('optical', ('opsz', 'Optical Size')), ('custom', ('xxxx', 'Custom'))]) if axisMap: # a dictionary mapping axis-id to (axis-tag, axis-name) was provided axis_map = standard_axis_map.copy() axis_map.update(axisMap) elif axes: # the designspace file loaded had an <axes> element. # honor the order of the axes axis_map = OrderedDict() for axis in axes: axis_name = axis['name'] if axis_name in standard_axis_map: axis_map[axis_name] = standard_axis_map[axis_name] else: tag = axis['tag'] assert axis['labelname']['en'] label = axis['labelname']['en'] axis_map[axis_name] = (tag, label) else: axis_map = standard_axis_map # TODO: For weight & width, use OS/2 values and setup 'avar' mapping. master_locs = [o['location'] for o in masters] axis_names = set(master_locs[0].keys()) assert all(axis_names == set(m.keys()) for m in master_locs) # Set up axes axes_dict = {} if axes: # the designspace file loaded had an <axes> element for axis in axes: default = axis['default'] lower = axis['minimum'] upper = axis['maximum'] name = axis['name'] axes_dict[name] = (lower, default, upper) else: for name in axis_names: default = master_locs[base_idx][name] lower = min(m[name] for m in master_locs) upper = max(m[name] for m in master_locs) if default == lower == upper: continue axes_dict[name] = (lower, default, upper) log.info("Axes:\n%s", pformat(axes_dict)) assert all(name in axis_map for name in axes_dict.keys()) 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_dict, instances, axis_map) # Normalize master locations master_locs = [models.normalizeLocation(m, axes_dict) for m in master_locs] log.info("Normalized master locations:\n%s", pformat(master_locs)) # TODO Clean this up. del instances del axes_dict 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
def interpolate_layout(designspace_filename, loc, finder): ds = designspace.load(designspace_filename) axes = ds['axes'] if 'axes' in ds else [] if 'sources' not in ds or not ds['sources']: raise VarLibError("no 'sources' defined in .designspace") masters = ds['sources'] 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 variable font") print("Loading master fonts") 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_names = set(master_locs[0].keys()) assert all(axis_names == set(m.keys()) for m in master_locs) # Set up axes axes_dict = {} if axes: # the designspace file loaded had an <axes> element for axis in axes: default = axis['default'] lower = axis['minimum'] upper = axis['maximum'] name = axis['name'] axes_dict[name] = (lower, default, upper) else: for tag in axis_names: 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_dict[tag] = (lower, default, upper) print("Axes:") pprint(axes_dict) print("Location:", loc) print("Master locations:") pprint(master_locs) # Normalize locations loc = models.normalizeLocation(loc, axes_dict) master_locs = [models.normalizeLocation(m, axes_dict) 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") merger.mergeTables(font, master_fonts, axes_dict, base_idx, ['GPOS']) return font