This commit is contained in:
Paul Mathieu 2022-07-12 12:04:38 +02:00
commit d73b492cf2
16 changed files with 7196 additions and 0 deletions

11
Dockerfile Normal file
View 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

Binary file not shown.

60
makeplanche.py Executable file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.0 MiB

877
templates/Chocolat - Lavande.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.0 MiB

983
templates/Gelée - Cassis.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 3.3 MiB

1058
templates/Sirop - Cassis.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.0 MiB

1001
templates/Tisane - Digestion.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.0 MiB

119
web.py Normal file
View 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.")