Changeset - 23ede92b1326
[Not reviewed]
0 0 4
Branko Majic (branko) - 10 years ago 2013-11-27 21:11:12
branko@majic.rs
PYD-1: Initial port of Sigil with some basic documentation, licensing information, and setup script.
4 files changed with 404 insertions and 0 deletions:
0 comments (0 inline, 0 general)
LICENSE
Show inline comments
 
new file 100644
 
Copyright (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.
README.rst
Show inline comments
 
new file 100644
 
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..
pydenticon/__init__.py
Show inline comments
 
new file 100644
 
# For digest operations.
 
import hashlib
 

	
 
# For saving the images from Pillow.
 
from io import BytesIO
 

	
 
# Pillow for Image processing.
 
from PIL import Image, ImageDraw
 

	
 

	
 
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("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:
 
                digest = data.decode("hex")
 
            except TypeError:
 
                digest = self.digest(data).hexdigest()
 
        else:
 
            digest = self.digest(data).hexdigest()
 

	
 
        return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
 

	
 
    def _generate_png(self, matrix, width, height, padding, foreground, background):
 
        """
 
        Generates an identicon image in the PNG format out of the passed block
 
        matrix, with the requested width, height, padding, foreground colour,
 
        and background colour.
 

	
 
        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.
 

	
 
        Returns:
 

	
 
          Identicon image in PNG format, returned as a byte list.
 
        """
 

	
 
        # Set-up a new image object, setting the background to provided value.
 
        image = Image.new("RGB", (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
 
                    y2 = padding[0] + (row + 1) * block_height
 

	
 
                    # Draw the rectangle.
 
                    draw.rectangle((x1, y1, x2, y2), fill=foreground)
 

	
 
        # Set-up a stream where image will be saved.
 
        stream = BytesIO()
 

	
 
        # Save the image to stream.
 
        image.save(stream, format="png", optimize=True)
 
        image_raw = stream.getvalue()
 
        stream.close()
 

	
 
        # Return the resulting PNG.
 
        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: "png", "ascii". Default is "png".
 

	
 
          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 == "png":
 
            background = self.background
 
            foreground = self.foreground[digest_byte_list[0] % len(self.foreground)]
 
        elif output_format == "ascii":
 
            foreground = "+"
 
            background = "-"
 

	
 
        # Swtich the colours if inverted image was requested.
 
        if inverted:
 
            foreground, background = background, foreground
 

	
 
        # Generate the identicon in requested format.
 
        if output_format == "png":
 
            return self._generate_png(matrix, width, height, padding, foreground, background)
 
        if output_format == "ascii":
 
            return self._generate_ascii(matrix, foreground, background)
 
        else:
 
            raise ValueError("Unsupported format requested: %s" % output_format)
setup.py
Show inline comments
 
new file 100644
 
import os
 
from setuptools import setup
 

	
 
README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read()
 
INSTALL_REQUIREMENTS = ["Pillow"]
 
TEST_REQUIREMENTS = []
 

	
 
# 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.0-dev',
 
    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,
 
    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',
 
        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
 
        'Topic :: Multimedia :: Graphics',
 
        'Topic :: Software Development :: Libraries',
 
    ],
 
)
0 comments (0 inline, 0 general)