diff --git a/.gitignore b/.gitignore index 679c9b957873..722a42ec6185 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ dist .eggs # tox testing tool .tox +setup.cfg # OS generated files # ###################### diff --git a/.travis.yml b/.travis.yml index c01cd8b337d2..47931f7680ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -75,7 +75,7 @@ install: # version since is it basically just a .ttf file # The current Travis Ubuntu image is to old to search .local/share/fonts so we store fonts in .fonts - # We install ipython to use the console highlighting. From IPython 3 this depends on jsonschema and misture. + # We install ipython to use the console highlighting. From IPython 3 this depends on jsonschema and mistune. # Neihter is installed as a dependency of IPython since they are not used by the IPython console. - | if [[ $BUILD_DOCS == true ]]; then @@ -89,7 +89,11 @@ install: cp tmp/usr/share/fonts/truetype/humor-sans/Humor-Sans.ttf ~/.fonts cp Felipa-Regular.ttf ~/.fonts fc-cache -f -v + else + # Use the special local version of freetype for testing + cp .travis/setup.cfg . fi; + - python setup.py install script: diff --git a/.travis/setup.cfg b/.travis/setup.cfg new file mode 100644 index 000000000000..61cdc102a0f8 --- /dev/null +++ b/.travis/setup.cfg @@ -0,0 +1,2 @@ +[test] +local_freetype=True \ No newline at end of file diff --git a/INSTALL b/INSTALL index 96dbae42b717..7a35f1df2eba 100644 --- a/INSTALL +++ b/INSTALL @@ -208,7 +208,7 @@ libpng 1.2 (or later) `pytz` Used to manipulate time-zone aware datetimes. -:term:`freetype` 2.3 or later +:term:`FreeType` 2.3 or later library for reading true type font files. ``cycler`` 0.9 or later diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst index ddce97568e71..3b8fb66e9872 100644 --- a/doc/devel/testing.rst +++ b/doc/devel/testing.rst @@ -33,6 +33,22 @@ Optionally you can install: - `pep8 `_ to test coding standards +Building matplotlib for image comparison tests +---------------------------------------------- + +matplotlib's test suite makes heavy use of image comparison tests, +meaning the result of a plot is compared against a known good result. +Unfortunately, different versions of FreeType produce differently +formed characters, causing these image comparisons to fail. To make +them reproducible, matplotlib can be built with a special local copy +of FreeType. This is recommended for all matplotlib developers. + +Add the following content to a ``setup.cfg`` file at the root of the +matplotlib source directory:: + + [test] + local_freetype = True + Running the tests ----------------- @@ -185,17 +201,6 @@ decorator: If some variation is expected in the image between runs, this value may be adjusted. -Freetype version ----------------- - -Due to subtle differences in the font rendering under different -version of freetype some care must be taken when generating the -baseline images. Currently (early 2015), almost all of the images -were generated using ``freetype 2.5.3-21`` on Fedora 21 and only the -fonts that ship with ``matplotlib`` (regenerated in PR #4031 / commit -005cfde02751d274f2ab8016eddd61c3b3828446) and travis is using -``freetype 2.4.8`` on ubuntu. - Known failing tests ------------------- diff --git a/doc/glossary/index.rst b/doc/glossary/index.rst index fe75339287d2..5f0b683f14cf 100644 --- a/doc/glossary/index.rst +++ b/doc/glossary/index.rst @@ -22,8 +22,8 @@ Glossary EPS Encapsulated Postscript (`EPS `_) - freetype - `freetype `_ is a font rasterization + FreeType + `FreeType `_ is a font rasterization library used by matplotlib which supports TrueType, Type 1, and OpenType fonts. diff --git a/doc/users/screenshots.rst b/doc/users/screenshots.rst index 57001fa763c1..90dafe5eba14 100644 --- a/doc/users/screenshots.rst +++ b/doc/users/screenshots.rst @@ -252,7 +252,7 @@ Mathtext_examples Below is a sampling of the many TeX expressions now supported by matplotlib's internal mathtext engine. The mathtext module provides TeX style mathematical -expressions using `freetype2 `_ +expressions using `FreeType `_ and the BaKoMa computer modern or `STIX `_ fonts. See the :mod:`matplotlib.mathtext` module for additional details. diff --git a/doc/users/text_intro.rst b/doc/users/text_intro.rst index 4f04c9360f28..67d504dcbd5c 100644 --- a/doc/users/text_intro.rst +++ b/doc/users/text_intro.rst @@ -8,7 +8,7 @@ expressions, truetype support for raster and vector outputs, newline separated text with arbitrary rotations, and unicode support. Because we embed the fonts directly in the output documents, e.g., for postscript or PDF, what you see on the screen is what you get in the hardcopy. -`freetype2 `_ support +`FreeType `_ support produces very nice, antialiased fonts, that look good even at small raster sizes. matplotlib includes its own :mod:`matplotlib.font_manager`, thanks to Paul Barrett, which diff --git a/examples/pylab_examples/font_table_ttf.py b/examples/pylab_examples/font_table_ttf.py index 8708ae502e67..a78d39716573 100755 --- a/examples/pylab_examples/font_table_ttf.py +++ b/examples/pylab_examples/font_table_ttf.py @@ -1,6 +1,6 @@ # -*- noplot -*- """ -matplotlib has support for freetype fonts. Here's a little example +matplotlib has support for FreeType fonts. Here's a little example using the 'table' command to build a font table that shows the glyphs by character code. diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index dbd2f56aa453..c81cc8106a53 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1499,6 +1499,18 @@ def _init_tests(): if not os.path.isdir(os.path.join(os.path.dirname(__file__), 'tests')): raise ImportError("matplotlib test data is not installed") + # The version of FreeType to install locally for running the + # tests. This must match the value in `setupext.py` + LOCAL_FREETYPE_VERSION = '2.6.1' + + from matplotlib import ft2font + if (ft2font.__freetype_version__ != LOCAL_FREETYPE_VERSION or + ft2font.__freetype_build_type__ != 'local'): + warnings.warn( + "matplotlib is not built with the correct FreeType version to run " + "tests. Set local_freetype=True in setup.cfg and rebuild. " + "Expect many image comparison failures below.") + try: import nose try: diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index 99440a655070..e569616dc75b 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -113,7 +113,7 @@ class MathtextBackend(object): - :meth:`render_rect_filled` - :meth:`get_results` - And optionally, if you need to use a Freetype hinting style: + And optionally, if you need to use a FreeType hinting style: - :meth:`get_hinting_type` """ @@ -150,7 +150,7 @@ def get_results(self, box): def get_hinting_type(self): """ - Get the Freetype hinting type to use with this particular + Get the FreeType hinting type to use with this particular backend. """ return LOAD_NO_HINTING diff --git a/matplotlibrc.template b/matplotlibrc.template index 7492fe7b7be4..f9bb84cb4f85 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -195,10 +195,10 @@ backend : %(backend)s #text.hinting : auto # May be one of the following: # 'none': Perform no hinting - # 'auto': Use freetype's autohinter + # 'auto': Use FreeType's autohinter # 'native': Use the hinting information in the # font file, if available, and if your - # freetype library supports it + # FreeType library supports it # 'either': Use the native hinting information, # or the autohinter if none is available. # For backward compatibility, this value may also be @@ -357,9 +357,9 @@ backend : %(backend)s #image.lut : 256 # the size of the colormap lookup table #image.origin : upper # lower | upper #image.resample : False -#image.composite_image : True # When True, all the images on a set of axes are - # combined into a single composite image before - # saving a figure as a vector graphics file, +#image.composite_image : True # When True, all the images on a set of axes are + # combined into a single composite image before + # saving a figure as a vector graphics file, # such as a PDF. ### CONTOUR PLOTS diff --git a/setup.cfg.template b/setup.cfg.template index 9d50b4441582..cae6f678e19f 100644 --- a/setup.cfg.template +++ b/setup.cfg.template @@ -8,6 +8,13 @@ # This can be a single directory or a comma-delimited list of directories. #basedirlist = /usr +[test] +# If you plan to develop matplotlib and run or add to the test suite, +# set this to True. It will download and build a specific version of +# FreeType, and then use that to build the ft2font extension. This +# ensures that test images are exactly reproducible. +#local_freetype = False + [status] # To suppress display of the dependencies and their versions # at the top of the build log, uncomment the following line: diff --git a/setup.py b/setup.py index e2a6393bc406..3e1393e8a6d3 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,7 @@ from distribute_setup import use_setuptools use_setuptools() from setuptools.command.test import test as TestCommand +from setuptools.command.build_ext import build_ext as BuildExtCommand import sys @@ -239,8 +240,19 @@ def run_tests(self): argv=['nosetests'] + self.test_args, exit=True) + +class BuildExtraLibraries(BuildExtCommand): + def run(self): + for package in good_packages: + package.do_custom_build() + + return BuildExtCommand.run(self) + + cmdclass = versioneer.get_cmdclass() cmdclass['test'] = NoseTestCommand +cmdclass['build_ext'] = BuildExtraLibraries + # One doesn't normally see `if __name__ == '__main__'` blocks in a setup.py, # however, this is needed on Windows to avoid creating infinite subprocesses @@ -303,8 +315,6 @@ def run_tests(self): # Now collect all of the information we need to build all of the # packages. for package in good_packages: - if isinstance(package, str): - continue packages.extend(package.get_packages()) namespace_packages.extend(package.get_namespace_packages()) py_modules.extend(package.get_py_modules()) diff --git a/setupext.py b/setupext.py index 2c3bd1fa8cc5..cdeb709389b1 100755 --- a/setupext.py +++ b/setupext.py @@ -20,6 +20,13 @@ PY3 = (sys.version_info[0] >= 3) +# This is the version of FreeType to use when building a local +# version. It must match the value in +# lib/matplotlib.__init__.py +LOCAL_FREETYPE_VERSION = '2.6.1' +# md5 hash of the freetype tarball +LOCAL_FREETYPE_HASH = '348e667d728c597360e4a87c16556597' + if sys.platform != 'win32': if sys.version_info[0] < 3: from commands import getstatusoutput @@ -47,22 +54,19 @@ config = configparser.SafeConfigParser() config.read(setup_cfg) - try: + if config.has_option('status', 'suppress'): options['display_status'] = not config.getboolean("status", "suppress") - except: - pass - try: + if config.has_option('rc_options', 'backend'): options['backend'] = config.get("rc_options", "backend") - except: - pass - try: + if config.has_option('directories', 'basedirlist'): options['basedirlist'] = [ x.strip() for x in config.get("directories", "basedirlist").split(',')] - except: - pass + + if config.has_option('test', 'local_freetype'): + options['local_freetype'] = config.get("test", "local_freetype") else: config = None @@ -219,6 +223,21 @@ def make_extension(name, files, *args, **kwargs): return ext +def get_file_hash(filename): + """ + Get the MD5 hash of a given filename. + """ + import hashlib + BLOCKSIZE = 1 << 16 + hasher = hashlib.md5() + with open(filename, 'rb') as fd: + buf = fd.read(BLOCKSIZE) + while len(buf) > 0: + hasher.update(buf) + buf = fd.read(BLOCKSIZE) + return hasher.hexdigest() + + class PkgConfig(object): """ This is a class for communicating with pkg-config. @@ -448,6 +467,14 @@ def _check_for_pkg_config(self, package, include_file, min_version=None, return 'version %s' % version + def do_custom_build(self): + """ + If a package needs to do extra custom things, such as building a + third-party library, before building an extension, it should + override this method. + """ + pass + class OptionalPackage(SetupPackage): optional = True @@ -461,10 +488,9 @@ def get_config(cls): if the package is at default state ("auto"), forced by the user (True) or opted-out (False). """ - try: - return config.getboolean(cls.config_category, cls.name) - except: - return "auto" + if config is not None and config.has_option(cls.config_category, cls.name): + return config.get(cls.config_category, cls.name) + return "auto" def check(self): """ @@ -872,6 +898,9 @@ class FreeType(SetupPackage): name = "freetype" def check(self): + if options.get('local_freetype'): + return "Using local version for testing" + if sys.platform == 'win32': check_include_file(get_include_dirs(), 'ft2build.h', 'freetype') return 'Using unknown version found on system.' @@ -917,15 +946,67 @@ def version_from_header(self): return '.'.join([major, minor, patch]) def add_flags(self, ext): - pkg_config.setup_extension( - ext, 'freetype2', - default_include_dirs=[ - 'include/freetype2', 'freetype2', - 'lib/freetype2/include', - 'lib/freetype2/include/freetype2'], - default_library_dirs=[ - 'freetype2/lib'], - default_libraries=['freetype', 'z']) + if options.get('local_freetype'): + src_path = os.path.join( + 'build', 'freetype-{0}'.format(LOCAL_FREETYPE_VERSION)) + # Statically link to the locally-built freetype. + # This is certainly broken on Windows. + ext.include_dirs.insert(0, os.path.join(src_path, 'include')) + ext.extra_objects.insert( + 0, os.path.join(src_path, 'objs', '.libs', 'libfreetype.a')) + ext.define_macros.append(('FREETYPE_BUILD_TYPE', 'local')) + else: + pkg_config.setup_extension( + ext, 'freetype2', + default_include_dirs=[ + 'include/freetype2', 'freetype2', + 'lib/freetype2/include', + 'lib/freetype2/include/freetype2'], + default_library_dirs=[ + 'freetype2/lib'], + default_libraries=['freetype', 'z']) + ext.define_macros.append(('FREETYPE_BUILD_TYPE', 'system')) + + def do_custom_build(self): + # We're using a system freetype + if not options.get('local_freetype'): + return + + src_path = os.path.join( + 'build', 'freetype-{0}'.format(LOCAL_FREETYPE_VERSION)) + + # We've already built freetype + if os.path.isfile(os.path.join(src_path, 'objs', '.libs', 'libfreetype.a')): + return + + tarball = 'freetype-{0}.tar.gz'.format(LOCAL_FREETYPE_VERSION) + tarball_path = os.path.join('build', tarball) + if not os.path.isfile(tarball_path): + tarball_url = 'http://download.savannah.gnu.org/releases/freetype/{0}'.format(tarball) + + print("Downloading {0}".format(tarball_url)) + if sys.version_info[0] == 2: + from urllib import urlretrieve + else: + from urllib.request import urlretrieve + + if not os.path.exists('build'): + os.makedirs('build') + urlretrieve(tarball_url, tarball_path) + + if get_file_hash(tarball_path) != LOCAL_FREETYPE_HASH: + raise IOError("{0} does not match expected hash.".format(tarball)) + + print("Building {0}".format(tarball)) + cflags = 'CFLAGS="{0} -fPIC" '.format(os.environ.get('CFLAGS', '')) + + subprocess.check_call( + ['tar', 'zxf', tarball], cwd='build') + subprocess.check_call( + [cflags + './configure --with-zlib=no --with-bzip2=no ' + '--with-png=no --with-harfbuzz=no'], shell=True, cwd=src_path) + subprocess.check_call( + [cflags + 'make'], shell=True, cwd=src_path) class FT2Font(SetupPackage): diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 13e843b81d93..b15a89ce1d92 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -25,8 +25,8 @@ transform is placed on the font to shrink it back to the desired size. While it is a bit surprising that the dpi setting affects hinting, whereas the global transform does not, this is documented - behavior of freetype, and therefore hopefully unlikely to change. - The freetype 2 tutorial says: + behavior of FreeType, and therefore hopefully unlikely to change. + The FreeType 2 tutorial says: NOTE: The transformation is applied to every glyph that is loaded through FT_Load_Glyph and is completely independent of @@ -511,6 +511,10 @@ FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_) : image(), face( throw "Could not set the fontsize"; } + if (open_args.stream != NULL) { + face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM; + } + static FT_Matrix transform = { 65536 / hinting_factor, 0, 0, 65536 }; FT_Set_Transform(face, &transform, 0); } diff --git a/src/ft2font.h b/src/ft2font.h index ee3e6e166db6..793b0e507f4a 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -1,6 +1,6 @@ /* -*- mode: c++; c-basic-offset: 4 -*- */ -/* A python interface to freetype2 */ +/* A python interface to FreeType */ #ifndef _FT2FONT_H #define _FT2FONT_H #include @@ -21,7 +21,7 @@ extern "C" { #define FIXED_MAJOR(val) (long)((val & 0xffff000) >> 16) #define FIXED_MINOR(val) (long)(val & 0xffff) -// the freetype string rendered into a width, height buffer +// the FreeType string rendered into a width, height buffer class FT2Image { public: diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 9bcb2e73d3a5..a97de686b242 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -7,6 +7,9 @@ // From Python #include +#define STRINGIFY(s) XSTRINGIFY(s) +#define XSTRINGIFY(s) #s + static PyObject *convert_xys_to_array(std::vector &xys) { npy_intp dims[] = {(npy_intp)xys.size() / 2, 2 }; @@ -460,11 +463,14 @@ static PyObject *PyFT2Font_new(PyTypeObject *type, PyObject *args, PyObject *kwd PyFT2Font *self; self = (PyFT2Font *)type->tp_alloc(type, 0); self->x = NULL; + self->fname = NULL; self->py_file = NULL; self->fp = NULL; self->close_file = 0; self->offset = 0; memset(&self->stream, 0, sizeof(FT_StreamRec)); + self->mem = 0; + self->mem_size = 0; return (PyObject *)self; } @@ -1759,7 +1765,7 @@ PyMODINIT_FUNC initft2font(void) int error = FT_Init_FreeType(&_ft2Library); if (error) { - PyErr_SetString(PyExc_RuntimeError, "Could not find initialize the freetype2 library"); + PyErr_SetString(PyExc_RuntimeError, "Could not initialize the freetype2 library"); INITERROR; } @@ -1774,6 +1780,10 @@ PyMODINIT_FUNC initft2font(void) } } + if (PyModule_AddStringConstant(m, "__freetype_build_type__", STRINGIFY(FREETYPE_BUILD_TYPE))) { + INITERROR; + } + import_array(); #if PY3K