├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── conf.py ├── dev │ ├── api.rst │ ├── contribute.rst │ ├── formats.rst │ ├── overview.rst │ └── setup.rst ├── index.rst ├── make.bat └── usr │ ├── installation.rst │ └── quickstart.rst ├── pymaging ├── __init__.py ├── affine.py ├── colors.py ├── exceptions.py ├── formats.py ├── helpers.py ├── image.py ├── pixelarray.py ├── resample.py ├── shapes.py ├── test_utils.py ├── tests │ ├── __init__.py │ ├── test_affine.py │ ├── test_basic.py │ ├── test_colors.py │ └── test_resampling.py ├── utils.py └── webcolors.py ├── runtests.sh └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = pymaging 4 | 5 | [report] 6 | precision = 2 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .* 3 | !.gitignore 4 | !.travis.yml 5 | !.coveragerc 6 | /dist/ 7 | /build/ 8 | /env*/ 9 | *.xml 10 | /docs/_* 11 | /htmlcov/ 12 | /pymaging.egg-info/ 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.1" 6 | - "3.2" 7 | - "pypy" 8 | script: python setup.py test 9 | install: pip install distribute --use-mirrors 10 | notifications: 11 | email: false 12 | irc: "irc.freenode.org#pymaging" 13 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Jonas Obrist 2 | Kristian Oellegaard 3 | Craig de Stigter 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The repository currently contains several files under a different license. 2 | These files will be moved from this repository into their own repository once 3 | this package get's ready to be shipped. For now, please refer to file headers 4 | to see their license. 5 | 6 | Files with different licenses: 7 | 8 | pymaging/formats/jpeg_raw.py: Custom license, held by Dr. Tony Lin and IJG 9 | pymaging/formats/png_reader.py: MIT License, held by Johann C. Rocholl, 10 | David Jones and Nicko van Someren. 11 | pymaging/formats/png_raw.py: MIT License, held by Johann C. Rocholl, 12 | David Jones and Nicko van Someren. 13 | 14 | All other files are licensed under the BSD License: 15 | 16 | Copyright (c) 2012, Jonas Obrist 17 | All rights reserved. 18 | 19 | Redistribution and use in source and binary forms, with or without 20 | modification, are permitted provided that the following conditions are met: 21 | * Redistributions of source code must retain the above copyright 22 | notice, this list of conditions and the following disclaimer. 23 | * Redistributions in binary form must reproduce the above copyright 24 | notice, this list of conditions and the following disclaimer in the 25 | documentation and/or other materials provided with the distribution. 26 | * Neither the name of the Jonas Obrist nor the 27 | names of its contributors may be used to endorse or promote products 28 | derived from this software without specific prior written permission. 29 | 30 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 31 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 32 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 33 | DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 34 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 35 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 36 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 37 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 39 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 40 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | recursive-exclude * *.pyc -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ######## 2 | pymaging 3 | ######## 4 | 5 | Pure Python imaging library. 6 | 7 | * Author: Jonas Obrist and https://github.com/ojii/pymaging/contributors 8 | * License: BSD (some files have other licenses, see LICENSE.txt) 9 | * Compatibility: Python 2.6, 2.7, 3.1, 3.2 and PyPy 1.8. 10 | * Requirements: distribute 11 | * Docs: http://pymaging.rtfd.org 12 | 13 | Thanks to @katylava for helping me pick the name 14 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pymaging.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pymaging.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Pymaging" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pymaging" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Pymaging documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Mar 12 10:51:31 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.intersphinx'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Pymaging' 44 | copyright = u'2012, Jonas Obrist' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.0' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.0.0' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'Pymagingdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'Pymaging.tex', u'Pymaging Documentation', 182 | u'Jonas Obrist', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'pymaging', u'Pymaging Documentation', 215 | [u'Jonas Obrist'], 1) 216 | ] 217 | 218 | intersphinx_mapping = { 219 | 'python': ('http://docs.python.org/3.2', None), 220 | 'distribute': ('http://packages.python.org/distribute/', None), 221 | } 222 | 223 | -------------------------------------------------------------------------------- /docs/dev/api.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Internal API 3 | ############ 4 | 5 | 6 | .. module:: pymaging.image 7 | 8 | ********************* 9 | :mod:`pymaging.image` 10 | ********************* 11 | 12 | 13 | .. class:: Image(mode, width, height, loader, meta=None) 14 | 15 | The image class. This is the core class of pymaging. 16 | 17 | :param mode: The color mode. A :class:`pymaging.colors.ColorType` instance. 18 | :param width: Width of the image (in pixels). 19 | :param height: Height of the image (in pixels). 20 | :param loader: A callable which when called returns a tuple containing a 21 | :class:`pymaging.pixelarray.GenericPixelArray` instance and a palette (which can be ``None``). If you 22 | already have all the pixels for your image loaded, use :class:`pymaging.image.LoadedImage` instead. 23 | :param meta: Any further information your format wants to pass along. Your format should document what users can 24 | expect in ``meta``. 25 | 26 | .. attribute:: mode 27 | 28 | The color mode used in this image. A :class:`pymaging.colors.ColorType` instance. 29 | 30 | .. attribute:: palette 31 | 32 | The palette used in this image. A list of :class:`pymaging.colors.Color` instances or ``None``. 33 | 34 | .. attribute:: reverse_palette 35 | 36 | Cache for :meth:`get_reverse_palette`. 37 | 38 | .. attribute:: pixels 39 | 40 | The :class:`pymaging.pixelarray.GenericPixelArray` (or subclass thereof) instance holding the image data. 41 | 42 | .. attribute:: width 43 | 44 | The width of the image (in pixels) 45 | 46 | .. attribute:: height 47 | 48 | The height of the image (in pixels) 49 | 50 | .. attribute:: pixelsize 51 | 52 | The size of a pixel (in :attr:`pixels`). ``1`` usually indicates an image with a palette. ``3`` is an standard 53 | RGB image. ``4`` is a RGBA image. 54 | 55 | .. classmethod:: open(fileobj) 56 | 57 | Creates a new image from a file object 58 | 59 | :param fileobj: A file like object open for reading. 60 | 61 | .. classmethod:: open_from_path(filepath) 62 | 63 | Creates a new image from a file path 64 | 65 | :param fileobj: A string pointing at a image file. 66 | 67 | .. classmethod:: new(mode, width, height, background_color, palette=None, meta=None) 68 | 69 | Creates a new image with a solid background color. 70 | 71 | :param mode: The color mode. Must be an instance of :class:`pymaging.colors.ColorType`. 72 | :param width: Width of the new image. 73 | :param height: Height of the new image. 74 | :param background_color: The color to use for the background. Must be an instance of 75 | :class:`pymaging.colors.Color`. 76 | :param palette: If given, the palette to use for the image. 77 | :param meta: Any further information your format wants to pass along. Your format should document what users can 78 | expect in ``meta``. 79 | 80 | .. method:: save(fileobj, format) 81 | 82 | Saves the image. 83 | 84 | :param fileobj: A file-like object (opened for writing) to which the image should be saved. 85 | :param format: The format to use for saving (as a string). 86 | 87 | .. method:: save_to_path(filepath, format=None): 88 | 89 | Saves the image to a path. 90 | 91 | :param filepath: A string pointing at a (writable) file location where the image should be saved. 92 | :param format: If given, the format (string) to use for saving. If ``None``, the format will be guessed from 93 | the file extension used in ``filepath``. 94 | 95 | .. method:: get_reverse_palette 96 | 97 | Returns :attr:`reverse_palette`. If :attr:`reverse_palette` is ``None``, calls :meth:`_fill_reverse_palette`. 98 | The reverse palette is a dictionary. If the image has no palette, an empty dictionary is returned. 99 | 100 | .. method:: _fill_reverse_palette 101 | 102 | Populates the reverse palette, which is a mapping of :class:`pymaging.colors.Color` instances to their index in 103 | the palette. Sets :attr:`reverse_palette`. 104 | 105 | .. method:: _copy(pixles, **kwargs) 106 | 107 | Creates a copy of this instances meta information, but setting pixel array to ``pixels``. ``kwargs`` can 108 | override any argument to the :class:`pymaging.image.LoadedImage` constructor. By default the values of this 109 | image are used. 110 | 111 | This method is mostly used by other APIs that return a new copy of the image. 112 | 113 | Returns a :class:`pymaging.image.LoadedImage`. 114 | 115 | .. method:: resize(width, height, resample_algorithm=nearest, resize_canvas=True) 116 | 117 | Resizes the image to the given ``width`` and ``height``, using given ``resample_algorithm``. If 118 | ``resize_canvas`` is ``False``, the actual image dimensions do not change, in which case the excess pixels will 119 | be filled by a background color (usually black). Returns the resized copy of this image. 120 | 121 | :param width: The new width as integer in pixels. 122 | :param height: The new height as integer in pixels. 123 | :param resample_algorithm: The resample algorithm to use. Should be a :class:`pymaging.resample.Resampler` 124 | instance. 125 | :param resize_canvas: Boolean flag whether to resize the canvas or not. 126 | 127 | .. method:: affine(transform, resample_algorithm=nearest, resize_canvas=True) 128 | 129 | Advanced version of :meth:`resize`. Instead of a ``height`` and ``width``, a 130 | :class:`pymaging.affine.AffineTransform` is passed according to which the image is transformed. 131 | Returns the transformed copy of the image. 132 | 133 | .. method:: rotate(degrees, clockwise=False, resample_algorithm=nearest, resize_canvas=True) 134 | 135 | Rotates the image by ``degrees`` degrees counter-clockwise (unless ``clockwise`` is ``True``). Interpolation of 136 | the pixels is done using ``resample_algorithm``. Returns the rotated copy of this image. 137 | 138 | .. method:: get_pixel(x, y) 139 | 140 | Returns the pixel at the given ``x``/``y`` location. If the pixel is outside the image, raises an 141 | :exc:`IndexError`. If the image has a palette, the palette lookup will be performed by this method. The pixel is 142 | returned as a list if integers. 143 | 144 | .. method:: get_color(x, y) 145 | 146 | Same as :meth:`get_pixel` but returns a :class:`pymaging.colors.Color` instance. 147 | 148 | .. method:: set_color(x, y, color) 149 | 150 | The core drawing API. This should be used to draw pixels to the image. Sets the pixel at ``x``/``y`` to the 151 | color given. The color should be a :class:`pymaging.colors.Color` instance. If the image has a palette, only 152 | colors that are in the palette are supported. 153 | 154 | .. method:: flip_top_bottom 155 | 156 | Vertically flips the image and returns the flipped copy. 157 | 158 | .. method:: flip_left_right 159 | 160 | Horizontally flips the image and returns the flipped copy. 161 | 162 | .. method:: crop(width, height, padding_top, padding_left) 163 | 164 | Crops the pixel to the new ``width`` and ``height``, starting the cropping at the offset given with 165 | ``padding_top`` and ``padding_left``. Returns the cropped copy of this image. 166 | 167 | .. method:: draw(shape, color) 168 | 169 | Draws the shape using the given color to this image. The shape should be a :class:`pymaging.shapes.BaseShape` 170 | subclass instance, or any object that has a ``iter_pixels`` method, which when called with a 171 | :class:`pymaging.colors.Color` instance, returns an iterator that yields tuples of ``(x, y, color)`` of colors 172 | to be drawn to pixels. 173 | 174 | This method is just a shortcut around :meth:`set_color` which allows users to write shape classes that do the 175 | heavy lifting for them. 176 | 177 | This method operates **in place** and does not return a copy of this image! 178 | 179 | .. method:: blit(padding_top, padding_left, image): 180 | 181 | Draws the image passed in on top of this image at the location indicated with the padding. 182 | 183 | This method operates **in place** and does not return a copy of this image! 184 | 185 | 186 | .. class:: LoadedImage(mode, width, height, pixels, palette=None, meta=None) 187 | 188 | Subclass of :class:`pymaging.image.Image` if you already have all pixels loaded. All parameters are the same as in 189 | :class:`pymaging.image.Image` except for ``loader`` which is replaced with ``pixels``. ``pixels`` must be an 190 | instance of :class:`pymaging.pixelarray.GenericPixelArray` or a subclass thereof. 191 | 192 | 193 | .. module:: pymaging.affine 194 | 195 | ********************** 196 | :mod:`pymaging.affine` 197 | ********************** 198 | 199 | 200 | .. class:: AffineTransform(matrix) 201 | 202 | Affine transformation matrix. Used by :meth:`pymaging.image.Image.affine`. 203 | 204 | The matrix should be given either as a sequence of 9 values or a sequence of 3 sequences of 3 values. 205 | 206 | .. note:: Needs documentation about the actual values of the matrix. 207 | 208 | .. attribute:: matrix 209 | 210 | .. note:: Needs documentation. 211 | 212 | .. method:: _determinant 213 | 214 | .. note:: Needs documentation. 215 | 216 | .. method:: inverse 217 | 218 | .. note:: Needs documentation. 219 | 220 | .. method:: rotate(degrees, clockwise=False) 221 | 222 | .. note:: Needs documentation. 223 | 224 | .. method:: scale(x_factor, y_factor=None) 225 | 226 | .. note:: Needs documentation. 227 | 228 | .. method:: translate(dx, dy) 229 | 230 | .. note:: Needs documentation. 231 | 232 | 233 | .. module:: pymaging.colors 234 | 235 | ********************** 236 | :mod:`pymaging.colors` 237 | ********************** 238 | 239 | .. function:: _mixin_alpha(colors, alpha) 240 | 241 | Applies the given alpha value to all colors. Colors should be a list of three items: ``r``, ``g`` and ``b``. 242 | 243 | 244 | .. class:: Color(red, green, blue alpha) 245 | 246 | Represents a color. All four parameters should be integers between 0 and 255. 247 | 248 | .. attribute:: red 249 | .. attribute:: green 250 | .. attribute:: blue 251 | .. attribute:: alpha 252 | 253 | .. classmethod:: from_pixel(pixel) 254 | 255 | Given a pixel (a list of colors), create a :class:`Color` instance. 256 | 257 | .. classmethod:: from_hexcode(hexcode) 258 | 259 | Given a hexcode (a string of 3, 4, 6 or 8 characters, optionally prefixed by ``'#'``), construct a 260 | :class:`Color` instance. 261 | 262 | .. method:: get_for_brightness(brightness) 263 | 264 | Given a brightness (alpha value) between 0 and 1, return the current color for that brightness. 265 | 266 | .. method:: cover_with(cover_color) 267 | 268 | Covers the current color with another color respecting their respective alpha values. If the ``cover_color`` 269 | is a solid color, return a copy of the ``cover_color``. ``cover_color`` must be an instance of :class:`Color`. 270 | 271 | .. method:: to_pixel(pixelsize) 272 | 273 | Returns this color as a pixel (list of integers) for the given ``pixelsize`` (3 or 4). 274 | 275 | .. method:: to_hexcode 276 | 277 | Returns this color as RGBA hexcode. (Without leading ``'#'``). 278 | 279 | 280 | .. class:: ColorType 281 | 282 | A named tuple holding the length of a color type (pixelsize) and whether this color type supports the alpha channel 283 | or not. 284 | 285 | .. attribute:: length 286 | .. attribute:: alpha 287 | 288 | 289 | .. data:: RGB 290 | 291 | RGB :class:`ColorType`. 292 | 293 | .. data:: RGBA 294 | 295 | RGBA :class:`ColorType`. 296 | 297 | 298 | .. module:: pymaging.exceptions 299 | 300 | ************************* 301 | :mod:`pymaging.exception` 302 | ************************* 303 | 304 | 305 | .. exception:: PymagingExcpetion 306 | 307 | The root exception type for all exceptions defined in this module. 308 | 309 | .. exception:: FormatNotSupported 310 | 311 | Raised if an image is saved or loaded in a format not supported by pymaging. 312 | 313 | .. exception:: InvalidColor 314 | 315 | Raised if an invalid color is used on an image (usually when the image has a palette). 316 | 317 | 318 | .. module:: pymaging.formats 319 | 320 | *********************** 321 | :mod:`pymaging.formats` 322 | *********************** 323 | 324 | Loads and maintains the formats supported in this installation. 325 | 326 | .. class:: Format(open, save, extensions) 327 | 328 | A named tuple that should be used to define formats for pymaging. ``open`` and ``save`` are callables that 329 | decode and encode an image in this format. ``extensions`` is a list of file extensions this image type could have. 330 | 331 | .. attribute:: open 332 | .. attribute:: save 333 | .. attribute:: extensions 334 | 335 | .. class:: FormatRegistry 336 | 337 | A singleton class for format registration 338 | 339 | .. method:: _populate 340 | 341 | Populates the registry using package resources. 342 | 343 | .. method:: register(format) 344 | 345 | Manually registers a format, which must be an instance of :class:`Format`. 346 | 347 | .. method:: get_format_objects 348 | 349 | Returns all formats in this registry. 350 | 351 | .. method:: get_format(format) 352 | 353 | Given a format name (eg file extension), returns the :class:`Format` instance if it's registered, otherwise 354 | ``None``. 355 | 356 | .. data:: registry 357 | 358 | The singleton instance of :class:`FormatRegistry`. 359 | 360 | .. function:: get_format_objects 361 | 362 | Shortcut to :data:`registry.get_format_objects`. 363 | 364 | .. function:: get_format 365 | 366 | Shortcut to :data:`registry.get_format`. 367 | 368 | .. function:: register 369 | 370 | Shortcut to :data:`registry.register`. 371 | 372 | 373 | .. module:: pymaging.helpers 374 | 375 | *********************** 376 | :mod:`pymaging.helpers` 377 | *********************** 378 | 379 | 380 | .. function:: get_transformed_dimensions(transform, box) 381 | 382 | Takes an affine transform and a four-tuple of (x0, y0, x1, y1) coordinates. Transforms each corner of the given box, 383 | and returns the (width, height) of the transformed box. 384 | 385 | 386 | .. module:: pymaging.pixelarray 387 | 388 | ************************** 389 | :mod:`pymaging.pixelarray` 390 | ************************** 391 | 392 | 393 | .. class:: GenericPixelArray(data, width, height, pixelsize) 394 | 395 | The base pixel array class. ``data`` should be a flat :class:`array.array` instance of pixel data, ``width`` and 396 | ``height`` are the dimensions of the array and ``pixelsize`` defines how many items in the ``data`` array define a 397 | single pixel. 398 | 399 | Use :func:`get_pixel_array` to instantiate this class! 400 | 401 | .. attribute:: data 402 | 403 | The image data as array. 404 | 405 | .. attribute:: width 406 | 407 | The width of the pixel array. 408 | 409 | .. attribute:: height 410 | 411 | The height of the pixel array. 412 | 413 | .. attribute:: pixelsize 414 | 415 | The size of a single pixel 416 | 417 | .. attribute:: line_length 418 | 419 | The length of a line. (:attr:`width` multiplied with :attr:`pixelsize`). 420 | 421 | .. attribute:: size 422 | 423 | The size of the pixel array. 424 | 425 | .. method:: _precalculate 426 | 427 | Precalculates :attr:`line_width` and :attr:`size`. Should be called whenever :attr:`width`, :attr:`height` or 428 | :attr:`pixelsize` change. 429 | 430 | .. method:: _translate(x, y) 431 | 432 | Translates the logical ``x``/``y`` coordinates into the start of the pixel in the pixel array. 433 | 434 | .. method:: get(x, y) 435 | 436 | Returns the pixel at ``x``/``y`` as list of integers. 437 | 438 | .. method:: set(x, y, pixel) 439 | 440 | Sets the ``pixel`` to ``x``/``y``. 441 | 442 | .. method:: copy_flipped_top_bottom 443 | 444 | Returns a copy of this pixel array with the lines flipped from top to bottom. 445 | 446 | .. method:: copy_flipped_left_right 447 | 448 | Returns a copy of this pixel array with the lines flipped from left to right. 449 | 450 | .. method:: copy 451 | 452 | Returns a copy of this pixel array. 453 | 454 | .. method:: remove_lines(offset, amount) 455 | 456 | Removes ``amount`` lines from this pixel array after ``offset`` (from the top). 457 | 458 | .. method:: remove_columns(offset, amount) 459 | 460 | Removes ``amount`` columns from this pixel array after ``offset`` (from the left). 461 | 462 | .. note:: 463 | 464 | If :meth:`remove_columns` and :meth:`remove_lines` are used together, :meth:`remove_lines` should always be 465 | called first, as that method is a lot faster and :meth:`remove_columns` gets faster the fewer lines there 466 | are in a pixel array. 467 | 468 | .. method:: add_lines(offset, amount, fill=0) 469 | 470 | Adds ``amount`` lines to the pixel array after ``offset`` (from the top) and fills it with ``fill``. 471 | 472 | .. method:: add_columns(offset, amount, fill=0) 473 | 474 | Adds ``amount`` columns to the pixel array after ``offset`` (from the left) and fill it with ``fill``. 475 | 476 | .. note:: 477 | 478 | As with :meth:`remove_columns`, the cost of this method grows with the amount of lines in the pixe array. 479 | If it is used together with :meth:`add_lines`, :meth:`add_columns` should be called first. 480 | 481 | 482 | .. class:: PixelArray1(data, width, height) 483 | 484 | Subclass of :class:`GenericPixelArray`, optimized for pixelsize 1. 485 | 486 | Use :func:`get_pixel_array` to instantiate this class! 487 | 488 | .. class:: PixelArray2(data, width, height) 489 | 490 | Subclass of :class:`GenericPixelArray`, optimized for pixelsize 2. 491 | 492 | Use :func:`get_pixel_array` to instantiate this class! 493 | 494 | 495 | .. class:: PixelArray3(data, width, height) 496 | 497 | Subclass of :class:`GenericPixelArray`, optimized for pixelsize 3. 498 | 499 | Use :func:`get_pixel_array` to instantiate this class! 500 | 501 | 502 | .. class:: PixelArray4(data, width, height) 503 | 504 | Subclass of :class:`GenericPixelArray`, optimized for pixelsize 4. 505 | 506 | Use :func:`get_pixel_array` to instantiate this class! 507 | 508 | 509 | .. function:: get_pixel_array(data, width, height, pixelsize) 510 | 511 | Returns the most optimal pixel array class for the given pixelsize. Use this function instead of instantating the 512 | pixel array classes directly. 513 | 514 | 515 | .. module:: pymaging.resample 516 | 517 | ************************ 518 | :mod:`pymaging.resample` 519 | ************************ 520 | 521 | 522 | .. class:: Resampler 523 | 524 | Base class for resampler algorithms. Should never be instantated directly. 525 | 526 | .. method:: affine(source, transform, resize_canvas=True) 527 | 528 | .. note:: Document. 529 | 530 | .. method:: resize(source, width, height, resize_canvas=True) 531 | 532 | .. note:: Document. 533 | 534 | 535 | .. class:: Nearest 536 | 537 | Subclass of :class:`Resampler`. Implements the nearest neighbor resampling algorithm which is very fast but creates 538 | very ugly resampling artifacts. 539 | 540 | 541 | .. class:: Bilinear 542 | 543 | Subclass of :class:`Resampler` implementing the bilinear resampling algorithm, which produces much nicer results at 544 | the cost of computation time. 545 | 546 | 547 | .. data:: nearest 548 | 549 | Singleton instance of the :class:`Nearest` resampler. 550 | 551 | 552 | .. data:: bilinear 553 | 554 | Singleton instance of the :class:`Bilinear` resampler. 555 | 556 | 557 | .. module:: pymaging.shapes 558 | 559 | ********************** 560 | :mod:`pymaging.shapes` 561 | ********************** 562 | 563 | 564 | Shapes are the high level drawing API used by :meth:`pymaging.image.Image.draw`. 565 | 566 | 567 | .. class:: BaseShape 568 | 569 | Dummy base class for shapes. 570 | 571 | .. method:: iter_pixels(color) 572 | 573 | In subclasses, this is the API used by :meth:`pymaging.image.Image.draw` to draw to an image. Should return an 574 | iterator that yields ``x``, ``y``, ``color`` tuples. 575 | 576 | 577 | .. class:: Pixel(x, y) 578 | 579 | A simple single-pixel drawing object. 580 | 581 | 582 | .. class:: Line(start_x, start_y, end_x, end_y) 583 | 584 | Simple line drawing algorithm using the Bresenham Line Algorithm. Draws non-anti-aliased lines, which is very fast 585 | but for lines that are not exactly horizontal or vertical, this produces rather ugly lines. 586 | 587 | 588 | .. class:: AntiAliasedLine(start_x, start_y, end_x, end_y) 589 | 590 | Draws an anti-aliased line using Xiaolin Wu's line algorithm. This has a lot higher computation costs than 591 | :class:`Line` but produces much nicer results. When used on an image with a palette, this shape might cause errors. 592 | 593 | 594 | .. module:: pymaging.test_utils 595 | 596 | ************************** 597 | :mod:`pymaging.test_utils` 598 | ************************** 599 | 600 | 601 | .. function:: image_factory(colors, alpha=True) 602 | 603 | Creates an image given a list of lists of :class:`pymaging.color.Color` instances. The ``alpha`` parameter defines 604 | the pixel size of the image. 605 | 606 | 607 | .. class:: PymagingBaseTestCase 608 | 609 | .. method:: assertImage(image, colors, alpha=True) 610 | 611 | Checks that an image is the same as the dummy image given. ``colors`` and ``alpha`` are passed to 612 | :func:`image_factory` to create a comparison image. 613 | 614 | 615 | .. module:: pymaging.utils 616 | 617 | ********************* 618 | :mod:`pymaging.utils` 619 | ********************* 620 | 621 | 622 | .. function:: fdiv(a, b) 623 | 624 | Does a float division of ``a`` and ``b`` regardless of their type and returns a float. 625 | 626 | 627 | .. function:: get_test_file(testfile, fname) 628 | 629 | Returns the full path to a file for a given test. 630 | 631 | 632 | .. module:: pymaging.webcolors 633 | 634 | ************************* 635 | :mod:`pymaging.webcolors` 636 | ************************* 637 | 638 | 639 | Defines constant :class:`pymaging.color.Color` instances for web colors. 640 | 641 | .. data:: IndianRed 642 | .. data:: LightCoral 643 | .. data:: Salmon 644 | .. data:: DarkSalmon 645 | .. data:: LightSalmon 646 | .. data:: Red 647 | .. data:: Crimson 648 | .. data:: FireBrick 649 | .. data:: DarkRed 650 | .. data:: Pink 651 | .. data:: LightPink 652 | .. data:: HotPink 653 | .. data:: DeepPink 654 | .. data:: MediumVioletRed 655 | .. data:: PaleVioletRed 656 | .. data:: LightSalmon 657 | .. data:: Coral 658 | .. data:: Tomato 659 | .. data:: OrangeRed 660 | .. data:: DarkOrange 661 | .. data:: Orange 662 | .. data:: Gold 663 | .. data:: Yellow 664 | .. data:: LightYellow 665 | .. data:: LemonChiffon 666 | .. data:: LightGoldenrodYellow 667 | .. data:: PapayaWhip 668 | .. data:: Moccasin 669 | .. data:: PeachPuff 670 | .. data:: PaleGoldenrod 671 | .. data:: Khaki 672 | .. data:: DarkKhaki 673 | .. data:: Lavender 674 | .. data:: Thistle 675 | .. data:: Plum 676 | .. data:: Violet 677 | .. data:: Orchid 678 | .. data:: Fuchsia 679 | .. data:: Magenta 680 | .. data:: MediumOrchid 681 | .. data:: MediumPurple 682 | .. data:: BlueViolet 683 | .. data:: DarkViolet 684 | .. data:: DarkOrchid 685 | .. data:: DarkMagenta 686 | .. data:: Purple 687 | .. data:: Indigo 688 | .. data:: DarkSlateBlue 689 | .. data:: SlateBlue 690 | .. data:: MediumSlateBlue 691 | .. data:: GreenYellow 692 | .. data:: Chartreuse 693 | .. data:: LawnGreen 694 | .. data:: Lime 695 | .. data:: LimeGreen 696 | .. data:: PaleGreen 697 | .. data:: LightGreen 698 | .. data:: MediumSpringGreen 699 | .. data:: SpringGreen 700 | .. data:: MediumSeaGreen 701 | .. data:: SeaGreen 702 | .. data:: ForestGreen 703 | .. data:: Green 704 | .. data:: DarkGreen 705 | .. data:: YellowGreen 706 | .. data:: OliveDrab 707 | .. data:: Olive 708 | .. data:: DarkOliveGreen 709 | .. data:: MediumAquamarine 710 | .. data:: DarkSeaGreen 711 | .. data:: LightSeaGreen 712 | .. data:: DarkCyan 713 | .. data:: Teal 714 | .. data:: Aqua 715 | .. data:: Cyan 716 | .. data:: LightCyan 717 | .. data:: PaleTurquoise 718 | .. data:: Aquamarine 719 | .. data:: Turquoise 720 | .. data:: MediumTurquoise 721 | .. data:: DarkTurquoise 722 | .. data:: CadetBlue 723 | .. data:: SteelBlue 724 | .. data:: LightSteelBlue 725 | .. data:: PowderBlue 726 | .. data:: LightBlue 727 | .. data:: SkyBlue 728 | .. data:: LightSkyBlue 729 | .. data:: DeepSkyBlue 730 | .. data:: DodgerBlue 731 | .. data:: CornflowerBlue 732 | .. data:: RoyalBlue 733 | .. data:: Blue 734 | .. data:: MediumBlue 735 | .. data:: DarkBlue 736 | .. data:: Navy 737 | .. data:: MidnightBlue 738 | .. data:: Cornsilk 739 | .. data:: BlanchedAlmond 740 | .. data:: Bisque 741 | .. data:: NavajoWhite 742 | .. data:: Wheat 743 | .. data:: BurlyWood 744 | .. data:: Tan 745 | .. data:: RosyBrown 746 | .. data:: SandyBrown 747 | .. data:: Goldenrod 748 | .. data:: DarkGoldenrod 749 | .. data:: Peru 750 | .. data:: Chocolate 751 | .. data:: SaddleBrown 752 | .. data:: Sienna 753 | .. data:: Brown 754 | .. data:: Maroon 755 | .. data:: White 756 | .. data:: Snow 757 | .. data:: Honeydew 758 | .. data:: MintCream 759 | .. data:: Azure 760 | .. data:: AliceBlue 761 | .. data:: GhostWhite 762 | .. data:: WhiteSmoke 763 | .. data:: Seashell 764 | .. data:: Beige 765 | .. data:: OldLace 766 | .. data:: FloralWhite 767 | .. data:: Ivory 768 | .. data:: AntiqueWhite 769 | .. data:: Linen 770 | .. data:: LavenderBlush 771 | .. data:: MistyRose 772 | .. data:: Gainsboro 773 | .. data:: LightGrey 774 | .. data:: Silver 775 | .. data:: DarkGray 776 | .. data:: Gray 777 | .. data:: DimGray 778 | .. data:: LightSlateGray 779 | .. data:: SlateGray 780 | .. data:: DarkSlateGray 781 | .. data:: Black 782 | -------------------------------------------------------------------------------- /docs/dev/contribute.rst: -------------------------------------------------------------------------------- 1 | ######################## 2 | Contributing to Pymaging 3 | ######################## 4 | 5 | 6 | ********* 7 | Community 8 | ********* 9 | 10 | People interested in developing for the Pymaging should join the #pymaging 11 | IRC channel on `freenode`_ for help and to discuss the development. 12 | 13 | 14 | ***************** 15 | Contributing Code 16 | ***************** 17 | 18 | 19 | General 20 | ======= 21 | 22 | - Code **must** be tested. Untested patches will be declined. 23 | - If a patch affects the public facing API, it must document these changes. 24 | - If a patch changes code, the internal documentation (docs/dev/) must be updated to reflect the changes. 25 | 26 | Since we're hosted on GitHub, pymaging uses `git`_ as a version control system. 27 | 28 | If you're not familiar with git, check out the `GitHub help`_ page. 29 | 30 | 31 | Syntax and conventions 32 | ====================== 33 | 34 | We try to conform to `PEP8`_ as much as possible. This means 4 space 35 | indentation. 36 | 37 | 38 | Process 39 | ======= 40 | 41 | This is how you fix a bug or add a feature: 42 | 43 | #. `fork`_ us on GitHub. 44 | #. Checkout your fork. 45 | #. Hack hack hack, test test test, commit commit commit, test again. 46 | #. Push to your fork. 47 | #. Open a pull request. 48 | 49 | 50 | Tests 51 | ===== 52 | 53 | If you're unsure how to write tests, feel free to ask for help on IRC. 54 | 55 | Running the tests 56 | ----------------- 57 | 58 | To run the tests we recommend using ``nose``. If you have ``nose`` installed, 59 | just run ``nosetests`` in the root directory. If you don't, you can also use 60 | ``python -m unittest discover``. 61 | 62 | 63 | ************************** 64 | Contributing Documentation 65 | ************************** 66 | 67 | The documentation is written using `Sphinx`_/`restructuredText`_. 68 | 69 | Section style 70 | ============= 71 | 72 | We use Python documentation conventions fo section marking: 73 | 74 | * ``#`` with overline, for parts 75 | * ``*`` with overline, for chapters 76 | * ``=``, for sections 77 | * ``-``, for subsections 78 | * ``^``, for subsubsections 79 | * ``"``, for paragraphs 80 | 81 | 82 | .. _fork: http://github.com/ojii/pymaging 83 | .. _Sphinx: http://sphinx.pocoo.org/ 84 | .. _PEP8: http://www.python.org/dev/peps/pep-0008/ 85 | .. _GitHub : http://www.github.com 86 | .. _GitHub help : http://help.github.com 87 | .. _freenode : http://freenode.net/ 88 | .. _pull request : http://help.github.com/send-pull-requests/ 89 | .. _git : http://git-scm.com/ 90 | .. _restructuredText: http://docutils.sourceforge.net/docs/ref/rst/introduction.html 91 | 92 | -------------------------------------------------------------------------------- /docs/dev/formats.rst: -------------------------------------------------------------------------------- 1 | ############### 2 | Writing formats 3 | ############### 4 | 5 | Formats in pymaging are represented as :class:`pymaging.formats.Format` instances. To make your own format, create an 6 | instance of that class, giving a method to **decode**, a method to **encode** and a list of **extensions** for this 7 | format as arguments. 8 | 9 | ******* 10 | Decoder 11 | ******* 12 | 13 | The decoder function takes one file-like object as argument. It should return ``None`` if the file object passed in is 14 | not in the format handled by this decoder, otherwise it should return an instance of :class:`pymaging.image.Image`. For 15 | help with image objects, see :ref:`about-image-objects`. 16 | 17 | ******* 18 | Encoder 19 | ******* 20 | 21 | The encoder takes an instance of :class:`pymaging.image.Image` and a file-like object as arguments and should save the 22 | image to that file object. For help with image objects, see :ref:`about-image-objects`. 23 | -------------------------------------------------------------------------------- /docs/dev/overview.rst: -------------------------------------------------------------------------------- 1 | ######## 2 | Overview 3 | ######## 4 | 5 | 6 | .. _about-image-objects: 7 | 8 | ******************* 9 | About Image objects 10 | ******************* 11 | 12 | 13 | :class:`pymaging.image.Image` vs :class:`pymaging.image.LoadedImage` 14 | ==================================================================== 15 | 16 | There are two main classes for representing an image in pymaging, :class:`pymaging.image.Image` and 17 | :class:`pymaging.image.LoadedImage`. Other than their constructor their APIs are the same. The difference is that 18 | :class:`pymaging.image.Image` didn't load all the image data from the file yet, whereas 19 | :class:`pymaging.image.LoadedImage` did. As a general rule, any format should use :class:`pymaging.image.Image` so 20 | opening an image will first load it's metadata (width, height) before loading all the pixel data (which can consume 21 | large amounts of memory). This is useful for users who just want to verify that what they have is an image supported by 22 | pymaging and maybe want to know the dimensions of the image before loading it. 23 | 24 | :class:`pymaging.image.LoadedImage` should only be used if you have all the pixel data in memory anyway or if there's no 25 | way around loading all the data at first, as it's required to extract the meta information. 26 | 27 | 28 | About loaders 29 | ============= 30 | 31 | :class:`pymaging.image.Image` takes a loader callable which will be called to actually load the image data. This loader 32 | should return a tuple ``(pixel_array, palette)``. ``pixel_array` should be constructed with 33 | :func:`pymaging.pixelarray.get_pixel_array`, whereas ``palette`` should either be a palette (list of colors) or 34 | ``None``. 35 | 36 | 37 | ************ 38 | Pixel arrays 39 | ************ 40 | 41 | Pixel arrays are the core data structure in which image data is represented in pymaging. Their base class is 42 | :class:`pymaging.pixelarray.GenericPixelArray`, but in practice they use one of the specialized subclasses. In almost 43 | all cases, you should use :func:`pymaging.pixelarray.get_pixel_array` to construct pixel arrays, instead of using the 44 | classes directly. 45 | 46 | :func:`pymaging.pixelarray.get_pixel_array` takes the image **data** (as an :class:`array.array`, more on this later), 47 | the **width** (in pixels), **height** (in pixels) and the **pixel size** as arguments and returns a, if possible 48 | specialized, pixel array. 49 | 50 | 51 | Pixel size 52 | ========== 53 | 54 | The **pixel size** indicates how many bytes form a single pixel. It also describes how data is stored in the array 55 | passed into the pixel array. A pixel size of one indicates either an image with a palette (where the bytes in the image 56 | data are indices into the palette) or a monochrome image. Pixel size 3 is probably the most common and usually indicates 57 | RGB, whereas pixel size 4 indicates RGBA. 58 | 59 | Given the **pixel size**, the **data** passed into the pixel array is translated into pixels at x/y coordinates through 60 | the APIs on pixel array. 61 | 62 | .. module:: pymaging.pixelarray 63 | 64 | Important methods 65 | ================= 66 | 67 | You should hardly ever manipulate the ``data`` attribute on pixel arrays directly, instead, you should use the provided 68 | APIs that handle things like x/y translation for the given width, height and pixel size. 69 | 70 | Pixel array methods usually operate **in place**, if you wish to have a copy of the data, use ``copy()``. 71 | 72 | ``get(x, y)`` 73 | ------------- 74 | 75 | Returns the **pixel** (a list of ints) at the given position. 76 | 77 | ``set(x, y, pixel)`` 78 | -------------------- 79 | 80 | Sets the given pixel (list of ints) to the given position. 81 | 82 | ``remove_lines``, ``remove_columns``, ``add_lines`` and ``add_columns`` 83 | ----------------------------------------------------------------------- 84 | 85 | Those four methods are closely related and are used to resize a pixel array (and thus the image canvas). They all take 86 | two arguments: ``amount`` and ``offset``. 87 | 88 | .. warning:: 89 | 90 | There is an important performance caveat with those four methods. Manipulating columns (``add_columns`` and 91 | ``remove_columns``) is slower the more lines there are. Therefore the column manipulating methods should always be 92 | called **before add_lines** or **after remove_lines** to keep the amount of lines where columns are changed the 93 | lowest. 94 | -------------------------------------------------------------------------------- /docs/dev/setup.rst: -------------------------------------------------------------------------------- 1 | ##################### 2 | Setup for development 3 | ##################### 4 | 5 | Clone the git repository using ``git clone https://github.com/ojii/pymaging.git``. 6 | 7 | To run the tests, simply execute ``setup.py test`` with a Python version of your choice, or run ``./runtests.sh`` to run 8 | it against all installed (and supported) Python versions. 9 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Pymaging documentation master file, created by 2 | sphinx-quickstart on Mon Mar 12 10:51:31 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | #################################### 8 | Welcome to Pymaging's documentation! 9 | #################################### 10 | 11 | 12 | ***** 13 | About 14 | ***** 15 | 16 | Pymaging is a pure Python imaging library that is compatible both with Python 2 17 | and Python 3. 18 | 19 | 20 | ****************** 21 | User Documentation 22 | ****************** 23 | 24 | If you want to learn more about how to use pymaging, this part of the documentation is for you: 25 | 26 | 27 | .. toctree:: 28 | :maxdepth: 3 29 | 30 | /usr/installation 31 | /usr/quickstart 32 | 33 | 34 | *********************** 35 | Developer Documentation 36 | *********************** 37 | 38 | If you want to hack (or extend) pymaging, this part is for you: 39 | 40 | .. toctree:: 41 | :maxdepth: 3 42 | 43 | /dev/setup 44 | /dev/overview 45 | /dev/api 46 | /dev/formats 47 | /dev/contribute 48 | 49 | 50 | Indices and tables 51 | ================== 52 | 53 | * :ref:`genindex` 54 | * :ref:`modindex` 55 | * :ref:`search` 56 | 57 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Pymaging.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Pymaging.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /docs/usr/installation.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Installation 3 | ############ 4 | 5 | 6 | ************ 7 | Requirements 8 | ************ 9 | 10 | Python Compatiblity 11 | =================== 12 | 13 | Any of the following: 14 | 15 | * Python 2.6 or a higher 2.x release 16 | * Python 3.1 or higher 17 | * PyPy 1.8 or higher 18 | * Any other Python 2.6/Python 3.1 compatible Python implementation. 19 | 20 | 21 | Required Python packages 22 | ======================== 23 | 24 | * ``distribute`` 25 | 26 | 27 | ********** 28 | Installing 29 | ********** 30 | 31 | * Create a virtualenv (in theory this is optional, but just do it) 32 | * ``pip install -e git+git://github.com/ojii/pymaging.git#egg=pymaging`` 33 | * Install formats. (eg: ``pip install -e git+git://github.com/ojii/pymaging-png.git#egg=pymaging-png`` 34 | -------------------------------------------------------------------------------- /docs/usr/quickstart.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | Quickstart 3 | ########## 4 | 5 | 6 | ***************** 7 | Resizing an image 8 | ***************** 9 | 10 | Resizing ``myimage.png`` to 300x300 pixels and save it as ``resized.png``:: 11 | 12 | from pymaging import Image 13 | 14 | img = Image.open_from_path('myimage.png') 15 | img = img.resize(300, 300) 16 | img.save('resized.png') 17 | -------------------------------------------------------------------------------- /pymaging/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Jonas Obrist 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Jonas Obrist nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from pymaging.image import Image 27 | 28 | __version__ = '0.1' 29 | -------------------------------------------------------------------------------- /pymaging/affine.py: -------------------------------------------------------------------------------- 1 | from math import pi, cos, sin 2 | 3 | _IDENTITY = (1, 0, 0, 0, 1, 0, 0, 0, 1) 4 | 5 | 6 | class AffineTransform(object): 7 | """ 8 | 2-dimensional affine-transform implementation in pure python. 9 | 10 | Initialise with a tuple (a, b, c, d, e, f, g, h, i), representing 11 | the affine transform given by the following matrix: 12 | | a b c | 13 | | d e f | 14 | | g h i | 15 | """ 16 | 17 | def __init__(self, matrix=_IDENTITY): 18 | if len(matrix) == 3: 19 | # accept 3x3 tuples 20 | matrix = matrix[0] + matrix[1] + matrix[2] 21 | if len(matrix) != 9: 22 | raise ValueError("AffineTransform expects a 9-tuple, or a 3x3 tuple") 23 | self.matrix = tuple(matrix) 24 | 25 | def __repr__(self): 26 | return '%s(\n %g, %g, %g,\n %g, %g, %g,\n %g, %g, %g\n)' % ( 27 | (self.__class__.__name__,) + self.matrix 28 | ) 29 | 30 | def __eq__(self, other): 31 | if not hasattr(other, 'matrix'): 32 | return False 33 | return self.matrix == other.matrix 34 | 35 | def __mul__(self, other): 36 | """ 37 | Multiply this affine transformation by something. 38 | Accepts: 39 | * another AffineTransform: 40 | ``A * B`` 41 | * a scalar: 42 | ``A * 3`` 43 | """ 44 | if not isinstance(other, AffineTransform): 45 | if isinstance(other, (tuple, list)): 46 | if len(other) not in (2, 3): 47 | raise ValueError( 48 | "AffineTransform can only be multiplied by vectors of length 2 or 3" 49 | ) 50 | sm = self.matrix 51 | if len(other) == 3: 52 | return ( 53 | other[0] * sm[0] + other[1] * sm[3] + other[2] * sm[6], 54 | other[0] * sm[1] + other[1] * sm[4] + other[2] * sm[7], 55 | other[0] * sm[2] + other[1] * sm[5] + other[2] * sm[8], 56 | ) 57 | else: 58 | return ( 59 | other[0] * sm[0] + other[1] * sm[3] + sm[6], 60 | other[0] * sm[1] + other[1] * sm[4] + sm[7], 61 | ) 62 | # scalars: accept any arg we can convert to float 63 | try: 64 | s = float(other) 65 | except (ValueError, TypeError): 66 | # this will throw a TypeError (unsupported operand type) 67 | return NotImplemented 68 | # scalar multiplications are the same as scale matrix multiplications 69 | other = AffineTransform(( 70 | s, 0, 0, 71 | 0, s, 0, 72 | 0, 0, s, 73 | )) 74 | 75 | sm = self.matrix 76 | om = other.matrix 77 | return AffineTransform(( 78 | sm[0] * om[0] + sm[1] * om[3] + sm[2] * om[6], 79 | sm[0] * om[1] + sm[1] * om[4] + sm[2] * om[7], 80 | sm[0] * om[2] + sm[1] * om[5] + sm[2] * om[8], 81 | sm[3] * om[0] + sm[4] * om[3] + sm[5] * om[6], 82 | sm[3] * om[1] + sm[4] * om[4] + sm[5] * om[7], 83 | sm[3] * om[2] + sm[4] * om[5] + sm[5] * om[8], 84 | sm[6] * om[0] + sm[7] * om[3] + sm[8] * om[6], 85 | sm[6] * om[1] + sm[7] * om[4] + sm[8] * om[7], 86 | sm[6] * om[2] + sm[7] * om[5] + sm[8] * om[8], 87 | )) 88 | 89 | def __rmul__(self, other): 90 | if not isinstance(other, AffineTransform): 91 | if not isinstance(other, (tuple, list)): 92 | # support commutative multiplying for scalars 93 | # (i.e. 3 * A == A * 3) 94 | return self.__mul__(other) 95 | return NotImplemented 96 | 97 | def __truediv__(self, other): 98 | if isinstance(other, AffineTransform): 99 | return self * other.inverse() 100 | else: 101 | # scalar division 102 | return self * (1.0 / other) 103 | __div__ = __truediv__ 104 | 105 | def _determinant(self): 106 | """ 107 | Returns the determinant of this 3x3 matrix. 108 | """ 109 | # http://en.wikipedia.org/wiki/Inverse_matrix#Inversion_of_3.C3.973_matrices 110 | a, b, c, d, e, f, g, h, k = self.matrix 111 | return ( 112 | a * (e * k - f * h) 113 | - b * (k * d - f * g) 114 | + c * (d * h - e * g) 115 | ) 116 | 117 | def inverse(self): 118 | """ 119 | Returns an AffineTransform which represents this AffineTransform's inverse. 120 | """ 121 | det = self._determinant() 122 | if det == 0: 123 | # this can happen for instance if you divide by a transform whose 124 | # matrix is filled with zeroes. 125 | raise ValueError("This AffineTransform doesn't have a valid inverse.") 126 | 127 | # http://en.wikipedia.org/wiki/Inverse_matrix#Inversion_of_3.C3.973_matrices 128 | a, b, c, d, e, f, g, h, k = self.matrix 129 | return (1.0 / det) * AffineTransform(( 130 | e * k - f * h, c * h - b * k, b * f - c * e, 131 | f * g - d * k, a * k - c * g, c * d - a * f, 132 | d * h - e * g, g * b - a * h, a * e - b * d, 133 | )) 134 | 135 | def rotate(self, degrees, clockwise=False): 136 | """ 137 | Returns an AffineTransform which is rotated by the given number 138 | of degrees. Anticlockwise unless clockwise=True is given. 139 | """ 140 | degrees %= 360 141 | if clockwise: 142 | degrees = 360 - degrees 143 | theta = degrees * pi / 180.0 144 | 145 | # HACK: limited precision of floats means rotate() operations 146 | # often cause numbers like 1.2246467991473532e-16. 147 | # So we round() those to 15 decimal digits. Better solution welcome :/ 148 | rotation = AffineTransform(( 149 | round(cos(theta), 15), round(-sin(theta), 15), 0, 150 | round(sin(theta), 15), round(cos(theta), 15), 0, 151 | 0, 0, 1, 152 | )) 153 | return self * rotation 154 | 155 | def scale(self, x_factor, y_factor=None): 156 | if y_factor is None: 157 | y_factor = x_factor 158 | return self * AffineTransform(( 159 | x_factor, 0, 0, 160 | 0, y_factor, 0, 161 | 0, 0, 1, 162 | )) 163 | 164 | def translate(self, dx, dy): 165 | return self * AffineTransform(( 166 | 1, 0, 0, 167 | 0, 1, 0, 168 | dx, dy, 1, 169 | )) 170 | -------------------------------------------------------------------------------- /pymaging/colors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2012, Jonas Obrist 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the Jonas Obrist nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | from __future__ import division 27 | from collections import namedtuple 28 | 29 | 30 | def _mixin_alpha(colors, alpha): 31 | ratio = alpha / 255 32 | return [int(round(color * ratio)) for color in colors] 33 | 34 | class Color(object): 35 | __slots__ = 'red', 'green', 'blue', 'alpha' 36 | 37 | def __init__(self, red, green, blue, alpha=255): 38 | self.red = red 39 | self.green = green 40 | self.blue = blue 41 | self.alpha = alpha 42 | 43 | def __str__(self): 44 | return 'Color: r:%s, g:%s, b:%s, a:%s' % (self.red, self.green, self.blue, self.alpha) 45 | 46 | def __repr__(self): 47 | return '<%s>' % self 48 | 49 | def __hash__(self): 50 | return hash((self.red, self.green, self.blue, self.alpha)) 51 | 52 | def __eq__(self, other): 53 | return ( 54 | self.red == other.red and 55 | self.green == other.green and 56 | self.blue == other.blue and 57 | self.alpha == other.alpha 58 | ) 59 | 60 | @classmethod 61 | def from_pixel(cls, pixel): 62 | """ 63 | Convert a pixel (list of 3-4 values) to a Color instance. 64 | """ 65 | assert len(pixel) in (3,4), "Color.from_pixel only supports 3 and 4 value pixels" 66 | return cls(*map(int, list(pixel))) 67 | 68 | @classmethod 69 | def from_hexcode(cls, hexcode): 70 | """ 71 | Convert hexcode to RGB/RGBA. 72 | """ 73 | hexcode = hexcode.strip('#') 74 | assert len(hexcode) in (3,4,6,8), "Hex codes must be 3, 4, 6 or 8 characters long" 75 | if len(hexcode) in (3,4): 76 | hexcode = ''.join(x*2 for x in hexcode) 77 | return cls(*[int(''.join(x), 16) for x in zip(hexcode[::2], hexcode[1::2])]) 78 | 79 | def get_for_brightness(self, brightness): 80 | """ 81 | Brightness is a float between 0 and 1 82 | """ 83 | return Color(self.red, self.green, self.blue, int(round((self.alpha + 1) * brightness)) - 1) 84 | 85 | def cover_with(self, cover_color): 86 | """ 87 | Mix the two colors respecting their alpha value. 88 | 89 | Puts cover_color over itself compositing the colors using the alpha 90 | values. 91 | """ 92 | # fastpath for solid colors 93 | if cover_color.alpha == 255: 94 | return Color(cover_color.red, cover_color.green, cover_color.blue, cover_color.alpha) 95 | 96 | srca = cover_color.alpha / 255 97 | dsta = self.alpha / 255 98 | outa = srca + dsta * (1 - srca) 99 | 100 | srcr, srcg, srcb = cover_color.red, cover_color.green, cover_color.blue 101 | dstr, dstg, dstb = self.red, self.green, self.blue 102 | 103 | outr = (srcr * srca + dstr * dsta * (1 - srca)) / outa 104 | outg = (srcg * srca + dstg * dsta * (1 - srca)) / outa 105 | outb = (srcb * srca + dstb * dsta * (1 - srca)) / outa 106 | 107 | red = int(round(outr)) 108 | green = int(round(outg)) 109 | blue = int(round(outb)) 110 | alpha = int(round(outa * 255)) 111 | 112 | return Color(red, green, blue, alpha) 113 | 114 | 115 | def to_pixel(self, pixelsize): 116 | """ 117 | Convert to pixel (list of 3-4 values) 118 | """ 119 | assert pixelsize in (3,4), "Color.to_pixel only supports 3 and 4 value pixels" 120 | if pixelsize == 3: 121 | return _mixin_alpha([self.red, self.green, self.blue], self.alpha) 122 | else: 123 | return [self.red, self.green, self.blue, self.alpha] 124 | 125 | def to_hexcode(self): 126 | """ 127 | Convert to RGBA hexcode 128 | """ 129 | return ''.join(hex(x)[2:] for x in (self.red, self.green, self.blue, self.alpha)) 130 | 131 | 132 | ColorType = namedtuple('ColorType', 'length alpha') 133 | 134 | RGB = ColorType(3, False) 135 | RGBA = ColorType(4, True) 136 | -------------------------------------------------------------------------------- /pymaging/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2012, Jonas Obrist 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the Jonas Obrist nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | 28 | class PymagingException(Exception): pass 29 | 30 | class FormatNotSupported(PymagingException): pass 31 | class InvalidColor(PymagingException): pass 32 | -------------------------------------------------------------------------------- /pymaging/formats.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2012, Jonas Obrist 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the Jonas Obrist nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | from collections import namedtuple 27 | import threading 28 | 29 | Format = namedtuple('Format', 'open save extensions') 30 | 31 | 32 | 33 | class FormatRegistry(object): 34 | # Use the Borg pattern to share state between all instances. Details at 35 | # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531. 36 | __shared_state = dict( 37 | names = {}, 38 | formats = [], 39 | 40 | # -- Everything below here is only used when populating the registry -- 41 | loaded = False, 42 | write_lock = threading.RLock(), 43 | ) 44 | 45 | def __init__(self): 46 | self.__dict__ = self.__shared_state 47 | 48 | def _populate(self): 49 | if self.loaded: 50 | return 51 | with self.write_lock: 52 | import pkg_resources 53 | for entry_point in pkg_resources.iter_entry_points('pymaging.formats'): 54 | format = entry_point.load() 55 | self.register(format) 56 | self.loaded = True 57 | 58 | def register(self, format): 59 | self.formats.append(format) 60 | for extension in format.extensions: 61 | self.names[extension] = format 62 | 63 | def get_format_objects(self): 64 | self._populate() 65 | return self.formats 66 | 67 | def get_format(self, format): 68 | self._populate() 69 | return self.names.get(format, None) 70 | 71 | registry = FormatRegistry() 72 | get_format_objects = registry.get_format_objects 73 | get_format = registry.get_format 74 | register = registry.register 75 | -------------------------------------------------------------------------------- /pymaging/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2012, Jonas Obrist 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the Jonas Obrist nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | from math import ceil 27 | 28 | 29 | def get_transformed_dimensions(transform, box): 30 | """ 31 | Takes an affine transform and a four-tuple of (x0, y0, x1, y1) 32 | coordinates. 33 | Transforms each corner of the given box, and returns the 34 | (width, height) of the transformed box. 35 | """ 36 | (x0, y0, x1, y1) = box 37 | xs = [] 38 | ys = [] 39 | for corner in ( 40 | (x0, y0), 41 | (x1, y0), 42 | (x0, y1), 43 | (x1, y1), 44 | ): 45 | x, y = transform * corner 46 | xs.append(x) 47 | ys.append(y) 48 | 49 | return int(ceil(max(xs))) - int(min(xs)), int(ceil(max(ys))) - int(min(ys)) 50 | -------------------------------------------------------------------------------- /pymaging/image.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2012, Jonas Obrist 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the Jonas Obrist nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | import array 27 | from pymaging.colors import Color 28 | from pymaging.affine import AffineTransform 29 | from pymaging.exceptions import FormatNotSupported, InvalidColor 30 | from pymaging.formats import get_format, get_format_objects 31 | from pymaging.helpers import get_transformed_dimensions 32 | from pymaging.pixelarray import get_pixel_array 33 | from pymaging.resample import nearest 34 | import os 35 | 36 | 37 | 38 | class Image(object): 39 | def __init__(self, mode, width, height, loader, meta=None): 40 | self.mode = mode 41 | self.width = width 42 | self.height = height 43 | self.loader = loader 44 | self.meta = meta 45 | self._pixelarray = None 46 | self._palette = None 47 | self.reverse_palette = None 48 | 49 | @property 50 | def pixels(self): 51 | self.load() 52 | return self._pixelarray 53 | 54 | @property 55 | def pixelsize(self): 56 | return self.pixels.pixelsize 57 | 58 | @property 59 | def palette(self): 60 | self.load() 61 | return self._palette 62 | 63 | #========================================================================== 64 | # Constructors 65 | #========================================================================== 66 | 67 | @classmethod 68 | def open(cls, fileobj): 69 | for format in get_format_objects(): 70 | image = format.open(fileobj) 71 | if image: 72 | return image 73 | raise FormatNotSupported() 74 | 75 | @classmethod 76 | def open_from_path(cls, filepath): 77 | with open(filepath, 'rb') as fobj: 78 | return cls.open(fobj) 79 | 80 | @classmethod 81 | def new(cls, mode, width, height, background_color, palette=None, meta=None): 82 | color = background_color.to_pixel(mode.length) 83 | pixel_array = get_pixel_array(array.array('B', color) * width * height, width, height, mode.length) 84 | return LoadedImage(mode, width, height, pixel_array, palette=palette, meta=meta) 85 | 86 | def load(self): 87 | if self._pixelarray is not None: 88 | return 89 | self._pixelarray, self._palette = self.loader() 90 | 91 | #========================================================================== 92 | # Saving 93 | #========================================================================== 94 | 95 | def save(self, fileobj, format): 96 | format_object = get_format(format) 97 | if not format_object: 98 | raise FormatNotSupported(format) 99 | format_object.save(self, fileobj) 100 | 101 | def save_to_path(self, filepath, format=None): 102 | if not format: 103 | format = os.path.splitext(filepath)[1][1:] 104 | with open(filepath, 'wb') as fobj: 105 | self.save(fobj, format) 106 | 107 | #========================================================================== 108 | # Helpers 109 | #========================================================================== 110 | 111 | def get_reverse_palette(self): 112 | if self.reverse_palette is None: 113 | self._fill_reverse_palette() 114 | return self.reverse_palette 115 | 116 | def _fill_reverse_palette(self): 117 | self.reverse_palette = {} 118 | if not self.palette: 119 | return 120 | for index, color in enumerate(self.palette): 121 | color_obj = Color.from_pixel(color) 122 | color_obj.to_hexcode() 123 | self.reverse_palette[color_obj] = index 124 | 125 | def _copy(self, pixels, **kwargs): 126 | defaults = { 127 | 'mode': self.mode, 128 | 'width': self.width, 129 | 'height': self.height, 130 | 'palette': self.palette, 131 | 'meta': self.meta, 132 | } 133 | defaults.update(kwargs) 134 | defaults['pixels'] = pixels 135 | return LoadedImage(**defaults) 136 | 137 | #========================================================================== 138 | # Geometry Operations 139 | #========================================================================== 140 | 141 | def resize(self, width, height, resample_algorithm=nearest, resize_canvas=True): 142 | pixels = resample_algorithm.resize( 143 | self, width, height, resize_canvas=resize_canvas 144 | ) 145 | return self._copy(pixels) 146 | 147 | def affine(self, transform, resample_algorithm=nearest, resize_canvas=True): 148 | """ 149 | Returns a copy of this image transformed by the given 150 | AffineTransform. 151 | """ 152 | pixels = resample_algorithm.affine( 153 | self, 154 | transform, 155 | resize_canvas=resize_canvas, 156 | ) 157 | return self._copy(pixels) 158 | 159 | def rotate(self, degrees, clockwise=False, resample_algorithm=nearest, resize_canvas=True): 160 | """ 161 | Returns the image obtained by rotating this image by the 162 | given number of degrees. 163 | Anticlockwise unless clockwise=True is given. 164 | """ 165 | # translate to the origin first, then rotate, then translate back 166 | transform = AffineTransform() 167 | transform = transform.translate(self.width * -0.5, self.height * -0.5) 168 | transform = transform.rotate(degrees, clockwise=clockwise) 169 | 170 | width, height = self.width, self.height 171 | if resize_canvas: 172 | # determine new width 173 | width, height = get_transformed_dimensions(transform, (0, 0, width, height)) 174 | 175 | transform = transform.translate(width * 0.5, height * 0.5) 176 | 177 | pixels = resample_algorithm.affine(self, transform, resize_canvas=resize_canvas) 178 | 179 | return self._copy(pixels) 180 | 181 | def get_pixel(self, x, y): 182 | try: 183 | raw_pixel = self.pixels.get(x, y) 184 | except IndexError: 185 | raise IndexError("Pixel (%d, %d) not in image" % (x, y)) 186 | if self.pixelsize == 1 and self.palette: 187 | return self.palette[raw_pixel[0]] 188 | else: 189 | return raw_pixel 190 | 191 | def get_color(self, x, y): 192 | return Color.from_pixel(self.get_pixel(x, y)) 193 | 194 | def set_color(self, x, y, color): 195 | if color.alpha != 255: 196 | base = self.get_color(x, y) 197 | color = base.cover_with(color) 198 | if self.reverse_palette and self.pixelsize == 1: 199 | if color not in self.reverse_palette: 200 | raise InvalidColor(str(color)) 201 | index = self.reverse_palette[color] 202 | self.pixels.set(x, y, [index]) 203 | else: 204 | self.pixels.set(x, y, color.to_pixel(self.pixelsize)) 205 | 206 | def flip_top_bottom(self): 207 | """ 208 | Vertically flips the pixels of source into target 209 | """ 210 | pixels = self.pixels.copy_flipped_top_bottom() 211 | return self._copy(pixels) 212 | 213 | def flip_left_right(self): 214 | """ 215 | Horizontally flips the pixels of source into target 216 | """ 217 | return self._copy(pixels=self.pixels.copy_flipped_left_right()) 218 | 219 | def crop(self, width, height, padding_top, padding_left): 220 | new_pixels = self.pixels.copy() 221 | new_pixels.remove_lines(0, padding_top) 222 | new_pixels.remove_lines(height, new_pixels.height - height) 223 | new_pixels.remove_columns(0, padding_left) 224 | new_pixels.remove_columns(width, new_pixels.width - width) 225 | return self._copy(new_pixels, width=width, height=height) 226 | 227 | #========================================================================== 228 | # Manipulation 229 | #========================================================================== 230 | 231 | def draw(self, shape, color): 232 | for x, y, pixelcolor in shape.iter_pixels(color): 233 | self.set_color(x, y, pixelcolor) 234 | 235 | def blit(self, padding_top, padding_left, image): 236 | """ 237 | Puts the image given on top of this image with the given padding 238 | """ 239 | # there *must* be a better/faster way to do this: 240 | # TODO: check that palettes etc match. 241 | # TODO: fastpath this by copying the array if pixelsize is identical/palette is the same 242 | for x in range(min([image.width, self.width - padding_left])): 243 | for y in range(min([image.height, self.height- padding_top])): 244 | self.set_color(padding_left + x, padding_top + y, image.get_color(x, y)) 245 | 246 | 247 | 248 | class LoadedImage(Image): 249 | def __init__(self, mode, width, height, pixels, palette=None, meta=None): 250 | self.mode = mode 251 | self.width = width 252 | self.height = height 253 | self.format = format 254 | self.loader = lambda:None 255 | self.meta = meta 256 | self._pixelarray = pixels 257 | self._palette = palette 258 | self.reverse_palette = None -------------------------------------------------------------------------------- /pymaging/pixelarray.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import array 3 | import copy 4 | 5 | 6 | class GenericPixelArray(object): 7 | def __init__(self, data, width, height, pixelsize): 8 | self.data = data 9 | self.width = width 10 | self.height = height 11 | self.pixelsize = pixelsize 12 | self._precalculate() 13 | 14 | def __repr__(self): 15 | nice_pixels = '\n'.join([' '.join(['%%%dd' % self.pixelsize % x for x in self.data[i * self.line_length:(i + 1) * self.line_length]]) for i in range(self.height)]) 16 | return '\n%s\n(width=%s, height=%s, pixelsize=%s)' % (nice_pixels, self.width, self.height, self.pixelsize) 17 | 18 | def __eq__(self, other): 19 | """ 20 | Mostly used for testing 21 | """ 22 | return self.data == other.data and self.width == other.width and self.height == other.height and self.pixelsize == other.pixelsize 23 | 24 | def _precalculate(self): 25 | """ 26 | Precalculate some values, this must be called whenever self.width, self.height or self.pixelsize is changed. 27 | """ 28 | self.line_length = self.width * self.pixelsize 29 | self.size = self.line_length * self.height 30 | 31 | def _translate(self, x, y): 32 | """ 33 | Translates a x/y coordinate into the start index. 34 | """ 35 | return (y * self.line_length) + (x * self.pixelsize) 36 | 37 | def get(self, x, y): 38 | """ 39 | Returns the pixel (a tuple of length `self.pixelsize`) from the pixel array at x/y 40 | """ 41 | start = self._translate(x, y) 42 | return [self.data[start+i] for i in range(self.pixelsize)] 43 | 44 | def set(self, x, y, pixel): 45 | """ 46 | Sets the pixel (a tuple of length `self.pixelsize`) to the pixel array at x/y 47 | """ 48 | start = self._translate(x, y) 49 | for i in range(self.pixelsize): 50 | self.data[start + i] = pixel[i] 51 | 52 | def copy_flipped_top_bottom(self): 53 | """ 54 | Flip the lines from top to bottom into a new copy of this pixel array 55 | """ 56 | newarr = array.array('B', [0]) * self.size 57 | for i in range(self.height): 58 | dst_start = i * self.line_length 59 | dst_end = dst_start + self.line_length 60 | src_start = ((self.height - i) * self.line_length) - self.line_length 61 | src_end = src_start + self.line_length 62 | newarr[dst_start:dst_end] = self.data[src_start:src_end] 63 | return get_pixel_array(newarr, self.width, self.height, self.pixelsize) 64 | 65 | def copy_flipped_left_right(self): 66 | """ 67 | Flip the lines from left to right into a new copy of this pixel array 68 | """ 69 | new_pixel_array = get_pixel_array(array.array('B', [0]) * self.size, 70 | self.width, self.height, self.pixelsize) 71 | for y in range(self.height): 72 | for dst_x in range(self.width): 73 | src_x = self.width - dst_x - 1 74 | new_pixel_array.set(dst_x, y, self.get(src_x, y)) 75 | return new_pixel_array 76 | 77 | def copy(self): 78 | return get_pixel_array(copy.copy(self.data), self.width, self.height, self.pixelsize) 79 | 80 | def remove_lines(self, offset, amount): 81 | """ 82 | Removes `amount` lines from the pixel array starting at line `offset`. 83 | """ 84 | if not amount: 85 | return 86 | start = self.line_length * offset 87 | end = start + (amount * self.line_length) 88 | self.height -= amount 89 | del self.data[start:end] 90 | self._precalculate() 91 | 92 | def remove_columns(self, offset, amount): 93 | """ 94 | Removes `amount` columns from the pixel array starting at column `offset`. 95 | """ 96 | if not amount: 97 | return 98 | start = offset * self.pixelsize 99 | end = start + (amount * self.pixelsize) 100 | # reversed is used because otherwise line_start would be all messed up. 101 | for i in reversed(range(self.height)): 102 | line_start = i * self.line_length 103 | del self.data[line_start+start:line_start+end] 104 | self.width -= amount 105 | self._precalculate() 106 | 107 | def add_lines(self, offset, amount, fill=0): 108 | """ 109 | Adds `amount` lines to the pixel array starting at `offset` and fills them with `fill`. 110 | """ 111 | if not amount: 112 | return 113 | # special case for adding to the end of the array: 114 | if offset == self.height: 115 | self.data.extend(array.array(self.data.typecode, [fill] * self.line_length * amount)) 116 | else: 117 | start = offset * self.line_length 118 | self.data[start:start] = array.array(self.data.typecode, [fill] * self.line_length * amount) 119 | self.height += amount 120 | self._precalculate() 121 | 122 | def add_columns(self, offset, amount, fill=0): 123 | """ 124 | Adds `amount` columns to the pixel array starting at `offset` and fills them with `fill`. 125 | """ 126 | if not amount: 127 | return 128 | start = offset * self.pixelsize 129 | for i in reversed(range(self.height)): 130 | line_start = (i * self.line_length) + start 131 | self.data[line_start:line_start] = array.array(self.data.typecode, [fill] * self.pixelsize * amount) 132 | self.width += amount 133 | self._precalculate() 134 | 135 | 136 | class PixelArray1(GenericPixelArray): 137 | def __init__(self, data, width, height): 138 | super(PixelArray1, self).__init__(data, width, height, 1) 139 | 140 | def get(self, x, y): 141 | """ 142 | Returns the pixel (a tuple of length `self.pixelsize`) from the pixel array at x/y 143 | """ 144 | start = self._translate(x, y) 145 | return [self.data[start]] 146 | 147 | def set(self, x, y, pixel): 148 | """ 149 | Sets the pixel (a tuple of length `self.pixelsize`) to the pixel array at x/y 150 | """ 151 | start = self._translate(x, y) 152 | self.data[start] = pixel[0] 153 | 154 | 155 | class PixelArray2(GenericPixelArray): 156 | def __init__(self, data, width, height): 157 | super(PixelArray2, self).__init__(data, width, height, 2) 158 | 159 | def get(self, x, y): 160 | """ 161 | Returns the pixel (a tuple of length `self.pixelsize`) from the pixel array at x/y 162 | """ 163 | start = self._translate(x, y) 164 | return [self.data[start], self.data[start + 1]] 165 | 166 | def set(self, x, y, pixel): 167 | """ 168 | Sets the pixel (a tuple of length `self.pixelsize`) to the pixel array at x/y 169 | """ 170 | start = self._translate(x, y) 171 | self.data[start] = pixel[0] 172 | self.data[start + 1] = pixel[1] 173 | 174 | 175 | class PixelArray3(GenericPixelArray): 176 | def __init__(self, data, width, height): 177 | super(PixelArray3, self).__init__(data, width, height, 3) 178 | 179 | def get(self, x, y): 180 | """ 181 | Returns the pixel (a tuple of length `self.pixelsize`) from the pixel array at x/y 182 | """ 183 | start = self._translate(x, y) 184 | return [self.data[start], self.data[start + 1], self.data[start + 2]] 185 | 186 | def set(self, x, y, pixel): 187 | """ 188 | Sets the pixel (a tuple of length `self.pixelsize`) to the pixel array at x/y 189 | """ 190 | start = self._translate(x, y) 191 | self.data[start] = pixel[0] 192 | self.data[start + 1] = pixel[1] 193 | self.data[start + 2] = pixel[2] 194 | 195 | 196 | class PixelArray4(GenericPixelArray): 197 | def __init__(self, data, width, height): 198 | super(PixelArray4, self).__init__(data, width, height, 4) 199 | 200 | def get(self, x, y): 201 | """ 202 | Returns the pixel (a tuple of length `self.pixelsize`) from the pixel array at x/y 203 | """ 204 | start = self._translate(x, y) 205 | return [self.data[start], self.data[start + 1], self.data[start + 2], self.data[start + 3]] 206 | 207 | def set(self, x, y, pixel): 208 | """ 209 | Sets the pixel (a tuple of length `self.pixelsize`) to the pixel array at x/y 210 | """ 211 | start = self._translate(x, y) 212 | self.data[start] = pixel[0] 213 | self.data[start + 1] = pixel[1] 214 | self.data[start + 2] = pixel[2] 215 | self.data[start + 3] = pixel[3] 216 | 217 | 218 | def get_pixel_array(data, width, height, pixelsize): 219 | if pixelsize == 1: 220 | return PixelArray1(data, width, height) 221 | elif pixelsize == 2: 222 | return PixelArray2(data, width, height) 223 | elif pixelsize == 3: 224 | return PixelArray3(data, width, height) 225 | elif pixelsize == 4: 226 | return PixelArray4(data, width, height) 227 | else: 228 | return GenericPixelArray(data, width, height, pixelsize) 229 | -------------------------------------------------------------------------------- /pymaging/resample.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2012, Jonas Obrist 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the Jonas Obrist nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | from abc import ABCMeta, abstractmethod 27 | 28 | from pymaging.pixelarray import get_pixel_array 29 | 30 | __all__ = ('nearest', 'bilinear', 'Resampler') 31 | 32 | from pymaging.affine import AffineTransform 33 | from pymaging.helpers import get_transformed_dimensions 34 | from pymaging.utils import fdiv 35 | import array 36 | 37 | 38 | class Resampler(object): 39 | __metaclass__ = ABCMeta 40 | 41 | @abstractmethod 42 | def _get_value(self, source, source_x, source_y, dx, dy): 43 | pass 44 | 45 | def affine(self, source, transform, resize_canvas=True): 46 | if resize_canvas: 47 | # get image dimensions 48 | width, height = get_transformed_dimensions( 49 | transform, 50 | (0, 0, source.width, source.height) 51 | ) 52 | else: 53 | width = source.width 54 | height = source.height 55 | 56 | pixelsize = source.pixelsize 57 | 58 | # transparent or black background 59 | background = [0] * pixelsize 60 | 61 | # we want to go from dest coords to src coords: 62 | transform = transform.inverse() 63 | 64 | # Optimisation: 65 | # Because affine transforms have no perspective component, 66 | # the *gradient* of each source row/column must be constant. 67 | # So, we can calculate the source coordinates for each corner, 68 | # and then interpolate for each pixel, instead of doing a 69 | # matrix multiplication for each pixel. 70 | 71 | x_range = range(width) 72 | y_range = range(height) 73 | new_array = source.pixels.copy() 74 | 75 | for y in y_range: 76 | # the 0.5's mean we use the center of each pixel 77 | row_x0, row_y0 = transform * (0.5, y + 0.5) 78 | row_x1, row_y1 = transform * (width + 0.5, y + 0.5) 79 | 80 | dx = float(row_x1 - row_x0) / source.width 81 | dy = float(row_y1 - row_y0) / source.width 82 | 83 | for x in x_range: 84 | source_x = int(row_x0 + dx * x) 85 | source_y = int(row_y0 + dy * x) 86 | 87 | new_array.set(x, y, 88 | self._get_value(source, source_x, source_y, dx, dy) 89 | or background 90 | ) 91 | return new_array 92 | 93 | def resize(self, source, width, height, resize_canvas=True): 94 | transform = AffineTransform().scale( 95 | width / float(source.width), 96 | height / float(source.height) 97 | ) 98 | return self.affine(source, transform, resize_canvas=resize_canvas) 99 | 100 | 101 | class Nearest(Resampler): 102 | 103 | def _get_value(self, source, source_x, source_y, dx, dy): 104 | if source_x < 0 or source_y < 0 or \ 105 | source_x >= source.width or source_y >= source.height: 106 | return None 107 | else: 108 | return source.pixels.get(source_y, source_x) 109 | 110 | def resize(self, source, width, height, resize_canvas=True): 111 | if not resize_canvas: 112 | # this optimised implementation doesn't deal with this. 113 | # so delegate to affine() 114 | return super(Nearest, self).resize( 115 | source, width, height, resize_canvas=resize_canvas 116 | ) 117 | pixels = array.array('B') 118 | pixelsize = source.pixelsize 119 | 120 | x_ratio = fdiv(source.width, width) # get the x-axis ratio 121 | y_ratio = fdiv(source.height, height) # get the y-axis ratio 122 | 123 | y_range = range(height) # an iterator over the indices of all lines (y-axis) 124 | x_range = range(width) # an iterator over the indices of all rows (x-axis) 125 | for y in y_range: 126 | y += 0.5 # use the center of each pixel 127 | source_y = int(y * y_ratio) # get the source line 128 | for x in x_range: 129 | x += 0.5 # use the center of each pixel 130 | source_x = int(x * x_ratio) # get the source row 131 | pixels.extend(source.pixels.get(source_x, source_y)) 132 | return get_pixel_array(pixels, width, height, pixelsize) 133 | 134 | 135 | class Bilinear(Resampler): 136 | 137 | def _get_value(self, source, source_x, source_y, dx, dy): 138 | if source_x < 0 or source_y < 0 or \ 139 | source_x >= source.width or source_y >= source.height: 140 | return None 141 | 142 | source_y_i = int(source_y) 143 | source_x_i = int(source_x) 144 | 145 | weight_y0 = 1 - abs(source_y - source_y_i) 146 | weight_x0 = 1 - abs(source_x - source_x_i) 147 | 148 | pixelsize = source.pixelsize 149 | channel_sums = [0.0] * pixelsize 150 | has_alpha = source.mode.alpha 151 | color_channels_range = range(pixelsize - 1 if has_alpha else pixelsize) 152 | 153 | # populate <=4 nearest src_pixels, taking care not to go off 154 | # the edge of the image. 155 | src_pixels = [source.get_pixel(source_x_i, source_y_i), None, None, None] 156 | next_x = int(source_x + dx) 157 | next_y = int(source_x + dy) 158 | 159 | if next_x < source.width and next_x >= 0: 160 | src_pixels[1] = source.get_pixel(int(next_x), source_y_i) 161 | else: 162 | weight_x0 = 1 163 | if next_y < source.height and next_y >= 0: 164 | src_pixels[2] = source.get_pixel(source_x_i, next_y) 165 | if next_x < source.width and next_x >= 0: 166 | src_pixels[3] = source.get_pixel(next_x, next_y) 167 | else: 168 | weight_y0 = 1 169 | 170 | for i, src_pixel in enumerate(src_pixels): 171 | if src_pixel is None: 172 | continue 173 | weight_x = (1 - weight_x0) if (i % 2) else weight_x0 174 | weight_y = (1 - weight_y0) if (i // 2) else weight_y0 175 | alpha_weight = weight_x * weight_y 176 | color_weight = alpha_weight 177 | alpha = 255 178 | if has_alpha: 179 | alpha = src_pixel[-1] 180 | if not alpha: 181 | continue 182 | color_weight *= (alpha / 255.0) 183 | for channel_index, channel_value in zip(color_channels_range, src_pixel): 184 | channel_sums[channel_index] += color_weight * channel_value 185 | 186 | if has_alpha: 187 | channel_sums[-1] += alpha_weight * alpha 188 | if has_alpha: 189 | total_alpha_multiplier = channel_sums[-1] / 255.0 190 | if total_alpha_multiplier: # (avoid div/0) 191 | for channel_index in color_channels_range: 192 | channel_sums[channel_index] /= total_alpha_multiplier 193 | 194 | return [int(round(s)) for s in channel_sums] 195 | 196 | def resize(self, source, width, height, resize_canvas=True): 197 | if not resize_canvas: 198 | # this optimised implementation doesn't deal with this. 199 | # so delegate to affine() 200 | return super(Bilinear, self).resize( 201 | source, width, height, resize_canvas=resize_canvas 202 | ) 203 | x_ratio = fdiv(source.width, width) # get the x-axis ratio 204 | y_ratio = fdiv(source.height, height) # get the y-axis ratio 205 | pixelsize = source.pixelsize 206 | pixels = array.array('B') 207 | 208 | if source.palette: 209 | raise NotImplementedError("Resampling of paletted images is not yet supported") 210 | 211 | if x_ratio < 1 and y_ratio < 1: 212 | if not (width % source.width) and not (height % source.height): 213 | # optimisation: if doing a perfect upscale, 214 | # can just use nearest neighbor (it's much faster) 215 | return nearest.resize(source, width, height) 216 | 217 | has_alpha = source.mode.alpha 218 | color_channels_range = range(pixelsize - 1 if has_alpha else pixelsize) 219 | 220 | y_range = range(height) # an iterator over the indices of all lines (y-axis) 221 | x_range = range(width) # an iterator over the indices of all rows (x-axis) 222 | for y in y_range: 223 | src_y = (y + 0.5) * y_ratio - 0.5 # use the center of each pixel 224 | src_y_i = int(src_y) 225 | 226 | weight_y0 = 1 - abs(src_y - src_y_i) 227 | 228 | for x in x_range: 229 | src_x = (x + 0.5) * x_ratio - 0.5 230 | src_x_i = int(src_x) 231 | 232 | weight_x0 = 1 - abs(src_x - src_x_i) 233 | 234 | channel_sums = [0.0] * pixelsize 235 | 236 | # populate <=4 nearest src_pixels, taking care not to go off 237 | # the edge of the image. 238 | src_pixels = [source.get_color(src_y_i, src_x_i), None, None, None] 239 | if src_x_i + 1 < source.width: 240 | src_pixels[1] = source.get_color(src_y_i, src_x_i + 1) 241 | else: 242 | weight_x0 = 1 243 | if src_y_i + 1 < source.height: 244 | src_pixels[2] = source.get_color(src_y_i + 1, src_x_i) 245 | if src_x_i + 1 < source.height: 246 | src_pixels[3] = source.get_color(src_y_i + 1, src_x_i + 1) 247 | else: 248 | weight_y0 = 1 249 | 250 | for i, src_pixel in enumerate(src_pixels): 251 | if src_pixel is None: 252 | continue 253 | src_pixel = src_pixel.to_pixel(pixelsize) 254 | weight_x = (1 - weight_x0) if (i % 2) else weight_x0 255 | weight_y = (1 - weight_y0) if (i // 2) else weight_y0 256 | alpha_weight = weight_x * weight_y 257 | color_weight = alpha_weight 258 | alpha = 255 259 | if has_alpha: 260 | alpha = src_pixel[-1] 261 | if not alpha: 262 | continue 263 | color_weight *= (alpha / 255.0) 264 | for channel_index, channel_value in zip(color_channels_range, src_pixel): 265 | channel_sums[channel_index] += color_weight * channel_value 266 | 267 | if has_alpha: 268 | channel_sums[-1] += alpha_weight * alpha 269 | if has_alpha: 270 | total_alpha_multiplier = channel_sums[-1] / 255.0 271 | if total_alpha_multiplier: # (avoid div/0) 272 | for channel_index in color_channels_range: 273 | channel_sums[channel_index] /= total_alpha_multiplier 274 | pixels.extend([int(round(s)) for s in channel_sums]) 275 | return get_pixel_array(pixels, width, height, pixelsize) 276 | 277 | 278 | nearest = Nearest() 279 | bilinear = Bilinear() 280 | -------------------------------------------------------------------------------- /pymaging/shapes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2012, Jonas Obrist 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the Jonas Obrist nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | from pymaging.utils import fdiv 28 | import math 29 | 30 | 31 | class BaseShape(object): 32 | def iter_pixels(self, color): 33 | raise StopIteration() 34 | 35 | 36 | class Pixel(BaseShape): 37 | def __init__(self, x, y): 38 | self.x = x 39 | self.y = y 40 | 41 | def iter_pixels(self, color): 42 | yield self.x, self.y, color 43 | 44 | 45 | class Line(Pixel): 46 | """ 47 | Use Bresenham Line Algorithm (http://en.wikipedia.org/wiki/Bresenham's_line_algorithm): 48 | 49 | function line(x0, x1, y0, y1) 50 | boolean steep := abs(y1 - y0) > abs(x1 - x0) 51 | if steep then 52 | swap(x0, y0) 53 | swap(x1, y1) 54 | if x0 > x1 then 55 | swap(x0, x1) 56 | swap(y0, y1) 57 | int deltax := x1 - x0 58 | int deltay := abs(y1 - y0) 59 | real error := 0 60 | real deltaerr := deltay / deltax 61 | int ystep 62 | int y := y0 63 | if y0 < y1 then ystep := 1 else ystep := -1 64 | for x from x0 to x1 65 | if steep then plot(y,x) else plot(x,y) 66 | error := error + deltaerr 67 | if error ≥ 0.5 then 68 | y := y + ystep 69 | error := error - 1.0 70 | """ 71 | def __init__(self, start_x, start_y, end_x, end_y): 72 | self.start_x = start_x 73 | self.start_y = start_y 74 | self.end_x = end_x 75 | self.end_y = end_y 76 | steep = abs(self.end_y - self.start_y) > abs(self.end_x - self.start_x) 77 | if steep: 78 | x0, y0 = self.start_y, self.start_x 79 | x1, y1 = self.end_y, self.end_x 80 | else: 81 | x0, y0 = self.start_x, self.start_y 82 | x1, y1 = self.end_x, self.end_y 83 | if x0 > x1: 84 | x0, x1 = x1, x0 85 | y0, y1 = y1, y0 86 | 87 | self.x0, self.x1, self.y0, self.y1 = x0, x1, y0, y1 88 | 89 | delta_x = x1 - x0 90 | delta_y = abs(y1 - y0) 91 | self.error = 0.0 92 | self.delta_error = fdiv(delta_y, delta_x) 93 | if y0 < y1: 94 | self.ystep = 1 95 | else: 96 | self.ystep = -1 97 | 98 | self.y = y0 99 | 100 | self.iterator = self.steep_iterator if steep else self.normal_iterator 101 | 102 | def steep_iterator(self): 103 | for x in range(self.x0, self.x1): 104 | yield self.y, x 105 | self.shift() 106 | yield self.y1, self.x1 107 | 108 | def normal_iterator(self): 109 | for x in range(self.x0, self.x1): 110 | yield x, self.y 111 | self.shift() 112 | yield self.x1, self.y1 113 | 114 | def shift(self): 115 | self.error += self.delta_error 116 | if self.error >= 0.5: 117 | self.y += self.ystep 118 | self.error -= 1.0 119 | 120 | def iter_pixels(self, color): 121 | for x, y in self.iterator(): 122 | yield x, y, color 123 | 124 | 125 | # For AntiAliasedLin 126 | _round = lambda x: int(round(x)) 127 | _ipart = int # integer part of x 128 | _fpart = lambda x: x - math.floor(x) # fractional part of x 129 | _rfpart = lambda x: 1 - _fpart(x) 130 | 131 | class AntiAliasedLine(Line): 132 | def __init__(self, start_x, start_y, end_x, end_y): 133 | self.start_x = start_x 134 | self.start_y = start_y 135 | self.end_x = end_x 136 | self.end_y = end_y 137 | 138 | def iter_pixels(self, color): 139 | """ 140 | Use Xiaolin Wu's line algorithm: http://en.wikipedia.org/wiki/Xiaolin_Wu%27s_line_algorithm 141 | 142 | function plot(x, y, c) is 143 | plot the pixel at (x, y) with brightness c (where 0 ≤ c ≤ 1) 144 | 145 | function ipart(x) is 146 | return integer part of x 147 | 148 | function round(x) is 149 | return ipart(x + 0.5) 150 | 151 | function fpart(x) is 152 | return fractional part of x 153 | 154 | function rfpart(x) is 155 | return 1 - fpart(x) 156 | 157 | function drawLine(x1,y1,x2,y2) is 158 | dx = x2 - x1 159 | dy = y2 - y1 160 | if abs(dx) < abs(dy) then 161 | swap x1, y1 162 | swap x2, y2 163 | swap dx, dy 164 | end if 165 | if x2 < x1 166 | swap x1, x2 167 | swap y1, y2 168 | end if 169 | gradient = dy / dx 170 | 171 | // handle first endpoint 172 | xend = round(x1) 173 | yend = y1 + gradient * (xend - x1) 174 | xgap = rfpart(x1 + 0.5) 175 | xpxl1 = xend // this will be used in the main loop 176 | ypxl1 = ipart(yend) 177 | plot(xpxl1, ypxl1, rfpart(yend) * xgap) 178 | plot(xpxl1, ypxl1 + 1, fpart(yend) * xgap) 179 | intery = yend + gradient // first y-intersection for the main loop 180 | 181 | // handle second endpoint 182 | xend = round (x2) 183 | yend = y2 + gradient * (xend - x2) 184 | xgap = fpart(x2 + 0.5) 185 | xpxl2 = xend // this will be used in the main loop 186 | ypxl2 = ipart (yend) 187 | plot (xpxl2, ypxl2, rfpart (yend) * xgap) 188 | plot (xpxl2, ypxl2 + 1, fpart (yend) * xgap) 189 | 190 | // main loop 191 | for x from xpxl1 + 1 to xpxl2 - 1 do 192 | plot (x, ipart (intery), rfpart (intery)) 193 | plot (x, ipart (intery) + 1, fpart (intery)) 194 | intery = intery + gradient 195 | end function 196 | """ 197 | def _plot(x, y, c): 198 | """ 199 | plot the pixel at (x, y) with brightness c (where 0 ≤ c ≤ 1) 200 | """ 201 | return int(x), int(y), color.get_for_brightness(c) 202 | dx = self.end_x - self.start_x 203 | dy = self.end_y - self.start_y 204 | x1, x2, y1, y2 = self.start_x, self.end_x, self.start_y, self.end_y 205 | if abs(dx) > abs(dy): 206 | x1, y1 = y1, x1 207 | x2, y2 = y2, x2 208 | dx, dy = dy, dx 209 | if x2 < x1: 210 | x1, x2 = x2, x1 211 | y1, y2 = y2, y1 212 | 213 | gradient = fdiv(dy, dx) 214 | 215 | xend = round(x1) 216 | yend = y1 + gradient * (xend - x1) 217 | xgap = _rfpart(x1 + 0.5) 218 | xpxl1 = xend 219 | ypxl1 = _ipart(yend) 220 | yield _plot(xpxl1, ypxl1, _rfpart(yend) * xgap) 221 | yield _plot(xpxl1, ypxl1 + 1, _fpart(yend) * xgap) 222 | 223 | intery = yend + gradient 224 | 225 | xend = _round(x2) 226 | yend = y2 + gradient * (xend - x2) 227 | xgap = _fpart(x2 + 0.5) 228 | xpxl2 = xend 229 | ypxl2 = _ipart(yend) 230 | yield _plot(xpxl2, ypxl2, _rfpart(yend) * xgap) 231 | yield _plot(xpxl2, ypxl2 + 1, _fpart(yend) * xgap) 232 | 233 | for x in range (xpxl1 + 1, xpxl2 - 1): 234 | yield _plot(x, _ipart(intery), _rfpart(intery)) 235 | yield _plot(x, _ipart(intery) + 1, _fpart(intery)) 236 | intery += gradient 237 | -------------------------------------------------------------------------------- /pymaging/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import array 4 | from pymaging.colors import ColorType 5 | from pymaging.image import Image 6 | from pymaging.pixelarray import get_pixel_array 7 | 8 | def pixel_array_factory(colors, alpha=True): 9 | height = len(colors) 10 | width = len(colors[0]) if height else 0 11 | pixel_size = 4 if alpha else 3 12 | pixel_array = get_pixel_array(array.array('B', [0] * width * height * pixel_size), width, height, pixel_size) 13 | for y in range(height): 14 | for x in range(width): 15 | pixel_array.set(x, y, colors[y][x].to_pixel(pixel_size)) 16 | return pixel_array 17 | 18 | def image_factory(colors, alpha=True): 19 | height = len(colors) 20 | width = len(colors[0]) if height else 0 21 | pixel_size = 4 if alpha else 3 22 | pixel_array = pixel_array_factory(colors, alpha) 23 | def loader(): 24 | return pixel_array, None 25 | return Image(ColorType(pixel_size, alpha), width, height, loader) 26 | 27 | 28 | if hasattr(unittest.TestCase, 'assertIsInstance'): 29 | class _Compat: pass 30 | else: 31 | class _Compat: 32 | def assertIsInstance(self, obj, cls, msg=None): 33 | if not isinstance(obj, cls): 34 | standardMsg = '%s is not an instance of %r' % (safe_repr(obj), cls) 35 | self.fail(self._formatMessage(msg, standardMsg)) 36 | 37 | 38 | class PymagingBaseTestCase(unittest.TestCase, _Compat): 39 | def assertImage(self, img, colors, alpha=True): 40 | check = image_factory(colors, alpha) 41 | self.assertEqual(img.pixels, check.pixels) 42 | 43 | -------------------------------------------------------------------------------- /pymaging/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojii/pymaging/596a08fce5664e58d6e8c96847393fbe987783f2/pymaging/tests/__init__.py -------------------------------------------------------------------------------- /pymaging/tests/test_affine.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pymaging.affine import AffineTransform 4 | 5 | 6 | class TestAffineTransform(unittest.TestCase): 7 | ## constructors 8 | def test_constructor_identity(self): 9 | a = AffineTransform() 10 | self.assertEqual(a.matrix, (1, 0, 0, 0, 1, 0, 0, 0, 1)) 11 | 12 | def test_constructor_9tuple(self): 13 | a = AffineTransform((1, 2, 3, 4, 5, 6, 7, 8, 9)) 14 | self.assertEqual(a.matrix, (1, 2, 3, 4, 5, 6, 7, 8, 9)) 15 | 16 | self.assertRaises(ValueError, AffineTransform, (1, 2, 3, 4, 5, 6, 7, 8)) 17 | 18 | def test_constructor_3x3tuple(self): 19 | a = AffineTransform(((1, 2, 3), (4, 5, 6), (7, 8, 9))) 20 | self.assertEqual(a.matrix, (1, 2, 3, 4, 5, 6, 7, 8, 9)) 21 | 22 | self.assertRaises(ValueError, AffineTransform, ((1, 2, 3), (4, 5, 6))) 23 | self.assertRaises(ValueError, AffineTransform, ((1, 2), (3, 4), (5, 6))) 24 | 25 | def test_inverse(self): 26 | # identity inverse 27 | self.assertEqual(AffineTransform().inverse(), AffineTransform()) 28 | 29 | self.assertEqual( 30 | AffineTransform((2, 0, 0, 0, 2, 0, 0, 0, 2)).inverse(), 31 | AffineTransform((0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5)) 32 | ) 33 | 34 | # inverted A*0 = A/0, which is an error 35 | self.assertRaises(ValueError, (AffineTransform() * 0).inverse) 36 | 37 | # binary operators 38 | def test_mult_scalar(self): 39 | a = AffineTransform() * 2 40 | self.assertEqual(a.matrix, (2, 0, 0, 0, 2, 0, 0, 0, 2)) 41 | 42 | a = AffineTransform() * -1 43 | self.assertEqual(a.matrix, (-1, 0, 0, 0, -1, 0, 0, 0, -1)) 44 | 45 | def test_rmult_scalar(self): 46 | a = 2 * AffineTransform() 47 | self.assertEqual(a.matrix, (2, 0, 0, 0, 2, 0, 0, 0, 2)) 48 | 49 | a = -1 * AffineTransform() 50 | self.assertEqual(a.matrix, (-1, 0, 0, 0, -1, 0, 0, 0, -1)) 51 | 52 | def test_div_scalar(self): 53 | a = AffineTransform() / 2 54 | self.assertEqual(a.matrix, (0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5)) 55 | 56 | def test_mult_affine(self): 57 | a = AffineTransform() * 2 58 | b = AffineTransform() * 3 59 | self.assertEqual((a * b).matrix, (6, 0, 0, 0, 6, 0, 0, 0, 6)) 60 | 61 | def test_div_affine(self): 62 | a = AffineTransform() * 8 63 | b = AffineTransform() * 2 64 | self.assertEqual((a / b).matrix, (4, 0, 0, 0, 4, 0, 0, 0, 4)) 65 | 66 | def test_mult_vector(self): 67 | a = AffineTransform() * 2 68 | self.assertEqual(a * (1, 2), (2, 4)) 69 | self.assertEqual(a * (1, 2, 3), (2, 4, 6)) 70 | 71 | self.assertRaises(ValueError, lambda: a * (1,)) 72 | self.assertRaises(ValueError, lambda: a * (1, 2, 3, 4)) 73 | self.assertRaises(ValueError, lambda: a * (1, 2, 3, 4)) 74 | 75 | self.assertRaises(TypeError, lambda: (1, 2, 3) * a) 76 | 77 | # simple transformations 78 | def test_translate(self): 79 | a = AffineTransform() 80 | t = a.translate(3, 4) 81 | self.assertEqual(t.matrix, (1, 0, 0, 0, 1, 0, 3, 4, 1)) 82 | self.assertEqual(t.translate(3, 4).matrix, (1, 0, 0, 0, 1, 0, 6, 8, 1)) 83 | 84 | def test_rotate(self): 85 | a = AffineTransform() 86 | self.assertEqual(a.rotate(0), a) 87 | self.assertEqual(a.rotate(360), a) 88 | 89 | self.assertEqual(a.rotate(90), a.rotate(270, clockwise=True)) 90 | 91 | self.assertEqual(a.rotate(90), AffineTransform((0, -1, 0, 1, 0, 0, 0, 0, 1))) 92 | 93 | # some chain multiplications 94 | def test_chained_translate_rotate(self): 95 | a = AffineTransform() 96 | # translate by (3, 4). rotate by 90 degrees 97 | t = a.translate(3, 4).rotate(90) 98 | self.assertEqual(t.matrix, (0, -1, 0, 1, 0, 0, 4, -3, 1)) 99 | 100 | def test_chained_translate_rotate_translate(self): 101 | a = AffineTransform() 102 | # translate by (3, 4). rotate by 90 degrees. then translate back. 103 | t = a.translate(3, 4).rotate(90).translate(-3, -4) 104 | self.assertEqual(t.matrix, (0, -1, 0, 1, 0, 0, 1, -7, 1)) 105 | -------------------------------------------------------------------------------- /pymaging/tests/test_basic.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Jonas Obrist 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Jonas Obrist nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | from __future__ import absolute_import 26 | from pymaging.colors import Color, RGBA 27 | from pymaging.exceptions import FormatNotSupported 28 | from pymaging.formats import register, Format 29 | from pymaging.image import Image 30 | from pymaging.shapes import Line, Pixel 31 | from pymaging.test_utils import PymagingBaseTestCase, image_factory, pixel_array_factory 32 | from pymaging.webcolors import Red, Green, Blue, Black, White, Lime 33 | try: # pragma: no-cover 34 | # 2.x 35 | from StringIO import StringIO 36 | except ImportError: # pragma: no-cover 37 | # 3.x 38 | from io import StringIO 39 | 40 | 41 | class BasicTests(PymagingBaseTestCase): 42 | def _get_fake_image(self): 43 | return image_factory([ 44 | [Red, Green, Blue], 45 | [Green, Blue, Red], 46 | [Blue, Red, Green], 47 | ]) 48 | 49 | def test_open_invalid_image(self): 50 | self.assertRaises(FormatNotSupported, Image.open, StringIO('')) 51 | 52 | def test_crop(self): 53 | img = self._get_fake_image() 54 | img.crop(1, 1, 1, 1) 55 | 56 | def test_flip_left_right(self): 57 | img = self._get_fake_image() 58 | l2r = img.flip_left_right() 59 | self.assertImage(l2r, [ 60 | [Blue, Green, Red], 61 | [Red, Blue, Green], 62 | [Green, Red, Blue], 63 | ]) 64 | 65 | def test_flip_top_bottom(self): 66 | img = self._get_fake_image() 67 | t2b = img.flip_top_bottom() 68 | self.assertImage(t2b, [ 69 | [Blue, Red, Green], 70 | [Green, Blue, Red], 71 | [Red, Green, Blue], 72 | ]) 73 | 74 | def test_get_pixel(self): 75 | img = self._get_fake_image() 76 | color = img.get_color(0, 0) 77 | self.assertEqual(color, Red) 78 | 79 | def test_set_pixel(self): 80 | img = image_factory([ 81 | [Black, Black], 82 | [Black, Black], 83 | ]) 84 | img.set_color(0, 0, White) 85 | self.assertImage(img, [ 86 | [White, Black], 87 | [Black, Black], 88 | ]) 89 | 90 | def test_color_mix_with(self): 91 | base = Red 92 | color = Lime.get_for_brightness(0.5) 93 | result = base.cover_with(color) 94 | self.assertEqual(result, Color(128, 127, 0, 255)) 95 | 96 | def test_color_mix_with_fastpath(self): 97 | base = Red 98 | color = Lime 99 | result = base.cover_with(color) 100 | self.assertEqual(result, Lime) 101 | 102 | def test_new(self): 103 | img = Image.new(RGBA, 5, 5, Black) 104 | self.assertImage(img, [ 105 | [Black, Black, Black, Black, Black], 106 | [Black, Black, Black, Black, Black], 107 | [Black, Black, Black, Black, Black], 108 | [Black, Black, Black, Black, Black], 109 | [Black, Black, Black, Black, Black], 110 | ]) 111 | 112 | 113 | 114 | class DrawTests(PymagingBaseTestCase): 115 | def test_draw_pixel(self): 116 | img = image_factory([ 117 | [Black, Black], 118 | [Black, Black], 119 | ]) 120 | pixel = Pixel(0, 0) 121 | img.draw(pixel, White) 122 | self.assertImage(img, [ 123 | [White, Black], 124 | [Black, Black], 125 | ]) 126 | 127 | def test_alpha_mixing(self): 128 | img = image_factory([[Red]]) 129 | semi_transparent_green = Lime.get_for_brightness(0.5) 130 | img.draw(Pixel(0, 0), semi_transparent_green) 131 | result = img.get_color(0, 0) 132 | self.assertEqual(result, Color(128, 127, 0, 255)) 133 | 134 | def test_draw_line_topleft_bottomright(self): 135 | img = image_factory([ 136 | [Black, Black, Black, Black, Black], 137 | [Black, Black, Black, Black, Black], 138 | [Black, Black, Black, Black, Black], 139 | [Black, Black, Black, Black, Black], 140 | [Black, Black, Black, Black, Black], 141 | ]) 142 | line = Line(0, 0, 4, 4) 143 | img.draw(line, White) 144 | self.assertImage(img, [ 145 | [White, Black, Black, Black, Black], 146 | [Black, White, Black, Black, Black], 147 | [Black, Black, White, Black, Black], 148 | [Black, Black, Black, White, Black], 149 | [Black, Black, Black, Black, White], 150 | ]) 151 | 152 | def test_draw_line_bottomright_topleft(self): 153 | img = image_factory([ 154 | [Black, Black, Black, Black, Black], 155 | [Black, Black, Black, Black, Black], 156 | [Black, Black, Black, Black, Black], 157 | [Black, Black, Black, Black, Black], 158 | [Black, Black, Black, Black, Black], 159 | ]) 160 | line = Line(4, 4, 0, 0) 161 | img.draw(line, White) 162 | self.assertImage(img, [ 163 | [White, Black, Black, Black, Black], 164 | [Black, White, Black, Black, Black], 165 | [Black, Black, White, Black, Black], 166 | [Black, Black, Black, White, Black], 167 | [Black, Black, Black, Black, White], 168 | ]) 169 | 170 | def test_draw_line_bottomleft_topright(self): 171 | img = image_factory([ 172 | [Black, Black, Black, Black, Black], 173 | [Black, Black, Black, Black, Black], 174 | [Black, Black, Black, Black, Black], 175 | [Black, Black, Black, Black, Black], 176 | [Black, Black, Black, Black, Black], 177 | ]) 178 | line = Line(0, 4, 4, 0) 179 | img.draw(line, White) 180 | self.assertImage(img, [ 181 | [Black, Black, Black, Black, White], 182 | [Black, Black, Black, White, Black], 183 | [Black, Black, White, Black, Black], 184 | [Black, White, Black, Black, Black], 185 | [White, Black, Black, Black, Black], 186 | ]) 187 | 188 | def test_draw_line_topright_bottomleft(self): 189 | img = image_factory([ 190 | [Black, Black, Black, Black, Black], 191 | [Black, Black, Black, Black, Black], 192 | [Black, Black, Black, Black, Black], 193 | [Black, Black, Black, Black, Black], 194 | [Black, Black, Black, Black, Black], 195 | ]) 196 | line = Line(4, 0, 0, 4) 197 | img.draw(line, White) 198 | self.assertImage(img, [ 199 | [Black, Black, Black, Black, White], 200 | [Black, Black, Black, White, Black], 201 | [Black, Black, White, Black, Black], 202 | [Black, White, Black, Black, Black], 203 | [White, Black, Black, Black, Black], 204 | ]) 205 | 206 | def test_draw_line_steep(self): 207 | img = image_factory([ 208 | [Black, Black, Black, Black, Black], 209 | [Black, Black, Black, Black, Black], 210 | [Black, Black, Black, Black, Black], 211 | [Black, Black, Black, Black, Black], 212 | [Black, Black, Black, Black, Black], 213 | ]) 214 | line = Line(0, 0, 1, 4) 215 | img.draw(line, White) 216 | self.assertImage(img, [ 217 | [White, Black, Black, Black, Black], 218 | [White, Black, Black, Black, Black], 219 | [Black, White, Black, Black, Black], 220 | [Black, White, Black, Black, Black], 221 | [Black, White, Black, Black, Black], 222 | ]) 223 | 224 | def test_blit_simple(self): 225 | main = image_factory([ 226 | [Black, Black, Black, Black, Black], 227 | [Black, Black, Black, Black, Black], 228 | [Black, Black, Black, Black, Black], 229 | [Black, Black, Black, Black, Black], 230 | [Black, Black, Black, Black, Black], 231 | ]) 232 | other =image_factory([ 233 | [White, White, White], 234 | [White, White, White], 235 | [White, White, White], 236 | ]) 237 | main.blit(1, 1, other) 238 | self.assertImage(main, [ 239 | [Black, Black, Black, Black, Black], 240 | [Black, White, White, White, Black], 241 | [Black, White, White, White, Black], 242 | [Black, White, White, White, Black], 243 | [Black, Black, Black, Black, Black], 244 | ]) 245 | 246 | def test_blit_partial(self): 247 | main = image_factory([ 248 | [Black, Black, Black, Black, Black], 249 | [Black, Black, Black, Black, Black], 250 | [Black, Black, Black, Black, Black], 251 | [Black, Black, Black, Black, Black], 252 | [Black, Black, Black, Black, Black], 253 | ]) 254 | other =image_factory([ 255 | [White, White, White], 256 | [White, White, White], 257 | [White, White, White], 258 | ]) 259 | main.blit(3, 3, other) 260 | self.assertImage(main, [ 261 | [Black, Black, Black, Black, Black], 262 | [Black, Black, Black, Black, Black], 263 | [Black, Black, Black, Black, Black], 264 | [Black, Black, Black, White, White], 265 | [Black, Black, Black, White, White], 266 | ]) 267 | 268 | def test_delayed_loading(self): 269 | pixel_array = pixel_array_factory([ 270 | [Black] 271 | ]) 272 | class Loader(object): 273 | def __init__(self): 274 | self.callcount = 0 275 | 276 | def __call__(self): 277 | self.callcount += 1 278 | return pixel_array, None 279 | loader = Loader() 280 | image = Image(RGBA, 1, 1, loader) 281 | self.assertEqual(loader.callcount, 0) 282 | image.set_color(0, 0, White) 283 | self.assertEqual(loader.callcount, 1) 284 | image.flip_left_right() 285 | self.assertEqual(loader.callcount, 1) 286 | 287 | def test_format_registration(self): 288 | def loader(): 289 | return pixel_array_factory([[Black]]), None 290 | def open_image(fobj): 291 | return Image( 292 | RGBA, 293 | 1, 294 | 1, 295 | loader, 296 | ) 297 | def save_image(image, fobj): 298 | fobj.write('saved') 299 | register(Format(open_image, save_image, ['test'])) 300 | img = Image.open(StringIO()) 301 | self.assertIsInstance(img, Image) 302 | self.assertEqual(img.width, 1) 303 | self.assertEqual(img.height, 1) 304 | self.assertEqual(img.get_color(0, 0), Black) 305 | sio = StringIO() 306 | img.save(sio, 'test') 307 | self.assertEqual(sio.getvalue(), 'saved') 308 | 309 | -------------------------------------------------------------------------------- /pymaging/tests/test_colors.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pymaging.colors import Color 4 | 5 | def ctuple(c): 6 | return c.red, c.green, c.blue, c.alpha 7 | 8 | class TestColor(unittest.TestCase): 9 | ## constructors 10 | def test_constructor(self): 11 | c = Color(10, 20, 30) 12 | self.assertEqual(c.red, 10) 13 | self.assertEqual(c.alpha, 255) 14 | 15 | def test_from_pixel(self): 16 | c = Color.from_pixel([10, 20, 30]) 17 | self.assertEqual(ctuple(c), (10, 20, 30, 255)) 18 | 19 | c = Color.from_pixel([10, 20, 30, 40]) 20 | self.assertEqual(ctuple(c), (10, 20, 30, 40)) 21 | 22 | def test_from_hexcode(self): 23 | c = Color.from_hexcode('feef1510') 24 | self.assertEqual(ctuple(c), (254, 239, 21, 16)) 25 | 26 | c = Color.from_hexcode('123') 27 | self.assertEqual(ctuple(c), (17, 34, 51, 255)) 28 | -------------------------------------------------------------------------------- /pymaging/tests/test_resampling.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Jonas Obrist 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Jonas Obrist nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | from pymaging.tests.test_basic import PymagingBaseTestCase, image_factory 26 | from pymaging.colors import Color 27 | from pymaging.webcolors import Red, Green, Blue 28 | from pymaging.resample import bilinear 29 | 30 | 31 | transparent = Color(0, 0, 0, 0) 32 | 33 | 34 | class NearestResamplingTests(PymagingBaseTestCase): 35 | def test_resize_nearest_down(self): 36 | img = image_factory([ 37 | [Red, Green, Blue], 38 | [Green, Blue, Red], 39 | [Blue, Red, Green], 40 | ]) 41 | img = img.resize(2, 2) 42 | self.assertImage(img, [ 43 | [Red, Blue], 44 | [Blue, Green], 45 | ]) 46 | 47 | def test_resize_nearest_down_transparent(self): 48 | img = image_factory([ 49 | [Red, Green, Blue], 50 | [Green, Blue, Red], 51 | [Blue, Red, transparent], 52 | ]) 53 | img = img.resize(2, 2) 54 | self.assertImage(img, [ 55 | [Red, Blue], 56 | [Blue, transparent], 57 | ]) 58 | 59 | def test_resize_nearest_up(self): 60 | img = image_factory([ 61 | [Red, Blue], 62 | [Blue, Green], 63 | ]) 64 | img = img.resize(4, 4) 65 | self.assertImage(img, [ 66 | [Red, Red, Blue, Blue], 67 | [Red, Red, Blue, Blue], 68 | [Blue, Blue, Green, Green], 69 | [Blue, Blue, Green, Green], 70 | ]) 71 | 72 | def test_affine_rotate_nearest_90(self): 73 | img = image_factory([ 74 | [Red, Blue], 75 | [Blue, Blue], 76 | ]) 77 | img = img.rotate(90) 78 | self.assertImage(img, [ 79 | [Blue, Blue], 80 | [Red, Blue], 81 | ]) 82 | 83 | def test_transparent_background(self): 84 | img = image_factory([ 85 | [Red, Red, Blue, Blue], 86 | [Red, Red, Blue, Blue], 87 | [Blue, Blue, Red, Red], 88 | [Blue, Blue, Red, Red], 89 | ]) 90 | img = img.resize(2, 2, resize_canvas=False) 91 | self.assertImage(img, [ 92 | [Red, Blue, transparent, transparent], 93 | [Blue, Red, transparent, transparent], 94 | [transparent, transparent, transparent, transparent], 95 | [transparent, transparent, transparent, transparent], 96 | ]) 97 | 98 | 99 | class ResizeBilinearResamplingTests(PymagingBaseTestCase): 100 | def test_resize_bilinear_down_simple(self): 101 | img = image_factory([ 102 | [Red, Blue], 103 | [Blue, Green], 104 | ]) 105 | img = img.resize(1, 1, resample_algorithm=bilinear) 106 | self.assertImage(img, [ 107 | # all the colors blended equally 108 | [Color(64, 32, 128, 255)] 109 | ]) 110 | 111 | def test_resize_bilinear_down_proportional(self): 112 | img = image_factory([ 113 | [Red, Red, Blue], 114 | [Red, Red, Blue], 115 | [Blue, Blue, Blue], 116 | ]) 117 | img = img.resize(2, 2, resample_algorithm=bilinear) 118 | self.assertImage(img, [ 119 | [Red, Color(64, 0, 191, 255)], 120 | [Color(64, 0, 191, 255), Color(16, 0, 239, 255)], 121 | ]) 122 | 123 | def test_resize_bilinear_down_simple_transparent(self): 124 | img = image_factory([ 125 | [Red, Blue], 126 | [Blue, transparent], 127 | ]) 128 | img = img.resize(1, 1, resample_algorithm=bilinear) 129 | 130 | # - the alpha values get blended equally. 131 | # - all non-alpha channels get multiplied by their alpha, so the 132 | # transparent pixel does not contribute to the result. 133 | self.assertImage(img, [ 134 | [Color(85, 0, 170, 191)] 135 | ]) 136 | 137 | def test_resize_bilinear_down_simple_completely_transparent(self): 138 | img = image_factory([ 139 | [transparent, transparent], 140 | [transparent, transparent], 141 | ]) 142 | img = img.resize(1, 1, resample_algorithm=bilinear) 143 | 144 | # testing this because a naive implementation can cause div/0 error 145 | self.assertImage(img, [ 146 | [Color(0, 0, 0, 0)] 147 | ]) 148 | 149 | def test_resize_bilinear_down_proportional_transparent(self): 150 | img = image_factory([ 151 | [Red, Red, transparent], 152 | [Red, Red, transparent], 153 | [transparent, transparent, transparent], 154 | ]) 155 | img = img.resize(2, 2, resample_algorithm=bilinear) 156 | 157 | # - the alpha values get blended equally. 158 | # - all non-alpha channels get multiplied by their alpha, so the 159 | # transparent pixel does not contribute to the result. 160 | self.assertImage(img, [ 161 | [Red, Color(255, 0, 0, 64)], 162 | [Color(255, 0, 0, 64), Color(255, 0, 0, 16)], 163 | ]) 164 | 165 | def test_resize_bilinear_no_change(self): 166 | img = image_factory([ 167 | [Red, Blue], 168 | [Blue, Green], 169 | ]) 170 | img = img.resize(2, 2, resample_algorithm=bilinear) 171 | self.assertImage(img, [ 172 | [Red, Blue], 173 | [Blue, Green], 174 | ]) 175 | 176 | def test_resize_bilinear_up_simple(self): 177 | img = image_factory([ 178 | [Red, Blue], 179 | [Blue, Green], 180 | ]) 181 | img = img.resize(4, 4, resample_algorithm=bilinear) 182 | self.assertImage(img, [ 183 | [Red, Red, Blue, Blue], 184 | [Red, Red, Blue, Blue], 185 | [Blue, Blue, Green, Green], 186 | [Blue, Blue, Green, Green], 187 | ]) 188 | 189 | def test_resize_bilinear_up_proportional(self): 190 | img = image_factory([ 191 | [Red, Blue], 192 | [Blue, Green], 193 | ]) 194 | img = img.resize(3, 3, resample_algorithm=bilinear) 195 | self.assertImage(img, [ 196 | [Color(177, 4, 71, 255), Color(106, 11, 128, 255), Color(0, 21, 212, 255)], 197 | [Color(106, 11, 128, 255), Color(64, 32, 128, 255), Color(0, 64, 128, 255)], 198 | [Color(0, 21, 212, 255), Color(0, 64, 128, 255), Color(0, 128, 0, 255)], 199 | ]) 200 | 201 | def test_affine_rotate_bilinear_90(self): 202 | img = image_factory([ 203 | [Red, Blue], 204 | [Blue, Blue], 205 | ]) 206 | img = img.rotate(90, resample_algorithm=bilinear) 207 | self.assertImage(img, [ 208 | [Blue, Blue], 209 | [Red, Blue], 210 | ]) 211 | -------------------------------------------------------------------------------- /pymaging/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2012, Jonas Obrist 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the Jonas Obrist nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | import os 27 | 28 | def fdiv(a, b): 29 | return float(a) / float(b) 30 | 31 | 32 | def get_test_file(testfile, fname): 33 | return os.path.join(os.path.dirname(testfile), 'testdata', fname) 34 | get_test_file.__test__ = False # nose... 35 | -------------------------------------------------------------------------------- /pymaging/webcolors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2012, Jonas Obrist 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the Jonas Obrist nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | from pymaging.colors import Color 27 | 28 | 29 | # List of web color names taken from http://en.wikipedia.org/wiki/Web_colors 30 | 31 | IndianRed = Color(205, 92, 92, 255) 32 | LightCoral = Color(240, 128, 128, 255) 33 | Salmon = Color(250, 128, 114, 255) 34 | DarkSalmon = Color(233, 150, 122, 255) 35 | LightSalmon = Color(255, 160, 122, 255) 36 | Red = Color(255, 0, 0, 255) 37 | Crimson = Color(220, 20, 60, 255) 38 | FireBrick = Color(178, 34, 34, 255) 39 | DarkRed = Color(139, 0, 0, 255) 40 | Pink = Color(255, 192, 203, 255) 41 | LightPink = Color(255, 182, 193, 255) 42 | HotPink = Color(255, 105, 180, 255) 43 | DeepPink = Color(255, 20, 147, 255) 44 | MediumVioletRed = Color(199, 21, 133, 255) 45 | PaleVioletRed = Color(219, 112, 147, 255) 46 | LightSalmon = Color(255, 160, 122, 255) 47 | Coral = Color(255, 127, 80, 255) 48 | Tomato = Color(255, 99, 71, 255) 49 | OrangeRed = Color(255, 69, 0, 255) 50 | DarkOrange = Color(255, 140, 0, 255) 51 | Orange = Color(255, 165, 0, 255) 52 | Gold = Color(255, 215, 0, 255) 53 | Yellow = Color(255, 255, 0, 255) 54 | LightYellow = Color(255, 255, 224, 255) 55 | LemonChiffon = Color(255, 250, 205, 255) 56 | LightGoldenrodYellow = Color(250, 250, 210, 255) 57 | PapayaWhip = Color(255, 239, 213, 255) 58 | Moccasin = Color(255, 228, 181, 255) 59 | PeachPuff = Color(255, 218, 185, 255) 60 | PaleGoldenrod = Color(238, 232, 170, 255) 61 | Khaki = Color(240, 230, 140, 255) 62 | DarkKhaki = Color(189, 183, 107, 255) 63 | Lavender = Color(230, 230, 250, 255) 64 | Thistle = Color(216, 191, 216, 255) 65 | Plum = Color(221, 160, 221, 255) 66 | Violet = Color(238, 130, 238, 255) 67 | Orchid = Color(218, 112, 214, 255) 68 | Fuchsia = Color(255, 0, 255, 255) 69 | Magenta = Color(255, 0, 255, 255) 70 | MediumOrchid = Color(186, 85, 211, 255) 71 | MediumPurple = Color(147, 112, 219, 255) 72 | BlueViolet = Color(138, 43, 226, 255) 73 | DarkViolet = Color(148, 0, 211, 255) 74 | DarkOrchid = Color(153, 50, 204, 255) 75 | DarkMagenta = Color(139, 0, 139, 255) 76 | Purple = Color(128, 0, 128, 255) 77 | Indigo = Color(75, 0, 130, 255) 78 | DarkSlateBlue = Color(72, 61, 139, 255) 79 | SlateBlue = Color(106, 90, 205, 255) 80 | MediumSlateBlue = Color(123, 104, 238, 255) 81 | GreenYellow = Color(173, 255, 47, 255) 82 | Chartreuse = Color(127, 255, 0, 255) 83 | LawnGreen = Color(124, 252, 0, 255) 84 | Lime = Color(0, 255, 0, 255) 85 | LimeGreen = Color(50, 205, 50, 255) 86 | PaleGreen = Color(152, 251, 152, 255) 87 | LightGreen = Color(144, 238, 144, 255) 88 | MediumSpringGreen = Color(0, 250, 154, 255) 89 | SpringGreen = Color(0, 255, 127, 255) 90 | MediumSeaGreen = Color(60, 179, 113, 255) 91 | SeaGreen = Color(46, 139, 87, 255) 92 | ForestGreen = Color(34, 139, 34, 255) 93 | Green = Color(0, 128, 0, 255) 94 | DarkGreen = Color(0, 100, 0, 255) 95 | YellowGreen = Color(154, 205, 50, 255) 96 | OliveDrab = Color(107, 142, 35, 255) 97 | Olive = Color(128, 128, 0, 255) 98 | DarkOliveGreen = Color(85, 107, 47, 255) 99 | MediumAquamarine = Color(102, 205, 170, 255) 100 | DarkSeaGreen = Color(143, 188, 143, 255) 101 | LightSeaGreen = Color(32, 178, 170, 255) 102 | DarkCyan = Color(0, 139, 139, 255) 103 | Teal = Color(0, 128, 128, 255) 104 | Aqua = Color(0, 255, 255, 255) 105 | Cyan = Color(0, 255, 255, 255) 106 | LightCyan = Color(224, 255, 255, 255) 107 | PaleTurquoise = Color(175, 238, 238, 255) 108 | Aquamarine = Color(127, 255, 212, 255) 109 | Turquoise = Color(64, 224, 208, 255) 110 | MediumTurquoise = Color(72, 209, 204, 255) 111 | DarkTurquoise = Color(0, 206, 209, 255) 112 | CadetBlue = Color(95, 158, 160, 255) 113 | SteelBlue = Color(70, 130, 180, 255) 114 | LightSteelBlue = Color(176, 196, 222, 255) 115 | PowderBlue = Color(176, 224, 230, 255) 116 | LightBlue = Color(173, 216, 230, 255) 117 | SkyBlue = Color(135, 206, 235, 255) 118 | LightSkyBlue = Color(135, 206, 250, 255) 119 | DeepSkyBlue = Color(0, 191, 255, 255) 120 | DodgerBlue = Color(30, 144, 255, 255) 121 | CornflowerBlue = Color(100, 149, 237, 255) 122 | RoyalBlue = Color(65, 105, 225, 255) 123 | Blue = Color(0, 0, 255, 255) 124 | MediumBlue = Color(0, 0, 205, 255) 125 | DarkBlue = Color(0, 0, 139, 255) 126 | Navy = Color(0, 0, 128, 255) 127 | MidnightBlue = Color(25, 25, 112, 255) 128 | Cornsilk = Color(255, 248, 220, 255) 129 | BlanchedAlmond = Color(255, 235, 205, 255) 130 | Bisque = Color(255, 228, 196, 255) 131 | NavajoWhite = Color(255, 222, 173, 255) 132 | Wheat = Color(245, 222, 179, 255) 133 | BurlyWood = Color(222, 184, 135, 255) 134 | Tan = Color(210, 180, 140, 255) 135 | RosyBrown = Color(188, 143, 143, 255) 136 | SandyBrown = Color(244, 164, 96, 255) 137 | Goldenrod = Color(218, 165, 32, 255) 138 | DarkGoldenrod = Color(184, 134, 11, 255) 139 | Peru = Color(205, 133, 63, 255) 140 | Chocolate = Color(210, 105, 30, 255) 141 | SaddleBrown = Color(139, 69, 19, 255) 142 | Sienna = Color(160, 82, 45, 255) 143 | Brown = Color(165, 42, 42, 255) 144 | Maroon = Color(128, 0, 0, 255) 145 | White = Color(255, 255, 255, 255) 146 | Snow = Color(255, 250, 250, 255) 147 | Honeydew = Color(240, 255, 240, 255) 148 | MintCream = Color(245, 255, 250, 255) 149 | Azure = Color(240, 255, 255, 255) 150 | AliceBlue = Color(240, 248, 255, 255) 151 | GhostWhite = Color(248, 248, 255, 255) 152 | WhiteSmoke = Color(245, 245, 245, 255) 153 | Seashell = Color(255, 245, 238, 255) 154 | Beige = Color(245, 245, 220, 255) 155 | OldLace = Color(253, 245, 230, 255) 156 | FloralWhite = Color(255, 250, 240, 255) 157 | Ivory = Color(255, 255, 240, 255) 158 | AntiqueWhite = Color(250, 235, 215, 255) 159 | Linen = Color(250, 240, 230, 255) 160 | LavenderBlush = Color(255, 240, 245, 255) 161 | MistyRose = Color(255, 228, 225, 255) 162 | Gainsboro = Color(220, 220, 220, 255) 163 | LightGrey = Color(211, 211, 211, 255) 164 | Silver = Color(192, 192, 192, 255) 165 | DarkGray = Color(169, 169, 169, 255) 166 | Gray = Color(128, 128, 128, 255) 167 | DimGray = Color(105, 105, 105, 255) 168 | LightSlateGray = Color(119, 136, 153, 255) 169 | SlateGray = Color(112, 128, 144, 255) 170 | DarkSlateGray = Color(47, 79, 79, 255) 171 | Black = Color(0, 0, 0, 255) 172 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PYTHON_VERSIONS="2.6 2.7 3.1 3.2 3.3" 4 | COMMAND="setup.py test" 5 | STATUS=0 6 | 7 | for version in $PYTHON_VERSIONS; do 8 | pybin="python$version" 9 | if [ `which $pybin` ]; then 10 | echo "****************************" 11 | echo "Running tests for Python $version" 12 | echo "****************************" 13 | $pybin $COMMAND 14 | STATUS=$(($STATUS+$?)) 15 | else 16 | echo "****************************" 17 | echo "Python $version not found, skipping" 18 | echo "****************************" 19 | fi 20 | done 21 | 22 | if [ `which pypy` ]; then 23 | pypyversion=`pypy -c "import sys;print(sys.version).splitlines()[1]"` 24 | echo "**************************************************" 25 | echo "Running tests for PyPy $pypyversion" 26 | echo "**************************************************" 27 | pypy $COMMAND 28 | STATUS=$(($STATUS+$?)) 29 | fi 30 | echo 31 | if [ $STATUS -eq 0 ]; then 32 | echo "All versions OK" 33 | else 34 | echo "One or more versions FAILED" 35 | fi 36 | 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from pymaging import __version__ 3 | from setuptools import setup 4 | 5 | setup( 6 | name = "pymaging", 7 | version = __version__, 8 | packages = ['pymaging'], 9 | author = "Jonas Obrist", 10 | author_email = "ojiidotch@gmail.com", 11 | description = "Pure Python imaging library.", 12 | license = "BSD", 13 | keywords = "pymaging png imaging", 14 | url = "https://github.com/ojii/pymaging/", 15 | zip_safe = False, 16 | test_suite = 'pymaging.tests', 17 | ) 18 | --------------------------------------------------------------------------------