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 cc96f76db2bc..f2e682b78b7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -82,7 +82,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 @@ -96,7 +96,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/doc/users/whats_new/faster-text-rendering.rst b/doc/users/whats_new/faster-text-rendering.rst new file mode 100644 index 000000000000..d7807bb772f1 --- /dev/null +++ b/doc/users/whats_new/faster-text-rendering.rst @@ -0,0 +1,5 @@ +Faster text rendering +--------------------- + +Rendering text in the Agg backend is now less fuzzy and about 20% +faster to draw. diff --git a/examples/misc/ftface_props.py b/examples/misc/ftface_props.py index 1a821cb531e4..acc28482dd15 100755 --- a/examples/misc/ftface_props.py +++ b/examples/misc/ftface_props.py @@ -63,5 +63,4 @@ print(dir(font)) -cmap = font.get_charmap() print(font.get_kerning) diff --git a/examples/misc/multiprocess.py b/examples/misc/multiprocess.py index febe6202b0d2..510e9001fa3e 100644 --- a/examples/misc/multiprocess.py +++ b/examples/misc/multiprocess.py @@ -1,15 +1,10 @@ # Demo of using multiprocessing for generating data in one process and plotting # in another. # Written by Robert Cimrman -# Requires >= Python 2.6 for the multiprocessing module or having the -# standalone processing module installed from __future__ import print_function import time -try: - from multiprocessing import Process, Pipe -except ImportError: - from processing import Process, Pipe +from multiprocessing import Process, Pipe import numpy as np import matplotlib 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 5c749bafc599..92404c376668 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -197,10 +197,11 @@ def _forward_ilshift(self, other): major, minor1, minor2, s, tmp = sys.version_info -_python26 = (major == 2 and minor1 >= 6) or major >= 3 +_python27 = (major == 2 and minor1 >= 7) +_python34 = (major == 3 and minor1 >= 4) -if not _python26: - raise ImportError('matplotlib requires Python 2.6 or later') +if not (_python27 or _python34): + raise ImportError('matplotlib requires Python 2.7 or 3.4 or later') if not compare_versions(numpy.__version__, __version__numpy__): @@ -1487,6 +1488,18 @@ def verify_test_dependencies(): 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_data.py b/lib/matplotlib/_mathtext_data.py index a8379e30c9b3..81e1f579f3aa 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -1,8 +1,6 @@ """ font data tables for truetype and afm computer modern fonts """ -# this dict maps symbol names to fontnames, glyphindex. To get the -# glyph index from the character code, you have to use get_charmap from __future__ import (absolute_import, division, print_function, unicode_literals) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 091301692683..12f9e336680d 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3062,116 +3062,115 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, Parameters ---------- + x : Array or a sequence of vectors. + The input data. + + notch : bool, default = False + If False, produces a rectangular box plot. + If True, will produce a notched box plot + + sym : str or None, default = None + The default symbol for flier points. + Enter an empty string ('') if you don't want to show fliers. + If `None`, then the fliers default to 'b+' If you want more + control use the flierprops kwarg. + + vert : bool, default = True + If True (default), makes the boxes vertical. + If False, makes horizontal boxes. + + whis : float, sequence (default = 1.5) or string + As a float, determines the reach of the whiskers past the first + and third quartiles (e.g., Q3 + whis*IQR, IQR = interquartile + range, Q3-Q1). Beyond the whiskers, data are considered outliers + and are plotted as individual points. Set this to an unreasonably + high value to force the whiskers to show the min and max values. + Alternatively, set this to an ascending sequence of percentile + (e.g., [5, 95]) to set the whiskers at specific percentiles of + the data. Finally, *whis* can be the string 'range' to force the + whiskers to the min and max of the data. In the edge case that + the 25th and 75th percentiles are equivalent, *whis* will be + automatically set to 'range'. + + bootstrap : None (default) or integer + Specifies whether to bootstrap the confidence intervals + around the median for notched boxplots. If bootstrap==None, + no bootstrapping is performed, and notches are calculated + using a Gaussian-based asymptotic approximation (see McGill, R., + Tukey, J.W., and Larsen, W.A., 1978, and Kendall and Stuart, + 1967). Otherwise, bootstrap specifies the number of times to + bootstrap the median to determine it's 95% confidence intervals. + Values between 1000 and 10000 are recommended. + + usermedians : array-like or None (default) + An array or sequence whose first dimension (or length) is + compatible with *x*. This overrides the medians computed by + matplotlib for each element of *usermedians* that is not None. + When an element of *usermedians* == None, the median will be + computed by matplotlib as normal. + + conf_intervals : array-like or None (default) + Array or sequence whose first dimension (or length) is compatible + with *x* and whose second dimension is 2. When the current element + of *conf_intervals* is not None, the notch locations computed by + matplotlib are overridden (assuming notch is True). When an + element of *conf_intervals* is None, boxplot compute notches the + method specified by the other kwargs (e.g., *bootstrap*). - x : Array or a sequence of vectors. - The input data. - - notch : bool, default = False - If False, produces a rectangular box plot. - If True, will produce a notched box plot - - sym : str or None, default = None - The default symbol for flier points. - Enter an empty string ('') if you don't want to show fliers. - If `None`, then the fliers default to 'b+' If you want more - control use the flierprops kwarg. - - vert : bool, default = True - If True (default), makes the boxes vertical. - If False, makes horizontal boxes. - - whis : float, sequence (default = 1.5) or string - As a float, determines the reach of the whiskers past the first - and third quartiles (e.g., Q3 + whis*IQR, IQR = interquartile - range, Q3-Q1). Beyond the whiskers, data are considered outliers - and are plotted as individual points. Set this to an unreasonably - high value to force the whiskers to show the min and max values. - Alternatively, set this to an ascending sequence of percentile - (e.g., [5, 95]) to set the whiskers at specific percentiles of - the data. Finally, *whis* can be the string 'range' to force the - whiskers to the min and max of the data. In the edge case that - the 25th and 75th percentiles are equivalent, *whis* will be - automatically set to 'range'. - - bootstrap : None (default) or integer - Specifies whether to bootstrap the confidence intervals - around the median for notched boxplots. If bootstrap==None, - no bootstrapping is performed, and notches are calculated - using a Gaussian-based asymptotic approximation (see McGill, R., - Tukey, J.W., and Larsen, W.A., 1978, and Kendall and Stuart, - 1967). Otherwise, bootstrap specifies the number of times to - bootstrap the median to determine it's 95% confidence intervals. - Values between 1000 and 10000 are recommended. - - usermedians : array-like or None (default) - An array or sequence whose first dimension (or length) is - compatible with *x*. This overrides the medians computed by - matplotlib for each element of *usermedians* that is not None. - When an element of *usermedians* == None, the median will be - computed by matplotlib as normal. - - conf_intervals : array-like or None (default) - Array or sequence whose first dimension (or length) is compatible - with *x* and whose second dimension is 2. When the current element - of *conf_intervals* is not None, the notch locations computed by - matplotlib are overridden (assuming notch is True). When an - element of *conf_intervals* is None, boxplot compute notches the - method specified by the other kwargs (e.g., *bootstrap*). - - positions : array-like, default = [1, 2, ..., n] - Sets the positions of the boxes. The ticks and limits - are automatically set to match the positions. - - widths : array-like, default = 0.5 - Either a scalar or a vector and sets the width of each box. The - default is 0.5, or ``0.15*(distance between extreme positions)`` - if that is smaller. - - labels : sequence or None (default) - Labels for each dataset. Length must be compatible with - dimensions of *x* - - patch_artist : bool, default = False - If False produces boxes with the Line2D artist - If True produces boxes with the Patch artist - - showmeans : bool, default = False - If True, will toggle one the rendering of the means - - showcaps : bool, default = True - If True, will toggle one the rendering of the caps - - showbox : bool, default = True - If True, will toggle one the rendering of box - - showfliers : bool, default = True - If True, will toggle one the rendering of the fliers - - boxprops : dict or None (default) - If provided, will set the plotting style of the boxes - - whiskerprops : dict or None (default) - If provided, will set the plotting style of the whiskers - - capprops : dict or None (default) - If provided, will set the plotting style of the caps - - flierprops : dict or None (default) - If provided, will set the plotting style of the fliers - - medianprops : dict or None (default) - If provided, will set the plotting style of the medians - - meanprops : dict or None (default) + positions : array-like, default = [1, 2, ..., n] + Sets the positions of the boxes. The ticks and limits + are automatically set to match the positions. + + widths : array-like, default = 0.5 + Either a scalar or a vector and sets the width of each box. The + default is 0.5, or ``0.15*(distance between extreme positions)`` + if that is smaller. + + labels : sequence or None (default) + Labels for each dataset. Length must be compatible with + dimensions of *x* + + patch_artist : bool, default = False + If False produces boxes with the Line2D artist + If True produces boxes with the Patch artist + + showmeans : bool, default = False + If True, will toggle one the rendering of the means + + showcaps : bool, default = True + If True, will toggle one the rendering of the caps + + showbox : bool, default = True + If True, will toggle one the rendering of box + + showfliers : bool, default = True + If True, will toggle one the rendering of the fliers + + boxprops : dict or None (default) + If provided, will set the plotting style of the boxes + + whiskerprops : dict or None (default) + If provided, will set the plotting style of the whiskers + + capprops : dict or None (default) + If provided, will set the plotting style of the caps + + flierprops : dict or None (default) + If provided, will set the plotting style of the fliers + + medianprops : dict or None (default) + If provided, will set the plotting style of the medians + + meanprops : dict or None (default) If provided, will set the plotting style of the means - meanline : bool, default = False + meanline : bool, default = False If True (and *showmeans* is True), will try to render the mean as a line spanning the full width of the box according to *meanprops*. Not recommended if *shownotches* is also True. Otherwise, means will be shown as points. - manage_xticks : bool, default = True + manage_xticks : bool, default = True If the function should adjust the xlim and xtick locations. Returns @@ -4977,7 +4976,7 @@ def _pcolorargs(funcname, *args, **kw): allmatch = kw.pop("allmatch", False) if len(args) == 1: - C = args[0] + C = np.asanyarray(args[0]) numRows, numCols = C.shape if allmatch: X, Y = np.meshgrid(np.arange(numCols), np.arange(numRows)) @@ -4987,7 +4986,7 @@ def _pcolorargs(funcname, *args, **kw): return X, Y, C if len(args) == 3: - X, Y, C = args + X, Y, C = [np.asanyarray(a) for a in args] numRows, numCols = C.shape else: raise TypeError( diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 59c4cf6b8337..bd45bb5f4728 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -883,13 +883,12 @@ def get_char_width(charcode): # Make the "Differences" array, sort the ccodes < 255 from # the multi-byte ccodes, and build the whole set of glyph ids # that we need from this font. - cmap = font.get_charmap() glyph_ids = [] differences = [] multi_byte_chars = set() for c in characters: ccode = c - gind = cmap.get(ccode) or 0 + gind = font.get_char_index(ccode) glyph_ids.append(gind) glyph_name = font.get_glyph_name(gind) if ccode <= 255: @@ -999,12 +998,11 @@ def embedTTFType42(font, characters, descriptor): # Make the 'W' (Widths) array, CidToGidMap and ToUnicode CMap # at the same time cid_to_gid_map = ['\u0000'] * 65536 - cmap = font.get_charmap() widths = [] max_ccode = 0 for c in characters: ccode = c - gind = cmap.get(ccode) or 0 + gind = font.get_char_index(ccode) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) widths.append((ccode, glyph.horiAdvance / 6)) if ccode < 65536: @@ -2011,7 +2009,6 @@ def draw_text_woven(chunks): between chunks of 1-byte characters and 2-byte characters. Only used for Type 3 fonts.""" chunks = [(a, ''.join(b)) for a, b in chunks] - cmap = font.get_charmap() # Do the rotation and global translation as a single matrix # concatenation up front @@ -2041,7 +2038,7 @@ def draw_text_woven(chunks): lastgind = None for c in chunk: ccode = ord(c) - gind = cmap.get(ccode) + gind = font.get_char_index(ccode) if gind is not None: if mode == 2 and chunk_type == 2: glyph_name = font.get_glyph_name(gind) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 4f00ea0b43ab..3570b5380d9e 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -235,23 +235,6 @@ def get_latex_manager(): LatexManagerFactory.previous_instance = new_inst return new_inst -class WeakSet(object): - # TODO: Poor man's weakref.WeakSet. - # Remove this once python 2.6 support is dropped from matplotlib. - - def __init__(self): - self.weak_key_dict = weakref.WeakKeyDictionary() - - def add(self, item): - self.weak_key_dict[item] = None - - def discard(self, item): - if item in self.weak_key_dict: - del self.weak_key_dict[item] - - def __iter__(self): - return six.iterkeys(self.weak_key_dict) - class LatexManager(object): """ @@ -259,7 +242,7 @@ class LatexManager(object): determining the metrics of text elements. The LaTeX environment can be modified by setting fonts and/or a custem preamble in the rc parameters. """ - _unclean_instances = WeakSet() + _unclean_instances = weakref.WeakSet() @staticmethod def _build_latex_header(): diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index bb6f83afc667..4c4daeb33df5 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -762,7 +762,6 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): ps_name = ps_name.encode('ascii', 'replace').decode('ascii') self.set_font(ps_name, prop.get_size_in_points()) - cmap = font.get_charmap() lastgind = None #print 'text', s lines = [] @@ -770,7 +769,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): thisy = 0 for c in s: ccode = ord(c) - gind = cmap.get(ccode) + gind = font.get_char_index(ccode) if gind is None: ccode = ord('?') name = '.notdef' @@ -1138,10 +1137,9 @@ def print_figure_impl(): for font_filename, chars in six.itervalues(ps_renderer.used_characters): if len(chars): font = get_font(font_filename) - cmap = font.get_charmap() glyph_ids = [] for c in chars: - gind = cmap.get(c) or 0 + gind = font.get_char_index(c) glyph_ids.append(gind) fonttype = rcParams['ps.fonttype'] diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 18ef6b793c2b..c6420305b146 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -191,20 +191,7 @@ def deprecate(func, message=message, name=name, alternative=alternative, import textwrap if isinstance(func, classmethod): - try: - func = func.__func__ - except AttributeError: - # classmethods in Python2.6 and below lack the __func__ - # attribute so we need to hack around to get it - method = func.__get__(None, object) - if hasattr(method, '__func__'): - func = method.__func__ - elif hasattr(method, 'im_func'): - func = method.im_func - else: - # Nothing we can do really... just return the original - # classmethod - return func + func = func.__func__ is_classmethod = True else: is_classmethod = False diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 188d991981a6..72b19b5d11e4 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -733,8 +733,8 @@ class NonUniformImage(AxesImage): def __init__(self, ax, **kwargs): """ kwargs are identical to those for AxesImage, except - that 'interpolation' defaults to 'nearest', and 'bilinear' - is the only alternative. + that 'nearest' and 'bilinear' are the only supported 'interpolation' + options. """ interp = kwargs.pop('interpolation', 'nearest') AxesImage.__init__(self, ax, 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/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e8ac6508efdb..f2669e4451eb 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -894,7 +894,7 @@ def validate_cycler(s): 'mathtext.fallback_to_cm': [True, validate_bool], 'image.aspect': ['equal', validate_aspect], # equal, auto, a number - 'image.interpolation': ['bilinear', six.text_type], + 'image.interpolation': ['nearest', six.text_type], 'image.cmap': ['jet', six.text_type], # one of gray, jet, etc 'image.lut': [256, validate_int], # lookup table 'image.origin': ['upper', six.text_type], # lookup table diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index bc6168c62fd1..b6a4e79dc44f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -738,6 +738,21 @@ def test_symlog2(): ax.set_ylim(-0.1, 0.1) +@cleanup +def test_pcolorargs(): + # Smoketest to catch issue found in gh:5205 + x = [-1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5] + y = [-1.5, -1.25, -1.0, -0.75, -0.5, -0.25, 0, + 0.25, 0.5, 0.75, 1.0, 1.25, 1.5] + X, Y = np.meshgrid(x, y) + Z = np.hypot(X, Y) + + plt.pcolor(Z) + plt.pcolor(list(Z)) + plt.pcolor(x, y, Z) + plt.pcolor(X, Y, list(Z)) + + @image_comparison(baseline_images=['pcolormesh'], remove_text=True) def test_pcolormesh(): n = 12 diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index dac0710fd855..37f6671bc7ab 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -6,7 +6,7 @@ import os -from matplotlib.font_manager import findfont, FontProperties +from matplotlib.font_manager import (findfont, FontProperties, get_font) from matplotlib import rc_context @@ -17,3 +17,9 @@ def test_font_priority(): font = findfont( FontProperties(family=["sans-serif"])) assert_equal(os.path.basename(font), 'cmmi10.ttf') + + # Smoketest get_charmap, which isn't used internally anymore + font = get_font(font) + cmap = font.get_charmap() + assert len(cmap) == 131 + assert cmap[8729] == 30 diff --git a/lib/matplotlib/tests/test_labeled_data_unpacking.py b/lib/matplotlib/tests/test_labeled_data_unpacking.py index e8baf8235e4f..fe5427304345 100644 --- a/lib/matplotlib/tests/test_labeled_data_unpacking.py +++ b/lib/matplotlib/tests/test_labeled_data_unpacking.py @@ -7,17 +7,10 @@ # 3.2+ versions from nose.tools import assert_regex, assert_not_regex except ImportError: - try: - # 2.7 versions - from nose.tools import assert_regexp_matches, assert_not_regexp_matches - assert_regex = assert_regexp_matches - assert_not_regex = assert_not_regexp_matches - except ImportError: - # 2.6 versions - def noop(txt, regex): - raise SkipTest("No assert for regex matching in py2.6") - assert_regex = noop - assert_not_regex = noop + # 2.7 versions + from nose.tools import assert_regexp_matches, assert_not_regexp_matches + assert_regex = assert_regexp_matches + assert_not_regex = assert_not_regexp_matches from ..testing import assert_produces_warning diff --git a/lib/matplotlib/tests/test_spines.py b/lib/matplotlib/tests/test_spines.py index d8688c586d42..2f6a9a3084cc 100644 --- a/lib/matplotlib/tests/test_spines.py +++ b/lib/matplotlib/tests/test_spines.py @@ -2,7 +2,7 @@ unicode_literals) import numpy as np -from nose.tools import assert_true +from nose.tools import assert_true, assert_less from matplotlib.externals import six import matplotlib @@ -71,13 +71,11 @@ def test_label_without_ticks(): spine = ax.spines['left'] spinebbox = spine.get_transform().transform_path( spine.get_path()).get_extents() - # replace with assert_less if >python2.6 - assert_true(ax.yaxis.label.get_position()[0] < spinebbox.xmin, + assert_less(ax.yaxis.label.get_position()[0], spinebbox.xmin, "Y-Axis label not left of the spine") spine = ax.spines['bottom'] spinebbox = spine.get_transform().transform_path( spine.get_path()).get_extents() - # replace with assert_less if >python2.6 - assert_true(ax.xaxis.label.get_position()[1] < spinebbox.ymin, + assert_less(ax.xaxis.label.get_position()[1], spinebbox.ymin, "X-Axis label not below the spine") diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 79fbcc6c2da0..64b44c0726e1 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -173,7 +173,6 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, # Mostly copied from backend_svg.py. - cmap = font.get_charmap() lastgind = None currx = 0 @@ -192,7 +191,7 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, for c in s: ccode = ord(c) - gind = cmap.get(ccode) + gind = font.get_char_index(ccode) if gind is None: ccode = ord('?') gind = 0 diff --git a/matplotlibrc.template b/matplotlibrc.template index 7492fe7b7be4..3e50878b56a3 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 @@ -352,14 +352,14 @@ backend : %(backend)s ### IMAGES #image.aspect : equal # equal | auto | a number -#image.interpolation : bilinear # see help(imshow) for options +#image.interpolation : nearest # see help(imshow) for options #image.cmap : jet # gray | jet etc... #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 4a31bd8f049b..cdeb709389b1 100755 --- a/setupext.py +++ b/setupext.py @@ -9,6 +9,7 @@ import os import re import subprocess +from subprocess import check_output import sys import warnings from textwrap import fill @@ -19,31 +20,12 @@ PY3 = (sys.version_info[0] >= 3) -try: - from subprocess import check_output -except ImportError: - # check_output is not available in Python 2.6 - def check_output(*popenargs, **kwargs): - """ - Run command with arguments and return its output as a byte - string. - - Backported from Python 2.7 as it's implemented as pure python - on stdlib. - """ - process = subprocess.Popen( - stdout=subprocess.PIPE, *popenargs, **kwargs) - output, unused_err = process.communicate() - retcode = process.poll() - if retcode: - cmd = kwargs.get("args") - if cmd is None: - cmd = popenargs[0] - error = subprocess.CalledProcessError(retcode, cmd) - error.output = output - raise error - return output - +# 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: @@ -72,22 +54,19 @@ def check_output(*popenargs, **kwargs): 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 @@ -244,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. @@ -473,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 @@ -486,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): """ @@ -554,13 +555,13 @@ def check(self): if major < 2: raise CheckFailed( - "Requires Python 2.6 or later") - elif major == 2 and minor1 < 6: + "Requires Python 2.7 or later") + elif major == 2 and minor1 < 7: raise CheckFailed( - "Requires Python 2.6 or later (in the 2.x series)") - elif major == 3 and minor1 < 1: + "Requires Python 2.7 or later (in the 2.x series)") + elif major == 3 and minor1 < 4: raise CheckFailed( - "Requires Python 3.1 or later (in the 3.x series)") + "Requires Python 3.4 or later (in the 3.x series)") return sys.version @@ -897,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.' @@ -942,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/_backend_agg.h b/src/_backend_agg.h index 11abfcb98f5e..0249b3f4f13a 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -734,41 +734,65 @@ inline void RendererAgg::draw_text_image(GCAgg &gc, ImageArray &image, int x, in typedef agg::renderer_scanline_aa renderer_type; - theRasterizer.reset_clipping(); - rendererBase.reset_clipping(true); - set_clipbox(gc.cliprect, theRasterizer); + if (angle != 0.0) { + agg::rendering_buffer srcbuf( + image.data(), (unsigned)image.dim(1), + (unsigned)image.dim(0), (unsigned)image.dim(1)); + agg::pixfmt_gray8 pixf_img(srcbuf); + + theRasterizer.reset_clipping(); + rendererBase.reset_clipping(true); + set_clipbox(gc.cliprect, theRasterizer); + + agg::trans_affine mtx; + mtx *= agg::trans_affine_translation(0, -image.dim(0)); + mtx *= agg::trans_affine_rotation(-angle * agg::pi / 180.0); + mtx *= agg::trans_affine_translation(x, y); - agg::rendering_buffer srcbuf( - image.data(), (unsigned)image.dim(1), (unsigned)image.dim(0), (unsigned)image.dim(1)); - agg::pixfmt_gray8 pixf_img(srcbuf); - - agg::trans_affine mtx; - mtx *= agg::trans_affine_translation(0, -image.dim(0)); - mtx *= agg::trans_affine_rotation(-angle * agg::pi / 180.0); - mtx *= agg::trans_affine_translation(x, y); - - agg::path_storage rect; - rect.move_to(0, 0); - rect.line_to(image.dim(1), 0); - rect.line_to(image.dim(1), image.dim(0)); - rect.line_to(0, image.dim(0)); - rect.line_to(0, 0); - agg::conv_transform rect2(rect, mtx); - - agg::trans_affine inv_mtx(mtx); - inv_mtx.invert(); - - agg::image_filter_lut filter; - filter.calculate(agg::image_filter_spline36()); - interpolator_type interpolator(inv_mtx); - color_span_alloc_type sa; - image_accessor_type ia(pixf_img, agg::gray8(0)); - image_span_gen_type image_span_generator(ia, interpolator, filter); - span_gen_type output_span_generator(&image_span_generator, gc.color); - renderer_type ri(rendererBase, sa, output_span_generator); - - theRasterizer.add_path(rect2); - agg::render_scanlines(theRasterizer, slineP8, ri); + agg::path_storage rect; + rect.move_to(0, 0); + rect.line_to(image.dim(1), 0); + rect.line_to(image.dim(1), image.dim(0)); + rect.line_to(0, image.dim(0)); + rect.line_to(0, 0); + agg::conv_transform rect2(rect, mtx); + + agg::trans_affine inv_mtx(mtx); + inv_mtx.invert(); + + agg::image_filter_lut filter; + filter.calculate(agg::image_filter_spline36()); + interpolator_type interpolator(inv_mtx); + color_span_alloc_type sa; + image_accessor_type ia(pixf_img, agg::gray8(0)); + image_span_gen_type image_span_generator(ia, interpolator, filter); + span_gen_type output_span_generator(&image_span_generator, gc.color); + renderer_type ri(rendererBase, sa, output_span_generator); + + theRasterizer.add_path(rect2); + agg::render_scanlines(theRasterizer, slineP8, ri); + } else { + agg::rect_i fig, text; + + fig.init(0, 0, width, height); + text.init(x, y - image.dim(0), x + image.dim(1), y); + text.clip(fig); + + if (gc.cliprect.x1 != 0.0 || gc.cliprect.y1 != 0.0 || gc.cliprect.x2 != 0.0 || gc.cliprect.y2 != 0.0) { + agg::rect_i clip; + + clip.init(int(mpl_round(gc.cliprect.x1)), + int(mpl_round(gc.cliprect.y1)), + int(mpl_round(gc.cliprect.x2)), + int(mpl_round(gc.cliprect.y2))); + text.clip(clip); + } + + for (int yi = text.y1; yi < text.y2; ++yi) { + pixFmt.blend_solid_hspan(text.x1, yi, (text.x2 - text.x1), gc.color, + &image(yi - (y - image.dim(0)), text.x1 - x)); + } + } } class span_conv_alpha 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