Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
enable / qpainter.py
Size: Mime:
# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" This is the QPainter backend for kiva. """

from functools import partial
import numpy as np
import warnings

# Major package imports.
from pyface.qt import QtCore, QtGui

# Local imports.
from .abstract_graphics_context import AbstractGraphicsContext
from .arc_conversion import arc_to_tangent_points
from .fonttools import Font
import kiva.constants as constants

# These are the symbols that a backend has to define.
__all__ = ["CompiledPath", "Font", "font_metrics_provider", "GraphicsContext"]

cap_style = {}
cap_style[constants.CAP_ROUND] = QtCore.Qt.RoundCap
cap_style[constants.CAP_SQUARE] = QtCore.Qt.SquareCap
cap_style[constants.CAP_BUTT] = QtCore.Qt.FlatCap

join_style = {}
join_style[constants.JOIN_ROUND] = QtCore.Qt.RoundJoin
join_style[constants.JOIN_BEVEL] = QtCore.Qt.BevelJoin
join_style[constants.JOIN_MITER] = QtCore.Qt.MiterJoin

draw_modes = {}
draw_modes[constants.FILL] = QtCore.Qt.OddEvenFill
draw_modes[constants.EOF_FILL] = QtCore.Qt.WindingFill
draw_modes[constants.STROKE] = 0
draw_modes[constants.FILL_STROKE] = QtCore.Qt.OddEvenFill
draw_modes[constants.EOF_FILL_STROKE] = QtCore.Qt.WindingFill

font_styles = {}
font_styles["regular"] = constants.NORMAL
font_styles["bold"] = constants.NORMAL
font_styles["italic"] = constants.ITALIC
font_styles["bold italic"] = constants.ITALIC

font_weights = {
    "regular": constants.WEIGHT_NORMAL,
    "bold": constants.WEIGHT_BOLD,
    "italic": constants.WEIGHT_NORMAL,
    "bold italic": constants.WEIGHT_BOLD,
}

weight_to_qt_weight = {
    constants.WEIGHT_THIN: QtGui.QFont.Weight.Thin,
    constants.WEIGHT_EXTRALIGHT: QtGui.QFont.Weight.ExtraLight,
    constants.WEIGHT_LIGHT: QtGui.QFont.Weight.Light,
    constants.WEIGHT_NORMAL: QtGui.QFont.Weight.Normal,
    constants.WEIGHT_MEDIUM: QtGui.QFont.Weight.Medium,
    constants.WEIGHT_SEMIBOLD: QtGui.QFont.Weight.DemiBold,
    constants.WEIGHT_BOLD: QtGui.QFont.Weight.Bold,
    constants.WEIGHT_EXTRABOLD: QtGui.QFont.Weight.ExtraBold,
    constants.WEIGHT_HEAVY: QtGui.QFont.Weight.Black,
    constants.WEIGHT_EXTRAHEAVY: 99,
}

gradient_coord_modes = {}
gradient_coord_modes["userSpaceOnUse"] = QtGui.QGradient.LogicalMode
gradient_coord_modes["objectBoundingBox"] = QtGui.QGradient.ObjectBoundingMode

gradient_spread_modes = {}
gradient_spread_modes["pad"] = QtGui.QGradient.PadSpread
gradient_spread_modes["repeat"] = QtGui.QGradient.RepeatSpread
gradient_spread_modes["reflect"] = QtGui.QGradient.ReflectSpread


class GraphicsContext(object):
    """ Simple wrapper around a Qt QPainter object.
    """

    def __init__(self, size, *args, **kwargs):
        super().__init__()
        self._width = size[0]
        self._height = size[1]

        self.text_pos = [0.0, 0.0]
        self.text_transform = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)

        # create some sort of device context
        parent = kwargs.pop("parent", None)
        if parent is None:
            # no parent -> offscreen context
            self.qt_dc = QtGui.QPixmap(*size)
        else:
            # normal windowed context
            self.qt_dc = parent

        self.gc = QtGui.QPainter(self.qt_dc)
        self.path = CompiledPath()

        # For HiDPI support, we only need to adjust for `size`
        base_pixel_scale = kwargs.pop("base_pixel_scale", 1)

        # flip y
        trans = QtGui.QTransform()
        trans.translate(0, size[1] / base_pixel_scale)
        trans.scale(1.0, -1.0)
        self.gc.setWorldTransform(trans)

        # enable antialiasing
        self.gc.setRenderHints(
            QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing, True
        )
        # set the pen and brush to useful defaults
        self.gc.setPen(QtCore.Qt.black)
        self.gc.setBrush(QtGui.QBrush(QtCore.Qt.SolidPattern))

    def __del__(self):
        # stop the painter if needed
        if self.gc.isActive():
            self.gc.end()

    # ----------------------------------------------------------------
    # Size info
    # ----------------------------------------------------------------

    def height(self):
        """ Returns the height of the context.
        """
        return self._height

    def width(self):
        """ Returns the width of the context.
        """
        return self._width

    # ----------------------------------------------------------------
    # Coordinate Transform Matrix Manipulation
    # ----------------------------------------------------------------

    def scale_ctm(self, sx, sy):
        """ Set the coordinate system scale to the given values, (sx, sy).

            sx:float -- The new scale factor for the x axis
            sy:float -- The new scale factor for the y axis
        """
        self.gc.scale(sx, sy)

    def translate_ctm(self, tx, ty):
        """ Translate the coordinate system by the given value by (tx, ty)

            tx:float --  The distance to move in the x direction
            ty:float --   The distance to move in the y direction
        """
        self.gc.translate(tx, ty)

    def rotate_ctm(self, angle):
        """ Rotates the coordinate space for drawing by the given angle.

            angle:float -- the angle, in radians, to rotate the coordinate
                           system
        """
        self.gc.rotate(np.rad2deg(angle))

    def concat_ctm(self, transform):
        """ Concatenate the transform to current coordinate transform matrix.

            transform:affine_matrix -- the transform matrix to concatenate with
                                       the current coordinate matrix.
        """
        m11, m12, m21, m22, tx, ty = transform
        self.gc.setTransform(
            QtGui.QTransform(m11, m12, m21, m22, tx, ty), True
        )

    def get_ctm(self):
        """ Return the current coordinate transform matrix.
        """
        t = self.gc.transform()
        return (t.m11(), t.m12(), t.m21(), t.m22(), t.dx(), t.dy())

    # ----------------------------------------------------------------
    # Save/Restore graphics state.
    # ----------------------------------------------------------------

    def save_state(self):
        """ Save the current graphic's context state.

            This should always be paired with a restore_state
        """
        self.gc.save()

    def restore_state(self):
        """ Restore the previous graphics state.
        """
        self.gc.restore()

    # ----------------------------------------------------------------
    # context manager interface
    # ----------------------------------------------------------------

    def __enter__(self):
        self.save_state()

    def __exit__(self, type, value, traceback):
        self.restore_state()

    # ----------------------------------------------------------------
    # Manipulate graphics state attributes.
    # ----------------------------------------------------------------

    def set_antialias(self, value):
        """ Set/Unset antialiasing for bitmap graphics context.
        """
        self.gc.setRenderHints(
            QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing,
            value,
        )

    def set_line_width(self, width):
        """ Set the line width for drawing

            width:float -- The new width for lines in user space units.
        """
        pen = self.gc.pen()
        pen.setWidthF(width)
        self.gc.setPen(pen)

    def set_line_join(self, style):
        """ Set style for joining lines in a drawing.

            style:join_style -- The line joining style.  The available
                                styles are JOIN_ROUND, JOIN_BEVEL, JOIN_MITER.
        """
        try:
            sjoin = join_style[style]
        except KeyError:
            msg = "Invalid line join style. See documentation for valid styles"
            raise ValueError(msg)

        pen = self.gc.pen()
        pen.setJoinStyle(sjoin)
        self.gc.setPen(pen)

    def set_miter_limit(self, limit):
        """ Specifies limits on line lengths for mitering line joins.

            If line_join is set to miter joins, the limit specifies which
            line joins should actually be mitered.  If lines aren't mitered,
            they are joined with a bevel.  The line width is divided by
            the length of the miter.  If the result is greater than the
            limit, the bevel style is used.

            limit:float -- limit for mitering joins.
        """
        pen = self.gc.pen()
        pen.setMiterLimit(limit)
        self.gc.setPen(pen)

    def set_line_cap(self, style):
        """ Specify the style of endings to put on line ends.

            style:cap_style -- the line cap style to use. Available styles
                               are CAP_ROUND, CAP_BUTT, CAP_SQUARE
        """
        try:
            scap = cap_style[style]
        except KeyError:
            msg = "Invalid line cap style.  See documentation for valid styles"
            raise ValueError(msg)

        pen = self.gc.pen()
        pen.setCapStyle(scap)
        self.gc.setPen(pen)

    def set_line_dash(self, lengths, phase=0):
        """

            lengths:float array -- An array of floating point values
                                   specifing the lengths of on/off painting
                                   pattern for lines.
            phase:float -- Specifies how many units into dash pattern
                           to start.  phase defaults to 0.
        """
        lengths = list(lengths) if lengths is not None else []
        pen = self.gc.pen()
        pen.setDashPattern(lengths)
        pen.setDashOffset(phase)
        self.gc.setPen(pen)

    def set_flatness(self, flatness):
        """ Not implemented

            It is device dependent and therefore not recommended by
            the PDF documentation.
        """
        raise NotImplementedError()

    # ----------------------------------------------------------------
    # Sending drawing data to a device
    # ----------------------------------------------------------------

    def flush(self):
        """ Send all drawing data to the destination device.
        """
        pass

    def synchronize(self):
        """ Prepares drawing data to be updated on a destination device.
        """
        pass

    # ----------------------------------------------------------------
    # Page Definitions
    # ----------------------------------------------------------------

    def begin_page(self):
        """ Create a new page within the graphics context.
        """
        pass

    def end_page(self):
        """ End drawing in the current page of the graphics context.
        """
        pass

    # ----------------------------------------------------------------
    # Building paths (contours that are drawn)
    #
    # + Currently, nothing is drawn as the path is built.  Instead, the
    #   instructions are stored and later drawn.  Should this be changed?
    #   We will likely draw to a buffer instead of directly to the canvas
    #   anyway.
    #
    #   Hmmm. No.  We have to keep the path around for storing as a
    #   clipping region and things like that.
    #
    # + I think we should keep the current_path_point hanging around.
    #
    # ----------------------------------------------------------------

    def begin_path(self):
        """ Clear the current drawing path and begin a new one.
        """
        self.path = CompiledPath()

    def move_to(self, x, y):
        """ Start a new drawing subpath at place the current point at (x, y).
        """
        self.path.move_to(x, y)

    def line_to(self, x, y):
        """ Add a line from the current point to the given point (x, y).

            The current point is moved to (x, y).
        """
        self.path.line_to(x, y)

    def lines(self, points):
        """ Add a series of lines as a new subpath.

            Currently implemented by calling line_to a zillion times.

            Points is an Nx2 array of x, y pairs.
        """
        self.path.lines(points)

    def line_set(self, starts, ends):
        """ Draw multiple disjoint line segments.
        """
        for start, end in zip(starts, ends):
            self.path.path.moveTo(start[0], start[1])
            self.path.path.lineTo(end[0], end[1])

    def rect(self, x, y, sx, sy):
        """ Add a rectangle as a new subpath.
        """
        self.path.rect(x, y, sx, sy)

    def rects(self, rects):
        """ Add multiple rectangles as separate subpaths to the path.
        """
        self.path.rects(rects)

    def draw_rect(self, rect, mode=constants.FILL_STROKE):
        """ Draw a rect.
        """
        rect = QtCore.QRectF(*rect)
        if mode == constants.STROKE:
            save_brush = self.gc.brush()
            self.gc.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
            self.gc.drawRect(rect)
            self.gc.setBrush(save_brush)
        elif mode in [constants.FILL, constants.EOF_FILL]:
            self.gc.fillRect(rect, self.gc.brush())
        else:
            self.gc.fillRect(rect, self.gc.brush())
            self.gc.drawRect(rect)

    def add_path(self, path):
        """ Add a subpath to the current path.
        """
        self.path.add_path(path)

    def close_path(self):
        """ Close the path of the current subpath.
        """
        self.path.close_path()

    def curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y):
        """
        """
        self.path.curve_to(cp1x, cp1y, cp2x, cp2y, x, y)

    def quad_curve_to(self, cpx, cpy, x, y):
        """
        """
        self.path.quad_curve_to(cpx, cpy, x, y)

    def arc(self, x, y, radius, start_angle, end_angle, clockwise=False):
        """
        """
        self.path.arc(x, y, radius, start_angle, end_angle, clockwise)

    def arc_to(self, x1, y1, x2, y2, radius):
        """
        """
        self.path.arc_to(x1, y1, x2, y2, radius)

    # ----------------------------------------------------------------
    # Getting infomration on paths
    # ----------------------------------------------------------------

    def is_path_empty(self):
        """ Test to see if the current drawing path is empty
        """
        return self.path.is_empty()

    def get_path_current_point(self):
        """ Return the current point from the graphics context.
        """
        return self.path.get_current_point()

    def get_path_bounding_box(self):
        """ Return the bounding box for the current path object.
        """
        return self.path.get_bounding_box()

    # ----------------------------------------------------------------
    # Clipping path manipulation
    # ----------------------------------------------------------------

    def clip(self):
        """
        """
        self.gc.setClipPath(self.path.path)

    def even_odd_clip(self):
        """
        """
        self.gc.setClipPath(self.path.path, operation=QtCore.Qt.IntersectClip)

    def clip_to_rect(self, x, y, w, h):
        """ Clip context to the given rectangular region.

            Region should be a 4-tuple or a sequence.
        """
        self.gc.setClipRect(
            QtCore.QRectF(x, y, w, h), operation=QtCore.Qt.IntersectClip
        )

    def clip_to_rects(self, rects):
        """
        """
        # Create a region which is a union of all rects.
        clip_region = QtGui.QRegion()
        for rect in rects:
            clip_region = clip_region.unite(QtGui.QRegion(*rect))

        # Then intersect that region with the current clip region.
        self.gc.setClipRegion(clip_region, operation=QtCore.Qt.IntersectClip)

    # ----------------------------------------------------------------
    # Color space manipulation
    #
    # I'm not sure we'll mess with these at all.  They seem to
    # be for setting the color system.  Hard coding to RGB or
    # RGBA for now sounds like a reasonable solution.
    # ----------------------------------------------------------------

    def set_fill_color_space(self):
        """
        """
        msg = "set_fill_color_space not implemented on Qt yet."
        raise NotImplementedError(msg)

    def set_stroke_color_space(self):
        """
        """
        msg = "set_stroke_color_space not implemented on Qt yet."
        raise NotImplementedError(msg)

    def set_rendering_intent(self):
        """
        """
        msg = "set_rendering_intent not implemented on Qt yet."
        raise NotImplementedError(msg)

    # ----------------------------------------------------------------
    # Color manipulation
    # ----------------------------------------------------------------

    def set_fill_color(self, color):
        """
        """
        r, g, b = color[:3]
        try:
            a = color[3]
        except IndexError:
            a = 1.0
        brush = self.gc.brush()
        brush.setColor(QtGui.QColor.fromRgbF(r, g, b, a))
        self.gc.setBrush(brush)

    def set_stroke_color(self, color):
        """
        """
        r, g, b = color[:3]
        try:
            a = color[3]
        except IndexError:
            a = 1.0
        pen = self.gc.pen()
        pen.setColor(QtGui.QColor.fromRgbF(r, g, b, a))
        self.gc.setPen(pen)

    def set_alpha(self, alpha):
        """
        """
        self.gc.setOpacity(alpha)

    # ----------------------------------------------------------------
    # Gradients
    # ----------------------------------------------------------------

    def _apply_gradient(self, grad, stops, spread_method, units):
        """ Configures a gradient object and sets it as the current brush.
        """
        grad.setSpread(
            gradient_spread_modes.get(spread_method, QtGui.QGradient.PadSpread)
        )
        grad.setCoordinateMode(
            gradient_coord_modes.get(units, QtGui.QGradient.LogicalMode)
        )

        for stop in stops:
            grad.setColorAt(stop[0], QtGui.QColor.fromRgbF(*stop[1:]))

        self.gc.setBrush(QtGui.QBrush(grad))

    def linear_gradient(self, x1, y1, x2, y2, stops, spread_method,
                        units="userSpaceOnUse"):
        """ Sets a linear gradient as the current brush.
        """
        grad = QtGui.QLinearGradient(x1, y1, x2, y2)
        self._apply_gradient(grad, stops, spread_method, units)

    def radial_gradient(self, cx, cy, r, fx, fy, stops, spread_method,
                        units="userSpaceOnUse"):
        """ Sets a radial gradient as the current brush.
        """
        grad = QtGui.QRadialGradient(cx, cy, r, fx, fy)
        self._apply_gradient(grad, stops, spread_method, units)

    # ----------------------------------------------------------------
    # Drawing Images
    # ----------------------------------------------------------------

    def draw_image(self, img, rect=None):
        """
        img is either a N*M*3 or N*M*4 numpy array, or a PIL Image

        rect - a tuple (x, y, w, h)
        """
        from PIL import Image, ImageQt

        if isinstance(img, np.ndarray):
            # Numeric array
            pilimg = Image.fromarray(img)
            width, height = pilimg.width, pilimg.height
            draw_img = ImageQt.ImageQt(pilimg)
            pixmap = QtGui.QPixmap.fromImage(draw_img)
        elif isinstance(img, Image.Image):
            width, height = img.width, img.height
            draw_img = ImageQt.ImageQt(img)
            pixmap = QtGui.QPixmap.fromImage(draw_img)
        elif hasattr(img, "bmp_array"):
            # An offscreen kiva agg context
            pilimg = Image.fromarray(img.bmp_array)
            width, height = pilimg.width, pilimg.height
            draw_img = ImageQt.ImageQt(pilimg)
            pixmap = QtGui.QPixmap.fromImage(draw_img)
        elif (isinstance(img, GraphicsContext)
                and isinstance(img.qt_dc, QtGui.QPixmap)
                and img.gc.isActive()):
            # An offscreen Qt kiva context
            # Calling qpainter.device() appears to introduce a memory leak.
            # using the display context and calling qpainter.isActive() has
            # the same outcome.
            pixmap = img.qt_dc
            width, height = pixmap.width(), pixmap.height()
        else:
            msg = "Cannot render image of type '%r' into Qt4 context."
            warnings.warn(msg % type(img))
            return

        # create a rect object to draw into
        if rect is None:
            dest_rect = QtCore.QRectF(0.0, 0.0, self.width(), self.height())
        else:
            dest_rect = QtCore.QRectF(*rect)

        # draw using the entire image's data
        source_rect = QtCore.QRectF(0.0, 0.0, width, height)

        flip_trans = QtGui.QTransform()
        flip_trans.scale(1.0, -1.0)
        pixmap = pixmap.transformed(flip_trans)

        # draw
        self.gc.drawPixmap(dest_rect, pixmap, source_rect)

    # ----------------------------------------------------------------
    # Drawing Text
    # ----------------------------------------------------------------

    def select_font(self, name, size, style="regular", encoding=None):
        """ Set the font for the current graphics context.
        """
        style = font_styles.get(style, constants.NORMAL)
        weight = font_weights.get(style, constants.WEIGHT_NORMAL)
        font = Font(name, size=size, style=style, weight=weight)
        self.set_font(font)

    def set_font(self, font):
        """ Set the font for the current graphics context.
        """
        qfont = QtGui.QFont(font.face_name, font.size)

        weight = font._get_weight()
        qfont.setWeight(weight_to_qt_weight[weight])

        qfont.setItalic(font.style in constants.italic_styles)

        self.gc.setFont(qfont)

    def set_font_size(self, size):
        """
        """
        font = self.gc.font()
        font.setPointSizeF(size)
        self.gc.setFont(font)

    def set_character_spacing(self, spacing):
        """ Set the spacing between characters when drawing text
        """
        font = self.gc.font()
        font.setLetterSpacing(QtGui.QFont.AbsoluteSpacing, spacing)
        self.gc.setFont(font)

    def get_character_spacing(self):
        """ Get the spacing between characters when drawing text
        """
        font = self.gc.font()
        return font.letterSpacing()

    def set_text_drawing_mode(self, mode):
        """ Set the drawing mode to use with text
        """
        raise NotImplementedError()

    def set_text_position(self, x, y):
        """
        """
        self.text_pos = [x, y]

    def get_text_position(self):
        """
        """
        return self.text_pos

    def set_text_matrix(self, ttm):
        """
        """
        self.text_transform = ttm

    def get_text_matrix(self):
        """
        """
        return self.text_transform

    def show_text(self, text, point=None):
        """ Draw text on the device at current text position.

            This is also used for showing text at a particular point
            specified by x and y.
        """
        if point is None:
            pos = tuple(self.text_pos)
        else:
            pos = tuple(point)

        unflip_trans = QtGui.QTransform(*self.text_transform)
        unflip_trans.translate(0, self._height)
        unflip_trans.scale(1.0, -1.0)

        # Make some temporary modifications to the state
        with self:
            # Kiva uses the fill color for text
            brush = self.gc.brush()
            self.gc.setPen(brush.color())
            self.gc.setTransform(unflip_trans, True)
            pos = QtCore.QPointF(pos[0], self._flip_y(pos[1]))
            self.gc.drawText(pos, text)

    def show_text_at_point(self, text, x, y):
        """ Draw text at some point (x, y).
        """
        self.show_text(text, (x, y))

    def show_glyphs(self):
        """
        """
        msg = "show_glyphs not implemented on Qt yet."
        raise NotImplementedError(msg)

    def get_text_extent(self, text):
        """ Returns the bounding rect of the rendered text
        """
        fm = self.gc.fontMetrics()
        rect = fm.boundingRect(text)

        return rect.left(), -fm.descent(), rect.right(), fm.height()

    def get_full_text_extent(self, text):
        """ Backwards compatibility API over .get_text_extent() for Enable
        """
        x1, y1, x2, y2 = self.get_text_extent(text)

        return x2, y2, y1, x1

    # ----------------------------------------------------------------
    # Painting paths (drawing and filling contours)
    # ----------------------------------------------------------------

    def stroke_path(self):
        """
        """
        self.gc.strokePath(self.path.path, self.gc.pen())
        self.begin_path()

    def fill_path(self):
        """
        """
        self.path.path.setFillRule(QtCore.Qt.WindingFill)
        self.gc.fillPath(self.path.path, self.gc.brush())
        self.begin_path()

    def eof_fill_path(self):
        """
        """
        self.path.path.setFillRule(QtCore.Qt.OddEvenFill)
        self.gc.fillPath(self.path.path, self.gc.brush())
        self.begin_path()

    def stroke_rect(self, rect):
        """
        """
        self.gc.drawRect(QtCore.QRectF(*rect))

    def stroke_rect_with_width(self, rect, width):
        """
        """
        save_pen = self.gc.pen()
        draw_pen = QtGui.QPen(save_pen)
        draw_pen.setWidthF(width)

        self.gc.setPen(draw_pen)
        self.stroke_rect(rect)
        self.gc.setPen(save_pen)

    def fill_rect(self, rect):
        """
        """
        self.gc.fillRect(QtCore.QRectF(*rect), self.gc.brush())

    def fill_rects(self):
        """
        """
        msg = "fill_rects not implemented on Qt yet."
        raise NotImplementedError(msg)

    def clear_rect(self, rect):
        """
        """
        self.gc.eraseRect(QtCore.QRectF(*rect))

    def clear(self, clear_color=(1.0, 1.0, 1.0, 1.0)):
        """
        """
        if len(clear_color) == 4:
            r, g, b, a = clear_color
        else:
            r, g, b = clear_color
            a = 1.0
        self.gc.setBackground(QtGui.QBrush(QtGui.QColor.fromRgbF(r, g, b, a)))
        self.gc.eraseRect(QtCore.QRectF(0, 0, self.width(), self.height()))

    def draw_path(self, mode=constants.FILL_STROKE):
        """ Walk through all the drawing subpaths and draw each element.

            Each subpath is drawn separately.
        """
        if mode == constants.STROKE:
            self.stroke_path()
        elif mode in [constants.FILL, constants.EOF_FILL]:
            mode = draw_modes[mode]
            self.path.path.setFillRule(mode)
            self.fill_path()
        else:
            mode = draw_modes[mode]
            self.path.path.setFillRule(mode)
            self.gc.drawPath(self.path.path)
        self.begin_path()

    def get_empty_path(self):
        """ Return a path object that can be built up and then reused.
        """
        return CompiledPath()

    def draw_path_at_points(self, points, path, mode=constants.FILL_STROKE):
        # set up drawing state and function
        if mode == constants.STROKE:
            draw_func = partial(self.gc.strokePath, path.path, self.gc.pen())
        elif mode in [constants.FILL, constants.EOF_FILL]:
            mode = draw_modes[mode]
            path.path.setFillRule(mode)
            draw_func = partial(self.gc.fillPath, path.path, self.gc.brush())
        else:
            mode = draw_modes[mode]
            path.path.setFillRule(mode)
            draw_func = partial(self.gc.drawPath, path.path)

        for point in points:
            x, y = point
            self.gc.save()
            self.gc.translate(x, y)
            draw_func()
            self.gc.restore()

    def _flip_y(self, y):
        "Converts between a Kiva and a Qt y coordinate"
        return self._height - y - 1

    def save(self, filename, file_format=None, pil_options=None):
        """ Save the contents of the context to a file
        """
        if isinstance(self.qt_dc, QtGui.QPixmap):
            self.qt_dc.save(filename, format=file_format)
        else:
            msg = "save not implemented for window contexts."
            raise NotImplementedError(msg)


class CompiledPath(object):
    def __init__(self):
        self.path = QtGui.QPainterPath()

    def begin_path(self):
        return

    def move_to(self, x, y):
        self.path.moveTo(x, y)

    def arc(self, x, y, r, start_angle, end_angle, clockwise=False):
        sweep_angle = (
            end_angle - start_angle
            if not clockwise
            else start_angle - end_angle
        )
        self.path.moveTo(x, y)
        self.path.arcTo(
            QtCore.QRectF(x - r, y - r, r * 2, r * 2),
            np.rad2deg(start_angle),
            np.rad2deg(sweep_angle),
        )

    def arc_to(self, x1, y1, x2, y2, r):
        # get the current pen position
        current_point = self.get_current_point()

        # Get the two points on the curve where it touches the line segments
        t1, t2 = arc_to_tangent_points(current_point, (x1, y1), (x2, y2), r)

        # draw!
        self.path.lineTo(*t1)
        self.path.quadTo(x1, y1, *t2)
        self.path.lineTo(x2, y2)

    def line_to(self, x, y):
        self.path.lineTo(x, y)

    def lines(self, points):
        self.path.moveTo(points[0][0], points[0][1])
        for x, y in points[1:]:
            self.path.lineTo(x, y)

    def curve_to(self, cx1, cy1, cx2, cy2, x, y):
        self.path.cubicTo(cx1, cy1, cx2, cy2, x, y)

    def quad_curve_to(self, cx, cy, x, y):
        self.path.quadTo(cx, cy, x, y)

    def rect(self, x, y, sx, sy):
        self.path.addRect(x, y, sx, sy)

    def rects(self, rects):
        for x, y, sx, sy in rects:
            self.path.addRect(x, y, sx, sy)

    def add_path(self, other_path):
        if isinstance(other_path, CompiledPath):
            self.path.addPath(other_path.path)

    def close_path(self):
        self.path.closeSubpath()

    def is_empty(self):
        return self.path.isEmpty()

    def get_current_point(self):
        point = self.path.currentPosition()
        return point.x(), point.y()

    def get_bounding_box(self):
        rect = self.path.boundingRect()
        return rect.x(), rect.y(), rect.width(), rect.height()


# GraphicsContext should implement AbstractGraphicsContext
AbstractGraphicsContext.register(GraphicsContext)


def font_metrics_provider():
    """ Creates an object to be used for querying font metrics.
    """
    return GraphicsContext((1, 1))