From 1d78d3d160a0dfff2aa316e589cf256825f1f21b Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Mon, 27 Feb 2012 16:32:10 -0500 Subject: [PATCH 1/2] Makes the "hinting factor" tweakable at runtime. The hinting factor is the amount of softness in hinting is applied in the horizontal direction. For years, matplotlib has hinted to 1/8 pixels in the horizontal direction and whole pixels in the vertical direction. This results in text that is often more legible (particularly at smaller sizes) than standard 1:1 hinting. This is based on idea from this paper from the author of Agg: http://www.antigrain.com/research/font_rasterization/ However, sometimes the user may want full-on hinting, so this value is now tweakable using the `text.hinting_factor` rcParam. --- lib/matplotlib/backends/backend_agg.py | 4 +- lib/matplotlib/rcsetup.py | 1 + lib/matplotlib/tests/__init__.py | 1 + matplotlibrc.template | 3 ++ src/ft2font.cpp | 52 ++++++++++---------------- src/ft2font.h | 3 +- 6 files changed, 29 insertions(+), 35 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index db4bdf9e5e51..f0ae4c91f8eb 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -220,7 +220,9 @@ def _get_agg_font(self, prop): fname = findfont(prop) font = self._fontd.get(fname) if font is None: - font = FT2Font(str(fname)) + font = FT2Font( + str(fname), + hinting_factor=rcParams['text.hinting_factor']) self._fontd[fname] = font self._fontd[key] = font diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index c947b24d5154..6ba5be47a317 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -408,6 +408,7 @@ def __call__(self, s): 'text.latex.preview' : [False, validate_bool], 'text.dvipnghack' : [None, validate_bool_maybe_none], 'text.hinting' : [True, validate_bool], + 'text.hinting_factor' : [8, validate_int], # The following are deprecated and replaced by, e.g., 'font.style' #'text.fontstyle' : ['normal', str], diff --git a/lib/matplotlib/tests/__init__.py b/lib/matplotlib/tests/__init__.py index f2b102b7734c..bd133a4f6645 100644 --- a/lib/matplotlib/tests/__init__.py +++ b/lib/matplotlib/tests/__init__.py @@ -12,3 +12,4 @@ def setup(): rcdefaults() # Start with all defaults rcParams['font.family'] = 'Bitstream Vera Sans' rcParams['text.hinting'] = False + rcParams['text.hinting_factor'] = 8 diff --git a/matplotlibrc.template b/matplotlibrc.template index ffb659c617d3..f333f1c368ac 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -172,6 +172,9 @@ backend : %(backend)s #text.hinting : True # If True, text will be hinted, otherwise not. This only # affects the Agg backend. +#text.hinting_factor : 8 # Specifies the amount of softness for hinting in the + # horizontal direction. A value of 1 will hint to full + # pixels. A value of 2 will hint to half pixels etc. # The following settings allow you to select the fonts in math mode. # They map from a TeX font name to a fontconfig font pattern. diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 7cf44110bff7..98431d950f6d 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -36,17 +36,7 @@ at the size at 12 pixels scaled by 2 through a transform, because the hints will have been computed differently (except you have disabled hints). - - This hack is enabled only when VERTICAL_HINTING is defined, and will - only be effective when load_char and set_text are called with 'flags= - LOAD_DEFAULT', which is the default. */ -#define VERTICAL_HINTING -#ifdef VERTICAL_HINTING -#define HORIZ_HINTING 8 -#else -#define HORIZ_HINTING 1 -#endif FT_Library _ft2Library; @@ -408,7 +398,7 @@ FT2Image::py_get_height(const Py::Tuple & args) PYCXX_VARARGS_METHOD_DECL(FT2Image, py_get_height) Py::PythonClassObject Glyph::factory( - const FT_Face& face, const FT_Glyph& glyph, size_t ind) + const FT_Face& face, const FT_Glyph& glyph, size_t ind, long hinting_factor) { Py::Callable class_type(type()); Py::PythonClassObject obj = Py::PythonClassObject( @@ -419,12 +409,12 @@ Py::PythonClassObject Glyph::factory( FT_BBox bbox; FT_Glyph_Get_CBox(glyph, ft_glyph_bbox_subpixels, &bbox); - o->setattro("width", Py::Int(face->glyph->metrics.width / HORIZ_HINTING)); + o->setattro("width", Py::Int(face->glyph->metrics.width / hinting_factor)); o->setattro("height", Py::Int(face->glyph->metrics.height)); - o->setattro("horiBearingX", Py::Int(face->glyph->metrics.horiBearingX / HORIZ_HINTING)); + o->setattro("horiBearingX", Py::Int(face->glyph->metrics.horiBearingX / hinting_factor)); o->setattro("horiBearingY", Py::Int(face->glyph->metrics.horiBearingY)); o->setattro("horiAdvance", Py::Int(face->glyph->metrics.horiAdvance)); - o->setattro("linearHoriAdvance", Py::Int(face->glyph->linearHoriAdvance / HORIZ_HINTING)); + o->setattro("linearHoriAdvance", Py::Int(face->glyph->linearHoriAdvance / hinting_factor)); o->setattro("vertBearingX", Py::Int(face->glyph->metrics.vertBearingX)); o->setattro("vertBearingY", Py::Int(face->glyph->metrics.vertBearingY)); @@ -847,14 +837,15 @@ FT2Font::FT2Font(Py::PythonClassInstance *self, Py::Tuple &args, Py::Dict &kwds) } // set a default fontsize 12 pt at 72dpi -#ifdef VERTICAL_HINTING - error = FT_Set_Char_Size(face, 12 * 64, 0, 72 * HORIZ_HINTING, 72); - static FT_Matrix transform = { 65536 / HORIZ_HINTING, 0, 0, 65536 }; + hinting_factor = 8; + if (kwds.hasKey("hinting_factor")) + { + hinting_factor = Py::Long(kwds["hinting_factor"]); + } + + error = FT_Set_Char_Size(face, 12 * 64, 0, 72 * hinting_factor, 72); + static FT_Matrix transform = { 65536 / hinting_factor, 0, 0, 65536 }; FT_Set_Transform(face, &transform, 0); -#else - error = FT_Set_Char_Size(face, 12 * 64, 0, 72, 72); -#endif - //error = FT_Set_Char_Size( face, 20 * 64, 0, 80, 80 ); if (error) { std::ostringstream s; @@ -993,17 +984,12 @@ FT2Font::set_size(const Py::Tuple & args) double ptsize = Py::Float(args[0]); double dpi = Py::Float(args[1]); -#ifdef VERTICAL_HINTING int error = FT_Set_Char_Size(face, (long)(ptsize * 64), 0, - (unsigned int)dpi * HORIZ_HINTING, + (unsigned int)dpi * hinting_factor, (unsigned int)dpi); - static FT_Matrix transform = { 65536 / HORIZ_HINTING, 0, 0, 65536 }; + static FT_Matrix transform = { 65536 / hinting_factor, 0, 0, 65536 }; FT_Set_Transform(face, &transform, 0); -#else - int error = FT_Set_Char_Size(face, (long)(ptsize * 64), 0, - (unsigned int)dpi, - (unsigned int)dpi); -#endif + if (error) { throw Py::RuntimeError("Could not set the fontsize"); @@ -1126,7 +1112,7 @@ FT2Font::get_kerning(const Py::Tuple & args) if (!FT_Get_Kerning(face, left, right, mode, &delta)) { - return Py::Int(delta.x / HORIZ_HINTING); + return Py::Int(delta.x / hinting_factor); } else { @@ -1214,7 +1200,7 @@ FT2Font::set_text(const Py::Tuple & args, const Py::Dict & kwargs) FT_Vector delta; FT_Get_Kerning(face, previous, glyph_index, FT_KERNING_DEFAULT, &delta); - pen.x += delta.x / HORIZ_HINTING; + pen.x += delta.x / hinting_factor; } error = FT_Load_Glyph(face, glyph_index, flags); if (error) @@ -1319,7 +1305,7 @@ FT2Font::load_char(const Py::Tuple & args, const Py::Dict & kwargs) size_t num = glyphs.size(); //the index into the glyphs list glyphs.push_back(thisGlyph); - return Glyph::factory(face, thisGlyph, num); + return Glyph::factory(face, thisGlyph, num, hinting_factor); } PYCXX_KEYWORDS_METHOD_DECL(FT2Font, load_char) @@ -1369,7 +1355,7 @@ FT2Font::load_glyph(const Py::Tuple & args, const Py::Dict & kwargs) size_t num = glyphs.size(); //the index into the glyphs list glyphs.push_back(thisGlyph); - return Glyph::factory(face, thisGlyph, num); + return Glyph::factory(face, thisGlyph, num, hinting_factor); } PYCXX_KEYWORDS_METHOD_DECL(FT2Font, load_glyph) diff --git a/src/ft2font.h b/src/ft2font.h index 5d7739e7518c..502299b3a5f5 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -83,7 +83,7 @@ class Glyph : public Py::PythonClass Glyph(Py::PythonClassInstance *self, Py::Tuple &args, Py::Dict &kwds) : Py::PythonClass::PythonClass(self, args, kwds) { } virtual ~Glyph(); - static Py::PythonClassObject factory(const FT_Face&, const FT_Glyph&, size_t); + static Py::PythonClassObject factory(const FT_Face&, const FT_Glyph&, size_t, long); int setattro(const Py::String &name, const Py::Object &value); Py::Object getattro(const Py::String &name); static void init_type(void); @@ -137,6 +137,7 @@ class FT2Font : public Py::PythonClass double angle; double ptsize; double dpi; + long hinting_factor; FT_BBox compute_string_bbox(); void set_scalable_attributes(); From cf5e358e6a6f525b6cac56e8a6fac68a09ddfe58 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Tue, 28 Feb 2012 08:26:47 -0500 Subject: [PATCH 2/2] Expose more hinting options to the user. --- lib/matplotlib/backends/backend_agg.py | 25 ++++++++++++++++--------- lib/matplotlib/mathtext.py | 6 ++---- lib/matplotlib/rcsetup.py | 9 ++++++++- matplotlibrc.template | 14 +++++++++++--- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index f0ae4c91f8eb..694eaeccf264 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -30,7 +30,8 @@ from matplotlib.cbook import is_string_like, maxdict from matplotlib.figure import Figure from matplotlib.font_manager import findfont -from matplotlib.ft2font import FT2Font, LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING +from matplotlib.ft2font import FT2Font, LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, \ + LOAD_DEFAULT, LOAD_NO_AUTOHINT from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.transforms import Bbox, BboxBase @@ -40,6 +41,18 @@ backend_version = 'v2.2' +def get_hinting_flag(): + mapping = { + True: LOAD_FORCE_AUTOHINT, + False: LOAD_NO_HINTING, + 'either': LOAD_DEFAULT, + 'native': LOAD_NO_AUTOHINT, + 'auto': LOAD_FORCE_AUTOHINT, + 'none': LOAD_NO_HINTING + } + return mapping[rcParams['text.hinting']] + + class RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics @@ -69,12 +82,6 @@ def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying') - def _get_hinting_flag(self): - if rcParams['text.hinting']: - return LOAD_FORCE_AUTOHINT - else: - return LOAD_NO_HINTING - # for filtering to work with rasterization, methods needs to be wrapped. # maybe there is better way to do it. def draw_markers(self, *kl, **kw): @@ -141,7 +148,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath): if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) - flags = self._get_hinting_flag() + flags = get_hinting_flag() font = self._get_agg_font(prop) if font is None: return None if len(s) == 1 and ord(s) > 127: @@ -178,7 +185,7 @@ def get_text_width_height_descent(self, s, prop, ismath): self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent - flags = self._get_hinting_flag() + flags = get_hinting_flag() font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=flags) # the width and height of unrotated string w, h = font.get_width_height() diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index 9ab50b5dce7b..fb97088346a1 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -224,10 +224,8 @@ def get_results(self, box, used_characters): return result def get_hinting_type(self): - if rcParams['text.hinting']: - return LOAD_FORCE_AUTOHINT - else: - return LOAD_NO_HINTING + from matplotlib.backends import backend_agg + return backend_agg.get_hinting_flag() class MathtextBackendBitmap(MathtextBackendAgg): def get_results(self, box, used_characters): diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 6ba5be47a317..658ab2fd71b9 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -313,6 +313,13 @@ def deprecate_svg_embed_char_paths(value): validate_svg_fonttype = ValidateInStrings('fonttype', ['none', 'path', 'svgfont']) +def validate_hinting(s): + if s in (True, False): + return s + if s.lower() in ('auto', 'native', 'either', 'none'): + return s.lower() + raise ValueError("hinting should be 'auto', 'native', 'either' or 'none'") + class ValidateInterval: """ Value must be in interval @@ -407,7 +414,7 @@ def __call__(self, s): 'text.latex.preamble' : [[''], validate_stringlist], 'text.latex.preview' : [False, validate_bool], 'text.dvipnghack' : [None, validate_bool_maybe_none], - 'text.hinting' : [True, validate_bool], + 'text.hinting' : [True, validate_hinting], 'text.hinting_factor' : [8, validate_int], # The following are deprecated and replaced by, e.g., 'font.style' diff --git a/matplotlibrc.template b/matplotlibrc.template index f333f1c368ac..221d533ca9d3 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -170,9 +170,17 @@ backend : %(backend)s # correction off. None will try and # guess based on your dvipng version -#text.hinting : True # If True, text will be hinted, otherwise not. This only - # affects the Agg backend. -#text.hinting_factor : 8 # Specifies the amount of softness for hinting in the +#text.hinting : 'auto' # May be one of the following: + # 'none': Perform no hinting + # 'auto': Use freetype's autohinter + # 'native': Use the hinting information in the + # font file, if available, and if your + # 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 + # True === 'auto' or False === 'none'. +text.hinting_factor : 8 # Specifies the amount of softness for hinting in the # horizontal direction. A value of 1 will hint to full # pixels. A value of 2 will hint to half pixels etc.