diff --git a/.appveyor.yml b/.appveyor.yml index 4a093ea0049d..c924a5640329 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -26,6 +26,8 @@ environment: CONDA_INSTALL_LOCN: "C:\\Miniconda36-x64" TEST_ALL: "no" +image: Visual Studio 2017 + # We always use a 64-bit machine, but can build x86 distributions # with the PYTHON_ARCH variable platform: @@ -42,6 +44,7 @@ init: - echo %PYTHON_VERSION% %CONDA_INSTALL_LOCN% install: + - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" - set PATH=%CONDA_INSTALL_LOCN%;%CONDA_INSTALL_LOCN%\scripts;%PATH%; - set PYTHONUNBUFFERED=1 # for msinttypes and newer stuff diff --git a/.circleci/config.yml b/.circleci/config.yml index 6fa03850027f..0cdbc975a5bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,20 +13,24 @@ apt-run: &apt-install command: | sudo apt-get -qq update sudo apt-get install -y \ + cm-super \ + dvipng \ + graphviz \ inkscape \ libav-tools \ - dvipng \ - pgf \ + libgeos-dev \ lmodern \ - cm-super \ + otf-freefont \ + pgf \ + software-properties-common \ + texlive-fonts-recommended \ texlive-latex-base \ texlive-latex-extra \ - texlive-fonts-recommended \ texlive-latex-recommended \ - texlive-xetex \ - graphviz \ - libgeos-dev \ - otf-freefont + texlive-xetex + sudo add-apt-repository 'deb http://ftp.us.debian.org/debian testing main' + sudo apt-get update + sudo apt-get install g++-7 fonts-run: &fonts-install name: Install custom fonts @@ -64,7 +68,10 @@ deps-run: &deps-install mpl-run: &mpl-install name: Install Matplotlib - command: pip install --user -ve . + command: | + ls /usr/include/freetype2 + cat /usr/include/freetype2/ft2build.h + CC=gcc-7 CXX=g++-7 pip install --user -ve . doc-run: &doc-build name: Build documentation diff --git a/.travis.yml b/.travis.yml index 852837464d6c..58e7303b21f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ sudo: false branches: except: - - /^auto-backport-of-pr-\d*/ + - /^auto-backport-of-pr-\d*/ cache: pip: true @@ -16,11 +16,14 @@ cache: addons: artifacts: paths: - - result_images.tar.bz2 + - result_images.tar.bz2 apt: + sources: + - ubuntu-toolchain-r-test packages: - cm-super - dvipng + - g++-7 - gdb - gir1.2-gtk-3.0 - graphviz @@ -68,6 +71,8 @@ matrix: - python: 3.5 # pytest-cov>=2.3.1 due to https://github.com/pytest-dev/pytest-cov/issues/124. env: + - CC=gcc-7 + - CXX=g++-7 - CYCLER=cycler==0.10 - DATEUTIL=python-dateutil==2.1 - NOSE=nose @@ -78,14 +83,30 @@ matrix: - PYTEST_COV=pytest-cov==2.3.1 - SPHINX=sphinx==1.3 - python: 3.5 - env: PYTHON_ARGS=-OO + env: + - CC=gcc-7 + - CXX=g++-7 + - PYTHON_ARGS=-OO - python: 3.6 - env: DELETE_FONT_CACHE=1 PANDAS='pandas<0.21.0' PYTEST_PEP8=pytest-pep8 RUN_PEP8=--pep8 + env: + - CC=gcc-7 + - CXX=g++-7 + - DELETE_FONT_CACHE=1 + - PANDAS='pandas<0.21.0' + - PYTEST_PEP8=pytest-pep8 + - RUN_PEP8=--pep8 - python: "nightly" - env: PRE=--pre + env: + - CC=gcc-7 + - CXX=g++-7 + - PRE=--pre - os: osx language: generic # https://github.com/travis-ci/travis-ci/issues/2312 only: master + env: + - PATH="/usr/local/opt/llvm/bin:$PATH" + - CPPFLAGS=-L/usr/local/opt/llvm/include + - LDFLAGS='-L/usr/local/opt/llvm/lib -Wl,-rpath,/usr/local/opt/llvm/lib' cache: # As for now travis caches only "$HOME/.cache/pip" # https://docs.travis-ci.com/user/caching/#pip-cache @@ -108,7 +129,9 @@ before_install: else ci/travis/silence brew update brew upgrade python - brew install ffmpeg imagemagick mplayer ccache + brew install ccache llvm + brew info llvm + brew install ffmpeg imagemagick mplayer hash -r which python python --version diff --git a/examples/misc/font_indexing.py b/examples/misc/font_indexing.py index 7625671968bd..de5947e94e22 100644 --- a/examples/misc/font_indexing.py +++ b/examples/misc/font_indexing.py @@ -4,39 +4,27 @@ ============= A little example that shows how the various indexing into the font -tables relate to one another. Mainly for mpl developers.... +tables relate to one another. Mainly for Matplotlib developers... """ -import matplotlib -from matplotlib.ft2font import FT2Font, KERNING_DEFAULT, KERNING_UNFITTED, KERNING_UNSCALED + +from matplotlib._ft2 import Kerning +import matplotlib.font_manager fname = matplotlib.get_data_path() + '/fonts/ttf/DejaVuSans.ttf' -font = FT2Font(fname) -font.set_charmap(0) - -codes = font.get_charmap().items() -#dsu = [(ccode, glyphind) for ccode, glyphind in codes] -#dsu.sort() -#for ccode, glyphind in dsu: -# try: name = font.get_glyph_name(glyphind) -# except RuntimeError: pass -# else: print('% 4d % 4d %s %s' % (glyphind, ccode, hex(int(ccode)), name)) - - -# make a charname to charcode and glyphind dictionary -coded = {} -glyphd = {} -for ccode, glyphind in codes: - name = font.get_glyph_name(glyphind) - coded[name] = ccode - glyphd[name] = glyphind - -code = coded['A'] -glyph = font.load_char(code) -print(glyph.bbox) -print(glyphd['A'], glyphd['V'], coded['A'], coded['V']) -print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_DEFAULT)) -print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_UNFITTED)) -print('AV', font.get_kerning(glyphd['A'], glyphd['V'], KERNING_UNSCALED)) -print('AV', font.get_kerning(glyphd['A'], glyphd['T'], KERNING_UNSCALED)) +font = matplotlib.font_manager.get_font(fname) + +# This assumes FreeType>=2.8.1, which automatically picks or synthesizes a +# Unicode charmap. + +def get_kerning(c1, c2, mode): + i1 = font.get_char_index(ord(c1)) + i2 = font.get_char_index(ord(c2)) + return font.get_kerning(i1, i2, mode) + + +print('AV default ', get_kerning('A', 'V', Kerning.DEFAULT)) +print('AV unfitted', get_kerning('A', 'V', Kerning.UNFITTED)) +print('AT default ', get_kerning('A', 'T', Kerning.DEFAULT)) +print('AT unfitted', get_kerning('A', 'T', Kerning.UNFITTED)) diff --git a/examples/misc/ftface_props.py b/examples/misc/ftface_props.py index b40a892715ae..f15cc126aff4 100644 --- a/examples/misc/ftface_props.py +++ b/examples/misc/ftface_props.py @@ -1,67 +1,53 @@ """ -============ -Ftface Props -============ - -This is a demo script to show you how to use all the properties of an -FT2Font object. These describe global font properties. For -individual character metrics, use the Glyph object, as returned by -load_char +=============== +Face properties +=============== + +This is a demo script to show you how to use all the properties of a Face +object. These describe global font properties. For individual character +metrics, use the Glyph object, as loaded from the glyph attribute after calling +load_char. """ import matplotlib -import matplotlib.ft2font as ft - - -#fname = '/usr/local/share/matplotlib/VeraIt.ttf' -fname = matplotlib.get_data_path() + '/fonts/ttf/DejaVuSans-Oblique.ttf' -#fname = '/usr/local/share/matplotlib/cmr10.ttf' - -font = ft.FT2Font(fname) - -print('Num faces :', font.num_faces) # number of faces in file -print('Num glyphs :', font.num_glyphs) # number of glyphs in the face -print('Family name :', font.family_name) # face family name -print('Style name :', font.style_name) # face style name -print('PS name :', font.postscript_name) # the postscript name -print('Num fixed :', font.num_fixed_sizes) # number of embedded bitmap in face - -# the following are only available if face.scalable -if font.scalable: - # the face global bounding box (xmin, ymin, xmax, ymax) - print('Bbox :', font.bbox) - # number of font units covered by the EM - print('EM :', font.units_per_EM) - # the ascender in 26.6 units - print('Ascender :', font.ascender) - # the descender in 26.6 units - print('Descender :', font.descender) - # the height in 26.6 units - print('Height :', font.height) - # maximum horizontal cursor advance - print('Max adv width :', font.max_advance_width) - # same for vertical layout - print('Max adv height :', font.max_advance_height) - # vertical position of the underline bar - print('Underline pos :', font.underline_position) - # vertical thickness of the underline - print('Underline thickness :', font.underline_thickness) - -for style in ('Italic', - 'Bold', - 'Scalable', - 'Fixed sizes', - 'Fixed width', - 'SFNT', - 'Horizontal', - 'Vertical', - 'Kerning', - 'Fast glyphs', - 'Multiple masters', - 'Glyph names', - 'External stream'): - bitpos = getattr(ft, style.replace(' ', '_').upper()) - 1 - print('%-17s:' % style, bool(font.style_flags & (1 << bitpos))) - -print(dir(font)) - -print(font.get_kerning) +from matplotlib import font_manager, _ft2 + + +fname = matplotlib.get_data_path() + "/fonts/ttf/DejaVuSans-Oblique.ttf" +font = font_manager.get_font(fname) + +print("Faces in file :", font.num_faces) +print("Glyphs in face :", font.num_glyphs) +print("Family name :", font.family_name) +print("Style name :", font.style_name) +print("Postscript name :", font.get_postscript_name()) +print("Embedded bitmap strikes:", font.num_fixed_sizes) + +if font.face_flags & _ft2.FACE_FLAG_SCALABLE: + print('Global bbox (xmin, ymin, xmax, ymax):', font.bbox) + print('Font units per EM :', font.units_per_EM) + print('Ascender (pixels) :', font.ascender) + print('Descender (pixels) :', font.descender) + print('Height (pixels) :', font.height) + print('Max horizontal advance :', font.max_advance_width) + print('Max vertical advance :', font.max_advance_height) + print('Underline position :', font.underline_position) + print('Underline thickness :', font.underline_thickness) + +for style in ['Style flag italic', + 'Style flag bold']: + flag = getattr(_ft2, style.replace(' ', '_').upper()) - 1 + print('%-26s:' % style, bool(font.style_flags & flag)) + +for style in ['Face flag scalable', + 'Face flag fixed sizes', + 'Face flag fixed width', + 'Face flag SFNT', + 'Face flag horizontal', + 'Face flag vertical', + 'Face flag kerning', + 'Face flag fast glyphs', + 'Face flag multiple masters', + 'Face flag glyph names', + 'Face flag external stream']: + flag = getattr(_ft2, style.replace(' ', '_').upper()) + print('%-26s:' % style, bool(font.face_flags & flag)) diff --git a/examples/text_labels_and_annotations/font_table_ttf_sgskip.py b/examples/text_labels_and_annotations/font_table_ttf_sgskip.py index 6de73e68dea3..928729874cc7 100644 --- a/examples/text_labels_and_annotations/font_table_ttf_sgskip.py +++ b/examples/text_labels_and_annotations/font_table_ttf_sgskip.py @@ -15,7 +15,7 @@ import os import matplotlib -from matplotlib.ft2font import FT2Font +from matplotlib import _ft2 from matplotlib.font_manager import FontProperties import matplotlib.pyplot as plt @@ -32,8 +32,8 @@ fontname = os.path.join(matplotlib.get_data_path(), 'fonts', 'ttf', 'DejaVuSans.ttf') -font = FT2Font(fontname) -codes = sorted(font.get_charmap().items()) +font = _ft2.Face(fontname) +codes = sorted(font.get_charmap().items()) # FIXME # a 16,16 array of character strings chars = [['' for c in range(16)] for r in range(16)] diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index c102044cc974..f2738192322b 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -12,7 +12,7 @@ * alpha blending * DPI scaling properly - everything scales properly (dashes, linewidths, etc) * draw polygon - * freetype2 w/ ft2font + * freetype2 TODO: @@ -28,19 +28,15 @@ import numpy as np from collections import OrderedDict from math import radians, cos, sin -from matplotlib import cbook, rcParams, __version__ + +from matplotlib import ( + _ft2, _png, cbook, colors as mcolors, font_manager, rcParams, __version__) from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, RendererBase, cursors) -from matplotlib.font_manager import findfont, get_font -from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, - LOAD_DEFAULT, LOAD_NO_AUTOHINT) +from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.transforms import Bbox, BboxBase -from matplotlib import colors as mcolors - -from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg -from matplotlib import _png try: from PIL import Image @@ -52,12 +48,12 @@ 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 + True: _ft2.LOAD_FORCE_AUTOHINT, + False: _ft2.LOAD_NO_HINTING, + 'either': _ft2.LOAD_DEFAULT, + 'native': _ft2.LOAD_NO_AUTOHINT, + 'auto': _ft2.LOAD_FORCE_AUTOHINT, + 'none': _ft2.LOAD_NO_HINTING } return mapping[rcParams['text.hinting']] @@ -105,9 +101,9 @@ def __setstate__(self, state): def _get_hinting_flag(self): if rcParams['text.hinting']: - return LOAD_FORCE_AUTOHINT + return _ft2.LOAD_FORCE_AUTOHINT else: - return LOAD_NO_HINTING + return _ft2.LOAD_NO_HINTING # for filtering to work with rasterization, methods needs to be wrapped. # maybe there is better way to do it. @@ -178,7 +174,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): yd = descent * cos(radians(angle)) x = np.round(x + ox + xd) y = np.round(y - oy + yd) - self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) + self._renderer.draw_text_image(font_image, x, y, angle, gc) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): """ @@ -192,23 +188,15 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if font is None: return None - if len(s) == 1 and ord(s) > 127: - font.load_char(ord(s), flags=flags) - else: - # We pass '0' for angle here, since it will be rotated (in raster - # space) in the following call to draw_text_image). - font.set_text(s, 0, flags=flags) - font.draw_glyphs_to_bitmap(antialiased=rcParams['text.antialiased']) - d = font.get_descent() / 64.0 + layout = _ft2.Layout.simple(s, font, flags) + d = -np.floor(layout.yMin) # The descent needs to be adjusted for the angle. - xo, yo = font.get_bitmap_offset() - xo /= 64.0 - yo /= 64.0 xd = -d * sin(radians(angle)) yd = d * cos(radians(angle)) self._renderer.draw_text_image( - font, np.round(x - xd + xo), np.round(y + yd + yo) + 1, angle, gc) + layout.render(antialiased=rcParams['text.antialiased']), + np.round(x - xd), np.round(y + yd), angle, gc) def get_text_width_height_descent(self, s, prop, ismath): """ @@ -232,13 +220,10 @@ def get_text_width_height_descent(self, s, prop, ismath): flags = get_hinting_flag() font = self._get_agg_font(prop) - font.set_text(s, 0.0, flags=flags) - w, h = font.get_width_height() # width and height of unrotated string - d = font.get_descent() - w /= 64.0 # convert from subpixels - h /= 64.0 - d /= 64.0 - return w, h, d + layout = _ft2.Layout.simple(s, font, flags) + return (layout.xMax - layout.xMin, + layout.yMax - layout.yMin, + -layout.yMin) def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): # todo, handle props, angle, origins @@ -265,13 +250,10 @@ def _get_agg_font(self, prop): """ Get the font for text instance t, cacheing for efficiency """ - fname = findfont(prop) - font = get_font(fname) - - font.clear() + fname = font_manager.findfont(prop) + font = font_manager.get_font(fname) size = prop.get_size_in_points() - font.set_size(size, self.dpi) - + font.set_char_size(size, self.dpi) return font def points_to_pixels(self, points): diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index e911d1c09391..98f5e512fdcc 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -8,7 +8,8 @@ import codecs import collections from datetime import datetime -from functools import total_ordering +from encodings import cp1252 +import functools from io import BytesIO import logging from math import ceil, cos, floor, pi, sin @@ -23,28 +24,24 @@ import numpy as np -from matplotlib import cbook, __version__, rcParams +import matplotlib +from matplotlib import ( + _ft2, _path, _png, cbook, dviread, ttconv, type1font, __version__, + rcParams) from matplotlib._pylab_helpers import Gcf +from matplotlib.afm import AFM from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer -from matplotlib.cbook import (get_realpath_and_stat, - is_writable_file_like, maxdict) +from matplotlib.cbook import ( + get_realpath_and_stat, is_writable_file_like, maxdict) +from matplotlib.dates import UTC from matplotlib.figure import Figure from matplotlib.font_manager import findfont, is_opentype_cff_font, get_font -from matplotlib.afm import AFM -import matplotlib.type1font as type1font -import matplotlib.dviread as dviread -from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE, - LOAD_NO_HINTING, KERNING_UNFITTED) from matplotlib.mathtext import MathTextParser -from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path -from matplotlib.dates import UTC -from matplotlib import _path -from matplotlib import _png -from matplotlib import ttconv +from matplotlib.transforms import Affine2D, BboxBase _log = logging.getLogger(__name__) @@ -247,7 +244,7 @@ def write(self, contents, file): write(b"\nendobj\n") -@total_ordering +@functools.total_ordering class Name(object): """PDF name object.""" __slots__ = ('name',) @@ -927,24 +924,15 @@ def embedTTFType3(font, characters, descriptor): } # Make the "Widths" array - from encodings import cp1252 - # The "decoding_map" was changed - # to a "decoding_table" as of Python 2.5. - if hasattr(cp1252, 'decoding_map'): - def decode_char(charcode): - return cp1252.decoding_map[charcode] or 0 - else: - def decode_char(charcode): - return ord(cp1252.decoding_table[charcode]) - def get_char_width(charcode): - s = decode_char(charcode) - width = font.load_char( - s, flags=LOAD_NO_SCALE | LOAD_NO_HINTING).horiAdvance - return cvt(width) + s = ord(cp1252.decoding_table[charcode]) + font.load_char( + s, flags=_ft2.LOAD_NO_SCALE | _ft2.LOAD_NO_HINTING) + width = font.glyph.horiAdvance + return cvt(width * 64) widths = [get_char_width(charcode) - for charcode in range(firstchar, lastchar+1)] + for charcode in range(firstchar, lastchar + 1)] descriptor['MaxWidth'] = max(widths) # Make the "Differences" array, sort the ccodes < 255 from @@ -953,8 +941,7 @@ def get_char_width(charcode): glyph_ids = [] differences = [] multi_byte_chars = set() - for c in characters: - ccode = c + for ccode in characters: gind = font.get_char_index(ccode) glyph_ids.append(gind) glyph_name = font.get_glyph_name(gind) @@ -1071,9 +1058,9 @@ def embedTTFType42(font, characters, descriptor): for c in characters: ccode = c gind = font.get_char_index(ccode) - glyph = font.load_char(ccode, - flags=LOAD_NO_SCALE | LOAD_NO_HINTING) - widths.append((ccode, cvt(glyph.horiAdvance))) + font.load_char( + ccode, flags=_ft2.LOAD_NO_SCALE | _ft2.LOAD_NO_HINTING) + widths.append((ccode, cvt(font.glyph.horiAdvance * 64))) if ccode < 65536: cid_to_gid_map[ccode] = chr(gind) max_ccode = max(ccode, max_ccode) @@ -1132,24 +1119,15 @@ def embedTTFType42(font, characters, descriptor): # Beginning of main embedTTF function... - # You are lost in a maze of TrueType tables, all different... - sfnt = font.get_sfnt() - try: - ps_name = sfnt[1, 0, 0, 6].decode('mac_roman') # Macintosh scheme - except KeyError: - # Microsoft scheme: - ps_name = sfnt[3, 1, 0x0409, 6].decode('utf-16be') - # (see freetype/ttnameid.h) - ps_name = ps_name.encode('ascii', 'replace') - ps_name = Name(ps_name) - pclt = font.get_sfnt_table('pclt') or {'capHeight': 0, 'xHeight': 0} - post = font.get_sfnt_table('post') or {'italicAngle': (0, 0)} + ps_name = Name(font.get_postscript_name().encode('ascii', 'replace')) + pclt = font.get_sfnt_table('pclt') or {'CapHeight': 0, 'xHeight': 0} + post = font.get_sfnt_table('post') or {'italicAngle': 0} ff = font.face_flags sf = font.style_flags flags = 0 symbolic = False # ps_name.name in ('Cmsy10', 'Cmmi10', 'Cmex10') - if ff & FIXED_WIDTH: + if ff & _ft2.FACE_FLAG_FIXED_WIDTH: flags |= 1 << 0 if 0: # TODO: serif flags |= 1 << 1 @@ -1157,7 +1135,7 @@ def embedTTFType42(font, characters, descriptor): flags |= 1 << 2 else: flags |= 1 << 5 - if sf & ITALIC: + if sf & _ft2.STYLE_FLAG_ITALIC: flags |= 1 << 6 if 0: # TODO: all caps flags |= 1 << 16 @@ -1173,11 +1151,11 @@ def embedTTFType42(font, characters, descriptor): 'FontBBox': [cvt(x, nearest=False) for x in font.bbox], 'Ascent': cvt(font.ascender, nearest=False), 'Descent': cvt(font.descender, nearest=False), - 'CapHeight': cvt(pclt['capHeight'], nearest=False), + 'CapHeight': cvt(pclt['CapHeight'], nearest=False), 'XHeight': cvt(pclt['xHeight']), - 'ItalicAngle': post['italicAngle'][1], # ??? + 'ItalicAngle': post['italicAngle'], 'StemV': 0 # ??? - } + } # The font subsetting to a Type 3 font does not work for # OpenType (.otf) that embed a Postscript CFF font, so avoid that -- @@ -1643,7 +1621,7 @@ def track_characters(self, font, s): if isinstance(font, str): fname = font else: - fname = font.fname + fname = font.pathname realpath, stat_key = get_realpath_and_stat(fname) used_characters = self.file.used_characters.setdefault( stat_key, (realpath, set())) @@ -2020,13 +1998,12 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): else: font = self._get_font_ttf(prop) self.track_characters(font, s) - font.set_text(s, 0.0, flags=LOAD_NO_HINTING) fonttype = rcParams['pdf.fonttype'] # We can't subset all OpenType fonts, so switch to Type 42 # in that case. - if is_opentype_cff_font(font.fname): + if is_opentype_cff_font(font.pathname): fonttype = 42 def check_simple_method(s): @@ -2107,21 +2084,20 @@ def draw_text_woven(chunks): 0, 0.001 * fontsize, newx, 0, Op.concat_matrix) name = self.file._get_xobject_symbol_name( - font.fname, glyph_name) + font.pathname, glyph_name) self.file.output(Name(name), Op.use_xobject) self.file.output(Op.grestore) # Move the pointer based on the character width # and kerning - glyph = font.load_char(ccode, - flags=LOAD_NO_HINTING) + font.load_char(ccode, flags=_ft2.LOAD_NO_HINTING) if lastgind is not None: - kern = font.get_kerning( - lastgind, gind, KERNING_UNFITTED) + kern, _ = font.get_kerning( + lastgind, gind, _ft2.Kerning.UNFITTED) else: kern = 0 lastgind = gind - newx += kern/64.0 + glyph.linearHoriAdvance/65536.0 + newx += kern + font.glyph.linearHoriAdvance if mode == 1: self.file.output(Op.end_text) @@ -2138,14 +2114,10 @@ def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() - w, h, d = texmanager.get_text_width_height_descent(s, fontsize, - renderer=self) - return w, h, d - - if ismath: - w, h, d, glyphs, rects, used_characters = \ - self.mathtext_parser.parse(s, 72, prop) - + w, h, d = texmanager.get_text_width_height_descent( + s, fontsize, renderer=self) + elif ismath: + w, h, d, _, _, _ = self.mathtext_parser.parse(s, 72, prop) elif rcParams['pdf.use14corefonts']: font = self._get_font_afm(prop) l, b, w, h, d = font.get_str_bbox_and_descent(s) @@ -2155,13 +2127,10 @@ def get_text_width_height_descent(self, s, prop, ismath): d *= scale / 1000 else: font = self._get_font_ttf(prop) - font.set_text(s, 0.0, flags=LOAD_NO_HINTING) - w, h = font.get_width_height() - scale = (1.0 / 64.0) - w *= scale - h *= scale - d = font.get_descent() - d *= scale + layout = _ft2.Layout.simple(s, font, _ft2.LOAD_NO_HINTING) + w = layout.xMax - layout.xMin + h = layout.yMax - layout.yMin + d = -layout.yMin return w, h, d def _get_font_afm(self, prop): @@ -2185,8 +2154,7 @@ def _get_font_afm(self, prop): def _get_font_ttf(self, prop): filename = findfont(prop) font = get_font(filename) - font.clear() - font.set_size(prop.get_size_in_points(), 72) + font.set_char_size(prop.get_size_in_points(), 72) return font def flipy(self): diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index f4c9fa768fba..3e05a5f05f2c 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -8,37 +8,38 @@ import six from six.moves import StringIO -import glob, os, shutil, sys, time, datetime +import binascii +import datetime +import glob import io import logging +import os +import re +import shutil import subprocess +import sys +import tempfile +import time + +import numpy as np -from tempfile import mkstemp -from matplotlib import cbook, __version__, rcParams, checkdep_ghostscript +from matplotlib import _ft2, _path, __version__, rcParams, checkdep_ghostscript from matplotlib.afm import AFM from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase) - from matplotlib.cbook import (get_realpath_and_stat, is_writable_file_like, maxdict, file_requires_unicode) - from matplotlib.font_manager import findfont, is_opentype_cff_font, get_font -from matplotlib.ft2font import KERNING_DEFAULT, LOAD_NO_HINTING -from matplotlib.ttconv import convert_ttf_to_ps from matplotlib.mathtext import MathTextParser from matplotlib._mathtext_data import uni2type1 from matplotlib.path import Path -from matplotlib import _path from matplotlib.transforms import Affine2D +from matplotlib.ttconv import convert_ttf_to_ps from matplotlib.backends.backend_mixed import MixedModeRenderer -import numpy as np -import binascii -import re - _log = logging.getLogger(__name__) backend_version = 'Level II' @@ -227,7 +228,7 @@ def __init__(self, width, height, pswriter, imagedpi=72): def track_characters(self, font, s): """Keeps track of which characters are required from each font.""" - realpath, stat_key = get_realpath_and_stat(font.fname) + realpath, stat_key = get_realpath_and_stat(font.pathname) used_characters = self.used_characters.setdefault( stat_key, (realpath, set())) used_characters[1].update([ord(x) for x in s]) @@ -335,34 +336,25 @@ def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() - w, h, d = texmanager.get_text_width_height_descent(s, fontsize, - renderer=self) - return w, h, d - - if ismath: - width, height, descent, pswriter, used_characters = \ - self.mathtext_parser.parse(s, 72, prop) - return width, height, descent - - if rcParams['ps.useafm']: + w, h, d = texmanager.get_text_width_height_descent( + s, fontsize, renderer=self) + elif ismath: + w, h, d, _, _ = self.mathtext_parser.parse(s, 72, prop) + elif rcParams['ps.useafm']: if ismath: s = s[1:-1] font = self._get_font_afm(prop) - l,b,w,h,d = font.get_str_bbox_and_descent(s) - + l, b, w, h, d = font.get_str_bbox_and_descent(s) fontsize = prop.get_size_in_points() - scale = 0.001*fontsize + scale = fontsize / 1000 w *= scale h *= scale d *= scale - return w, h, d - - font = self._get_font_ttf(prop) - font.set_text(s, 0.0, flags=LOAD_NO_HINTING) - w, h = font.get_width_height() - w /= 64.0 # convert from subpixels - h /= 64.0 - d = font.get_descent() - d /= 64.0 + else: + font = self._get_font_ttf(prop) + layout = _ft2.Layout.simple(s, font, _ft2.LOAD_NO_HINTING) + w = layout.xMax - layout.xMin + h = layout.yMax - layout.yMin + d = -layout.yMin return w, h, d def flipy(self): @@ -386,11 +378,9 @@ def _get_font_afm(self, prop): return font def _get_font_ttf(self, prop): - fname = findfont(prop) - font = get_font(fname) - font.clear() + font = get_font(findfont(prop)) size = prop.get_size_in_points() - font.set_size(size, 72.0) + font.set_char_size(size, 72.0) return font def _rgb(self, rgba): @@ -709,17 +699,11 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): else: font = self._get_font_ttf(prop) - font.set_text(s, 0, flags=LOAD_NO_HINTING) self.track_characters(font, s) self.set_color(*gc.get_rgb()) - sfnt = font.get_sfnt() - try: - ps_name = sfnt[1, 0, 0, 6].decode('mac_roman') - except KeyError: - ps_name = sfnt[3, 1, 0x0409, 6].decode('utf-16be') - ps_name = ps_name.encode('ascii', 'replace').decode('ascii') - self.set_font(ps_name, prop.get_size_in_points()) + self.set_font( + font.get_postscript_name(), prop.get_size_in_points()) lastgind = None lines = [] @@ -734,18 +718,18 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): gind = 0 else: name = font.get_glyph_name(gind) - glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: - kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) + kern, _ = font.get_kerning( + lastgind, gind, _ft2.Kerning.DEFAULT) else: kern = 0 lastgind = gind - thisx += kern/64.0 + thisx += kern lines.append('%f %f m /%s glyphshow'%(thisx, thisy, name)) - thisx += glyph.linearHoriAdvance/65536.0 - + font.load_char(ccode, flags=_ft2.LOAD_NO_HINTING) + thisx += font.glyph.linearHoriAdvance thetext = '\n'.join(lines) ps = """gsave @@ -1161,7 +1145,7 @@ def print_figure_impl(fh): if rcParams['ps.usedistiller']: # We are going to use an external program to process the output. # Write to a temporary file. - fd, tmpfile = mkstemp() + fd, tmpfile = tempfile.mkstemp() try: with io.open(fd, 'w', encoding='latin-1') as fh: print_figure_impl(fh) @@ -1269,7 +1253,7 @@ def write(self, *kl, **kwargs): # write to a temp file, we'll move it to outfile when done - fd, tmpfile = mkstemp() + fd, tmpfile = tempfile.mkstemp() try: with io.open(fd, 'w', encoding='latin-1') as fh: # write the Encapsulated PostScript headers diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index bb5fec9e9a5f..dc4598a00b6e 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -17,18 +17,16 @@ import numpy as np -from matplotlib import cbook, __version__, rcParams +from matplotlib import ( + _ft2, _path, _png, cbook, colors as mcolors, __version__, rcParams) from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer -from matplotlib.colors import rgb2hex from matplotlib.font_manager import findfont, get_font -from matplotlib.ft2font import LOAD_NO_HINTING from matplotlib.mathtext import MathTextParser from matplotlib.path import Path -from matplotlib import _path from matplotlib.transforms import Affine2D, Affine2DBase -from matplotlib import _png + _log = logging.getLogger(__name__) @@ -331,11 +329,8 @@ def _make_flip_transform(self, transform): .translate(0.0, self.height)) def _get_font(self, prop): - fname = findfont(prop) - font = get_font(fname) - font.clear() - size = prop.get_size_in_points() - font.set_size(size, 72.0) + font = get_font(findfont(prop)) + font.set_char_size(prop.get_size_in_points(), 72.0) return font def _get_hatch(self, gc, rgbFace): @@ -376,7 +371,7 @@ def _write_hatches(self): if face is None: fill = 'none' else: - fill = rgb2hex(face) + fill = mcolors.to_hex(face) writer.element( 'rect', x="0", y="0", width=six.text_type(HATCH_SIZE+1), @@ -386,8 +381,8 @@ def _write_hatches(self): 'path', d=path_data, style=generate_css({ - 'fill': rgb2hex(stroke), - 'stroke': rgb2hex(stroke), + 'fill': mcolors.to_hex(stroke), + 'stroke': mcolors.to_hex(stroke), 'stroke-width': six.text_type(rcParams['hatch.linewidth']), 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter' @@ -414,7 +409,7 @@ def _get_style_dict(self, gc, rgbFace): attrib['fill'] = 'none' else: if tuple(rgbFace[:3]) != (0, 0, 0): - attrib['fill'] = rgb2hex(rgbFace) + attrib['fill'] = mcolors.to_hex(rgbFace) if len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha: attrib['fill-opacity'] = short_float_fmt(rgbFace[3]) @@ -429,7 +424,7 @@ def _get_style_dict(self, gc, rgbFace): linewidth = gc.get_linewidth() if linewidth: rgb = gc.get_rgb() - attrib['stroke'] = rgb2hex(rgb) + attrib['stroke'] = mcolors.to_hex(rgb) if not forced_alpha and rgb[3] != 1.0: attrib['stroke-opacity'] = short_float_fmt(rgb[3]) if linewidth != 1.0: @@ -499,7 +494,7 @@ def _write_svgfonts(self): for font_fname, chars in six.iteritems(self._fonts): font = get_font(font_fname) font.set_size(72, 72) - sfnt = font.get_sfnt() + sfnt = font.get_sfnt_name_table() writer.start('font', id=sfnt[1, 0, 0, 4].decode("mac_roman")) writer.element( 'font-face', @@ -510,7 +505,8 @@ def _write_svgfonts(self): 'bbox': ' '.join( short_float_fmt(x / 64.0) for x in font.bbox)}) for char in chars: - glyph = font.load_char(char, flags=LOAD_NO_HINTING) + font.load_char(char, flags=_ft2.LOAD_NO_HINTING) + glyph = font.glyph verts, codes = font.get_path() path = Path(verts, codes) path_data = self._convert_path(path) @@ -522,7 +518,7 @@ def _write_svgfonts(self): # 'glyph-name': name, 'unicode': unichr(char), 'horiz-adv-x': - short_float_fmt(glyph.linearHoriAdvance / 65536.0)}) + short_float_fmt(glyph.linearHoriAdvance)}) writer.end('font') writer.end('defs') @@ -740,12 +736,12 @@ def draw_gouraud_triangle(self, gc, points, colors, trans): writer.element( 'stop', offset='0', - style=generate_css({'stop-color': rgb2hex(c), + style=generate_css({'stop-color': mcolors.to_hex(c), 'stop-opacity': short_float_fmt(c[-1])})) writer.element( 'stop', offset='1', - style=generate_css({'stop-color': rgb2hex(c), + style=generate_css({'stop-color': mcolors.to_hex(c), 'stop-opacity': "0"})) writer.end('linearGradient') @@ -761,7 +757,7 @@ def draw_gouraud_triangle(self, gc, points, colors, trans): writer.element( 'use', attrib={'xlink:href': href, - 'fill': rgb2hex(avg_color), + 'fill': mcolors.to_hex(avg_color), 'fill-opacity': short_float_fmt(avg_color[-1])}) for i in range(3): writer.element( @@ -895,7 +891,7 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): glyph_map=self._glyph_map text2path = self._text2path - color = rgb2hex(gc.get_rgb()) + color = mcolors.to_hex(gc.get_rgb()) fontsize = prop.get_size_in_points() style = {} @@ -998,7 +994,7 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): writer = self.writer - color = rgb2hex(gc.get_rgb()) + color = mcolors.to_hex(gc.get_rgb()) style = {} if color != '#000000': style['fill'] = color @@ -1007,7 +1003,7 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): if not ismath: font = self._get_font(prop) - font.set_text(s, 0.0, flags=LOAD_NO_HINTING) + font.set_text(s, 0.0, flags=_ft2.LOAD_NO_HINTING) fontsize = prop.get_size_in_points() @@ -1060,7 +1056,7 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): writer.element('text', s, attrib=attrib) if rcParams['svg.fonttype'] == 'svgfont': - fontset = self._fonts.setdefault(font.fname, set()) + fontset = self._fonts.setdefault(font.pathname, set()) for c in s: fontset.add(ord(c)) else: @@ -1097,7 +1093,7 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): if rcParams['svg.fonttype'] == 'svgfont': for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: - fontset = self._fonts.setdefault(font.fname, set()) + fontset = self._fonts.setdefault(font.pathname, set()) fontset.add(thetext) for style, chars in six.iteritems(spans): diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 30fc59684df0..a7e39cced462 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -276,7 +276,7 @@ def get_label_width(self, lev, fmt, fsize): self._mathtext_parser = mathtext.MathTextParser('bitmap') img, _ = self._mathtext_parser.parse(lev, dpi=72, prop=self.labelFontProps) - lw = img.get_width() # at dpi=72, the units are PostScript points + lh, lw = img.shape # at dpi=72, the units are PostScript points else: # width is much less than "font size" lw = (len(lev)) * fsize * 0.6 diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index e7a3ce071195..79ffcf1cd217 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -34,14 +34,14 @@ from collections import Iterable from functools import lru_cache import json +import logging import os import subprocess import sys from threading import Timer import warnings -import logging -from matplotlib import afm, cbook, ft2font, rcParams, get_cachedir +from matplotlib import _ft2, afm, cbook, rcParams, get_cachedir from matplotlib.fontconfig_pattern import ( parse_fontconfig_pattern, generate_fontconfig_pattern) @@ -143,9 +143,10 @@ def get_fontext_synonyms(fontext): Return a list of file extensions extensions that are synonyms for the given file extension *fileext*. """ - return {'ttf': ('ttf', 'otf'), - 'otf': ('ttf', 'otf'), - 'afm': ('afm',)}[fontext] + for exts in [["afm"], ["otf", "ttf"]]: + if fontext in exts: + return exts + raise ValueError("Unknown font extension") def list_fonts(directory, extensions): @@ -353,20 +354,19 @@ def ttfFontProperty(font): Parameters ---------- - font : `.FT2Font` + font : `.Font` The TrueType font file from which information will be extracted. Returns ------- `FontEntry` The extracted font properties. - """ name = font.family_name # Styles are: italic, oblique, and normal (default) - sfnt = font.get_sfnt() + sfnt = font.get_sfnt_name_table() sfnt2 = sfnt.get((1,0,0,2)) sfnt4 = sfnt.get((1,0,0,4)) if sfnt2: @@ -383,7 +383,7 @@ def ttfFontProperty(font): style = 'italic' elif sfnt2.find('regular') >= 0: style = 'normal' - elif font.style_flags & ft2font.ITALIC: + elif font.style_flags & _ft2.STYLE_FLAG_ITALIC: style = 'italic' else: style = 'normal' @@ -398,7 +398,7 @@ def ttfFontProperty(font): weight = next((w for w in weight_dict if sfnt4.find(w) >= 0), None) if not weight: - if font.style_flags & ft2font.BOLD: + if font.style_flags & _ft2.STYLE_FLAG_BOLD: weight = 700 else: weight = 400 @@ -427,11 +427,12 @@ def ttfFontProperty(font): # Length value is an absolute font size, e.g., 12pt # Percentage values are in 'em's. Most robust specification. - if not font.scalable: + if not (font.face_flags & _ft2.FACE_FLAG_SCALABLE): raise NotImplementedError("Non-scalable fonts are not supported") size = 'scalable' - return FontEntry(font.fname, name, style, variant, weight, stretch, size) + return FontEntry( + font.pathname, name, style, variant, weight, stretch, size) def afmFontProperty(fontpath, font): @@ -501,15 +502,7 @@ def afmFontProperty(fontpath, font): return FontEntry(fontpath, name, style, variant, weight, stretch, size) -def createFontList(fontfiles, fontext='ttf'): - """ - A function to create a font lookup list. The default is to create - a list of TrueType fonts. An AFM font list can optionally be - created. - """ - - fontlist = [] - # Add fonts from list of known font files. +def _iter_font_list(fontfiles, fontext): seen = set() for fpath in fontfiles: _log.debug('createFontDict: %s', fpath) @@ -532,28 +525,26 @@ def createFontList(fontfiles, fontext='ttf'): finally: fh.close() try: - prop = afmFontProperty(fpath, font) + yield afmFontProperty(fpath, font) except KeyError: continue else: try: - font = ft2font.FT2Font(fpath) + font = get_font(fpath) except RuntimeError: _log.info("Could not open font file %s", fpath) continue - except UnicodeError: - _log.info("Cannot handle unicode filenames") - continue - except OSError: - _log.info("IO error - cannot open font file %s", fpath) - continue try: - prop = ttfFontProperty(font) + yield ttfFontProperty(font) except (KeyError, RuntimeError, ValueError, NotImplementedError): continue - fontlist.append(prop) - return fontlist + +def createFontList(fontfiles, fontext="ttf"): + """ + Create a font lookup list -- by default TrueType, but AFM is also possible. + """ + return list(_iter_font_list(fontfiles, fontext)) class FontProperties(object): @@ -1246,7 +1237,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, for font in fontlist: if (directory is not None and - os.path.commonprefix([os.path.normcase(font.fname), + os.path.commonprefix([os.path.normcase(font.pathname), directory]) != directory): continue # Matching family should have highest priority, so it is multiplied @@ -1315,12 +1306,11 @@ def is_opentype_cff_font(filename): _fmcache = None -_get_font = lru_cache(64)(ft2font.FT2Font) - -def get_font(filename, hinting_factor=None): +def get_font(filename, index=0, hinting_factor=None, + _get_font=lru_cache(64)(_ft2.Face)): if hinting_factor is None: hinting_factor = rcParams['text.hinting_factor'] - return _get_font(filename, hinting_factor) + return _get_font(filename, index, hinting_factor) # The experimental fontconfig-based backend. diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index 8541f5fc3d01..f93cb744bc7e 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -31,18 +31,15 @@ ParserElement.enablePackrat() -from matplotlib import _png, cbook, colors as mcolors, get_data_path, rcParams +from matplotlib import ( + _ft2, _png, cbook, colors as mcolors, get_data_path, rcParams) from matplotlib.afm import AFM from matplotlib.cbook import get_realpath_and_stat -from matplotlib.ft2font import FT2Image, KERNING_DEFAULT, LOAD_NO_HINTING from matplotlib.font_manager import findfont, FontProperties, get_font from matplotlib._mathtext_data import (latex_to_bakoma, latex_to_standard, tex2uni, latex_to_cmex, stix_virtual_fonts) -#################### - - ############################################################################## # FONTS @@ -132,7 +129,8 @@ def get_hinting_type(self): Get the FreeType hinting type to use with this particular backend. """ - return LOAD_NO_HINTING + return _ft2.LOAD_NO_HINTING + class MathtextBackendAgg(MathtextBackend): """ @@ -140,80 +138,39 @@ class MathtextBackendAgg(MathtextBackend): transferred to the Agg image by the Agg backend. """ def __init__(self): - self.ox = 0 - self.oy = 0 - self.image = None - self.mode = 'bbox' - self.bbox = [0, 0, 0, 0] - MathtextBackend.__init__(self) - - def _update_bbox(self, x1, y1, x2, y2): - self.bbox = [min(self.bbox[0], x1), - min(self.bbox[1], y1), - max(self.bbox[2], x2), - max(self.bbox[3], y2)] - - def set_canvas_size(self, w, h, d): - MathtextBackend.set_canvas_size(self, w, h, d) - if self.mode != 'bbox': - self.image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0))) + super(MathtextBackendAgg, self).__init__() + self._positioned_glyphs = [] + self._rectangles = [] def render_glyph(self, ox, oy, info): - if self.mode == 'bbox': - self._update_bbox(ox + info.metrics.xmin, - oy - info.metrics.ymax, - ox + info.metrics.xmax, - oy - info.metrics.ymin) - else: - info.font.draw_glyph_to_bitmap( - self.image, ox, oy - info.metrics.iceberg, info.glyph, - antialiased=rcParams['text.antialiased']) + self._positioned_glyphs.append((info.glyph, ox, oy)) def render_rect_filled(self, x1, y1, x2, y2): - if self.mode == 'bbox': - self._update_bbox(x1, y1, x2, y2) - else: - height = max(int(y2 - y1) - 1, 0) - if height == 0: - center = (y2 + y1) / 2.0 - y = int(center - (height + 1) / 2.0) - else: - y = int(y1) - self.image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height) + self._rectangles.append((x1, x2, y1, y2)) def get_results(self, box, used_characters): - self.mode = 'bbox' - orig_height = box.height - orig_depth = box.depth ship(0, 0, box) - bbox = self.bbox - bbox = [bbox[0] - 1, bbox[1] - 1, bbox[2] + 1, bbox[3] + 1] - self.mode = 'render' - self.set_canvas_size( - bbox[2] - bbox[0], - (bbox[3] - bbox[1]) - orig_depth, - (bbox[3] - bbox[1]) - orig_height) - ship(-bbox[0], -bbox[1], box) - result = (self.ox, - self.oy, - self.width, - self.height + self.depth, - self.depth, - self.image, - used_characters) - self.image = None - return result + layout = _ft2.Layout.manual( + self._positioned_glyphs, self._rectangles, self.get_hinting_type()) + return (0, 0, + self.width, + self.height + self.depth, + self.depth, + layout.render(antialiased=rcParams["text.antialiased"]), + used_characters) def get_hinting_type(self): from matplotlib.backends import backend_agg return backend_agg.get_hinting_flag() + class MathtextBackendBitmap(MathtextBackendAgg): def get_results(self, box, used_characters): ox, oy, width, height, depth, image, characters = \ MathtextBackendAgg.get_results(self, box, used_characters) return image, depth + class MathtextBackendPs(MathtextBackend): """ Store information to write a mathtext rendering to the PostScript @@ -264,7 +221,7 @@ def __init__(self): self.rects = [] def render_glyph(self, ox, oy, info): - filename = info.font.fname + filename = info.font.pathname oy = self.height - oy + info.offset self.glyphs.append( (ox, oy, filename, info.fontsize, @@ -475,7 +432,7 @@ def render_glyph(self, ox, oy, facename, font_class, sym, fontsize, dpi): - *dpi*: The dpi to draw at. """ info = self._get_info(facename, font_class, sym, fontsize, dpi) - realpath, stat_key = get_realpath_and_stat(info.font.fname) + realpath, stat_key = get_realpath_and_stat(info.font.pathname) used_characters = self.used_characters.setdefault( stat_key, (realpath, set())) used_characters[1].add(info.num) @@ -528,8 +485,8 @@ def get_sized_alternatives_for_symbol(self, fontname, sym): class TruetypeFonts(Fonts): """ - A generic base class for all font setups that use Truetype fonts - (through FT2Font). + A generic base class for all font setups that use Truetype fonts (through + _ft2). """ def __init__(self, default_font_prop, mathtext_backend): Fonts.__init__(self, default_font_prop, mathtext_backend) @@ -554,12 +511,12 @@ def _get_font(self, font): if cached_font is None and os.path.exists(basename): cached_font = get_font(basename) self._fonts[basename] = cached_font - self._fonts[cached_font.postscript_name] = cached_font - self._fonts[cached_font.postscript_name.lower()] = cached_font + self._fonts[cached_font.get_postscript_name()] = cached_font + self._fonts[cached_font.get_postscript_name().lower()] = cached_font return cached_font def _get_offset(self, font, glyph, fontsize, dpi): - if font.postscript_name == 'Cmex10': + if font.get_postscript_name() == 'Cmex10': return ((glyph.height/64.0/2.0) + (fontsize/3.0 * dpi/72.0)) return 0. @@ -572,30 +529,29 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): font, num, symbol_name, fontsize, slanted = \ self._get_glyph(fontname, font_class, sym, fontsize, math) - font.set_size(fontsize, dpi) - glyph = font.load_char( - num, - flags=self.mathtext_backend.get_hinting_type()) + font.set_char_size(fontsize, dpi) + font.load_char(num, flags=self.mathtext_backend.get_hinting_type()) + glyph = font.glyph - xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox] + xmin, xmax, ymin, ymax = glyph.get_cbox(_ft2.GlyphBbox.SUBPIXELS) offset = self._get_offset(font, glyph, fontsize, dpi) metrics = types.SimpleNamespace( - advance = glyph.linearHoriAdvance/65536.0, - height = glyph.height/64.0, - width = glyph.width/64.0, + advance = glyph.linearHoriAdvance, + height = glyph.height, + width = glyph.width, xmin = xmin, xmax = xmax, ymin = ymin+offset, ymax = ymax+offset, # iceberg is the equivalent of TeX's "height" - iceberg = glyph.horiBearingY/64.0 + offset, + iceberg = glyph.horiBearingY + offset, slanted = slanted ) result = self.glyphd[key] = types.SimpleNamespace( font = font, fontsize = fontsize, - postscript_name = font.postscript_name, + postscript_name = font.get_postscript_name(), metrics = metrics, symbol_name = symbol_name, num = num, @@ -606,7 +562,7 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): def get_xheight(self, fontname, fontsize, dpi): font = self._get_font(fontname) - font.set_size(fontsize, dpi) + font.set_char_size(fontsize, dpi) pclt = font.get_sfnt_table('pclt') if pclt is None: # Some fonts don't store the xHeight, so we do a poor man's xHeight @@ -627,9 +583,12 @@ def get_kern(self, font1, fontclass1, sym1, fontsize1, info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) font = info1.font - return font.get_kerning(info1.num, info2.num, KERNING_DEFAULT) / 64.0 - return Fonts.get_kern(self, font1, fontclass1, sym1, fontsize1, - font2, fontclass2, sym2, fontsize2, dpi) + kx, ky = font.get_kerning( + info1.num, info2.num, _ft2.Kerning.DEFAULT) + return kx + else: + return Fonts.get_kern(self, font1, fontclass1, sym1, fontsize1, + font2, fontclass2, sym2, fontsize2, dpi) class BakomaFonts(TruetypeFonts): """ @@ -1087,7 +1046,7 @@ def __init__(self, default_font_prop): directory=self.basepath) with open(filename, 'rb') as fd: default_font = AFM(fd) - default_font.fname = filename + default_font.pathname = filename self.fonts['default'] = default_font self.fonts['regular'] = default_font @@ -1104,7 +1063,7 @@ def _get_font(self, font): fname = os.path.join(self.basepath, basename + ".afm") with open(fname, 'rb') as fd: cached_font = AFM(fd) - cached_font.fname = fname + cached_font.pathname = fname self.fonts[basename] = cached_font self.fonts[cached_font.get_fontname()] = cached_font return cached_font @@ -3295,9 +3254,7 @@ def to_mask(self, texstr, dpi=120, fontsize=14): assert self._output == "bitmap" prop = FontProperties(size=fontsize) ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop) - - x = ftimage.as_array() - return x, depth + return ftimage, depth def to_rgba(self, texstr, color='black', dpi=120, fontsize=14): """ diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index 301f7b54d4c2..d3fe742f9fb7 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -15,18 +15,13 @@ # This allows other functions here to be used by pytest-based testing suites # without requiring nose to be installed. - import matplotlib as mpl import matplotlib.style import matplotlib.units import matplotlib.testing -from matplotlib import cbook -from matplotlib import ticker -from matplotlib import pyplot as plt -from matplotlib import ft2font -from matplotlib.testing.compare import ( - comparable_formats, compare_images, make_test_filename) +from matplotlib import _ft2, cbook, pyplot as plt, ticker from . import _copy_metadata, is_called_from_pytest +from .compare import comparable_formats, compare_images, make_test_filename from .exceptions import ImageComparisonFailure @@ -152,7 +147,7 @@ def check_freetype_version(ver): if isinstance(ver, six.string_types): ver = (ver, ver) ver = [version.StrictVersion(x) for x in ver] - found = version.StrictVersion(ft2font.__freetype_version__) + found = version.StrictVersion(_ft2.__freetype_version__) return found >= ver[0] and found <= ver[1] @@ -163,7 +158,7 @@ def _checked_on_freetype_version(required_freetype_version): reason = ("Mismatched version of freetype. " "Test requires '%s', you have '%s'" % - (required_freetype_version, ft2font.__freetype_version__)) + (required_freetype_version, _ft2.__freetype_version__)) return _knownfailureif('indeterminate', msg=reason, known_exception_class=ImageComparisonFailure) diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index cdc1093e1417..386e769b5c9a 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -197,11 +197,10 @@ def test_mathfont_rendering(baseline_images, fontset, index, test): def test_fontinfo(): import matplotlib.font_manager as font_manager - import matplotlib.ft2font as ft2font - fontpath = font_manager.findfont("DejaVu Sans") - font = ft2font.FT2Font(fontpath) + from matplotlib._ft2 import Face + face = Face(font_manager.findfont("DejaVu Sans")) table = font.get_sfnt_table("head") - assert table['version'] == (1, 0) + assert table['Table_Version'] == 65536 @pytest.mark.parametrize( diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 7654a2ae1115..a12d8d6a2860 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -6,22 +6,17 @@ from collections import OrderedDict import six -from six.moves import zip +from six.moves import urllib, zip import warnings import numpy as np -from matplotlib.path import Path -from matplotlib import rcParams -import matplotlib.font_manager as font_manager -from matplotlib.ft2font import KERNING_DEFAULT, LOAD_NO_HINTING -from matplotlib.ft2font import LOAD_TARGET_LIGHT +from matplotlib import _ft2, cbook, dviread, font_manager, rcParams +from matplotlib.font_manager import FontProperties from matplotlib.mathtext import MathTextParser -import matplotlib.dviread as dviread -from matplotlib.font_manager import FontProperties, get_font +from matplotlib.path import Path from matplotlib.transforms import Affine2D -from six.moves.urllib.parse import quote as urllib_quote class TextToPath(object): @@ -35,12 +30,8 @@ class TextToPath(object): def __init__(self): self.mathtext_parser = MathTextParser('path') self.tex_font_map = None - - from matplotlib.cbook import maxdict - self._ps_fontd = maxdict(50) - + self._ps_fontd = cbook.maxdict(50) self._texmanager = None - self._adobe_standard_encoding = None def _get_adobe_standard_encoding(self): @@ -52,35 +43,29 @@ def _get_font(self, prop): """ find a ttf font. """ - fname = font_manager.findfont(prop) - font = get_font(fname) - font.set_size(self.FONT_SCALE, self.DPI) - + font = font_manager.get_font(font_manager.findfont(prop)) + font.set_char_size(self.FONT_SCALE, self.DPI) return font def _get_hinting_flag(self): - return LOAD_NO_HINTING + return _ft2.LOAD_NO_HINTING def _get_char_id(self, font, ccode): """ Return a unique id for the given font and character-code set. """ - sfnt = font.get_sfnt() - try: - ps_name = sfnt[1, 0, 0, 6].decode('mac_roman') - except KeyError: - ps_name = sfnt[3, 1, 0x0409, 6].decode('utf-16be') - char_id = urllib_quote('%s-%x' % (ps_name, ccode)) - return char_id + return urllib.parse.quote( + "{}-{:x}".format(font.get_postscript_name(), ccode)) def _get_char_id_ps(self, font, ccode): """ Return a unique id for the given font and character-code set (for tex). """ - ps_name = font.get_ps_font_info()[2] - char_id = urllib_quote('%s-%d' % (ps_name, ccode)) + ps_name = font.get_ps_font_info()["full_name"] + char_id = urllib.parse.quote("{}-{}".format(ps_name, ccode)) return char_id + @cbook.deprecated("3.0") def glyph_to_path(self, font, currx=0.): """ convert the ft2font glyph to vertices and codes. @@ -94,8 +79,8 @@ def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() - w, h, d = texmanager.get_text_width_height_descent(s, fontsize, - renderer=None) + w, h, d = texmanager.get_text_width_height_descent( + s, fontsize, renderer=None) return w, h, d fontsize = prop.get_size_in_points() @@ -104,18 +89,15 @@ def get_text_width_height_descent(self, s, prop, ismath): if ismath: prop = prop.copy() prop.set_size(self.FONT_SCALE) - width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width * scale, height * scale, descent * scale font = self._get_font(prop) - font.set_text(s, 0.0, flags=LOAD_NO_HINTING) - w, h = font.get_width_height() - w /= 64.0 # convert from subpixels - h /= 64.0 - d = font.get_descent() - d /= 64.0 + layout = _ft2.Layout.simple(s, font, _ft2.LOAD_NO_HINTING) + w = layout.xMax - layout.xMin + h = layout.yMax - layout.yMin + d = -layout.yMin return w * scale, h * scale, d * scale def get_text_path(self, prop, s, ismath=False, usetex=False): @@ -193,20 +175,21 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, if gind is None: ccode = ord('?') gind = 0 - if lastgind is not None: - kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) + kern, _ = font.get_kerning( + lastgind, gind, _ft2.Kerning.DEFAULT) else: kern = 0 - glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) - horiz_advance = glyph.linearHoriAdvance / 65536 + font.load_char(ccode, flags=_ft2.LOAD_NO_HINTING) + glyph = font.glyph + horiz_advance = glyph.linearHoriAdvance char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: - glyph_map_new[char_id] = self.glyph_to_path(font) + glyph_map_new[char_id] = _ft2.glyph_to_path(glyph) - currx += kern / 64 + currx += kern xpositions.append(currx) glyph_ids.append(char_id) @@ -253,10 +236,9 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, for font, fontsize, ccode, ox, oy in glyphs: char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: - font.clear() - font.set_size(self.FONT_SCALE, self.DPI) - glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) - glyph_map_new[char_id] = self.glyph_to_path(font) + font.set_char_size(self.FONT_SCALE, self.DPI) + font.load_char(ccode, flags=_ft2.LOAD_NO_HINTING) + glyph_map_new[char_id] = _ft2.glyph_to_path(font.glyph) xpositions.append(ox) ypositions.append(oy) @@ -337,24 +319,23 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, "The font may lack a Type-1 version.") % (font_bunch.psname, dvifont.texname)) - font = get_font(font_bunch.filename) + font = font_manager.get_font(font_bunch.filename) - for charmap_name, charmap_code in [("ADOBE_CUSTOM", - 1094992451), - ("ADOBE_STANDARD", - 1094995778)]: + for charmap in [_ft2.Encoding.ADOBE_CUSTOM, + _ft2.Encoding.ADOBE_STANDARD]: try: - font.select_charmap(charmap_code) + font.select_charmap(charmap) except (ValueError, RuntimeError): pass else: break else: - charmap_name = "" + charmap = None warnings.warn("No supported encoding in font (%s)." % font_bunch.filename) - if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding: + if (charmap is _ft2.Encoding.ADOBE_STANDARD + and font_bunch.encoding): enc0 = dviread.Encoding(font_bunch.encoding) enc = {i: self._adobe_standard_encoding.get(c, None) for i, c in enumerate(enc0.encoding)} @@ -365,28 +346,26 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, else: font, enc = font_and_encoding - ft2font_flag = LOAD_TARGET_LIGHT + ft2font_flag = _ft2.LOAD_TARGET_NORMAL char_id = self._get_char_id_ps(font, glyph) if char_id not in glyph_map: - font.clear() - font.set_size(self.FONT_SCALE, self.DPI) + font.set_char_size(self.FONT_SCALE, self.DPI) if enc: charcode = enc.get(glyph, None) else: charcode = glyph if charcode is not None: - glyph0 = font.load_char(charcode, flags=ft2font_flag) + font.load_char(charcode, flags=ft2font_flag) else: warnings.warn("The glyph (%d) of font (%s) cannot be " "converted with the encoding. Glyph may " "be wrong" % (glyph, font_bunch.filename)) + font.load_char(glyph, flags=ft2font_flag) - glyph0 = font.load_char(glyph, flags=ft2font_flag) - - glyph_map_new[char_id] = self.glyph_to_path(font) + glyph_map_new[char_id] = _ft2.glyph_to_path(font.glyph) glyph_ids.append(char_id) xpositions.append(x1) diff --git a/setup.py b/setup.py index 668bc5b0cf2f..68db2b7e942c 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,7 @@ setupext.LibAgg(), setupext.FreeType(), setupext.FT2Font(), + setupext.FT2(), setupext.Png(), setupext.Qhull(), setupext.Image(), diff --git a/setupext.py b/setupext.py index 37679d9f3bb3..f45a435dd6e8 100644 --- a/setupext.py +++ b/setupext.py @@ -668,6 +668,21 @@ def check(self): return sys.version +class get_pybind_include(object): + """Helper class to determine the pybind11 include path. + + The purpose of this class is to postpone importing pybind11 until it is + actually installed, so that the ``get_include()`` method can be invoked. + """ + + def __init__(self, user=False): + self.user = user + + def __str__(self): + import pybind11 + return pybind11.get_include(self.user) + + class Matplotlib(SetupPackage): name = "matplotlib" @@ -1198,6 +1213,22 @@ def get_extension(self): return ext +class FT2(SetupPackage): + name = "ft2" + + def get_extension(self): + ext = make_extension( + "matplotlib._ft2", sorted(glob.glob("src/ft2/*.cpp"))) + if sys.platform in ["linux", "darwin"]: + ext.extra_compile_args += ["-std=c++1z", "-Wextra", "-Wpedantic"] + elif sys.platform == "win32": + ext.extra_compile_args += ["/std:c++17"] + ext.include_dirs += [ + get_pybind_include(), get_pybind_include(user=True)] + FreeType().add_flags(ext) + return ext + + class Png(SetupPackage): name = "png" pkg_names = { @@ -1365,6 +1396,7 @@ def get_install_requires(self): return [ "cycler>=0.10", "kiwisolver>=1.0.1", + "pybind11", "pyparsing>=2.0.1,!=2.0.4,!=2.1.2,!=2.1.6", "python-dateutil>=2.1", "pytz", diff --git a/src/ft2/_ft2.cpp b/src/ft2/_ft2.cpp new file mode 100644 index 000000000000..256d66fa7a11 --- /dev/null +++ b/src/ft2/_ft2.cpp @@ -0,0 +1,729 @@ +#include "_ft2.h" +#include "_layout.h" + +namespace matplotlib::ft2 { + +Face::Face(std::string const& path, FT_Long index, double hinting_factor) : + ptr{ + [&]() -> std::shared_ptr { + auto face = FT_Face{}; + FT_CHECK(FT_New_Face, library, path.data(), index, &face); + auto transform = FT_Matrix{FT_Fixed(65536 / hinting_factor), 0, 0, 65536}; + FT_Set_Transform(face, &transform, nullptr); + return {face, FT_Done_Face}; + }() + }, + path{path}, + hinting_factor{hinting_factor} +{} + +Glyph::Glyph(FT_Face const& face, double hinting_factor) : + ptr{ + [&]() -> std::shared_ptr { + auto glyph = FT_Glyph{}; + FT_CHECK(FT_Get_Glyph, face->glyph, &glyph); + return {glyph, FT_Done_Glyph}; + }() + }, + metrics{face->glyph->metrics}, + linearHoriAdvance{face->glyph->linearHoriAdvance}, + linearVertAdvance{face->glyph->linearVertAdvance}, + hinting_factor{hinting_factor} +{} + +PYBIND11_MODULE(_ft2, m) +{ + using namespace pybind11::literals; + + m.doc() = R"__doc__( +A wrapper extension module for FreeType2. + +Unless stated otherwise, all methods directly wrap a corresponding FreeType +function, and all lengths of this API are either in pixels (if FreeType +intenally uses 26.6 or 16.16 fixed point -- conversion is handled by this +module) or in font units (if FreeType internally uses font units). The latter +case is explicitly mentioned where applicable. +)__doc__"; + + FT_CHECK(FT_Init_FreeType, &library); + + auto major = FT_Int{}, minor = FT_Int{}, patch = FT_Int{}; + FT_Library_Version(library, &major, &minor, &patch); + m.attr("__freetype_version__") + = std::to_string(major) + "." + + std::to_string(minor) + "." + + std::to_string(patch); + +#define DECLARE_FLAG(name) m.attr(#name) = FT_##name + DECLARE_FLAG(FACE_FLAG_SCALABLE); + DECLARE_FLAG(FACE_FLAG_FIXED_SIZES); + DECLARE_FLAG(FACE_FLAG_FIXED_WIDTH); + DECLARE_FLAG(FACE_FLAG_SFNT); + DECLARE_FLAG(FACE_FLAG_HORIZONTAL); + DECLARE_FLAG(FACE_FLAG_VERTICAL); + DECLARE_FLAG(FACE_FLAG_KERNING); + DECLARE_FLAG(FACE_FLAG_FAST_GLYPHS); + DECLARE_FLAG(FACE_FLAG_MULTIPLE_MASTERS); + DECLARE_FLAG(FACE_FLAG_GLYPH_NAMES); + DECLARE_FLAG(FACE_FLAG_EXTERNAL_STREAM); + DECLARE_FLAG(FACE_FLAG_HINTER); + DECLARE_FLAG(FACE_FLAG_CID_KEYED); + DECLARE_FLAG(FACE_FLAG_TRICKY); + DECLARE_FLAG(FACE_FLAG_COLOR); + + DECLARE_FLAG(LOAD_DEFAULT); + DECLARE_FLAG(LOAD_NO_SCALE); + DECLARE_FLAG(LOAD_NO_HINTING); + DECLARE_FLAG(LOAD_RENDER); + DECLARE_FLAG(LOAD_NO_BITMAP); + DECLARE_FLAG(LOAD_VERTICAL_LAYOUT); + DECLARE_FLAG(LOAD_FORCE_AUTOHINT); + DECLARE_FLAG(LOAD_CROP_BITMAP); + DECLARE_FLAG(LOAD_PEDANTIC); + DECLARE_FLAG(LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH); + DECLARE_FLAG(LOAD_NO_RECURSE); + DECLARE_FLAG(LOAD_IGNORE_TRANSFORM); + DECLARE_FLAG(LOAD_MONOCHROME); + DECLARE_FLAG(LOAD_LINEAR_DESIGN); + DECLARE_FLAG(LOAD_NO_AUTOHINT); + DECLARE_FLAG(LOAD_COLOR); +#ifdef FT_LOAD_COMPUTE_METRICS // backcompat: ft 2.6.1. + DECLARE_FLAG(LOAD_COMPUTE_METRICS); +#endif +#ifdef FT_LOAD_BITMAP_METRICS_ONLY // backcompat: ft 2.7.1. + DECLARE_FLAG(LOAD_BITMAP_METRICS_ONLY); +#endif + + DECLARE_FLAG(LOAD_TARGET_NORMAL); + DECLARE_FLAG(LOAD_TARGET_LIGHT); + DECLARE_FLAG(LOAD_TARGET_MONO); + DECLARE_FLAG(LOAD_TARGET_LCD); + DECLARE_FLAG(LOAD_TARGET_LCD_V); + + DECLARE_FLAG(STYLE_FLAG_ITALIC); + DECLARE_FLAG(STYLE_FLAG_BOLD); +#undef DECLARE_FLAG + + py::enum_(m, "Encoding") +#define DECLARE_ENUM(name) .value(#name, FT_ENCODING_##name) + DECLARE_ENUM(NONE) + DECLARE_ENUM(UNICODE) + DECLARE_ENUM(MS_SYMBOL) + DECLARE_ENUM(ADOBE_LATIN_1) + DECLARE_ENUM(OLD_LATIN_2) + DECLARE_ENUM(SJIS) +#ifdef FT_ENCODING_PRC // backcompat: ft 2.8.0.1. + DECLARE_ENUM(PRC) +#endif + DECLARE_ENUM(BIG5) + DECLARE_ENUM(WANSUNG) + DECLARE_ENUM(JOHAB) + DECLARE_ENUM(ADOBE_STANDARD) + DECLARE_ENUM(ADOBE_EXPERT) + DECLARE_ENUM(ADOBE_CUSTOM) + DECLARE_ENUM(APPLE_ROMAN); +#undef DECLARE_ENUM + + py::enum_(m, "GlyphBbox") +#define DECLARE_ENUM(name) .value(#name, FT_GLYPH_BBOX_##name) + DECLARE_ENUM(UNSCALED) + DECLARE_ENUM(SUBPIXELS) + DECLARE_ENUM(GRIDFIT) + DECLARE_ENUM(TRUNCATE) + DECLARE_ENUM(PIXELS); +#undef DECLARE_ENUM + + py::enum_(m, "Kerning") +#define DECLARE_ENUM(name) .value(#name, FT_KERNING_##name) + DECLARE_ENUM(DEFAULT) + DECLARE_ENUM(UNFITTED) + DECLARE_ENUM(UNSCALED); +#undef DECLARE_ENUM + + py::class_(m, "Face", R"__doc__( +A lightweight wrapper around a ``FT_Face``. + +Length attributes of this class (`ascender`, `descender`, etc.) are in font +units. +)__doc__") + .def( + py::init( + [](py::object path, FT_Long index, double hinting_factor) -> Face* { + PyBytesObject* bytes = nullptr; + char* buffer = nullptr; + auto length = std::string::size_type{}; // i.e. size_t. + PyUnicode_FSConverter(path.ptr(), &bytes) + && PyBytes_AsStringAndSize( + reinterpret_cast(bytes), + &buffer, + reinterpret_cast(&length)); + Py_XDECREF(bytes); + if (PyErr_Occurred()) { + throw py::error_already_set(); + } + return new Face{std::string{buffer, length}, index, hinting_factor}; + }), + "path"_a, "index"_a, "hinting_factor"_a=1, + R"__doc__( +Load a `Face` from a path and an index. +)__doc__") + + .def_property_readonly( + "pathname", + [](Face const& pyface) -> py::handle { + return + PyUnicode_DecodeFSDefaultAndSize( + pyface.path.c_str(), pyface.path.size()); + }, + R"__doc__( +The path from which the `Face` was loaded, as a string. This is the only +attribute that does not come from FreeType itself. +)__doc__") + +#define DECLARE_FIELD(prop) \ + .def_property_readonly( \ + #prop, [](Face const& pyface) { return pyface.ptr->prop; }) + DECLARE_FIELD(num_faces) + DECLARE_FIELD(face_flags) + DECLARE_FIELD(style_flags) + DECLARE_FIELD(num_glyphs) + DECLARE_FIELD(family_name) + DECLARE_FIELD(style_name) + DECLARE_FIELD(num_fixed_sizes) + // available_sizes -> not supported. + DECLARE_FIELD(num_charmaps) + // charmaps -> not supported. + // generic -> not supported. + .def_property_readonly( + "bbox", + [](Face const& pyface) -> std::tuple { + auto [xmin, ymin, xmax, ymax] = pyface.ptr->bbox; + return {xmin, ymin, xmax, ymax}; + }) + DECLARE_FIELD(units_per_EM) + DECLARE_FIELD(ascender) + DECLARE_FIELD(descender) + DECLARE_FIELD(height) + DECLARE_FIELD(max_advance_width) + DECLARE_FIELD(max_advance_height) + DECLARE_FIELD(underline_position) + DECLARE_FIELD(underline_thickness) + .def_property_readonly( + "glyph", + [](Face const& pyface) -> Glyph { + return {pyface.ptr.get(), pyface.hinting_factor}; + }) + // size -> set_char_size. + // charmap -> not supported. +#undef DECLARE_FIELD + .def( + "get_char_index", + [](Face const& pyface, FT_ULong codepoint) -> FT_UInt { + return FT_Get_Char_Index(pyface.ptr.get(), codepoint); + }, + "codepoint"_a) + .def( + "get_font_format", + [](Face const& pyface) -> std::string { + // backcompat: FT_Get_Font_Format in ft 2.6. + return FT_Get_X11_Font_Format(pyface.ptr.get()); + }) + .def( + "get_glyph_name", + [](Face const& pyface, FT_UInt index) -> std::string { + auto face = pyface.ptr.get(); + char buf[128]; // Limit to PS identifier size. + // This branch is copied from ft2font.cpp. + if (!FT_HAS_GLYPH_NAMES(face)) { + /* Note that this generated name must match the name that + is generated by ttconv in ttfont_CharStrings_getname. */ + PyOS_snprintf(buf, 128, "uni%08x", index); + } else { + FT_CHECK(FT_Get_Glyph_Name, face, index, buf, 128); + } + return buf; + }, + "index"_a) + .def( + "get_kerning", + [](Face const& pyface, FT_UInt left, FT_UInt right, FT_Kerning_Mode mode) + -> std::tuple { + if (FT_HAS_KERNING(pyface.ptr.get())) { + if (mode == FT_KERNING_UNSCALED) { + // So that whoever *actually* needs this can implement the correct + // conversion factor. + throw + std::runtime_error("Unknown conversion for FT_KERNING_UNSCALED"); + } + auto delta = FT_Vector{}; + FT_CHECK( + FT_Get_Kerning, pyface.ptr.get(), left, right, mode, &delta); + return {delta.x / 64., delta.y / 64.}; + } else { + return {0, 0}; + } + }, + "left"_a, "right"_a, "mode"_a) + .def( + "get_postscript_name", + [](Face const& pyface) -> char const* { + return FT_Get_Postscript_Name(pyface.ptr.get()); + }) + .def( + "get_ps_font_info", + [](Face const& pyface) -> py::dict { + auto info = PS_FontInfoRec{}; + FT_CHECK(FT_Get_PS_Font_Info, pyface.ptr.get(), &info); + auto pyinfo = py::dict{}; +#define COPY_FIELD(field) pyinfo[#field] = info.field + COPY_FIELD(version); + COPY_FIELD(notice); + COPY_FIELD(full_name); + COPY_FIELD(family_name); + COPY_FIELD(weight); + COPY_FIELD(italic_angle); + COPY_FIELD(is_fixed_pitch); + COPY_FIELD(underline_position); + COPY_FIELD(underline_thickness); +#undef COPY_FIELD + return pyinfo; + }) + .def( + "get_sfnt_name_table", + // Don't bother returning a + // std::unordered_map< + // std::tuple, py::bytes> + // because std::hash is not specialized for tuples... + [](Face const& pyface) -> py::dict { + auto face = pyface.ptr.get(); + if (!FT_IS_SFNT(face)) { + throw std::runtime_error("Font not using the SFNT storage scheme"); + } + auto table = py::dict{}; + auto name_count = FT_Get_Sfnt_Name_Count(face); + for (auto i = 0u; i < name_count; ++i) { + auto sfnt_name = FT_SfntName{}; + FT_CHECK(FT_Get_Sfnt_Name, face, i, &sfnt_name); + table[py::make_tuple(sfnt_name.platform_id, sfnt_name.encoding_id, + sfnt_name.language_id, sfnt_name.name_id)] + = py::bytes((char*)(sfnt_name.string), sfnt_name.string_len); + } + return table; + }, R"__doc__( +Return the SFNT names table. + +The returned dict maps ``(platform_id, encoding_id, language_id, name_id)`` +keys to the corresponding 'name' bytestrings. +)__doc__") + .def( + "get_sfnt_table", + [](Face const& pyface, std::string tag) -> std::optional { + auto face = pyface.ptr.get(); + if (!FT_IS_SFNT(face)) { + throw std::runtime_error("Font not using the SFNT storage scheme"); + } + auto table = py::dict{}; + auto copy_field = [&](char const* name, auto value) -> void { + if constexpr (std::is_array_v) { + table[name] = + py::bytes( + static_cast(value), std::extent_v); + } else { + table[name] = value; + } + }; +#define COPY_FIELD(field) copy_field(#field, ptr->field) + if (tag == "head") { + auto ptr = + // backcompat: FT_SFNT_HEAD in ft 2.5.4. Same for other ft_sfnt's. + static_cast(FT_Get_Sfnt_Table(face, ft_sfnt_head)); + if (!ptr) { + return {}; + } + COPY_FIELD(Table_Version); + COPY_FIELD(Font_Revision); + COPY_FIELD(CheckSum_Adjust); + COPY_FIELD(Magic_Number); + COPY_FIELD(Flags); + COPY_FIELD(Units_Per_EM); + table["Created"] = py::make_tuple(ptr->Created[0], ptr->Created[1]); + table["Modified"] = py::make_tuple(ptr->Modified[0], ptr->Modified[1]); + COPY_FIELD(xMin); + COPY_FIELD(yMin); + COPY_FIELD(xMax); + COPY_FIELD(yMax); + COPY_FIELD(Mac_Style); + COPY_FIELD(Lowest_Rec_PPEM); + COPY_FIELD(Font_Direction); + COPY_FIELD(Index_To_Loc_Format); + COPY_FIELD(Glyph_Data_Format); + } else if (tag == "maxp") { + auto ptr = + static_cast(FT_Get_Sfnt_Table(face, ft_sfnt_maxp)); + COPY_FIELD(version); + COPY_FIELD(numGlyphs); + COPY_FIELD(maxPoints); + COPY_FIELD(maxContours); + COPY_FIELD(maxCompositePoints); + COPY_FIELD(maxCompositeContours); + COPY_FIELD(maxZones); + COPY_FIELD(maxTwilightPoints); + COPY_FIELD(maxStorage); + COPY_FIELD(maxFunctionDefs); + COPY_FIELD(maxInstructionDefs); + COPY_FIELD(maxStackElements); + COPY_FIELD(maxSizeOfInstructions); + COPY_FIELD(maxComponentElements); + COPY_FIELD(maxComponentDepth); + } else if (tag == "OS/2") { + auto ptr = + static_cast(FT_Get_Sfnt_Table(face, ft_sfnt_os2)); + if (!ptr) { + return {}; + } + // NOTE: Don't bother with optional tags. + COPY_FIELD(version); + COPY_FIELD(xAvgCharWidth); + COPY_FIELD(usWeightClass); + COPY_FIELD(usWidthClass); + COPY_FIELD(fsType); + COPY_FIELD(ySubscriptXSize); + COPY_FIELD(ySubscriptYSize); + COPY_FIELD(ySubscriptXOffset); + COPY_FIELD(ySubscriptYOffset); + COPY_FIELD(ySuperscriptXSize); + COPY_FIELD(ySuperscriptYSize); + COPY_FIELD(ySuperscriptXOffset); + COPY_FIELD(ySuperscriptYOffset); + COPY_FIELD(yStrikeoutSize); + COPY_FIELD(yStrikeoutPosition); + COPY_FIELD(sFamilyClass); + COPY_FIELD(panose); + COPY_FIELD(ulUnicodeRange1); + COPY_FIELD(ulUnicodeRange2); + COPY_FIELD(ulUnicodeRange3); + COPY_FIELD(ulUnicodeRange4); + COPY_FIELD(achVendID); + COPY_FIELD(fsSelection); + COPY_FIELD(usFirstCharIndex); + COPY_FIELD(usLastCharIndex); + COPY_FIELD(sTypoAscender); + COPY_FIELD(sTypoDescender); + COPY_FIELD(sTypoLineGap); + COPY_FIELD(usWinAscent); + COPY_FIELD(usWinDescent); + } else if (tag == "hhea") { + auto ptr = + static_cast(FT_Get_Sfnt_Table(face, ft_sfnt_hhea)); + if (!ptr) { + return {}; + } + // NOTE: Skip reserved, {long,short}_metrics. + COPY_FIELD(Version); + COPY_FIELD(Ascender); + COPY_FIELD(Descender); + COPY_FIELD(Line_Gap); + COPY_FIELD(advance_Width_Max); + COPY_FIELD(min_Left_Side_Bearing); + COPY_FIELD(min_Right_Side_Bearing); + COPY_FIELD(xMax_Extent); + COPY_FIELD(caret_Slope_Rise); + COPY_FIELD(caret_Slope_Run); + COPY_FIELD(caret_Offset); + COPY_FIELD(metric_Data_Format); + COPY_FIELD(number_Of_HMetrics); + } else if (tag == "vhea") { + auto ptr = + static_cast(FT_Get_Sfnt_Table(face, ft_sfnt_vhea)); + if (!ptr) { + throw std::runtime_error("No \"vhea\" table"); + } + // NOTE: Skip reserved, {long,short}_metrics. + COPY_FIELD(Version); + COPY_FIELD(Ascender); + COPY_FIELD(Descender); + COPY_FIELD(Line_Gap); + COPY_FIELD(advance_Height_Max); + COPY_FIELD(min_Top_Side_Bearing); + COPY_FIELD(min_Bottom_Side_Bearing); + COPY_FIELD(yMax_Extent); + COPY_FIELD(caret_Slope_Rise); + COPY_FIELD(caret_Slope_Run); + COPY_FIELD(caret_Offset); + COPY_FIELD(metric_Data_Format); + COPY_FIELD(number_Of_VMetrics); + } else if (tag == "post") { + auto ptr = + static_cast(FT_Get_Sfnt_Table(face, ft_sfnt_post)); + if (!ptr) { + return {}; + } + COPY_FIELD(FormatType); + // NOTE: ft2font splits italicAngle into two uint16s but this seems wrong. + COPY_FIELD(italicAngle); + COPY_FIELD(underlinePosition); + COPY_FIELD(underlineThickness); + COPY_FIELD(isFixedPitch); + COPY_FIELD(minMemType42); + COPY_FIELD(maxMemType42); + COPY_FIELD(minMemType1); + COPY_FIELD(maxMemType1); + } else if (tag == "pclt") { + auto ptr = + static_cast(FT_Get_Sfnt_Table(face, ft_sfnt_pclt)); + if (!ptr) { + return {}; + } + // NOTE: Skip reserved. + COPY_FIELD(Version); + COPY_FIELD(FontNumber); + COPY_FIELD(Pitch); + COPY_FIELD(xHeight); + COPY_FIELD(Style); + COPY_FIELD(TypeFamily); + COPY_FIELD(CapHeight); + COPY_FIELD(SymbolSet); + COPY_FIELD(TypeFace); + COPY_FIELD(CharacterComplement); + COPY_FIELD(FileName); + COPY_FIELD(StrokeWeight); + COPY_FIELD(WidthType); + COPY_FIELD(SerifStyle); + } else { + throw std::runtime_error("Invalid SFNT table"); + } +#undef COPY_FIELD + return table; + }) + .def( + "load_char", + [](Face const& pyface, FT_ULong codepoint, FT_Int32 flags) -> void { + FT_CHECK(FT_Load_Char, pyface.ptr.get(), codepoint, flags); + }, + "codepoint"_a, "flags"_a) + .def( + "select_charmap", + [](Face const& pyface, FT_Encoding encoding) -> void { + FT_CHECK(FT_Select_Charmap, pyface.ptr.get(), encoding); + }, + "encoding"_a) + .def( + "set_char_size", + [](Face const& pyface, double pt_size, double dpi) -> void { + auto face = pyface.ptr.get(); + FT_CHECK(FT_Set_Char_Size, + face, pt_size * 64, 0, dpi * pyface.hinting_factor, dpi); + auto transform = + FT_Matrix{FT_Fixed(65536 / pyface.hinting_factor), 0, 0, 65536}; + FT_Set_Transform(face, &transform, nullptr); + }, + "pt_size"_a, "dpi"_a) + ; + + py::class_(m, "Glyph", R"__doc__( +A lightweight wrapper around a ``FT_Glyph``. + +This object cannot be constructed directly. Instead, load a glyph in a face's +glyph slot with `Face.load_char`, then access the face's `glyph` property +(which calls ``FT_Get_Glyph``). + +This class exposes the attributes of the original glyph slot and its glyph +metrics. Length attibutes are in pixels (this module handles conversion from +26.6 and 16.6 fixed point formats internally). +)__doc__") + .def_property_readonly( + "width", + [](Glyph& pyglyph) -> double { + return pyglyph.metrics.width / 64. / pyglyph.hinting_factor; + }) + .def_property_readonly( + "height", + [](Glyph& pyglyph) -> double { + return pyglyph.metrics.height / 64.; + }) + .def_property_readonly( + "horiBearingX", + [](Glyph& pyglyph) -> double { + return pyglyph.metrics.horiBearingX / 64. / pyglyph.hinting_factor; + }) + .def_property_readonly( + "horiBearingY", + [](Glyph& pyglyph) -> double { + return pyglyph.metrics.horiBearingY / 64.; + }) + .def_property_readonly( + "horiAdvance", + [](Glyph& pyglyph) -> double { + return pyglyph.metrics.horiAdvance / 64.; + }) + .def_property_readonly( + "vertBearingX", + [](Glyph& pyglyph) -> double { + return pyglyph.metrics.vertBearingX / 64.; + }) + .def_property_readonly( + "vertBearingY", + [](Glyph& pyglyph) -> double { + return pyglyph.metrics.vertBearingY / 64.; + }) + .def_property_readonly( + "vertAdvance", + [](Glyph& pyglyph) -> double { + return pyglyph.metrics.vertAdvance / 64.; + }) + .def_property_readonly( + "linearHoriAdvance", + [](Glyph& pyglyph) -> double { + return pyglyph.linearHoriAdvance / 65536. / pyglyph.hinting_factor; + }) + .def_property_readonly( + "linearVertAdvance", + [](Glyph& pyglyph) -> double { + return pyglyph.linearVertAdvance / 65536.; + }) + .def( + "get_cbox", + [](Glyph& pyglyph, FT_UInt mode) + -> std::tuple { + auto bbox = FT_BBox{}; + FT_Glyph_Get_CBox(pyglyph.ptr.get(), mode, &bbox); + auto conv = ((mode == FT_GLYPH_BBOX_SUBPIXELS) + || mode == FT_GLYPH_BBOX_GRIDFIT) ? 1. / 64 : 1.; + return {bbox.xMin * conv, bbox.xMax * conv, + bbox.yMin * conv, bbox.yMax * conv}; + }); + + py::class_(m, "Layout", R"__doc__( +A text (and rectangles) layout engine. + +Use as follows:: + + # Construct a layout. + layout = Layout.simple(...) + # or + layout = Layout.manual(...) + # Access the layout's bounds: + layout.xMin, layout.xMax, layout.yMin, layout.yMax + # Render the text into a numpy array: + result = layout.render() +)__doc__") + .def("render", &Layout::render, "antialiased"_a, R"__doc__( +Render the laid out text into a numpy array. +)__doc__") + .def_property_readonly( + "xMin", + [](Layout& layout) -> double { return layout.bbox.xMin / 64.; }) + .def_property_readonly( + "xMax", + [](Layout& layout) -> double { return layout.bbox.xMax / 64.; }) + .def_property_readonly( + "yMin", + [](Layout& layout) -> double { return layout.bbox.yMin / 64.; }) + .def_property_readonly( + "yMax", + [](Layout& layout) -> double { return layout.bbox.yMax / 64.; }) + .def_static( + "simple", + [](std::u32string const& string, Face const& pyface, FT_Int32 flags) + -> Layout { + return Layout::simple(string, pyface.ptr.get(), flags); + }, + "string"_a, "face"_a, "flags"_a, R"__doc__( +Layout a string by positioning individual glyphs one after the other. + +After each glyph, the current position is advanced by the glyph's advance and +the kerning between this glyph and the next one. +)__doc__") + .def_static( + "manual", + [](std::vector> const& positioned_pyglyphs, + std::vector const& rectangles, + FT_Int32 flags) + -> Layout { + auto positioned_glyphs + = std::vector>{}; + std::transform( + positioned_pyglyphs.begin(), positioned_pyglyphs.end(), + std::back_inserter(positioned_glyphs), + [](std::tuple const& positioned_pyglyph) + -> std::tuple { + auto& [pyglyph, x, y] = positioned_pyglyph; + return {pyglyph.ptr.get(), x, y}; + }); + return Layout::manual(positioned_glyphs, rectangles, flags); + }, + "positioned_glyphs"_a, "rectangles"_a, "flags"_a, R"__doc__( +Prepare a list of manually laid out glyphs and rectangles for rendering. +)__doc__"); + + m.def( + "glyph_to_path", + [](Glyph const& glyph) + -> std::tuple>, std::vector> { + if (glyph.ptr->format != FT_GLYPH_FORMAT_OUTLINE) { + throw std::runtime_error("Not an outline glyph"); + } + // outline.n_points seems invalid :-( + auto path = std::tuple>, + std::vector>{}; + auto& [vertices, codes] = path; + auto move_to = + [](FT_Vector const* to, void* raw) -> int { + auto& [vertices, codes] = *static_cast(raw); + if (vertices.size()) { + vertices.push_back({NAN, NAN}); + codes.push_back(79); + } + vertices.push_back({to->x / 64., to->y / 64.}); + codes.push_back(1); + return 0; + }; + auto line_to = + [](FT_Vector const* to, void* raw) -> int { + auto& [vertices, codes] = *static_cast(raw); + vertices.push_back({to->x / 64., to->y / 64.}); + codes.push_back(2); + return 0; + }; + auto conic_to = + [](FT_Vector const* control, FT_Vector const* to, void* raw) -> int { + auto& [vertices, codes] = *static_cast(raw); + vertices.push_back({control->x / 64., control->y / 64.}); + vertices.push_back({to->x / 64., to->y / 64.}); + codes.push_back(3); + codes.push_back(3); + return 0; + }; + auto cubic_to = + [](FT_Vector const* control1, FT_Vector const* control2, + FT_Vector const* to, void* raw) -> int { + auto& [vertices, codes] = *static_cast(raw); + vertices.push_back({control1->x / 64., control1->y / 64.}); + vertices.push_back({control2->x / 64., control2->y / 64.}); + vertices.push_back({to->x / 64., to->y / 64.}); + codes.push_back(4); + codes.push_back(4); + codes.push_back(4); + return 0; + }; + auto outline_funcs = + FT_Outline_Funcs{move_to, line_to, conic_to, cubic_to, 0, 0}; + FT_Outline_Decompose( + &reinterpret_cast(glyph.ptr.get())->outline, + &outline_funcs, + &path); + if (vertices.size()) { + vertices.push_back({NAN, NAN}); + codes.push_back(79); + } else { + vertices.push_back({0, 0}); + codes.push_back(1); + } + return path; + }, + "glyph"_a, R"__doc__( +Extract a glyph's outline in Matplotlib's ``(vertices, codes)`` format. +)__doc__"); +} + +} diff --git a/src/ft2/_ft2.h b/src/ft2/_ft2.h new file mode 100644 index 000000000000..d0a7abc9b518 --- /dev/null +++ b/src/ft2/_ft2.h @@ -0,0 +1,29 @@ +#pragma once + +#include "_util.h" + +namespace matplotlib::ft2 { + +FT_Library library; + +struct Face { + std::shared_ptr const ptr; + std::string const path; + double const hinting_factor; + + Face(std::string const& path, FT_Long index, double hinting_factor); +}; + +struct Glyph { + std::shared_ptr const ptr; + + // Fields copied from the glyph slot. + FT_Glyph_Metrics const metrics; + FT_Fixed const linearHoriAdvance; + FT_Fixed const linearVertAdvance; + double const hinting_factor; + + Glyph(FT_Face const& face, double hinting_factor); +}; + +} diff --git a/src/ft2/_layout.cpp b/src/ft2/_layout.cpp new file mode 100644 index 000000000000..6d3c5253ab13 --- /dev/null +++ b/src/ft2/_layout.cpp @@ -0,0 +1,200 @@ +#include "_layout.h" +// Use 26.6 throughout. + +namespace matplotlib::ft2 { + +FT_BBox compute_bbox( + FT_Vector pos, + std::vector const& glyphs, + std::vector const& rectangles) +{ + // Use the advance, because spaces are reported as xMin = xMax = 0, so those + // at the end would be ignored (OTOH, (0, 0) is always in the bbox so we + // don't need to special-case that). + auto bbox = FT_BBox{0, 0, pos.x, pos.y}; + for (auto& glyph: glyphs) { + auto glyph_bbox = FT_BBox{}; + FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph_bbox); + bbox.xMin = std::min(bbox.xMin, glyph_bbox.xMin); + bbox.xMax = std::max(bbox.xMax, glyph_bbox.xMax); + bbox.yMin = std::min(bbox.yMin, glyph_bbox.yMin); + bbox.yMax = std::max(bbox.yMax, glyph_bbox.yMax); + } + for (auto& [x0, x1, y0, y1]: rectangles) { + bbox.xMin = std::min(bbox.xMin, FT_Pos(std::floor(x0 * 64))); + bbox.xMax = std::max(bbox.xMax, FT_Pos(std::ceil(x1 * 64))); + bbox.yMin = std::min(bbox.yMin, FT_Pos(std::floor(-y1 * 64))); + bbox.yMax = std::max(bbox.yMax, FT_Pos(std::ceil(-y0 * 64))); + } + return bbox; +} + +Layout::Layout( + std::vector const& glyphs, + std::vector const& rectangles, + FT_Int32 flags, + FT_BBox const& bbox) : + glyphs{glyphs}, rectangles{rectangles}, flags{flags}, bbox{bbox} +{} + +Layout Layout::simple( + std::u32string const& string, + FT_Face const& face, + FT_Int32 flags) +{ + auto has_kerning = FT_HAS_KERNING(face); + auto previous = char32_t{}; + auto pos = FT_Vector{}; + auto glyphs = std::vector{}; + glyphs.reserve(string.size()); + for (auto& codepoint: string) { + auto current = FT_Get_Char_Index(face, codepoint); + if (has_kerning && previous && current) { + auto delta = FT_Vector{}; + FT_CHECK( + FT_Get_Kerning, face, previous, current, FT_KERNING_DEFAULT, &delta); + pos.x += delta.x; + pos.y += delta.y; + } + FT_CHECK(FT_Load_Glyph, face, current, flags); + auto glyph = FT_Glyph{}; + FT_CHECK(FT_Get_Glyph, face->glyph, &glyph); + FT_CHECK(FT_Glyph_Transform, glyph, nullptr, &pos); + glyphs.push_back(glyph); + // 16.16 -> 26.6. + pos.x += glyph->advance.x >> 10; + pos.y += glyph->advance.y >> 10; + } + return {glyphs, {}, flags, compute_bbox(pos, glyphs, {})}; +} + +Layout Layout::manual( + std::vector> const& positioned_glyphs, + std::vector const& rectangles, + FT_Int32 flags) +{ + auto glyphs = std::vector{}; + glyphs.reserve(positioned_glyphs.size()); + for (auto& [glyph, x, y]: positioned_glyphs) { + auto copy = FT_Glyph{}; + FT_CHECK(FT_Glyph_Copy, glyph, ©); + auto pos = + FT_Vector{FT_Pos(std::round(x * 64)), FT_Pos(std::round(-y * 64))}; + FT_Glyph_Transform(copy, nullptr, &pos); + glyphs.push_back(copy); + } + return {glyphs, rectangles, flags, compute_bbox({}, glyphs, rectangles)}; +} + +Layout::~Layout() +{ + if (!moved) { + std::for_each(glyphs.begin(), glyphs.end(), FT_Done_Glyph); + } +} + +Layout::Layout(Layout&& other) : + glyphs{std::move(other.glyphs)}, + rectangles{std::move(other.rectangles)}, + flags{std::move(other.flags)}, + bbox{std::move(other.bbox)}, + moved{true} +{} + +py::array_t Layout::render(bool antialiased) +{ + auto xmin = int(std::floor(bbox.xMin / 64.)), + xmax = int(std::ceil(bbox.xMax / 64.)), + ymin = int(std::floor(bbox.yMin / 64.)), + ymax = int(std::ceil(bbox.yMax / 64.)), + width = xmax - xmin, + height = ymax - ymin; + auto array = + py::array_t{size_t(height * width), nullptr} + .attr("reshape")(height, width).cast>(); + auto buf = array.mutable_unchecked().mutable_data(0); + std::memset(buf, 0, width * height); + for (auto glyph: glyphs) { + if (glyph->format != FT_GLYPH_FORMAT_BITMAP) { + // Do *not* destroy the previous glyph: this would invalidate a + // Python-level Glyph holding the old value. + FT_CHECK( + FT_Glyph_To_Bitmap, &glyph, + antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO, + nullptr, false); + } + auto b_glyph = reinterpret_cast(glyph); + auto bitmap = b_glyph->bitmap; + if (bitmap.pitch < 0) { // FIXME: Pitch can be negative. + throw std::runtime_error("Negative pitches are not supported"); + } + switch (bitmap.pixel_mode) { + case FT_PIXEL_MODE_MONO: + for (auto i = 0u; i < bitmap.rows; ++i) { + auto src = bitmap.buffer + i * bitmap.pitch, + dst = buf + (ymax - b_glyph->top + i) * width + + b_glyph->left - xmin; + uint8_t k = 7; + for (auto j = 0u; j < bitmap.width; ++j, --k, k %= 8) { + if (*src & (1 << k)) { // MSB order. + *dst = 0xff; + } + if (!k) { + ++src; + } + ++dst; + } + } + break; + case FT_PIXEL_MODE_GRAY: + for (auto i = 0u; i < bitmap.rows; ++i) { + auto src = bitmap.buffer + i * bitmap.pitch, + dst = buf + (ymax - b_glyph->top + i) * width + + b_glyph->left - xmin; + for (auto j = 0u; j < bitmap.width; ++j) { + *dst = std::max(*dst, *src); + ++src; + ++dst; + } + } + break; + default: + throw std::runtime_error( + "Unsupported pixel mode: " + std::to_string(bitmap.pixel_mode)); + } + FT_Done_Glyph(glyph); // Destroy the new glyph. + } + for (auto& [x0, x1, y0, y1]: rectangles) { + // FIXME: Aliased version. + auto x0i = int(std::ceil(x0)), x1i = int(std::floor(x1)), + y0i = int(std::ceil(y0)), y1i = int(std::floor(y1)); + for (auto y = y0i; y < y1i; ++y) { + std::memset(buf + y * width + x0i - xmin, 0xff, x1i - x0i); + } + // We only bother with antialiasing of thin horizontal lines. + if (y0i > y1i) { // e.g. y0 = 1.2, y1 = 1.8 -> fill = 0.6. + auto fill = uint8_t(0xff * (y1 - y0)); + std::transform( + buf + y1i * width + x0i - xmin, + buf + y1i * width + x1i - xmin, + buf + y1i * width + x0i - xmin, + [&](uint8_t value) { return std::max(fill, value); }); + } else if (y0i == y1i) { // e.g. y0 = 1.6, y1 = 2.3. + auto fill0 = uint8_t(0xff * (y0i - y0)); + auto fill1 = uint8_t(0xff * (y1 - y1i)); + std::transform( + buf + (y0i - 1) * width + x0i - xmin, + buf + (y0i - 1) * width + x1i - xmin, + buf + (y0i - 1) * width + x0i - xmin, + [&](uint8_t value) { return std::max(fill0, value); }); + std::transform( + buf + y0i * width + x0i - xmin, + buf + y0i * width + x1i - xmin, + buf + y0i * width + x0i - xmin, + [&](uint8_t value) { return std::max(fill1, value); }); + } + } + return array; +} + +} diff --git a/src/ft2/_layout.h b/src/ft2/_layout.h new file mode 100644 index 000000000000..15c3edc70a44 --- /dev/null +++ b/src/ft2/_layout.h @@ -0,0 +1,48 @@ +#pragma once + +#include "_util.h" + +namespace matplotlib::ft2 { + +using rect_t = std::tuple; + +FT_BBox compute_bbox( + FT_Vector pos, + std::vector const& glyphs, + std::vector const& rectangles); + +struct Layout { + private: + std::vector glyphs; // Needs to be non-const for rendering :-( + std::vector rectangles; + + public: + FT_Int32 const flags; + FT_BBox const bbox; // 26.6. + + private: + bool moved = false; + + Layout( + std::vector const& glyphs, + std::vector const& rectangles, + FT_Int32 flags, + FT_BBox const& bbox); + + public: + static Layout simple( + std::u32string const& string, + FT_Face const& face, + FT_Int32 flags); + static Layout manual( + std::vector> const& positioned_glyphs, + std::vector const& rectangles, + FT_Int32 flags); + ~Layout(); + Layout(const Layout& other) = delete; + Layout(Layout&& other); + + py::array_t render(bool antialiased); +}; + +} diff --git a/src/ft2/_util.cpp b/src/ft2/_util.cpp new file mode 100644 index 000000000000..28b64c6d5a99 --- /dev/null +++ b/src/ft2/_util.cpp @@ -0,0 +1,20 @@ +#include "_util.h" + +namespace matplotlib::ft2 { + +namespace detail { +// Load FreeType error codes. This approach (modified to use +// std::unordered_map) is documented in fterror.h. +// NOTE: If we require FreeType>=2.6.3 then the macro can be replaced by +// FTERRORS_H_. +#undef __FTERRORS_H__ +#define FT_ERRORDEF( e, v, s ) { e, s }, +#define FT_ERROR_START_LIST { +#define FT_ERROR_END_LIST }; + +std::unordered_map ft_errors = + +#include FT_ERRORS_H +} + +} diff --git a/src/ft2/_util.h b/src/ft2/_util.h new file mode 100644 index 000000000000..cdb0b42dbbaf --- /dev/null +++ b/src/ft2/_util.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include FT_FREETYPE_H +#include FT_GLYPH_H +#include FT_OUTLINE_H +#include FT_SFNT_NAMES_H +#include FT_TRUETYPE_TABLES_H +#include FT_TYPE1_TABLES_H +// backcompat: FT_FONT_FORMATS_H in ft 2.6.1. +#include FT_XFREE86_H + +#include +#include +#include + +#include +#include + +namespace matplotlib::ft2 { + +namespace py = pybind11; + +namespace detail { + +extern std::unordered_map ft_errors; + +} + +} + +#define FT_CHECK(func, ...) { \ + if (auto error_ = func(__VA_ARGS__)) { \ + throw \ + std::runtime_error( \ + #func " (" __FILE__ " line " + std::to_string(__LINE__) + ") failed " \ + "with error: " + ft2::detail::ft_errors.at(error_)); \ + } \ +}