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.")
|