{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "# Building the HAFFA Logo" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The HAFFA logo was originally drawn up by Diane Basta and scanned to provide an image file. That image file has been in use for a long time, appearing on club hats, tee-shirts and the club website. However, since the only available image is a bitmap, scaling the logo up for use on banners, or incorporating it in a badge for use on models or membership cards has not been practical. \n", "\n", "\n", "\n", "In this note, we will construct an SVG file that can provide the missing functionality. We will use a Python Jupyter notebook to document how this new logo is being generated from the original club logo.\n", "\n", "## Logo Components\n", "\n", "The original logo has five basic elements:\n", "\n", "1. The US Flag.\n", "\n", "2. A heart shape mask used to mask off parts of the flag.\n", "\n", "3. A Modeler launching an airplane.\n", "\n", "4. A free-flight model.\n", "\n", "5. The text \"HAFFA\" styled in what looks like the papyrus font.\n", "\n", "## Tracing the image file\n", "\n", "I started off by using **Snagit**, an image editing tool, to crop off the modeler, airplane and HAFFA text from the image file. The flag and heart are well defined, so there was no need to attempt to process those parts of the logo image. I broke up the text into the three letters used so each could be processed individually.\n", "\n", "Once I had isolated those components, I used **Inkscape** to trace the parts, producing a rough SVG version of each item. **Inkscape** has a \"simplify\" command that cleans up the SVG somewhat, at the expense of rounding some areas. The resulting vector form of each component was very messy, since the resolution of the images used was fairly low. \n", "\n", "Finally, I then manually edited the images using **Inkscape**, deleting many of the remaining points to smooth up the final SVG for each item. I did some minor editing to make the data easier t see as well. Here are the basic component files I produced:\n", "\n", "### Pilot:\n", "\n", "\n", "\n", "### Airplane:\n", "\n", "\n", "\n", "### Letter H:\n", "\n", "\n", "\n", "### Letter A:\n", "\n", "\n", "\n", "### Letter F:\n", "\n", "\n", "\n", "All of these elements stll retain the hand-drawn feel. We may need to tweak them further as we build large format images from the final SVG files we produce.\n", "\n", "**inkscape** produces SVG that is a bit odd. For most users, this does not matter. However, My goal was to build a clean SVG file for the club logo, so I decided to manually cut out the needed drawing data from the **inkscape** SVG code and use that to produce an SVG file more readable by humans.\n", "\n", "We will use a bit of Python to do this job." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Processing a Basic SVG file\n", "\n", "SVG is a text format, based on XML, which uses \"tags\" to identify each fundamental component of an image. If you have not seen this type of markup before, the idea is pretty simple. Here is a basic \"tag\":\n", "\n", " \n", " tag body\n", " \n", " \n", "Other tags can be placed in the tag body area producing a nested structure. It is common to indent those nested elements for readability.\n", "\n", "SVG defines a lot of standard tag names, each performing some action when the image file is displayed. Some tags cause visible drawing effects to appear. Other tags are more administrative in nature.\n", "\n", "For instance, the outermost tag in any SVG file is the **svg** tag itself. This tag defines the drawing canvas to be used for the image. we will see an example of this in a bit.\n", "\n", "SVG files can be processed using a number of Python tools. One common tool is **lxml** which can be installed using the standard library installer **pip**. You can inatall **lxml** using this command:\n", "\n", " $ pip install lxml\n", "\n", "Once this step finishes, you can check your installation by importing the library:" ] }, { "cell_type": "code", "execution_count": 65, "metadata": {}, "outputs": [], "source": [ "import lxml" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you see no errors, you are ready to go.\n", "\n", "Let's begin by building a simple function that displays the text in an SVG file:" ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [], "source": [ "def show(filename):\n", " with open('_images/%s' % filename) as fin:\n", " lines = fin.readlines()\n", " for l in lines:\n", " print(l.rstrip())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now use this function to diaplsy one of our **inkscape** SVG files:" ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", " \n", " \n", " \n", " image/svg+xml\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ] } ], "source": [ "show('haffa-letter-H.svg')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you examine this **inkscape** file closly, you will see that it defines a drawing canvas in the **svg** tag, and has a **path** tag that defines the shape to be drawn and the color to use to fill that shape. The shape may be contained inside of a **g** tag which can provide transformation information in case the shape needs to be scaled, moved, or rotated for final display. \n", "\n", "The **path** tag contains the important data used to draw the shape. We will extract those data items and collect each one in a list of items to draw. There will be two kinds of items in this list: command letters, and coordinate pair values. Fortunately, **inkscape** uses spaces to separate these items making is easy for Python to break things up for processing. \n", "\n", "Jupyter Notebook makes it difficult to build code using a top-down approach, so we will build a few utility functons first, then work up to the final management code." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will need a function that takes an SVG filename and uses **lxml** to parse that file into a structure we can use to extract data. This structure is called a parse tree:" ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [], "source": [ "import os\n", "from lxml import etree\n", "\n", "def parse(filename):\n", " fname = os.path.join(\"_images\", filename)\n", " tree = etree.parse(fname)\n", " return tree" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can check this function out by processing the same file:" ] }, { "cell_type": "code", "execution_count": 69, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "tree = parse(\"haffa-letter-H.svg\")\n", "print(tree)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The tree is a data structure, not something we can print this way!\n", "\n", "We will need to extract three basic pieces of data from each file: canvas size (width and height), path data, and fill color. One of the files also has a transform data item in the **g** tag, so we need to check for this as well. To simplify extracting these data items, this, we will build functions for each one:" ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "233.30176 200\n" ] } ], "source": [ "def get_canvas_size(tree):\n", " for element in tree.iter():\n", " tag = element.tag.split('}')[1]\n", " if tag == 'svg': break\n", " width = element.get('width')\n", " height = element.get('height')\n", " return width, height\n", " \n", "w,h = get_canvas_size(tree)\n", "print(w,h)" ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "#b30000\n" ] } ], "source": [ "def get_fill_color(tree):\n", " for element in tree.iter():\n", " tag = element.tag.split('}')[1]\n", " if tag == 'path': break\n", " style = element.get('style')\n", " f1 = style.find('fill:')\n", " style = style[f1+5:]\n", " f2 = style.find(';')\n", " if f2>0:\n", " color = style[:f2]\n", " else:\n", " color = style\n", " return color\n", "\n", "color = get_fill_color(tree)\n", "print(color)" ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['m', '-135.01024,132.70382', 'c', '10.54609,3.28331', '21.06694,-3.94871', '24.20863,-14.05748', '4.83401,-12.31659', '4.18421,-25.82625', '7.88479,-38.447441', '4.573935,-21.167941', '9.167168,-42.343603', '14.707323,-63.284024', '19.235095,-5.954538', '39.279958,2.222467', '58.889842,0.247879', '18.340853,0.452088', '36.9701078,-1.646062', '55.092227,2.271167', '0.790803,20.083306', '-5.016041,39.717921', '-8.910998,59.261021', '-2.763164,11.181582', '-4.782812,22.665968', '-8.1356686,33.631268', '-2.3256387,7.99989', '-9.35474886,14.56234', '-12.7824555,20.60349', '9.4996704,3.40853', '20.1369771,1.19488', '29.9556781,-0.006', '4.274571,-10.41031', '2.242043,-25.10932', '4.4021,-37.099568', '0.325371,-13.547194', '7.011616,-25.690748', '8.520576,-38.895347', '1.485797,-15.289763', '4.23731,-30.443234', '8.0051,-45.294194', '1.084922,-13.9603685', '4.103866,-27.680181', '6.002906,-41.514128', '2.50087,-10.587528', '7.267076,-20.619131', '14.015169,-29.145285', '0.01284,-11.146878', '-18.523894,-1.562864', '-23.464886,3.334707', '-7.88075,7.836575', '-3.357508,8.803847', '-7.31786,22.433612', 'C', '33.537958,-24.622444', '33.692745,-0.79858141', '25.555547,2.3678998', '6.5808517,1.8772569', '-12.394418,0.0218051', '-31.364679,0.5161385', 'c', '-17.990282,-1.9676646', '-36.150518,-1.24229362', '-54.137347,-3.2591353', '0.515947,-14.8566282', '5.406472,-29.1459632', '5.766658,-44.0271972', '1.919969,-4.340467', '13.249644,-18.475635', '4.153312,-18.40317', '-6.77894,6.101398', '-18.69357,7.820132', '-23.160506,15.10823', '4.561691,9.960814', '0.02443,20.720079', '-0.293812,31.06479', '-3.716266,28.611616', '-7.586166,57.535089', '-17.149176,84.85951', '-5.24254,21.780535', '-5.41783,45.980844', '-19.90285,64.298704', '-0.4781,1.37089', '-1.96263,3.62855', '1.07816,2.54595', 'z']\n" ] } ], "source": [ "def get_path_data(tree):\n", " for element in tree.iter():\n", " tag = element.tag.split('}')[1]\n", " if tag == 'path': break\n", " data = element.get('d').split()\n", " return data\n", " \n", "data = get_path_data(tree)\n", "print(data)" ] }, { "cell_type": "code", "execution_count": 73, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "136.94005 65.687287\n" ] } ], "source": [ "def get_translate_data(tree):\n", " xtranslate = 0\n", " ytranslate = 0\n", " tag = None\n", " for element in tree.iter():\n", " tag = element.tag.split('}')[1]\n", " if tag == 'g': break\n", " if tag:\n", " data = element.get('transform')\n", " if data:\n", " f1 = data.find('(')\n", " x,y = (data[f1+1:-1]).split(',')\n", " xtranslate = float(x)\n", " ytranslate = float(y)\n", " return xtranslate, ytranslate\n", " \n", "xt,yt = get_translate_data(tree)\n", "print(xt, yt)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We now have the tools needed to extract the basic drawing data. We can create a clean SVG file using the data provided by our new functions." ] }, { "cell_type": "code", "execution_count": 74, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " \n", " \n", " \n", "\n" ] } ], "source": [ "def clean_svg(filename, data, xt, yt, color, width, height):\n", " fname = os.path.join('_images', filename)\n", " svg = \"\"\"\"\"\" % (width, height)\n", " svg += \"\"\" \n", " \n", " \n", " \n", "\"\"\" % color\n", " with open(fname, \"w\") as fout:\n", " fout.write(svg)\n", " \n", "clean_svg('clean-haffa-letter-H.svg', data, xt, yt, color, w, h)\n", "show('clean-haffa-letter-H.svg')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The coordinate data in this file has a precision that far exceeds anything we might actually need. We can clean that up by processing the data list. Here is a routine that does this." ] }, { "cell_type": "code", "execution_count": 75, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['m', '-135.01,132.70', 'c', '10.55,3.28', '21.07,-3.95', '24.21,-14.06', '4.83,-12.32', '4.18,-25.83', '7.88,-38.45', '4.57,-21.17', '9.17,-42.34', '14.71,-63.28', '19.24,-5.95', '39.28,2.22', '58.89,0.25', '18.34,0.45', '36.97,-1.65', '55.09,2.27', '0.79,20.08', '-5.02,39.72', '-8.91,59.26', '-2.76,11.18', '-4.78,22.67', '-8.14,33.63', '-2.33,8.00', '-9.35,14.56', '-12.78,20.60', '9.50,3.41', '20.14,1.19', '29.96,-0.01', '4.27,-10.41', '2.24,-25.11', '4.40,-37.10', '0.33,-13.55', '7.01,-25.69', '8.52,-38.90', '1.49,-15.29', '4.24,-30.44', '8.01,-45.29', '1.08,-13.96', '4.10,-27.68', '6.00,-41.51', '2.50,-10.59', '7.27,-20.62', '14.02,-29.15', '0.01,-11.15', '-18.52,-1.56', '-23.46,3.33', '-7.88,7.84', '-3.36,8.80', '-7.32,22.43', 'C', '33.54,-24.62', '33.69,-0.80', '25.56,2.37', '6.58,1.88', '-12.39,0.02', '-31.36,0.52', 'c', '-17.99,-1.97', '-36.15,-1.24', '-54.14,-3.26', '0.52,-14.86', '5.41,-29.15', '5.77,-44.03', '1.92,-4.34', '13.25,-18.48', '4.15,-18.40', '-6.78,6.10', '-18.69,7.82', '-23.16,15.11', '4.56,9.96', '0.02,20.72', '-0.29,31.06', '-3.72,28.61', '-7.59,57.54', '-17.15,84.86', '-5.24,21.78', '-5.42,45.98', '-19.90,64.30', '-0.48,1.37', '-1.96,3.63', '1.08,2.55', 'z']\n" ] } ], "source": [ "def normalize_path_data(data):\n", " norm = []\n", " for item in data:\n", " if item.isalpha():\n", " norm.append(item)\n", " else:\n", " x,y = item.split(',')\n", " fx = float(x)\n", " fy = float(y)\n", " norm.append(\"%.2f,%.2f\" % (fx,fy))\n", " return norm\n", " \n", "norm = normalize_path_data(data)\n", "print(norm)" ] }, { "cell_type": "code", "execution_count": 76, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " \n", " \n", " \n", "\n" ] } ], "source": [ "clean_svg('clean-haffa-letter-H.svg', norm, xt, yt, color, w, h)\n", "show('clean-haffa-letter-H.svg')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is a bit cleaner. \n", "\n", "## Removing Relative Coordinates\n", "\n", "For reference purposes, we can inspect the **path** commands to see ewhat they actually will draw.most of these items are coordinate pairs. The items that are simple leters are SVG drawing commands. To understand them, we need to examine what each command does.\n", "\n", "SVG uses a basic dimensionless coordinate system. Normally, these dimensions refer to pixels on a screen, but we can make them dimensional with a little work. The **x** coordinate runs left to right. The **y** coordinate runs from top to bottom, which is a little odd for humans. The drawing commands move a virtual pen around in this space, and may define a line of simply move the pen frm place to place leaving no line. In out output we only see a few of the available SVG drawing commands. These commands may be upper case, or lower case. If they are upper case, the coordinates needed are absolute. If they are lower case, the provided coordinates are relative to the last point drawn. In our file, most commands are relative, so the coordinates shown are not the final values. We need to do some math to get the actual drawing coordinates. Here are the commands we see here:\n", "\n", "1. **m** - move the pen to the specified coordinate (since we start off at 0,0, the provide coordinates are actually absolute values\n", "\n", "2. **c** - draw a bezier curve. The final coordinate and two control points are specified.\n", "\n", "3. **l** draw a straight line from the last point specified to the next coordinate.\n", "\n", "4. **z** - close the current path by drawing a straight line from the last point specified to the first point in the path. The resulting area can be filled. The fill color is specified by the **style** paramener.\n", "\n", "If a command is followed by more coordinate values than the command needs, the same command is implied. This continues until another literal command letter is seen.\n", "\n", "In the code that follows, we will process the path data and eliminate the relative commands. The resulting drawing commands will now include absolute dimensions, which might be easier to use when we combine these elements into the final logo SVG file.\n", "\n" ] }, { "cell_type": "code", "execution_count": 77, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "\n", "\n" ] } ], "source": [ "x = xt\n", "y = yt\n", "i = 0\n", "xmin = 5000\n", "ymin = 5000\n", "xmax = -5000\n", "ymax = -5000\n", "\n", "numitems = len(norm)\n", "command = ''\n", "\n", "def split_coord(cstr):\n", " global xt, yt\n", " x,y = cstr.split(',')\n", " x = float(x)\n", " y = float(y)\n", " return x,y\n", "\n", "def move(i, relative):\n", " global x,y, xt, yt, xmax, ymax, newpath\n", " \n", " dx,dy = split_coord(norm[i])\n", " if relative:\n", " x += dx\n", " y += dy\n", " else:\n", " x = dx\n", " y = dy\n", " xs = \"%.2f\" % x\n", " ys = \"%.2f\" % y\n", " if x > xmax: xmax = x\n", " if y > ymax: ymax = y\n", " newpath.extend([xs,ys])\n", " return 1\n", " \n", "def curve(i, relative):\n", " global x, y, xmax, ymax, newpath\n", " cx1,cy1 = split_coord(norm[i])\n", " cx2,cy2 = split_coord(norm[i+1])\n", " cx3,cy3 = split_coord(norm[i+2])\n", " if relative:\n", " cx1 = x + cx1\n", " cy1 = y + cy1\n", " cx2 = x + cx2\n", " cy2 = y + cy2\n", " x += cx3 \n", " y += cy3\n", " else:\n", " x = cx3\n", " y = cy3\n", " cx1 = \"%.2f\" % cx1\n", " cy1 = \"%.2f\" % cy1\n", " cx2 = \"%.2f\" % cx2\n", " cy2 = \"%.2f\" % cy2\n", " xs = \"%.2f\" % x\n", " ys = \"%.2f\" % y\n", " newpath.extend([cx1,cy1,cx2,cy2,xs,ys])\n", " if x > xmax: xmax = x\n", " if y > ymax: ymax = y\n", " return 3\n", "\n", "def close(i, relative):\n", " return 1\n", " \n", "def commands(argument, i):\n", " relative = argument.islower()\n", " switcher = {\n", " 'm' : move,\n", " 'M' : move,\n", " 'c' : curve,\n", " 'C' : curve,\n", " 'l' : move,\n", " 'L' : move,\n", " 'z' : close,\n", " 'Z' : close,\n", " }\n", " func = switcher.get(argument, lambda: \"Invalid command\")\n", " return func(i, relative)\n", " \n", "def gen_svg(xmax, ymax, path, color):\n", " svg = \"\"\"\n", "\n", "\n", "\"\"\" % (color)\n", " print(svg)\n", "\n", "newpath = []\n", "while i < numitems:\n", " command = norm[i]\n", " i += 1\n", " newpath.append(command.upper())\n", " done = False\n", " while not done:\n", " i += commands(command,i)\n", " if x > xmax: xmax = x\n", " if y > ymax: ymax = y\n", " if x < xmin: xmin = x\n", " if y < ymin: ymin = y\n", " \n", " done = i >= numitems or norm[i].isalpha()\n", "gen_svg(xmax, ymax, newpath, color)\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "this code is not being used for the current project, but may be useful later.\n", "\n", "Now, we can build clean files for all of our component parts:" ] }, { "cell_type": "code", "execution_count": 80, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " \n", " \n", " \n", "\n", "\n", " \n", " \n", " \n", "\n", "\n", " \n", " \n", " \n", "\n", "\n", " \n", " \n", " \n", "\n", "\n", " \n", " \n", " \n", "\n" ] } ], "source": [ "files = [\n", " 'haffa-letter-A.svg',\n", " 'haffa-letter-F.svg',\n", " 'haffa-letter-H.svg',\n", " 'haffa-pilot.svg',\n", " 'haffa-airplane.svg'\n", "]\n", "\n", "for fname in files:\n", " tree = parse(fname)\n", " w,h = get_canvas_size(tree)\n", " color = get_fill_color(tree)\n", " xt,yt = get_translate_data(tree)\n", " data = get_path_data(tree)\n", " norm = normalize_path_data(data)\n", " \n", " cname = 'clean-' + fname\n", " clean_svg(cname, norm, xt, yt, color, w, h)\n", " show(cname)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As a check, let's display these files and make sure each one looks correct.\n", "\n", "### Pilot:\n", "\n", "\n", "\n", "### Airplane:\n", "\n", "\n", "\n", "### Letter H:\n", "\n", "\n", "\n", "### Letter A:\n", "\n", "\n", "\n", "### Letter F:\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These look fine. We still have two basic items to generate: the flag, and the heart shape that will mask that flag. Both of these elements will be set up in a separate notebook." ] }, { "cell_type": "code", "execution_count": 82, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'draw' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mflag\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mdraw\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDrawing\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mflag_height\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mflag_width\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;31m# Draw an irregular polygon\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m flag.append(draw.Lines(0, 0,\n\u001b[1;32m 5\u001b[0m \u001b[0mflag_width\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mNameError\u001b[0m: name 'draw' is not defined" ] } ], "source": [ "flag = draw.Drawing(flag_height, flag_width)\n", "\n", "# Draw an irregular polygon\n", "flag.append(draw.Lines(0, 0,\n", " flag_width, 0,\n", " flag_width, flag_height,\n", " 0, flag_height,\n", " close=True,\n", " fill='red',\n", " stroke='black'))\n", "flag.setRenderSize(600)" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "flag" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.5" } }, "nbformat": 4, "nbformat_minor": 4 }