Repository: celiagg/celiagg (language: python)

Issue:
Make the font cache a separate object
This is a rather sizable refactor which aims to fix the [Windows] text rendering bugs introduced in #65.

Instead of creating `thread_local` globals in `Font` for management of glyph caching, I've separated out the font cache into a new `FontCache` object which exists independently of the `Canvas*` and `Font` objects. A font cache can be shared between any number of canvas instances, as long as one takes care not to use the cache simultaneously from multiple threads.

One big downside of this change is that it **breaks** the existing `Font` API by moving the `Font.string_width` method to `FontCache.width`. Therefore I intend to bump the package's major version number to 2, as an extra warning to downstream users. `Canvas*` classes also need to be passed a `FontCache` instance at construction time, but existing code can continue to work _without modification_ due to some Python wrappers in the package's `__init__` module.

~It _should_ fix #62, but some tests are needed.~

Fixes #62
Rework text rendering
Hot on the heels of #74, I'm stirring up text rendering yet again.

Namely, I've removed `FontCacheType` entirely. It was something of an implementation detail that had poked its head out and now it's gone. `Font` objects are really just basic descriptions of a font to be loaded when rendering text; they aren't concerned with how they're cached now.

`FontCacheType` was up until now the way to control if text was rendered in raster mode or vector mode. That decision is now captured by the `TextDrawingMode.TextDrawRaster` enumeration value which is also the default value for `GraphicsState.text_drawing_mode`. `TextDrawRaster` is the only text drawing mode which uses the raster rendering path; all other modes use the slower [but much more powerful] vector rendering path.

Another breaking change is that the canvas `draw_text` method now uses the `fill` paint instead of the `stroke` paint when drawing in raster mode. If all of your colored text turns black after this change, that's the reason.


Discussion / hints:

Fix patch:
diff --git a/MANIFEST.in b/MANIFEST.in
index 51a7e6f..659b7f2 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,7 +3,7 @@ include celiagg/*.h
 include celiagg/*.pxd
 include celiagg/*.pxi
 include celiagg/*.pyx
-include celiagg/tests/data/*
+include celiagg/data/*
 include LICENSE
 include README.rst
 graft agg-svn/agg-2.4/include
diff --git a/celiagg/__init__.py b/celiagg/__init__.py
index 894694a..35f176d 100644
--- a/celiagg/__init__.py
+++ b/celiagg/__init__.py
@@ -36,9 +36,21 @@
 # Query the library
 HAS_TEXT = _celiagg.has_text_rendering()
 
+
+def example_font():
+    """ Returns the path to a TTF font which is included with the library for
+    testing purposes.
+    """
+    import pkg_resources
+
+    return pkg_resources.resource_filename(
+        'celiagg', 'data/Montserrat-Regular.ttf'
+    )
+
+
 # Be explicit
 __all__ = [
-    'HAS_TEXT',
+    'HAS_TEXT', 'example_font',
 
     'AggError', 'BlendMode', 'BSpline', 'DrawingMode', 'Font', 'FontCache',
     'GradientSpread', 'GradientUnits', 'GraphicsState', 'Image', 'InnerJoin',
diff --git a/celiagg/tests/data/Montserrat-Regular.ttf b/celiagg/data/Montserrat-Regular.ttf
similarity index 100%
rename from celiagg/tests/data/Montserrat-Regular.ttf
rename to celiagg/data/Montserrat-Regular.ttf
diff --git a/celiagg/tests/data/OFL.txt b/celiagg/data/OFL.txt
similarity index 100%
rename from celiagg/tests/data/OFL.txt
rename to celiagg/data/OFL.txt
diff --git a/docs/source/api.rst b/docs/source/api.rst
index b0df554..4cd9bc8 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -90,6 +90,12 @@ which can be drawn using the ``Canvas.draw_shape`` method.
    :inherited-members:
 
 
+Functions
+---------
+
+.. autofunction:: example_font
+
+
 Enumerations
 ------------
 
@@ -136,17 +142,6 @@ fill variations use the even-odd rule when filling self-intersecting curves.
   * ``DrawFillStroke``
   * ``DrawEofFillStroke``
 
-FontCacheType
-~~~~~~~~~~~~~
-
-Determines whether text is rendered with a raster or vector representation.
-Raster text is faster, but *doesn't support different stroke and fill paints*;
-only solid color. Vector text is slower but very powerful, and can make use of
-gradients or patterned fills/strokes.
-
-  * ``RasterFontCache``
-  * ``VectorFontCache``
-
 GradientSpread
 ~~~~~~~~~~~~~~
 
@@ -236,6 +231,7 @@ TextDrawingMode
   * ``TextDrawFill``
   * ``TextDrawStroke``
   * ``TextDrawClip``
+  * ``TextDrawRaster``
   * ``TextDrawFillStroke``
   * ``TextDrawFillClip``
   * ``TextDrawStrokeClip``
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 3d1af62..31284d7 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -53,7 +53,7 @@
 
 # General information about the project.
 project = 'celiagg'
-copyright = '2015-2018, John Wiggins, Erik Hvatum'
+copyright = '2015-2021, John Wiggins, Erik Hvatum'
 author = 'John Wiggins, Erik Hvatum'
 
 # The version info for the project you're documenting, acts as replacement for
@@ -61,9 +61,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = '1.0.4'
+version = '1.1.2'
 # The full version, including alpha/beta/rc tags.
-release = '1.0.4'
+release = '1.1.2'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/docs/source/example.png b/docs/source/example.png
index 93439de..8213631 100644
Binary files a/docs/source/example.png and b/docs/source/example.png differ
diff --git a/docs/source/example.rst b/docs/source/example.rst
index ebac981..0f65113 100644
--- a/docs/source/example.rst
+++ b/docs/source/example.rst
@@ -19,7 +19,7 @@ Imports
 -------
 
 First, we include some libraries. ``numpy`` is needed for the memory buffer
-which the canvas drawns into. ``scikit-image`` is used for simple image file I/O.
+which the canvas drawns into. ``PIL`` is used for simple image file I/O.
 Finally, ``celiagg`` is imported for obvious reasons:
 
 .. literalinclude:: simple_ex.py
@@ -33,10 +33,10 @@ State setup
 Next, some state objects are created. Most important is the ``GraphicsState``
 object. Pay attention to the ``drawing_mode=agg.DrawingMode.DrawStroke`` keyword
 argument. This means that all drawing calls using this ``state`` will only draw
-the outline of the shapes or text being drawn.
+the outline of the shapes being drawn.
 
 The remaining state objects are ``transform`` which sets the affine
-transformation of the drawing and ``red_paint``/``black_paint`` which determine
+transformation of the drawing and ``red_paint``/``orange_paint`` which determine
 the color of strokes (outlines) or fills when drawing.
 
 .. literalinclude:: simple_ex.py
@@ -61,7 +61,7 @@ Draw a circle
 We're now ready to do some drawing. A ``path`` object is created to describe the
 shape that will be drawn. In this case, it's an ellipse with an equal width and
 height of 190 pixels, ie: a circle. Because the canvas was instantiated with
-``np.ones``, we call the ``canvas.clear`` method first to clear the background
+``np.zeros``, we call the ``canvas.clear`` method first to clear the background
 color to white. Finally, the circle is drawn with ``canvas.draw_shape``.
 
 .. literalinclude:: simple_ex.py
@@ -72,26 +72,29 @@ color to white. Finally, the circle is drawn with ``canvas.draw_shape``.
 Draw some text
 --------------
 
-Next up, some text. As a small caveat, this demo was written to run on macOS
-where the path ``/Library/Fonts/Verdana.ttf`` points to a TTF font file. A raster
-``font`` object is created with a point size of 96. The ``transform`` gets a
+Next up, some text. We'll use the `Montserrat <https://github.com/JulietaUla/Montserrat>`_ font which is included with
+celiagg, conveniently available via the ``example_font`` function. A ``font``
+object is created with a point size of 96. The ``transform`` gets a
 translation of (30, 220) set. This corresponds to a point which is 30 pixels
 from the left side of the image and 220 pixels from the *top* of the image.
 (celiagg defaults to a top-left origin, but also supports bottom-left origin)
 The text will be drawn starting from that point.
 
 .. literalinclude:: simple_ex.py
-  :lines: 18-22
+  :lines: 18-20
   :linenos:
   :lineno-match:
 
 .. note::
    Even though ``state`` specifies a ``drawing_mode`` of ``DrawStroke``, the
    text will still be rendered as filled instead of drawing an outline. This
-   is because of the ``agg.FontCacheType.RasterFontCache`` argument given when
-   instantiating ``font``. To honor the ``drawing_mode``,
-   ``agg.FontCacheType.VectorFontCache`` can be passed instead. Note that there
-   is a performance penalty paid for this flexibility.
+   is because text drawing mode is separate from the main [shape] drawing mode.
+   ``text_drawing_mode`` in ``state`` defaults to
+   ``agg.TextDrawingMode.TextDrawRaster``. To draw text as vector data,
+   ``state.text_drawing_mode`` can be set to something like
+   ``agg.TextDrawingMode.TextDrawStroke``. All modes except ``TextDrawRaster``
+   are drawn as vectors. Note that there is a performance penalty paid for this
+   flexibility, but it allows things like patterned fills.
 
 Save
 ----
@@ -100,6 +103,6 @@ Last but not least, the image is written to a PNG file for enjoyment in your
 favorite image viewer.
 
 .. literalinclude:: simple_ex.py
-  :lines: 24
+  :lines: 22-23
   :linenos:
   :lineno-match:
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 72fb52e..d0621ad 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -3,7 +3,7 @@ Welcome to celiagg's documentation!
 
 celiagg is a Cython wrapper of the popular
 `Anti-Grain Geometry library  <http://www.antigrain.com>`_ which
-supports both Python 3 (3.5+).
+supports Python 3 (3.5+).
 
 Keeping with the tradition of AGG, celiagg aims to have a minimal number of
 dependencies. Currently only `numpy <http://www.numpy.org/>`_ is a required
diff --git a/docs/source/philosophy.rst b/docs/source/philosophy.rst
index aff2869..b0984e0 100644
--- a/docs/source/philosophy.rst
+++ b/docs/source/philosophy.rst
@@ -20,6 +20,8 @@ These are the main objects used for drawing in celiagg.
 
 * ``Canvas``: The object which has the main drawing methods. It also manages
   the frame buffer which is being drawn into.
+* ``GraphicsState``: The main state container to be passed to all ``Canvas``
+  drawing methods.
 * ``VertexSource``: A family of classes which contain vector data to be
   rasterized. Includes ``Path``, ``BSpline``, and ``ShapeAtPoints``
 * ``Paint``: Another family of classes which describe the color and texture of
diff --git a/docs/source/simple_ex.py b/docs/source/simple_ex.py
index d35afb8..13b9e98 100644
--- a/docs/source/simple_ex.py
+++ b/docs/source/simple_ex.py
@@ -1,24 +1,23 @@
 import celiagg as agg
 import numpy as np
-from skimage.io import imsave
+from PIL import Image
 
 state = agg.GraphicsState(drawing_mode=agg.DrawingMode.DrawStroke,
                           line_width=10.0)
 transform = agg.Transform()
 red_paint = agg.SolidPaint(1.0, 0.0, 0.0)
-black_paint = agg.SolidPaint(0.0, 0.0, 0.0)
+orange_paint = agg.SolidPaint(0.99, 0.66, 0.0)
 
-canvas = agg.CanvasRGB24(np.ones((400, 400, 3), dtype=np.uint8))
+canvas = agg.CanvasRGB24(np.zeros((400, 400, 3), dtype=np.uint8))
 
 path = agg.Path()
 path.ellipse(200, 200, 190, 190)
 canvas.clear(1.0, 1.0, 1.0)
 canvas.draw_shape(path, transform, state, stroke=red_paint)
 
-font = agg.Font("/Library/Fonts/Verdana.ttf", 96.0,
-                agg.FontCacheType.RasterFontCache)
+font = agg.Font(agg.example_font(), 96.0)
 transform.translate(30.0, 220.0)
-canvas.draw_text("celiagg", font, transform, state,
-                 stroke=black_paint)
+canvas.draw_text("celiagg", font, transform, state, fill=orange_paint)
 
-imsave("example.png", canvas.array)
+image = Image.fromarray(canvas.array, "RGB")
+image.save("example.png")
diff --git a/setup.py b/setup.py
index 8ff2526..b19235a 100755
--- a/setup.py
+++ b/setup.py
@@ -82,6 +82,6 @@ def configuration(parent_package='', top_path=None):
     requires=['numpy', 'freetype'],
     packages=['celiagg', 'celiagg.tests'],
     package_data={
-        'celiagg.tests': ['data/*'],
+        'celiagg': ['data/*'],
     },
 )


Test patch:
diff --git a/celiagg/tests/test_canvas.py b/celiagg/tests/test_canvas.py
index 6a23b30..b5bd326 100644
--- a/celiagg/tests/test_canvas.py
+++ b/celiagg/tests/test_canvas.py
@@ -50,7 +50,7 @@ def test_bad_method_args(self):
 
         if agg.HAS_TEXT:
             text = 'Hello!'
-            font = agg.Font('Times New Roman', 12.0)
+            font = agg.Font(agg.example_font(), 12.0)
             with self.assertRaises(TypeError):
                 canvas.draw_text(text, None, transform, gs)
             with self.assertRaises(TypeError):
diff --git a/celiagg/tests/test_text.py b/celiagg/tests/test_text.py
index 3f1b337..dd50cca 100644
--- a/celiagg/tests/test_text.py
+++ b/celiagg/tests/test_text.py
@@ -3,14 +3,9 @@
 import unittest
 
 import numpy as np
-import pkg_resources
 
 import celiagg as agg
 
-FONT_FILE = pkg_resources.resource_filename(
-    'celiagg.tests', 'data/Montserrat-Regular.ttf'
-)
-
 
 @unittest.skipIf(not agg.HAS_TEXT, 'Text support is not available')
 class TestTextDrawing(unittest.TestCase):
@@ -20,9 +15,9 @@ def test_font_byte_string(self):
         transform = agg.Transform()
 
         text_unicode = 'Hello!'
-        font_unicode = agg.Font(FONT_FILE, 12.0, face_index=1)
+        font_unicode = agg.Font(agg.example_font(), 12.0, face_index=1)
         text_byte = b'Hello!'
-        font_byte = agg.Font(FONT_FILE.encode('utf8'), 12.0)
+        font_byte = agg.Font(agg.example_font().encode('utf8'), 12.0)
 
         canvas.draw_text(text_unicode, font_unicode, transform, gs)
         canvas.draw_text(text_byte, font_unicode, transform, gs)
@@ -36,7 +31,7 @@ def test_text_rendering(self):
         )
         canvas.clear(1.0, 1.0, 1.0)
 
-        font = agg.Font(FONT_FILE, 24.0)
+        font = agg.Font(agg.example_font(), 24.0)
         gs = agg.GraphicsState()
         paint = agg.SolidPaint(1.0, 0.0, 0.0, 1.0)
         transform = agg.Transform()
@@ -56,7 +51,7 @@ def test_text_measurement(self):
         canvas = agg.CanvasRGB24(
             np.zeros((100, 100, 3), dtype=np.uint8), font_cache=font_cache,
         )
-        font = agg.Font(FONT_FILE, 12.0)
+        font = agg.Font(agg.example_font(), 12.0)
         gs = agg.GraphicsState()
         paint = agg.SolidPaint(1.0, 0.0, 0.0, 1.0)
         text = 'Some appropriate string'