Emoji Blender

If you need to download the starter files, you can do so here.

In completing this assignment, you’ll get practice with:

  • Reading & parsing simple JSON and SVG files
  • Sets & dictionaries
  • Implementing methods, including magic methods that allow us to use common operators on our objects

Summary of Deliverables

By the end of this, here’s what you’ll need to submit to Gradescope:

  • emoji_blender.py
  • test_emoji_blender_student.py
  • my_emoji.svg
  • generate_my_emoji.py
  • readme_emoji.txt

0. Getting Started

Please make sure to read all of this section before proceeding to write any code. This section describes the assumptions and the design of the assignment in great detail.

Background and Goals

I think emojis are great. Some of them are really funny (🫡), some of them are useful when writing slides about Python behavior (🖨️), and some of them are this one: 🤠.

Sometimes I feel that there’s a complex feeling I want to express that’s just not available as a pre-made emoji. What if I wanted to throw some starry eyes (🤩) on a sick guy (🤢) to show how conflicted I feel about something? Nothing doing… until now!

Openmoji and SVGs

The emojis that we have on our phones and computers are actually copyrighted images. The standard for what emojis are supposed to represent is universal, but the actual images that are used to represent, for example, the “zipper-mouth” (🤐) emoji changes how it looks based on what browser you’re using and what phone/computer/operating system you’re using.

Openmoji is a project that provides open source images for most of the emojis that people use. That means that if you want to use these images, you can do so for free provided that you attribute the Openmoji project as the source.

These images come in a couple formats: .png and .svg. An SVG is file that uses the .svg file extenion, and it represents a Scalable Vector Graphic image. This file format has several desirable properties. For one, they are scalable, meaning that they can be shrunk down or enlarged without losing any detail. For another, the contents of an SVG file are actually the instructions for drawing the image stored in the XML format.

Important: If you open an SVG file in Codio, it just shows you the underlying code. This is useful, but if you want to see what it looks like, you can select Run SVG Viewer from the dropdown at the top, and then press SVG Viewer to open up a filetree browser that shows previews of the SVGs as images. (This is also set to open up for you automatically when you open the project.)

If you need to navigate back up to a previous folder, you can do that with a button at the top of the filetree browser.

XML and ElementTree

SVG files contain XML data. You can review the finer details of XML data using class materials from 10/30, including these slides and these videos.

All of the emojis for this assignment are constructed using the same pattern of XML. The XML always contains an <svg> root. The children of the root are all <g> (group) tags. Neither <svg> nor <g> tags actually describe visual elements. All visual elements of the emoji are found as the children of the <g> tags, although not every <g> tag will have any children.

Below is an example of an SVG file for an emoji. It starts with an <svg> tag that contains all other tags. The <svg> tag has a bunch of children that are <g> tags. The first one with the id of "color" stores a single <circle> tag. The next three <g> tags are empty with no children or descendants. The final <g> tag with the id of "line" stores a number of <path> tags and a single <ellipse> tag.

<svg xmlns="http://www.w3.org/2000/svg" id="emoji" viewBox="0 0 72 72">
  <g id="color">
    <circle cx="36" cy="36" r="23" fill="#FCEA2B" id="temp_id_0" class="face" />
  </g>
  <g id="hair" />
  <g id="skin" />
  <g id="skin-shadow" />
  <g id="line">
    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M41.6308,48.0192c-1.1233,1.2679-3.0497,2.0788-5.7815,2.0788c-2.7113,0-4.6397-0.8017-5.7749-2.0544" id="temp_id_1" class="mouth" />
    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M21.1086,27.2015c0.7207-1.3857,1.9278-2.4541,3.3907-3c1.4052-0.7002,3.0205-0.8486,4.5302-0.4209" id="temp_id_2" class="eyes" />
    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M50.719,27.2015c-1.582-2.7724-4.8037-4.1699-7.9092-3.4306" id="temp_id_3" class="eyes" />
    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M23.4843,34.2452c0,0,3.9322-2.1695,8,0" id="temp_id_4" class="eyes" />
    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M40.7343,34.2452c0,0,3.9322-2.1695,8,0" id="temp_id_5" class="eyes" />
    <ellipse cx="36" cy="36" rx="23.0001" ry="23.0001" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" id="temp_id_6" class="face" />
  </g>
</svg>

Altogether, that SVG with all of its constituent graphical elements can be interpreted as a series of instructions that draw this emoji:

Keep in mind your XML processing toolkit:

Snippet Purpose
import xml.etree.ElementTree as ET Import the library that stores all of the important XML parsing machinery
tree = ET.parse(filename) Reads the contents of an XML file and parses them as an ElementTree
tree = ET.fromstring(str_of_xml_content) Parses a string that contains XML data and returns it as an ElementTree
for child in elem Iterates over all the direct descendants of the current elem. This works the same independent of whether elem is an Element or an ElementTree.
elem.attrib['attr_name'] Returns the value of the attribute called attr_name that belongs to the Element elem.


What You Will Do: Emoji and EmojiLibrary

In this assignment, you’ll fill out a few important methods in an Emoji class and an EmojiLibrary class. This will let you load a bunch of open-source emojis from a folder and then do Emoji Arithmetic on them. Load one emoji, delete its eyes, and add the eyes and sparkles from another emoji… now you’re cooking.

The Emoji class defines how a single Emoji behaves. It will contain a static method, from_svg(), as well as an initializer/constructor (__init__()) and several magic methods (__getitem__, __setitem__, __add__, and __sub__)

Static Methods

Static methods are functions that belong to a class but are not called with reference to a particular object from that class. They are tagged with the @staticmethod decorator. A typical method my_method() would be called on an object o like o.my_method(). This cause that particular o instance to execute the statements in my_method() with reference to o’s attributes. A static method must be called in reference to a class, like Emoji. To call from_svg(), we write Emoji.from_svg(my_svg_string). This means that the function can’t access the self variable, because there is no particular object it’s being called on!

Emoji.from_svg() is kind of like an initializer in the sense that it takes in some input and returns a new instance of an Emoji. In some cases—including this one—we want it to be possible to initialize an Emoji in several different ways. Leaving Emoji.__init__() as a very simple initializer that assigns input arguments to attribute variables allows us to have a flexible way of creating Emoji instances. Then, Emoji.from_svg() will be used to do the work of parsing all of the visual elements out of the SVGs, arranging them in a dictionary with the proper shape, and then passing that in as an input to the initializer.

Magic Methods

The name “magic method” is very exciting, but the functionality of a magic method is fairly straightforward. Sometimes referred to as “dunder” or “double underscore” methods, magic methods are easily recognizable by the pair of underscore characters that precede and follow them. __init__ is an example of a magic method, as are some others that you have to implement in this assignment: __getitem__, __setitem__, __add__, and __sub__.

The defining feature of a magic method is that it is not typically called by its own name. Instead, its functionality is used when calling a more convenient function name or using a different operator. For example, in the Emoji class, these magic methods define the following behaviors:

Magic Method Corresponding Function/Operator
__init__ Creating a new Emoji with Emoji()
__add__ e + other, describes how other would be “added” to an Emoji named e
__sub__ e - other, describes how other would be “subtracted” from an Emoji named e
__getitem__ e["attribute"], describes how to retrieve information associated with the key "attribute" in e
__setitem__ e["attribute"] = d, describes how to associate key "attribute" with data d in e

This will allow us to do Emoji Blending with simple arithmetic expressions. If we wanted to get the decorations of one Emoji e and add it to another plain Emoji f, we could do that with the following:

decorations = e["decorations"]
f_with_decorations = f + decorations

Neat!


1. Reading Emoji

Implement Emoji.from_svg and then implement EmojiLibrary.load_emojis.

from_svg

Given the contents of an SVG as text, parse the SVG to create a dictionary that maps the class names to sets that stores the elements of the SVG with that class. Then, initialize a new Emoji object that stores that dictionary as its group attribute.

The SVG has a root <svg> tag, and this root will contain one or more <g> tags. Each of these <g> tags will have zero or more child elements of several different kinds. Each of these child elements will have a class attribute that specifies the group to which it belongs: "face", "decoration", "mouth", or "eyes". The dictionary that you return will use these class names as keys that map to sets of elements that have those classes applied.

For example, examine the following filename "simple.svg":

<svg xmlns="http://www.w3.org/2000/svg" id="emoji" viewBox="0 0 72 72">
    <g id="color">
        <circle class="face"/>
        <rect class="decoration"/>
    </g>
    <g id="line">
        <path class="mouth"/>
        <line class="eyes"/>
    </g>
    <g id="skin" />
</svg>

We could run this code and we would expect the following output:

input_file = open("simple.svg", "r")
input_str = input_file.read()
simple_emoji = Emoji.from_svg(input_str)
print(simple_emoji.groups)
{
    'face': {<Element '{http://www.w3.org/2000/svg}circle' at 0x10531c04>},
    'decoration': {<Element '{http://www.w3.org/2000/svg}rect' at 0x10531c0e0>},
    'mouth': {<Element '{http://www.w3.org/2000/svg}path' at 0x10531c1d0>},
    'eyes': {<Element '{http://www.w3.org/2000/svg}line' at 0x10531c270>}
}

If an SVG does not have any elements belonging to one of these four classes, then the Emoji that gets returned should still have that class name present as a key in its groups dictionary. The value that it maps to should be an empty set.

load_emojis

When an EmojiLibrary is initialized, it calls the load_emojis method that initializes a new Emoji object for each SVG file found in the directory denoted by directory_path and stores each emoji inside of the attribute dict called emojis. The keys for emojis will be the string names of the emojis and the values will be the Emoji objects themselves.

You can assume that the input directory contains a file named emojis.json in addition to one or more SVG files. This emojis.json file has the following structure:

[
  {
    "name": "grinning face",
    "hex": "1F600"
  },
  {
    "name": "grinning face with big eyes",
    "hex": "1F603"
  }
]

That is, it represents a list of dictionaries. Each dictionary has a "name" key that maps to a string representing the name of an emoji. The "hex" key represents the unique identifier for an emoji, and it also helps you find the file containing that emoji. The filename for the emoji will always be its hex code followed by ".svg". For example, the emojis in the JSON list above can be found in the files 1F600.svg and 1F603.svg.

Implement the load_emojis method. To do so, you will need to:

  1. Load the emojis.json file that lives in the input directory as a list of dictionaries using json.load()
  2. Iterate through the list of dictionaries, and for each one:
    1. retrieve the emoji name,
    2. retrieve the SVG filename,
    3. create an Emoji object out of the contents of each of the SVG file,
    4. and map the name to the Emoji object in the attribute dictionary emojis.

2. Emoji Magic Methods

Implement the __getitem__, __setitem__, __add__, and __sub__ magic methods.

__getitem__

Remember that e.__getitem__(key) is what gets called when we write e[key].

This function should return the set of elements in this emoji that have the class named key. If the key is not in the groups dictionary, this method should return an empty set.

For example, if we have an Emoji with the groups that looks like this dictionary:

{
    'face': {<Element '{http://www.w3.org/2000/svg}circle' at 0x10531c04>},
    'decoration': set(),
    'mouth': {<Element '{http://www.w3.org/2000/svg}path' at 0x10531c1d0>},
    'eyes': {<Element '{http://www.w3.org/2000/svg}line' at 0x10531c270>}
}

Then e["face"] should return the set {<Element '{http://www.w3.org/2000/svg}circle' at 0x10531c04>}. Both of e["decoration"] and e["garbage"] should return empty sets.

__setitem__

Remember that e.__setitem__(key, value) is what gets called when we write e[key] = value.

This function should set this Emoji’s group dictionary to map key to the input value. This will totally overwrite any existing elements in the group. It should be possible to use a key that is not already in the groups dictionary (i.e. not face, decoration, mouth, or eyes).

For example, if we have an Emoji with the groups that looks like this dictionary:

{
    'face': {<Element '{http://www.w3.org/2000/svg}circle' at 0x10531c04>},
    'decoration': {<Element '{http://www.w3.org/2000/svg}rect' at 0x10531c0e0>},
    'mouth': {<Element '{http://www.w3.org/2000/svg}path' at 0x10531c1d0>},
    'eyes': {<Element '{http://www.w3.org/2000/svg}line' at 0x10531c270>}
}

Then e["decoration"] = set() will overwrite e.groups["decoration"]. Querying for either e["decoration"] or e.groups["decoration"] afterwards would return an empty set.

__add__

Remember that e.add(other) is what gets called when we write e + other.

The input other for this method can either be another Emoji object or a dictionary that maps group names to sets of elements. If you want to check the type of a value, you can use isinstance(value, type) to check if value belongs to the type type. For example, isinstance(x, tuple) evaluates to True if x is a tuple and False otherwise. Similarly, isinstance(x, Emoji) will be True if and only if x is an Emoji.

Independent of whether the input is another Emoji or a dictionary mapping class names to sets of elements, this method should initialize and return a new Emoji object. Neither the existing Emoji nor other should be modified by this method call.

If other is another Emoji object, then the new Emoji that you create and return should contain all elements from both self and other. The groups dictionary should preserve the classes of each of the elements. That is, the new Emoji object’s groups dicionary should map "face" to a set containing all of self.groups["face"] and other.groups["face"].

If other is a dictionary instead, the behavior is similar: create a new Emoji object with a groups dictionary that maps all of the keys in self.groups and other to sets that contain all of the corresponding inputs from those two input dictionaries.

The resulting Emoji will have as its groups attribute a dictionary storing the unions of the elements in each group.

For example: If we took an Emoji with the following groups

{
    'face': {<Element 'circle' at 0x10531c04>},
    'decoration': set(),
    'mouth': {<Element 'path' at 0x10531c1d0>},
    'eyes': {<Element 'line' at 0x10531c270>}
}

and added it to another Emoji with the following groups:

{
    'face': set(),
    'decoration': {<Element 'rect' at 0x10531c0e0>},
    'mouth': {<Element 'path' at 0xFFFFFFFFFFF>},
    'eyes': set()
}

we would end up with a new Emoji with the following groups:

{
'face': {<Element '{http://www.w3.org/2000/svg}circle' at 0x10531c04>},
'decoration': {<Element '{http://www.w3.org/2000/svg}rect' at 0x10531c0e0>},
'mouth': {<Element '{http://www.w3.org/2000/svg}path' at 0x10531c1d0>,
            <Element '{http://www.w3.org/2000/svg}path' at 0xFFFFFFFFFFF>},
'eyes': {<Element '{http://www.w3.org/2000/svg}line' at 0x10531c270>}
}

__sub__

Remember that e.add(other) is what gets called when we write e - other.

The input other for this method should be a tuple of strings.

This method returns a new Emoji. Each string in the input tuple is the name of a key that should have its values set to the empty set in the groups dictionary of the output Emoji. Any groups not present in the input tuple should be copied as-is into the new Emoji.

Example: If we took an Emoji e with the following groups…

{
    'face': {<Element '{http://www.w3.org/2000/svg}circle' at 0x10531c04>},
    'decoration': {<Element '{http://www.w3.org/2000/svg}rect' at 0x10531c0e0>},
    'mouth': {<Element '{http://www.w3.org/2000/svg}path' at 0x10531c1d0>},
    'eyes': {<Element '{http://www.w3.org/2000/svg}line' at 0x10531c270>}
}

…and wrote e - ('face', 'mouth'), we would return up with a new Emoji with the following groups:

{
    'face': set(),
    'decoration': {<Element '{http://www.w3.org/2000/svg}rect' at 0x10531c0e0>},
    'mouth': set(),
    'eyes': {<Element '{http://www.w3.org/2000/svg}line' at 0x10531c270>}
}

3. Test Your Code

Write unit tests for each of the magic methods you implemented in test_emoji_blender_student.py. Unit tests for Emoji.from_svg and EmojiLibrary.load_emojis are provided for you.

It will be challenging to write unit tests that directly compare your outputs to expected outputs. Instead of checking to make sure that the actual output Emoji is exactly equal to some expected Emoji, you will probably have an easier time comparing the sizes of the sets in the group attributes of Emojis. For example, if Emoji created by some addition is supposed to have one element in the face class, three in the decoration class, two in the mouth class, and one in the eyes class, then you could verify it this way:

result = some_emoji + other_emoji # some_emoji and other_emoji are two Emoji objects
actual_groups = result.groups
self.assertEqual(1, len(actual_groups["face"]))
self.assertEqual(3, len(actual_groups["decoration"]))
self.assertEqual(2, len(actual_groups["mouth"]))
self.assertEqual(1, len(actual_groups["eyes"]))

4. Using the whole program

Run demo_emoji_blender.py to generate the silly constructions I came up with. You can use this file to experiment with your code. Each function generates a new emoji. My creations are saved to the out directory, which is done by using "out\" as a prefix to the argument passed into the to_svg method. The calls to the functions are commented out in main(); you can uncomment them as you complete the necessary methods in your implementation. Feel free to change anything in this file as you like.

Finally, add code to generate_my_emoji.py. In this file, you must generate your own emoji by combining at least two emoji together with +, -, or directly setting its values with e[key] = some_features. Save this emoji to a file named my_emoji.svg by using the to_svg method. (There are no additional requirements other than those listed here, but I hope you have some fun with this part!)

5. Readme and Submission

A. Readme

Complete readme_emoji.txt in the same way that you have done for previous assignments.

B. Submission

Submit emoji_blender.py, test_emoji_blender_student.py, generate_my_emoji.py, my_emoji.svg, and readme_emoji.txt on Gradescope.

Your code will be tested for compilation and checkstyle errors upon submission.

Important: Don’t forget to write comments for your function headers and test cases as mentioned earlier in the specifiaction.

Important: Remember to delete any print statements you added to your code before submitting.

If you encounter any autograder-related issues, please make a private post on Ed.