diff --git a/.gitignore b/.gitignore index db4561ea..5616577a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ var/ *.egg-info/ .installed.cfg *.egg +*.eggs # PyInstaller # Usually these files are written by a python script from a template @@ -52,3 +53,6 @@ docs/_build/ # PyBuilder target/ + +# Visual Studio Code +.vscode/ diff --git a/examples/SourceSansVariable-LICENSE.md b/examples/SourceSansVariable-LICENSE.md new file mode 100644 index 00000000..3079b6db --- /dev/null +++ b/examples/SourceSansVariable-LICENSE.md @@ -0,0 +1,93 @@ +Copyright 2010-2018 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/SourceSansVariable-Roman.otf b/examples/SourceSansVariable-Roman.otf new file mode 100644 index 00000000..326ad126 Binary files /dev/null and b/examples/SourceSansVariable-Roman.otf differ diff --git a/examples/hello-vf.py b/examples/hello-vf.py new file mode 100644 index 00000000..cac79ff8 --- /dev/null +++ b/examples/hello-vf.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# +# FreeType high-level python API - Copyright 2011-2015 Nicolas P. Rougier +# Distributed under the terms of the new BSD license. +# +# ----------------------------------------------------------------------------- +from freetype import * + +if __name__ == '__main__': + import numpy + import matplotlib.pyplot as plt + + face = Face('./SourceSansVariable-Roman.otf') + text = 'Hello variable fonts!' + face.set_char_size( 48*64 ) + slot = face.glyph + face.set_var_design_coords((900,)) # set to widest for width calc. + + # First pass to compute bbox + width, height, baseline = 0, 0, 0 + previous = 0 + for i,c in enumerate(text): + face.load_char(c) + bitmap = slot.bitmap + height = max(height, + bitmap.rows + max(0,-(slot.bitmap_top-bitmap.rows))) + baseline = max(baseline, max(0,-(slot.bitmap_top-bitmap.rows))) + kerning = face.get_kerning(previous, c) + width += (slot.advance.x >> 6) + (kerning.x >> 6) + previous = c + + height *= 12 + + Z = numpy.zeros((height,width), dtype=numpy.ubyte) + + # Second pass for actual rendering. Iterate through some weight axis values + # and render the result, one per line. + for i, wght in enumerate((900, 750, 675, 600, 500, 450, 325, 275, 100)): + face.set_var_design_coords((wght,)) # set the weight axis value using "design coords" + x, y = 0, 0 + previous = 0 + for c in text: + face.load_char(c) + bitmap = slot.bitmap + top = slot.bitmap_top + left = slot.bitmap_left + w,h = bitmap.width, bitmap.rows + y = (height - baseline - top) - (i * 48) + kerning = face.get_kerning(previous, c) + x += (kerning.x >> 6) + Z[y:y+h,x:x+w] += numpy.array(bitmap.buffer, dtype='ubyte').reshape(h,w) + x += (slot.advance.x >> 6) + previous = c + + plt.figure(figsize=(10, 10*Z.shape[0]/float(Z.shape[1]))) + plt.imshow(Z, interpolation='nearest', origin='upper', cmap=plt.cm.gray) + plt.xticks([]), plt.yticks([]) + plt.show() diff --git a/freetype/__init__.py b/freetype/__init__.py index 7d765f33..f7da2d1e 100644 --- a/freetype/__init__.py +++ b/freetype/__init__.py @@ -18,6 +18,8 @@ import io import sys from ctypes import * +import ctypes.util +import struct from freetype.raw import * @@ -25,6 +27,13 @@ PY3 = sys.version_info[0] == 3 if PY3: unicode = str + +def unmake_tag(i): + # roughly opposite of FT_MAKE_TAG, converts 32-bit int to Python string + # could do with .to_bytes if limited to Python 3.2 or higher... + b = struct.pack('>I', i) + return b.decode('ascii', errors='replace') + _handle = None @@ -166,6 +175,28 @@ def _encode_filename(filename): Matrix = FT_Matrix +# ----------------------------------------------------------------------------- +# Handling for FT_Done_MM_Var, which was added in FreeType 2.9. Prior to that, +# we need to import libc and use libc free on the memory allocated for the +# FT_MM_Var data structure. See Face.get_variation_info(). +# ----------------------------------------------------------------------------- +if version() < (2,9,1): + if platform.system() == "Windows": + libcpath = ctypes.util.find_library("msvcrt") + else: + libcpath = ctypes.util.find_library("c") + libc = CDLL(libcpath) + libc.free.argtypes = [c_void_p] + libc.free.restype = None + + def FT_Done_MM_Var_func(p): + libc.free(p) +else: + def FT_Done_MM_Var_func(p): + error = FT_Done_MM_Var(get_handle(), p) + if error: + raise FT_Exception("Failure calling FT_Done_MM_Var") + # ----------------------------------------------------------------------------- class BBox( object ): @@ -1070,6 +1101,7 @@ def __init__( self, path_or_stream, index = 0 ): raise FT_Exception(error) self._index = index self._FT_Face = face + self._name_strings = dict() def _init_from_file(self, library, face, index, path): u_filename = c_char_p(_encode_filename(path)) @@ -1083,6 +1115,19 @@ def _init_from_memory(self, library, face, index, byte_stream): self._filebodys.append(byte_stream) # prevent gc return error + def _init_name_string_map(self): + # build map of (nID, pID, eID, lID) keys to name string bytes + self._name_strings = dict() + + for nidx in range(self._get_sfnt_name_count()): + namerec = self.get_sfnt_name(nidx) + nk = (namerec.name_id, + namerec.platform_id, + namerec.encoding_id, + namerec.language_id) + + self._name_strings[nk] = namerec.string + @classmethod def from_bytes(cls, bytes_, index=0): return cls(io.BytesIO(bytes_), index) @@ -1512,6 +1557,169 @@ def get_sfnt_name( self, index ): if error: raise FT_Exception( error ) return SfntName( name ) + def get_best_name_string(self, nameID, default_string='', preferred_order=None): + ''' + Retrieve a name string given nameID. Searches available font names + matching nameID and returns the decoded bytes of the best match. + "Best" is defined as a preferred list of platform/encoding/languageIDs + which can be overridden by supplying a preferred_order matching the + scheme of 'sort_order' (see below). + + The routine will attempt to decode the string's bytes to a Python str, when the + platform/encoding[/langID] are known (Windows, Mac, or Unicode platforms). + + If you prefer more control over name string selection and decoding than + this routine provides: + - call self._init_name_string_map() + - use (nameID, platformID, encodingID, languageID) as a key into + the self._name_strings dict + ''' + if not(self._name_strings): + self._init_name_string_map() + + sort_order = preferred_order or ( + (3, 1, 1033), # Microsoft/Windows/US English + (1, 0, 0), # Mac/Roman/English + (0, 6, 0), # Unicode/SMP/* + (0, 4, 0), # Unicode/SMP/* + (0, 3, 0), # Unicode/BMP/* + (0, 2, 0), # Unicode/10646-BMP/* + (0, 1, 0), # Unicode/1.1/* + ) + + # get all keys matching nameID + keys_present = [k for k in self._name_strings.keys() if k[0] == nameID] + + if keys_present: + # sort found keys by sort_order + key_order = {k: v for v, k in enumerate(sort_order)} + keys_present.sort(key=lambda x: key_order.get(x[1:4])) + best_key = keys_present[0] + nsbytes = self._name_strings[best_key] + + if best_key[1:3] == (3, 1) or best_key[1] == 0: + enc = "utf-16-be" + elif best_key[1:4] == (1, 0, 0): + enc = "mac-roman" + else: + enc = "unicode_escape" + + ns = nsbytes.decode(enc) + + else: + ns = default_string + + return ns + + def get_variation_info(self): + ''' + Retrieves variation space information for the current face. + ''' + if version() < (2, 8, 1): + raise NotImplementedError("freetype-py VF support requires FreeType 2.8.1 or later") + + p_amaster = pointer(FT_MM_Var()) + error = FT_Get_MM_Var(self._FT_Face, byref(p_amaster)) + + if error: + raise FT_Exception(error) + + vsi = VariationSpaceInfo(self, p_amaster) + + FT_Done_MM_Var_func(p_amaster) + + return vsi + + def get_var_blend_coords(self): + ''' + Get the current blend coordinates (-1.0..+1.0) + ''' + vsi = self.get_variation_info() + num_coords = len(vsi.axes) + ft_coords = (FT_Fixed * num_coords)() + error = FT_Get_Var_Blend_Coordinates(self._FT_Face, num_coords, byref(ft_coords)) + + if error: + raise FT_Exception(error) + + coords = tuple([ft_coords[ai]/65536.0 for ai in range(num_coords)]) + + return coords + + def set_var_blend_coords(self, coords, reset=False): + ''' + Set blend coords. Using reset=True will set all axes to + their default coordinates. + ''' + if reset: + error = FT_Set_Var_Blend_Coordinates(self._FT_Face, 0, 0) + else: + num_coords = len(coords) + ft_coords = [int(round(c * 65536.0)) for c in coords] + coords_array = (FT_Fixed * num_coords)(*ft_coords) + error = FT_Set_Var_Blend_Coordinates(self._FT_Face, num_coords, byref(coords_array)) + + if error: + raise FT_Exception(error) + + def get_var_design_coords(self): + ''' + Get the current design coordinates + ''' + vsi = self.get_variation_info() + num_coords = len(vsi.axes) + ft_coords = (FT_Fixed * num_coords)() + error = FT_Get_Var_Design_Coordinates(self._FT_Face, num_coords, byref(ft_coords)) + + if error: + raise FT_Exception(error) + + coords = tuple([ft_coords[ai]/65536.0 for ai in range(num_coords)]) + + return coords + + def set_var_design_coords(self, coords, reset=False): + ''' + Set design coords. Using reset=True will set all axes to + their default coordinates. + ''' + if reset: + error = FT_Set_Var_Design_Coordinates(self._FT_Face, 0, 0) + + else: + num_coords = len(coords) + ft_coords = [int(round(c * 65536.0)) for c in coords] + coords_array = (FT_Fixed * num_coords)(*ft_coords) + error = FT_Set_Var_Design_Coordinates(self._FT_Face, num_coords, byref(coords_array)) + + if error: + raise FT_Exception(error) + + def set_var_named_instance(self, instance_name): + ''' + Set instance by name. This will work with any FreeType with variable support + (for our purposes: v2.8.1 or later). If the actual FT_Set_Named_Instance() + function is available (v2.9.1 or later), we use it (which, despite what you might + expect from its name, sets instances by *index*). Otherwise we just use the coords + of the named instance (if found) and call self.set_var_design_coords. + ''' + have_func = freetype.version() >= (2, 9, 1) + vsi = self.get_variation_info() + + for inst_idx, inst in enumerate(vsi.instances, start=1): + if inst.name == instance_name: + if have_func: + error = FT_Set_Named_Instance(self._FT_Face, inst_idx) + else: + error = self.set_var_design_coords(inst.coords) + + if error: + raise FT_Exception(error) + + break + + # named instance not found; do nothing + def _get_postscript_name( self ): return FT_Get_Postscript_Name( self._FT_Face ) postscript_name = property( _get_postscript_name, @@ -2048,3 +2256,65 @@ def export( self, outline ): :param outline: The target outline. ''' FT_Stroker_Export( self._FT_Stroker, outline._FT_Outline ) + + +# ----------------------------------------------------------------------------- +# Classes related to Variable Font support +# +class VariationAxis(object): + tag = None + coords = tuple() + + def __init__(self, ftvaraxis): + self.tag = unmake_tag(ftvaraxis.tag) + self.name = ftvaraxis.name.decode('ascii') + self.minimum = ftvaraxis.minimum/65536.0 + self.default = ftvaraxis.default/65536.0 + self.maximum = ftvaraxis.maximum/65536.0 + self.strid = ftvaraxis.strid # do we need this? Should be same as 'name'... + + def __repr__(self): + return "".format( + self.tag, self.name, self.minimum, self.default, self.maximum) + +class VariationInstance(object): + def __init__(self, name, psname, coords): + self.name = name + self.psname = psname + self.coords = coords + + def __repr__(self): + return "".format( + self.name, self.coords) + +class VariationSpaceInfo(object): + """ + VF info (axes & instances). + """ + def __init__(self, face, p_ftmmvar): + """ + Build a VariationSpaceInfo object given face (freetype.Face) and + p_ftmmvar (pointer to FT_MM_Var). + """ + ftmv = p_ftmmvar.contents + + axes = [] + for axidx in range(ftmv.num_axis): + axes.append(VariationAxis(ftmv.axis[axidx])) + + self.axes = tuple(axes) + + inst = [] + for instidx in range(ftmv.num_namedstyles): + instinfo = ftmv.namedstyle[instidx] + nid = instinfo.strid + name = face.get_best_name_string(nid) + psid = instinfo.psid + psname = face.get_best_name_string(psid) + coords = [] + for cidx in range(len(self.axes)): + coords.append(instinfo.coords[cidx]/65536.0) + + inst.append(VariationInstance(name, psname, tuple(coords))) + + self.instances = tuple(inst) diff --git a/freetype/ft_structs.py b/freetype/ft_structs.py index 3fd0a0df..14c91786 100644 --- a/freetype/ft_structs.py +++ b/freetype/ft_structs.py @@ -983,3 +983,48 @@ class FT_BitmapGlyphRec(Structure): ('bitmap', FT_Bitmap) ] FT_BitmapGlyph = POINTER(FT_BitmapGlyphRec) + + +# ----------------------------------------------------------------------------- +# Structures related to variable fonts as used in the various VF routines. +# +class FT_Var_Axis(Structure): + ''' + A structure to model a given axis in design space for Multiple Masters, + TrueType GX, and OpenType variation fonts. + ''' + _fields_ = [ + ('name', FT_String_p), + ('minimum', FT_Fixed), + ('default', FT_Fixed), + ('maximum', FT_Fixed), + ('tag', FT_ULong), + ('strid', FT_UInt) + ] + + +class FT_Var_Named_Style(Structure): + ''' + A structure to model a named instance in a TrueType GX or OpenType + variation font. + ''' + _fields_ = [ + ('coords', POINTER(FT_Fixed)), + ('strid', FT_UInt), + ('psid', FT_UInt) + ] + + +class FT_MM_Var(Structure): + ''' + A structure to model the axes and space of an Adobe MM, TrueType GX, + or OpenType variation font. + Some fields are specific to one format and not to the others. + ''' + _fields_ = [ + ('num_axis', FT_UInt), + ('num_designs', FT_UInt), + ('num_namedstyles', FT_UInt), + ('axis', POINTER(FT_Var_Axis)), + ('namedstyle', POINTER(FT_Var_Named_Style)) + ] diff --git a/freetype/raw.py b/freetype/raw.py index 2e324d80..ff3bea33 100644 --- a/freetype/raw.py +++ b/freetype/raw.py @@ -166,6 +166,29 @@ def FT_Library_SetLcdFilter (*args, **kwargs): pass +# Routines for variable font support. These were introduced at different +# points in FreeType's history (except for FT_Get_MM_Var which has been +# around for a long time). +FT_Get_MM_Var = _lib.FT_Get_MM_Var # v2.old + +# -- since 2.8.1 for sure (some 2.7.1 or possibly older, but to be safe, +# implementation should check FT version >= 2.8.1 +try: + FT_Get_Var_Axis_Flags = _lib.FT_Get_Var_Axis_Flags + FT_Get_Var_Blend_Coordinates = _lib.FT_Get_Var_Blend_Coordinates + FT_Get_Var_Design_Coordinates = _lib.FT_Get_Var_Design_Coordinates + FT_Set_Var_Blend_Coordinates = _lib.FT_Set_Var_Blend_Coordinates + FT_Set_Var_Design_Coordinates = _lib.FT_Set_Var_Design_Coordinates +except AttributeError: + pass + +# -- since v2.9; we can work around if these are not present. +try: + FT_Done_MM_Var = _lib.FT_Done_MM_Var + FT_Set_Named_Instance = _lib.FT_Set_Named_Instance +except AttributeError: + pass + # Wholesale import of 102 routines which can be reasonably expected # to be found in freetype 2.2.x onwards. Some of these might need # to be protected with try:/except AttributeError: in some freetype builds. @@ -208,7 +231,6 @@ def FT_Library_SetLcdFilter (*args, **kwargs): FT_Get_BDF_Property = _lib.FT_Get_BDF_Property except AttributeError: pass -FT_Get_MM_Var = _lib.FT_Get_MM_Var FT_Get_Module = _lib.FT_Get_Module FT_Get_Multi_Master = _lib.FT_Get_Multi_Master FT_Get_PFR_Advance = _lib.FT_Get_PFR_Advance @@ -260,8 +282,6 @@ def FT_Library_SetLcdFilter (*args, **kwargs): FT_Set_MM_Blend_Coordinates = _lib.FT_Set_MM_Blend_Coordinates FT_Set_MM_Design_Coordinates = _lib.FT_Set_MM_Design_Coordinates FT_Set_Renderer = _lib.FT_Set_Renderer -FT_Set_Var_Blend_Coordinates = _lib.FT_Set_Var_Blend_Coordinates -FT_Set_Var_Design_Coordinates = _lib.FT_Set_Var_Design_Coordinates FT_Sfnt_Table_Info = _lib.FT_Sfnt_Table_Info FT_Sin = _lib.FT_Sin FT_Stream_OpenGzip = _lib.FT_Stream_OpenGzip diff --git a/tests/smoke_test.py b/tests/smoke_test.py index 3ae36364..e6a3c44a 100644 --- a/tests/smoke_test.py +++ b/tests/smoke_test.py @@ -4,18 +4,22 @@ import freetype import pytest +test_folder = os.path.realpath(os.path.dirname(__file__)) + def test_load_ft_face(): """A smoke test.""" - assert freetype.Face("../examples/Vera.ttf") + p = os.path.join(test_folder, "..", "examples", "Vera.ttf") + assert freetype.Face(p) def test_load_ft_face_from_memory(): """Another smoke test.""" - with open("../examples/Vera.ttf", mode="rb") as f: + p = os.path.join(test_folder, "..", "examples", "Vera.ttf") + with open(p, mode="rb") as f: assert freetype.Face(f) - with open("../examples/Vera.ttf", mode="rb") as f: + with open(p, mode="rb") as f: byte_stream = f.read() assert freetype.Face.from_bytes(byte_stream) @@ -25,7 +29,8 @@ def test_bundle_version(): shared_object = glob.glob(os.path.join(module_dir, "libfreetype*")) if shared_object: import re - with open("../setup-build-freetype.py") as f: + p = os.path.join(test_folder, "..", "setup-build-freetype.py") + with open(p) as f: m = re.findall(r"freetype-(\d+)\.(\d+)\.?(\d+)?\.tar", f.read()) version = m[0] if not version[2]: diff --git a/tests/vf_test.py b/tests/vf_test.py new file mode 100644 index 00000000..06f7fa53 --- /dev/null +++ b/tests/vf_test.py @@ -0,0 +1,92 @@ +import os + +import freetype +import pytest + +test_folder = os.path.realpath(os.path.dirname(__file__)) + +def _ft_ver(): + ft_ver_tuple = freetype.version() + ft_ver_str = ".".join(['{}'.format(i) for i in ft_ver_tuple]) + + return ft_ver_tuple, ft_ver_str + + +def test_variable_font_basics(): + """ + Check that basic VF functionality functions (open a VF font, get VF info) + """ + ft_vt, ft_vs = _ft_ver() + if ft_vt < (2, 8, 1): + # note: there is some proto-VF (Multiple Master) support in older + # FreeType versions, but not enough to work with here. + pytest.skip("Incomplete VF support in FreeType lib {} (need 2.8.1 or later)".format(ft_vs)) + return + + font_path = os.path.join( + test_folder, + "..", + "examples", + "SourceSansVariable-Roman.otf") + face = freetype.Face(font_path) + var_info = face.get_variation_info() + + assert len(var_info.axes) == 1 + assert var_info.axes[0].tag == 'wght' + + assert len(var_info.instances) == 6 + for ii, cv in enumerate((200, 300, 400, 600, 700, 900)): + assert var_info.instances[ii].coords == (cv,) + + +def test_vf_axis_get_set(): + ft_vt, ft_vs = _ft_ver() + if ft_vt < (2, 8, 1): + pytest.skip("Incomplete VF support in FreeType lib {} (need 2.8.1 or later)".format(ft_vs)) + return + + font_path = os.path.join( + test_folder, + "..", + "examples", + "SourceSansVariable-Roman.otf") + + face = freetype.Face(font_path) + + dc_in = (373,) + face.set_var_design_coords(dc_in) + dcoords = face.get_var_design_coords() + assert dcoords[0] == dc_in[0] + + bc_in = (-0.5,) + face.set_var_blend_coords(bc_in) + bcoords = face.get_var_blend_coords() + assert bcoords[0] == bc_in[0] + + # try 'reset to defaults' option + face.set_var_design_coords(None, reset=True) + dcoords = face.get_var_design_coords() + vsi = face.get_variation_info() + expected = tuple([vsi.axes[i].default for i in range(len(dcoords))]) + assert dcoords == expected + + +def test_vf_set_named_instance(): + ft_vt, ft_vs = _ft_ver() + if ft_vt < (2, 8, 1): + pytest.skip("Incomplete VF support in FreeType lib {} (need 2.8.1 or later)".format(ft_vs)) + return + + font_path = os.path.join( + test_folder, + "..", + "examples", + "SourceSansVariable-Roman.otf") + + face = freetype.Face(font_path) + vsi = face.get_variation_info() + + for inst in vsi.instances: + face.set_var_named_instance(inst.name) + dcoords = face.get_var_design_coords() + assert dcoords == inst.coords