Fix planche generation issues

This commit is contained in:
Paul Mathieu 2025-08-05 20:58:05 +02:00
parent 6a05e46946
commit d706ae1645
25 changed files with 85 additions and 68 deletions

View File

@ -1,6 +1,6 @@
FROM alpine FROM alpine:3.18
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript ttf-opensans RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript font-droid
RUN apk --no-cache add py3-pip && pip3 install --break-system-packages django tzdata gunicorn RUN apk --no-cache add py3-pip && pip3 install --break-system-packages django tzdata gunicorn
ADD backend /zetikettes ADD backend /zetikettes

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -23,7 +23,7 @@
"model": "tikette.tisub", "model": "tikette.tisub",
"pk": 4, "pk": 4,
"fields": { "fields": {
"name": "qty", "name": "q",
"descritpion": "Poids net (g)", "descritpion": "Poids net (g)",
"default": "370", "default": "370",
"type": "ST" "type": "ST"
@ -43,7 +43,7 @@
"model": "tikette.tisub", "model": "tikette.tisub",
"pk": 6, "pk": 6,
"fields": { "fields": {
"name": "fruit", "name": "f",
"descritpion": "Quantité de fruits pour 100g (g)", "descritpion": "Quantité de fruits pour 100g (g)",
"default": "60", "default": "60",
"type": "ST" "type": "ST"

View File

@ -50,7 +50,7 @@ class Tikette(models.Model):
description = models.TextField() description = models.TextField()
color = models.CharField(max_length=6) color = models.CharField(max_length=6)
ab = models.CharField(max_length=7, choices=AbVisibility) ab = models.CharField(max_length=7, choices=AbVisibility)
# designation_fontsize is hardcoded to 42.6667 # designation_fontsize is hardcoded to 42.6667 in planche/generate.py
def __str__(self): def __str__(self):
return self.title return self.title

View File

@ -1,24 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 896 B

View File

@ -7,6 +7,8 @@ from .makeplanche import makeplanche
OUT_DIR = tempfile.gettempdir() OUT_DIR = tempfile.gettempdir()
DEFAULT_DPI = 300 DEFAULT_DPI = 300
DESIGNATION_FONTSIZE = 42.6667
def inkscapize(svg_in, pdf_out): def inkscapize(svg_in, pdf_out):
png_out = tempfile.mktemp(suffix='.png') png_out = tempfile.mktemp(suffix='.png')
@ -22,31 +24,31 @@ def inkscapize(svg_in, pdf_out):
os.unlink(png_out) os.unlink(png_out)
def generate(request, out_dir): def generate(template, subs, out_dir, landscape=False):
""" Generate a sticker sheet. """ Generate a sticker sheet.
request: dict-like with the following fields: template: file name for the sticker template
sticker: mandatory. filename for the sticker subs: dict-like with key-value template subtitution fields
subs: mandatory. dict-like with key-value template subtitution fields
landscape: optional. defaults to False
out_dir: you get it out_dir: you get it
landscape: optional. defaults to False
""" """
# default designation font size
subs['designation_fontsize'] = subs.get('designation_fontsize',
DESIGNATION_FONTSIZE)
# fill in sticker details # fill in sticker details
sticker_out = tempfile.mktemp(suffix='.svg') sticker_out = tempfile.mktemp(suffix='.svg')
try: try:
with open(sticker_out, 'w') as stickout: with open(sticker_out, 'w') as stickout:
landscape = request.get('landscape', False) makesticker(os.path.join(out_dir, template), stickout, subs)
makesticker(os.path.join(out_dir, request['sticker']), stickout,
request['subs'], landscape=landscape)
# make sticker sheet # make sticker sheet
planche_out = tempfile.mktemp(suffix='.svg') planche_out = tempfile.mktemp(suffix='.svg')
with open(sticker_out, 'r') as stickin: with open(sticker_out, 'r') as stickin:
with open(planche_out, 'w') as planchout: with open(planche_out, 'w') as planchout:
makeplanche(stickin, planchout) makeplanche(stickin, planchout, landscape=landscape)
# process to printable pdf # process to printable pdf
pdf_out = tempfile.mktemp(dir=out_dir, suffix='.pdf') pdf_out = tempfile.mktemp(dir=out_dir, suffix='.pdf')

View File

@ -20,6 +20,9 @@ def parse_args():
default=sys.stdout, default=sys.stdout,
type=argparse.FileType('w'), type=argparse.FileType('w'),
help='output path (default: stdout)') help='output path (default: stdout)')
parser.add_argument('--landscape',
action='store_true',
help='input sticker is in landscape orientation')
parser.add_argument('sticker', parser.add_argument('sticker',
type=argparse.FileType('r'), type=argparse.FileType('r'),
default=sys.stdin, default=sys.stdin,
@ -29,7 +32,7 @@ def parse_args():
return parser.parse_args() return parser.parse_args()
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE): def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE ,landscape=False):
with open(template) as tpl: with open(template) as tpl:
tpl_data = tpl.read() tpl_data = tpl.read()
@ -38,19 +41,11 @@ def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE):
lines = lines[1:] lines = lines[1:]
sticker_data = ''.join(lines) sticker_data = ''.join(lines)
rotate = "translate(102, 0) rotate(90)" if not landscape else ""
subs = { subs = {
'left0': sticker_data, 'sticker': sticker_data,
'left1': sticker_data, 'rotate': rotate,
'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)) out.write(Template(tpl_data).substitute(subs))

View File

@ -22,25 +22,18 @@ def parse_args():
parser.add_argument('--teneur', '-t', required=False, help='Teneur en sucre') 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('--fruit', '-f', required=False, help='Quantité de fruits')
parser.add_argument('--size', '-s', required=False, help='Masse de produit') 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') parser.add_argument('sticker', help='path to the sticker template')
return parser.parse_args() return parser.parse_args()
def makesticker(sticker: str, out: typing.IO, subs: dict, landscape=False): def makesticker(sticker: str, out: typing.IO, subs: dict):
with open(sticker) as fin: with open(sticker) as fin:
lines = fin.readlines() lines = fin.readlines()
if lines[0].startswith('<?xml'): if lines[0].startswith('<?xml'):
lines = lines[1:] lines = lines[1:]
sticker_data = ''.join(lines) 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)) out.write(Template(sticker_data).substitute(subs))
@ -50,8 +43,8 @@ if __name__ == "__main__":
'dluo': args.pop('dluo'), 'dluo': args.pop('dluo'),
'lot': args.pop('lot'), 'lot': args.pop('lot'),
'teneur': args.pop('teneur'), 'teneur': args.pop('teneur'),
'fruit': args.pop('fruit'), 'f': args.pop('fruit'),
'qty': args.pop('quantite'), 'q': args.pop('quantite'),
'size': args.pop('size'), 'size': args.pop('size'),
} }
args['subs'] = subs args['subs'] = subs

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(0, 0) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 48) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 96) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 144) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 192) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 240) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="rotate(180, 102, 144)">
<g transform="translate(0, 0) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 48) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 96) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 144) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 192) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 240) ${rotate} scale(0.26458)">${sticker}</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -5,18 +5,34 @@ from django.http import JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from . import generate as stickersheet from .planche import generate as stickersheet
from .models import Tikette, Tikategory from .models import Tikette, Tikategory
CORS={'access-control-allow-origin': '*'} CORS={'access-control-allow-origin': '*'}
def quirk_bold_allergens(ingredients):
out = []
for ing in (x.strip() for x in ingredients.split(',')):
if ing.startswith('*'):
out.append(f'<tspan style="font-weight:bold">{ing[1:]}</tspan>')
else:
out.append(ing)
return ", ".join(out)
def get_list(request): def get_list(request):
tikettes = [{ tikettes = [{
'id': x.id, 'id': x.id,
'title': x.title, 'title': x.title,
'category': x.category.name, 'category': x.category.name,
'prototempalte': x.category.prototempalte.name,
'landscape': x.category.landscape, 'landscape': x.category.landscape,
'designation': x.designation,
'ingredients': quirk_bold_allergens(x.ingredients),
'description': x.description,
'ab': x.ab,
'color': x.color,
'subs': {x.name: x.default for x in x.category.subs.all()}, 'subs': {x.name: x.default for x in x.category.subs.all()},
} for x in Tikette.objects.all()] } for x in Tikette.objects.all()]
return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS) return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS)
@ -36,7 +52,14 @@ def generate(request):
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'}) return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
payload = json.loads(request.body) payload = json.loads(request.body)
pdfpath = stickersheet.generate(payload, out_dir=settings.TIKETTE_OUT_DIR)
subs = dict(payload['subs'])
for key in ('designation', 'ingredients', 'description', 'color', 'AB'):
subs[key] = payload[key]
pdfpath = stickersheet.generate(template=payload['template'], subs=subs,
out_dir=settings.TIKETTE_OUT_DIR,
landscape=payload['landscape'])
return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS) return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS)

View File

@ -1,10 +1,9 @@
const params = { const params = {
'dluo': 'DLUO', 'dluo': 'DLUO',
'lot': 'Nº de lot', 'lot': 'Nº de lot',
'qty': 'Poids net (g)', 'q': 'Poids net (g)',
'vol': 'Volume net (cL)',
'teneur': 'Teneur en fruits (%)', 'teneur': 'Teneur en fruits (%)',
'fruit': 'Quantité de fruits pour 100g (g)', 'f': 'Quantité de fruits pour 100g (g)',
} }
var tikats; var tikats;
@ -26,7 +25,12 @@ function addProduct(tikette) {
.toArray() .toArray()
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {}); .reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
const req = { const req = {
sticker: zett.sticker, template: zett.prototempalte,
designation: zett.designation,
description: zett.description,
ingredients: zett.ingredients,
color: zett.color,
AB: zett.ab, // mind the capitalization here
subs, subs,
landscape: zett.landscape, landscape: zett.landscape,
}; };