├── docs ├── _static │ └── .keep ├── images │ ├── branko.png │ ├── logo.png │ ├── favicon.ico │ ├── pydenticon.png │ ├── branko_inverted.png │ └── pydenticon_inverted.png ├── apireference.rst ├── testing.rst ├── index.rst ├── installation.rst ├── about.rst ├── releasenotes.rst ├── privacy.rst ├── make.bat ├── Makefile ├── usage.rst ├── algorithm.rst └── conf.py ├── tests ├── __init__.py ├── samples │ ├── test1.png │ ├── test2.png │ └── test3.png └── test_pydenticon.py ├── .gitignore ├── assets └── favicon.xcf ├── MANIFEST.in ├── README.rst ├── setup.py ├── LICENSE └── pydenticon └── __init__.py /docs/_static/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | tmp/ 4 | docs/_build/ 5 | -------------------------------------------------------------------------------- /assets/favicon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azaghal/pydenticon/HEAD/assets/favicon.xcf -------------------------------------------------------------------------------- /docs/images/branko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azaghal/pydenticon/HEAD/docs/images/branko.png -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azaghal/pydenticon/HEAD/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azaghal/pydenticon/HEAD/docs/images/favicon.ico -------------------------------------------------------------------------------- /tests/samples/test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azaghal/pydenticon/HEAD/tests/samples/test1.png -------------------------------------------------------------------------------- /tests/samples/test2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azaghal/pydenticon/HEAD/tests/samples/test2.png -------------------------------------------------------------------------------- /tests/samples/test3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azaghal/pydenticon/HEAD/tests/samples/test3.png -------------------------------------------------------------------------------- /docs/images/pydenticon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azaghal/pydenticon/HEAD/docs/images/pydenticon.png -------------------------------------------------------------------------------- /docs/apireference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. automodule:: pydenticon 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/images/branko_inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azaghal/pydenticon/HEAD/docs/images/branko_inverted.png -------------------------------------------------------------------------------- /docs/images/pydenticon_inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azaghal/pydenticon/HEAD/docs/images/pydenticon_inverted.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include assets *.xcf 2 | recursive-include docs Makefile make.bat *.py *.rst *.png *.ico 3 | include LICENSE 4 | include MANIFEST.in 5 | include README.rst 6 | include setup.py 7 | recursive-include pydenticon *.py 8 | recursive-include tests *.py *.png 9 | prune docs/_build 10 | exclude tmp/ 11 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | Pydenticon includes a number of unit tests which are used for regression 5 | testing. The tests are fairly comprehensive, and also include comparison of 6 | Pydenticon-generated identicons against a couple of samples generated by Sigil. 7 | 8 | Tests depend on the following additional libraries: 9 | 10 | * `Mock `_ 11 | 12 | Test dependencies will be automatically downloaded when running the tests if 13 | they're not present. 14 | 15 | Pydenticon tests can be run with the following command:: 16 | 17 | python setup.py test 18 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pydenticon 2 | ========== 3 | 4 | Pydenticon is a small utility library that can be used for deterministically 5 | generating identicons based on the hash of provided data. 6 | 7 | The implementation is a port of the Sigil identicon implementation from: 8 | 9 | * https://github.com/cupcake/sigil/ 10 | 11 | Pydenticon provides a couple of extensions of its own when compared to the 12 | original Sigil implementation, like: 13 | 14 | * Ability to supply custom digest algorithms (allowing for larger identicons if 15 | digest provides enough entropy). 16 | * Ability to specify a rectangle for identicon size.. 17 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Pydenticon documentation 2 | ======================== 3 | 4 | .. image:: images/pydenticon.png 5 | .. image:: images/pydenticon_inverted.png 6 | 7 | Pydenticon is a small utility library that can be used for deterministically 8 | generating identicons based on the hash of provided data. 9 | 10 | The implementation is a port of the Sigil identicon implementation from: 11 | 12 | * https://github.com/cupcake/sigil/ 13 | 14 | Support 15 | ------- 16 | 17 | In case of problems with the library, please do not hestitate to contact the 18 | author at **pydenticon (at) majic.rs**. The library itself is hosted on Github, 19 | and on author's own websites: 20 | 21 | * https://github.com/azaghal/pydenticon 22 | * https://code.majic.rs/pydenticon 23 | * https://projects.majic.rs/pydenticon 24 | 25 | Contents: 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | 30 | about 31 | installation 32 | usage 33 | algorithm 34 | privacy 35 | apireference 36 | testing 37 | releasenotes 38 | 39 | Indices and tables 40 | ================== 41 | 42 | * :ref:`genindex` 43 | * :ref:`modindex` 44 | * :ref:`search` 45 | 46 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Pydenticon can be installed through one of the following methods: 5 | 6 | * Using *pip*, which is the easiest and recommended way for production websites. 7 | * Manually, by copying the necessary files and installing the dependencies. 8 | 9 | Requirements 10 | ------------ 11 | 12 | The main external requirement for Pydenticon is `Pillow 13 | `_, which is used for generating the images. 14 | 15 | Using pip 16 | --------- 17 | 18 | In order to install latest stable release of Pydenticon using *pip*, run the 19 | following command:: 20 | 21 | pip install pydenticon 22 | 23 | In order to install the latest development version of Pydenticon from Github, 24 | use the following command:: 25 | 26 | pip install -e git+https://github.com/azaghal/pydenticon#egg=pydenticon 27 | 28 | Manual installation 29 | ------------------- 30 | 31 | If you wish to install Pydenticon manually, make sure that its dependencies have 32 | been met first, and then simply copy the ``pydenticon`` directory (that contains 33 | the ``__init__.py`` file) somewhere on the Python path. 34 | 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read() 5 | INSTALL_REQUIREMENTS = ["Pillow"] 6 | TEST_REQUIREMENTS = ["mock"] 7 | 8 | # allow setup.py to be run from any path 9 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 10 | 11 | setup( 12 | name='pydenticon', 13 | version='0.3-dev', 14 | packages=['pydenticon'], 15 | include_package_data=True, 16 | license='BSD', # example license 17 | description='Library for generating identicons. Port of Sigil (https://github.com/cupcake/sigil) with enhancements.', 18 | long_description=README, 19 | url='https://github.com/azaghal/pydenticon', 20 | author='Branko Majic', 21 | author_email='branko@majic.rs', 22 | install_requires=INSTALL_REQUIREMENTS, 23 | tests_require=TEST_REQUIREMENTS, 24 | test_suite="tests", 25 | classifiers=[ 26 | 'Environment :: Other Environment', 27 | 'Environment :: Web Environment', 28 | 'Intended Audience :: Developers', 29 | 'License :: OSI Approved :: BSD License', 30 | 'Operating System :: OS Independent', 31 | 'Programming Language :: Python', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 35 | 'Topic :: Multimedia :: Graphics', 36 | 'Topic :: Software Development :: Libraries', 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Branko Majic 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | Neither the name of Branko Majic nor the names of any other 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /docs/about.rst: -------------------------------------------------------------------------------- 1 | About Pydenticon 2 | ================ 3 | 4 | Pydenticon is a small utility library that can be used for deterministically 5 | generating identicons based on the hash of provided data. 6 | 7 | The implementation is a port of the Sigil identicon implementation from: 8 | 9 | * https://github.com/cupcake/sigil/ 10 | 11 | Why was this library created? 12 | ----------------------------- 13 | 14 | A number of web-based applications written in Python have a need for visually 15 | differentiating between users by using avatars for each one of them. 16 | 17 | This functionality is particularly popular with comment-posting since it 18 | increases the readability of threads. 19 | 20 | The problem is that lots of those applications need to allow anonymous users to 21 | post their comments as well. Since anonymous users cannot set the avatar for 22 | themselves, usually a random avatar is created for them instead. 23 | 24 | There is a number of free (as in free beer) services out there that allow web 25 | application developers to create such avatars. Unfortunately, this usually means 26 | that the users visiting websites based on those applications are leaking 27 | information about their browsing habits etc to these third-party providers. 28 | 29 | Pydenticon was written in order to resolve such an issue for one of the 30 | application (Django Blog Zinnia, in particular), and to allow the author to set 31 | up his own avatar/identicon service. 32 | 33 | Features 34 | -------- 35 | 36 | Pydenticon has the following features: 37 | 38 | * Compatible with Sigil implementation (https://github.com/cupcake/sigil/) if 39 | set-up with right parameters. 40 | * Creates vertically symmetrical identicons of any rectangular shape and size. 41 | * Uses digests of passed data for generating the identicons. 42 | * Automatically detects if passed data is hashed already or not. 43 | * Custom digest implementations can be passed to identicon generator (defaults 44 | to 'MD5'). 45 | * Support for multiple image formats. 46 | * PNG 47 | * ASCII 48 | * Foreground colour picked from user-provided list. 49 | * Background colour set by the user. 50 | * Ability to invert foreground and background colour in the generated identicon. 51 | * Customisable padding around generated identicon using the background colour 52 | (foreground if inverted identicon was requested). 53 | 54 | -------------------------------------------------------------------------------- /docs/releasenotes.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | 0.3.1 5 | ----- 6 | 7 | Minor bug-fixes. 8 | 9 | Bug fixes: 10 | 11 | * `PYD-8 - Cannot generate identicons in JPEG format when using Pillow >= 4.2.0 12 | `_ 13 | 14 | 0.3 15 | --- 16 | 17 | Update introducing support for more output formats and ability to use 18 | transparency for PNG identicons. 19 | 20 | New features: 21 | 22 | * `PYD-6: Add support for having transparent backgrounds in identicons 23 | `_ 24 | 25 | Ability to use alpha-channel specification in PNG identicons to obtain 26 | complete or partial transparency. Works for both background and foreground 27 | colour. 28 | 29 | * `PYD-7: Ability to specify image format 30 | `_ 31 | 32 | Ability to specify any output format supported by the Pillow library. 33 | 34 | 0.2 35 | --- 36 | 37 | A small release that adds support for Python 3 in addition to Python 2.7. 38 | 39 | New features: 40 | 41 | * `PYD-5: Add support for Python 3.x 42 | `_ 43 | 44 | Support for Python 3.x, in addition to Python 2.7. 45 | 46 | 0.1.1 47 | ----- 48 | 49 | This is a very small release feature-wise, with a single bug-fix. 50 | 51 | New features: 52 | 53 | * `PYD-3: Initial tests `_ 54 | 55 | Unit tests covering most of the library functionality. 56 | 57 | Bug fixes: 58 | 59 | * `PYD-4: Identicon generation using pre-hashed data raises ValueError 60 | `_ 61 | 62 | Fixed some flawed logic which prevented identicons to be generated from 63 | existing hashes. 64 | 65 | 0.1 66 | --- 67 | 68 | Initial release of Pydenticon. Implemented features: 69 | 70 | * Supported parameters for identicon generator (shared between multiple 71 | identicons): 72 | * Number of blocks in identicon (rows and columns). 73 | * Digest algorithm. 74 | * List of foreground colours to choose from. 75 | * Background colour. 76 | * Supported parameters when generating induvidual identicons: 77 | * Data that should be used for identicon generation. 78 | * Width and height of resulting image in pixels. 79 | * Padding around identicon (top, bottom, left, right). 80 | * Output format. 81 | * Inverted identicon (swaps foreground with background). 82 | * Support for PNG and ASCII format of resulting identicons. 83 | * Full documentation covering installation, usage, algorithm, privacy. API 84 | reference included as well. 85 | -------------------------------------------------------------------------------- /docs/privacy.rst: -------------------------------------------------------------------------------- 1 | Privacy 2 | ======= 3 | 4 | It is fundamentally important to understand the privacy issues if using 5 | Pydenticon in order to generate uniquelly identifiable avatars for users leaving 6 | the comments etc. 7 | 8 | The most common way to expose the identicons is by having a web application 9 | generate them on the fly from data that is being passed to it through HTTP GET 10 | requests. Those GET requests would commonly include either the raw data, or data 11 | as hex string that is then used to generate an identicon. The URLs for GET 12 | requests would most commonly be made as part of image tags in an HTML page. 13 | 14 | The data passed needs to be unique in order to generate distinct identicons. In 15 | most cases the data used will be either name or e-mail address that the visitor 16 | posting the comment fills-in in some field. That being said, e-mails usually 17 | provide a much better identifier than name (especially if the website verifies 18 | the comments through by sending-out e-mails). 19 | 20 | Needless to say, in such cases, especially if the website where the comments are 21 | being posted is public, using raw data can completely reveal the identity of the 22 | user. If e-mails are used for generating the identicons, the situation is even 23 | worse, since now those e-mails can be easily harvested for spam purposes. Using 24 | the e-mails also provides data mining companies with much more reliable user 25 | identifier that can be coupled with information from other websites. 26 | 27 | Therefore, it is highly recommended to pass the data to web application that 28 | generates the identicons using **hex digest only**. I.e. **never** pass the raw 29 | data. 30 | 31 | Although passing hash instead of real data as part of the GET request is a good 32 | step forward, it can still cause problems since the hashses can be collected, 33 | and then used in conjunction with rainbow tables to identify the original 34 | data. This is particularly problematic when using hex digests of e-mail 35 | addresses as data for generating the identicon. 36 | 37 | There's two feasible approaches to resolve this: 38 | 39 | * Always apply *salt* to user-identifiable data before calculating a hex 40 | digest. This can hugely reduce the efficiency of brute force attacks based on 41 | rainbow tables (althgouh it will not mitigate it completely). 42 | * Instead of hashing the user-identifiable data itself, every time you need to 43 | do so, create some random data instead, hash that random data, and store it 44 | for future use (cache it), linking it to the original data that it was 45 | generated for. This way the hex digest being put as part of an image link into 46 | HTML pages is not derived in any way from the original data, and can therefore 47 | not be used to reveal what the original data was. 48 | 49 | Keep in mind that using identicons will inevitably still allow people to track 50 | someone's posts across your website. Identicons will effectively automatically 51 | create pseudonyms for people posting on your website. If that may pose a 52 | problem, it might be better not to use identicons at all. 53 | 54 | Finally, small summary of the points explained above: 55 | 56 | * Always use hex digests in order to retrieve an identicon from a server. 57 | * Instead of using privately identifiable data for generating the hex digest, 58 | use randmoly generated data, and associate it with privately identifiable 59 | data. This way hex digest cannot be traced back to the original data through 60 | brute force or rainbow tables. 61 | * If unwilling to generate and store random data, at least make sure to use 62 | salt when hashing privately identifiable data. 63 | 64 | -------------------------------------------------------------------------------- /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 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Pydenticon.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Pydenticon.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /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 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pydenticon.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pydenticon.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Pydenticon" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pydenticon" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Pydenticon provides simple and straightforward interface for setting-up the 5 | identicon generator, and for generating the identicons. 6 | 7 | Instantiating a generator 8 | ------------------------- 9 | 10 | The starting point is to create a generator instance. Generator implements 11 | interface that can be used for generating the identicons. 12 | 13 | In its simplest form, the generator instances needs to be passed only the size 14 | of identicon in blocks (first parameter is width, second is height):: 15 | 16 | # Import the library. 17 | import pydenticon 18 | 19 | # Instantiate a generator that will create 5x5 block identicons. 20 | generator = pydenticon.Generator(5, 5) 21 | 22 | The above example will instantiate a generator that can be used for producing 23 | identicons which are 5x5 blocks in size, using the default values for digest 24 | (*MD5*), foreground colour (*black*), and background colour (*white*). 25 | 26 | Alternatively, you may choose to pass in a different digest algorithm, and 27 | foreground and background colours:: 28 | 29 | # Import the libraries. 30 | import pydenticon 31 | import hashlib 32 | 33 | # Set-up a list of foreground colours (taken from Sigil). 34 | foreground = [ "rgb(45,79,255)", 35 | "rgb(254,180,44)", 36 | "rgb(226,121,234)", 37 | "rgb(30,179,253)", 38 | "rgb(232,77,65)", 39 | "rgb(49,203,115)", 40 | "rgb(141,69,170)" ] 41 | 42 | # Set-up a background colour (taken from Sigil). 43 | background = "rgb(224,224,224)" 44 | 45 | # Instantiate a generator that will create 5x5 block identicons using SHA1 46 | # digest. 47 | generator = pydenticon.Generator(5, 5, digest=hashlib.sha1, 48 | foreground=foreground, background=background) 49 | 50 | Generating identicons 51 | --------------------- 52 | 53 | With generator initialised, it's now possible to use it to create the 54 | identicons. 55 | 56 | The most basic example would be creating an identicon using default padding (no 57 | padding) and output format ("png"), without inverting the colours (which is also 58 | the default):: 59 | 60 | # Generate a 240x240 PNG identicon. 61 | identicon = generator.generate("john.doe@example.com", 240, 240) 62 | 63 | The result of the ``generate()`` method will be a raw representation of an 64 | identicon image in requested format that can be written-out to file, sent back 65 | as an HTTP response etc. 66 | 67 | Usually it can be nice to have some padding around the generated identicon in 68 | order to make it stand-out better, or maybe to invert the colours. This can be 69 | done with:: 70 | 71 | # Set-up the padding (top, bottom, left, right) in pixels. 72 | padding = (20, 20, 20, 20) 73 | 74 | # Generate a 200x200 identicon with padding around it, and invert the 75 | # background/foreground colours. 76 | identicon = generator.generate("john.doe@example.com", 200, 200, 77 | padding=padding, inverted=True) 78 | 79 | Finally, the resulting identicons can be in different formats:: 80 | 81 | # Create identicon in PNG format. 82 | identicon_png = generator.generate("john.doe@example.com", 200, 200, 83 | output_format="png") 84 | # Create identicon in ASCII format. 85 | identicon_ascii = generator.generate("john.doe@example.com", 200, 200, 86 | output_format="ascii") 87 | 88 | Supported output formats are dependant on the local Pillow installation. For 89 | exact list of available formats, have a look at `Pillow documentation 90 | `_. The ``ascii`` format is the only format 91 | explicitly handled by the *Pydenticon* library itself (mainly useful for 92 | debugging purposes). 93 | 94 | Using the generated identicons 95 | ------------------------------ 96 | 97 | Of course, just generating the identicons is not that fun. They usually need 98 | either to be stored somewhere on disk, or maybe streamed back to the user via 99 | HTTP response. Since the generate function returns raw data, this is quite easy 100 | to achieve:: 101 | 102 | # Generate same identicon in three different formats. 103 | identicon_png = generator.generate("john.doe@example.com", 200, 200, 104 | output_format="png") 105 | identicon_gif = generator.generate("john.doe@example.com", 200, 200, 106 | output_format="gif") 107 | identicon_ascii = generator.generate("john.doe@example.com", 200, 200, 108 | output_format="ascii") 109 | 110 | # Identicon can be easily saved to a file. 111 | f = open("sample.png", "wb") 112 | f.write(identicon_png) 113 | f.close() 114 | 115 | f = open("sample.gif", "wb") 116 | f.write(identicon_gif) 117 | f.close() 118 | 119 | # ASCII identicon can be printed-out to console directly. 120 | print identicon_ascii 121 | 122 | 123 | Working with transparency 124 | ------------------------- 125 | 126 | .. note:: 127 | New in version ``0.3``. 128 | 129 | .. warning:: 130 | The only output format that properly supports transparency at the moment is 131 | ``PNG``. If you are using anything else, transparency will not work. 132 | 133 | If you ever find yourself in need of having a transparent background or 134 | foreground, you can easily do this using the syntax 135 | ``rgba(224,224,224,0)``. All this does is effectively adding alpha channel to 136 | selected colour. 137 | 138 | The alpha channel value ranges from ``0`` to ``255``, letting you specify how 139 | much transparency/opaqueness you want. For example, to have it at roughly 50% 140 | (more like at ``50.2%`` since you can't use fractions), you would simply specify 141 | value as ``rgba(224,224,224,128)``. 142 | 143 | 144 | Full example 145 | ------------ 146 | 147 | Finally, here is a full example that will create a number of identicons and 148 | output them in PNG format to local directory:: 149 | 150 | #!/usr/bin/env python 151 | 152 | # Import the libraries. 153 | import pydenticon 154 | import hashlib 155 | 156 | # Set-up some test data. 157 | users = ["alice", "bob", "eve", "dave"] 158 | 159 | # Set-up a list of foreground colours (taken from Sigil). 160 | foreground = [ "rgb(45,79,255)", 161 | "rgb(254,180,44)", 162 | "rgb(226,121,234)", 163 | "rgb(30,179,253)", 164 | "rgb(232,77,65)", 165 | "rgb(49,203,115)", 166 | "rgb(141,69,170)" ] 167 | 168 | # Set-up a background colour (taken from Sigil). 169 | background = "rgb(224,224,224)" 170 | 171 | # Set-up the padding (top, bottom, left, right) in pixels. 172 | padding = (20, 20, 20, 20) 173 | 174 | # Instantiate a generator that will create 5x5 block identicons using SHA1 175 | # digest. 176 | generator = pydenticon.Generator(5, 5, foreground=foreground, 177 | background=background) 178 | 179 | for user in users: 180 | identicon = generator.generate(user, 200, 200, padding=padding, 181 | output_format="png") 182 | 183 | filename = user + ".png" 184 | with open(filename, "wb") as f: 185 | f.write(identicon) 186 | 187 | -------------------------------------------------------------------------------- /docs/algorithm.rst: -------------------------------------------------------------------------------- 1 | Algorithm 2 | ========= 3 | 4 | A generated identicon can be described as one big rectangle divided into ``rows 5 | x columns`` rectangle blocks of equal size, where each block can be filled with 6 | the foreground colour or the background colour. Additionally, the whole 7 | identicon is symmetrical to the central vertical axis, making it much more 8 | aesthetically pleasing. 9 | 10 | The algorithm used for generating the identicon is fairly simple. The input 11 | arguments that determine what the identicon will look like are: 12 | 13 | * Size of identicon in blocks (``rows x columns``). 14 | * Algorithm used to create digests out of user-provided data. 15 | * List of colours used for foreground fill (foreground colours). This list will 16 | be referred to as ``foreground_list``. 17 | * Single colour used for background fill (background colour). This colour wil be 18 | referred to as ``background``. 19 | * Whether the foreground and background colours should be inverted (swapped) or 20 | not. 21 | * Data passed to be used for digest. 22 | 23 | The first step is to generate a *digest* out of the passed data using the 24 | selected digest algorithm. This digest is then split into two parts: 25 | 26 | * The first byte of digest (``f``, for foreground) is used for determining the 27 | foreground colour. 28 | * The remaining portion of digest (``l``, for layout) is used for determining 29 | which blocks of identicon will be filled using foreground and background 30 | colours. 31 | 32 | In order to select a ``foreground`` colour, the algorithm will try to determine 33 | the index of the colour in the ``foreground_list`` by doing modulo division of 34 | the first byte's integer value with number of colours in 35 | ``foreground_list``:: 36 | 37 | foreground = foreground_list[int(f) % len(foreground_list)] 38 | 39 | The layout of blocks (which block gets filled with foreground colour, and which 40 | block gets filled with background colour) is determined by the bit values of 41 | remaining portion of digest (``l``). This remaining portion of digest can also 42 | be seen as a list of bits. The bit positions would range from ``0`` to ``b`` 43 | (where the size of ``b`` would depend on the digest algoirthm that was picked). 44 | 45 | Since the identicon needs to be symmetrical, the number of blocks for which the 46 | fill colour needs to be calculated is equal to ``rows * (columns / 2 + columns % 47 | 2)``. I.e. the block matrix is split in half vertically (if number of columns is 48 | odd, the middle column is included as well). 49 | 50 | Those blocks can then be marked with whole numbers from ``0`` to ``c`` (where 51 | ``c`` would be equal to the above formula - ``rows * (columns / 2 + columns % 52 | 2)``). Number ``0`` would correspond to first block of the first half-row, ``1`` 53 | to the first block of the second row, ``2`` to the first block of the third row, 54 | and so on to the first block of the last half-row. Then the blocks in the next 55 | column would be indexed with numbers in a similar (incremental) way. 56 | 57 | With these two numbering methods in place (for digest bits and blocks of 58 | half-matrix), every block is assigned a bit that has the same position number. 59 | 60 | If no inversion of foreground and background colours was requested, bit value of 61 | ``1`` for a cell would mean the block should be filled with foreground colour, 62 | while value of ``0`` would mean the block should be filled with background 63 | colour. 64 | 65 | If an inverted identicon was requested, then ``1`` would correspond to 66 | background colour fill, and ``0`` would correspond to foreground colour fill. 67 | 68 | Examples 69 | -------- 70 | 71 | An identicon should be created with the following parameters: 72 | 73 | * Size of identicon in blocks is ``5 x 5`` (a square). 74 | * Digest algorithm is *MD5*. 75 | * Five colours are used for identicon foreground (``0`` through ``4``). 76 | * Some background colour is selected (marked as ``b``). 77 | * Foreground and background colours are not to be inverted (swapped). 78 | * Data used for digest is ``branko``. 79 | 80 | MD5 digest for data (``branko``) would be (reperesented as hex value) equal to 81 | ``d41c0e80c44173dcf7575745bdddb704``. 82 | 83 | In other words, 16 bytes would be present with the following hex values:: 84 | 85 | d4 1c 0e 80 c4 41 73 dc f7 57 57 45 bd dd b7 04 86 | 87 | Following the algorithm, the first byte (``d4``) is used to determine which 88 | foreground colour to use. ``d4`` is equal to ``212`` in decimal format. Divided 89 | by modulo ``5`` (number of foreground colours), the resulting index of 90 | foreground colour is ``2`` (third colour in the foreground list). 91 | 92 | The remaining 15 bytes will be used for figuring out the layout. The 93 | representation of those bytes in binary format would look like this (5 bytes per 94 | row):: 95 | 96 | 00011100 00001110 10000000 11000100 01000001 97 | 01110011 11011100 11110111 01010111 01010111 98 | 01000101 10111101 11011101 10110111 00000100 99 | 100 | Since identicon consits out of 5 columns and 5 rows, the number of bits that's 101 | needed from ``l`` for the layout would be ``5 * (5 / 2 + 5 % 2) == 15``. This 102 | means that the following bits will determine the layout of identicon (whole 103 | first byte, and 7 bits of the second byte):: 104 | 105 | 00011100 0000111 106 | 107 | The half-matrix would therefore end-up looking like this (5 bits per column for 108 | 5 blocks per column):: 109 | 110 | 010 111 | 000 112 | 001 113 | 101 114 | 101 115 | 116 | The requested identicon is supposed to have 5 block columns, so a reflection 117 | will be applied to the first and second column, with third column as center of 118 | the symmetry. This would result in the following ideticon matrix:: 119 | 120 | 01010 121 | 00000 122 | 00100 123 | 10101 124 | 10101 125 | 126 | Since no inversion was requested, ``1`` would correspond to calculated 127 | foreground colour, while ``0`` would correspond to provided background colour. 128 | 129 | To spicen the example up a bit, here is what the above identicon would look like 130 | in regular and inverted variant (with some sample foreground colours and a bit 131 | of padding): 132 | 133 | .. image:: images/branko.png 134 | .. image:: images/branko_inverted.png 135 | 136 | Limitations 137 | ----------- 138 | 139 | There's some practical limitations to the algorithm described above. 140 | 141 | The first limitation is the maximum number of different foreground colours that 142 | can be used for identicon generation. Since a single byte (which is used to 143 | determining the colour) can represent 256 values (between 0 and 255), there can 144 | be no more than 256 colours passed to be used for foreground of the 145 | identicon. Any extra colours passed above that count would simply be ignored. 146 | 147 | The second limitation is that the maximum dimensions (in blocks) of a generated 148 | identicon depend on digest algorithm used. In order for a digest algorithm to be 149 | able to satisfy requirements of producing an identcion with ``rows`` number of 150 | rows, and ``columns`` number of columns (in blocks), it must be able to produce at 151 | least the following number of bits (i.e. the number of bits equal to the number 152 | of blocks in the half-matrix):: 153 | 154 | rows * (columns / 2 + columns % 2) + 8 155 | 156 | The expression is the result of vertical symmetry of identicon. Only the 157 | columns up to, and including, the middle one middle one (``(columns / 2 + colums 158 | % 2)``) need to be processed, with every one of those columns having ``row`` 159 | rows (``rows *``). Finally, an extra 8 bits (1 byte) are necessary for 160 | determining the foreground colour. 161 | 162 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Pydenticon documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Nov 25 23:13:33 2013. 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 | sys.path.insert(0, os.path.abspath('..')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Pydenticon' 45 | copyright = u'2013, Branko Majic' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '0.3-dev' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '0.3-dev' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'default' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | #html_theme_path = [] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | html_logo = "images/logo.png" 115 | 116 | # The name of an image file (within the static path) to use as favicon of the 117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | html_favicon = "images/favicon.ico" 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ['_static'] 125 | 126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 127 | # using the given strftime format. 128 | #html_last_updated_fmt = '%b %d, %Y' 129 | 130 | # If true, SmartyPants will be used to convert quotes and dashes to 131 | # typographically correct entities. 132 | #html_use_smartypants = True 133 | 134 | # Custom sidebar templates, maps document names to template names. 135 | #html_sidebars = {} 136 | 137 | # Additional templates that should be rendered to pages, maps page names to 138 | # template names. 139 | #html_additional_pages = {} 140 | 141 | # If false, no module index is generated. 142 | #html_domain_indices = True 143 | 144 | # If false, no index is generated. 145 | #html_use_index = True 146 | 147 | # If true, the index is split into individual pages for each letter. 148 | #html_split_index = False 149 | 150 | # If true, links to the reST sources are added to the pages. 151 | #html_show_sourcelink = True 152 | 153 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 154 | #html_show_sphinx = True 155 | 156 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 157 | #html_show_copyright = True 158 | 159 | # If true, an OpenSearch description file will be output, and all pages will 160 | # contain a tag referring to it. The value of this option must be the 161 | # base URL from which the finished HTML is served. 162 | #html_use_opensearch = '' 163 | 164 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 165 | #html_file_suffix = None 166 | 167 | # Output file base name for HTML help builder. 168 | htmlhelp_basename = 'Pydenticondoc' 169 | 170 | 171 | # -- Options for LaTeX output -------------------------------------------------- 172 | 173 | latex_elements = { 174 | # The paper size ('letterpaper' or 'a4paper'). 175 | #'papersize': 'letterpaper', 176 | 177 | # The font size ('10pt', '11pt' or '12pt'). 178 | #'pointsize': '10pt', 179 | 180 | # Additional stuff for the LaTeX preamble. 181 | #'preamble': '', 182 | } 183 | 184 | # Grouping the document tree into LaTeX files. List of tuples 185 | # (source start file, target name, title, author, documentclass [howto/manual]). 186 | latex_documents = [ 187 | ('index', 'Pydenticon.tex', u'Pydenticon Documentation', 188 | u'Branko Majic', 'manual'), 189 | ] 190 | 191 | # The name of an image file (relative to this directory) to place at the top of 192 | # the title page. 193 | #latex_logo = None 194 | 195 | # For "manual" documents, if this is true, then toplevel headings are parts, 196 | # not chapters. 197 | #latex_use_parts = False 198 | 199 | # If true, show page references after internal links. 200 | #latex_show_pagerefs = False 201 | 202 | # If true, show URL addresses after external links. 203 | #latex_show_urls = False 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_domain_indices = True 210 | 211 | 212 | # -- Options for manual page output -------------------------------------------- 213 | 214 | # One entry per manual page. List of tuples 215 | # (source start file, name, description, authors, manual section). 216 | man_pages = [ 217 | ('index', 'pydenticon', u'Pydenticon Documentation', 218 | [u'Branko Majic'], 1) 219 | ] 220 | 221 | # If true, show URL addresses after external links. 222 | #man_show_urls = False 223 | 224 | 225 | # -- Options for Texinfo output ------------------------------------------------ 226 | 227 | # Grouping the document tree into Texinfo files. List of tuples 228 | # (source start file, target name, title, author, 229 | # dir menu entry, description, category) 230 | texinfo_documents = [ 231 | ('index', 'Pydenticon', u'Pydenticon Documentation', 232 | u'Branko Majic', 'Pydenticon', 'One line description of project.', 233 | 'Miscellaneous'), 234 | ] 235 | 236 | # Documents to append as an appendix to all manuals. 237 | #texinfo_appendices = [] 238 | 239 | # If false, no module index is generated. 240 | #texinfo_domain_indices = True 241 | 242 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 243 | #texinfo_show_urls = 'footnote' 244 | -------------------------------------------------------------------------------- /pydenticon/__init__.py: -------------------------------------------------------------------------------- 1 | # For digest operations. 2 | import hashlib 3 | 4 | # For saving the images from Pillow. 5 | from io import BytesIO 6 | 7 | # Pillow for Image processing. 8 | from PIL import Image, ImageDraw 9 | 10 | # For decoding hex values (works both for Python 2.7.x and Python 3.x). 11 | import binascii 12 | 13 | 14 | class Generator(object): 15 | """ 16 | Factory class that can be used for generating the identicons 17 | deterministically based on hash of the passed data. 18 | 19 | Resulting identicons are images of requested size with optional padding. The 20 | identicon (without padding) consists out of M x N blocks, laid out in a 21 | rectangle, where M is the number of blocks in each column, while N is number 22 | of blocks in each row. 23 | 24 | Each block is a smallself rectangle on its own, filled using the foreground or 25 | background colour. 26 | 27 | The foreground is picked randomly, based on the passed data, from the list 28 | of foreground colours set during initialisation of the generator. 29 | 30 | The blocks are always laid-out in such a way that the identicon will be 31 | symterical by the Y axis. The center of symetry will be the central column 32 | of blocks. 33 | 34 | Simply put, the generated identicons are small symmetric mosaics with 35 | optional padding. 36 | """ 37 | 38 | def __init__(self, rows, columns, digest=hashlib.md5, foreground=["#000000"], background="#ffffff"): 39 | """ 40 | Initialises an instance of identicon generator. The instance can be used 41 | for creating identicons with differing image formats, sizes, and with 42 | different padding. 43 | 44 | Arguments: 45 | 46 | rows - Number of block rows in an identicon. 47 | 48 | columns - Number of block columns in an identicon. 49 | 50 | digest - Digest class that should be used for the user's data. The 51 | class should support accepting a single constructor argument for 52 | passing the data on which the digest will be run. Instances of the 53 | class should also support a single hexdigest() method that should 54 | return a digest of passed data as a hex string. Default is 55 | hashlib.md5. Selection of the digest will limit the maximum values 56 | that can be set for rows and columns. Digest needs to be able to 57 | generate (columns / 2 + columns % 2) * rows + 8 bits of entropy. 58 | 59 | foreground - List of colours which should be used for drawing the 60 | identicon. Each element should be a string of format supported by the 61 | PIL.ImageColor module. Default is ["#000000"] (only black). 62 | 63 | background - Colour (single) which should be used for background and 64 | padding, represented as a string of format supported by the 65 | PIL.ImageColor module. Default is "#ffffff" (white). 66 | """ 67 | 68 | # Check if the digest produces sufficient entropy for identicon 69 | # generation. 70 | entropy_provided = len(digest(b"test").hexdigest()) // 2 * 8 71 | entropy_required = (columns // 2 + columns % 2) * rows + 8 72 | 73 | if entropy_provided < entropy_required: 74 | raise ValueError("Passed digest '%s' is not capable of providing %d bits of entropy" % (str(digest), entropy_required)) 75 | 76 | # Set the expected digest size. This is used later on to detect if 77 | # passed data is a digest already or not. 78 | self.digest_entropy = entropy_provided 79 | 80 | self.rows = rows 81 | self.columns = columns 82 | 83 | self.foreground = foreground 84 | self.background = background 85 | 86 | self.digest = digest 87 | 88 | def _get_bit(self, n, hash_bytes): 89 | """ 90 | Determines if the n-th bit of passed bytes is 1 or 0. 91 | 92 | Arguments: 93 | 94 | hash_bytes - List of hash byte values for which the n-th bit value 95 | should be checked. Each element of the list should be an integer from 96 | 0 to 255. 97 | 98 | Returns: 99 | 100 | True if the bit is 1. False if the bit is 0. 101 | """ 102 | 103 | if hash_bytes[n // 8] >> int(8 - ((n % 8) + 1)) & 1 == 1: 104 | return True 105 | 106 | return False 107 | 108 | def _generate_matrix(self, hash_bytes): 109 | """ 110 | Generates matrix that describes which blocks should be coloured. 111 | 112 | Arguments: 113 | hash_bytes - List of hash byte values for which the identicon is being 114 | generated. Each element of the list should be an integer from 0 to 115 | 255. 116 | 117 | Returns: 118 | List of rows, where each element in a row is boolean. True means the 119 | foreground colour should be used, False means a background colour 120 | should be used. 121 | """ 122 | 123 | # Since the identicon needs to be symmetric, we'll need to work on half 124 | # the columns (rounded-up), and reflect where necessary. 125 | half_columns = self.columns // 2 + self.columns % 2 126 | cells = self.rows * half_columns 127 | 128 | # Initialise the matrix (list of rows) that will be returned. 129 | matrix = [[False] * self.columns for _ in range(self.rows)] 130 | 131 | # Process the cells one by one. 132 | for cell in range(cells): 133 | 134 | # If the bit from hash correpsonding to this cell is 1, mark the 135 | # cell as foreground one. Do not use first byte (since that one is 136 | # used for determining the foreground colour. 137 | if self._get_bit(cell, hash_bytes[1:]): 138 | 139 | # Determine the cell coordinates in matrix. 140 | column = cell // self.columns 141 | row = cell % self.rows 142 | 143 | # Mark the cell and its reflection. Central column may get 144 | # marked twice, but we don't care. 145 | matrix[row][column] = True 146 | matrix[row][self.columns - column - 1] = True 147 | 148 | return matrix 149 | 150 | def _data_to_digest_byte_list(self, data): 151 | """ 152 | Creates digest of data, returning it as a list where every element is a 153 | single byte of digest (an integer between 0 and 255). 154 | 155 | No digest will be calculated on the data if the passed data is already a 156 | valid hex string representation of digest, and the passed value will be 157 | used as digest in hex string format instead. 158 | 159 | Arguments: 160 | 161 | data - Raw data or hex string representation of existing digest for 162 | which a list of one-byte digest values should be returned. 163 | 164 | Returns: 165 | 166 | List of integers where each element is between 0 and 255, and 167 | repesents a single byte of a data digest. 168 | """ 169 | 170 | # If data seems to provide identical amount of entropy as digest, it 171 | # could be a hex digest already. 172 | if len(data) // 2 == self.digest_entropy // 8: 173 | try: 174 | binascii.unhexlify(data.encode('utf-8')) 175 | digest = data.encode('utf-8') 176 | # Handle Python 2.x exception. 177 | except (TypeError): 178 | digest = self.digest(data.encode('utf-8')).hexdigest() 179 | # Handle Python 3.x exception. 180 | except (binascii.Error): 181 | digest = self.digest(data.encode('utf-8')).hexdigest() 182 | else: 183 | digest = self.digest(data.encode('utf-8')).hexdigest() 184 | 185 | return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)] 186 | 187 | def _generate_image(self, matrix, width, height, padding, foreground, background, image_format): 188 | """ 189 | Generates an identicon image in requested image format out of the passed 190 | block matrix, with the requested width, height, padding, foreground 191 | colour, background colour, and image format. 192 | 193 | Arguments: 194 | 195 | matrix - Matrix describing which blocks in the identicon should be 196 | painted with foreground (background if inverted) colour. 197 | 198 | width - Width of resulting identicon image in pixels. 199 | 200 | height - Height of resulting identicon image in pixels. 201 | 202 | padding - Tuple describing padding around the generated identicon. The 203 | tuple should consist out of four values, where each value is the 204 | number of pixels to use for padding. The order in tuple is: top, 205 | bottom, left, right. 206 | 207 | foreground - Colour which should be used for foreground (filled 208 | blocks), represented as a string of format supported by the 209 | PIL.ImageColor module. 210 | 211 | background - Colour which should be used for background and padding, 212 | represented as a string of format supported by the PIL.ImageColor 213 | module. 214 | 215 | image_format - Format to use for the image. Format needs to be 216 | supported by the Pillow library. 217 | 218 | Returns: 219 | 220 | Identicon image in requested format, returned as a byte list. 221 | """ 222 | 223 | # Set-up a new image object, setting the background to provided value. 224 | image = Image.new("RGBA", (width + padding[2] + padding[3], height + padding[0] + padding[1]), background) 225 | 226 | # Set-up a draw image (for drawing the blocks). 227 | draw = ImageDraw.Draw(image) 228 | 229 | # Calculate the block widht and height. 230 | block_width = width // self.columns 231 | block_height = height // self.rows 232 | 233 | # Go through all the elements of a matrix, and draw the rectangles. 234 | for row, row_columns in enumerate(matrix): 235 | for column, cell in enumerate(row_columns): 236 | if cell: 237 | # Set-up the coordinates for a block. 238 | x1 = padding[2] + column * block_width 239 | y1 = padding[0] + row * block_height 240 | x2 = padding[2] + (column + 1) * block_width - 1 241 | y2 = padding[0] + (row + 1) * block_height - 1 242 | 243 | # Draw the rectangle. 244 | draw.rectangle((x1, y1, x2, y2), fill=foreground) 245 | 246 | # Set-up a stream where image will be saved. 247 | stream = BytesIO() 248 | 249 | if image_format.upper() == "JPEG": 250 | image = image.convert(mode="RGB") 251 | 252 | # Save the image to stream. 253 | try: 254 | image.save(stream, format=image_format, optimize=True) 255 | except KeyError: 256 | raise ValueError("Pillow does not support requested image format: %s" % image_format) 257 | image_raw = stream.getvalue() 258 | stream.close() 259 | 260 | # Return the resulting image. 261 | return image_raw 262 | 263 | def _generate_ascii(self, matrix, foreground, background): 264 | """ 265 | Generates an identicon "image" in the ASCII format. The image will just 266 | output the matrix used to generate the identicon. 267 | 268 | Arguments: 269 | 270 | matrix - Matrix describing which blocks in the identicon should be 271 | painted with foreground (background if inverted) colour. 272 | 273 | foreground - Character which should be used for representing 274 | foreground. 275 | 276 | background - Character which should be used for representing 277 | background. 278 | 279 | Returns: 280 | 281 | ASCII representation of an identicon image, where one block is one 282 | character. 283 | """ 284 | 285 | return "\n".join(["".join([foreground if cell else background for cell in row]) for row in matrix]) 286 | 287 | def generate(self, data, width, height, padding=(0, 0, 0, 0), output_format="png", inverted=False): 288 | """ 289 | Generates an identicon image with requested width, height, padding, and 290 | output format, optionally inverting the colours in the indeticon 291 | (swapping background and foreground colours) if requested. 292 | 293 | Arguments: 294 | 295 | data - Hashed or raw data that will be used for generating the 296 | identicon. 297 | 298 | width - Width of resulting identicon image in pixels. 299 | 300 | height - Height of resulting identicon image in pixels. 301 | 302 | padding - Tuple describing padding around the generated identicon. The 303 | tuple should consist out of four values, where each value is the 304 | number of pixels to use for padding. The order in tuple is: top, 305 | bottom, left, right. 306 | 307 | output_format - Output format of resulting identicon image. Supported 308 | formats are anything that is supported by Pillow, plus a special 309 | "ascii" mode. 310 | 311 | inverted - Specifies whether the block colours should be inverted or 312 | not. Default is False. 313 | 314 | Returns: 315 | 316 | Byte representation of an identicon image. 317 | """ 318 | 319 | # Calculate the digest, and get byte list. 320 | digest_byte_list = self._data_to_digest_byte_list(data) 321 | 322 | # Create the matrix describing which block should be filled-in. 323 | matrix = self._generate_matrix(digest_byte_list) 324 | 325 | # Determine the background and foreground colours. 326 | if output_format == "ascii": 327 | foreground = "+" 328 | background = "-" 329 | else: 330 | background = self.background 331 | foreground = self.foreground[digest_byte_list[0] % len(self.foreground)] 332 | 333 | # Swtich the colours if inverted image was requested. 334 | if inverted: 335 | foreground, background = background, foreground 336 | 337 | # Generate the identicon in requested format. 338 | if output_format == "ascii": 339 | return self._generate_ascii(matrix, foreground, background) 340 | else: 341 | return self._generate_image(matrix, width, height, padding, foreground, background, output_format) 342 | -------------------------------------------------------------------------------- /tests/test_pydenticon.py: -------------------------------------------------------------------------------- 1 | # Standard library imports. 2 | import hashlib 3 | import unittest 4 | from io import BytesIO 5 | 6 | # Third-party Python library imports. 7 | import mock 8 | import PIL 9 | import PIL.ImageChops 10 | 11 | # Library imports. 12 | from pydenticon import Generator 13 | 14 | 15 | class GeneratorTest(unittest.TestCase): 16 | """ 17 | Implements tests for pydenticon.Generator class. 18 | """ 19 | 20 | def test_init_entropy(self): 21 | """ 22 | Tests if the constructor properly checks for entropy provided by a 23 | digest algorithm. 24 | """ 25 | 26 | # Set-up the mock instance. 27 | hexdigest_method = mock.MagicMock(return_value="aabb") 28 | digest_instance = mock.MagicMock() 29 | digest_instance.hexdigest = hexdigest_method 30 | 31 | # Set-up digest function that will always return the same digest 32 | # instance. 33 | digest_method = mock.MagicMock(return_value=digest_instance) 34 | 35 | # This should require 23 bits of entropy, while the digest we defined 36 | # provided 2*8 bits of entropy (2 bytes). 37 | self.assertRaises(ValueError, Generator, 5, 5, digest=digest_method) 38 | 39 | def test_init_parameters(self): 40 | """ 41 | Verifies that the constructor sets-up the instance properties correctly. 42 | """ 43 | 44 | generator = Generator(5, 5, digest=hashlib.sha1, foreground=["#111111", "#222222"], background="#aabbcc") 45 | 46 | # sha1 provides 160 bits of entropy - 20 bytes. 47 | self.assertEqual(generator.digest_entropy, 20 * 8) 48 | self.assertEqual(generator.digest, hashlib.sha1) 49 | self.assertEqual(generator.rows, 5) 50 | self.assertEqual(generator.columns, 5) 51 | self.assertEqual(generator.foreground, ["#111111", "#222222"]) 52 | self.assertEqual(generator.background, "#aabbcc") 53 | 54 | def test_get_bit(self): 55 | """ 56 | Tests if the check whether bit is 1 or 0 is performed correctly. 57 | """ 58 | 59 | generator = Generator(5, 5) 60 | hash_bytes = [0b10010001, 0b10001000, 0b00111001] 61 | 62 | # Check a couple of bits from the above hash bytes. 63 | self.assertEqual(True, generator._get_bit(0, hash_bytes)) 64 | self.assertEqual(True, generator._get_bit(7, hash_bytes)) 65 | self.assertEqual(False, generator._get_bit(22, hash_bytes)) 66 | self.assertEqual(True, generator._get_bit(23, hash_bytes)) 67 | 68 | def test_generate_matrix(self): 69 | """ 70 | Verifies that the matrix is generated correctly based on passed hashed 71 | bytes. 72 | """ 73 | 74 | # The resulting half-matrix should be as follows (first byte is for 75 | # ignored in matrix generation): 76 | # 77 | # 100 78 | # 011 79 | # 100 80 | # 001 81 | # 110 82 | hash_bytes = [0b11111111, 0b10101010, 0b01010101] 83 | 84 | expected_matrix = [ 85 | [True, False, False, False, True], 86 | [False, True, True, True, False], 87 | [True, False, False, False, True], 88 | [False, False, True, False, False], 89 | [True, True, False, True, True], 90 | ] 91 | 92 | generator = Generator(5, 5) 93 | 94 | matrix = generator._generate_matrix(hash_bytes) 95 | 96 | self.assertEqual(matrix, expected_matrix) 97 | 98 | def test_data_to_digest_byte_list_raw(self): 99 | """ 100 | Test if correct digest byte list is returned for raw (non-hex-digest) 101 | data passed to the method. 102 | """ 103 | 104 | # Set-up some raw data, and set-up the expected result. 105 | data = "this is a test\n" 106 | expected_digest_byte_list = [225, 156, 18, 131, 201, 37, 179, 32, 102, 133, 255, 82, 42, 207, 227, 230] 107 | 108 | # Instantiate a generator. 109 | generator = Generator(5, 5, digest=hashlib.md5) 110 | 111 | # Call the method and get the results. 112 | digest_byte_list = generator._data_to_digest_byte_list(data) 113 | 114 | # Verify the expected and actual result are identical. 115 | self.assertEqual(expected_digest_byte_list, digest_byte_list) 116 | 117 | def test_data_to_digest_byte_list_hex(self): 118 | """ 119 | Test if correct digest byte list is returned for passed hex digest 120 | string. 121 | """ 122 | 123 | # Set-up some test hex digest (md5), and expected result. 124 | hex_digest = "e19c1283c925b3206685ff522acfe3e6" 125 | expected_digest_byte_list = [225, 156, 18, 131, 201, 37, 179, 32, 102, 133, 255, 82, 42, 207, 227, 230] 126 | 127 | # Instantiate a generator. 128 | generator = Generator(5, 5, digest=hashlib.md5) 129 | 130 | # Call the method and get the results. 131 | digest_byte_list = generator._data_to_digest_byte_list(hex_digest) 132 | 133 | # Verify the expected and actual result are identical. 134 | self.assertEqual(expected_digest_byte_list, digest_byte_list) 135 | 136 | def test_data_to_digest_byte_list_hex_lookalike(self): 137 | """ 138 | Test if correct digest byte list is returned for passed raw data that 139 | has same length as hex digest string. 140 | """ 141 | 142 | # Set-up some test hex digest (md5), and expected result. 143 | data = "qqwweerrttyyuuiiooppaassddffgghh" 144 | expected_digest_byte_list = [25, 182, 52, 218, 118, 220, 26, 145, 164, 222, 33, 221, 183, 140, 98, 246] 145 | 146 | # Instantiate a generator. 147 | generator = Generator(5, 5, digest=hashlib.md5) 148 | 149 | # Call the method and get the results. 150 | digest_byte_list = generator._data_to_digest_byte_list(data) 151 | 152 | # Verify the expected and actual result are identical. 153 | self.assertEqual(expected_digest_byte_list, digest_byte_list) 154 | 155 | def test_generate_image_basics(self): 156 | """ 157 | Tests some basics about generated PNG identicon image. This includes: 158 | 159 | - Dimensions of generated image. 160 | - Format of generated image. 161 | - Mode of generated image. 162 | """ 163 | 164 | # Set-up parameters that will be used for generating the image. 165 | width = 200 166 | height = 200 167 | padding = [20, 20, 20, 20] 168 | foreground = "#ffffff" 169 | background = "#000000" 170 | matrix = [ 171 | [0, 0, 1, 0, 0], 172 | [0, 0, 1, 0, 0], 173 | [0, 0, 1, 0, 0], 174 | [0, 1, 1, 1, 0], 175 | [0, 1, 1, 1, 0], 176 | ] 177 | 178 | # Set-up a generator. 179 | generator = Generator(5, 5) 180 | 181 | # Generate the raw image. 182 | raw_image = generator._generate_image(matrix, width, height, padding, foreground, background, "png") 183 | 184 | # Try to load the raw image. 185 | image_stream = BytesIO(raw_image) 186 | image = PIL.Image.open(image_stream) 187 | 188 | # Verify image size, format, and mode. 189 | self.assertEqual(image.size[0], 240) 190 | self.assertEqual(image.size[1], 240) 191 | self.assertEqual(image.format, "PNG") 192 | self.assertEqual(image.mode, "RGBA") 193 | 194 | def test_generate_ascii(self): 195 | """ 196 | Tests the generated identicon in ASCII format. 197 | """ 198 | 199 | # Set-up parameters that will be used for generating the image. 200 | foreground = "1" 201 | background = "0" 202 | matrix = [ 203 | [0, 0, 1, 0, 0], 204 | [0, 0, 1, 0, 0], 205 | [0, 0, 1, 0, 0], 206 | [0, 1, 1, 1, 0], 207 | [0, 1, 1, 1, 0], 208 | ] 209 | 210 | # Set-up a generator. 211 | generator = Generator(5, 5) 212 | 213 | # Generate the ASCII image. 214 | ascii_image = generator._generate_ascii(matrix, foreground, background) 215 | 216 | # Verify that the result is as expected. 217 | expected_result = """00100 218 | 00100 219 | 00100 220 | 01110 221 | 01110""" 222 | self.assertEqual(ascii_image, expected_result) 223 | 224 | def test_generate_format(self): 225 | """ 226 | Tests if identicons are generated in requested format. 227 | """ 228 | 229 | # Set-up a generator. 230 | generator = Generator(5, 5) 231 | 232 | # Set-up some test data. 233 | data = "some test data" 234 | 235 | # Verify that PNG image is returned when requested. 236 | raw_image = generator.generate(data, 200, 200, output_format="png") 237 | image_stream = BytesIO(raw_image) 238 | image = PIL.Image.open(image_stream) 239 | self.assertEqual(image.format, "PNG") 240 | 241 | # Verify that JPEG image is returned when requested. 242 | raw_image = generator.generate(data, 200, 200, output_format="jpeg") 243 | image_stream = BytesIO(raw_image) 244 | image = PIL.Image.open(image_stream) 245 | self.assertEqual(image.format, "JPEG") 246 | 247 | # Verify that GIF image is returned when requested. 248 | raw_image = generator.generate(data, 200, 200, output_format="gif") 249 | image_stream = BytesIO(raw_image) 250 | image = PIL.Image.open(image_stream) 251 | self.assertEqual(image.format, "GIF") 252 | 253 | # Verify that ASCII "image" is returned when requested. 254 | raw_image = generator.generate(data, 200, 200, output_format="ascii") 255 | self.assertIsInstance(raw_image, str) 256 | 257 | def test_generate_format_invalid(self): 258 | """ 259 | Tests if an exception is raised in case an unsupported format is 260 | requested when generating the identicon. 261 | """ 262 | 263 | # Set-up a generator. 264 | generator = Generator(5, 5) 265 | 266 | # Set-up some test data. 267 | data = "some test data" 268 | 269 | # Verify that an exception is raised in case of unsupported format. 270 | self.assertRaises(ValueError, generator.generate, data, 200, 200, output_format="invalid") 271 | 272 | @mock.patch.object(Generator, '_generate_image') 273 | def test_generate_inverted_png(self, generate_image_mock): 274 | """ 275 | Tests if the foreground and background are properly inverted when 276 | generating PNG images. 277 | """ 278 | 279 | # Set-up some test data. 280 | data = "Some test data" 281 | 282 | # Set-up one foreground and background colour. 283 | foreground = "#ffffff" 284 | background = "#000000" 285 | 286 | # Set-up the generator. 287 | generator = Generator(5, 5, foreground=[foreground], background=background) 288 | 289 | # Verify that colours are picked correctly when no inverstion is requsted. 290 | generator.generate(data, 200, 200, inverted=False, output_format="png") 291 | generate_image_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, foreground, background, "png") 292 | 293 | # Verify that colours are picked correctly when inversion is requsted. 294 | generator.generate(data, 200, 200, inverted=True, output_format="png") 295 | generate_image_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, background, foreground, "png") 296 | 297 | @mock.patch.object(Generator, '_generate_ascii') 298 | def test_generate_inverted_ascii(self, generate_ascii_mock): 299 | """ 300 | Tests if the foreground and background are properly inverted when 301 | generating ASCII "images". 302 | """ 303 | 304 | # Set-up some test data. 305 | data = "Some test data" 306 | 307 | # Set-up one foreground and background colour. These are not used for 308 | # ASCII itself (instead a plus/minus sign is used). 309 | foreground = "#ffffff" 310 | background = "#000000" 311 | 312 | # Set-up the generator. 313 | generator = Generator(5, 5, foreground=[foreground], background=background) 314 | 315 | # Verify that foreground/background is picked correctly when no 316 | # inverstion is requsted. 317 | generator.generate(data, 200, 200, inverted=False, output_format="ascii") 318 | generate_ascii_mock.assert_called_with(mock.ANY, "+", "-") 319 | 320 | # Verify that foreground/background is picked correctly when inversion 321 | # is requsted. 322 | generator.generate(data, 200, 200, inverted=True, output_format="ascii") 323 | generate_ascii_mock.assert_called_with(mock.ANY, "-", "+") 324 | 325 | @mock.patch.object(Generator, '_generate_image') 326 | def test_generate_foreground(self, generate_image_mock): 327 | """ 328 | Tests if the foreground colour is picked correctly. 329 | """ 330 | 331 | # Set-up some foreground colours and a single background colour. 332 | foreground = ["#000000", "#111111", "#222222", "#333333", "#444444", "#555555"] 333 | background = "#ffffff" 334 | 335 | # Set-up the generator. 336 | generator = Generator(5, 5, foreground=foreground, background=background) 337 | 338 | # The first byte of hex digest should be 121 for this data, which should 339 | # result in foreground colour of index '1'. 340 | data = "some test data" 341 | generator.generate(data, 200, 200) 342 | generate_image_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, foreground[1], background, "png") 343 | 344 | # The first byte of hex digest should be 149 for this data, which should 345 | # result in foreground colour of index '5'. 346 | data = "some other test data" 347 | generator.generate(data, 200, 200) 348 | generate_image_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, foreground[5], background, "png") 349 | 350 | def test_generate_image_compare(self): 351 | """ 352 | Tests generated PNG identicon against a set of pre-generated samples. 353 | """ 354 | 355 | # Set-up a list of foreground colours (taken from Sigil). Same as used 356 | # for reference images. 357 | foreground = ["rgb(45,79,255)", 358 | "rgb(254,180,44)", 359 | "rgb(226,121,234)", 360 | "rgb(30,179,253)", 361 | "rgb(232,77,65)", 362 | "rgb(49,203,115)", 363 | "rgb(141,69,170)"] 364 | 365 | # Set-up a background colour (taken from Sigil). Same as used for 366 | # reference images. 367 | background = "rgb(224,224,224)" 368 | 369 | # Set-up parameters equivalent as used for samples. 370 | width = 200 371 | height = 200 372 | padding = (20, 20, 20, 20) 373 | 374 | # Load the reference images, making sure they're in RGBA mode. 375 | test1_ref = PIL.Image.open("tests/samples/test1.png").convert(mode="RGBA") 376 | test2_ref = PIL.Image.open("tests/samples/test2.png").convert(mode="RGBA") 377 | test3_ref = PIL.Image.open("tests/samples/test3.png").convert(mode="RGBA") 378 | 379 | # Set-up the Generator. 380 | generator = Generator(5, 5, foreground=foreground, background=background) 381 | 382 | # Generate first test identicon. 383 | raw_image = generator.generate("test1", width, height, padding=padding) 384 | image_stream = BytesIO(raw_image) 385 | test1 = PIL.Image.open(image_stream) 386 | 387 | # Generate second test identicon. 388 | raw_image = generator.generate("test2", width, height, padding=padding) 389 | image_stream = BytesIO(raw_image) 390 | test2 = PIL.Image.open(image_stream) 391 | 392 | # Generate third test identicon. 393 | raw_image = generator.generate("test3", width, height, padding=padding) 394 | image_stream = BytesIO(raw_image) 395 | test3 = PIL.Image.open(image_stream) 396 | 397 | # Calculate differences between generated identicons and references. 398 | diff1 = PIL.ImageChops.difference(test1, test1_ref) 399 | diff2 = PIL.ImageChops.difference(test2, test2_ref) 400 | diff3 = PIL.ImageChops.difference(test3, test3_ref) 401 | 402 | # Verify that all the diffs are essentially black (i.e. no differences 403 | # between generated identicons and reference samples). 404 | expected_extrema = ((0, 0), (0, 0), (0, 0), (0, 0)) 405 | 406 | self.assertEqual(diff1.getextrema(), expected_extrema) 407 | self.assertEqual(diff2.getextrema(), expected_extrema) 408 | self.assertEqual(diff3.getextrema(), expected_extrema) 409 | 410 | if __name__ == '__main__': 411 | unittest.main() 412 | --------------------------------------------------------------------------------