Skip to content

Commit

Permalink
Merge f905d8f into 64ab5a5
Browse files Browse the repository at this point in the history
  • Loading branch information
emcconville committed Sep 14, 2014
2 parents 64ab5a5 + f905d8f commit 64049f1
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 4 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -6,6 +6,7 @@ python:
- 3.3
- 3.4
- pypy
- pypy3
env:
- secure: "EhG2uSD2m1eGxuL2HeQewJJx7MVL4WpjrxyfiUBEgsApemD1yKJPjUnLwAyd\nbPi5HJx5ySm1TTRSvj6/yP85YLYTaJHA8OabKk7p0wFW294qcrYIDGovU7NL\n3YOqZmqN+S3XOBGFCOnByxE+pcxxWW/3/I09EgeW7D6tBPh67G0="
install:
Expand Down
80 changes: 79 additions & 1 deletion docs/guide/draw.rst
Expand Up @@ -20,6 +20,36 @@ It's also callable and takes an :class:`~wand.image.Image` object::
draw(image)


.. _draw-bezier:

Bezier
------

.. versionadded:: 0.4.0

You can draw bezier curves using :meth:`~wand.drawing.Drawing.bezier()` method.
This method requires at lest four points to determine a bezier curve. Given
as a list of (x, y) coordinates. The first & last pair of coordinates are
treated as start & end, and the second & third pair of coordinates act as
controls.

For example::

with Drawing() as draw:
points = [(40,10), # Start point
(20,50), # First control
(90,10), # Second control
(70,40)] # End point
draw.bezier(points)
with Image(width=100, height=100, background=Color("#fff")) as image:
draw(image)

Control width & color of curve with the drawing properties:

- :attr:`~wand.drawing.Drawing.stroke_color`
- :attr:`~wand.drawing.Drawing.stroke_width`


.. _draw-lines:

Lines
Expand Down Expand Up @@ -49,6 +79,54 @@ a red diagonal line into the ``image``::
draw(image)


.. _draw-polygon:

Polygon
-------

.. versionadded:: 0.4.0

Complex shapes can be created with the :meth:`~wand.drawing.Drawing.polygon()`
method. You can draw a polygon by given this method a list of points. Stroke
line will automatically close between first & last point.

For example, the following code will draw a triangle into the ``image``::

points = [(25, 25), (75, 50), (25, 75)]
draw.polygon(points)
draw(image)

Control the fill & stroke with the following properties:

- :attr:`~wand.drawing.Drawing.stroke_color`
- :attr:`~wand.drawing.Drawing.stroke_width`
- :attr:`~wand.drawing.Drawing.fill_color`


.. _draw-polyline:

Polyline
-------

.. versionadded:: 0.4.0

Identical to :meth:`~wand.drawing.Drawing.polygon()`, except
:meth:`~wand.drawing.Drawing.polyline()` will not close the stroke line
between the first & last point.

For example, the following code will draw a two line path on the ``image``::

points = [(25, 25), (75, 50), (25, 75)]
draw.polyline(points)
draw(image)

Control the fill & stroke with the following properties:

- :attr:`~wand.drawing.Drawing.stroke_color`
- :attr:`~wand.drawing.Drawing.stroke_width`
- :attr:`~wand.drawing.Drawing.fill_color`


.. _draw-rectangles:

Rectangles
Expand Down Expand Up @@ -83,7 +161,7 @@ Texts

:class:`~wand.drawing.Drawing` object can write texts as well using its
:meth:`~wand.drawing.Drawing.text()` method. It takes ``x`` and ``y``
cordinates to be drawn and a string to write::
coordinates to be drawn and a string to write::

draw.font = 'wandtests/assets/League_Gothic.otf'
draw.font_size = 40
Expand Down
47 changes: 47 additions & 0 deletions tests/drawing_test.py
Expand Up @@ -120,6 +120,53 @@ def test_draw_line(fx_wand):
assert img[7,5] == Color('#333333')
assert img[8,5] == Color('#ccc')

def test_draw_polygon(fx_wand):
with nested(Color('#fff'),
Color('#f00'),
Color('#00f')) as (white, red, blue):
with Image(width=50, height=50, background=white) as img:
with Drawing() as draw:
draw.fill_color = blue
draw.stroke_color = red
draw.polygon([(10,10),
(40,25),
(10,40)])
draw.draw(img)
assert img[10,25] == red
assert img[25,25] == blue
assert img[35,15] == img[35,35] == white

def test_draw_polyline(fx_wand):
with nested(Color('#fff'),
Color('#f00'),
Color('#00f')) as (white, red, blue):
with Image(width=50, height=50, background=white) as img:
with Drawing() as draw:
draw.fill_color = blue
draw.stroke_color = red
draw.polyline([(10,10),
(40,25),
(10,40)])
draw.draw(img)
assert img[10,25] == img[25,25] == blue
assert img[35,15] == img[35,35] == white

def test_draw_bezier(fx_wand):
with nested(Color('#fff'),
Color('#f00'),
Color('#00f')) as (white, red, blue):
with Image(width=50, height=50, background=white) as img:
with Drawing() as draw:
draw.fill_color = blue
draw.stroke_color = red
draw.bezier([(10,10),
(10,40),
(40,10),
(40,40)])
draw.draw(img)
assert img[10,10] == img[25,25] == img[40,40] == red
assert img[34,32] == img[15,18] == blue
assert img[34,38] == img[15,12] == white

@mark.parametrize('kwargs', itertools.product(
[('right', 40), ('width', 30)],
Expand Down
21 changes: 19 additions & 2 deletions wand/api.py
Expand Up @@ -15,8 +15,8 @@
import sys
import traceback

__all__ = ('MagickPixelPacket', 'c_magick_char_p', 'library', 'libc',
'libmagick', 'load_library')
__all__ = ('MagickPixelPacket', 'PointInfo', 'c_magick_char_p', 'library',
'libc', 'libmagick', 'load_library')


class c_magick_char_p(ctypes.c_char_p):
Expand Down Expand Up @@ -129,6 +129,11 @@ class MagickPixelPacket(ctypes.Structure):
('opacity', ctypes.c_double),
('index', ctypes.c_double)]

class PointInfo(ctypes.Structure):

_fields_ = [('x', ctypes.c_double),
('y', ctypes.c_double)]


# Preserve the module itself even if it fails to import
sys.modules['wand._api'] = sys.modules['wand.api']
Expand Down Expand Up @@ -734,6 +739,18 @@ class MagickPixelPacket(ctypes.Structure):
ctypes.c_double,
ctypes.c_double]

library.DrawBezier.argtypes = [ctypes.c_void_p,
ctypes.c_ulong,
ctypes.POINTER(PointInfo)]

library.DrawPolygon.argtypes = [ctypes.c_void_p,
ctypes.c_ulong,
ctypes.POINTER(PointInfo)]

library.DrawPolyline.argtypes = [ctypes.c_void_p,
ctypes.c_ulong,
ctypes.POINTER(PointInfo)]

library.DrawAnnotation.argtypes = [ctypes.c_void_p,
ctypes.c_double,
ctypes.c_double,
Expand Down
97 changes: 96 additions & 1 deletion wand/drawing.py
Expand Up @@ -10,7 +10,7 @@
import ctypes
import numbers

from .api import library, MagickPixelPacket
from .api import library, MagickPixelPacket, PointInfo
from .color import Color
from .compat import binary, string_type, text, text_type, xrange
from .image import Image
Expand Down Expand Up @@ -493,6 +493,76 @@ def rectangle(self, left=None, top=None, right=None, bottom=None,
library.DrawRectangle(self.resource, left, top, right, bottom)
self.raise_exception()

def polygon(self, points=None):
"""Draws a polygon using the current :attr:`stoke_color`,
:attr:`stroke_width`, and :attr:`fill_color`, using the specified
array of coordinates.
Example polygon on ``image`` ::
with Drawing() as draw:
points = [(40,10), (20,50), (90,10), (70,40)]
draw.polygon(points)
draw.draw(image)
.. versionadded:: 0.4.0
:param points: list of x,y tuples
:type points: :class:`list`
"""

(points_l, points_p) = _list_to_point_info(points)
library.DrawPolygon(self.resource, points_l,
ctypes.cast(points_p,ctypes.POINTER(PointInfo)))

def polyline(self, points=None):
"""Draws a polyline using the current :attr:`stoke_color`,
:attr:`stroke_width`, and :attr:`fill_color`, using the specified
array of coordinates.
Identical to :class:`~wand.drawing.Drawing.polygon`, but without closed
stroke line.
:param points: list of x,y tuples
:type points: :class:`list`
.. versionadded:: 0.4.0
"""

(points_l, points_p) = _list_to_point_info(points)
library.DrawPolyline(self.resource, points_l,
ctypes.cast(points_p,ctypes.POINTER(PointInfo)))

def bezier(self, points=None):
"""Draws a bezier curve through a set of points on the image, using
the specified array of coordinates.
At least four points should be given to complete a bezier path.
The first & forth point being the start & end point, and the second
& third point controlling the direction & curve.
Example bezier on ``image`` ::
with Drawing() as draw:
points = [(40,10), # Start point
(20,50), # First control
(90,10), # Second control
(70,40)] # End point
draw.stroke_color = Color('#000')
draw.fill_color = Color('#fff')
draw.bezier(points)
draw.draw(image)
:param points: list of x,y tuples
:type points: :class:`list`
.. versionadded:: 0.4.0
"""

(points_l, points_p) = _list_to_point_info(points)
library.DrawBezier(self.resource, points_l,
ctypes.cast(points_p,ctypes.POINTER(PointInfo)))

def text(self, x, y, body):
"""Writes a text ``body`` into (``x``, ``y``).
Expand Down Expand Up @@ -558,3 +628,28 @@ def get_font_metrics(self, image, text, multiline=False):

def __call__(self, image):
return self.draw(image)

def _list_to_point_info(points):
"""
Helper method to convert a list of tuples to ``const * PointInfo``
:param points: a list of tuples
:type points: `list`
:returns: tuple of point length and c_double array
:rtype: `tuple`
:raises: `TypeError`
.. versionadded:: 0.4.0
"""
if not isinstance(points, list):
raise TypeError('points must be a list, not ' + repr(points))
point_length = len(points)
tuple_size = 2
point_info_size = point_length * tuple_size
# Allocate sequence of memory
point_info = (ctypes.c_double * point_info_size)()
for double_index in xrange(0, point_info_size):
tuple_index = double_index // tuple_size
tuple_offset = double_index % tuple_size
point_info[double_index] = ctypes.c_double(points[tuple_index][tuple_offset])
return (point_length, point_info)

0 comments on commit 64049f1

Please sign in to comment.