diff --git a/conda_requirements.txt b/conda_requirements.txt index 3bbdc1c..46b3abf 100644 --- a/conda_requirements.txt +++ b/conda_requirements.txt @@ -6,3 +6,6 @@ libgd # for development pytest +# to build the docs +sphinx + diff --git a/docs/_static/readme.txt b/docs/_static/readme.txt new file mode 100644 index 0000000..c26bda6 --- /dev/null +++ b/docs/_static/readme.txt @@ -0,0 +1 @@ +Just here so that git will keep the dir around diff --git a/docs/conf.py b/docs/conf.py index fccb6e8..d2a6ae7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,9 +11,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function import sys, os # If extensions (or modules to document with autodoc) are in another directory, @@ -28,7 +25,12 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.mathjax'] +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + # 'sphinx.ext.pngmath', + # 'sphinx.ext.mathjax', + ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/index.rst b/docs/index.rst index 52be623..75e4fb3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,7 @@ Welcome to py_gd's documentation! Contents: .. toctree:: - :maxdepth: 2 + :maxdepth: 2 introduction installing diff --git a/docs/installing.rst b/docs/installing.rst index 16969cf..d0bf9d6 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -27,7 +27,9 @@ Ideally, it is as simple as:: $ python setup.py build $ python setup.py install + or:: + $ python setup.py develop (develop mode installs links to the code, rather than copying the code into python's site-packages -- it is helpful if you want to be updating the code, and have the new version run right away.) diff --git a/docs/reference.rst b/docs/reference.rst index 24476a8..2331d07 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -3,23 +3,37 @@ ``py_gd`` Reference =========================== -``py_gd`` is a python wrapper around teh GD graphics library. For the real detail, see the source and docs of GD itself: +``py_gd`` is a python wrapper around the GD graphics library. For the real detail, see the source and docs of GD itself: -http://libgd.bitbucket.org/ +https://github.com/libgd/libgd + +This is the documentation for the python inteface -- it more or less mirrors the GD API, but makes it more object oriented and generally uses "pythonic" names and style. + +Notes: +------ + +There is a module attribute: ``MAX_IMAGE_SIZE``. It is currently set 1GB. It can be changed after import, before initializing an Image. On the develoment system, creating images greater than 1GB brings the system to an almost halt before raising a memory error. But your machine may be able to tolerae larger images. Hoever, as of this writting, pixel coordintes are C ``int`` type, so very parge images may have issues anyway + +If you want to change it: + +from py_gd import py_gd # to get the actual cython module + +py_gd.MAX_IMAGE_SIZE = 4 * 1024**3 # 4 GB + +Then you can create a larger Image -This is the documentation for teh python inteface -- it more or less mirrors the GD API, but makes it more object oriented and generally used "pythonic" names and style. Class Reference: ---------------------- +---------------- .. automodule:: py_gd :members: ``py_gd.Image`` -- the gd image class --------------------------------------------------- +------------------------------------- .. autoclass:: py_gd.Image :members: Factory functions ------------------------------------------------------ +----------------- .. autofunction:: py_gd.from_array diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 5120fca..1ca4d72 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -1,6 +1,7 @@ .. _tutorials: + Tutorials -===================== +========= A collection of tutorials and example scripts to get you up and running. diff --git a/py_gd/__init__.py b/py_gd/__init__.py index 425c4b6..868da46 100644 --- a/py_gd/__init__.py +++ b/py_gd/__init__.py @@ -7,20 +7,13 @@ This __init__ does some kludges to load libraries, and then does an -import * - +from .py_gd import * """ -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import absolute_import -from __future__ import division - import sys import os -__version__ = "1.1.2.dev0" - +__version__ = "2.0.0" if sys.platform.startswith('win'): # This only works for Anaconda / miniconda diff --git a/py_gd/py_gd.pxd b/py_gd/py_gd.pxd index e269bcf..a88f3a1 100644 --- a/py_gd/py_gd.pxd +++ b/py_gd/py_gd.pxd @@ -7,6 +7,10 @@ declarations for the cython wrapper around the gd drawing lib import cython from libc.stdio cimport FILE +IF UNAME_SYSNAME == "Windows": + cdef extern from "": + ctypedef Py_UNICODE wchar_t + FILE *_wfopen(const wchar_t *filename,const wchar_t *mode) ## access the gd header files: cdef extern from "gd.h": diff --git a/py_gd/py_gd.pyx b/py_gd/py_gd.pyx index 612abdb..5b87cbb 100644 --- a/py_gd/py_gd.pyx +++ b/py_gd/py_gd.pyx @@ -18,9 +18,9 @@ from py_gd cimport * from libc.stdio cimport FILE, fopen, fclose from libc.string cimport memcpy, strlen from libc.stdlib cimport malloc, free -import os -# from libc.stdint cimport uint8_t, uint32_t +import os +import sys import operator import numpy as np @@ -51,9 +51,10 @@ __gd_version__ = gdVersionString().decode('ascii') # ('purple', (127, 0, 127))] # note that the 1GB max is arbitrary -- you can change it after import, -# before initizing an Image. On my system going bigger than this brings +# before initializing an Image. On my system going bigger than this brings # the system to an almost halt before raising a memory error, so I set # a limit here. + MAX_IMAGE_SIZE = 2 ** 30 # 1 GB limit cpdef cnp.ndarray[int, ndim=2, mode='c'] asn2array(obj, dtype): @@ -77,6 +78,33 @@ cpdef cnp.ndarray[int, ndim=2, mode='c'] asn2array(obj, dtype): return arr +cdef FILE* open_file(file_path) except *: + """ + opens a file + + :param path: python str or PathLike + + :returns: File Pointer + + Note: On Windows, it uses a wchar, UTC-16 encoded + On other platforms (Mac and Linux), it assumes utf-8 + """ + cdef FILE* fp + + file_path = os.fspath(file_path) + + fp = NULL + + IF UNAME_SYSNAME == 'Windows': + fp = _wfopen(file_path, "wb") + ELSE: + fp = fopen(file_path.encode('utf-8'), 'wb') + + if fp is NULL: + raise OSError('could not open the file: {}'.format(file_path)) + + return fp + cdef class Image: """ @@ -142,8 +170,9 @@ cdef class Image: None - no pre-allocated colors -- the first one you allocate will be - the background color - or any of the colors in py_gd.colors + the background color or any of + the colors in py_gd.colors + :type preset_colors: string or None The Image is created as a 8-bit Paletted Image. @@ -436,31 +465,22 @@ cdef class Image: was compiled. But bmp and gif should always be there. :param file_name: full or relative path to file you want created - :type file_name: str or unicode object (but only ascii is supported - for now) + :type file_name: str or PathLike :param file_type: type of file you want written :type file_type: string """ - cdef bytes file_path cdef FILE *fp cdef int compression_level - try: - file_path = file_name.encode('ascii') - except UnicodeEncodeError: - raise ValueError('can only accept ascii filenames') - - file_type_codes = ["bmp", "jpg", "jpeg", "gif", "GIF", "png", "PNG"] + file_type_codes = {"bmp", "jpg", "jpeg", "gif", "GIF", "png", "PNG"} if file_type not in file_type_codes: raise ValueError('file_type must be one of: {}' .format(file_type_codes)) + fp = open_file(file_name) # open the file here: - fp = fopen(file_path, "wb") - if fp is NULL: - raise IOError('could not open the file: {}'.format(file_path)) # then call the right writer: if file_type in ["bmp", "BMP"]: @@ -936,16 +956,15 @@ cdef class Image: :type point: 2-tuple of (x,y) integers :param font: Desired font -- gd built in fonts are one of: - {"tiny", "small", "medium", "large", - "giant"} + ``{"tiny", "small", "medium", "large", "giant"}`` :type font: string :param color: Color of text :type color=None: color name or index :param align: The principal point that the text box references - :type align: one of the following: {'lt', 'ct', 'rt', 'r', - 'rb', 'cb', 'lb', 'l'} + :type align: one of the following: ``{'lt', 'ct', 'rt', 'r', + 'rb', 'cb', 'lb', 'l'}`` :param background: The background color of the text box. Default is 'none' (nothing is drawn) @@ -955,8 +974,8 @@ cdef class Image: try: text_bytes = text.encode('ascii') - except UnicodeEncodeError: - raise ValueError("can only accept ascii text") + except UnicodeEncodeError as err: + raise ValueError("can only accept ascii text") from err cdef gdFontPtr gdfont @@ -1040,30 +1059,26 @@ cdef class Animation: cdef Image prev_frame cdef int base_delay cdef FILE *_fp - cdef bytes _file_path cdef int _has_begun cdef int _has_closed cdef int _frames_written cdef int _global_colormap + cdef object _file_path - def __cinit__(self, str file_name, int delay=50, int global_colormap=1): + def __cinit__(self, file_name, int delay=50, int global_colormap=1): """ :param file_name: The name/file path of the animation that will be saved - :type file_name: string + :type file_name: str or PathLike object :param delay: the default delay between frames :type delay: int :param global_colormap=1: Whether to use a global colormap. If 1, the same colormap is used for - all images inthe animation. If 0, + all images in the animation. If 0, a new colormap is used for each frame. """ - try: - self._file_path = file_name.encode('ascii') - except UnicodeEncodeError: - raise ValueError("can only accept ascii filenames") self._fp = NULL self.base_delay = delay @@ -1078,7 +1093,7 @@ cdef class Animation: """ :param file_name: The name/file path of the animation that will be saved - :type file_name: string + :type file_name: path_like e.g. str or pathlib.Path :param delay: the default delay between frames :type delay: int @@ -1088,24 +1103,23 @@ cdef class Animation: all images in the animation. If 0, a new colormap is used for each frame. """ + self._file_path = file_name self.cur_frame = None self.prev_frame = None def __dealloc__(self): """ - cleans up the file and file pointer if animation was started - and not closed + closes the file and file pointer if animation was started + and not closed. + + also calls gdImageGifAnimEnd to hopefully result in a valid file. """ - if (self._fp is not NULL - and self._has_closed != 1 - and self._has_begun == 1): - fclose(self._fp) + if self._has_begun > 0: + self.close_anim() - try: - os.remove(self._file_path) - except OSError: - raise OSError('file {} could not be removed' - .format(self._file_path)) + if self._fp is not NULL: + fclose(self._fp) + self._fp = NULL def begin_anim(self, Image first, int loops=0): """ @@ -1120,17 +1134,13 @@ cdef class Animation: (0 -> loop, -1 -> no loop, n > 0 -> loop n times) :type loops: int """ - self._fp = fopen(self._file_path, "wb") - if self._fp is NULL: - raise IOError('could not open the file: {}' - .format(self._file_path)) - - if self._has_begun is 1: + if self._has_begun == 1: raise RuntimeError('Animation has already been started') - if self._has_closed is 1: + if self._has_closed == 1: raise RuntimeError('Cannot re-begin closed animation') + self._fp = open_file(self._file_path) self.cur_frame = Image(first.width, first.height) self.cur_frame.copy(first) @@ -1153,10 +1163,10 @@ cdef class Animation: <1 reverts to default delay :type delay: int """ - if self._has_begun is 0: + if self._has_begun == 0: raise IOError('Cannot add frame to non-started animation') - if self._has_closed is 1: + if self._has_closed == 1: raise IOError('Cannot add frame to closed animation') if self.cur_frame is None or image is None: @@ -1198,12 +1208,13 @@ cdef class Animation: self._frames_written += 1 def close_anim(self): - if self._has_begun is 0: + if self._has_begun == 0: raise RuntimeError("Cannot close animation that hasn't been " - "opened") + "opened (begun)") cdef gdImagePtr prev + if self._fp is not NULL: prev = NULL @@ -1220,24 +1231,21 @@ cdef class Animation: self._has_closed = 1 - def reset(self, Image img not None, str file_path not None): + def reset(self, file_path=None): """ Resets the object state so it can be used again to create another animation - :param img: new first frame - :type img: Image + NOTE: begin_anim needs to be called again - :param file_path: path and filename of new animation - :param file_path: str + :param file_path=None: filename of new animation. Will reuse existing + name if not specified + :param file_path: pathlike """ - self.cur_frame = img self.prev_frame = None - try: - self._file_path = file_path.encode('ascii') - except UnicodeEncodeError: - raise ValueError('can only except ascii filenames') + if file_path is not None: + self._file_path = file_path if self._fp is not NULL: fclose(self._fp) diff --git a/py_gd/test/build_checksums.py b/py_gd/test/build_checksums.py index 385fc22..7fefa6f 100644 --- a/py_gd/test/build_checksums.py +++ b/py_gd/test/build_checksums.py @@ -5,16 +5,9 @@ the output can be cut&pasted into the test file -and updated when neccessary. +and updated when necessary. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from future import standard_library -standard_library.install_aliases() -from builtins import * import os import hashlib from pprint import pprint diff --git a/py_gd/test/intersect_comp.py b/py_gd/test/intersect_comp.py index 8062327..9d8adbb 100644 --- a/py_gd/test/intersect_comp.py +++ b/py_gd/test/intersect_comp.py @@ -1,16 +1,6 @@ -from __future__ import print_function -from __future__ import division -from __future__ import absolute_import -from __future__ import unicode_literals - # test of scanline: -from future import standard_library -standard_library.install_aliases() -from builtins import range -from builtins import * -from past.utils import old_div import numpy as np # to get an actual 32 bit int def ip1(y, y1, y2, x1, x2): @@ -21,24 +11,25 @@ def ip1(y, y1, y2, x1, x2): # print ((y - y1) * (x2 - x1)).dtype # print # print "end ip1" - return ( old_div(((y - y1) * (x2 - x1)).astype(np.float32), - ( (y2 - y1) ).astype(np.float32)) + 0.5 + x1 + return ( ((y - y1) * (x2 - x1)).astype(np.float32) / + ( (y2 - y1) ).astype(np.float32) + 0.5 + x1 ).astype(np.int32) def ip2(y, y1, y2, x1, x2): # refactored to minimize the overflow #this one fails at max value of 4234524 (about 2**22) - return ((old_div((y-y1).astype(np.float32), - (y2 - y1).astype(np.float32)) * + return (((y-y1).astype(np.float32) / + (y2 - y1).astype(np.float32) * (x2 - x1).astype(np.float32)) + 0.5 + x1).astype(np.int32) def ip3(y, y1, y2, x1, x2): # refactored to minimize the overflow - return ((old_div((y-y1).astype(np.float64), - (y2 - y1).astype(np.float64)) * + return (((y-y1).astype(np.float64) / + (y2 - y1).astype(np.float64) * (x2 - x1).astype(np.float64)) + 0.5 + x1).astype(np.int32) + # def ip23(y, y1, y2, x1, x2): # #but python and C do different things with truncation negative integers # return (int( float( (y - y1) * (x2 - x1) ) / diff --git a/py_gd/test/test_buffer.py b/py_gd/test/test_buffer.py index bb499b0..bd0186e 100644 --- a/py_gd/test/test_buffer.py +++ b/py_gd/test/test_buffer.py @@ -3,9 +3,6 @@ """ tests for buffer access to py_gd Image """ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division import pytest from py_gd import Image diff --git a/py_gd/test/test_colors.py b/py_gd/test/test_colors.py index ffb3e87..d84b0bd 100644 --- a/py_gd/test/test_colors.py +++ b/py_gd/test/test_colors.py @@ -1,6 +1,3 @@ -from __future__ import unicode_literals -from __future__ import division -from __future__ import absolute_import import pytest @@ -61,13 +58,13 @@ def test_drawing_with_colors(color_name): break # im.draw_text(self, text, point, font="medium", color='black', align='lt', # background='none') - print('drawing a rectagle in:', color) + print('drawing a rectangle in:', color) x = i * dx y = j * dy im.draw_rectangle((x, y), (x + w, y + h), fill_color=color, ) - im.save(outfile("sample_{}.png").format(color_name), 'png') + im.save(outfile(f"sample_{color_name}.png"), 'png') diff --git a/py_gd/test/test_gd.py b/py_gd/test/test_gd.py index 45ffc1b..3e2d6e6 100644 --- a/py_gd/test/test_gd.py +++ b/py_gd/test/test_gd.py @@ -6,25 +6,26 @@ py.test test_gd.py """ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals import os +import sys import hashlib -import pytest +from pathlib import Path import numpy as np +import pytest + from py_gd import Image, Animation, asn2array, from_array +HERE = Path(__file__).parent + def outfile(file_name): # just to make it a little easier to type.. - output_dir = "./test_images_output" - if not os.path.exists(output_dir): - os.mkdir(output_dir) - return os.path.join(output_dir, file_name) + output_dir = HERE / "test_images_output" + if not output_dir.exists(): + output_dir.mkdir() + return output_dir / file_name def check_file(name): @@ -85,7 +86,7 @@ def check_file(name): def test_init_simple(): """ - simplest possible initilization -- no preset color palette + simplest possible initialization -- no preset color palette """ img = Image(width=400, height=400, preset_colors=None) assert img @@ -118,10 +119,30 @@ def test_asn2array_fail(): def test_cant_save_file(): img = Image(width=400, height=400) - with pytest.raises(IOError): + with pytest.raises(OSError): img.save("a/non_existant/file_path") +def test_non_ascii_file_name(): + """ + Can use full Unicode on utf-8 filesystems only + e.g. OS-X and most Linux + + Windows only supports ASCII at this point + """ + img = Image(width=400, height=400) + + filename = outfile("file\u2014name_with_unicode.png") # u2014 is an EmDash + if sys.getfilesystemencoding() == 'utf-8': + # this should work + img.save(filename) + assert filename.exists() + else: + # other filesystem encodings raise an Exception + with pytest.raises(ValueError): + img.save(filename) + + def test_init_simple_add_rgb(): """ simplest possible initilization -- no preset color palette @@ -979,53 +1000,62 @@ def test_clip_draw(): assert check_file(fname) -def test_animation(): - img = Image(200, 200) - endpoints = np.array(((-100, 0), (100, 0))) - offset = np.array((100, 100)) +def rotating_line(size=200): + """ + generate the endpoints of a rotating line - fname = "test_animation.gif" - anim = Animation(outfile(fname)) + for use in animation tests - anim.begin_anim(img, 0) + it's expecting to be used in a square image: + + (size,size) + """ + endpoints = np.array(((-size / 2, 0), (size / 2, 0))) + offset = np.array((size / 2, size / 2)) for ang in range(0, 360, 10): rad = np.deg2rad(ang) rot_matrix = [(np.cos(rad), np.sin(rad)), (-np.sin(rad), np.cos(rad))] points = np.dot(endpoints, rot_matrix).astype(np.int32) + offset + yield points + + +def test_animation(): + img = Image(200, 200) - if (ang < 180): - img.draw_line(points[0], points[1], 'red') - else: - img.draw_line(points[0], points[1], 'red') + anim = Animation(outfile("test_animation.gif")) + anim.begin_anim(img, 0) + + for points in rotating_line(200): + img.draw_line(points[0], points[1], 'red') anim.add_frame(img) img.draw_line(np.array((0, 100)), np.array((200, 100)), 'green') - anim.add_frame(img) - anim.close_anim() - print(anim.frames_written) + anim.close_anim() + # not much to auto-check here + print(f"{anim.frames_written} frames were written") + assert anim.frames_written == 22 def test_static_animation(): + """ + If subsequent frames are identical, then it should add tot he delay, + rather than adding duplicate images + + Not sure how to actually test the delay, but looking at the animation + you can tell it's slower than the previous test one, which is otherwise + the same + """ img1 = Image(200, 200) img2 = Image(200, 200) - endpoints = np.array(((-100, 0), (100, 0))) - offset = np.array((100, 100)) - - fname = "test_animation.gif" - - anim = Animation(outfile(fname)) + anim = Animation(outfile("test_animation_static.gif")) anim.begin_anim(img1, 0) - for ang in range(0, 360, 10): - rad = np.deg2rad(ang) - rot_matrix = [(np.cos(rad), np.sin(rad)), (-np.sin(rad), np.cos(rad))] - points = np.dot(endpoints, rot_matrix).astype(np.int32) + offset - + for points in rotating_line(200): img1.draw_line(points[0], points[1], 'red') img2.draw_line(points[0], points[1], 'red') @@ -1035,13 +1065,166 @@ def test_static_animation(): anim.add_frame(img2) anim.close_anim() - print(anim.frames_written) + print(f"{anim.frames_written} frames were written") + # duplicate images should have added to delay, rather than adding an image + assert anim.frames_written == 21 + + +def test_animation_reuse_filename(): + """ + make an animation, then make another one with the same filename + + The final one should be green lines + + NOTE: we were having issues on Windows with permissions + """ + + for color in ('red', 'green'): + img = Image(200, 200) + + anim = Animation(outfile("test_animation_reuse.gif")) + anim.begin_anim(img, 0) + + for points in rotating_line(200): + img.draw_line(points[0], points[1], color, line_width=3) + anim.add_frame(img) + anim.close_anim() + + # not much to auto-check here + print(f"{anim.frames_written} frames were written") + assert anim.frames_written == 21 + + +def test_animation_reuse_filename_not_close(): + """ + make an animation, then make another one with the same filename + + The final one should be blue lines + + NOTE: we were having issues on Windows with permissions + """ + + for color in ('red', 'blue'): + img = Image(200, 200) + + anim = Animation(outfile("test_animation_reuse_not_close.gif")) + anim.begin_anim(img, 0) + + for points in rotating_line(200): + img.draw_line(points[0], points[1], color, line_width=3) + anim.add_frame(img) + # anim.close_anim() + + # not much to auto-check here + print(f"{anim.frames_written} frames were written") + assert anim.frames_written == 21 + + +def test_animation_reset_new_filename(): + """ + create an animation, then reset and make another one + """ + anim = Animation(outfile("test_animation_reset1.gif")) + img = Image(200, 200) + + anim.begin_anim(img, 0) + + for points in rotating_line(200): + img.draw_line(points[0], points[1], 'red', line_width=3) + anim.add_frame(img) + + assert anim.frames_written == 21 + + anim.reset(file_path=outfile("test_animation_reset2.gif")) + assert anim.frames_written == 0 + + img = Image(300, 300) + anim.begin_anim(img, 0) + + for points in rotating_line(300): + img.draw_line(points[0], points[1], 'blue', line_width=3) + anim.add_frame(img) + + anim.close_anim() + + # not much to auto-check here + print(f"{anim.frames_written} frames were written") + assert anim.frames_written == 21 + + +def test_animation_reset_same_filename(): + """ + create an animation, then reset and make another one + using the same filename (by default) + """ + anim = Animation(outfile("test_animation_reset_same.gif")) + img = Image(200, 200) + + anim.begin_anim(img, 0) + + for points in rotating_line(200): + img.draw_line(points[0], points[1], 'red', line_width=3) + anim.add_frame(img) + + assert anim.frames_written == 21 + + anim.reset() + assert anim.frames_written == 0 + + img = Image(300, 300) + anim.begin_anim(img, 0) + + for points in rotating_line(300): + img.draw_line(points[0], points[1], 'blue', line_width=3) + anim.add_frame(img) + + anim.close_anim() + + # not much to auto-check here + print(f"{anim.frames_written} frames were written") + assert anim.frames_written == 21 + + +def test_animation_delete_before_use(): + """ + make sure the dealloc doesn't barf if the file hasn't + been opened yet + """ + + filename = outfile("nothing.gif") + + filename.unlink(missing_ok=True) + assert not filename.exists() + + anim = Animation(filename) + del anim + assert not filename.exists() + + # note: this creates a broken gif + anim = Animation(filename) + img = Image(200, 200) + anim.begin_anim(img, 0) + del anim + assert filename.exists() + + +def test_animation_delete_one_frame(): + """ + make sure the dealloc creates a valid gif with only one frame added + """ + filename = outfile("one_frame_delete.gif") + anim = Animation(filename) + img = Image(200, 200) + anim.begin_anim(img, 0) + img.draw_line((0, 0), (200, 200), color="white", line_width=4) + anim.add_frame(img) + del anim + + assert filename.exists() + + + + + -if __name__ == "__main__": - # just run these tests.. - # test_init_default_palette() - # test_init_BW() - # test_init_simple_add_rgb() - # test_init_simple_add_rgba() - test_animation() diff --git a/py_gd/test/test_overflow.py b/py_gd/test/test_overflow.py index 522e747..a496864 100644 --- a/py_gd/test/test_overflow.py +++ b/py_gd/test/test_overflow.py @@ -8,13 +8,10 @@ can fit in a 32 bit int. -- it seems as thoough there is a multiplication in play -- the limit is around the square root of a max int. """ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division -import pytest - import numpy as np +import pytest + from py_gd import Image