first!1!
11
Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM alpine
|
||||||
|
|
||||||
|
RUN apk --no-cache add python3 inkscape bash imagemagick
|
||||||
|
|
||||||
|
ADD . /root/zetikettes
|
||||||
|
|
||||||
|
RUN apk --no-cache add ttf-opensans && cp /root/zetikettes/Karumbi.ttf /usr/share/fonts/TTF/ && fc-cache -fv
|
||||||
|
|
||||||
|
# the script will look for templates in /data
|
||||||
|
WORKDIR /root/zetikettes
|
||||||
|
CMD /usr/bin/python3 web.py
|
BIN
Karumbi.ttf
Normal file
60
makeplanche.py
Executable file
@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from string import Template
|
||||||
|
import sys
|
||||||
|
|
||||||
|
DEFAULT_SHEET_TEMPLATE = Path(__file__).parent / 'planche.svg.in'
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Make a sheet with 12 stickers')
|
||||||
|
parser.add_argument('--template',
|
||||||
|
'-t',
|
||||||
|
default=DEFAULT_SHEET_TEMPLATE,
|
||||||
|
help='path to the sheet template')
|
||||||
|
parser.add_argument('--out',
|
||||||
|
'-o',
|
||||||
|
default=sys.stdout,
|
||||||
|
type=argparse.FileType('w'),
|
||||||
|
help='output path (default: stdout)')
|
||||||
|
parser.add_argument('sticker',
|
||||||
|
type=argparse.FileType('r'),
|
||||||
|
default=sys.stdin,
|
||||||
|
nargs='?',
|
||||||
|
help='path to the sticker SVG (default: stdin)')
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE):
|
||||||
|
with open(template) as tpl:
|
||||||
|
tpl_data = tpl.read()
|
||||||
|
|
||||||
|
lines = sticker.readlines()
|
||||||
|
if lines[0].startswith('<?xml'):
|
||||||
|
lines = lines[1:]
|
||||||
|
sticker_data = ''.join(lines)
|
||||||
|
|
||||||
|
subs = {
|
||||||
|
'left0': sticker_data,
|
||||||
|
'left1': sticker_data,
|
||||||
|
'left2': sticker_data,
|
||||||
|
'left3': sticker_data,
|
||||||
|
'left4': sticker_data,
|
||||||
|
'left5': sticker_data,
|
||||||
|
'right0': sticker_data,
|
||||||
|
'right1': sticker_data,
|
||||||
|
'right2': sticker_data,
|
||||||
|
'right3': sticker_data,
|
||||||
|
'right4': sticker_data,
|
||||||
|
'right5': sticker_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
out.write(Template(tpl_data).substitute(subs))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
makeplanche(**vars(parse_args()))
|
58
makesticker.py
Executable file
@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from string import Template
|
||||||
|
import sys
|
||||||
|
import typing
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(description='Fill in sticker details')
|
||||||
|
parser.add_argument('--out',
|
||||||
|
'-o',
|
||||||
|
default=sys.stdout,
|
||||||
|
type=argparse.FileType('w'),
|
||||||
|
help='output path (default: stdout)')
|
||||||
|
parser.add_argument('--dluo',
|
||||||
|
'-d',
|
||||||
|
required=True,
|
||||||
|
help='Date Limite d\'Utilisation Optimale')
|
||||||
|
parser.add_argument('--lot', '-l', required=True, help='Numéro de lot')
|
||||||
|
parser.add_argument('--quantite', '-q', required=False, help='Quantité (volume ou masse)')
|
||||||
|
parser.add_argument('--teneur', '-t', required=False, help='Teneur en sucre')
|
||||||
|
parser.add_argument('--fruit', '-f', required=False, help='Quantité de fruits')
|
||||||
|
parser.add_argument('--size', '-s', required=False, help='Masse de produit')
|
||||||
|
parser.add_argument('--landscape',
|
||||||
|
action='store_true',
|
||||||
|
help='input sticker is in landscape orientation')
|
||||||
|
parser.add_argument('sticker', help='path to the sticker template')
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def makesticker(sticker: str, out: typing.IO, subs: dict, landscape=False):
|
||||||
|
with open(sticker) as fin:
|
||||||
|
lines = fin.readlines()
|
||||||
|
if lines[0].startswith('<?xml'):
|
||||||
|
lines = lines[1:]
|
||||||
|
sticker_data = ''.join(lines)
|
||||||
|
|
||||||
|
if landscape:
|
||||||
|
# Rotate the sticker 90 degrees
|
||||||
|
sticker_data = "<g transform=\"rotate(-90,0,0) translate(-102,0)\">{}</g>".format(sticker_data)
|
||||||
|
|
||||||
|
out.write(Template(sticker_data).substitute(subs))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = vars(parse_args())
|
||||||
|
subs = {
|
||||||
|
'dluo': args.pop('dluo'),
|
||||||
|
'lot': args.pop('lot'),
|
||||||
|
'teneur': args.pop('teneur'),
|
||||||
|
'fruit': args.pop('fruit'),
|
||||||
|
'qty': args.pop('quantite'),
|
||||||
|
'size': args.pop('size'),
|
||||||
|
}
|
||||||
|
args['subs'] = subs
|
||||||
|
makesticker(**args)
|
21
mkjam.sh
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
DEFAULT_DPI=300
|
||||||
|
LARGE='370' # Large container (370g)
|
||||||
|
SMALL='220' # Small container (220g)
|
||||||
|
|
||||||
|
SIZE=$LARGE
|
||||||
|
DLUO='décembre 2023'
|
||||||
|
LOT='0722-2'
|
||||||
|
TENEUR='50%'
|
||||||
|
FRUIT='80g'
|
||||||
|
STICKER='Gelée - Cassis.svg'
|
||||||
|
|
||||||
|
PDF="`basename \"${STICKER}\" .svg` - ${LOT} (${SIZE}g).pdf"
|
||||||
|
|
||||||
|
./makesticker.py --landscape --dluo "$DLUO" --lot "$LOT" --teneur "$TENEUR" --fruit "$FRUIT" --size "$SIZE" -o out.svg "$STICKER" && \
|
||||||
|
./makeplanche.py -o pout.svg -t planche.svg.in out.svg && \
|
||||||
|
rm out.svg && \
|
||||||
|
inkscape --export-type="png" --export-dpi=$DEFAULT_DPI pout.svg && \
|
||||||
|
rm pout.svg && \
|
||||||
|
convert pout.png "$PDF"
|
24
planche.svg.in
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 210 297"
|
||||||
|
height="297mm"
|
||||||
|
width="210mm">
|
||||||
|
<g transform="translate(3, 4.5)">
|
||||||
|
<g transform="translate(-42,144) rotate(-90,144,0)">
|
||||||
|
<g transform="translate( 0,0)">$right0</g>
|
||||||
|
<g transform="translate( 48,0)">$right1</g>
|
||||||
|
<g transform="translate( 96,0)">$right2</g>
|
||||||
|
<g transform="translate(144,0)">$right3</g>
|
||||||
|
<g transform="translate(192,0)">$right4</g>
|
||||||
|
<g transform="translate(240,0)">$right5</g>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(-42,144) rotate(90,144,0)">
|
||||||
|
<g transform="translate( 0,0)">$left0</g>
|
||||||
|
<g transform="translate( 48,0)">$left1</g>
|
||||||
|
<g transform="translate( 96,0)">$left2</g>
|
||||||
|
<g transform="translate(144,0)">$left3</g>
|
||||||
|
<g transform="translate(192,0)">$left4</g>
|
||||||
|
<g transform="translate(240,0)">$left5</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 896 B |
44
static/index.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>zetikettes 0.1</title>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- Compiled and minified CSS -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
|
||||||
|
|
||||||
|
<!-- Compiled and minified JavaScript -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||||
|
|
||||||
|
<script src="zetikettes.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="container center orange-text">
|
||||||
|
<h1>Zétikwett's</h1>
|
||||||
|
</header>
|
||||||
|
<main class="container row">
|
||||||
|
<div class="col m6 offset-m3 s12">
|
||||||
|
<ul class="collapsible" id="appbody"></ul>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="page-footer">
|
||||||
|
<div class="container row">
|
||||||
|
<span class="col right">displayed with recycled electrons.</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
128
static/zetikettes.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
const backend_api = '/zetikettes/srv/';
|
||||||
|
|
||||||
|
const zetikettes = [
|
||||||
|
{
|
||||||
|
'title': 'Aromate thym',
|
||||||
|
'sticker': 'Aromate - Thym.svg',
|
||||||
|
'subs': {
|
||||||
|
'dluo': 'décembre 2023',
|
||||||
|
'lot': '0722-2',
|
||||||
|
'qty': '40',
|
||||||
|
},
|
||||||
|
'landscape': false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Chocolat lavande',
|
||||||
|
'sticker': 'Chocolat - Lavande.svg',
|
||||||
|
'subs': {
|
||||||
|
'dluo': 'décembre 2023',
|
||||||
|
'lot': '0722-2',
|
||||||
|
'qty': '100',
|
||||||
|
},
|
||||||
|
'landscape': false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Gelée de cassis',
|
||||||
|
'sticker': 'Gelée - Cassis.svg',
|
||||||
|
'subs': {
|
||||||
|
'dluo': 'décembre 2023',
|
||||||
|
'fruit': '80g',
|
||||||
|
'teneur': '50%',
|
||||||
|
'lot': '0722-2',
|
||||||
|
'qty': '370',
|
||||||
|
},
|
||||||
|
'landscape': true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Pesto ail des ours',
|
||||||
|
'sticker': 'Pesto - Ail des Ours - 100% Olive.svg',
|
||||||
|
'subs': {
|
||||||
|
'dluo': 'décembre 2023',
|
||||||
|
'lot': '0722-2',
|
||||||
|
'qty': '150',
|
||||||
|
},
|
||||||
|
'landscape': true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Sel salade sans basilic',
|
||||||
|
'sticker': 'Sel - Salade - Sans Basilic.svg',
|
||||||
|
'subs': {
|
||||||
|
'dluo': 'décembre 2023',
|
||||||
|
'lot': '0722-2',
|
||||||
|
'qty': '40',
|
||||||
|
},
|
||||||
|
'landscape': true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Sirop de cassis',
|
||||||
|
'sticker': 'Sirop - Cassis.svg',
|
||||||
|
'subs': {
|
||||||
|
'dluo': 'décembre 2023',
|
||||||
|
'lot': '0722-2',
|
||||||
|
'qty': '75',
|
||||||
|
},
|
||||||
|
'landscape': false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Tisane digestion',
|
||||||
|
'sticker': 'Tisane - Digestion.svg',
|
||||||
|
'subs': {
|
||||||
|
'dluo': 'décembre 2023',
|
||||||
|
'lot': '0722-2',
|
||||||
|
'qty': '25',
|
||||||
|
},
|
||||||
|
'landscape': false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
$(document).ready(() => {
|
||||||
|
const appbody = $("#appbody");
|
||||||
|
for (let zett of zetikettes) {
|
||||||
|
const block = $('<div class="section">');
|
||||||
|
for (let sub in zett.subs) {
|
||||||
|
block.append($(`<div class="input-field"><label class="active">${sub}</label><input type="text" name="${sub}" value="${zett.subs[sub]}">`));
|
||||||
|
}
|
||||||
|
const loader = $('<div class="progress"><div class="indeterminate"></div></div>')
|
||||||
|
.hide();
|
||||||
|
|
||||||
|
const action = $('<div class="section">')
|
||||||
|
.append($('<a class="btn">generate<a>')
|
||||||
|
.click(() => {
|
||||||
|
const subs = block.find(':text')
|
||||||
|
.toArray()
|
||||||
|
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
|
||||||
|
const req = {
|
||||||
|
sticker: zett.sticker,
|
||||||
|
subs,
|
||||||
|
landscape: zett.landscape,
|
||||||
|
};
|
||||||
|
|
||||||
|
loader.show();
|
||||||
|
$('.btn').addClass("disabled");
|
||||||
|
|
||||||
|
$.post(backend_api, JSON.stringify(req))
|
||||||
|
.then(data => {
|
||||||
|
console.log(data);
|
||||||
|
const pdfbtn = $(`<a class="btn" href="${backend_api}/data/${data.file}" target="_blank">open pdf</a>`);
|
||||||
|
action.append(pdfbtn);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
loader.hide();
|
||||||
|
$('.btn').removeClass('disabled');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.append(loader));
|
||||||
|
|
||||||
|
appbody
|
||||||
|
.append($('<li>')
|
||||||
|
.append($(`<div class="collapsible-header"><h6 class="blue-text">${zett.title}</h6></div>`))
|
||||||
|
.append($('<div class="collapsible-body">')
|
||||||
|
.append(block)
|
||||||
|
.append(action)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.collapsible').collapsible();
|
||||||
|
});
|
1030
templates/Aromate - Thym.svg
Executable file
After Width: | Height: | Size: 2.0 MiB |
877
templates/Chocolat - Lavande.svg
Executable file
After Width: | Height: | Size: 2.0 MiB |
983
templates/Gelée - Cassis.svg
Executable file
After Width: | Height: | Size: 5.3 MiB |
973
templates/Pesto - Ail des Ours - 100% Olive.svg
Executable file
After Width: | Height: | Size: 5.3 MiB |
809
templates/Sel - Salade - Sans Basilic.svg
Executable file
After Width: | Height: | Size: 3.3 MiB |
1058
templates/Sirop - Cassis.svg
Executable file
After Width: | Height: | Size: 2.0 MiB |
1001
templates/Tisane - Digestion.svg
Executable file
After Width: | Height: | Size: 2.0 MiB |
119
web.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from makesticker import makesticker
|
||||||
|
from makeplanche import makeplanche
|
||||||
|
|
||||||
|
PORT = 8000
|
||||||
|
OUT_DIR = '/data'
|
||||||
|
TEMPLATE_DIR = '/data'
|
||||||
|
DEFAULT_DPI = 300
|
||||||
|
|
||||||
|
|
||||||
|
def inkscapize(svg_in, pdf_out):
|
||||||
|
png_out = tempfile.mktemp(suffix='.png')
|
||||||
|
try:
|
||||||
|
cmd = ['inkscape', '--export-type=png', f'--export-filename={png_out}',
|
||||||
|
f'--export-dpi={DEFAULT_DPI}', svg_in]
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
|
||||||
|
cmd = ['convert', png_out, pdf_out]
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(png_out):
|
||||||
|
os.unlink(png_out)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_request(request):
|
||||||
|
request = json.loads(request)
|
||||||
|
|
||||||
|
# fill in sticker details
|
||||||
|
sticker_out = tempfile.mktemp(suffix='.svg')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(sticker_out, 'w') as stickout:
|
||||||
|
landscape = request.get('landscape', False)
|
||||||
|
print(f'landscape: {landscape}')
|
||||||
|
makesticker(os.path.join(TEMPLATE_DIR, request['sticker']), stickout,
|
||||||
|
request['subs'], landscape=landscape)
|
||||||
|
|
||||||
|
# make sticker sheet
|
||||||
|
planche_out = tempfile.mktemp(suffix='.svg')
|
||||||
|
with open(sticker_out, 'r') as stickin:
|
||||||
|
with open(planche_out, 'w') as planchout:
|
||||||
|
makeplanche(stickin, planchout)
|
||||||
|
|
||||||
|
# process to printable pdf
|
||||||
|
pdf_out = tempfile.mktemp(dir=OUT_DIR, suffix='.pdf')
|
||||||
|
inkscapize(planche_out, pdf_out)
|
||||||
|
|
||||||
|
response = {'status': 'ok', 'file': os.path.basename(pdf_out),
|
||||||
|
'message': 'this is the way'}
|
||||||
|
return json.dumps(response)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.exists(sticker_out):
|
||||||
|
os.unlink(sticker_out)
|
||||||
|
if 'planche_out' in locals() and os.path.exists(planche_out):
|
||||||
|
os.unlink(planche_out)
|
||||||
|
|
||||||
|
|
||||||
|
class MyServer(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if re.match(r'/data/\w+\.pdf', self.path) is not None:
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "application/pdf")
|
||||||
|
self.end_headers()
|
||||||
|
with open(self.path, 'rb') as f:
|
||||||
|
self.wfile.write(f.read())
|
||||||
|
return
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "text/html; charset=UTF-8")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b"<html><head><title>Zetikettes</title></head>")
|
||||||
|
self.wfile.write(b"<body>")
|
||||||
|
self.wfile.write("<p>This is not the way.</p>".encode())
|
||||||
|
self.wfile.write(b"</body></html>")
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
try:
|
||||||
|
length = int(self.headers['content-length'])
|
||||||
|
req = self.rfile.read(length).decode()
|
||||||
|
resp = parse_request(req).encode()
|
||||||
|
except:
|
||||||
|
self.send_response(500)
|
||||||
|
self.send_header("Content-type", "text/plain")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(traceback.format_exc().encode())
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "application/json")
|
||||||
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(resp)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
webServer = HTTPServer(('', PORT), MyServer)
|
||||||
|
print(f"Server started on port {PORT}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
webServer.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
webServer.server_close()
|
||||||
|
print("Server stopped.")
|