pydenticon-0.3.1/0000755000175000017500000000000013153246062014443 5ustar brankobranko00000000000000pydenticon-0.3.1/LICENSE0000640000175000017500000000272012573102731015444 0ustar brankobranko00000000000000Copyright (c) 2013, Branko Majic All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of Branko Majic nor the names of any other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pydenticon-0.3.1/tests/0000755000175000017500000000000013153246062015605 5ustar brankobranko00000000000000pydenticon-0.3.1/tests/samples/0000755000175000017500000000000013153246062017251 5ustar brankobranko00000000000000pydenticon-0.3.1/tests/samples/test3.png0000640000175000017500000000061312573102731021014 0ustar brankobranko00000000000000PNG  IHDR PLTE1s#@IDATx1 0]W X(W r\'Np:u \7.\=o~|m[Xp:u \'Np:u \'Np:u \'Np:u \'Np:u L̸ \'Np:u \'Np:u \:u \'Np:u \'Np:u-iz\'Np:u \'Np:u \'N01\'Np:u \w9f&(IENDB`pydenticon-0.3.1/tests/samples/test2.png0000640000175000017500000000061412573102731021014 0ustar brankobranko00000000000000PNG  IHDR PLTE1s#AIDATx10$ d?S`ZK}\'Np:u \7.Ge‚ \'Np:u \'Np:u [=Buvs[Xp:u \'Np:u \'Np:Č[Xp:u \'Np:u \'Np:u=\'Np:u \'Np:u \'N01\'Np:u \'Np:u \'Np:u \'Np:ugS IENDB`pydenticon-0.3.1/tests/samples/test1.png0000640000175000017500000000062512573102731021015 0ustar brankobranko00000000000000PNG  IHDR PLTEEh]JIDATx1 1@Qe-lSy%LqFp:u \'Np:u \'Np:u \'Np:u׍ ~wc܄ \'Np:u \'Np:u \'Np:u \'Np:u \'Np:u}ZWrrq\'Np:u \'Np:u \'Np|}ظ \'Np:u \'Np:u \'q\'Np:u \G=&IENDB`pydenticon-0.3.1/tests/test_pydenticon.py0000640000175000017500000003537713020551770021403 0ustar brankobranko00000000000000# Standard library imports. import hashlib import unittest from io import BytesIO # Third-party Python library imports. import mock import PIL import PIL.ImageChops # Library imports. from pydenticon import Generator class GeneratorTest(unittest.TestCase): """ Implements tests for pydenticon.Generator class. """ def test_init_entropy(self): """ Tests if the constructor properly checks for entropy provided by a digest algorithm. """ # Set-up the mock instance. hexdigest_method = mock.MagicMock(return_value="aabb") digest_instance = mock.MagicMock() digest_instance.hexdigest = hexdigest_method # Set-up digest function that will always return the same digest # instance. digest_method = mock.MagicMock(return_value=digest_instance) # This should require 23 bits of entropy, while the digest we defined # provided 2*8 bits of entropy (2 bytes). self.assertRaises(ValueError, Generator, 5, 5, digest=digest_method) def test_init_parameters(self): """ Verifies that the constructor sets-up the instance properties correctly. """ generator = Generator(5, 5, digest=hashlib.sha1, foreground=["#111111", "#222222"], background="#aabbcc") # sha1 provides 160 bits of entropy - 20 bytes. self.assertEqual(generator.digest_entropy, 20 * 8) self.assertEqual(generator.digest, hashlib.sha1) self.assertEqual(generator.rows, 5) self.assertEqual(generator.columns, 5) self.assertEqual(generator.foreground, ["#111111", "#222222"]) self.assertEqual(generator.background, "#aabbcc") def test_get_bit(self): """ Tests if the check whether bit is 1 or 0 is performed correctly. """ generator = Generator(5, 5) hash_bytes = [0b10010001, 0b10001000, 0b00111001] # Check a couple of bits from the above hash bytes. self.assertEqual(True, generator._get_bit(0, hash_bytes)) self.assertEqual(True, generator._get_bit(7, hash_bytes)) self.assertEqual(False, generator._get_bit(22, hash_bytes)) self.assertEqual(True, generator._get_bit(23, hash_bytes)) def test_generate_matrix(self): """ Verifies that the matrix is generated correctly based on passed hashed bytes. """ # The resulting half-matrix should be as follows (first byte is for # ignored in matrix generation): # # 100 # 011 # 100 # 001 # 110 hash_bytes = [0b11111111, 0b10101010, 0b01010101] expected_matrix = [ [True, False, False, False, True], [False, True, True, True, False], [True, False, False, False, True], [False, False, True, False, False], [True, True, False, True, True], ] generator = Generator(5, 5) matrix = generator._generate_matrix(hash_bytes) self.assertEqual(matrix, expected_matrix) def test_data_to_digest_byte_list_raw(self): """ Test if correct digest byte list is returned for raw (non-hex-digest) data passed to the method. """ # Set-up some raw data, and set-up the expected result. data = "this is a test\n" expected_digest_byte_list = [225, 156, 18, 131, 201, 37, 179, 32, 102, 133, 255, 82, 42, 207, 227, 230] # Instantiate a generator. generator = Generator(5, 5, digest=hashlib.md5) # Call the method and get the results. digest_byte_list = generator._data_to_digest_byte_list(data) # Verify the expected and actual result are identical. self.assertEqual(expected_digest_byte_list, digest_byte_list) def test_data_to_digest_byte_list_hex(self): """ Test if correct digest byte list is returned for passed hex digest string. """ # Set-up some test hex digest (md5), and expected result. hex_digest = "e19c1283c925b3206685ff522acfe3e6" expected_digest_byte_list = [225, 156, 18, 131, 201, 37, 179, 32, 102, 133, 255, 82, 42, 207, 227, 230] # Instantiate a generator. generator = Generator(5, 5, digest=hashlib.md5) # Call the method and get the results. digest_byte_list = generator._data_to_digest_byte_list(hex_digest) # Verify the expected and actual result are identical. self.assertEqual(expected_digest_byte_list, digest_byte_list) def test_data_to_digest_byte_list_hex_lookalike(self): """ Test if correct digest byte list is returned for passed raw data that has same length as hex digest string. """ # Set-up some test hex digest (md5), and expected result. data = "qqwweerrttyyuuiiooppaassddffgghh" expected_digest_byte_list = [25, 182, 52, 218, 118, 220, 26, 145, 164, 222, 33, 221, 183, 140, 98, 246] # Instantiate a generator. generator = Generator(5, 5, digest=hashlib.md5) # Call the method and get the results. digest_byte_list = generator._data_to_digest_byte_list(data) # Verify the expected and actual result are identical. self.assertEqual(expected_digest_byte_list, digest_byte_list) def test_generate_image_basics(self): """ Tests some basics about generated PNG identicon image. This includes: - Dimensions of generated image. - Format of generated image. - Mode of generated image. """ # Set-up parameters that will be used for generating the image. width = 200 height = 200 padding = [20, 20, 20, 20] foreground = "#ffffff" background = "#000000" matrix = [ [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 1, 1, 1, 0], [0, 1, 1, 1, 0], ] # Set-up a generator. generator = Generator(5, 5) # Generate the raw image. raw_image = generator._generate_image(matrix, width, height, padding, foreground, background, "png") # Try to load the raw image. image_stream = BytesIO(raw_image) image = PIL.Image.open(image_stream) # Verify image size, format, and mode. self.assertEqual(image.size[0], 240) self.assertEqual(image.size[1], 240) self.assertEqual(image.format, "PNG") self.assertEqual(image.mode, "RGBA") def test_generate_ascii(self): """ Tests the generated identicon in ASCII format. """ # Set-up parameters that will be used for generating the image. foreground = "1" background = "0" matrix = [ [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 1, 1, 1, 0], [0, 1, 1, 1, 0], ] # Set-up a generator. generator = Generator(5, 5) # Generate the ASCII image. ascii_image = generator._generate_ascii(matrix, foreground, background) # Verify that the result is as expected. expected_result = """00100 00100 00100 01110 01110""" self.assertEqual(ascii_image, expected_result) def test_generate_format(self): """ Tests if identicons are generated in requested format. """ # Set-up a generator. generator = Generator(5, 5) # Set-up some test data. data = "some test data" # Verify that PNG image is returned when requested. raw_image = generator.generate(data, 200, 200, output_format="png") image_stream = BytesIO(raw_image) image = PIL.Image.open(image_stream) self.assertEqual(image.format, "PNG") # Verify that JPEG image is returned when requested. raw_image = generator.generate(data, 200, 200, output_format="jpeg") image_stream = BytesIO(raw_image) image = PIL.Image.open(image_stream) self.assertEqual(image.format, "JPEG") # Verify that GIF image is returned when requested. raw_image = generator.generate(data, 200, 200, output_format="gif") image_stream = BytesIO(raw_image) image = PIL.Image.open(image_stream) self.assertEqual(image.format, "GIF") # Verify that ASCII "image" is returned when requested. raw_image = generator.generate(data, 200, 200, output_format="ascii") self.assertIsInstance(raw_image, str) def test_generate_format_invalid(self): """ Tests if an exception is raised in case an unsupported format is requested when generating the identicon. """ # Set-up a generator. generator = Generator(5, 5) # Set-up some test data. data = "some test data" # Verify that an exception is raised in case of unsupported format. self.assertRaises(ValueError, generator.generate, data, 200, 200, output_format="invalid") @mock.patch.object(Generator, '_generate_image') def test_generate_inverted_png(self, generate_image_mock): """ Tests if the foreground and background are properly inverted when generating PNG images. """ # Set-up some test data. data = "Some test data" # Set-up one foreground and background colour. foreground = "#ffffff" background = "#000000" # Set-up the generator. generator = Generator(5, 5, foreground=[foreground], background=background) # Verify that colours are picked correctly when no inverstion is requsted. generator.generate(data, 200, 200, inverted=False, output_format="png") generate_image_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, foreground, background, "png") # Verify that colours are picked correctly when inversion is requsted. generator.generate(data, 200, 200, inverted=True, output_format="png") generate_image_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, background, foreground, "png") @mock.patch.object(Generator, '_generate_ascii') def test_generate_inverted_ascii(self, generate_ascii_mock): """ Tests if the foreground and background are properly inverted when generating ASCII "images". """ # Set-up some test data. data = "Some test data" # Set-up one foreground and background colour. These are not used for # ASCII itself (instead a plus/minus sign is used). foreground = "#ffffff" background = "#000000" # Set-up the generator. generator = Generator(5, 5, foreground=[foreground], background=background) # Verify that foreground/background is picked correctly when no # inverstion is requsted. generator.generate(data, 200, 200, inverted=False, output_format="ascii") generate_ascii_mock.assert_called_with(mock.ANY, "+", "-") # Verify that foreground/background is picked correctly when inversion # is requsted. generator.generate(data, 200, 200, inverted=True, output_format="ascii") generate_ascii_mock.assert_called_with(mock.ANY, "-", "+") @mock.patch.object(Generator, '_generate_image') def test_generate_foreground(self, generate_image_mock): """ Tests if the foreground colour is picked correctly. """ # Set-up some foreground colours and a single background colour. foreground = ["#000000", "#111111", "#222222", "#333333", "#444444", "#555555"] background = "#ffffff" # Set-up the generator. generator = Generator(5, 5, foreground=foreground, background=background) # The first byte of hex digest should be 121 for this data, which should # result in foreground colour of index '1'. data = "some test data" generator.generate(data, 200, 200) generate_image_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, foreground[1], background, "png") # The first byte of hex digest should be 149 for this data, which should # result in foreground colour of index '5'. data = "some other test data" generator.generate(data, 200, 200) generate_image_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, foreground[5], background, "png") def test_generate_image_compare(self): """ Tests generated PNG identicon against a set of pre-generated samples. """ # Set-up a list of foreground colours (taken from Sigil). Same as used # for reference images. foreground = ["rgb(45,79,255)", "rgb(254,180,44)", "rgb(226,121,234)", "rgb(30,179,253)", "rgb(232,77,65)", "rgb(49,203,115)", "rgb(141,69,170)"] # Set-up a background colour (taken from Sigil). Same as used for # reference images. background = "rgb(224,224,224)" # Set-up parameters equivalent as used for samples. width = 200 height = 200 padding = (20, 20, 20, 20) # Load the reference images, making sure they're in RGBA mode. test1_ref = PIL.Image.open("tests/samples/test1.png").convert(mode="RGBA") test2_ref = PIL.Image.open("tests/samples/test2.png").convert(mode="RGBA") test3_ref = PIL.Image.open("tests/samples/test3.png").convert(mode="RGBA") # Set-up the Generator. generator = Generator(5, 5, foreground=foreground, background=background) # Generate first test identicon. raw_image = generator.generate("test1", width, height, padding=padding) image_stream = BytesIO(raw_image) test1 = PIL.Image.open(image_stream) # Generate second test identicon. raw_image = generator.generate("test2", width, height, padding=padding) image_stream = BytesIO(raw_image) test2 = PIL.Image.open(image_stream) # Generate third test identicon. raw_image = generator.generate("test3", width, height, padding=padding) image_stream = BytesIO(raw_image) test3 = PIL.Image.open(image_stream) # Calculate differences between generated identicons and references. diff1 = PIL.ImageChops.difference(test1, test1_ref) diff2 = PIL.ImageChops.difference(test2, test2_ref) diff3 = PIL.ImageChops.difference(test3, test3_ref) # Verify that all the diffs are essentially black (i.e. no differences # between generated identicons and reference samples). expected_extrema = ((0, 0), (0, 0), (0, 0), (0, 0)) self.assertEqual(diff1.getextrema(), expected_extrema) self.assertEqual(diff2.getextrema(), expected_extrema) self.assertEqual(diff3.getextrema(), expected_extrema) if __name__ == '__main__': unittest.main() pydenticon-0.3.1/tests/__init__.py0000640000175000017500000000000012573102731017677 0ustar brankobranko00000000000000pydenticon-0.3.1/pydenticon.egg-info/0000755000175000017500000000000013153246062020311 5ustar brankobranko00000000000000pydenticon-0.3.1/pydenticon.egg-info/top_level.txt0000644000175000017500000000001313153246062023035 0ustar brankobranko00000000000000pydenticon pydenticon-0.3.1/pydenticon.egg-info/PKG-INFO0000644000175000017500000000276213153246062021415 0ustar brankobranko00000000000000Metadata-Version: 1.1 Name: pydenticon Version: 0.3.1 Summary: Library for generating identicons. Port of Sigil (https://github.com/cupcake/sigil) with enhancements. Home-page: https://github.com/azaghal/pydenticon Author: Branko Majic Author-email: branko@majic.rs License: BSD Description: Pydenticon ========== Pydenticon is a small utility library that can be used for deterministically generating identicons based on the hash of provided data. The implementation is a port of the Sigil identicon implementation from: * https://github.com/cupcake/sigil/ Pydenticon provides a couple of extensions of its own when compared to the original Sigil implementation, like: * Ability to supply custom digest algorithms (allowing for larger identicons if digest provides enough entropy). * Ability to specify a rectangle for identicon size.. Platform: UNKNOWN Classifier: Environment :: Other Environment Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Multimedia :: Graphics Classifier: Topic :: Software Development :: Libraries pydenticon-0.3.1/pydenticon.egg-info/dependency_links.txt0000644000175000017500000000000113153246062024357 0ustar brankobranko00000000000000 pydenticon-0.3.1/pydenticon.egg-info/requires.txt0000644000175000017500000000000713153246062022706 0ustar brankobranko00000000000000Pillow pydenticon-0.3.1/pydenticon.egg-info/SOURCES.txt0000644000175000017500000000133513153246062022177 0ustar brankobranko00000000000000LICENSE MANIFEST.in README.rst setup.py assets/favicon.xcf docs/Makefile docs/about.rst docs/algorithm.rst docs/apireference.rst docs/conf.py docs/index.rst docs/installation.rst docs/make.bat docs/privacy.rst docs/releasenotes.rst docs/testing.rst docs/usage.rst docs/images/branko.png docs/images/branko_inverted.png docs/images/favicon.ico docs/images/logo.png docs/images/pydenticon.png docs/images/pydenticon_inverted.png pydenticon/__init__.py pydenticon.egg-info/PKG-INFO pydenticon.egg-info/SOURCES.txt pydenticon.egg-info/dependency_links.txt pydenticon.egg-info/requires.txt pydenticon.egg-info/top_level.txt tests/__init__.py tests/test_pydenticon.py tests/samples/test1.png tests/samples/test2.png tests/samples/test3.pngpydenticon-0.3.1/PKG-INFO0000644000175000017500000000276213153246062015547 0ustar brankobranko00000000000000Metadata-Version: 1.1 Name: pydenticon Version: 0.3.1 Summary: Library for generating identicons. Port of Sigil (https://github.com/cupcake/sigil) with enhancements. Home-page: https://github.com/azaghal/pydenticon Author: Branko Majic Author-email: branko@majic.rs License: BSD Description: Pydenticon ========== Pydenticon is a small utility library that can be used for deterministically generating identicons based on the hash of provided data. The implementation is a port of the Sigil identicon implementation from: * https://github.com/cupcake/sigil/ Pydenticon provides a couple of extensions of its own when compared to the original Sigil implementation, like: * Ability to supply custom digest algorithms (allowing for larger identicons if digest provides enough entropy). * Ability to specify a rectangle for identicon size.. Platform: UNKNOWN Classifier: Environment :: Other Environment Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Multimedia :: Graphics Classifier: Topic :: Software Development :: Libraries pydenticon-0.3.1/README.rst0000640000175000017500000000105012573102731016121 0ustar brankobranko00000000000000Pydenticon ========== Pydenticon is a small utility library that can be used for deterministically generating identicons based on the hash of provided data. The implementation is a port of the Sigil identicon implementation from: * https://github.com/cupcake/sigil/ Pydenticon provides a couple of extensions of its own when compared to the original Sigil implementation, like: * Ability to supply custom digest algorithms (allowing for larger identicons if digest provides enough entropy). * Ability to specify a rectangle for identicon size.. pydenticon-0.3.1/pydenticon/0000755000175000017500000000000013153246062016617 5ustar brankobranko00000000000000pydenticon-0.3.1/pydenticon/__init__.py0000644000175000017500000003155113152771310020733 0ustar brankobranko00000000000000# For digest operations. import hashlib # For saving the images from Pillow. from io import BytesIO # Pillow for Image processing. from PIL import Image, ImageDraw # For decoding hex values (works both for Python 2.7.x and Python 3.x). import binascii class Generator(object): """ Factory class that can be used for generating the identicons deterministically based on hash of the passed data. Resulting identicons are images of requested size with optional padding. The identicon (without padding) consists out of M x N blocks, laid out in a rectangle, where M is the number of blocks in each column, while N is number of blocks in each row. Each block is a smallself rectangle on its own, filled using the foreground or background colour. The foreground is picked randomly, based on the passed data, from the list of foreground colours set during initialisation of the generator. The blocks are always laid-out in such a way that the identicon will be symterical by the Y axis. The center of symetry will be the central column of blocks. Simply put, the generated identicons are small symmetric mosaics with optional padding. """ def __init__(self, rows, columns, digest=hashlib.md5, foreground=["#000000"], background="#ffffff"): """ Initialises an instance of identicon generator. The instance can be used for creating identicons with differing image formats, sizes, and with different padding. Arguments: rows - Number of block rows in an identicon. columns - Number of block columns in an identicon. digest - Digest class that should be used for the user's data. The class should support accepting a single constructor argument for passing the data on which the digest will be run. Instances of the class should also support a single hexdigest() method that should return a digest of passed data as a hex string. Default is hashlib.md5. Selection of the digest will limit the maximum values that can be set for rows and columns. Digest needs to be able to generate (columns / 2 + columns % 2) * rows + 8 bits of entropy. foreground - List of colours which should be used for drawing the identicon. Each element should be a string of format supported by the PIL.ImageColor module. Default is ["#000000"] (only black). background - Colour (single) which should be used for background and padding, represented as a string of format supported by the PIL.ImageColor module. Default is "#ffffff" (white). """ # Check if the digest produces sufficient entropy for identicon # generation. entropy_provided = len(digest(b"test").hexdigest()) // 2 * 8 entropy_required = (columns // 2 + columns % 2) * rows + 8 if entropy_provided < entropy_required: raise ValueError("Passed digest '%s' is not capable of providing %d bits of entropy" % (str(digest), entropy_required)) # Set the expected digest size. This is used later on to detect if # passed data is a digest already or not. self.digest_entropy = entropy_provided self.rows = rows self.columns = columns self.foreground = foreground self.background = background self.digest = digest def _get_bit(self, n, hash_bytes): """ Determines if the n-th bit of passed bytes is 1 or 0. Arguments: hash_bytes - List of hash byte values for which the n-th bit value should be checked. Each element of the list should be an integer from 0 to 255. Returns: True if the bit is 1. False if the bit is 0. """ if hash_bytes[n // 8] >> int(8 - ((n % 8) + 1)) & 1 == 1: return True return False def _generate_matrix(self, hash_bytes): """ Generates matrix that describes which blocks should be coloured. Arguments: hash_bytes - List of hash byte values for which the identicon is being generated. Each element of the list should be an integer from 0 to 255. Returns: List of rows, where each element in a row is boolean. True means the foreground colour should be used, False means a background colour should be used. """ # Since the identicon needs to be symmetric, we'll need to work on half # the columns (rounded-up), and reflect where necessary. half_columns = self.columns // 2 + self.columns % 2 cells = self.rows * half_columns # Initialise the matrix (list of rows) that will be returned. matrix = [[False] * self.columns for _ in range(self.rows)] # Process the cells one by one. for cell in range(cells): # If the bit from hash correpsonding to this cell is 1, mark the # cell as foreground one. Do not use first byte (since that one is # used for determining the foreground colour. if self._get_bit(cell, hash_bytes[1:]): # Determine the cell coordinates in matrix. column = cell // self.columns row = cell % self.rows # Mark the cell and its reflection. Central column may get # marked twice, but we don't care. matrix[row][column] = True matrix[row][self.columns - column - 1] = True return matrix def _data_to_digest_byte_list(self, data): """ Creates digest of data, returning it as a list where every element is a single byte of digest (an integer between 0 and 255). No digest will be calculated on the data if the passed data is already a valid hex string representation of digest, and the passed value will be used as digest in hex string format instead. Arguments: data - Raw data or hex string representation of existing digest for which a list of one-byte digest values should be returned. Returns: List of integers where each element is between 0 and 255, and repesents a single byte of a data digest. """ # If data seems to provide identical amount of entropy as digest, it # could be a hex digest already. if len(data) // 2 == self.digest_entropy // 8: try: binascii.unhexlify(data.encode('utf-8')) digest = data.encode('utf-8') # Handle Python 2.x exception. except (TypeError): digest = self.digest(data.encode('utf-8')).hexdigest() # Handle Python 3.x exception. except (binascii.Error): digest = self.digest(data.encode('utf-8')).hexdigest() else: digest = self.digest(data.encode('utf-8')).hexdigest() return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)] def _generate_image(self, matrix, width, height, padding, foreground, background, image_format): """ Generates an identicon image in requested image format out of the passed block matrix, with the requested width, height, padding, foreground colour, background colour, and image format. Arguments: matrix - Matrix describing which blocks in the identicon should be painted with foreground (background if inverted) colour. width - Width of resulting identicon image in pixels. height - Height of resulting identicon image in pixels. padding - Tuple describing padding around the generated identicon. The tuple should consist out of four values, where each value is the number of pixels to use for padding. The order in tuple is: top, bottom, left, right. foreground - Colour which should be used for foreground (filled blocks), represented as a string of format supported by the PIL.ImageColor module. background - Colour which should be used for background and padding, represented as a string of format supported by the PIL.ImageColor module. image_format - Format to use for the image. Format needs to be supported by the Pillow library. Returns: Identicon image in requested format, returned as a byte list. """ # Set-up a new image object, setting the background to provided value. image = Image.new("RGBA", (width + padding[2] + padding[3], height + padding[0] + padding[1]), background) # Set-up a draw image (for drawing the blocks). draw = ImageDraw.Draw(image) # Calculate the block widht and height. block_width = width // self.columns block_height = height // self.rows # Go through all the elements of a matrix, and draw the rectangles. for row, row_columns in enumerate(matrix): for column, cell in enumerate(row_columns): if cell: # Set-up the coordinates for a block. x1 = padding[2] + column * block_width y1 = padding[0] + row * block_height x2 = padding[2] + (column + 1) * block_width - 1 y2 = padding[0] + (row + 1) * block_height - 1 # Draw the rectangle. draw.rectangle((x1, y1, x2, y2), fill=foreground) # Set-up a stream where image will be saved. stream = BytesIO() if image_format.upper() == "JPEG": image = image.convert(mode="RGB") # Save the image to stream. try: image.save(stream, format=image_format, optimize=True) except KeyError: raise ValueError("Pillow does not support requested image format: %s" % image_format) image_raw = stream.getvalue() stream.close() # Return the resulting image. return image_raw def _generate_ascii(self, matrix, foreground, background): """ Generates an identicon "image" in the ASCII format. The image will just output the matrix used to generate the identicon. Arguments: matrix - Matrix describing which blocks in the identicon should be painted with foreground (background if inverted) colour. foreground - Character which should be used for representing foreground. background - Character which should be used for representing background. Returns: ASCII representation of an identicon image, where one block is one character. """ return "\n".join(["".join([foreground if cell else background for cell in row]) for row in matrix]) def generate(self, data, width, height, padding=(0, 0, 0, 0), output_format="png", inverted=False): """ Generates an identicon image with requested width, height, padding, and output format, optionally inverting the colours in the indeticon (swapping background and foreground colours) if requested. Arguments: data - Hashed or raw data that will be used for generating the identicon. width - Width of resulting identicon image in pixels. height - Height of resulting identicon image in pixels. padding - Tuple describing padding around the generated identicon. The tuple should consist out of four values, where each value is the number of pixels to use for padding. The order in tuple is: top, bottom, left, right. output_format - Output format of resulting identicon image. Supported formats are anything that is supported by Pillow, plus a special "ascii" mode. inverted - Specifies whether the block colours should be inverted or not. Default is False. Returns: Byte representation of an identicon image. """ # Calculate the digest, and get byte list. digest_byte_list = self._data_to_digest_byte_list(data) # Create the matrix describing which block should be filled-in. matrix = self._generate_matrix(digest_byte_list) # Determine the background and foreground colours. if output_format == "ascii": foreground = "+" background = "-" else: background = self.background foreground = self.foreground[digest_byte_list[0] % len(self.foreground)] # Swtich the colours if inverted image was requested. if inverted: foreground, background = background, foreground # Generate the identicon in requested format. if output_format == "ascii": return self._generate_ascii(matrix, foreground, background) else: return self._generate_image(matrix, width, height, padding, foreground, background, output_format) pydenticon-0.3.1/setup.py0000644000175000017500000000252413153246061016157 0ustar brankobranko00000000000000import os from setuptools import setup README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read() INSTALL_REQUIREMENTS = ["Pillow"] TEST_REQUIREMENTS = ["mock"] # allow setup.py to be run from any path os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='pydenticon', version='0.3.1', packages=['pydenticon'], include_package_data=True, license='BSD', # example license description='Library for generating identicons. Port of Sigil (https://github.com/cupcake/sigil) with enhancements.', long_description=README, url='https://github.com/azaghal/pydenticon', author='Branko Majic', author_email='branko@majic.rs', install_requires=INSTALL_REQUIREMENTS, tests_require=TEST_REQUIREMENTS, test_suite="tests", classifiers=[ 'Environment :: Other Environment', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Multimedia :: Graphics', 'Topic :: Software Development :: Libraries', ], ) pydenticon-0.3.1/docs/0000755000175000017500000000000013153246062015373 5ustar brankobranko00000000000000pydenticon-0.3.1/docs/privacy.rst0000640000175000017500000000657012573102731017605 0ustar brankobranko00000000000000Privacy ======= It is fundamentally important to understand the privacy issues if using Pydenticon in order to generate uniquelly identifiable avatars for users leaving the comments etc. The most common way to expose the identicons is by having a web application generate them on the fly from data that is being passed to it through HTTP GET requests. Those GET requests would commonly include either the raw data, or data as hex string that is then used to generate an identicon. The URLs for GET requests would most commonly be made as part of image tags in an HTML page. The data passed needs to be unique in order to generate distinct identicons. In most cases the data used will be either name or e-mail address that the visitor posting the comment fills-in in some field. That being said, e-mails usually provide a much better identifier than name (especially if the website verifies the comments through by sending-out e-mails). Needless to say, in such cases, especially if the website where the comments are being posted is public, using raw data can completely reveal the identity of the user. If e-mails are used for generating the identicons, the situation is even worse, since now those e-mails can be easily harvested for spam purposes. Using the e-mails also provides data mining companies with much more reliable user identifier that can be coupled with information from other websites. Therefore, it is highly recommended to pass the data to web application that generates the identicons using **hex digest only**. I.e. **never** pass the raw data. Although passing hash instead of real data as part of the GET request is a good step forward, it can still cause problems since the hashses can be collected, and then used in conjunction with rainbow tables to identify the original data. This is particularly problematic when using hex digests of e-mail addresses as data for generating the identicon. There's two feasible approaches to resolve this: * Always apply *salt* to user-identifiable data before calculating a hex digest. This can hugely reduce the efficiency of brute force attacks based on rainbow tables (althgouh it will not mitigate it completely). * Instead of hashing the user-identifiable data itself, every time you need to do so, create some random data instead, hash that random data, and store it for future use (cache it), linking it to the original data that it was generated for. This way the hex digest being put as part of an image link into HTML pages is not derived in any way from the original data, and can therefore not be used to reveal what the original data was. Keep in mind that using identicons will inevitably still allow people to track someone's posts across your website. Identicons will effectively automatically create pseudonyms for people posting on your website. If that may pose a problem, it might be better not to use identicons at all. Finally, small summary of the points explained above: * Always use hex digests in order to retrieve an identicon from a server. * Instead of using privately identifiable data for generating the hex digest, use randmoly generated data, and associate it with privately identifiable data. This way hex digest cannot be traced back to the original data through brute force or rainbow tables. * If unwilling to generate and store random data, at least make sure to use salt when hashing privately identifiable data. pydenticon-0.3.1/docs/index.rst0000640000175000017500000000164112573102731017231 0ustar brankobranko00000000000000Pydenticon documentation ======================== .. image:: images/pydenticon.png .. image:: images/pydenticon_inverted.png Pydenticon is a small utility library that can be used for deterministically generating identicons based on the hash of provided data. The implementation is a port of the Sigil identicon implementation from: * https://github.com/cupcake/sigil/ Support ------- In case of problems with the library, please do not hestitate to contact the author at **pydenticon (at) majic.rs**. The library itself is hosted on Github, and on author's own websites: * https://github.com/azaghal/pydenticon * https://code.majic.rs/pydenticon * https://projects.majic.rs/pydenticon Contents: .. toctree:: :maxdepth: 2 about installation usage algorithm privacy apireference testing releasenotes Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pydenticon-0.3.1/docs/Makefile0000640000175000017500000001271412573102731017033 0ustar brankobranko00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pydenticon.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pydenticon.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Pydenticon" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pydenticon" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." pydenticon-0.3.1/docs/algorithm.rst0000640000175000017500000001553312573102731020115 0ustar brankobranko00000000000000Algorithm ========= A generated identicon can be described as one big rectangle divided into ``rows x columns`` rectangle blocks of equal size, where each block can be filled with the foreground colour or the background colour. Additionally, the whole identicon is symmetrical to the central vertical axis, making it much more aesthetically pleasing. The algorithm used for generating the identicon is fairly simple. The input arguments that determine what the identicon will look like are: * Size of identicon in blocks (``rows x columns``). * Algorithm used to create digests out of user-provided data. * List of colours used for foreground fill (foreground colours). This list will be referred to as ``foreground_list``. * Single colour used for background fill (background colour). This colour wil be referred to as ``background``. * Whether the foreground and background colours should be inverted (swapped) or not. * Data passed to be used for digest. The first step is to generate a *digest* out of the passed data using the selected digest algorithm. This digest is then split into two parts: * The first byte of digest (``f``, for foreground) is used for determining the foreground colour. * The remaining portion of digest (``l``, for layout) is used for determining which blocks of identicon will be filled using foreground and background colours. In order to select a ``foreground`` colour, the algorithm will try to determine the index of the colour in the ``foreground_list`` by doing modulo division of the first byte's integer value with number of colours in ``foreground_list``:: foreground = foreground_list[int(f) % len(foreground_list)] The layout of blocks (which block gets filled with foreground colour, and which block gets filled with background colour) is determined by the bit values of remaining portion of digest (``l``). This remaining portion of digest can also be seen as a list of bits. The bit positions would range from ``0`` to ``b`` (where the size of ``b`` would depend on the digest algoirthm that was picked). Since the identicon needs to be symmetrical, the number of blocks for which the fill colour needs to be calculated is equal to ``rows * (columns / 2 + columns % 2)``. I.e. the block matrix is split in half vertically (if number of columns is odd, the middle column is included as well). Those blocks can then be marked with whole numbers from ``0`` to ``c`` (where ``c`` would be equal to the above formula - ``rows * (columns / 2 + columns % 2)``). Number ``0`` would correspond to first block of the first half-row, ``1`` to the first block of the second row, ``2`` to the first block of the third row, and so on to the first block of the last half-row. Then the blocks in the next column would be indexed with numbers in a similar (incremental) way. With these two numbering methods in place (for digest bits and blocks of half-matrix), every block is assigned a bit that has the same position number. If no inversion of foreground and background colours was requested, bit value of ``1`` for a cell would mean the block should be filled with foreground colour, while value of ``0`` would mean the block should be filled with background colour. If an inverted identicon was requested, then ``1`` would correspond to background colour fill, and ``0`` would correspond to foreground colour fill. Examples -------- An identicon should be created with the following parameters: * Size of identicon in blocks is ``5 x 5`` (a square). * Digest algorithm is *MD5*. * Five colours are used for identicon foreground (``0`` through ``4``). * Some background colour is selected (marked as ``b``). * Foreground and background colours are not to be inverted (swapped). * Data used for digest is ``branko``. MD5 digest for data (``branko``) would be (reperesented as hex value) equal to ``d41c0e80c44173dcf7575745bdddb704``. In other words, 16 bytes would be present with the following hex values:: d4 1c 0e 80 c4 41 73 dc f7 57 57 45 bd dd b7 04 Following the algorithm, the first byte (``d4``) is used to determine which foreground colour to use. ``d4`` is equal to ``212`` in decimal format. Divided by modulo ``5`` (number of foreground colours), the resulting index of foreground colour is ``2`` (third colour in the foreground list). The remaining 15 bytes will be used for figuring out the layout. The representation of those bytes in binary format would look like this (5 bytes per row):: 00011100 00001110 10000000 11000100 01000001 01110011 11011100 11110111 01010111 01010111 01000101 10111101 11011101 10110111 00000100 Since identicon consits out of 5 columns and 5 rows, the number of bits that's needed from ``l`` for the layout would be ``5 * (5 / 2 + 5 % 2) == 15``. This means that the following bits will determine the layout of identicon (whole first byte, and 7 bits of the second byte):: 00011100 0000111 The half-matrix would therefore end-up looking like this (5 bits per column for 5 blocks per column):: 010 000 001 101 101 The requested identicon is supposed to have 5 block columns, so a reflection will be applied to the first and second column, with third column as center of the symmetry. This would result in the following ideticon matrix:: 01010 00000 00100 10101 10101 Since no inversion was requested, ``1`` would correspond to calculated foreground colour, while ``0`` would correspond to provided background colour. To spicen the example up a bit, here is what the above identicon would look like in regular and inverted variant (with some sample foreground colours and a bit of padding): .. image:: images/branko.png .. image:: images/branko_inverted.png Limitations ----------- There's some practical limitations to the algorithm described above. The first limitation is the maximum number of different foreground colours that can be used for identicon generation. Since a single byte (which is used to determining the colour) can represent 256 values (between 0 and 255), there can be no more than 256 colours passed to be used for foreground of the identicon. Any extra colours passed above that count would simply be ignored. The second limitation is that the maximum dimensions (in blocks) of a generated identicon depend on digest algorithm used. In order for a digest algorithm to be able to satisfy requirements of producing an identcion with ``rows`` number of rows, and ``columns`` number of columns (in blocks), it must be able to produce at least the following number of bits (i.e. the number of bits equal to the number of blocks in the half-matrix):: rows * (columns / 2 + columns % 2) + 8 The expression is the result of vertical symmetry of identicon. Only the columns up to, and including, the middle one middle one (``(columns / 2 + colums % 2)``) need to be processed, with every one of those columns having ``row`` rows (``rows *``). Finally, an extra 8 bits (1 byte) are necessary for determining the foreground colour. pydenticon-0.3.1/docs/conf.py0000644000175000017500000001727313153246061016703 0ustar brankobranko00000000000000# -*- coding: utf-8 -*- # # Pydenticon documentation build configuration file, created by # sphinx-quickstart on Mon Nov 25 23:13:33 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('..')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Pydenticon' copyright = u'2013, Branko Majic' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.3.1' # The full version, including alpha/beta/rc tags. release = '0.3.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "images/logo.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = "images/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Pydenticondoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Pydenticon.tex', u'Pydenticon Documentation', u'Branko Majic', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pydenticon', u'Pydenticon Documentation', [u'Branko Majic'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Pydenticon', u'Pydenticon Documentation', u'Branko Majic', 'Pydenticon', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' pydenticon-0.3.1/docs/apireference.rst0000640000175000017500000000010612573102731020545 0ustar brankobranko00000000000000API Reference ============= .. automodule:: pydenticon :members: pydenticon-0.3.1/docs/releasenotes.rst0000640000175000017500000000434313153244735020623 0ustar brankobranko00000000000000Release Notes ============= 0.3.1 ----- Minor bug-fixes. Bug fixes: * `PYD-8 - Cannot generate identicons in JPEG format when using Pillow >= 4.2.0 `_ 0.3 --- Update introducing support for more output formats and ability to use transparency for PNG identicons. New features: * `PYD-6: Add support for having transparent backgrounds in identicons `_ Ability to use alpha-channel specification in PNG identicons to obtain complete or partial transparency. Works for both background and foreground colour. * `PYD-7: Ability to specify image format `_ Ability to specify any output format supported by the Pillow library. 0.2 --- A small release that adds support for Python 3 in addition to Python 2.7. New features: * `PYD-5: Add support for Python 3.x `_ Support for Python 3.x, in addition to Python 2.7. 0.1.1 ----- This is a very small release feature-wise, with a single bug-fix. New features: * `PYD-3: Initial tests `_ Unit tests covering most of the library functionality. Bug fixes: * `PYD-4: Identicon generation using pre-hashed data raises ValueError `_ Fixed some flawed logic which prevented identicons to be generated from existing hashes. 0.1 --- Initial release of Pydenticon. Implemented features: * Supported parameters for identicon generator (shared between multiple identicons): * Number of blocks in identicon (rows and columns). * Digest algorithm. * List of foreground colours to choose from. * Background colour. * Supported parameters when generating induvidual identicons: * Data that should be used for identicon generation. * Width and height of resulting image in pixels. * Padding around identicon (top, bottom, left, right). * Output format. * Inverted identicon (swaps foreground with background). * Support for PNG and ASCII format of resulting identicons. * Full documentation covering installation, usage, algorithm, privacy. API reference included as well. pydenticon-0.3.1/docs/usage.rst0000640000175000017500000001457113020551770017233 0ustar brankobranko00000000000000Usage ===== Pydenticon provides simple and straightforward interface for setting-up the identicon generator, and for generating the identicons. Instantiating a generator ------------------------- The starting point is to create a generator instance. Generator implements interface that can be used for generating the identicons. In its simplest form, the generator instances needs to be passed only the size of identicon in blocks (first parameter is width, second is height):: # Import the library. import pydenticon # Instantiate a generator that will create 5x5 block identicons. generator = pydenticon.Generator(5, 5) The above example will instantiate a generator that can be used for producing identicons which are 5x5 blocks in size, using the default values for digest (*MD5*), foreground colour (*black*), and background colour (*white*). Alternatively, you may choose to pass in a different digest algorithm, and foreground and background colours:: # Import the libraries. import pydenticon import hashlib # Set-up a list of foreground colours (taken from Sigil). foreground = [ "rgb(45,79,255)", "rgb(254,180,44)", "rgb(226,121,234)", "rgb(30,179,253)", "rgb(232,77,65)", "rgb(49,203,115)", "rgb(141,69,170)" ] # Set-up a background colour (taken from Sigil). background = "rgb(224,224,224)" # Instantiate a generator that will create 5x5 block identicons using SHA1 # digest. generator = pydenticon.Generator(5, 5, digest=hashlib.sha1, foreground=foreground, background=background) Generating identicons --------------------- With generator initialised, it's now possible to use it to create the identicons. The most basic example would be creating an identicon using default padding (no padding) and output format ("png"), without inverting the colours (which is also the default):: # Generate a 240x240 PNG identicon. identicon = generator.generate("john.doe@example.com", 240, 240) The result of the ``generate()`` method will be a raw representation of an identicon image in requested format that can be written-out to file, sent back as an HTTP response etc. Usually it can be nice to have some padding around the generated identicon in order to make it stand-out better, or maybe to invert the colours. This can be done with:: # Set-up the padding (top, bottom, left, right) in pixels. padding = (20, 20, 20, 20) # Generate a 200x200 identicon with padding around it, and invert the # background/foreground colours. identicon = generator.generate("john.doe@example.com", 200, 200, padding=padding, inverted=True) Finally, the resulting identicons can be in different formats:: # Create identicon in PNG format. identicon_png = generator.generate("john.doe@example.com", 200, 200, output_format="png") # Create identicon in ASCII format. identicon_ascii = generator.generate("john.doe@example.com", 200, 200, output_format="ascii") Supported output formats are dependant on the local Pillow installation. For exact list of available formats, have a look at `Pillow documentation `_. The ``ascii`` format is the only format explicitly handled by the *Pydenticon* library itself (mainly useful for debugging purposes). Using the generated identicons ------------------------------ Of course, just generating the identicons is not that fun. They usually need either to be stored somewhere on disk, or maybe streamed back to the user via HTTP response. Since the generate function returns raw data, this is quite easy to achieve:: # Generate same identicon in three different formats. identicon_png = generator.generate("john.doe@example.com", 200, 200, output_format="png") identicon_gif = generator.generate("john.doe@example.com", 200, 200, output_format="gif") identicon_ascii = generator.generate("john.doe@example.com", 200, 200, output_format="ascii") # Identicon can be easily saved to a file. f = open("sample.png", "wb") f.write(identicon_png) f.close() f = open("sample.gif", "wb") f.write(identicon_gif) f.close() # ASCII identicon can be printed-out to console directly. print identicon_ascii Working with transparency ------------------------- .. note:: New in version ``0.3``. .. warning:: The only output format that properly supports transparency at the moment is ``PNG``. If you are using anything else, transparency will not work. If you ever find yourself in need of having a transparent background or foreground, you can easily do this using the syntax ``rgba(224,224,224,0)``. All this does is effectively adding alpha channel to selected colour. The alpha channel value ranges from ``0`` to ``255``, letting you specify how much transparency/opaqueness you want. For example, to have it at roughly 50% (more like at ``50.2%`` since you can't use fractions), you would simply specify value as ``rgba(224,224,224,128)``. Full example ------------ Finally, here is a full example that will create a number of identicons and output them in PNG format to local directory:: #!/usr/bin/env python # Import the libraries. import pydenticon import hashlib # Set-up some test data. users = ["alice", "bob", "eve", "dave"] # Set-up a list of foreground colours (taken from Sigil). foreground = [ "rgb(45,79,255)", "rgb(254,180,44)", "rgb(226,121,234)", "rgb(30,179,253)", "rgb(232,77,65)", "rgb(49,203,115)", "rgb(141,69,170)" ] # Set-up a background colour (taken from Sigil). background = "rgb(224,224,224)" # Set-up the padding (top, bottom, left, right) in pixels. padding = (20, 20, 20, 20) # Instantiate a generator that will create 5x5 block identicons using SHA1 # digest. generator = pydenticon.Generator(5, 5, foreground=foreground, background=background) for user in users: identicon = generator.generate(user, 200, 200, padding=padding, output_format="png") filename = user + ".png" with open(filename, "wb") as f: f.write(identicon) pydenticon-0.3.1/docs/testing.rst0000640000175000017500000000102012573102731017566 0ustar brankobranko00000000000000Testing ======= Pydenticon includes a number of unit tests which are used for regression testing. The tests are fairly comprehensive, and also include comparison of Pydenticon-generated identicons against a couple of samples generated by Sigil. Tests depend on the following additional libraries: * `Mock `_ Test dependencies will be automatically downloaded when running the tests if they're not present. Pydenticon tests can be run with the following command:: python setup.py test pydenticon-0.3.1/docs/make.bat0000640000175000017500000001176012573102731017000 0ustar brankobranko00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Pydenticon.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Pydenticon.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end pydenticon-0.3.1/docs/images/0000755000175000017500000000000013153246062016640 5ustar brankobranko00000000000000pydenticon-0.3.1/docs/images/pydenticon_inverted.png0000640000175000017500000000120712573102731023415 0ustar brankobranko00000000000000PNG  IHDR7~ pHYs  tIME a&IDATx @aޚ(",j,L" 7!Hk 4 F h4A h4AA h'||WȠcsqиrA h AA A4 A#h4 F h4A h4AA hGj#F A4 A#h4 F h4A h4AA h AA A4 'ZNh4A h4AA h4AA h AA A4 A#h4aB{uBA4 A#h4 F h4A h4AA h4AA h AY\;Q + F h4A h4AA h4AA h AA A4 fqbpBA h AA A4 A3::q{IENDB`pydenticon-0.3.1/docs/images/branko.png0000640000175000017500000000110512573102731020612 0ustar brankobranko00000000000000PNG  IHDR7~ IDATx @@Q+NۨtWQ8`^3qۖd*f#@ h4AA h4AA h A˯p믜; h4AA h AA A4 A#h4 F h4A h4A×$ A4 A#h4 F h4A h4AA h4AA h A4԰믜;mh AA A4 A#h4 F h4A h4AA h AGA4 A#h4 F h4A h4AA h4AA h A4 A#h4 F h4A h4AA h4AA h AA A#h(%1lh4A h4AA h4AA h;IENDB`pydenticon-0.3.1/docs/images/pydenticon.png0000640000175000017500000000120212573102731021510 0ustar brankobranko00000000000000PNG  IHDR7~ pHYs  tIME -Շ!IDATx @A1ܭlm[+D ܍|^7JD,0 h4A h4AA h4AA|{ʠsqиrA h AA A4 A#h4 F h4A h4AA h&W{l#f A4 A#h4 F h4A h4AA h AA A4tDU A4 A#h4 F h4A h4AA h4AA h A4|{ʠJNh\9@ h4AA h AA A4 A#h4 F h4A h4kz0<:qAA A4 A#h4 F h4A h4AA h4AALDU A4 A#h4 F h4Q gIENDB`pydenticon-0.3.1/docs/images/logo.png0000640000175000017500000000112512573102731020300 0ustar brankobranko00000000000000PNG  IHDRXbKGD pHYs  tIME 7"P,IDATx10Q l!@  t` ueSI͖[׺`_, which is used for generating the images. Using pip --------- In order to install latest stable release of Pydenticon using *pip*, run the following command:: pip install pydenticon In order to install the latest development version of Pydenticon from Github, use the following command:: pip install -e git+https://github.com/azaghal/pydenticon#egg=pydenticon Manual installation ------------------- If you wish to install Pydenticon manually, make sure that its dependencies have been met first, and then simply copy the ``pydenticon`` directory (that contains the ``__init__.py`` file) somewhere on the Python path. pydenticon-0.3.1/docs/about.rst0000640000175000017500000000415012573102731017232 0ustar brankobranko00000000000000About Pydenticon ================ Pydenticon is a small utility library that can be used for deterministically generating identicons based on the hash of provided data. The implementation is a port of the Sigil identicon implementation from: * https://github.com/cupcake/sigil/ Why was this library created? ----------------------------- A number of web-based applications written in Python have a need for visually differentiating between users by using avatars for each one of them. This functionality is particularly popular with comment-posting since it increases the readability of threads. The problem is that lots of those applications need to allow anonymous users to post their comments as well. Since anonymous users cannot set the avatar for themselves, usually a random avatar is created for them instead. There is a number of free (as in free beer) services out there that allow web application developers to create such avatars. Unfortunately, this usually means that the users visiting websites based on those applications are leaking information about their browsing habits etc to these third-party providers. Pydenticon was written in order to resolve such an issue for one of the application (Django Blog Zinnia, in particular), and to allow the author to set up his own avatar/identicon service. Features -------- Pydenticon has the following features: * Compatible with Sigil implementation (https://github.com/cupcake/sigil/) if set-up with right parameters. * Creates vertically symmetrical identicons of any rectangular shape and size. * Uses digests of passed data for generating the identicons. * Automatically detects if passed data is hashed already or not. * Custom digest implementations can be passed to identicon generator (defaults to 'MD5'). * Support for multiple image formats. * PNG * ASCII * Foreground colour picked from user-provided list. * Background colour set by the user. * Ability to invert foreground and background colour in the generated identicon. * Customisable padding around generated identicon using the background colour (foreground if inverted identicon was requested). pydenticon-0.3.1/MANIFEST.in0000640000175000017500000000041312573102731016172 0ustar brankobranko00000000000000recursive-include assets *.xcf recursive-include docs Makefile make.bat *.py *.rst *.png *.ico include LICENSE include MANIFEST.in include README.rst include setup.py recursive-include pydenticon *.py recursive-include tests *.py *.png prune docs/_build exclude tmp/ pydenticon-0.3.1/setup.cfg0000644000175000017500000000007313153246062016264 0ustar brankobranko00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pydenticon-0.3.1/assets/0000755000175000017500000000000013153246062015745 5ustar brankobranko00000000000000pydenticon-0.3.1/assets/favicon.xcf0000640000175000017500000002071412573102731020073 0ustar brankobranko00000000000000gimp xcf fileBBGgimp-image-grid(style solid) (fgcolor (color-rgba 0.000000 0.000000 0.000000 1.000000)) (bgcolor (color-rgba 1.000000 1.000000 1.000000 1.000000)) (xspacing 10.000000) (yspacing 10.000000) (spacing-unit inches) (xoffset 0.000000) (yoffset 0.000000) (offset-unit inches) y sm favicon32.png     00E Y i              , MMMMMMMMMMMM M M M M M M M M M M M MMMMMMMMMMMMMMMMMMM MMMMMM, AAAAAAAAAAAA A A A A A A A A A A A AAAAAAAAAAAAAAAAAAA AAAAAA,              ,00favicon48.png     ((/00C00S'''''''MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM'M'M'M'M'M'M'MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'A'A'A'A'A'A'A'''''''@@favicon64.png       )@@ =@@ M #  #  #  #  #  #  #  #  #  #  #  #                          3 3 3 3 3 3 3 3 3 3 3  M# M M# M M# M M# M M# M M# M M# M M# M M# M M# M M# M M# MM MM MM MM MM MM MM MM MM MM MM MM M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M A# A A# A A# A A# A A# A A# A A# A A# A A# A A# A A# A A# AA AA AA AA AA AA AA AA AA AA AA AA A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A #  #  #  #  #  #  #  #  #  #  #  #                          3 3 3 3 3 3 3 3 3 3 3 favicon128.png     ,D!`x(''''''''''''''''''''''''//////////////////////// M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A ''''''''''''''''''''''''//////////////////////// $'''''''''''''''''''''''////////////////////////$M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M'M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M/M$A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A'A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A/A$'''''''''''''''''''''''////////////////////////                        3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 M MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 MA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A                        3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3                          3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM MM M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M3 M4 AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A3 A4                         3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4@@