Skip to content

Commit 7ddbcbd

Browse files
author
Lorena Mesa
committed
Update sample cases to confirm resizing works when less than default 320 by 320 dimensions
1 parent 29d847d commit 7ddbcbd

File tree

3 files changed

+72
-51
lines changed

3 files changed

+72
-51
lines changed

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ Users often work collaboratively in digital environments where a profile picture
1717

1818
## Identicon Requirements
1919
1. The identicon's should be symmetrical meaning the left horizontal half is equal to the right horizontal half.
20-
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
21-
3. Identicon's should use accessible colors as specified by [W3](https://www.w3.org/WAI/WCAG21/Techniques/general/G207)
20+
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
21+
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
2222

2323
## TODO:
24-
- [ ] Finish script to implement identicon with multiple colors
25-
- [X] Implement core logic to generate a Python PIL or Tinkr image
24+
- [X] Implement core logic to generate a Python PIL image
2625
- [X] Write baseline tests
27-
- [ ] Add CI/CD with GitHub actions to run tests
26+
- [X] Add CI/CD with GitHub actions to run tests
2827
- [X] Add CI/CD with GitHub Actions to run linter

src/main.py

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class Identicon:
1212

1313
def __init__(self, input_str: str) -> None:
1414
self.md5hash_str: str = self._convert_string_to_sha_hash(input_str)
15+
self.grid_size: int = 5
16+
self.square_size: int = 64
17+
self.identicon_size: tuple = (self.grid_size * self.square_size, self.grid_size * self.square_size)
1518

1619
def _convert_string_to_sha_hash(self, input_str: str) -> str:
1720
"""
@@ -27,74 +30,82 @@ def _convert_string_to_sha_hash(self, input_str: str) -> str:
2730
def _build_grid(self) -> list[list]:
2831
"""
2932
Function that takes an input md5 hexdigest string and builds
30-
a list of lists using grid size to determine the size of the
31-
grid. Each value within the list of lists contains a row of booleans
32-
that indicates if that given element will be filled with a color.
33+
a list of lists using self.GRID_SIZE to determine the size of the
34+
grid; with the default set to a 5X5 grid.
35+
36+
Each value within the list of lists contains a row of booleans
37+
that indicates if that given pizel will be filled with a color.
3338
34-
:return: a list of lists representing a grid of the pixels to be drawn in a PIL Image
39+
:return: a list of lists representing a grid of the pixels to be
40+
drawn and filled in a PIL Image
3541
"""
36-
grid_size: int = 5
3742
grid: list = []
38-
for row_number in range(grid_size):
43+
for row_number in range(self.grid_size):
3944
row: list = list()
40-
for element_number in range(grid_size):
41-
element: int = row_number * grid_size + element_number + 6
42-
fill_element: bool = int(self.md5hash_str[element], base=16) % 2 == 0
45+
for pixel in range(self.grid_size):
46+
current_pixel: int = row_number * self.grid_size + pixel + 6
47+
fill_element: bool = int(self.md5hash_str[current_pixel], base=16) % 2 == 0
4348
row.append(fill_element)
4449
grid.append(row)
4550
return grid
4651

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

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

67-
fill_color: tuple = self._generate_image_fill_color(self.md5hash_str)
73+
fill_color: tuple = self._generate_pixel_fill_color(self.md5hash_str)
6874
grid: list[list] = self._build_grid()
6975

70-
# Default to a 250X250 pixel image
71-
SQUARE: int = 50
72-
size: tuple = (5 * 50, 5 * 50)
73-
bg_color: tuple = (214,214,214)
74-
75-
image: Image = Image.new("RGB", size, bg_color)
76+
# Default to a 320X320, a recommended avtar size per social platforms like Instagram,
77+
# pixel image where each shape filled within the identicon is of size 64 pixels
78+
background_color: tuple = (214,214,214)
79+
image: Image = Image.new("RGB", self.identicon_size, background_color)
7680
draw: ImageDraw = ImageDraw.Draw(image)
7781

78-
# Makes the identicon symmetrical
79-
for i in range(5):
80-
grid[i][4] = grid[i][0]
81-
grid[i][3] = grid[i][1]
82+
# Makes the identicon symmetrical by setting the right columns
83+
# values to the same as the left columns, minus the center column
84+
for i in range(self.grid_size):
85+
grid[i][self.grid_size - 1] = grid[i][0]
86+
grid[i][self.grid_size - 2] = grid[i][1]
8287

83-
for row in range(5):
84-
for element in range(5):
88+
for row in range(self.grid_size):
89+
for pixel in range(self.grid_size):
8590
# Boolean check to confirm 'True' to draw and fill the pixel in the iamge
86-
if grid[row][element]:
87-
bounding_box: list[int] = [element * SQUARE, row * SQUARE, element * SQUARE + SQUARE, row * SQUARE + SQUARE]
88-
# TODO: Should we use multiple fill colors? May need to draw multiple rectangles to obtain this
89-
draw.rectangle(bounding_box, fill=fill_color)
91+
if grid[row][pixel]:
92+
shape_coords: list[int] = [
93+
pixel * self.square_size,
94+
row * self.square_size,
95+
pixel * self.square_size + self.square_size,
96+
row * self.square_size + self.square_size
97+
]
98+
draw.rectangle(shape_coords, fill=fill_color)
9099

91100
if not filename:
92101
filename: str = 'example'
93102

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

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

@@ -141,4 +152,4 @@ def dimensions_gt_zero(input_dimensions: str):
141152
args = parser.parse_args()
142153

143154
identicon = Identicon(input_str=args.string)
144-
identicon.draw_image(filename=args.output, dimensions=args.dimensions)
155+
identicon.render(filename=args.output, dimensions=args.dimensions)

test/sample_cases_test.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,18 @@ def test_ui_fails_to_create_identicon_with_dimensions_lt_1(self):
2929
class TestHappyPath(unittest.TestCase):
3030
def test_successfully_creates_identicon(self):
3131
identicon = Identicon("931D387731bBbC988B31220")
32-
identicon.draw_image(filename="output")
32+
identicon.render(filename="output")
3333
generated_image = Image.open(f"{PROJECT_ROOT}/output.png", mode="r")
3434
self.assertIsInstance(generated_image, PngImagePlugin.PngImageFile)
3535
remove(f"{PROJECT_ROOT}/output.png")
3636

3737
def test_successfully_creates_same_identicon_for_same_input_strings(self):
3838
# Make 1st identicon
3939
identicon_john_1 = Identicon("john")
40-
identicon_john_1.draw_image(filename="john1")
40+
identicon_john_1.render(filename="john1")
4141
# Make 2nd identicon
4242
identicon_john_2 = Identicon("john")
43-
identicon_john_2.draw_image(filename="john2")
43+
identicon_john_2.render(filename="john2")
4444

4545
# Assertions
4646
generated_john_1 = Image.open(f"{PROJECT_ROOT}/john1.png", mode="r")
@@ -58,10 +58,10 @@ def test_successfully_creates_same_identicon_for_same_input_strings(self):
5858
def test_does_not_create_same_identicon_for_different_input_strings(self):
5959
# Make 1st identicon
6060
identicon_john = Identicon("john")
61-
identicon_john.draw_image(filename="john")
61+
identicon_john.render(filename="john")
6262
# Make 2nd identicon
6363
identicon_john_2 = Identicon("jane")
64-
identicon_john_2.draw_image(filename="jane")
64+
identicon_john_2.render(filename="jane")
6565

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

79-
def test_successfully_resizes_identicon_gt_250_when_dimensions_provided(self):
79+
def test_successfully_resizes_identicon_gt_default_when_dimensions_provided(self):
8080
identicon_john = Identicon("john")
81-
identicon_john.draw_image(filename="john", dimensions=300)
81+
identicon_john.render(filename="john", dimensions=450)
8282

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

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

91+
def test_successfully_resizes_identicon_lt_default_when_dimensions_provided(self):
92+
identicon_john = Identicon("john")
93+
identicon_john.render(filename="john", dimensions=150)
94+
95+
# Assertions
96+
generated_john = Image.open(f"{PROJECT_ROOT}/john.png", mode="r")
97+
self.assertIsInstance(generated_john, PngImagePlugin.PngImageFile)
98+
self.assertEqual(generated_john.size, (150, 150))
99+
100+
# Cleanup
101+
remove(f"{PROJECT_ROOT}/john.png")
91102

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

0 commit comments

Comments
 (0)