A Web Paint App with Flask and MongoDB
By Prabeesh Keezhathra
- 5 minutes read - 855 wordsSchema-less, document-shaped data like freehand drawings is a good fit for MongoDB: there’s no rigid table to design around stroke coordinates that vary per drawing. This post ties the canvas front-end to a Flask backend that persists drawings in MongoDB.
How the app works
The user draws lines, rectangles, or circles on an HTML5 canvas. Each shape is stored in a JavaScript object with its coordinates and colour. When the user clicks “save”, the frontend POSTs the entire drawing as a JSON string to Flask, which inserts it into MongoDB keyed by image name. Loading a drawing is a GET request: Flask queries MongoDB, passes the JSON back to the template, and the canvas redraws it.
The Flask backend
The entire server is one file. It connects to a local MongoDB instance, serves the canvas page, and handles save/load:
1from flask import Flask, request, render_template, Response
2from pymongo import Connection
3
4app = Flask(__name__)
5
6connection = Connection()
7collection = connection.paint.images
8
9@app.route("/")
10@app.route('/<imagename>', methods=['POST', 'GET'])
11def mainpage(imagename=None):
12 if request.method == 'GET':
13 if imagename:
14 rows = collection.find({'imgname': imagename})
15 if rows:
16 for row in rows:
17 imgdata = row["imgdata"]
18 return render_template('paint.html', saved=imgdata)
19 else:
20 resp = Response(
21 '<html><script>'
22 'alert("Image not found");'
23 'document.location.href="/"'
24 '</script></html>'
25 )
26 return resp
27 else:
28 return render_template('paint.html')
29
30 if request.method == 'POST':
31 imgname = request.form['imagename']
32 imgdata = request.form['string']
33 collection.insert({"imgname": imgname, "imgdata": imgdata})
34 return Response("saved")
35
36if __name__ == '__main__':
37 app.debug = True
38 app.run()
A few things to note:
Connection()(from the older pymongo API) connects tolocalhost:27017by default. In current pymongo you would useMongoClient().- The route handles both
/(new drawing) and/<imagename>(load or save). Themethodslist lets the same URL respond to GET and POST. - On save, the drawing data arrives as a JSON string in
request.form['string']and gets stored verbatim in MongoDB. No schema needed. - On load, Flask passes the stored JSON string into the Jinja template as
saved, and the JavaScript parses it back into drawing objects.
The MongoDB document
Each saved drawing produces one document in the paint.images collection:
1{
2 "_id": ObjectId("..."),
3 "imgname": "my-drawing",
4 "imgdata": "{\"line\":[{\"beginx\":120,\"beginy\":80,\"endx\":400,\"endy\":300,\"color\":\"red\"}],\"rect\":[],\"circle\":[]}"
5}
The imgdata field is a JSON string containing three arrays (one per shape type). Each shape stores its start/end coordinates and colour. This is the simplest possible approach: store the drawing as an opaque blob keyed by name.
The canvas frontend
The template (templates/paint.html) sets up two stacked canvases: one for the active stroke being drawn, one for the committed shapes underneath. The drawing tools (line, rectangle, circle) are selected from a dropdown, and colour buttons set the stroke colour.
The core interaction captures mousedown, mousemove, and mouseup events:
1var data = {"line":[], "rect":[], "circle":[]};
2
3canvas.addEventListener('mousedown', function(evt) {
4 var mousePos = getMousePos(canvas, evt);
5 select = 1;
6 beginX = mousePos.x;
7 beginY = mousePos.y;
8}, false);
9
10canvas.addEventListener('mousemove', function(evt) {
11 var mousePos = getMousePos(canvas, evt);
12 if (select && toolselect.value == 'line') {
13 endX = mousePos.x;
14 endY = mousePos.y;
15 drawLine(beginX, beginY, endX, endY);
16 }
17 if (select && toolselect.value == 'rect') {
18 sideX = mousePos.x - beginX;
19 sideY = mousePos.y - beginY;
20 drawRect(beginX, beginY, sideX, sideY);
21 }
22 if (select && toolselect.value == 'circle') {
23 radius = mousePos.x - beginX;
24 drawCircle(beginX, beginY, radius);
25 }
26}, false);
27
28canvas.addEventListener('mouseup', function(evt) {
29 select = 0;
30 context1.drawImage(canvas, 0, 0);
31
32 if (toolselect.value == 'line') {
33 data.line.push({
34 beginx: beginX, beginy: beginY,
35 endx: endX, endy: endY, color: currentColor
36 });
37 }
38 if (toolselect.value == 'rect') {
39 data.rect.push({
40 beginx: beginX, beginy: beginY,
41 sidex: sideX, sidey: sideY, color: currentColor
42 });
43 }
44 if (toolselect.value == 'circle') {
45 data.circle.push({
46 beginx: beginX, beginy: beginY,
47 radius: radius, color: currentColor
48 });
49 }
50}, false);
On mouseup, the shape is committed to canvas1 (the background layer) with drawImage, and its coordinates are pushed into the data object. That object is what gets serialised to JSON and sent to Flask on save.
Save and load use jQuery to POST/redirect:
1function saveImage() {
2 if (imagename.value == "")
3 alert("Image name cannot be empty");
4 else {
5 $.post("/" + imagename.value,
6 {imagename: imagename.value, string: JSON.stringify(data)});
7 alert("saved");
8 }
9}
10
11function loadImage() {
12 if (imagename.value == "")
13 alert("Image name cannot be empty");
14 else
15 document.location.href = "/" + imagename.value;
16}
When loading, Flask passes the stored JSON to the template, and the init() function rehydrates it:
1function init() {
2 var dataString = "{{ saved }}";
3 if (dataString) {
4 data = JSON.parse(dataString.replace(/"/g, '"'));
5 drawAll();
6 }
7}
" is the HTML entity for ", which Jinja escapes by default. The replace call converts it back before parsing.
Running it
1# Start MongoDB
2mongod
3
4# In another terminal
5pip install flask pymongo
6python test.py
Open http://localhost:5000, draw something, type a name, and click save. Navigate to http://localhost:5000/your-name to load it back.
The complete source is on GitHub.