├── .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 | --------------------------------------------------------------------------------