def test_default_groups_only2(data_dir, caplog): """Test that the group difference warning is not triggered if non-default source groups are empty.""" d = designspaceLib.DesignSpaceDocument() d.addAxisDescriptor(name="Weight", tag="wght", minimum=300, default=300, maximum=900) d.addSourceDescriptor(location={"Weight": 300}, font=ufoLib2.Font()) d.addSourceDescriptor(location={"Weight": 900}, font=ufoLib2.Font()) d.addInstanceDescriptor(styleName="2", location={"Weight": 400}) d.findDefault() d.sources[0].font.groups["public.kern1.GRK_alpha_alt_LC_1ST"] = [ "alpha.alt", "alphatonos.alt", ] generator = fontmake.instantiator.Instantiator.from_designspace(d) assert "contains different groups than the default source" not in caplog.text instance = generator.generate_instance(d.instances[0]) assert instance.groups == { "public.kern1.GRK_alpha_alt_LC_1ST": ["alpha.alt", "alphatonos.alt"] }
def _ufo(config: FontConfig) -> ufoLib2.Font: ufo = ufoLib2.Font() ufo.info.familyName = config.family # set various font metadata; see the full list of fontinfo attributes at # https://unifiedfontobject.org/versions/ufo3/fontinfo.plist/#generic-dimension-information ufo.info.unitsPerEm = config.upem # we just use a simple scheme that makes all sets of vertical metrics the same; # if one needs more fine-grained control they can fix up post build ufo.info.ascender = (ufo.info.openTypeHheaAscender ) = ufo.info.openTypeOS2TypoAscender = config.ascender ufo.info.descender = ( ufo.info.openTypeHheaDescender ) = ufo.info.openTypeOS2TypoDescender = config.descender ufo.info.openTypeHheaLineGap = ufo.info.openTypeOS2TypoLineGap = config.linegap # set USE_TYPO_METRICS flag (OS/2.fsSelection bit 7) to make sure OS/2 Typo* metrics # are preferred to define Windows line spacing over legacy WinAscent/WinDescent: # https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fsselection ufo.info.openTypeOS2Selection = [7] # version ufo.info.versionMajor = config.version_major ufo.info.versionMinor = config.version_minor # Must have .notdef and Win 10 Chrome likes a blank gid1 so make gid1 space ufo.newGlyph(".notdef") space = ufo.newGlyph(".space") space.unicodes = [0x0020] space.width = config.width ufo.glyphOrder = [".notdef", ".space"] # use 'post' format 3.0 for TTFs, shaving a kew KBs of unneeded glyph names ufo.lib[ufo2ft.constants.KEEP_GLYPH_NAMES] = config.keep_glyph_names return ufo
def main(args=None): import argparse parser = argparse.ArgumentParser( description= "Generates I matra glyphs and pres feature lookup in FEA syntax") parser.add_argument( 'threshold', type=int, nargs=2, help="Left and right threshold to find a match.", ) parser.add_argument("output", help="Output for FEA lookup.") parser.add_argument('ufos', nargs="+", help="UFOs to process.") options = parser.parse_args(args) fonts = [] for path in options.ufos: fonts.append(ufoLib2.Font(path)) matra_maker = MatraMaker(fonts, options.threshold[0], options.threshold[1]) matra_maker.generate_matra_variants(matra_maker.bucketed_matra_widths) for font in fonts: font.save() with open(options.output, "w") as fp: fp.write(matra_maker.feature_text(matra_maker.bucketed_matra_widths))
def test_constructor_from_path(datadir): path = datadir / "UbuTestData.ufo" font = ufoLib2.Font(path) assert font._path == path assert font._lazy is True assert font._validate is True assert font._reader is not None font2 = ufoLib2.Font(path, lazy=False, validate=False) assert font2._path == path assert font2._lazy is False assert font2._validate is False assert font2._reader is None assert font == font2
def test_LayerSet_load_layers_on_iteration(tmp_path): ufo = ufoLib2.Font() ufo.layers.newLayer("test") ufo_save_path = tmp_path / "test.ufo" ufo.save(ufo_save_path) ufo = ufoLib2.Font.open(ufo_save_path) assert set(ufo.layers.keys()) == {"public.default", "test"} for layer in ufo.layers: assert layer is not _NOT_LOADED
def test_build_GDEF_incomplete_glyphOrder(): import ufoLib2 font = ufoLib2.Font() font.lib["public.glyphOrder"] = ["b", "c"] for name in ("d", "c", "b", "a"): glyph = font.newGlyph(name) glyph.appendAnchor({"name": "top", "x": 0, "y": 0}) assert "[b c a d], # Base" in _build_gdef(font)
def _font_to_quadratic(input_path, output_path=None, **kwargs): ufo = ufo_module.Font(input_path) logger.info('Converting curves for %s', input_path) if font_to_quadratic(ufo, **kwargs): logger.info("Saving %s", output_path) if output_path: ufo.save(output_path) else: ufo.save() # save in-place elif output_path: _copytree(input_path, output_path)
def empty_UFO(style_name: str) -> ufoLib2.Font: ufo = ufoLib2.Font() ufo.info.familyName = "Test" ufo.info.styleName = style_name ufo.info.unitsPerEm = 1000 ufo.info.ascender = 800 ufo.info.descender = -200 ufo.info.xHeight = 500 ufo.info.capHeight = 700 ufo.info.postscriptUnderlineThickness = 50 ufo.info.postscriptUnderlinePosition = -75 g = ufo.newGlyph("a") g.width = 500 return ufo
def _ufo(family, upem): ufo = ufoLib2.Font() ufo.info.familyName = family # set various font metadata; see the full list of fontinfo attributes at # http://unifiedfontobject.org/versions/ufo3/fontinfo.plist/ ufo.info.unitsPerEm = upem # Must have .notdef and Win 10 Chrome likes a blank gid1 so make gid1 space ufo.newGlyph(".notdef") space = ufo.newGlyph(".space") space.unicodes = [0x0020] space.width = upem ufo.glyphOrder = [".notdef", ".space"] return ufo
def __init__(self, filename, features): self._font = font = ufoLib2.Font(validate=False) parser = SFDParser(filename, font, ignore_uvs=False, ufo_anchors=False, ufo_kerning=False, minimal=True) parser.parse() if features: preprocessor = Preprocessor() for d in ("italic", "sans", "display", "math"): if d in filename.lower(): preprocessor.define(d.upper()) with open(features) as f: preprocessor.parse(f) feafile = StringIO() preprocessor.write(feafile) feafile.write(font.features.text) font.features.text = feafile.getvalue()
def _ufo(family: str, upem: int, keep_glyph_names: bool = False) -> ufoLib2.Font: ufo = ufoLib2.Font() ufo.info.familyName = family # set various font metadata; see the full list of fontinfo attributes at # http://unifiedfontobject.org/versions/ufo3/fontinfo.plist/ ufo.info.unitsPerEm = upem # Must have .notdef and Win 10 Chrome likes a blank gid1 so make gid1 space ufo.newGlyph(".notdef") space = ufo.newGlyph(".space") space.unicodes = [0x0020] space.width = upem ufo.glyphOrder = [".notdef", ".space"] # use 'post' format 3.0 for TTFs, shaving a kew KBs of unneeded glyph names ufo.lib[ufo2ft.constants.KEEP_GLYPH_NAMES] = keep_glyph_names return ufo
def test_guidelines(): font = ufoLib2.Font() # accept either a mapping or a Guideline object font.appendGuideline({"x": 100, "y": 50, "angle": 315}) font.appendGuideline(ufoLib2.objects.Guideline(x=30)) assert len(font.guidelines) == 2 assert font.guidelines == [ ufoLib2.objects.Guideline(x=100, y=50, angle=315), ufoLib2.objects.Guideline(x=30), ] # setter should clear existing guidelines font.guidelines = [{"x": 100}, ufoLib2.objects.Guideline(y=20)] assert len(font.guidelines) == 2 assert font.guidelines == [ ufoLib2.objects.Guideline(x=100), ufoLib2.objects.Guideline(y=20), ]
def _ufo(config): ufo = ufoLib2.Font() ufo.info.unitsPerEm = config.upem ufo.info.ascender = config.ascender ufo.info.descender = config.descender return ufo
def open(self, path, font=None): if font is None: font = Font() ufo = ufoLib2.Font(path) # font info = ufo.info if info.openTypeHeadCreated: try: font.date = datetime.strptime( info.openTypeHeadCreated, "%Y/%m/%d %H:%M:%S") except ValueError: pass if info.familyName: font.familyName = info.familyName if info.copyright: font.copyright = info.copyright if info.openTypeNameDesigner: font.designer = info.openTypeNameDesigner if info.openTypeNameDesignerURL: font.designerURL = info.openTypeNameDesignerURL if info.openTypeNameManufacturer: font.manufacturer = info.openTypeNameManufacturer if info.openTypeNameManufacturerURL: font.manufacturerURL = info.openTypeNameManufacturerURL if info.unitsPerEm: font.unitsPerEm = info.unitsPerEm if info.versionMajor: font.versionMajor = info.versionMajor if info.versionMinor: font.versionMinor = info.versionMinor if ufo.lib: font._extraData = ufo.lib # features if ufo.features: font.featureHeaders.append(FeatureHeader("fea", ufo.features)) # master master = font.selectedMaster if info.styleName: master.name = info.styleName for blues in (info.postscriptBlueValues, info.postscriptOtherBlues): for yMin, yMax in zip(blues[::2], blues[1::2]): master.alignmentZones.append(AlignmentZone(yMin, yMax-yMin)) if info.postscriptStemSnapH: master.hStems = info.postscriptStemSnapH if info.postscriptStemSnapV: master.vStems = info.postscriptStemSnapV for g in ufo.guidelines: guideline = Guideline() if g.x: guideline.x = g.x if g.y: guideline.y = g.y if g.angle: guideline.angle = g.angle if g.name: guideline.name = g.name # ufo color and identifier are skipped master.guidelines.append(guideline) # note: unlike ufo, we store kerning in visual order. hard to convert # between the two (given that ltr and rtl pairs can be mixed) if ufo.kerning: master.hKerning = ufo.kerning if info.ascender: master.ascender = info.ascender if info.capHeight: master.capHeight = info.capHeight if info.descender: master.descender = info.descender if info.italicAngle: master.italicAngle = info.italicAngle if info.xHeight: master.xHeight = info.xHeight # glyphs font._glyphs.clear() glyphs = font.glyphs for g in ufo: glyph = Glyph(g.name) glyphs.append(glyph) if g.unicodes: glyph.unicodes = ["%04X" % uniValue for uniValue in g.unicodes] # TODO assign kerning groups # layer layer = glyph.layerForMaster(None) layer.width = g.width layer.height = g.height lib = g.lib vertOrigin = lib.pop("public.verticalOrigin", None) if vertOrigin: layer.yOrigin = vertOrigin if lib: layer._extraData = lib # anchors anchors = layer.anchors for a in g.anchors: if not a.name: continue anchors[a.name] = Anchor(a.x or 0, a.y or 0) # ufo color and identifier are skipped # components components = layer.components for c in g.components: component = Component(c.baseGlyph) if c.transformation: component.transformation = Transformation( *tuple(c.transformation)) # ufo identifier is skipped components.append(component) # guidelines guidelines = layer.guidelines for g_ in g.guidelines: guideline = Guideline(g_.x or 0, g_.y or 0, g_.angle or 0) if g_.name: guideline.name = g_.name # ufo color and identifier are skipped guidelines.append(guideline) # paths paths = layer.paths for c in self.unstructure(g.contours): pts = c.pop("_points") for p in pts: name = p.pop("name", None) ident = p.pop("identifier", None) if name or ident: p["extraData"] = d = {} if name: d["name"] = name if ident: d["id"] = ident while pts[-1]["type"] is None: pts.insert(0, pts.pop()) c["points"] = pts ident = c.pop("identifier", None) if ident: c["id"] = ident path = self.structure(c, Path) paths.append(path) glyph._lastModified = None return font
def generate_instance( self, instance: designspaceLib.InstanceDescriptor) -> ufoLib2.Font: """Generate an interpolated instance font object for an InstanceDescriptor.""" if anisotropic(instance.location): raise InstantiatorError( f"Instance {instance.familyName}-" f"{instance.styleName}: Anisotropic location " f"{instance.location} not supported by varLib.") font = ufoLib2.Font() # Instances may leave out locations that match the default source, so merge # default location with the instance's location. location = {**self.default_design_location, **instance.location} location_normalized = varLib.models.normalizeLocation( location, self.axis_bounds) # Kerning kerning_instance = self.kerning_mutator.instance_at( location_normalized) if self.round_geometry: kerning_instance.round() kerning_instance.extractKerning(font) # Info self._generate_instance_info(instance, location_normalized, location, font) # Non-kerning groups. Kerning groups have been taken care of by the kerning # instance. for key, glyph_names in self.copy_nonkerning_groups.items(): font.groups[key] = [name for name in glyph_names] # Features font.features.text = self.copy_feature_text # Lib # 1. Copy the default lib to the instance. font.lib = typing.cast(dict, copy.deepcopy(self.copy_lib)) # 2. Copy the Designspace's skipExportGlyphs list over to the UFO to # make sure it wins over the default UFO one. font.lib["public.skipExportGlyphs"] = [ name for name in self.skip_export_glyphs ] # 3. Write _design_ location to instance's lib. font.lib["designspace.location"] = [loc for loc in location.items()] # Glyphs for glyph_name, glyph_mutator in self.glyph_mutators.items(): glyph = font.newGlyph(glyph_name) try: glyph_instance = glyph_mutator.instance_at(location_normalized) if self.round_geometry: glyph_instance = glyph_instance.round() # onlyGeometry=True does not set name and unicodes, in ufoLib2 we can't # modify a glyph's name. Copy unicodes from default font. glyph_instance.extractGlyph(glyph, onlyGeometry=True) except Exception as e: # TODO: Figure out what exceptions fontMath/varLib can throw. # By default, explode if we cannot generate a glyph instance for # whatever reason (usually outline incompatibility)... if glyph_name not in self.skip_export_glyphs: raise InstantiatorError( f"Failed to generate instance of glyph '{glyph_name}'." ) from e # ...except if the glyph is in public.skipExportGlyphs and would # therefore be removed from the compiled font anyway. There's not much # we can do except leave it empty in the instance and tell the user. logger.warning( "Failed to generate instance of glyph '%s', which is marked as " "non-exportable. Glyph will be left empty. Failure reason: %s", glyph_name, e, ) glyph.unicodes = [ uv for uv in self.glyph_name_to_unicodes[glyph_name] ] # Process rules glyph_names_list = self.glyph_mutators.keys() glyph_names_list_renamed = designspaceLib.processRules( self.designspace_rules, location, glyph_names_list) for name_old, name_new in zip(glyph_names_list, glyph_names_list_renamed): if name_old != name_new: swap_glyph_names(font, name_old, name_new) return font
def main(args=None): """Convert a UFO font from cubic to quadratic curves""" parser = argparse.ArgumentParser(prog="cu2qu") parser.add_argument("--version", action="version", version=fontTools.__version__) parser.add_argument("infiles", nargs="+", metavar="INPUT", help="one or more input UFO source file(s).") parser.add_argument("-v", "--verbose", action="count", default=0) parser.add_argument( "-e", "--conversion-error", type=float, metavar="ERROR", default=None, help="maxiumum approximation error measured in EM (default: 0.001)") parser.add_argument("--keep-direction", dest="reverse_direction", action="store_false", help="do not reverse the contour direction") mode_parser = parser.add_mutually_exclusive_group() mode_parser.add_argument( "-i", "--interpolatable", action="store_true", help="whether curve conversion should keep interpolation compatibility" ) mode_parser.add_argument( "-j", "--jobs", type=int, nargs="?", default=1, const=_cpu_count(), metavar="N", help="Convert using N multiple processes (default: %(default)s)") output_parser = parser.add_mutually_exclusive_group() output_parser.add_argument( "-o", "--output-file", default=None, metavar="OUTPUT", help=("output filename for the converted UFO. By default fonts are " "modified in place. This only works with a single input.")) output_parser.add_argument( "-d", "--output-dir", default=None, metavar="DIRECTORY", help="output directory where to save converted UFOs") options = parser.parse_args(args) if ufo_module is None: parser.error( "Either ufoLib2 or defcon are required to run this script.") if not options.verbose: level = "WARNING" elif options.verbose == 1: level = "INFO" else: level = "DEBUG" logging.basicConfig(level=level) if len(options.infiles) > 1 and options.output_file: parser.error("-o/--output-file can't be used with multile inputs") if options.output_dir: output_dir = options.output_dir if not os.path.exists(output_dir): os.mkdir(output_dir) elif not os.path.isdir(output_dir): parser.error("'%s' is not a directory" % output_dir) output_paths = [ os.path.join(output_dir, os.path.basename(p)) for p in options.infiles ] elif options.output_file: output_paths = [options.output_file] else: # save in-place output_paths = [None] * len(options.infiles) kwargs = dict(dump_stats=options.verbose > 0, max_err_em=options.conversion_error, reverse_direction=options.reverse_direction) if options.interpolatable: logger.info('Converting curves compatibly') ufos = [ufo_module.Font(infile) for infile in options.infiles] if fonts_to_quadratic(ufos, **kwargs): for ufo, output_path in zip(ufos, output_paths): logger.info("Saving %s", output_path) if output_path: ufo.save(output_path) else: ufo.save() else: for input_path, output_path in zip(options.infiles, output_paths): if output_path: _copytree(input_path, output_path) else: jobs = min(len(options.infiles), options.jobs) if options.jobs > 1 else 1 if jobs > 1: func = partial(_font_to_quadratic, **kwargs) logger.info('Running %d parallel processes', jobs) with closing(mp.Pool(jobs)) as pool: pool.starmap(func, zip(options.infiles, output_paths)) else: for input_path, output_path in zip(options.infiles, output_paths): _font_to_quadratic(input_path, output_path, **kwargs)
def _ufo(upem): ufo = ufoLib2.Font() ufo.info.unitsPerEm = upem return ufo
def open_ufo(path): if hasattr(ufo_module.Font, "open"): # ufoLib2 return ufo_module.Font.open(path) return ufo_module.Font(path) # defcon