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