├── images ├── in.png └── out.png ├── templates ├── static │ ├── css │ │ └── style.css │ └── js │ │ └── script.js └── layouts │ ├── result.html │ ├── results.html │ ├── index.html │ └── base.html ├── Pipfile ├── .gitignore ├── app.py └── README.md /images/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsejan/python-flask-with-javascript/HEAD/images/in.png -------------------------------------------------------------------------------- /images/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsejan/python-flask-with-javascript/HEAD/images/out.png -------------------------------------------------------------------------------- /templates/static/css/style.css: -------------------------------------------------------------------------------- 1 | .btn{ 2 | background-color: rgb(102,153,255); 3 | } 4 | img.results-img{ 5 | float: left; 6 | height: 100px; 7 | } 8 | #inputCanvas{ 9 | border: 2px solid rgb(102,153,255); 10 | } 11 | #footer{ 12 | margin-top: 100px; 13 | } 14 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | matplotlib = "*" 10 | flask = "*" 11 | mypy = "*" 12 | flake8 = "*" 13 | isort = "*" 14 | black = "*" 15 | 16 | [requires] 17 | python_version = "3.9" 18 | 19 | [pipenv] 20 | allow_prereleases = true 21 | -------------------------------------------------------------------------------- /templates/layouts/result.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% block title %}{{title}}{% endblock %} 3 | {% block head %} 4 | {{ super() }} 5 | {% endblock %} 6 | {% block content %} 7 |
8 |

Result

9 | Image Placeholder 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/layouts/results.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% block title %}{{title}}{% endblock %} 3 | {% block head %} 4 | {{ super() }} 5 | {% endblock %} 6 | {% block content %} 7 |
8 | {% for data in datalist %} 9 | Image Placeholder 10 | {% endfor %} 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/layouts/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | {% block title %}{{title}}{% endblock %} 3 | {% block head %} 4 | {{ super() }} 5 | {% endblock %} 6 | {% block content %} 7 |
8 |

{{ title}}

9 | 10 |
11 |

12 |
13 | 14 | 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/layouts/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | 7 | 8 | 9 | {% block title %}{% endblock %} - Python & Javascript 10 | {% endblock %} 11 | 12 | 13 |
14 | 30 |
31 |
32 | {% block content %}{% endblock %}
33 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | Pipfile.lock 106 | images/*.csv 107 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import io 3 | import os 4 | import uuid 5 | 6 | import numpy as np 7 | from flask import Flask, jsonify, make_response, render_template, request 8 | from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas 9 | from matplotlib.figure import Figure 10 | 11 | app = Flask(__name__) 12 | app.secret_key = "s3cr3t" 13 | app.debug = False 14 | app._static_folder = os.path.abspath("templates/static/") 15 | 16 | 17 | @app.route("/", methods=["GET"]) 18 | def index(): 19 | title = "Create the input image" 20 | return render_template("layouts/index.html", title=title) 21 | 22 | 23 | @app.route("/results/", methods=["GET"]) 24 | def results(): 25 | title = "Results" 26 | datalist = [] 27 | for csv in glob.iglob("images/*.csv"): 28 | datalist.append(get_file_content(csv)) 29 | return render_template("layouts/results.html", title=title, datalist=datalist) 30 | 31 | 32 | @app.route("/results/", methods=["GET"]) 33 | def result_for_uuid(unique_id): 34 | title = "Result" 35 | data = get_file_content(get_file_name(unique_id)) 36 | return render_template("layouts/result.html", title=title, data=data) 37 | 38 | 39 | @app.route("/postmethod", methods=["POST"]) 40 | def post_javascript_data(): 41 | jsdata = request.form["canvas_data"] 42 | unique_id = create_csv(jsdata) 43 | params = {"unique_id": unique_id} 44 | return jsonify(params) 45 | 46 | 47 | @app.route("/plot/") 48 | def plot(imgdata): 49 | data = [float(i) for i in imgdata.strip("[]").split(",")] 50 | data = np.reshape(data, (200, 200)) 51 | fig = Figure() 52 | axis = fig.add_subplot(1, 1, 1) 53 | axis.axis("off") 54 | axis.imshow(data, interpolation="nearest") 55 | canvas = FigureCanvas(fig) 56 | output = io.BytesIO() 57 | canvas.print_png(output) 58 | response = make_response(output.getvalue()) 59 | response.mimetype = "image/png" 60 | return response 61 | 62 | 63 | def create_csv(text): 64 | unique_id = str(uuid.uuid4()) 65 | with open(get_file_name(unique_id), "a") as file: 66 | file.write(text[1:-1] + "\n") 67 | return unique_id 68 | 69 | 70 | def get_file_name(unique_id): 71 | return f"images/{unique_id}.csv" 72 | 73 | 74 | def get_file_content(filename): 75 | with open(filename, "r") as file: 76 | return file.read() 77 | 78 | 79 | if __name__ == "__main__": 80 | app.run(host="0.0.0.0", port=5000) 81 | -------------------------------------------------------------------------------- /templates/static/js/script.js: -------------------------------------------------------------------------------- 1 | $( document ).ready(function() { 2 | 3 | function createCanvas(parent, width, height) { 4 | var canvas = document.getElementById("inputCanvas"); 5 | canvas.context = canvas.getContext('2d'); 6 | return canvas; 7 | } 8 | 9 | function init(container, width, height, fillColor) { 10 | var canvas = createCanvas(container, width, height); 11 | var ctx = canvas.context; 12 | ctx.fillCircle = function(x, y, radius, fillColor) { 13 | this.fillStyle = fillColor; 14 | this.beginPath(); 15 | this.moveTo(x, y); 16 | this.arc(x, y, radius, 0, Math.PI * 2, false); 17 | this.fill(); 18 | }; 19 | ctx.clearTo = function(fillColor) { 20 | ctx.fillStyle = fillColor; 21 | ctx.fillRect(0, 0, width, height); 22 | }; 23 | ctx.clearTo("#fff"); 24 | 25 | canvas.onmousemove = function(e) { 26 | if (!canvas.isDrawing) { 27 | return; 28 | } 29 | var x = e.pageX - this.offsetLeft; 30 | var y = e.pageY - this.offsetTop; 31 | var radius = 10; 32 | var fillColor = 'rgb(102,153,255)'; 33 | ctx.fillCircle(x, y, radius, fillColor); 34 | }; 35 | canvas.onmousedown = function(e) { 36 | canvas.isDrawing = true; 37 | }; 38 | canvas.onmouseup = function(e) { 39 | canvas.isDrawing = false; 40 | }; 41 | } 42 | 43 | var container = document.getElementById('canvas'); 44 | init(container, 200, 200, '#ddd'); 45 | 46 | function clearCanvas() { 47 | var canvas = document.getElementById("inputCanvas"); 48 | var ctx = canvas.getContext("2d"); 49 | ctx.clearRect(0, 0, canvas.width, canvas.height); 50 | } 51 | 52 | function getData() { 53 | var canvas = document.getElementById("inputCanvas"); 54 | var imageData = canvas.context.getImageData(0, 0, canvas.width, canvas.height); 55 | var data = imageData.data; 56 | var outputData = [] 57 | for(var i = 0; i < data.length; i += 4) { 58 | var brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]; 59 | outputData.push(brightness); 60 | } 61 | $.post( "/postmethod", { 62 | canvas_data: JSON.stringify(outputData) 63 | }, function(err, req, resp){ 64 | window.location.href = "/results/"+resp["responseJSON"]["unique_id"]; 65 | console.log(resp); 66 | }); 67 | } 68 | 69 | $( "#clearButton" ).click(function(){ 70 | clearCanvas(); 71 | }); 72 | 73 | $( "#sendButton" ).click(function(){ 74 | getData(); 75 | }); 76 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Using Python and Javascript together with Flask 2 | 3 | ### Quickstart 4 | 5 | ``` bash 6 | $ git clone https://github.com/jitsejan/python-flask-with-javascript.git 7 | $ cd python-flask-with-javascript 8 | python-flask-with-javascript $ pipenv run flask run 9 | * Environment: production 10 | WARNING: This is a development server. Do not use it in a production deployment. 11 | Use a production WSGI server instead. 12 | * Debug mode: off 13 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 14 | ``` 15 | 16 | Open http://127.0.0.1:5000/, draw a character, click on Send and watch your terminal! 17 | 18 | image-20191206022811472 19 | 20 | image-20191206022811472 21 | 22 | ### Introduction 23 | 24 | In this project I am experimenting with sending data between Javascript and Python using the web framework Flask. Additionally I will use matplotlib to generate a dynamic graph based on the provided user input data. 25 | 26 | ### Key learning points 27 | 28 | * Sending data from Python to Javascript 29 | * Receiving data in Python from Javascript 30 | * Creating an image dynamically using a special Flask route 31 | 32 | ### Important bits 33 | 34 | Send the `outputData` from Javascript to Python with a POST call to postmethod and use the form variable canvas_data. The POST call give a response from Python and the page is redirected to the results page with the given unique ID. 35 | 36 | ```javascript 37 | ... 38 | $.post( "/postmethod", { 39 | canvas_data: JSON.stringify(outputData) 40 | }, function(err, req, resp){ 41 | window.location.href = "/results/"+resp["responseJSON"]["unique_id"]; 42 | }); 43 | ... 44 | ``` 45 | 46 | Retrieve the `canvas_data` from the POST request and write the content to a file. Return the unique id that was used for writing to the file. 47 | 48 | ```python 49 | ... 50 | @app.route('/postmethod', methods=['POST']) 51 | def post_javascript_data(): 52 | jsdata = request.form['canvas_data'] 53 | unique_id = create_csv(jsdata) 54 | params = {'unique_id': unique_id } 55 | return jsonify(params) 56 | ... 57 | ``` 58 | 59 | ### Implementation 60 | 61 | The core of the web application is inside this file. Here I define the different routes for the website and specify the settings. The default route shows the index.html where a canvas is shown. The result route will show the image once a picture is drawn, based on the provided unique ID. The postmethod route is defined to handle the data coming from Javascript into Python via a POST call. The content of the POST variable are written to a CSV file which can be used again on the result page where data is loaded from this same file. 62 | 63 | `app.py` 64 | 65 | ```python 66 | from __future__ import print_function 67 | from flask import Flask, render_template, make_response 68 | from flask import redirect, request, jsonify, url_for 69 | 70 | import io 71 | import os 72 | import uuid 73 | from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas 74 | from matplotlib.figure import Figure 75 | import numpy as np 76 | 77 | app = Flask(__name__) 78 | app.secret_key = 's3cr3t' 79 | app.debug = True 80 | app._static_folder = os.path.abspath("templates/static/") 81 | 82 | @app.route('/', methods=['GET']) 83 | def index(): 84 | title = 'Create the input' 85 | return render_template('layouts/index.html', 86 | title=title) 87 | 88 | @app.route('/results/', methods=['GET']) 89 | def results(unique_id): 90 | title = 'Result' 91 | data = get_file_content(unique_id) 92 | return render_template('layouts/results.html', 93 | title=title, 94 | data=data) 95 | 96 | @app.route('/postmethod', methods = ['POST']) 97 | def post_javascript_data(): 98 | jsdata = request.form['canvas_data'] 99 | unique_id = create_csv(jsdata) 100 | params = { 'unique_id' : unique_id } 101 | return jsonify(params) 102 | 103 | @app.route('/plot/') 104 | def plot(imgdata): 105 | data = [float(i) for i in imgdata.strip('[]').split(',')] 106 | data = np.reshape(data, (200, 200)) 107 | fig = Figure() 108 | axis = fig.add_subplot(1, 1, 1) 109 | axis.axis('off') 110 | axis.imshow(data, interpolation='nearest') 111 | canvas = FigureCanvas(fig) 112 | output = io.BytesIO() 113 | canvas.print_png(output) 114 | response = make_response(output.getvalue()) 115 | response.mimetype = 'image/png' 116 | return response 117 | 118 | def create_csv(text): 119 | unique_id = str(uuid.uuid4()) 120 | with open('images/'+unique_id+'.csv', 'a') as file: 121 | file.write(text[1:-1]+"\n") 122 | return unique_id 123 | 124 | def get_file_content(unique_id): 125 | with open('images/'+unique_id+'.csv', 'r') as file: 126 | return file.read() 127 | 128 | if __name__ == '__main__': 129 | app.run(host='0.0.0.0', port=5000) 130 | ``` 131 | 132 | The second part of the magic happens in the Javascript file. In this file a canvas is generated and added to the DOM. The mouse is used to draw dots on the canvas with a predefined color and radius. One button is used to send the data of the current drawing on the canvas and another one is used to clear the canvas. 133 | 134 | `templates/static/js/script.js` 135 | 136 | ```javascript 137 | $( document ).ready(function() { 138 | 139 | function createCanvas(parent, width, height) { 140 | var canvas = document.getElementById("inputCanvas"); 141 | canvas.context = canvas.getContext('2d'); 142 | return canvas; 143 | } 144 | 145 | function init(container, width, height, fillColor) { 146 | var canvas = createCanvas(container, width, height); 147 | var ctx = canvas.context; 148 | ctx.fillCircle = function(x, y, radius, fillColor) { 149 | this.fillStyle = fillColor; 150 | this.beginPath(); 151 | this.moveTo(x, y); 152 | this.arc(x, y, radius, 0, Math.PI * 2, false); 153 | this.fill(); 154 | }; 155 | ctx.clearTo = function(fillColor) { 156 | ctx.fillStyle = fillColor; 157 | ctx.fillRect(0, 0, width, height); 158 | }; 159 | ctx.clearTo("#fff"); 160 | 161 | canvas.onmousemove = function(e) { 162 | if (!canvas.isDrawing) { 163 | return; 164 | } 165 | var x = e.pageX - this.offsetLeft; 166 | var y = e.pageY - this.offsetTop; 167 | var radius = 10; 168 | var fillColor = 'rgb(102,153,255)'; 169 | ctx.fillCircle(x, y, radius, fillColor); 170 | }; 171 | canvas.onmousedown = function(e) { 172 | canvas.isDrawing = true; 173 | }; 174 | canvas.onmouseup = function(e) { 175 | canvas.isDrawing = false; 176 | }; 177 | } 178 | 179 | var container = document.getElementById('canvas'); 180 | init(container, 200, 200, '#ddd'); 181 | 182 | function clearCanvas() { 183 | var canvas = document.getElementById("inputCanvas"); 184 | var ctx = canvas.getContext("2d"); 185 | ctx.clearRect(0, 0, canvas.width, canvas.height); 186 | } 187 | 188 | function getData() { 189 | var canvas = document.getElementById("inputCanvas"); 190 | var imageData = canvas.context.getImageData(0, 0, canvas.width, canvas.height); 191 | var data = imageData.data; 192 | var outputData = [] 193 | for(var i = 0; i < data.length; i += 4) { 194 | var brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]; 195 | outputData.push(brightness); 196 | } 197 | $.post( "/postmethod", { 198 | canvas_data: JSON.stringify(outputData) 199 | }, function(err, req, resp){ 200 | window.location.href = "/results/"+resp["responseJSON"]["unique_id"]; 201 | }); 202 | } 203 | 204 | $( "#clearButton" ).click(function(){ 205 | clearCanvas(); 206 | }); 207 | 208 | $( "#sendButton" ).click(function(){ 209 | getData(); 210 | }); 211 | }); 212 | ``` 213 | 214 | Finally we need to define a base template to be used by the index and result page. I know this could be split up nicer and I could make better use of the templating engine, but for this experiment it seemed sufficient. 215 | 216 | `templates/layouts/base.html` 217 | 218 | ```html 219 | 220 | 221 | 222 | {% block head %} 223 | 224 | 225 | 226 | 227 | {% block title %}{% endblock %} - Simple Flask app 228 | {% endblock %} 229 | 230 | 231 |
232 | 248 |
249 |
250 | {% block content %}{% endblock %}
251 | 256 |
257 | 258 |
259 | 260 | 261 | ``` 262 | 263 | The code for the index.html and results.html can be kept to a minimum this way. 264 | 265 | `templates/layouts/index.html` 266 | 267 | ```html 268 | {% extends "layouts/base.html" %} 269 | {% block title %}{{title}}{% endblock %} 270 | {% block head %} 271 | {{ super() }} 272 | {% endblock %} 273 | {% block content %} 274 |
275 |

{{ title}}

276 | 277 |
278 |

279 |
280 | 281 | 282 |
283 | {% endblock %} 284 | ``` 285 | 286 | `templates/layouts/result.html` 287 | 288 | ```html 289 | {% extends "layouts/base.html" %} 290 | {% block title %}{{title}}{% endblock %} 291 | {% block head %} 292 | {{ super() }} 293 | {% endblock %} 294 | {% block content %} 295 |
296 |

Results

297 | Image Placeholder 298 |
299 | {% endblock %} 300 | ``` 301 | 302 | Important: Please note that for the source of the image the specific URL for the matplotlib image is used. The route for plot is called with the parameter `imgdata` containing the data. 303 | 304 | I have kept the stylesheet very basic since this project is not aimed at making the most slick interface. 305 | 306 | `templates/static/css/style.css` 307 | 308 | ```css 309 | .btn{ 310 | background-color: rgb(102,153,255); 311 | } 312 | #inputCanvas{ 313 | border: 2px solid rgb(102,153,255); 314 | } 315 | #footer{ 316 | margin-top: 100px; 317 | } 318 | ``` 319 | 320 | After putting all the files together the application can be started and visited on port 5000 on the localhost. 321 | 322 | ```shell 323 | ~/code/flask-app $ FLASK_APP=app.py FLASK_DEBUG=1 flask run 324 | ``` 325 | 326 | [Original blog post](https://www.jitsejan.com/python-and-javascript-in-flask.html) --------------------------------------------------------------------------------