Compare commits

...

10 commits

Author SHA1 Message Date
TT-392
cc00ceff58 Add advanced options to the image page 2025-11-03 11:15:44 +01:00
TT-392
f433ae663f Add image presets functionality 2025-11-02 15:56:02 +01:00
TT-392
f138db573a Add todo 2025-10-31 21:23:58 +01:00
TT-392
23a5935a7d Tile input sections 2025-10-31 21:17:11 +01:00
TT-392
fc0b7acc8f Reimplement bold, and fix bug that broke adding fields 2025-10-31 20:54:49 +01:00
TT-392
9062be9f54 Move templates selection client side 2025-10-31 19:54:10 +01:00
TT-392
6652bf02ed Fix bug in gen_image that would cause font size to be ignored 2025-10-31 19:35:51 +01:00
TT-392
e41695e256 Make the back end accept n text fields 2025-10-31 15:31:00 +00:00
TT-392
eb1056144c modify gen_image to allow for n number of lines 2025-10-31 14:58:23 +00:00
TT-392
c7b5ac9f5f Create some javascript to make the front end have n lines 2025-10-31 15:27:40 +01:00
8 changed files with 340 additions and 140 deletions

128
app.py
View file

@ -19,57 +19,10 @@ app = Flask(__name__)
app.secret_key = "blahaj"
templates = {
"DNH": {
"text1": {
"string": "Do Not Hack",
"size": 130,
"pos": 0,
"bold": True
},
"text2": {
"string": "bottom text",
"size": 50,
"pos": 130,
"bold": False
},
"cut": True
},
"Food": {
"text1": {
"string": "Nickname",
"size": 130,
"pos": 0,
"bold": True
},
"text2": {
"string": "",
"size": 50,
"pos": 130,
"bold": False
},
"cut": True
},
"Remove by": {
"text1": {
"string": "Remove by",
"size": 130,
"pos": 0,
"bold": True
},
"text2": {
"string": "",
"size": 50,
"pos": 130,
"bold": False
},
"cut": True
}
}
def render_text_template(info=None, info_color=None, scrollDown=None):
return render_template('text.html', filename=session["text image path"], text1=session["text1"], text2=session["text2"], cut=session["cut"], info=info, info_color=info_color, scrollDown=scrollDown, fonts=session["fonts"])
return render_template('text.html', filename=session["text image path"], cut=session["cut"], info=info, info_color=info_color, scrollDown=scrollDown, fonts=session["fonts"])
@ -90,13 +43,20 @@ def on_new_user(session):
session["text image path"] = None
session["uploaded image path"] = None
session["cut"] = True
session["text1"] = templates["DNH"]["text1"]
session["text2"] = templates["DNH"]["text2"]
session["cut"] = templates["DNH"]["cut"]
session["cut"] = True
session["fonts"] = fonts()
class Image_preset:
def __init__(self, path, dithering=True, threshold_if_not_dithering=0.5):
self.path = path
self.dithering = dithering
self.threshold_if_not_dithering = threshold_if_not_dithering
image_presets = {
"Abandoned": Image_preset("resources/abandoned.jpg", False, 0.5)
}
@app.route('/image', methods=['GET', 'POST'])
def image():
check_for_new_user(session)
@ -105,15 +65,28 @@ def image():
if 'image' not in request.files:
return render_image_template()
file = request.files['image']
if "dropdown" in request.form and request.form["dropdown"] != "file":
path = image_presets[request.form["dropdown"]].path
dither = image_presets[request.form["dropdown"]].dithering
threshold_if_not_dithering = image_presets[request.form["dropdown"]].threshold_if_not_dithering
class file:
filename = os.path.basename(path)
stream = open(path, "rb")
else:
file = request.files['image']
dither = "dithering" in request.form
threshold_if_not_dithering = float(request.form["threshold"]) * 0.01
if file.filename == '':
return render_image_template()
if file:
else:
extension = os.path.splitext(file.filename)[1]
try:
message, status, img = process_image(file.stream)
message, status, img = process_image(file.stream, dither, threshold_if_not_dithering)
file.stream.close()
session["uploaded image path"] = uploaded_image_filename
img.save(get_file_path(session, session["uploaded image path"]))
except Exception as e:
@ -155,44 +128,23 @@ def image_print():
return render_image_template()
@app.route('/text-template', methods=['GET', 'POST'])
def text_template():
check_for_new_user(session)
if request.method == 'POST':
template = templates[request.form["template"]]
if request.form["template"] == "Food":
template["text2"]["string"] = datetime.now().strftime('%Y-%m-%d')
if request.form["template"] == "Remove by":
template["text2"]["string"] = (datetime.now() + timedelta(weeks=3)).strftime('%Y-%m-%d')
session["text1"] = template["text1"]
session["text2"] = template["text2"]
session["cut"] = template["cut"]
return render_text_template()
@app.route('/text-form', methods=['GET', 'POST'])
def text_form():
check_for_new_user(session)
if request.method == 'POST':
session["text1"] = {
"string": request.form["string1"],
"size": int(request.form["size1"]),
"pos": int(request.form["pos1"]),
"bold": "bold1" in request.form,
}
session["text2"] = {
"string": request.form["string2"],
"size": int(request.form["size2"]),
"pos": int(request.form["pos2"]),
"bold": "bold2" in request.form,
}
line_count = int(request.form["lineCount"])
lines = []
for i in range(0, line_count):
lines.append({
"string": request.form["text" + str(i)],
"size": int(request.form["size" + str(i)]),
"pos": int(request.form["y_pos" + str(i)]),
"bold": "bold" + str(i) in request.form
})
print(request.form)
# Clear previously saved font
for font in session["fonts"]:
session["fonts"][font]["selected"] = False
@ -216,7 +168,9 @@ def text_form():
if chosen_font is None:
return
message, status, img = gen_image(label_width, session["text1"], session["text2"], chosen_font)
print(chosen_font)
message, status, img = gen_image(label_width, lines, chosen_font)
if status == "Error":
session["text image path"] = None

View file

@ -1,6 +1,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
label_width = 213
#label_width = 213
label_width = 450
printer_width = 512
max_label_length = 2000

View file

@ -2,38 +2,39 @@
from PIL import Image, ImageDraw, ImageFont
from colorama import Fore, Style
from config import *
from fonts import fonts
#font = "resources/ComicMono.ttf"
def gen_image(height, text1, text2, font):
font1 = ImageFont.truetype(font["path"], size=text1["size"])
font2 = ImageFont.truetype(font["path"], size=text2["size"])
def gen_image(height, lines, font):
text_widths = []
for text in lines:
pil_font = ImageFont.truetype(font["path"], size=text["size"])
text1["string"] = text1["string"].replace("\r\n", "\n")
text2["string"] = text2["string"].replace("\r\n", "\n")
text["string"] = text["string"].replace("\r\n", "\n")
lines1 = text1["string"].split('\n')
lines2 = text2["string"].split('\n')
sub_lines = text["string"].split('\n')
text1_width = max([font1.getbbox(line)[2] for line in lines1])
text2_width = max([font2.getbbox(line)[2] for line in lines2])
text_widths.append(max([pil_font.getbbox(sub_line)[2] for sub_line in sub_lines]))
text_max_start = max([pil_font.getbbox(sub_line)[0] for sub_line in sub_lines])
text1_max_start = max([font1.getbbox(line)[0] for line in lines1])
text2_max_start = max([font2.getbbox(line)[0] for line in lines2])
# I am assuming the left corner of the bbox to always be 0, I have yet to
# encounter a situation when this isn't the case
if (text_max_start != 0):
print(Fore.YELLOW + "Warning, found situation where left corner of bbox of text != 0,", "text1_box:", text1, "text2_box", text2)
# I am assuming the left corner of the bbox to always be 0, I have yet to
# encounter a situation when this isn't the case
if (text1_max_start != 0 or text2_max_start != 0):
print(Fore.YELLOW + "Warning, found situation where left corner of bbox of text != 0,", "text1_box:", text1, "text2_box", text2)
# the 1 here assures we don't get a 0 width image
width = max(text1_width, text2_width, 1)
text_widths.append(1)
width = max(text_widths)
# '1' is 1 bit bitdepth
img = Image.new('1', (width, height), color=1)
draw = ImageDraw.Draw(img)
draw.text((0, text1["pos"]), text1["string"], font=font1, stroke_width = font["stroke_width_bold"] if text1["bold"] else font["stroke_width"], stroke_fill="black")
draw.text((0, text2["pos"]), text2["string"], font=font2, stroke_width = font["stroke_width_bold"] if text2["bold"] else font["stroke_width"], stroke_fill="black")
for line in lines:
pil_font = ImageFont.truetype(font["path"], size=line["size"])
draw.text((0, line["pos"]), line["string"], font=pil_font, stroke_width = font["stroke_width_bold"] if line["bold"] else font["stroke_width"], stroke_fill="black")
if (width > max_label_length):
@ -49,11 +50,22 @@ Text1 = {
"string": "Hello world",
"size": 30,
"pos": 10,
"bold": False,
}
Text2 = {
"string": "Hello worhfeiheifhefld\nafefe",
"size": 20,
"pos": 40,
"bold": False,
}
Text3 = {
"string": "Hello worhfeiheifhefld\nafefe",
"size": 20,
"pos": 80,
"bold": False,
}
#gen_image(200, [Text1, Text2, Text3], fonts()["CYBER"])[2].save("aaa.png")

View file

@ -11,7 +11,7 @@ def format_image_to_label(path):
return new_image
def process_image(image_filestream):
def process_image(image_filestream, dither: bool, threshold_if_not_dithering: float):
image = Image.open(image_filestream)
message = None
status = None
@ -26,7 +26,15 @@ def process_image(image_filestream):
image = image.resize((new_width, new_height))
image = image.convert('1', dither=Image.FLOYDSTEINBERG)
if dither:
image = image.convert('1', dither=Image.FLOYDSTEINBERG)
else:
threshold = int(threshold_if_not_dithering * 255)
print("thesholding")
image = image.point(lambda p: 255 if p > threshold else 0)
print("done thesholding")
image = image.convert("1")
if image.width > max_label_length:
message= f"Label too long, max allowed length = {max_label_length}, yours = {image.width}."

BIN
resources/abandoned.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -74,6 +74,12 @@
border: 2px solid #ff69b4;
}
.tiles {
display: grid;
grid-template-columns: repeat(auto-fit, 190px);
gap: 20px
}
</style>
{% block head %}

View file

@ -3,12 +3,41 @@
{% block head %}
<title>LabelPrinter Image upload</title>
<style>
.threshold {
display: inline;
}
.dithering:checked ~ .threshold {
display: none;
}
</style>
{% endblock %}
{% block content %}
<h1>Upload Image</h1>
<form method="POST" action="/image" enctype="multipart/form-data">
<input type="file" name="image">
<form method="POST" action="/image" id="image_form" enctype="multipart/form-data">
{% if label_width == 450 %}
<label for="dropdown">Image:</label>
<select name="dropdown" id="image_preset_dropdown" onchange="handle_dropdown(this.value)">
<option value="file">Upload file</option>
{% if label_width == 450 %}
<option value="Abandoned">Abandoned item</option>
{% endif %}
</select><br>
{%endif%}
<div id="file">
<input type="file" name="image"><br>
<br>
<details name="advanced_options">
<summary>Advanced options</summary>
Dithering: <input type="checkbox" name="dithering" value="dithering" class="dithering" checked><br>
<div class="threshold">
Black/white cutoff threshold: <input type="number" value="50" name="threshold" style="width: 26px; padding: 3px; margin: 0px"> %
</div>
</details>
</div>
<input type="submit" value="Generate preview"><br>
Recommended image height = {{label_width}}, other sizes will be scaled
</form>
@ -18,7 +47,7 @@
{% endif %}
{% if filename %}
<img src="user_data/{{filename}}" alt="Uploaded image" style="height: 150px; width: auto;">
<img src="user_data/{{filename}}" alt="Uploaded image" style="height: {{label_width * 0.7 }}px; width: auto;">
<form method="POST" action="/image-print">
{% if cut %}
@ -30,5 +59,66 @@
<input type="submit" value="Print" style="font-size: 2em; padding: 20px 40px;">
{% endif %}
<script>
function get_persistent(name, initial_value) {
if (localStorage.getItem(name) == null) {
localStorage.setItem(name, initial_value)
}
return localStorage.getItem(name)
}
function handle_dropdown(value) {
localStorage.setItem("image_preset_dropdown", value)
if (value == "file") {
document.getElementById("file").style.display = "inline";
} else {
document.getElementById("file").style.display = "none";
}
}
const persistent_elements = ["dropdown", "threshold", "dithering", "advanced_options"]
for (element_name of persistent_elements) {
if (localStorage.getItem("image_form " + element_name) == null) {
continue;
}
element = document.getElementsByName(element_name)[0]
if (element.type == 'checkbox') {
element.checked = localStorage.getItem("image_form " + element_name) === "true";
} else if (element.name == 'advanced_options') {
element.open = localStorage.getItem("image_form " + element_name) === "true";
} else {
element.value = localStorage.getItem("image_form " + element_name);
}
if (element.name == 'dropdown') {
handle_dropdown(element.value);
}
}
const image_form = document.getElementById("image_form")
image_form.addEventListener('submit', function(e) {
e.preventDefault();
for (const element of image_form.elements) {
if (element.type == 'checkbox') {
localStorage.setItem("image_form " + element.name, element.checked);
} else {
localStorage.setItem("image_form " + element.name, element.value);
}
}
element = document.getElementsByName("advanced_options")[0]
localStorage.setItem("image_form " + element.name, element.open);
image_form.submit();
})
</script>
{% endblock %}

View file

@ -3,46 +3,25 @@
{% block head %}
<title>LabelPrinter Label generator</title>
{% endblock %}
<!-- TODO: Make fields outside the form also update based on localStorage, instead of serverside, add dropshadows to white text, and make size and y-position boxes smaller, also, text alignment -->
{% block content %}
<form method="POST" action="/text-template">
<form id="template">
<label for="template">Template:</label>
<select name="template">
<option value="DNH">Do not hack</option>
<option value="Food">Food</option>
<option value="Remove by">Remove by</option>
<option value="Remove by">Remove by</option>
</select>
<input type="submit" value="Apply">
</form>
<form method="POST" action="/text-form">
<h2>Top text</h2>
Text:<br>
<textarea name="string1">{{text1["string"]}}</textarea><br>
Size: <input type="number" value="{{text1["size"]}}" name="size1" min="0" required
style="width: 40px"><br>
Y position: <input type="number" value="{{text1["pos"]}}" name="pos1" required
style="width: 40px"><br>
{% if text1["bold"]%}
Bold: <input type="checkbox" name="bold1" value="bold1" checked>
{% else %}
Bold: <input type="checkbox" name="bold1" value="bold1">
{% endif %}
<h2>Bottom text</h2>
Text:<br>
<textarea name="string2">{{text2["string"]}}</textarea><br>
Size: <input type="number" value="{{text2["size"]}}" name="size2" min="0" required
style="width: 40px"><br>
Y position: <input type="number" value="{{text2["pos"]}}" name="pos2" required
style="width: 40px"><br>
{% if text2["bold"]%}
Bold: <input type="checkbox" name="bold2" value="bold2" checked>
{% else %}
Bold: <input type="checkbox" name="bold2" value="bold2">
{% endif %}
<form method="POST" action="/text-form", id="text_form">
<div id="input", class="tiles"> </div>
<div id="plus minus buttons"> </div>
<h2>Font</h2>
<label for="font">Font:</label>
@ -75,6 +54,156 @@
</form>
{% endif %}
<script>
const line_form = `<div><h2>[title]</h2>
Text:<br>
<textarea name="[text]">(text)</textarea><br>
Size: <input type="number" value="(size)" name="[size]" min="0" required
style="width: 40px"><br>
Y position: <input type="number" value="(y_pos)" name="[y_pos]" required
style="width: 40px"><br>
Bold: <input type="checkbox" name="[bold]" value="bold" (bold)></div>`
const min_button = `<button type="button" onClick="remove_line()">-</button>`
const plus_button = `<button type="button" onClick="add_line()">+</button>`
function get_persistent(name, initial_value) {
if (localStorage.getItem(name) == null) {
localStorage.setItem(name, initial_value)
}
return localStorage.getItem(name)
}
function init_field(local_name, initial_value, i, html) {
html = html.replace("[" + local_name + "]", local_name + i);
html = html.replace("(" + local_name + ")", get_persistent(local_name + i, initial_value));
return html;
}
const templates = new Map ([
["DNH", [{
text: "Do Not Hack",
size: 130,
y_pos: 0,
bold: true
}, {
text: "bottom text",
size: 50,
y_pos: 130,
bold: false
}]],
["Food", [{
text: "Nickname",
size: 130,
y_pos: 0,
bold: true
}, {
text: new Date().toISOString().slice(0, 10),
size: 50,
y_pos: 130,
bold: false
}]],
["Remove by", [{
text: "Remove by",
size: 130,
y_pos: 0,
bold: true
}, {
text: new Date(Date.now() + 3 * 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),
size: 50,
y_pos: 130,
bold: false
}]],
]);
function generate_and_populate_fields() {
let html = ""
if (localStorage.getItem("text0") == null) {
template = templates.get("DNH");
populate_local_storage_from_template(template);
}
let lineCnt = get_persistent("lineCount", 2);
for (let i = 0; i < lineCnt; i++) {
let modified_line_form = line_form;
modified_line_form
modified_line_form = modified_line_form.replace("[title]", "Line " + i);
modified_line_form = init_field("text", "Text", i, modified_line_form);
modified_line_form = init_field("size", 130, i, modified_line_form);
modified_line_form = init_field("y_pos", 130 * i, i, modified_line_form);
modified_line_form = init_field("bold", false ? "checked" : "", i, modified_line_form);
html += modified_line_form;
}
let buttons = ""
if (lineCnt == 1) {
buttons = plus_button;
} else {
buttons += min_button + plus_button;
}
document.getElementById("input").innerHTML = html;
document.getElementById("plus minus buttons").innerHTML = buttons;
}
generate_and_populate_fields()
const text_form = document.getElementById("text_form")
text_form.addEventListener('submit', function(e) {
e.preventDefault();
for (const element of text_form.elements) {
if (element.type == 'checkbox') {
localStorage.setItem(element.name, element.checked ? "checked" : "");
} else {
localStorage.setItem(element.name, element.value);
}
}
const lineCount = document.createElement('input');
lineCount.type = 'hidden';
lineCount.name = 'lineCount';
lineCount.value = localStorage.getItem("lineCount");
text_form.appendChild(lineCount);
text_form.submit();
});
function add_line() {
localStorage.setItem("lineCount", Number(localStorage.getItem("lineCount")) + 1);
generate_and_populate_fields();
}
function remove_line() {
localStorage.setItem("lineCount", Number(localStorage.getItem("lineCount")) - 1);
generate_and_populate_fields();
}
function populate_local_storage_from_template(template) {
for (let i = 0; i < template.length; i++) {
localStorage.setItem("text" + i, template[i].text);
localStorage.setItem("size" + i, template[i].size);
localStorage.setItem("y_pos" + i, template[i].y_pos);
localStorage.setItem("bold" + i, template[i].bold ? "checked" : "");
}
localStorage.setItem("lineCount", template.length);
}
const template_selector = document.getElementById("template")
template_selector.addEventListener('submit', function(e) {
e.preventDefault();
template = templates.get(template_selector.elements.template.value);
populate_local_storage_from_template(template);
generate_and_populate_fields();
});
</script>
{% endblock %}