Skip to content

Commit 09e95ec

Browse files
authored
Merge pull request #411 from realpython/code-image-generator
Add materials for the Code Image Generator tutorial
2 parents 727ad8d + 149c7fa commit 09e95ec

File tree

33 files changed

+1126
-0
lines changed

33 files changed

+1126
-0
lines changed

code-image-generator/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Build a Code Image Generator with Flask, Pygments, and Playwright
2+
3+
Follow the [step-by-step instructions](https://realpython.com/python-code-image-generator/) on Real Python.
4+
5+
## Setup
6+
7+
You can run the provided example project on your local machine by following the steps outlined below.
8+
9+
Create a new virtual environment:
10+
11+
```bash
12+
python3 -m venv venv/
13+
```
14+
15+
Activate the virtual environment:
16+
17+
```bash
18+
source ./venv/bin/activate
19+
```
20+
21+
Navigate to the folder for the step that you're currently on.
22+
23+
Install the dependencies for this project if you haven't installed them yet:
24+
25+
```bash
26+
(venv) $ python -m pip install -r requirements.txt
27+
```
28+
29+
Next, you need to install Playwright:
30+
31+
```bash
32+
(venv) $ playwright install
33+
```
34+
35+
Finally, run the Flask development server
36+
37+
```bash
38+
(venv) $ python -m flask run
39+
```
40+
41+
Now you can navigate to the address that's shown in the output when you start the server. Commonly, that's `http://localhost:5000/`.
42+
43+
## Secret Key
44+
45+
If you want to deploy your Flask app later, then it's a good idea to generate a proper secret key.
46+
47+
If you need to create cryptographically sound data like a Flask secret key, then you can use Python's [`secrets`](https://docs.python.org/3/library/secrets.html) module:
48+
49+
```pycon
50+
>>> import secrets
51+
>>> secrets.token_hex()
52+
'2e9ac41b1e0b66a8d93d66400e2300c4b4c2953f'
53+
```
54+
55+
The `.token_hex()` method returns a [hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) string containing random numbers and letters from `0` to `9` and `a` to `f`. Use the value that `secrets.token_hex()` outputs for you and add it to your Flask project's `app.py` file:
56+
57+
```python hl_lines="6"
58+
# app.py
59+
60+
from flask import Flask, render_template
61+
62+
app = Flask(__name__)
63+
app.secret_key = "2e9ac41b1e0b66a8d93d66400e2300c4b4c2953f"
64+
65+
# ...
66+
```
67+
68+
To avoid saving the secret key directly in your code, it may be a good idea to work with [environment variables](https://12factor.net/config). You can learn more about that in the Flask documentation on [configuration handling](https://flask.palletsprojects.com/en/2.3.x/config/).
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import base64
2+
from flask import (
3+
Flask,
4+
redirect,
5+
render_template,
6+
request,
7+
session,
8+
url_for,
9+
)
10+
from pygments import highlight
11+
from pygments.formatters import HtmlFormatter
12+
from pygments.lexers import Python3Lexer
13+
from pygments.styles import get_all_styles
14+
from utils import take_screenshot_from_url
15+
16+
app = Flask(__name__)
17+
app.secret_key = (
18+
"AddYourSecretKeyHere" # See the README.md file for instructions
19+
)
20+
21+
PLACEHOLDER_CODE = "print('Hello, World!')"
22+
DEFAULT_STYLE = "monokai"
23+
NO_CODE_FALLBACK = "# No Code Entered"
24+
25+
26+
@app.route("/", methods=["GET"])
27+
def code():
28+
if session.get("code") is None:
29+
session["code"] = PLACEHOLDER_CODE
30+
lines = session["code"].split("\n")
31+
context = {
32+
"message": "Paste Your Python Code 🐍",
33+
"code": session["code"],
34+
"num_lines": len(lines),
35+
"max_chars": len(max(lines, key=len)),
36+
}
37+
return render_template("code_input.html", **context)
38+
39+
40+
@app.route("/save_code", methods=["POST"])
41+
def save_code():
42+
session["code"] = request.form.get("code") or NO_CODE_FALLBACK
43+
return redirect(url_for("code"))
44+
45+
46+
@app.route("/reset_session", methods=["POST"])
47+
def reset_session():
48+
session.clear()
49+
session["code"] = PLACEHOLDER_CODE
50+
return redirect(url_for("code"))
51+
52+
53+
@app.route("/style", methods=["GET"])
54+
def style():
55+
if session.get("style") is None:
56+
session["style"] = DEFAULT_STYLE
57+
formatter = HtmlFormatter(style=session["style"])
58+
context = {
59+
"message": "Select Your Style 🎨",
60+
"all_styles": list(get_all_styles()),
61+
"style": session["style"],
62+
"style_definitions": formatter.get_style_defs(),
63+
"style_bg_color": formatter.style.background_color,
64+
"highlighted_code": highlight(
65+
session["code"], Python3Lexer(), formatter
66+
),
67+
}
68+
return render_template("style_selection.html", **context)
69+
70+
71+
@app.route("/save_style", methods=["POST"])
72+
def save_style():
73+
if request.form.get("style") is not None:
74+
session["style"] = request.form.get("style")
75+
if request.form.get("code") is not None:
76+
session["code"] = request.form.get("code") or NO_CODE_FALLBACK
77+
return redirect(url_for("style"))
78+
79+
80+
@app.route("/image", methods=["GET"])
81+
def image():
82+
session_data = {
83+
"name": app.config["SESSION_COOKIE_NAME"],
84+
"value": request.cookies.get(app.config["SESSION_COOKIE_NAME"]),
85+
"url": request.host_url,
86+
}
87+
target_url = request.host_url + url_for("style")
88+
image_bytes = take_screenshot_from_url(target_url, session_data)
89+
context = {
90+
"message": "Done! 🎉",
91+
"image_b64": base64.b64encode(image_bytes).decode("utf-8"),
92+
}
93+
return render_template("image.html", **context)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Flask==2.3.2
2+
playwright==1.35.0
3+
Pygments==2.15.1
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
body {
6+
font-family: sans-serif;
7+
font-size: 20px;
8+
line-height: 1.3em;
9+
}
10+
11+
main {
12+
display: flex;
13+
justify-content: center;
14+
text-align: center;
15+
}
16+
17+
h1 {
18+
font-size: 2em;
19+
text-align: center;
20+
line-height: 1em;
21+
}
22+
23+
h1 a {
24+
text-decoration: none;
25+
color: inherit;
26+
}
27+
28+
span {
29+
color: deepskyblue;
30+
}
31+
32+
button,
33+
select {
34+
cursor: pointer;
35+
font-size: 100%;
36+
margin: 0.25em;
37+
min-width: 8em;
38+
}
39+
40+
.controls {
41+
margin: 1em 0;
42+
display: flex;
43+
justify-content: center;
44+
align-items: center;
45+
flex-wrap: wrap;
46+
}
47+
48+
.code {
49+
font-family: monospace;
50+
box-shadow: #767676 0px 20px 30px -10px;
51+
border-radius: 0.5em;
52+
padding: 1em 2em;
53+
min-width: 32em;
54+
max-width: 100%;
55+
min-height: 12em;
56+
text-align: left;
57+
border: 1px solid #cecece;
58+
margin: 1em;
59+
line-height: 1.4em;
60+
}
61+
62+
.code-picture {
63+
text-align: center;
64+
}
65+
66+
.code-picture img {
67+
margin: 1em 0;
68+
border: 1px solid #cecece;
69+
max-width: 80%;
70+
padding: 1em;
71+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8"/>
5+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
6+
<title>
7+
Code to Image
8+
{% block title %}
9+
{% endblock title %}
10+
</title>
11+
<link rel="stylesheet"
12+
type="text/css"
13+
href="{{ url_for('static', filename='styles.css') }}"/>
14+
</head>
15+
<body>
16+
<h1>
17+
<a href="{{ url_for('code') }}">Code to Image</a>: <span>{{ message }}</span>
18+
</h1>
19+
<main>
20+
{% block content %}
21+
{% endblock content %}
22+
</main>
23+
</body>
24+
</html>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{% extends "base.html" %}
2+
3+
{% block title %}
4+
- Code Input
5+
{% endblock title %}
6+
7+
{% block content %}
8+
<style>
9+
.code {
10+
min-height: calc({{ num_lines }}em * 1.5 + 2 * 1em);
11+
min-width: calc({{ max_chars }}ch + 4 * 2em);
12+
}
13+
</style>
14+
<form>
15+
<textarea class="code"
16+
name="code"
17+
placeholder="Paste your Python code here">{{ code }}</textarea>
18+
<div class="controls">
19+
<button formmethod="get" formaction="{{ url_for("reset_session") }}">
20+
Reset Session ♻️
21+
</button>
22+
<button formmethod="post" formaction="{{ url_for("save_code") }}">
23+
Save Code 💾
24+
</button>
25+
<button formmethod="post" formaction="{{ url_for("save_style") }}">
26+
Next ➡️
27+
</button>
28+
</div>
29+
</form>
30+
{% endblock content %}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{% extends "base.html" %}
2+
3+
{% block title %}
4+
- Your Image
5+
{% endblock title %}
6+
7+
{% block content %}
8+
<form>
9+
{% if image_b64 %}
10+
<div class="code-picture">
11+
<img src="data:image/png;base64,{{ image_b64 | safe }}"
12+
alt="Code Image" />
13+
</div>
14+
<a href="data:image/png;base64,{{ image_b64 | safe }}"
15+
download="Your_CodeImage.png">
16+
Download Your Code Image ⤵️
17+
</a>
18+
{% endif %}
19+
<div class="controls">
20+
<button formmethod="get" formaction="{{ url_for("code") }}">
21+
🔄 Back to Start
22+
</button>
23+
</div>
24+
</form>
25+
{% endblock content %}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{% extends "base.html" %}
2+
3+
{% block title %}
4+
- Style Selection
5+
{% endblock title %}
6+
7+
{% block content %}
8+
<style>
9+
{{ style_definitions }}
10+
11+
.code {
12+
background-color: {{ style_bg_color }};
13+
}
14+
</style>
15+
<script>
16+
document.addEventListener("DOMContentLoaded", () => {
17+
document.querySelector("select").addEventListener("change", () => {
18+
document.querySelector("form").submit();
19+
});
20+
});
21+
</script>
22+
<form method="post" action="{{ url_for("save_style") }}">
23+
<div class="controls">
24+
<select name="style">
25+
{% for style_name in all_styles %}
26+
<option value="{{ style_name }}"
27+
{% if style_name == style %}selected{% endif %}>
28+
{{ style_name }}
29+
</option>
30+
{% endfor %}
31+
</select>
32+
</div>
33+
<div class="code">
34+
{{ highlighted_code | safe }}
35+
</div>
36+
<div class="controls">
37+
<button formmethod="get" formaction="{{ url_for("code") }}">
38+
⬅️ Back
39+
</button>
40+
<button formmethod="get" formaction="{{ url_for("image") }}">
41+
Create Image 📸
42+
</button>
43+
</div>
44+
</form>
45+
{% endblock content %}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from playwright.sync_api import sync_playwright
2+
3+
4+
def take_screenshot_from_url(url, session_data):
5+
with sync_playwright() as playwright:
6+
webkit = playwright.webkit
7+
browser = webkit.launch()
8+
browser_context = browser.new_context(device_scale_factor=2)
9+
browser_context.add_cookies([session_data])
10+
page = browser_context.new_page()
11+
page.goto(url)
12+
screenshot_bytes = page.locator(".code").screenshot()
13+
browser.close()
14+
return screenshot_bytes
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from flask import Flask, render_template
2+
3+
app = Flask(__name__)
4+
5+
6+
@app.route("/", methods=["GET"])
7+
def code():
8+
context = {
9+
"message": "Paste Your Python Code 🐍",
10+
}
11+
return render_template("code_input.html", **context)

0 commit comments

Comments
 (0)