Skip to content

Commit

Permalink
Update sample cases to confirm resizing works when less than default …
Browse files Browse the repository at this point in the history
…320 by 320 dimensions
  • Loading branch information
Lorena Mesa committed Jul 17, 2024
1 parent 29d847d commit 7ddbcbd
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 51 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ Users often work collaboratively in digital environments where a profile picture

## Identicon Requirements
1. The identicon's should be symmetrical meaning the left horizontal half is equal to the right horizontal half.
2. The identicon is 5X5 pixels, following the standard specified for [GitHub identicons](https://github.blog/2013-08-14-identicons/), so we'll generate square identicons only with a default of 250X250 pixels
3. Identicon's should use accessible colors as specified by [W3](https://www.w3.org/WAI/WCAG21/Techniques/general/G207)
2. The identicon is 5X5 pixels, following the standard specified for [GitHub identicons](https://github.blog/2013-08-14-identicons/), so we'll generate square identicons only with a default of 320X320 pixels which other social media platforms like Instagram define as an ideal size
3. Identicon's should use proper resizing sampling technique to ensure quality is maintained, see [Pillow](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.resize) documentation for options

## TODO:
- [ ] Finish script to implement identicon with multiple colors
- [X] Implement core logic to generate a Python PIL or Tinkr image
- [X] Implement core logic to generate a Python PIL image
- [X] Write baseline tests
- [ ] Add CI/CD with GitHub actions to run tests
- [X] Add CI/CD with GitHub actions to run tests
- [X] Add CI/CD with GitHub Actions to run linter
87 changes: 49 additions & 38 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class Identicon:

def __init__(self, input_str: str) -> None:
self.md5hash_str: str = self._convert_string_to_sha_hash(input_str)
self.grid_size: int = 5
self.square_size: int = 64
self.identicon_size: tuple = (self.grid_size * self.square_size, self.grid_size * self.square_size)

def _convert_string_to_sha_hash(self, input_str: str) -> str:
"""
Expand All @@ -27,74 +30,82 @@ def _convert_string_to_sha_hash(self, input_str: str) -> str:
def _build_grid(self) -> list[list]:
"""
Function that takes an input md5 hexdigest string and builds
a list of lists using grid size to determine the size of the
grid. Each value within the list of lists contains a row of booleans
that indicates if that given element will be filled with a color.
a list of lists using self.GRID_SIZE to determine the size of the
grid; with the default set to a 5X5 grid.
Each value within the list of lists contains a row of booleans
that indicates if that given pizel will be filled with a color.
:return: a list of lists representing a grid of the pixels to be drawn in a PIL Image
:return: a list of lists representing a grid of the pixels to be
drawn and filled in a PIL Image
"""
grid_size: int = 5
grid: list = []
for row_number in range(grid_size):
for row_number in range(self.grid_size):
row: list = list()
for element_number in range(grid_size):
element: int = row_number * grid_size + element_number + 6
fill_element: bool = int(self.md5hash_str[element], base=16) % 2 == 0
for pixel in range(self.grid_size):
current_pixel: int = row_number * self.grid_size + pixel + 6
fill_element: bool = int(self.md5hash_str[current_pixel], base=16) % 2 == 0
row.append(fill_element)
grid.append(row)
return grid

def _generate_image_fill_color(self, md5hash_str: str) -> tuple:
def _generate_pixel_fill_color(self, md5hash_str: str) -> tuple:
"""
Function that generates a R,G,B value to use to fill the PIL Image.
Function that generates a R,G,B value to use to fill the PIL Image pixels.
:param md5hash_str: md5 hexdigest of an input string
:return: a tuple of numbers representing the R,G.B value to fill the PIL Image
:return: a tuple of numbers representing the R,G,B value to fill the PIL Image pixels
"""
return tuple(int(md5hash_str[i:i+2], base=16) for i in range(0, 2*3, 2))

def draw_image(self, filename: str=None, dimensions: int=0) -> Image:
def render(self, filename: str=None, dimensions: int=0) -> Image:
"""
Function that generates a grid - a list of lists - indicating which pixels are to be filled
and uses the md5hash_str to generate an image fill color. Function creates a PIL Image, drawing it,
and saving it. By default a 250 pixel by 250 pixel identicon is created, if upon executing the code
a dimensions parameter is passed in the image will be resized.
Function that generates a grid - a list of lists - indicating which pixels
are to be filled and uses the md5hash_str to generate an image fill color.
Function creates a PIL Image, drawing it, and saving it. By default a 320
pixel by 320 pixel identicon is rendered, if upon executing the code a
dimensions parameter is passed in the image will be resized.
:param filename: filename of PIL png image generated
:return: None
"""

fill_color: tuple = self._generate_image_fill_color(self.md5hash_str)
fill_color: tuple = self._generate_pixel_fill_color(self.md5hash_str)
grid: list[list] = self._build_grid()

# Default to a 250X250 pixel image
SQUARE: int = 50
size: tuple = (5 * 50, 5 * 50)
bg_color: tuple = (214,214,214)

image: Image = Image.new("RGB", size, bg_color)
# Default to a 320X320, a recommended avtar size per social platforms like Instagram,
# pixel image where each shape filled within the identicon is of size 64 pixels
background_color: tuple = (214,214,214)
image: Image = Image.new("RGB", self.identicon_size, background_color)
draw: ImageDraw = ImageDraw.Draw(image)

# Makes the identicon symmetrical
for i in range(5):
grid[i][4] = grid[i][0]
grid[i][3] = grid[i][1]
# Makes the identicon symmetrical by setting the right columns
# values to the same as the left columns, minus the center column
for i in range(self.grid_size):
grid[i][self.grid_size - 1] = grid[i][0]
grid[i][self.grid_size - 2] = grid[i][1]

for row in range(5):
for element in range(5):
for row in range(self.grid_size):
for pixel in range(self.grid_size):
# Boolean check to confirm 'True' to draw and fill the pixel in the iamge
if grid[row][element]:
bounding_box: list[int] = [element * SQUARE, row * SQUARE, element * SQUARE + SQUARE, row * SQUARE + SQUARE]
# TODO: Should we use multiple fill colors? May need to draw multiple rectangles to obtain this
draw.rectangle(bounding_box, fill=fill_color)
if grid[row][pixel]:
shape_coords: list[int] = [
pixel * self.square_size,
row * self.square_size,
pixel * self.square_size + self.square_size,
row * self.square_size + self.square_size
]
draw.rectangle(shape_coords, fill=fill_color)

if not filename:
filename: str = 'example'

if dimensions:
wpercent: float = (dimensions / float(image.size[0]))
hsize: int = int((float(image.size[1]) * float(wpercent)))
image = image.resize((dimensions, hsize), Image.Resampling.LANCZOS)
# Possible resampling filters here: https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.resize
# BICUBIC and LANCZOS take longer to process than NEAREST, but the quality of the former is better.
width_percent: float = (dimensions / float(image.size[0]))
height: int = int((float(image.size[1]) * float(width_percent)))
image = image.resize((dimensions, height), Image.Resampling.LANCZOS)

image.save(f'{filename}.png')

Expand Down Expand Up @@ -141,4 +152,4 @@ def dimensions_gt_zero(input_dimensions: str):
args = parser.parse_args()

identicon = Identicon(input_str=args.string)
identicon.draw_image(filename=args.output, dimensions=args.dimensions)
identicon.render(filename=args.output, dimensions=args.dimensions)
27 changes: 19 additions & 8 deletions test/sample_cases_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ def test_ui_fails_to_create_identicon_with_dimensions_lt_1(self):
class TestHappyPath(unittest.TestCase):
def test_successfully_creates_identicon(self):
identicon = Identicon("931D387731bBbC988B31220")
identicon.draw_image(filename="output")
identicon.render(filename="output")
generated_image = Image.open(f"{PROJECT_ROOT}/output.png", mode="r")
self.assertIsInstance(generated_image, PngImagePlugin.PngImageFile)
remove(f"{PROJECT_ROOT}/output.png")

def test_successfully_creates_same_identicon_for_same_input_strings(self):
# Make 1st identicon
identicon_john_1 = Identicon("john")
identicon_john_1.draw_image(filename="john1")
identicon_john_1.render(filename="john1")
# Make 2nd identicon
identicon_john_2 = Identicon("john")
identicon_john_2.draw_image(filename="john2")
identicon_john_2.render(filename="john2")

# Assertions
generated_john_1 = Image.open(f"{PROJECT_ROOT}/john1.png", mode="r")
Expand All @@ -58,10 +58,10 @@ def test_successfully_creates_same_identicon_for_same_input_strings(self):
def test_does_not_create_same_identicon_for_different_input_strings(self):
# Make 1st identicon
identicon_john = Identicon("john")
identicon_john.draw_image(filename="john")
identicon_john.render(filename="john")
# Make 2nd identicon
identicon_john_2 = Identicon("jane")
identicon_john_2.draw_image(filename="jane")
identicon_john_2.render(filename="jane")

# Assertions
generated_john = Image.open(f"{PROJECT_ROOT}/john.png", mode="r")
Expand All @@ -76,18 +76,29 @@ def test_does_not_create_same_identicon_for_different_input_strings(self):
remove(f"{PROJECT_ROOT}/john.png")
remove(f"{PROJECT_ROOT}/jane.png")

def test_successfully_resizes_identicon_gt_250_when_dimensions_provided(self):
def test_successfully_resizes_identicon_gt_default_when_dimensions_provided(self):
identicon_john = Identicon("john")
identicon_john.draw_image(filename="john", dimensions=300)
identicon_john.render(filename="john", dimensions=450)

# Assertions
generated_john = Image.open(f"{PROJECT_ROOT}/john.png", mode="r")
self.assertIsInstance(generated_john, PngImagePlugin.PngImageFile)
self.assertEqual(generated_john.size, (300, 300))
self.assertEqual(generated_john.size, (450, 450))

# Cleanup
remove(f"{PROJECT_ROOT}/john.png")

def test_successfully_resizes_identicon_lt_default_when_dimensions_provided(self):
identicon_john = Identicon("john")
identicon_john.render(filename="john", dimensions=150)

# Assertions
generated_john = Image.open(f"{PROJECT_ROOT}/john.png", mode="r")
self.assertIsInstance(generated_john, PngImagePlugin.PngImageFile)
self.assertEqual(generated_john.size, (150, 150))

# Cleanup
remove(f"{PROJECT_ROOT}/john.png")

if __name__ == "__main__":
unittest.main()

0 comments on commit 7ddbcbd

Please sign in to comment.