Skip to content

Commit

Permalink
Updates and improvements to Kiva documentation (#914)
Browse files Browse the repository at this point in the history
* Updates and improvements to documentation.

In particular, this:
- adds many lins to the API documentation
- fixes #869
- adds a stub document discussing writing new Kiva backends

* Add more info about backends.

* Fixes from review.
  • Loading branch information
corranwebster authored Mar 18, 2022
1 parent b9b145d commit a9b069f
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 152 deletions.
5 changes: 3 additions & 2 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Kiva
:maxdepth: 2

kiva/overview
kiva/quickref
kiva/drawing_details
kiva/quickref

Enable
------
Expand Down Expand Up @@ -48,9 +48,10 @@ Internal Documentation
----------------------

.. toctree::
:maxdepth: 1
:maxdepth: 2

kiva/font_manager
kiva/writing_a_backend

----

Expand Down
184 changes: 122 additions & 62 deletions docs/source/kiva/drawing_details.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Kiva Drawing In-depth

Kiva State
==========

Kiva is a "stateful" drawing API. What this means is that the graphics context
has a collection of state which affects the results of its drawing actions.
Furthermore, Kiva enables this state to be managed with a stack such that state
Expand All @@ -13,36 +14,52 @@ includes those changes.

State Components
----------------

Here is a list of all the pieces of state tracked by a Kiva graphics context,
along with the methods which operate on them:

* Affine transformation (:py:meth:`translate_ctm`, :py:meth:`rotate_ctm`,
:py:meth:`scale_ctm`, :py:meth:`concat_ctm`, :py:meth:`set_ctm`,
:py:meth:`get_ctm`)
* Clipping (:py:meth:`clip_to_rect`, :py:meth:`clip_to_rects`, :py:meth:`clip`,
:py:meth:`even_odd_clip`)
* Fill color (:py:meth:`set_fill_color`, :py:meth:`get_fill_color`,
:py:meth:`linear_gradient`, :py:meth:`radial_gradient`)
* Stroke color (:py:meth:`set_stroke_color`, :py:meth:`get_stroke_color`)
* Line width (:py:meth:`set_line_width`)
* Line join style (:py:meth:`set_line_join`)
* Line cap style (:py:meth:`set_line_cap`)
* Line dashing (:py:meth:`set_line_dash`)
* Global transparency (:py:meth:`set_alpha`, :py:meth:`get_alpha`)
* Anti-aliasing (:py:meth:`set_antialias`, :py:meth:`get_antialias`)
* Miter limit (:py:meth:`set_miter_limit`)
* Flatness (:py:meth:`set_flatness`)
* Image interpolation (:py:meth:`set_image_interpolation`, :py:meth:`get_image_interpolation`)
* Text drawing mode (:py:meth:`set_text_drawing_mode`)
* Affine transformation: :py:meth:`~.AbstractGraphicsContext.translate_ctm`,
:py:meth:`~.AbstractGraphicsContext.rotate_ctm`,
:py:meth:`~.AbstractGraphicsContext.scale_ctm`,
:py:meth:`~.AbstractGraphicsContext.concat_ctm`,
:py:meth:`~.AbstractGraphicsContext.set_ctm`,
:py:meth:`~.AbstractGraphicsContext.get_ctm`
* Clipping: :py:meth:`~.AbstractGraphicsContext.clip_to_rect`,
:py:meth:`~.AbstractGraphicsContext.clip_to_rects`,
:py:meth:`~.AbstractGraphicsContext.clip`,
:py:meth:`~.AbstractGraphicsContext.even_odd_clip`
* Fill color: :py:meth:`~.AbstractGraphicsContext.set_fill_color`,
:py:meth:`~.AbstractGraphicsContext.get_fill_color`,
:py:meth:`~.AbstractGraphicsContext.linear_gradient`,
:py:meth:`~.AbstractGraphicsContext.radial_gradient`
* Stroke color: :py:meth:`~.AbstractGraphicsContext.set_stroke_color`,
:py:meth:`~.AbstractGraphicsContext.get_stroke_color`
* Line width: :py:meth:`~.AbstractGraphicsContext.set_line_width`
* Line join style: :py:meth:`~.AbstractGraphicsContext.set_line_join`
* Line cap style: :py:meth:`~.AbstractGraphicsContext.set_line_cap`
* Line dashing: :py:meth:`~.AbstractGraphicsContext.set_line_dash`
* Global transparency: :py:meth:`~.AbstractGraphicsContext.set_alpha`,
:py:meth:`~.AbstractGraphicsContext.get_alpha`
* Anti-aliasing: :py:meth:`~.AbstractGraphicsContext.set_antialias`,
:py:meth:`~.AbstractGraphicsContext.get_antialias`
* Miter limit: :py:meth:`~.AbstractGraphicsContext.set_miter_limit`
* Flatness: :py:meth:`~.AbstractGraphicsContext.set_flatness`
* Image interpolation:
:py:meth:`~.AbstractGraphicsContext.set_image_interpolation`,
:py:meth:`~.AbstractGraphicsContext.get_image_interpolation`)
* Text drawing mode: :py:meth:`~.AbstractGraphicsContext.set_text_drawing_mode`

Color
-----

Kiva has two colors in its graphics state: stroke color and fill color. Stroke
color is used for the lines in paths when the drawing mode is ``STROKE``,
``FILL_STROKE`` or ``EOF_FILL_STROKE``. Fill color is used for text and for
the enclosed sections of paths when the drawing mode is ``FILL``, ``EOF_FILL``,
``FILL_STROKE``, or ``EOF_FILL_STROKE``. Additionally, the fill color can be
set by the :py:meth:`linear_gradient` and :py:meth:`radial_gradient` methods.
set by the :py:meth:`~.AbstractGraphicsContext.linear_gradient` and
:py:meth:`~.AbstractGraphicsContext.radial_gradient` methods where they are
available.

.. note::
Even though text uses the fill color, text will not be filled with a
Expand All @@ -56,27 +73,34 @@ blending, it's still OK to pass a 4 component color value when setting state.

State Stack Management
----------------------

Graphics context instances have two methods for saving and restoring the state,
:py:meth:`save_state` ("push") and :py:meth:`restore_state` ("pop"). That said,
it isn't recommended practice to call the methods directly. Instead, you can
treat the graphics context object as a
`context manager <https://docs.python.org/3/library/stdtypes.html#typecontextmanager>`_
and use the ``with`` keyword to create a block of code where the graphics state
is temporarily modified. Using the context manager approach provides safety from
"temporary" modifications becoming permanent if an uncaught exception is raised
while drawing.
:py:meth:`~.AbstractGraphicsContext.save_state` ("push") and
:py:meth:`~.AbstractGraphicsContext.restore_state` ("pop"). For robust drawing
every "push" should be matched by a corresponding "pop" at some later point,
even if there is an error or other exception.

For this reason all graphics contexts are
`context managers <https://docs.python.org/3/library/stdtypes.html#typecontextmanager>`_
and can use the ``with`` keyword to create a block of code where the graphics
state is temporarily modified: it is "pushed" at the start of the ``with``
block and "popped" at the end. Using the context manager approach provides
safety from "temporary" modifications becoming permanent if an uncaught
exception is raised while drawing.

In Enable and Chaco, it is frequently the case that a graphics context instance
will be passed into a method for the purpose of some drawing. Because it is not
reasonable to push the responsibility of state management "up" the call stack,
the onus is on the code making state modifications to do them safely so that
other changes don't leak into other code.

**Well behaved code should take care to only modify graphics state inside a**
``with`` **block**.
.. note::
Well-behaved code should take care to only modify graphics state inside a
``with`` block.

Example
-------

.. image:: images/state_ex.png
:width: 300
:height: 300
Expand All @@ -96,10 +120,11 @@ join, and cap:
:linenos:
:lineno-match:

Then in a loop, we draw twice (the two :py:meth:`stroke_path` calls). The first
draw uses a ``with`` block to temporarily modify the drawing state. It adds more
affine transformations: a rotate and a translate. It also changes some line
properties: stroke color, width, and cap. A rectangle is then added to the
Then in a loop, we draw twice (the two
:py:meth:`~.AbstractGraphicsContext.stroke_path` calls). The first draw uses a
``with`` block to temporarily modify the drawing state. It adds more affine
transformations: a rotate and a translate. It also changes some line
properties: stroke color, width, and cap. A rectangle is then added to the
current path and stroked.

.. literalinclude:: state_ex.py
Expand Down Expand Up @@ -129,22 +154,24 @@ basic unit of drawing in a graphics context.
Every graphics context instance has a current path which can be manipulated by
the :ref:`kiva_path_functions`. However, some drawing operations are easier to
implement with an independent path instance
(specifically :py:meth:`draw_path_at_points`).
(specifically :py:meth:`~.AbstractGraphicsContext.draw_path_at_points`).

An independent path instance can be created in two ways. The first is via the
:py:meth:`GraphicsContext.get_empty_path` method. The second method is to use
the :class:`CompiledPath` class imported from the backend being used. The
interface of a :class:`CompiledPath` instance is the same as the
:ref:`kiva_path_functions` (modulo :py:meth:`get_empty_path`).
:py:meth:`~.AbstractGraphicsContext.get_empty_path` method. The second method
is to use the :class:`CompiledPath` class imported from the backend being used.
The interface of a :class:`CompiledPath` instance is the same as the
:ref:`kiva_path_functions` (modulo
:py:meth:`~.AbstractGraphicsContext.get_empty_path`).

Once you have a path object, it can be drawn by adding it to the graphics
context with the :py:meth:`GraphicsContext.add_path` method (which adds the path
to the current path) and then calling any of the :ref:`kiva_drawing_functions`
which operate on the current path.
context with the :py:meth:`~.AbstractGraphicsContext.add_path` method (which
adds the path to the current path) and then calling any of the
:ref:`kiva_drawing_functions` which operate on the current path.

For certain backends which support it, the
:py:meth:`GraphicsContext.draw_path_at_points` method can be used to draw a
path object at many different positions with a single function call.
:py:meth:`~.EnhancedAbstractGraphicsContext.draw_path_at_points` method can be
used to draw a path object at many different positions with a single function
call.

Example
-------
Expand All @@ -160,9 +187,10 @@ Kiva Image Rendering
====================

Drawing images in kiva is accomplished via
:py:meth:`GraphicsContext.draw_image`. A unique feature of drawing images
(relative to path drawing) is that you can apply an arbitrary translation and
scaling to the image without involving the current transformation matrix.
:py:meth:`~.AbstractGraphicsContext.draw_image`. A unique feature of drawing
images (relative to path drawing) is that you can apply an arbitrary
translation and scaling to the image without involving the current
transformation matrix.

The signature for :py:meth:`draw_image` is straightforward:

Expand Down Expand Up @@ -191,7 +219,7 @@ the scaling is the ratio of the image's width and height to those of the
rectangle. In every case, ``rect`` will be transformed by the current
transformation matrix.

Special considerations
Special Considerations
----------------------
If you only want to draw a subset of an image, you should pass only that subset
to :py:meth:`draw_image`. The Kiva API does not support defining a "source"
Expand All @@ -208,17 +236,20 @@ with the :py:meth:`set_image_interpolation` method.

Saving images
-------------

One can also save the contents of a graphics context to an image. This is done
via the :py:meth:`save` method:
via the :py:meth:`~.AbstractGraphicsContext.save` method. Different backends
support different output formats, and so in most cases you want to render to
the graphics context that best matches the output (eg. a raster format for
JPEG or PNG, or SVG, PDF and PS backends for the approriate vector formats).

.. automethod:: kiva.abstract_graphics_context.AbstractGraphicsContext.save
:noindex:


Kiva Text Rendering
===================

Drawing text in kiva is accomplished via a few methods on
Drawing text in kiva is accomplished via a few methods on
:class:`GraphicsContext`. There are three basic topics: selecting a font,
measuring the size of rendered text, and drawing the text.

Expand All @@ -233,10 +264,7 @@ Simplest: ``select_font``
~~~~~~~~~~~~~~~~~~~~~~~~~

The simplest form of font selection is the
:py:meth:`GraphicsContext.select_font` method. The tradeoff for this simplicity
is that you're at the mercy of the backend's font lookup. If your desired font
isn't available from the system you're using, it's not defined what you will end
up with.
:py:meth:`~.AbstractGraphicsContext.select_font` method.

``select_font(name, size=12)``

Expand All @@ -245,30 +273,36 @@ up with.

``size`` is the size in points.

**Supported backends**: cairo, celiagg, pdf, ps, qpainter, quartz, svg.
*Supported backends*: cairo, celiagg, pdf, ps, qpainter, quartz, svg.


The ``KivaFont`` trait and ``set_font``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you're already doing your drawing within an application using traits, you can
use the :class:`kiva.trait_defs.api.KivaFont` trait.
use the :class:`~kiva.trait_defs.kiva_font_trait.KivaFont` trait.

``KivaFont`` traits are initialized with a string which describes the font:
"Times Italic 18", "Courier Bold 10", etc. The *value* of the trait is a
:class:`kiva.fonttools.font.Font` instance which can be passed to the
:py:meth:`GraphicsContext.set_font` method.
:class:`~kiva.trait_defs.kiva_font_trait.KivaFont` traits are initialized with
a string which describes the font: "Times Italic 18", "Courier Bold 10", etc.
The *value* of the trait is a :class:`~kiva.fonttools.font.Font` instance which
can be passed to the :py:meth:`~.AbstractGraphicsContext.set_font` method.

**Supported backends**: all backends
*Supported backends*: all backends

.. note::
The :class:`~kiva.trait_defs.kiva_font_trait.KivaFont` parser is very
simplistic and special-cases some words. For example "roman" means a
generic serif-style font family, so for example a face name of "Times New
Roman" will not resolve as expected. In these cases, use a
:class:`~kiva.fonttools.font.Font` instance.

``Font`` objects
~~~~~~~~~~~~~~~~

If you don't want to rely on the font description parsing in ``KivaFont``, you
can also manually construct a :class:`kiva.fonttools.font.Font` instance. Once
you have a ``Font`` instance, it can be passed to the
:py:meth:`GraphicsContext.set_font` method.
:py:meth:`~.AbstractGraphicsContext.set_font` method.

``Font(face_name="", size=12, family=SWISS, weight=NORMAL, style=NORMAL)``

Expand All @@ -287,6 +321,32 @@ desired font.
``style`` is a constant from :py:mod:`kiva.constants`. Pick from ``NORMAL`` or
``ITALIC``.

*Supported backends*: all backends

Resolving Fonts
---------------

In all of the above cases, Kiva attempts to find a good concrete font that
matches the specification to do the drawing. However it is possible that the
desired font is not available on the system where the code is running. In that
case Kiva will fall back to a default font, and Kiva includes a basic font in
case all else fails.

Different backends use different mechanisms for font resolution. For example
the "qpainter" backend uses Qt's font resolution system, while the SVG backend
translates the font to an SVG font description and leaves it up to the viewer
application to find an appropriate font.

However a number of backends do not have any built-in font support, and so in
those cases Kiva's :mod:`kiva.fonttools` module is used to find system font
files which match the requirements. Application developers who wish to ship
particular fonts as resources with their application can add these to the
fonttools management system via the
:py:func:`~kiva.fonttools.app_font.add_application_fonts` function (see
:ref:`adding_custom_fonts` for more details).

Further details are available in the
:ref:`kiva_font_management` section.

Measuring Text
--------------
Expand Down
Loading

0 comments on commit a9b069f

Please sign in to comment.