├── .gitignore
├── .travis.yml
├── LICENCE.txt
├── MANIFEST.in
├── README.rst
├── examples
├── README.rst
├── _run_all_examples.py
├── logo.py
├── random_squares.py
├── roses.py
├── star.py
├── transparent_colors.py
└── yin_yang.py
├── gizeh
├── __init__.py
├── geometry.py
├── gizeh.py
├── tools.py
└── version.py
├── logo.jpeg
├── setup.py
└── tests
├── samples
├── random_squares.png
├── roses.png
├── star.png
├── transparent_colors.png
└── yin_yang.png
├── test_pdfsurface.py
└── test_samples.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 | __pycache__
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | .tox
28 | nosetests.xml
29 |
30 | # Translations
31 | *.mo
32 |
33 | # Mr Developer
34 | .mr.developer.cfg
35 | .project
36 | .pydevproject
37 |
38 | # Temp files
39 |
40 | *~
41 |
42 | # Pipy codes
43 |
44 | .pypirc
45 | .cache
46 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "3.6"
4 | # command to install dependencies
5 | before_install:
6 | - sudo apt-get install libcairo2-dev python-dev libffi-dev
7 | - pip install pytest coveralls pytest-cov pillow
8 | - pip install -e .
9 | # command to run tests
10 | script:
11 | - python -m pytest -v --cov gizeh --cov-report term-missing
12 |
13 | after_success:
14 | - coveralls
15 |
--------------------------------------------------------------------------------
/LICENCE.txt:
--------------------------------------------------------------------------------
1 |
2 | The MIT License (MIT)
3 | [OSI Approved License]
4 |
5 | The MIT License (MIT)
6 |
7 | Copyright (c) 2014 Zulko
8 |
9 | Permission is hereby granted, free of charge, to any person obtaining a copy
10 | of this software and associated documentation files (the "Software"), to deal
11 | in the Software without restriction, including without limitation the rights
12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | copies of the Software, and to permit persons to whom the Software is
14 | furnished to do so, subject to the following conditions:
15 |
16 | The above copyright notice and this permission notice shall be included in
17 | all copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | THE SOFTWARE.
26 |
27 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.txt
2 | recursive-include examples *.txt *.py
3 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. image:: https://raw.githubusercontent.com/Zulko/gizeh/master/logo.jpeg
2 | :alt: [logo]
3 | :align: center
4 |
5 | Gizeh - Cairo for tourists
6 | ===========================
7 |
8 | Gizeh is a Python library for vector graphics:
9 |
10 | .. code:: python
11 |
12 | # Let's draw a red circle !
13 | import gizeh
14 | surface = gizeh.Surface(width=320, height=260) # in pixels
15 | circle = gizeh.circle(r=30, xy=[40,40], fill=(1,0,0))
16 | circle.draw(surface) # draw the circle on the surface
17 | surface.write_to_png("circle.png") # export the surface as a PNG
18 |
19 | You can see examples of Gizeh in action (combined with MoviePy to make animations) in `this blog post `_.
20 |
21 | Gizeh is written on top of the module ``cairocffi``, which is a Python binding of the popular C library Cairo_. Cairo is powerful, but difficult to learn and use. Gizeh implements a few classes on top of Cairo that make it more intuitive.
22 |
23 | Gizeh should work on any platform and with python 2 and 3.
24 |
25 | Installation
26 | --------------
27 |
28 | To use Gizeh you must first install Cairo_ on your computer (see their website).
29 |
30 | Gizeh depends on the Python packages ``cairocffi`` and ``Numpy``. They will both be automatically installed (if they aren't already) during the installation of Gizeh. If you have trouble with the installation, head to the last section of this README for troubleshooting. If it doesn't help, you can ask for help on Github.
31 |
32 | **Installation from the sources:** Gizeh can be installed by unzipping the source code in some directory and using this command in the same directory:
33 | ::
34 |
35 | (sudo) python setup.py install
36 |
37 | **Installation with pip:** Alternatively, you can install Gizeh directly from the Python Package Index with this command:
38 | ::
39 |
40 | (sudo) pip install gizeh
41 |
42 | This method may fail if ``ez_setup`` is not installed on your computer. In this case install ``ez_setup`` first, with ::
43 |
44 | (sudo) pip install ez_setup
45 |
46 | Contribute !
47 | -------------
48 |
49 | Gizeh is an open-source software written by Zulko_ and released under the MIT licence. The project is hosted on Github_.
50 | Everyone is welcome to contribute !
51 |
52 |
53 | User Guide
54 | -------------
55 |
56 | This guide, along with the examples in the ``gizeh/examples`` folder, should give you everything you need to get started. To go further, read the function docstrings.
57 |
58 | Surfaces
59 | ~~~~~~~~
60 |
61 | A Surface is a rectangle of fixed dimensions (in pixels), on which you will draw elements, and that you can save or export as an image:
62 |
63 | .. code:: python
64 |
65 | import gizeh
66 |
67 | # initialize surface
68 | surface = gizeh.Surface(width=320, height=260) # in pixels
69 |
70 | # Now make a shape and draw it on the surface
71 | circle = gizeh.circle(r=30, xy= [40,40], fill=(1,1,1))
72 | circle.draw(surface)
73 |
74 | # Now export the surface
75 | surface.get_npimage() # returns a (width x height x 3) numpy array
76 | surface.write_to_png("circle.png")
77 |
78 |
79 |
80 | Elements
81 | ~~~~~~~~~
82 |
83 | Basic elements are circles, rectangles, lines, texts, etc., that you can draw on a surface using ``my_element.draw(surface)``. You can specify the properties and coordinates of these elements at creation time:
84 |
85 | - ``xy`` : coordinates of the center of the object. At rendering time (in function ``surface.write_to_png``) you can set the parameter ``y_origin`` to ``top`` (default) or ``bottom``. If you leave it to ``top``, (0,0) corresponds to the upper left corner of the final picture, and the bottom right corner has coordinates (width, height). If you choose ``y_origin=bottom``, (0,0) will be at the bottom left of the picture (like in a standard plot) and (width, height) will be at the upper right corner.
86 | - ``angle`` : angle (in radians) of the rotation of the element around its center ``xy``.
87 | - ``fill`` : what will fill the element (default is no fill). Can be a color (R,G,B), a color gradient, an image, etc. See section below.
88 | - ``stroke`` : What will fill the element's contour. Same rules as for ``fill``.
89 | - ``stroke_width`` : the width (in pixels) of the element's contour. Default is 0 (no stroke).
90 |
91 | Examples of elements:
92 |
93 | .. code:: python
94 |
95 | Pi = 3.14
96 | circ = gizeh.circle(r=30, xy=(50,50), fill=(1,1,1))
97 | rect = gizeh.rectangle(lx=60.3, ly=45, xy=(60,70), fill=(0,1,0), angle=Pi/8)
98 | sqr = gizeh.square(l=20, stroke=(1,1,1), stroke_width= 1.5)
99 | arc = gizeh.arc(r=20, a1=Pi/4, a2=3*Pi/4, fill=(1,1,1))
100 | text = gizeh.text("Hello world", fontfamily="Impact", fontsize=40,
101 | fill=(1,1,1), xy=(100,100), angle=Pi/12)
102 | polygon = gizeh.regular_polygon(r=40, n=5, angle=np.pi/4, xy=[40,50], fill=(1,0,1))
103 | line = gizeh.polyline(points=[(0,0), (20,30), (40,40), (0,10)], stroke_width=3,
104 | stroke=(1,0,0), fill=(0,1,0))
105 |
106 | Fill and stroke
107 | ----------------
108 |
109 | When you make a shape, the ``fill`` and ``stroke`` parameters can be one of the following:
110 |
111 | - A RGB color of the form (r,g,b) where each element is comprised between 0 and 1 (1 is 100%).
112 | - A RGBA color of the form (r,g,b,a), where ``a`` is comprised between 0 (totally transparent) and 1 (totally opaque).
113 | - A gizeh.ColorGradient (see the docstring).
114 | - A gizeh.ImagePattern, i.e. an image (see the docstring).
115 | - A numpy array representing a RGB or RGBA image (not implemented yet).
116 | - A PNG image file (not implemented yet).
117 |
118 |
119 | Transformations
120 | ~~~~~~~~~~~~~~~~
121 |
122 | Any element can be transformed (translated, rotated or scaled). All transformations are *outplace*: they do not modify the original element, they create a modified version of it.
123 |
124 | Examples:
125 |
126 | .. code:: python
127 |
128 | square_1 = gizeh.square(l=20, xy = [30,35], fill=(1,0,0))
129 | square_2 = square_1.rotate(Pi/8) # rotation around [0,0] by default
130 | square_3 = square_2.rotate(Pi/4, center=[10,15]) # rotation around a center
131 | square_4 = square_1.scale(2) # two times bigger
132 | square_5 = square1.scale(sx=2, sy=3) # width times 2, height times 3
133 | square_6 = square_1.scale(2, center=[30,30]) # zoom: scales around a center
134 | square_7 = square_1.translate(xy=[5,15]) # translation
135 |
136 |
137 | Groups
138 | ~~~~~~~
139 |
140 | A Group is a collection of elements which will be transformed and drawn together. The elements can be a basic element (square, circle...) or even groups.
141 |
142 | Examples:
143 |
144 | .. code:: python
145 |
146 | square = gizeh.square(l=20, fill=(1,0,0), xy=(40,40))
147 | circle = gizeh.circle(r=20, fill=(1,2,0), xy=(50,30))
148 | group_1 = gizeh.Group([square, circle])
149 | group_2 = group.translate(xy=[30,30]).rotate(Pi/4)
150 | group_3 = gizeh.Group([circle, group_1])
151 |
152 | surface = gizeh.Surface(width=300,height=200)
153 | group.draw(surface)
154 | group_1.draw(surface)
155 | group_2.draw(surface)
156 | group_3.draw(surface)
157 | surface.write_to_png("my_masterwork.png")
158 |
159 |
160 | That's all folks !
161 | ~~~~~~~~~~~~~~~~~~~
162 |
163 | That's about all there is to know.
164 | To go further, see the examples in the ``examples`` folder or the documentation
165 | directly in the code.
166 |
167 |
168 | Installation support
169 | ---------------------
170 |
171 | Sometimes the installation through `pip` fails because
172 |
173 | Some people have had problems to install ``cairocffi``, Here is how they solved
174 | their problem:
175 |
176 | On Debian/Ubuntu ::
177 |
178 | sudo apt-get install python-dev python-pip ffmpeg libffi-dev
179 | sudo pip install gizeh
180 |
181 | On macOSX ::
182 |
183 | pip install ez_setup
184 |
185 |
186 | brew install pkg-config libffi
187 | export PKG_CONFIG_PATH=/usr/local/Cellar/libffi/3.0.13/lib/pkgconfig/
188 |
189 | # go to https://xquartz.macosforge.org and download and install XQuartz,
190 | # which is needed for cairo, then...
191 | brew install cairo
192 |
193 | pip install gizeh
194 |
195 | .. _Zulko : https://github.com/Zulko
196 | .. _Github: https://github.com/Zulko/gizeh
197 | .. _Cairo: http://cairographics.org/
198 |
--------------------------------------------------------------------------------
/examples/README.rst:
--------------------------------------------------------------------------------
1 | Gizeh examples
2 | ----------------
3 |
4 | The resulting PNGs are not included here to keep the library lightweight.
5 | To have a look at the expected results see here:
6 |
7 | http://imgur.com/a/mjNLG
--------------------------------------------------------------------------------
/examples/_run_all_examples.py:
--------------------------------------------------------------------------------
1 | """
2 | Run this Python script to generate all the PNGs from the examples in this folder.
3 | """
4 |
5 |
6 | import os
7 |
8 |
9 | def execfile(filename):
10 | exec(compile(open(filename, "rb").read(), filename, 'exec'))
11 |
12 | examples = [f for f in os.listdir('.') if f.endswith(".py") and
13 | not f.startswith('_')]
14 |
15 | for f in examples:
16 | print ("running: "+f)
17 | exec("import "+f[:-3])
--------------------------------------------------------------------------------
/examples/logo.py:
--------------------------------------------------------------------------------
1 | """
2 | This example generates the logo of Gizeh, a fractal made of triangles.
3 | """
4 |
5 | import gizeh as gz
6 | import numpy as np
7 |
8 | # PARAMETERS
9 |
10 | L = 600 # Picture dimension
11 | r = 100 # Size of the triangles
12 | angles = np.arange(3)*2*np.pi/3 # directions 3h00, 7h00, 11h00
13 |
14 | # INITIALIZE THE SURFACE
15 |
16 | surface = gz.Surface(L,L, bg_color=(1,1,1))
17 |
18 | # DRAW THE TEXT
19 |
20 | txt = gz.text("Gizeh", fontfamily="Dancing Script",
21 | fontsize=120, fill=(0,0,0),xy=(L/2,L/9))
22 | txt.draw(surface)
23 |
24 | # MAKE THE FIRST TRIANGLE
25 |
26 | gradient=gz.ColorGradient('radial', [(0,(.3,.2,.8)),(1,(.4,.6,.8))],
27 | xy1=(0,0), xy2=(0,r/3), xy3=(0,r))
28 |
29 | triangle = gz.regular_polygon(r, n=3,fill=gradient, stroke=(0,0,0),
30 | stroke_width=3, xy = (r,0))
31 |
32 | # BUILD THE FRACTAL RECURSIVELY
33 |
34 | fractal = gz.Group([triangle.rotate(a) for a in angles])
35 | for i in range(6):
36 | fractal = gz.Group([fractal.scale(.5).rotate(-np.pi)
37 | .translate(gz.polar2cart(3*r/2,np.pi+a))
38 | for a in angles]+[fractal])
39 |
40 | # PLACE AND DRAW THE FRACTAL (this will take time)
41 |
42 | fractal.rotate(-np.pi/2).translate((L/2,1.1*L/2)).draw(surface)
43 |
44 | # SAVE
45 | surface.write_to_png("logo.png")
--------------------------------------------------------------------------------
/examples/random_squares.py:
--------------------------------------------------------------------------------
1 | """
2 | This generates a picture with 100 squares of random size, angle,
3 | color and position.
4 | """
5 |
6 | import gizeh as gz
7 | import numpy as np
8 |
9 | L = 200 # <- dimensions of the final picture
10 | surface = gz.Surface(L, L, bg_color=(1,1,1))
11 |
12 | # We generate 1000 random values of size, angle, color, position.
13 | # 'rand' is a function that generates numbers between 0 and 1
14 | n_squares = 1000 # number of squares
15 | angles = 2*np.pi* np.random.rand(n_squares) # n_squares angles between 0 and 2pi
16 | sizes = 20 + 20 * np.random.rand(n_squares) # all sizes between 20 and 40
17 | positions = L * np.random.rand(n_squares, 2) # [ [x1, y1] [x2 y2] [x3 y3]]...
18 | colors = np.random.rand(n_squares, 3)
19 |
20 |
21 | for angle, size, position, color in zip(angles, sizes, positions, colors):
22 | square = gz.square(size, xy=position, angle=angle, fill=color,
23 | stroke_width=size/20) # stroke is black by default.
24 | square.draw(surface)
25 |
26 | surface.write_to_png("random_squares.png")
27 |
--------------------------------------------------------------------------------
/examples/roses.py:
--------------------------------------------------------------------------------
1 | """
2 | We will draw several varieties of beautiful mathematical roses, which are
3 | defined by the polar equation r = cos (n*a/d) where r=radius, a=angle, and
4 | d,n are the parameters of the curve. We will trace roses for a few d and n.
5 | http://en.wikipedia.org/wiki/Rose_mathematics
6 | """
7 |
8 | import gizeh as gz
9 | import numpy as np
10 | from fractions import gcd
11 |
12 | def rose(d, n):
13 | """ Returns a polyline representing a rose of radius 1 """
14 | n_cycles = 1.0 * d / gcd(n,d) # <- number of cycles to close the rose
15 | aa = np.linspace(0,2*np.pi*n_cycles,1000)
16 | rr = np.cos( n*aa/d)
17 | points = gz.polar2cart(rr, aa)
18 | return gz.polyline(points, stroke=[0,0,0], stroke_width=.05)
19 |
20 | max_d = 8
21 | max_n = 7
22 | rose_radius = 30
23 |
24 | def position(d, n):
25 | """ Defines the (x,y) position of the rose(d,n)."""
26 | return [(1.1*(2*i-.6) * rose_radius) for i in [d, n]]
27 |
28 | W, H = [int(c+2*rose_radius) for c in position(max_d,max_n)]
29 |
30 | surface = gz.Surface(W, H, bg_color=(1, 1, 1))
31 |
32 | for d in range(1, max_d+1):
33 | for n in range(1, max_n+1):
34 | rose_nd = rose(n, d).scale(rose_radius).translate( position(d,n) )
35 | rose_nd.draw(surface)
36 |
37 | surface.write_to_png("roses.png")
--------------------------------------------------------------------------------
/examples/star.py:
--------------------------------------------------------------------------------
1 | import gizeh as gz
2 | import numpy as np
3 |
4 | surface = gz.Surface(200,200, bg_color=(1, 0.9, 0.6))
5 |
6 | star1 = gz.star(radius=70, ratio=.4, fill=(1,1,1), angle=-np.pi/2,
7 | stroke_width=2, stroke=(1,0,0))
8 | star2 = gz.star(radius =55, ratio=.4, fill=(1,0,0), angle=-np.pi/2)
9 | stars = gz.Group([ star1, star2 ]).translate([100,100])
10 | stars.draw(surface)
11 |
12 | surface.write_to_png("star.png")
--------------------------------------------------------------------------------
/examples/transparent_colors.py:
--------------------------------------------------------------------------------
1 | """
2 | This generates a picture of 3 semi-transparent circles of different colors
3 | which overlap to some extent.
4 | """
5 |
6 | import gizeh as gz
7 | from numpy import pi # <- 3.14... :)
8 |
9 | L = 200 # <- dimensions of the final picture
10 |
11 | surface = gz.Surface(L,L, bg_color=(1,1,1)) # <- white background
12 |
13 | radius = 50
14 | centers = [ gz.polar2cart(40, angle) for angle in [0, 2*pi/3, 4*pi/3]]
15 | colors = [ (1,0,0,.4), # <- Semi-tranparent red (R,G,B, transparency)
16 | (0,1,0,.4), # <- Semi-tranparent green
17 | (0,0,1,.4)] # <- Semi-tranparent blue
18 |
19 | circles = gz.Group( [ gz.circle(radius, xy=center, fill=color,
20 | stroke_width=3, stroke=(0,0,0)) # black stroke
21 | for center, color in zip(centers, colors)] )
22 |
23 | circles.translate([L/2,L/2]).draw(surface)
24 |
25 | surface.write_to_png("transparent_colors.png")
26 |
--------------------------------------------------------------------------------
/examples/yin_yang.py:
--------------------------------------------------------------------------------
1 | """
2 | This generates a Yin Yang.
3 | """
4 |
5 | import gizeh as gz
6 | from math import pi
7 |
8 | L = 200 # <- dimensions of the final picture
9 | surface = gz.Surface(L, L, bg_color=(0 ,.3, .6)) # blue background
10 | r = 70 # radius of the whole yin yang
11 |
12 | yin_yang = gz.Group([
13 | gz.arc(r, pi/2, 3*pi/2, fill = (1,1,1)), # white half
14 | gz.arc(r, -pi/2, pi/2, fill = (0,0,0)), # black half
15 |
16 | gz.arc(r/2, -pi/2, pi/2, fill = (1,1,1), xy = [0,-r/2]), # white semihalf
17 | gz.arc(r/2, pi/2, 3*pi/2, fill = (0,0,0), xy = [0, r/2]), # black semihalf
18 |
19 | gz.circle(r/8, xy = [0, +r/2], fill = (1,1,1)), # white dot
20 | gz.circle(r/8, xy = [0, -r/2], fill = (0,0,0)) ]) # black dot
21 |
22 | yin_yang.translate([L/2,L/2]).draw(surface)
23 |
24 | surface.write_to_png("yin_yang.png")
--------------------------------------------------------------------------------
/gizeh/__init__.py:
--------------------------------------------------------------------------------
1 | """ gizeh/__init__.py """
2 |
3 | # __all__ = []
4 |
5 | from .gizeh import *
6 | from .geometry import *
7 |
8 | from .version import __version__
9 |
--------------------------------------------------------------------------------
/gizeh/geometry.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 |
4 | def rotation_matrix(a):
5 | """Return a 3x3 2D geometric rotation matrix"""
6 | return np.array([[np.cos(a), -np.sin(a), 0],
7 | [np.sin(a), np.cos(a), 0],
8 | [0, 0, 1.0]])
9 |
10 |
11 | def translation_matrix(xy):
12 | """Return a 3x3 2D geometric translation matrix"""
13 | return np.array([[1.0, 0, xy[0]],
14 | [0, 1, xy[1]],
15 | [0, 0, 1]])
16 |
17 |
18 | def scaling_matrix(sx, sy):
19 | """Return a 3x3 geometric scaling matrix"""
20 | return np.array([[sx, 0, 0],
21 | [0, sy, 0],
22 | [0, 0, 1]])
23 |
24 |
25 | def polar_polygon(nfaces, radius, npoints):
26 | """ Returns the (x,y) coordinates of n points regularly spaced
27 | along a regular polygon of `nfaces` faces and given radius.
28 | """
29 | theta = np.linspace(0, 2 * np.pi, npoints)[:-1]
30 | cos, pi, n = np.cos, np.pi, nfaces
31 | r = cos(pi / n) / cos((theta % (2 * pi / n)) - pi / n)
32 | d = np.cumsum(np.sqrt(((r[1:] - r[:-1])**2)))
33 | d = [0] + list(d / d.max())
34 | return zip(radius * r, theta, d)
35 |
36 |
37 | def polar2cart(r, theta):
38 | """ Transforms polar coodinates into cartesian coordinates (x,y).
39 | If r or theta or both are vectors, returns a np. array of the list
40 | [(x1,y1),(x2,y2),etc...]
41 | """
42 |
43 | res = r * np.array([np.cos(theta), np.sin(theta)])
44 | return res if len(res.shape) == 1 else res.T
45 |
--------------------------------------------------------------------------------
/gizeh/gizeh.py:
--------------------------------------------------------------------------------
1 | from copy import copy, deepcopy
2 | from base64 import b64encode
3 | import numpy as np
4 | import cairocffi as cairo
5 | from .geometry import (rotation_matrix,
6 | translation_matrix,
7 | scaling_matrix,
8 | polar2cart)
9 | from itertools import chain
10 | from math import sqrt
11 |
12 | try:
13 | from cStringIO import StringIO
14 | except ImportError:
15 | try:
16 | from StringIO import StringIO
17 | except ImportError:
18 | # Python 3 compatibility
19 | from io import BytesIO as StringIO
20 |
21 |
22 | class Surface:
23 | """
24 | A Surface is an object on which Elements are drawn, and which can be
25 | exported as PNG images, numpy arrays, or be displayed into an IPython
26 | Notebook.
27 |
28 | Note that this class is simply a thin wrapper around Cairo's Surface class.
29 | """
30 |
31 | def __init__(self, width, height, bg_color=None):
32 | """"Initialize."""
33 | self.width = width
34 | self.height = height
35 | self._cairo_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
36 | width, height)
37 | if bg_color:
38 | rectangle(2 * width, 2 * height, fill=bg_color).draw(self)
39 |
40 | @staticmethod
41 | def from_image(image):
42 | """Initialize the surface from an np array of an image."""
43 | h, w, d = image.shape
44 | if d == 4:
45 | image = image[:, :, [2, 1, 0, 3]]
46 | if d == 1:
47 | image = np.array(3 * [image])
48 | elif d == 3:
49 | image = image[:, :, [2, 1, 0]]
50 | image = np.dstack([image, 255 * np.ones((h, w))])
51 | sf = Surface(w, h)
52 | arr = np.frombuffer(sf._cairo_surface.get_data(), np.uint8)
53 | arr += image.flatten()
54 | sf._cairo_surface.mark_dirty()
55 | return sf
56 |
57 | def get_new_context(self):
58 | """Return a new context for drawing on the surface."""
59 | return cairo.Context(self._cairo_surface)
60 |
61 | def write_to_png(self, filename, y_origin="top"):
62 | """Write the image to a PNG.
63 |
64 | Parameter y_origin ("top" or "bottom") decides whether point (0,0)
65 | lies in the top-left or bottom-left corner of the screen.
66 | """
67 |
68 | if y_origin == "bottom":
69 | W, H = self.width, self.height
70 | new_surface = Surface(W, H)
71 | rect = (rectangle(2 * W, 2 * H, fill=ImagePattern(self))
72 | .scale(1, -1).translate([0, H]))
73 | rect.draw(new_surface)
74 | new_surface.write_to_png(filename, y_origin="top")
75 | else:
76 | self._cairo_surface.write_to_png(filename)
77 |
78 | def get_npimage(self, transparent=False, y_origin="top"):
79 | """ Returns a WxHx[3-4] numpy array representing the RGB picture.
80 |
81 | If `transparent` is True the image is WxHx4 and represents a RGBA
82 | picture, i.e. array[i,j] is the [r,g,b,a] value of the pixel at
83 | position [i,j]. If `transparent` is false, a RGB array is returned.
84 |
85 | Parameter y_origin ("top" or "bottom") decides whether point (0,0)
86 | lies in the top-left or bottom-left corner of the screen.
87 | """
88 |
89 | im = 0 + np.frombuffer(self._cairo_surface.get_data(), np.uint8)
90 | im.shape = (self.height, self.width, 4)
91 | im = im[:, :, [2, 1, 0, 3]] # put RGB back in order
92 | if y_origin == "bottom":
93 | im = im[::-1]
94 | return im if transparent else im[:, :, :3]
95 |
96 | def get_html_embed_code(self, y_origin="top"):
97 | """Return an html code containing all the PNG data of the surface. """
98 | png_data = self._repr_png_()
99 | data = b64encode(png_data).decode('utf-8')
100 | return """
""" % (data)
101 |
102 | def ipython_display(self, y_origin="top"):
103 | """Display the surface in the IPython notebook.
104 |
105 | Will only work if surface.ipython_display() is written at the end of
106 | one of the notebook's cells.
107 | """
108 |
109 | from IPython.display import HTML
110 | return HTML(self.get_html_embed_code(y_origin=y_origin))
111 |
112 | def _repr_html_(self):
113 | return self.get_html_embed_code()
114 |
115 | def _repr_png_(self):
116 | """Return the raw PNG data to be displayed in the IPython notebook."""
117 | data = StringIO()
118 | self.write_to_png(data)
119 | return data.getvalue()
120 |
121 |
122 | class PDFSurface(object):
123 | """Simple class to allow Gizeh to create PDF figures."""
124 |
125 | def __init__(self, name, width, height, bg_color=None):
126 | self.width = width
127 | self.height = height
128 | self._cairo_surface = cairo.PDFSurface(name, width, height)
129 |
130 | def get_new_context(self):
131 | """Return a new context for drawing on the surface."""
132 | return cairo.Context(self._cairo_surface)
133 |
134 | def flush(self):
135 | """Write the file"""
136 | self._cairo_surface.flush()
137 |
138 | def finish(self):
139 | """Close the surface"""
140 | self._cairo_surface.finish()
141 |
142 |
143 | class Element:
144 | """Base class for objects that can be transformed (rotated, translated,
145 | scaled) and drawn to a Surface.
146 |
147 | Parameter `draw_method` is a function which takes a cairo.Surface.Context()
148 | as argument and draws on this context. All Elements are draw on a different
149 | context.
150 | """
151 |
152 | def __init__(self, draw_method):
153 | """Initialize."""
154 | self.draw_method = draw_method
155 | self.matrix = 1.0 * np.eye(3)
156 |
157 | def _cairo_matrix(self):
158 | """Return the element's matrix in cairo form """
159 | m = self.matrix
160 | return cairo.Matrix(m[0, 0], m[1, 0],
161 | m[0, 1], m[1, 1],
162 | m[0, 2], m[1, 2])
163 |
164 | def _transform_ctx(self, ctx):
165 | """Tranform the context before drawing.
166 | It applies all the rotation, translation, etc. to the context.
167 | In short, it sets the context's matrix to the element's matrix.
168 | """
169 | ctx.set_matrix(self._cairo_matrix())
170 |
171 | def draw(self, surface):
172 | """Draw the Element on a new context of the given Surface """
173 | ctx = surface.get_new_context()
174 | self._transform_ctx(ctx)
175 | self.draw_method(ctx)
176 |
177 | def set_matrix(self, new_mat):
178 | """Return a copy of the element, with a new transformation matrix """
179 | new = deepcopy(self)
180 | new.matrix = new_mat
181 | return new
182 |
183 | def rotate(self, angle, center=[0, 0]):
184 | """Rotate the element.
185 |
186 | Returns a new element obtained by rotating the current element
187 | by the given `angle` (unit: rad) around the `center`.
188 | """
189 |
190 | center = np.array(center)
191 | mat = (translation_matrix(center)
192 | .dot(rotation_matrix(angle))
193 | .dot(translation_matrix(-center)))
194 |
195 | return self.set_matrix(mat.dot(self.matrix))
196 |
197 | def translate(self, xy):
198 | """Translate the element.
199 |
200 | Returns a new element obtained by translating the current element
201 | by a vector xy
202 | """
203 | return self.set_matrix(translation_matrix(xy).dot(self.matrix))
204 |
205 | def scale(self, rx, ry=None, center=[0, 0]):
206 | """Scale the element.
207 |
208 | Returns a new element obtained by scaling the current element
209 | by a factor rx horizontally and ry vertically, with fix point `center`.
210 | If ry is not provided it is assumed that rx=ry.
211 | """
212 |
213 | ry = rx if (ry is None) else ry
214 | center = np.array(center)
215 | mat = (translation_matrix(center)
216 | .dot(scaling_matrix(rx, ry))
217 | .dot(translation_matrix(-center)))
218 | return self.set_matrix(mat.dot(self.matrix))
219 |
220 |
221 | class Group(Element):
222 | """
223 | Class for special Elements made out of a group of other elements which
224 | will be translated, scaled, rotated, and drawn together.
225 | These elements can be base elements (circles, squares) or even groups.
226 | """
227 |
228 | def __init__(self, elements):
229 | """Initialize."""
230 |
231 | self.elements = elements
232 | self.matrix = 1.0 * np.eye(3)
233 |
234 | def draw(self, surface):
235 | """Draw the group to a new context of the given Surface """
236 |
237 | for e in self.elements:
238 | m = self.matrix
239 | mi = np.linalg.inv(m)
240 | new_matrix = m.dot(e.matrix)
241 | e.set_matrix(new_matrix).draw(surface)
242 |
243 |
244 | class ColorGradient:
245 | """ This class is more like a structure to store the data for color gradients
246 |
247 | These gradients are used as sources for filling elements or their borders (see
248 | parameters `fill` and `stroke` in `shape_elements`).
249 |
250 | Parameters
251 | ------------
252 | type
253 | Type of gradient: "linear" or "radial"
254 |
255 | xy1, xy2, xy3
256 |
257 | stops_colors
258 | For instance, if you want a blue color then a red color then a green color
259 | you will write stops_colors=[(0,(1,0,0)), (0.5,(0,1,0)) , (1,(0,0,1))].
260 |
261 | """
262 |
263 | def __init__(self, type, stops_colors, xy1, xy2, xy3=None):
264 | """Initialize/"""
265 | self.xy1 = xy1
266 | self.xy2 = xy2
267 | self.xy3 = xy3
268 | self.stops_colors = stops_colors
269 | if type not in ["radial", "linear"]:
270 | raise ValueError("unkown gradient type")
271 | self.type = type
272 |
273 | def set_source(self, ctx):
274 | """Create a pattern and set it as source for the given context."""
275 | if self.type == "linear":
276 | (x1, y1), (x2, y2) = self.xy1, self.xy2
277 | pat = cairo.LinearGradient(x1, y1, x2, y2)
278 | elif self.type == "radial":
279 | (x1, y1), (x2, y2), (x3, y3) = self.xy1, self.xy2, self.xy3
280 | pat = cairo.RadialGradient(x1, y1, x2, y2, x3, y3)
281 | for stop, color in self.stops_colors:
282 | if len(color) == 4:
283 | pat.add_color_stop_rgba(stop, *color)
284 | else:
285 | pat.add_color_stop_rgb(stop, *color)
286 | ctx.set_source(pat)
287 |
288 |
289 | class ImagePattern(Element):
290 | """ Class for images that will be used to fill an element or its contour.
291 |
292 | Parameters
293 | ------------
294 | image
295 | A numpy RGB(A) image.
296 | pixel_zero
297 | The coordinates of the pixel of the image that will serve as 0,0 origin
298 | when filling the element.
299 |
300 | filter
301 | Determines the method with which the images are resized:
302 | "best": slow but good quality
303 | "nearest": takes nearest pixel (can create artifacts)
304 | "good": Good and faster than "best"
305 | "bilinear": use linear interpolation
306 | "fast":fast filter, quality like 'nearest'
307 |
308 | extend
309 | Determines what happends outside the boundaries of the picture:
310 | "none", "repeat", "reflect", "pad" (pad= use pixel closest from source)
311 |
312 | """
313 |
314 | def __init__(self, image, pixel_zero=[0, 0], filter="best", extend="none"):
315 | """Initialize"""
316 | if isinstance(image, Surface):
317 | self._cairo_surface = image
318 | else:
319 | self._cairo_surface = Surface.from_image(image)._cairo_surface
320 | self.matrix = translation_matrix(pixel_zero)
321 | self.filter = filter
322 | self.extend = extend
323 |
324 | def set_matrix(self, new_mat):
325 | """ Returns a copy of the element, with a new transformation matrix """
326 | new = copy(self)
327 | new.matrix = new_mat
328 | return new
329 |
330 | def make_cairo_pattern(self):
331 | pat = cairo.SurfacePattern(self._cairo_surface)
332 | pat.set_filter({"best": cairo.FILTER_BEST,
333 | "nearest": cairo.FILTER_NEAREST,
334 | "gaussian": cairo.FILTER_GAUSSIAN,
335 | "good": cairo.FILTER_GOOD,
336 | "bilinear": cairo.FILTER_BILINEAR,
337 | "fast": cairo.FILTER_FAST}[self.filter])
338 |
339 | pat.set_extend({"none": cairo.EXTEND_NONE,
340 | "repeat": cairo.EXTEND_REPEAT,
341 | "reflect": cairo.EXTEND_REFLECT,
342 | "pad": cairo.EXTEND_PAD}[self.extend])
343 |
344 | pat.set_matrix(self._cairo_matrix())
345 |
346 | return pat
347 |
348 |
349 | for meth in ["scale", "rotate", "translate", "_cairo_matrix"]:
350 | exec("ImagePattern.%s = Element.%s" % (meth, meth))
351 |
352 |
353 | def _set_source(ctx, src):
354 | """ Sets a source before drawing an element.
355 |
356 | The source is what fills an element (or the element's contour).
357 | If can be of many forms. See the documentation of shape_element for more
358 | details.
359 |
360 | """
361 | if isinstance(src, ColorGradient):
362 | src.set_source(ctx)
363 | elif isinstance(src, ImagePattern):
364 | ctx.set_source(src.make_cairo_pattern())
365 | elif isinstance(src, np.ndarray) and len(src.shape) > 1:
366 | string = src.to_string()
367 | surface = cairo.ImageSurface.create_for_data(string)
368 | set_source(ctx, surface)
369 | elif len(src) == 4: # RGBA
370 | ctx.set_source_rgba(*src)
371 | else: # RGB
372 | ctx.set_source_rgb(*src)
373 |
374 |
375 | #########################################################################
376 | # BASE ELEMENTS
377 |
378 | def shape_element(draw_contour, xy=(0, 0), angle=0, fill=None, stroke=(0, 0, 0),
379 | stroke_width=0, line_cap=None, line_join=None):
380 | """
381 |
382 | Parameters
383 | ------------
384 |
385 | xy
386 | vector [x,y] indicating where the Element should be inserted in the
387 | drawing. Note that for shapes like circle, square, rectangle,
388 | regular_polygon, the [x,y] indicates the *center* of the element.
389 | So these elements are centered around 0 by default.
390 |
391 | angle
392 | Angle by which to rotate the shape. The rotation uses (0,0) as center
393 | point. Therefore all circles, rectangles, squares, and regular_polygons
394 | are rotated around their center.
395 |
396 | fill
397 | Defines wath will fill the element. Default is None (no fill). `fill` can
398 | be one of the following:
399 | - A (r,g,b) color tuple, where 0 =< r,g,b =< 1
400 | - A (r,g,b, a) color tuple, where 0=< r,g,b,a =< 1 (a defines the
401 | transparency: 0 is transparent, 1 is opaque)
402 | - A gizeh.ColorGradient object.
403 | - A gizeh.Surface
404 | - A numpy image (not implemented yet)
405 |
406 | stroke
407 | Decides how the stroke (contour) of the element will be filled.
408 | Same rules as for argument ``fill``. Default is color black
409 |
410 | stroke_width
411 | Width of the stroke, in pixels. Default is 0 (no apparent stroke)
412 |
413 | line_cap
414 | The shape of the ends of the stroke: 'butt' or 'round' or 'square'
415 |
416 | line_join
417 | The shape of the 'elbows' of the contour: 'square', 'cut' or 'round'
418 |
419 | """
420 |
421 | def new_draw(ctx):
422 | draw_contour(ctx)
423 | if fill is not None:
424 | ctx.move_to(*xy)
425 | _set_source(ctx, fill)
426 | ctx.fill_preserve()
427 | if stroke_width > 0:
428 | ctx.move_to(*xy)
429 | ctx.set_line_width(stroke_width)
430 | if line_cap is not None:
431 | ctx.set_line_cap({"butt": cairo.LINE_CAP_BUTT,
432 | "round": cairo.LINE_CAP_ROUND,
433 | "square": cairo.LINE_CAP_SQUARE}[line_cap])
434 | if line_join is not None:
435 | ctx.set_line_join({"cut": cairo.LINE_JOIN_BEVEL,
436 | "square": cairo.LINE_JOIN_MITER,
437 | "round": cairo.LINE_JOIN_ROUND}[line_join])
438 | _set_source(ctx, stroke)
439 | ctx.stroke_preserve()
440 |
441 | if (angle == 0) and (tuple(xy) == (0, 0)):
442 | return Element(new_draw)
443 | elif angle == 0:
444 | return Element(new_draw).translate(xy)
445 | elif tuple(xy) == (0, 0):
446 | return Element(new_draw).rotate(angle)
447 | else:
448 | return Element(new_draw).rotate(angle).translate(xy)
449 |
450 |
451 | def rectangle(lx, ly, **kw):
452 | return shape_element(lambda c: c.rectangle(-lx / 2, -ly / 2, lx, ly), **kw)
453 |
454 |
455 | def square(l, **kw):
456 | return rectangle(l, l, **kw)
457 |
458 |
459 | def arc(r, a1, a2, **kw):
460 | return shape_element(lambda c: c.arc(0, 0, r, a1, a2), **kw)
461 |
462 |
463 | def circle(r, **kw):
464 | return arc(r, 0, 2 * np.pi, **kw)
465 |
466 |
467 | def polyline(points, close_path=False, **kw):
468 | def draw(ctx):
469 | ctx.move_to(*points[0])
470 | for p in points[1:]:
471 | ctx.line_to(*p)
472 | if close_path:
473 | ctx.close_path()
474 | return shape_element(draw, **kw)
475 |
476 |
477 | def regular_polygon(r, n, **kw):
478 | points = [polar2cart(r, a) for a in np.linspace(0, 2 * np.pi, n + 1)[:-1]]
479 | return polyline(points, close_path=True, **kw)
480 |
481 |
482 | def bezier_curve(points, **kw):
483 | '''Create cubic Bezier curve
484 |
485 | points
486 | List of four (x,y) tuples specifying the points of the curve.
487 | '''
488 | def draw(ctx):
489 | ctx.move_to(*points[0])
490 | ctx.curve_to(*tuple(chain(*points))[2:])
491 | return shape_element(draw, **kw)
492 |
493 |
494 | def ellipse(w, h, **kw):
495 | '''Create an ellipse.
496 |
497 | w, h
498 | These are used to set the control points for the first quarter
499 | of the ellipse.
500 | '''
501 |
502 | # Bezier control points for a quarter of an ellipse.
503 | ctrl_pnts = [((w / 2), 0), ((w / 2), (h / 2) * (4 / 3) * (sqrt(2) - 1)),
504 | ((w / 2) * (4 / 3) * (sqrt(2) - 1), (h / 2)), (0, (h / 2))]
505 |
506 | # Create a list, all_points, which will be populated with lists of control
507 | # points for 4 Bezier curves that will approximate the ellipse.
508 | all_points = []
509 | for i in [1, -1]:
510 | for j in [1, -1]:
511 | all_points.append([(pnt[0] * i, pnt[1] * (-j))
512 | for pnt in ctrl_pnts])
513 | # Permutes the last three lists to put the curves in correct order
514 | all_points.append(all_points.pop(1))
515 | # Correct the order of the two sublists defining their respective quarter
516 | # pieces of the ellipse so that the whole ellipse is drawn in order
517 | all_points[1].reverse()
518 | all_points[3].reverse()
519 |
520 | def draw(ctx):
521 | ctx.move_to(*ctrl_pnts[0])
522 | for points in all_points:
523 | ctx.curve_to(*tuple(chain(*points))[2:])
524 | ctx.close_path()
525 |
526 | return shape_element(draw, **kw)
527 |
528 |
529 | def star(nbranches=5, radius=1.0, ratio=0.5, **kwargs):
530 | """ This function draws a star with the given number of branches,
531 | radius, and ratio between branches and body. It accepts the usual
532 | parameters xy, angle, fill, etc. """
533 |
534 | rr = radius * np.array(nbranches * [1.0, ratio])
535 | aa = np.linspace(0, 2 * np.pi, 2 * nbranches + 1)[:-1]
536 | points = polar2cart(rr, aa)
537 | return polyline(points, close_path=True, **kwargs)
538 |
539 |
540 | def text(txt, fontfamily, fontsize, fill=(0, 0, 0),
541 | h_align="center", v_align="center",
542 | stroke=(0, 0, 0), stroke_width=0,
543 | fontweight="normal", fontslant="normal",
544 | angle=0, xy=[0, 0], y_origin="top"):
545 | """Create a text object.
546 |
547 | Parameters
548 | -----------
549 |
550 | v_align
551 | vertical alignment of the text: "top", "center", "bottom"
552 |
553 | h_align
554 | horizontal alignment of the text: "left", "center", "right"
555 |
556 | fontweight
557 | "normal" "bold"
558 |
559 | fontslant
560 | "normal" "oblique" "italic"
561 |
562 | y_origin
563 | Adapts the vertical orientation of the text to the coordinates system:
564 | if you are going to export the image with y_origin="bottom" (see for
565 | instance Surface.write_to_png) then set y_origin to "bottom" here too.
566 |
567 | angle, xy, stroke, stroke_width
568 | see the doc for ``shape_element``
569 | """
570 |
571 | fontweight = {"normal": cairo.FONT_WEIGHT_NORMAL,
572 | "bold": cairo.FONT_WEIGHT_BOLD}[fontweight]
573 | fontslant = {"normal": cairo.FONT_SLANT_NORMAL,
574 | "oblique": cairo.FONT_SLANT_OBLIQUE,
575 | "italic": cairo.FONT_SLANT_ITALIC}[fontslant]
576 |
577 | def draw(ctx):
578 |
579 | ctx.select_font_face(fontfamily, fontslant, fontweight)
580 | ctx.set_font_size(fontsize)
581 | xbear, ybear, w, h, xadvance, yadvance = ctx.text_extents(txt)
582 | xshift = {"left": 0, "center": -w / 2, "right": -w}[h_align] - xbear
583 | yshift = {"top": 0, "center": -h / 2, "bottom": -h}[v_align] - ybear
584 | new_xy = np.array(xy) + np.array([xshift, yshift])
585 | ctx.move_to(*new_xy)
586 | ctx.text_path(txt)
587 | _set_source(ctx, fill)
588 | ctx.fill()
589 | if stroke_width > 0:
590 | ctx.move_to(*new_xy)
591 | ctx.text_path(txt)
592 | _set_source(ctx, stroke)
593 | ctx.set_line_width(stroke_width)
594 | ctx.stroke()
595 |
596 | return (Element(draw).scale(1, 1 if (y_origin == "top") else -1)
597 | .rotate(angle))
598 |
--------------------------------------------------------------------------------
/gizeh/tools.py:
--------------------------------------------------------------------------------
1 |
2 | def htmlcolor_to_rgb(string):
3 |
4 | if not (string.startswith('#') and len(string)==7):
5 | raise ValueError("Bad html color format. Expected: '#RRGGBB' ")
6 |
7 |
8 | return [1.0*int(n,16)/255 for n in (string[:2], string[2:4], string[4:])]
9 |
--------------------------------------------------------------------------------
/gizeh/version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.1.11"
2 |
--------------------------------------------------------------------------------
/logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zulko/gizeh/d9fda97c9cc5508ecd3e6fbfa0590f763f4e2711/logo.jpeg
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | try:
2 | from setuptools import setup
3 | except ImportError:
4 | try:
5 | import ez_setup
6 | ez_setup.use_setuptools()
7 | except ImportError:
8 | raise ImportError("Gizeh could not be installed, probably because"
9 | " neither setuptools nor ez_setup are installed on this computer."
10 | "\nInstall ez_setup ([sudo] pip install ez_setup) and try again.")
11 |
12 | from setuptools import setup, find_packages
13 |
14 | exec(open('gizeh/version.py').read()) # loads __version__
15 |
16 | setup(name='gizeh',
17 | version=__version__,
18 | author='Zulko',
19 | description='Simple vector graphics in Python',
20 | long_description=open('README.rst').read(),
21 | license='see LICENSE.txt',
22 | keywords="Cairo vector graphics",
23 | install_requires=['cairocffi', 'numpy'],
24 | packages= find_packages(exclude='docs'))
25 |
--------------------------------------------------------------------------------
/tests/samples/random_squares.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zulko/gizeh/d9fda97c9cc5508ecd3e6fbfa0590f763f4e2711/tests/samples/random_squares.png
--------------------------------------------------------------------------------
/tests/samples/roses.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zulko/gizeh/d9fda97c9cc5508ecd3e6fbfa0590f763f4e2711/tests/samples/roses.png
--------------------------------------------------------------------------------
/tests/samples/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zulko/gizeh/d9fda97c9cc5508ecd3e6fbfa0590f763f4e2711/tests/samples/star.png
--------------------------------------------------------------------------------
/tests/samples/transparent_colors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zulko/gizeh/d9fda97c9cc5508ecd3e6fbfa0590f763f4e2711/tests/samples/transparent_colors.png
--------------------------------------------------------------------------------
/tests/samples/yin_yang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zulko/gizeh/d9fda97c9cc5508ecd3e6fbfa0590f763f4e2711/tests/samples/yin_yang.png
--------------------------------------------------------------------------------
/tests/test_pdfsurface.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for Gizeh
3 | """
4 |
5 | import gizeh as gz
6 | import os
7 |
8 |
9 | def test_pdfsurface(tmpdir):
10 | """Test PDFSurface class."""
11 | # 800x800 point image
12 | im_size = 800
13 |
14 | # Create a simple star shape with a fill
15 | shape = gz.star(stroke_width=0.01, fill=(0, 0, 0.3, 0.7))
16 | shape = shape.rotate(-3.14159265358979 / 2.0)
17 | shape = shape.scale((im_size - 100) // 2)
18 | shape = shape.translate([im_size // 2, im_size // 2])
19 |
20 | # Some text to throw on the shape...
21 | txt = gz.text("Gizeh on pdf",
22 | fontfamily="Arial",
23 | fontsize=50,
24 | fill=(0, 0, 0),
25 | xy=(im_size // 2, im_size // 2))
26 |
27 | # Create pdf surface
28 | filepath = os.path.join(str(tmpdir), "pdfsurface_test.pdf")
29 | s = gz.PDFSurface(filepath, im_size, im_size)
30 | # Draw shape on the PDF surface
31 | shape.draw(s)
32 | txt.draw(s)
33 |
34 | # Write file and close surface
35 | s.flush()
36 | s.finish()
37 |
38 | # Delete test PDF
39 |
--------------------------------------------------------------------------------
/tests/test_samples.py:
--------------------------------------------------------------------------------
1 | import os
2 | import numpy as np
3 | from PIL import Image
4 | import gizeh as gz
5 |
6 | samples_dir = os.path.join("tests", "samples")
7 | samples = {
8 | f[:-4]: np.array(Image.open(os.path.join(samples_dir, f)))
9 | for f in os.listdir(samples_dir)
10 | }
11 |
12 |
13 | def is_like_sample(surface, sample_name):
14 | return (surface.get_npimage() == samples[sample_name]).all()
15 |
16 |
17 | def test_random_squares():
18 | np.random.seed(123)
19 | L = 200
20 | surface = gz.Surface(L, L, bg_color=(1, 1, 1))
21 | n_squares = 1000
22 | angles = 2 * np.pi * np.random.rand(n_squares)
23 | sizes = 20 + 20 * np.random.rand(n_squares)
24 | positions = L * np.random.rand(n_squares, 2)
25 | colors = np.random.rand(n_squares, 3)
26 |
27 | for angle, size, position, color in zip(angles, sizes, positions, colors):
28 | square = gz.square(size, xy=position, angle=angle, fill=color,
29 | stroke_width=size / 20)
30 | square.draw(surface)
31 |
32 | assert is_like_sample(surface, 'random_squares')
33 |
34 |
35 | def test_star():
36 | surface = gz.Surface(200, 200, bg_color=(1, 0.9, 0.6))
37 |
38 | star1 = gz.star(radius=70, ratio=.4, fill=(1, 1, 1), angle=-np.pi / 2,
39 | stroke_width=2, stroke=(1, 0, 0))
40 | star2 = gz.star(radius=55, ratio=.4, fill=(1, 0, 0), angle=-np.pi / 2)
41 | stars = gz.Group([star1, star2]).translate([100, 100])
42 | stars.draw(surface)
43 |
44 | assert is_like_sample(surface, 'star')
45 |
46 |
47 | def test_yin_yang():
48 | L = 200 # <- dimensions of the final picture
49 | surface = gz.Surface(L, L, bg_color=(0, .3, .6)) # blue background
50 | r = 70 # radius of the whole yin yang
51 |
52 | yin_yang = gz.Group([
53 | gz.arc(r, np.pi / 2, 3 * np.pi / 2, fill=(1, 1, 1)), # white half
54 | gz.arc(r, -np.pi / 2, np.pi / 2, fill=(0, 0, 0)), # black half
55 |
56 | gz.arc(r / 2, -np.pi / 2, np.pi / 2, fill=(1, 1, 1), xy=[0, -r / 2]),
57 | gz.arc(r / 2, np.pi / 2, 3 * np.pi / 2, fill=(0, 0, 0), xy=[0, r / 2]),
58 |
59 | gz.circle(r / 8, xy=[0, +r / 2], fill=(1, 1, 1)), # white dot
60 | gz.circle(r / 8, xy=[0, -r / 2], fill=(0, 0, 0))]) # black dot
61 |
62 | yin_yang.translate([L / 2, L / 2]).draw(surface)
63 |
64 | assert is_like_sample(surface, 'yin_yang')
65 |
66 | def test_transparent_colors():
67 | L = 200 # <- dimensions of the final picture
68 | surface = gz.Surface(L, L, bg_color=(1, 1, 1)) # <- white background
69 | radius = 50
70 | centers = [gz.polar2cart(40, angle)
71 | for angle in [0, 2 * np.pi / 3, 4 * np.pi / 3]]
72 | colors = [(1, 0, 0, .4), # <- Semi-tranparent red (R,G,B, transparency)
73 | (0, 1, 0, .4), # <- Semi-tranparent green
74 | (0, 0, 1, .4)] # <- Semi-tranparent blue
75 | circles = gz.Group([gz.circle(radius, xy=center, fill=color,
76 | stroke_width=3, stroke=(0, 0, 0))
77 | for center, color in zip(centers, colors)])
78 | circles.translate([L / 2, L / 2]).draw(surface)
79 |
80 | assert is_like_sample(surface, 'transparent_colors')
81 |
--------------------------------------------------------------------------------