Skip to content

Commit 0bb037c

Browse files
committed
Add sample project
1 parent 9b63e6f commit 0bb037c

File tree

11 files changed

+341
-0
lines changed

11 files changed

+341
-0
lines changed

code-image-generator/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Build a Code Image Generator with Flask, Pygments, and Playwright
2+
3+
Follow the [step-by-step instructions](https://realpython.com/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 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) $ 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/`.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import base64
2+
3+
from flask import Flask, render_template, request, session, url_for
4+
from pygments import highlight
5+
from pygments.formatters import HtmlFormatter
6+
from pygments.lexers import Python3Lexer
7+
from pygments.styles import get_all_styles
8+
9+
from utils import make_screenshot
10+
11+
app = Flask(__name__)
12+
app.secret_key = "mysecretkey"
13+
14+
15+
DEFAULT_CODE = "print('Hello, World!')"
16+
DEFAULT_THEME = "default"
17+
18+
19+
@app.route("/", methods=["POST", "GET"])
20+
def code():
21+
if request.method == "POST" and "reset" in request.form.keys():
22+
session["code"] = DEFAULT_CODE
23+
code = session.get("code") or DEFAULT_CODE
24+
lines = code.split("\n")
25+
context = {
26+
"message": "Add Python Code 🐍",
27+
"code": code,
28+
"num_lines": len(lines),
29+
"max_chars": len(max(lines, key=len)),
30+
}
31+
app.logger.info(context)
32+
return render_template("code_input.html", **context)
33+
34+
35+
@app.route("/theme", methods=["POST", "GET"])
36+
def theme():
37+
if request.method == "POST":
38+
session["code"] = request.form.get("code") or session.get("code")
39+
session["theme"] = request.form.get("theme") or session.get("theme")
40+
theme = session.get("theme") or DEFAULT_THEME
41+
formatter = HtmlFormatter(style=theme)
42+
context = {
43+
"message": "Select Your Theme 🎨",
44+
"code": session["code"],
45+
"all_themes": list(get_all_styles()),
46+
"theme": theme,
47+
"theme_bg_color": formatter.style.background_color,
48+
"style": formatter.get_style_defs(),
49+
"code_highlighted": highlight(
50+
session["code"], Python3Lexer(), formatter
51+
),
52+
}
53+
return render_template("theme_selection.html", **context)
54+
55+
56+
@app.route("/preview", methods=["POST", "GET"])
57+
def preview():
58+
if request.method == "POST":
59+
session["theme"] = request.form.get("theme") or session.get("theme")
60+
theme = session.get("theme")
61+
formatter = HtmlFormatter(style=theme)
62+
context = {
63+
"message": "Preview Your Picture 🖼️",
64+
"code": session["code"],
65+
"all_themes": list(get_all_styles()),
66+
"theme": theme,
67+
"theme_bg_color": formatter.style.background_color,
68+
"style": formatter.get_style_defs(),
69+
"code_highlighted": highlight(
70+
session["code"], Python3Lexer(), formatter
71+
),
72+
}
73+
return render_template("preview.html", **context)
74+
75+
76+
@app.route("/screenshot", methods=["POST", "GET"])
77+
def screenshot():
78+
context = {"message": "No screenshot yet 👀"}
79+
if request.method == "POST":
80+
session["theme"] = request.form.get("theme") or session.get("theme")
81+
session_dict = {
82+
"name": app.config["SESSION_COOKIE_NAME"],
83+
"value": request.cookies.get(app.config["SESSION_COOKIE_NAME"]),
84+
"url": request.host_url,
85+
}
86+
target_url = request.host_url + url_for("theme")
87+
screenshot_bytes = make_screenshot(target_url, session_dict)
88+
context["message"] = "Done! 🎉"
89+
context["screenshot_b64"] = base64.b64encode(screenshot_bytes).decode()
90+
return render_template("screenshot.html", **context)
91+
return render_template("screenshot.html", **context)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
blinker==1.6.2
2+
click==8.1.3
3+
Flask==2.3.2
4+
greenlet==2.0.2
5+
itsdangerous==2.1.2
6+
Jinja2==3.1.2
7+
MarkupSafe==2.1.3
8+
playwright==1.35.0
9+
pyee==9.0.4
10+
Pygments==2.15.1
11+
typing_extensions==4.7.1
12+
Werkzeug==2.3.6
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
* {
2+
box-sizing: border-box;
3+
4+
}
5+
6+
body {
7+
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
8+
font-size: 20px;
9+
line-height: 1.3em;
10+
}
11+
12+
textarea,
13+
pre {
14+
font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
15+
}
16+
17+
h1 {
18+
font-size: 2em;
19+
text-align: center;
20+
}
21+
22+
h1 a {
23+
text-decoration: none;
24+
color: inherit;
25+
}
26+
27+
span {
28+
color: deepskyblue;
29+
}
30+
31+
button,
32+
select {
33+
cursor: pointer;
34+
font-size: 100%;
35+
margin: 0.25em;
36+
min-width: 8em;
37+
}
38+
39+
.columns {
40+
display: flex;
41+
justify-content: center;
42+
align-items: center;
43+
flex-wrap: wrap;
44+
}
45+
46+
47+
.code_input,
48+
.highlight {
49+
box-shadow: #767676 0px 20px 30px -10px;
50+
border-radius: 0.5em;
51+
padding: 1em 2em;
52+
min-width: 32em;
53+
max-width: 100%;
54+
display: inline-block;
55+
min-height: 12em;
56+
text-align: left;
57+
border: 1px solid #cecece;
58+
margin: 1em;
59+
line-height: 1.4em;
60+
overflow:scroll;
61+
}
62+
63+
.code_picture {
64+
text-align: center;
65+
margin-bottom: 1em;
66+
}
67+
68+
img {
69+
margin: 1em;
70+
display: block;
71+
border: 1px solid #cecece;
72+
max-width: 100%;
73+
}
74+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<div class="code_block">
2+
<style>
3+
{{ style }}
4+
5+
.highlight {
6+
background-color: {{ theme_bg_color }};
7+
}
8+
9+
pre {
10+
line-height: inherit;
11+
}
12+
</style>
13+
{{ code_highlighted | safe }}
14+
</div>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
{% block title %}Code Picture{% endblock %}
8+
</title>
9+
<link rel="stylesheet"
10+
type="text/css"
11+
href="{{ url_for('static', filename='styles.css') }}"/>
12+
</head>
13+
<body>
14+
<h1>
15+
<a href="{{ url_for('code') }}">Code Picture</a>: <span>{{ message }}</span>
16+
</h1>
17+
<main>
18+
{% block content %}
19+
{% endblock content %}
20+
</main>
21+
</body>
22+
</html>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{% extends "base.html" %}
2+
{% block title %}
3+
01 - Code Input
4+
{% endblock title %}
5+
{% block content %}
6+
<style>
7+
.code_input {
8+
min-height: calc({{ num_lines }}em * 1.4 + 2 * 1em);
9+
min-width: calc({{ max_chars }}ch + 2 * 2em);
10+
}
11+
</style>
12+
<form class="columns">
13+
<button name="reset" formmethod="post">Reset ♻️</button>
14+
<textarea class="code_input" name="code">{{ code }}</textarea>
15+
<button formmethod="post" formaction="{{ url_for("theme") }}">Next ➡️</button>
16+
</form>
17+
{% endblock content %}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{% extends "base.html" %}
2+
{% block title %}
3+
03 - Preview
4+
{% endblock title %}
5+
{% block content %}
6+
<form method="post" class="columns">
7+
<button formmethod="get" formaction="{{ url_for("theme") }}">⬅️ Back</button>
8+
{% include "_code_block.html" %}
9+
<button formaction="{{ url_for("screenshot") }}">Take Photo 📸</button>
10+
</form>
11+
{% endblock content %}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{% extends "base.html" %}
2+
{% block title %}
3+
04 - Your Screenshot
4+
{% endblock title %}
5+
{% block content %}
6+
<form class="columns">
7+
<button formmethod="get" formaction="{{ url_for("preview") }}">⬅️ Back</button>
8+
{% if screenshot_b64 %}
9+
<div class="code_picture">
10+
<img src="data:image/png;base64,{{ screenshot_b64 | safe }}"
11+
alt="Website Screenshot"/>
12+
<a href="data:image/png;base64,{{ screenshot_b64 | safe }}"
13+
download="screenshot.png">Download Your Code Picture ⤵️</a>
14+
</div>
15+
{% endif %}
16+
<button formmethod="get" formaction="{{ url_for("code") }}">🔄 Back to Start</button>
17+
</form>
18+
{% endblock content %}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{% extends "base.html" %}
2+
{% block title %}
3+
02 - Theme Selection
4+
{% endblock title %}
5+
{% block content %}
6+
<form method="post" class="columns">
7+
<button formaction="{{ url_for("code") }}">⬅️ Back</button>
8+
<div>
9+
<div class="columns">
10+
<select name="theme">
11+
{% for theme_name in all_themes %}
12+
<option value="{{ theme_name }}"
13+
{% if theme_name == theme %}selected{% endif %}>
14+
{{ theme_name }}
15+
</option>
16+
{% endfor %}
17+
</select>
18+
<button>Reload</button>
19+
</div>
20+
{% include "_code_block.html" %}
21+
</div>
22+
<div>
23+
<button formaction="{{ url_for("preview") }}">Next ➡️</button>
24+
</div>
25+
</form>
26+
{% endblock content %}

0 commit comments

Comments
 (0)