2016-11-20 15:57:39 +02:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
|
|
"""
|
2016-11-20 16:10:07 +02:00
|
|
|
This is a web service to print labels on Brother QL label printers.
|
2016-11-20 15:57:39 +02:00
|
|
|
"""
|
|
|
|
|
2016-11-21 12:39:09 +02:00
|
|
|
import sys, logging, socket, os, functools, textwrap
|
|
|
|
from io import BytesIO
|
2016-11-20 15:57:39 +02:00
|
|
|
|
2017-01-04 16:09:00 +02:00
|
|
|
from bottle import run, route, get, post, response, request, jinja2_view as view, static_file, redirect
|
2016-11-20 15:57:39 +02:00
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
|
2017-01-03 21:21:33 +02:00
|
|
|
from brother_ql.devicedependent import models, label_type_specs, label_sizes
|
|
|
|
from brother_ql.devicedependent import ENDLESS_LABEL, DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL
|
2016-11-20 16:28:00 +02:00
|
|
|
from brother_ql import BrotherQLRaster, create_label
|
2016-11-20 15:57:39 +02:00
|
|
|
from brother_ql.backends import backend_factory, guess_backend
|
|
|
|
|
2016-11-20 16:40:20 +02:00
|
|
|
from font_helpers import get_fonts
|
2016-11-20 15:57:39 +02:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
DEBUG = False
|
2016-11-20 16:40:20 +02:00
|
|
|
MODEL = None
|
|
|
|
BACKEND_CLASS = None
|
|
|
|
BACKEND_STRING_DESCR = None
|
2016-11-20 15:57:39 +02:00
|
|
|
FONTS = None
|
|
|
|
DEFAULT_FONT = None
|
|
|
|
DEFAULT_FONTS = [
|
|
|
|
{'family': 'Minion Pro', 'style': 'Semibold'},
|
|
|
|
{'family': 'Linux Libertine', 'style': 'Regular'},
|
|
|
|
{'family': 'DejaVu Serif', 'style': 'Book'},
|
|
|
|
]
|
|
|
|
|
2017-01-03 21:21:33 +02:00
|
|
|
LABEL_SIZES = [ (name, label_type_specs[name]['name']) for name in label_sizes]
|
2016-12-17 22:11:58 +02:00
|
|
|
|
2016-11-20 15:57:39 +02:00
|
|
|
@route('/')
|
|
|
|
def index():
|
2016-11-25 22:21:42 +02:00
|
|
|
redirect('/labeldesigner')
|
|
|
|
|
|
|
|
@route('/static/<filename:path>')
|
|
|
|
def serve_static(filename):
|
|
|
|
return static_file(filename, root='./static')
|
|
|
|
|
|
|
|
@route('/labeldesigner')
|
|
|
|
@view('labeldesigner.jinja2')
|
|
|
|
def labeldesigner():
|
|
|
|
fonts = sorted(list(FONTS.keys()))
|
2016-12-17 22:11:58 +02:00
|
|
|
label_sizes = LABEL_SIZES
|
2016-12-19 01:15:27 +02:00
|
|
|
title = 'Label Designer'
|
|
|
|
page_headline = 'Brother QL Label Designer'
|
|
|
|
return {'title': title, 'page_headline': page_headline, 'message': '', 'fonts': fonts, 'label_sizes': label_sizes}
|
2016-11-20 15:57:39 +02:00
|
|
|
|
2016-11-21 12:39:09 +02:00
|
|
|
def get_label_context(request):
|
|
|
|
""" might raise LookupError() """
|
|
|
|
|
2017-01-04 15:13:17 +02:00
|
|
|
d = request.params.decode() # UTF-8 decoded form data
|
|
|
|
|
2016-11-21 12:39:09 +02:00
|
|
|
context = {
|
2017-01-04 15:13:17 +02:00
|
|
|
'text': d.get('text', None),
|
|
|
|
'font_size': int(d.get('font_size', 100)),
|
|
|
|
'font_family': d.get('font_family'),
|
|
|
|
'font_style': d.get('font_style'),
|
|
|
|
'label_size': d.get('label_size', "62"),
|
|
|
|
'margin': int(d.get('margin', 10)),
|
|
|
|
'threshold': int(d.get('threshold', 70)),
|
|
|
|
'align': d.get('align', 'center'),
|
2017-01-04 15:14:56 +02:00
|
|
|
'margin_top': float(d.get('margin_top', 0.24)),
|
|
|
|
'margin_bottom': float(d.get('margin_bottom', 0.45)),
|
2016-11-21 12:39:09 +02:00
|
|
|
}
|
2017-01-04 15:14:56 +02:00
|
|
|
context['margin_top'] = int(context['font_size']*context['margin_top'])
|
|
|
|
context['margin_bottom'] = int(context['font_size']*context['margin_bottom'])
|
2016-11-21 12:39:09 +02:00
|
|
|
|
|
|
|
def get_font_path(font_family, font_style):
|
|
|
|
try:
|
|
|
|
if font_family is None:
|
|
|
|
font_family = DEFAULT_FONT['family']
|
|
|
|
font_style = DEFAULT_FONT['style']
|
|
|
|
if font_style is None:
|
|
|
|
font_style = 'Regular'
|
|
|
|
font_path = FONTS[font_family][font_style]
|
|
|
|
except KeyError:
|
|
|
|
raise LookupError("Couln't find the font & style")
|
|
|
|
return font_path
|
|
|
|
|
|
|
|
context['font_path'] = get_font_path(context['font_family'], context['font_style'])
|
|
|
|
|
|
|
|
def get_label_dimensions(label_size):
|
|
|
|
try:
|
|
|
|
ls = label_type_specs[context['label_size']]
|
|
|
|
except KeyError:
|
|
|
|
raise LookupError("Unknown label_size")
|
|
|
|
return ls['dots_printable']
|
|
|
|
|
|
|
|
width, height = get_label_dimensions(context['label_size'])
|
|
|
|
if height == 0:
|
|
|
|
height = context['font_size'] + 2 * context['margin']
|
|
|
|
if height > width: width, height = height, width
|
|
|
|
|
|
|
|
context['width'], context['height'] = width, height
|
|
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
|
def create_label_im(text, **kwargs):
|
2017-01-03 21:21:33 +02:00
|
|
|
label_type = label_type_specs[kwargs['label_size']]['kind']
|
2016-11-21 12:39:09 +02:00
|
|
|
im_font = ImageFont.truetype(kwargs['font_path'], kwargs['font_size'])
|
2016-12-19 01:33:36 +02:00
|
|
|
im = Image.new('L', (20, 20), 'white')
|
|
|
|
draw = ImageDraw.Draw(im)
|
|
|
|
linesize = im_font.getsize(text)
|
|
|
|
textsize = draw.multiline_textsize(text, font=im_font)
|
2017-01-03 21:21:33 +02:00
|
|
|
if label_type in (DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL):
|
2016-12-17 22:11:58 +02:00
|
|
|
height = kwargs['height']
|
|
|
|
else:
|
2016-12-19 01:33:36 +02:00
|
|
|
height = textsize[1] + kwargs['margin_top'] + kwargs['margin_bottom']
|
2016-11-25 22:21:42 +02:00
|
|
|
im = Image.new('L', (kwargs['width'], height), 'white')
|
|
|
|
draw = ImageDraw.Draw(im)
|
2017-01-03 21:21:33 +02:00
|
|
|
if label_type in (DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL):
|
2016-12-19 01:33:36 +02:00
|
|
|
vertical_offset = (height - textsize[1])//2
|
|
|
|
vertical_offset += (kwargs['margin_top'] - kwargs['margin_bottom'])//2
|
2016-12-17 23:58:18 +02:00
|
|
|
else:
|
2016-12-19 01:33:36 +02:00
|
|
|
vertical_offset = kwargs['margin_top']
|
2016-11-21 12:39:09 +02:00
|
|
|
horizontal_offset = max((kwargs['width'] - textsize[0])//2, 0)
|
|
|
|
offset = horizontal_offset, vertical_offset
|
2016-12-19 01:33:36 +02:00
|
|
|
draw.multiline_text(offset, text, (0), font=im_font, align=kwargs['align'])
|
2016-11-21 12:39:09 +02:00
|
|
|
return im
|
|
|
|
|
2017-01-04 15:13:17 +02:00
|
|
|
@get('/api/preview/text')
|
|
|
|
@post('/api/preview/text')
|
|
|
|
def get_preview_image():
|
2016-11-21 12:39:09 +02:00
|
|
|
context = get_label_context(request)
|
2017-01-04 15:13:17 +02:00
|
|
|
im = create_label_im(**context)
|
|
|
|
return_format = request.query.get('return_format', 'png')
|
|
|
|
if return_format == 'base64':
|
|
|
|
import base64
|
|
|
|
response.set_header('Content-type', 'text/plain')
|
|
|
|
return base64.b64encode(image_to_png_bytes(im))
|
|
|
|
else:
|
|
|
|
response.set_header('Content-type', 'image/png')
|
|
|
|
return image_to_png_bytes(im)
|
|
|
|
|
|
|
|
def image_to_png_bytes(im):
|
2016-11-21 12:39:09 +02:00
|
|
|
image_buffer = BytesIO()
|
|
|
|
im.save(image_buffer, format="PNG")
|
|
|
|
image_buffer.seek(0)
|
|
|
|
return image_buffer.read()
|
|
|
|
|
2017-01-04 15:13:17 +02:00
|
|
|
@post('/api/print/text')
|
|
|
|
@get('/api/print/text')
|
|
|
|
def print_text():
|
2016-11-20 15:57:39 +02:00
|
|
|
"""
|
|
|
|
API to print a label
|
|
|
|
|
|
|
|
returns: JSON
|
|
|
|
|
|
|
|
Ideas for additional URL parameters:
|
|
|
|
- alignment
|
|
|
|
"""
|
|
|
|
|
|
|
|
return_dict = {'success': False}
|
|
|
|
|
|
|
|
try:
|
2016-11-21 12:39:09 +02:00
|
|
|
context = get_label_context(request)
|
|
|
|
except LookupError as e:
|
|
|
|
return_dict['error'] = e.msg
|
2016-11-20 15:57:39 +02:00
|
|
|
return return_dict
|
|
|
|
|
2017-01-04 15:13:17 +02:00
|
|
|
if context['text'] is None:
|
|
|
|
return_dict['error'] = 'Please provide the text for the label'
|
|
|
|
return return_dict
|
|
|
|
|
|
|
|
im = create_label_im(**context)
|
2016-11-20 15:57:39 +02:00
|
|
|
if DEBUG: im.save('sample-out.png')
|
|
|
|
|
|
|
|
qlr = BrotherQLRaster(MODEL)
|
2016-11-21 12:39:09 +02:00
|
|
|
create_label(qlr, im, context['label_size'], threshold=context['threshold'], cut=True)
|
2016-11-20 15:57:39 +02:00
|
|
|
|
|
|
|
if not DEBUG:
|
|
|
|
try:
|
|
|
|
be = BACKEND_CLASS(BACKEND_STRING_DESCR)
|
|
|
|
be.write(qlr.data)
|
|
|
|
be.dispose()
|
|
|
|
del be
|
|
|
|
except Exception as e:
|
|
|
|
return_dict['message'] = str(e)
|
|
|
|
logger.warning('Exception happened: %s', e)
|
|
|
|
return return_dict
|
|
|
|
|
|
|
|
return_dict['success'] = True
|
|
|
|
if DEBUG: return_dict['data'] = str(qlr.data)
|
|
|
|
return return_dict
|
|
|
|
|
|
|
|
def main():
|
|
|
|
global DEBUG, FONTS, DEFAULT_FONT, MODEL, BACKEND_CLASS, BACKEND_STRING_DESCR
|
|
|
|
import argparse
|
2016-11-20 16:10:07 +02:00
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
2016-11-20 15:57:39 +02:00
|
|
|
parser.add_argument('--port', default=8013)
|
|
|
|
parser.add_argument('--loglevel', type=lambda x: getattr(logging, x.upper()), default='WARNING')
|
|
|
|
parser.add_argument('--font-folder', help='folder for additional .ttf/.otf fonts')
|
|
|
|
parser.add_argument('--model', default='QL-500', choices=models, help='The model of your printer (default: QL-500)')
|
|
|
|
parser.add_argument('printer', help='String descriptor for the printer to use (like tcp://192.168.0.23:9100 or file:///dev/usb/lp0)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
DEBUG = args.loglevel == logging.DEBUG
|
|
|
|
logging.basicConfig(level=args.loglevel)
|
|
|
|
|
|
|
|
try:
|
|
|
|
selected_backend = guess_backend(args.printer)
|
|
|
|
except:
|
|
|
|
parser.error("Couln't guess the backend to use from the printer string descriptor")
|
|
|
|
BACKEND_CLASS = backend_factory(selected_backend)['backend_class']
|
|
|
|
BACKEND_STRING_DESCR = args.printer
|
|
|
|
|
|
|
|
MODEL = args.model
|
|
|
|
|
|
|
|
FONTS = get_fonts()
|
|
|
|
if args.font_folder:
|
|
|
|
FONTS.update(get_fonts(args.font_folder))
|
|
|
|
|
|
|
|
for font in DEFAULT_FONTS:
|
|
|
|
try:
|
|
|
|
FONTS[font['family']][font['style']]
|
|
|
|
DEFAULT_FONT = font
|
|
|
|
logger.debug("Selected the following default font: {}".format(font))
|
|
|
|
break
|
|
|
|
except: pass
|
|
|
|
if DEFAULT_FONT is None:
|
|
|
|
sys.stderr.write('Could not find any of the default fonts')
|
|
|
|
sys.exit()
|
|
|
|
|
2016-11-20 16:40:20 +02:00
|
|
|
run(host='', port=args.port, debug=DEBUG)
|
2016-11-20 15:57:39 +02:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|