├── 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 |
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 |
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 | Clear
14 | Send data
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 |
15 |
22 |
23 |
29 |
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 |
19 |
20 |
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 |
233 |
240 |
241 |
247 |
248 |
249 |
250 | {% block content %}{% endblock %}
251 |
256 |
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 | Clear
281 | Send data
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 |
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)
--------------------------------------------------------------------------------