2016-11-20 15:57:39 +02:00
#!/usr/bin/env python
2018-01-19 15:59:08 +02:00
# -*- coding: utf-8 -*-
2016-11-20 15:57:39 +02:00
"""
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
"""
2018-03-05 04:35:31 +02:00
import sys , logging , random , json , argparse
2016-11-21 12:39:09 +02:00
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
2023-05-13 16:48:43 +03:00
from brother_ql . devicedependent import ENDLESS_LABEL , DIE_CUT_LABEL , ROUND_DIE_CUT_LABEL , PTOUCH_ENDLESS_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__ )
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
2018-03-05 04:35:31 +02:00
try :
2019-03-07 13:47:36 +02:00
with open ( ' config.json ' , encoding = ' utf-8 ' ) as fh :
2018-03-05 04:35:31 +02:00
CONFIG = json . load ( fh )
except FileNotFoundError as e :
2019-03-07 13:47:36 +02:00
with open ( ' config.example.json ' , encoding = ' utf-8 ' ) as fh :
2018-03-05 04:35:31 +02:00
CONFIG = json . load ( fh )
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 ( ) :
2018-01-03 22:40:22 +02:00
font_family_names = sorted ( list ( FONTS . keys ( ) ) )
2018-03-05 04:35:31 +02:00
return { ' font_family_names ' : font_family_names ,
' fonts ' : FONTS ,
' label_sizes ' : LABEL_SIZES ,
' website ' : CONFIG [ ' WEBSITE ' ] ,
' label ' : CONFIG [ ' LABEL ' ] }
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
2018-01-03 22:40:22 +02:00
font_family = d . get ( ' font_family ' ) . rpartition ( ' ( ' ) [ 0 ] . strip ( )
font_style = d . get ( ' font_family ' ) . rpartition ( ' ( ' ) [ 2 ] . rstrip ( ' ) ' )
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 ) ) ,
2018-01-03 22:40:22 +02:00
' font_family ' : font_family ,
' font_style ' : font_style ,
2017-01-04 15:13:17 +02:00
' label_size ' : d . get ( ' label_size ' , " 62 " ) ,
2017-10-01 15:15:31 +03:00
' kind ' : label_type_specs [ d . get ( ' label_size ' , " 62 " ) ] [ ' kind ' ] ,
2017-01-04 15:13:17 +02:00
' margin ' : int ( d . get ( ' margin ' , 10 ) ) ,
' threshold ' : int ( d . get ( ' threshold ' , 70 ) ) ,
' align ' : d . get ( ' align ' , ' center ' ) ,
2017-02-06 14:27:31 +02:00
' orientation ' : d . get ( ' orientation ' , ' standard ' ) ,
2017-02-10 11:07:04 +02:00
' margin_top ' : float ( d . get ( ' margin_top ' , 24 ) ) / 100. ,
' margin_bottom ' : float ( d . get ( ' margin_bottom ' , 45 ) ) / 100. ,
' margin_left ' : float ( d . get ( ' margin_left ' , 35 ) ) / 100. ,
' margin_right ' : float ( d . get ( ' margin_right ' , 35 ) ) / 100. ,
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 ' ] )
2017-02-06 14:27:31 +02:00
context [ ' margin_left ' ] = int ( context [ ' font_size ' ] * context [ ' margin_left ' ] )
context [ ' margin_right ' ] = int ( context [ ' font_size ' ] * context [ ' margin_right ' ] )
2016-11-21 12:39:09 +02:00
2020-01-30 16:19:22 +02:00
context [ ' fill_color ' ] = ( 255 , 0 , 0 ) if ' red ' in context [ ' label_size ' ] else ( 0 , 0 , 0 )
2018-01-03 22:40:22 +02:00
def get_font_path ( font_family_name , font_style_name ) :
2016-11-21 12:39:09 +02:00
try :
2018-01-03 22:40:22 +02:00
if font_family_name is None or font_style_name is None :
2018-03-05 04:35:31 +02:00
font_family_name = CONFIG [ ' LABEL ' ] [ ' DEFAULT_FONTS ' ] [ ' family ' ]
font_style_name = CONFIG [ ' LABEL ' ] [ ' DEFAULT_FONTS ' ] [ ' style ' ]
2018-01-03 22:40:22 +02:00
font_path = FONTS [ font_family_name ] [ font_style_name ]
2016-11-21 12:39:09 +02:00
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 > width : width , height = height , width
2017-02-06 14:27:31 +02:00
if context [ ' orientation ' ] == ' rotated ' : height , width = width , height
2016-11-21 12:39:09 +02:00
context [ ' width ' ] , context [ ' height ' ] = width , height
return context
def create_label_im ( text , * * kwargs ) :
2017-10-01 15:15:31 +03:00
label_type = kwargs [ ' 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 )
2017-09-11 13:01:20 +03:00
# workaround for a bug in multiline_textsize()
# when there are empty lines in the text:
lines = [ ]
for line in text . split ( ' \n ' ) :
if line == ' ' : line = ' '
lines . append ( line )
text = ' \n ' . join ( lines )
2016-12-19 01:33:36 +02:00
linesize = im_font . getsize ( text )
textsize = draw . multiline_textsize ( text , font = im_font )
2017-02-06 14:27:31 +02:00
width , height = kwargs [ ' width ' ] , kwargs [ ' height ' ]
if kwargs [ ' orientation ' ] == ' standard ' :
if label_type in ( ENDLESS_LABEL , ) :
height = textsize [ 1 ] + kwargs [ ' margin_top ' ] + kwargs [ ' margin_bottom ' ]
elif kwargs [ ' orientation ' ] == ' rotated ' :
if label_type in ( ENDLESS_LABEL , ) :
width = textsize [ 0 ] + kwargs [ ' margin_left ' ] + kwargs [ ' margin_right ' ]
2020-01-30 16:19:22 +02:00
im = Image . new ( ' RGB ' , ( width , height ) , ' white ' )
2016-11-25 22:21:42 +02:00
draw = ImageDraw . Draw ( im )
2017-02-06 14:27:31 +02:00
if kwargs [ ' orientation ' ] == ' standard ' :
if label_type in ( DIE_CUT_LABEL , ROUND_DIE_CUT_LABEL ) :
vertical_offset = ( height - textsize [ 1 ] ) / / 2
vertical_offset + = ( kwargs [ ' margin_top ' ] - kwargs [ ' margin_bottom ' ] ) / / 2
else :
vertical_offset = kwargs [ ' margin_top ' ]
horizontal_offset = max ( ( width - textsize [ 0 ] ) / / 2 , 0 )
elif kwargs [ ' orientation ' ] == ' rotated ' :
2016-12-19 01:33:36 +02:00
vertical_offset = ( height - textsize [ 1 ] ) / / 2
vertical_offset + = ( kwargs [ ' margin_top ' ] - kwargs [ ' margin_bottom ' ] ) / / 2
2017-02-06 14:27:31 +02:00
if label_type in ( DIE_CUT_LABEL , ROUND_DIE_CUT_LABEL ) :
horizontal_offset = max ( ( width - textsize [ 0 ] ) / / 2 , 0 )
else :
horizontal_offset = kwargs [ ' margin_left ' ]
2016-11-21 12:39:09 +02:00
offset = horizontal_offset , vertical_offset
2020-01-30 16:19:22 +02:00
draw . multiline_text ( offset , text , kwargs [ ' fill_color ' ] , 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 ' )
2023-05-13 16:48:43 +03:00
if context [ ' kind ' ] in ( ENDLESS_LABEL , PTOUCH_ENDLESS_LABEL ) :
2017-10-01 15:15:31 +03:00
rotate = 0 if context [ ' orientation ' ] == ' standard ' else 90
elif context [ ' kind ' ] in ( ROUND_DIE_CUT_LABEL , DIE_CUT_LABEL ) :
rotate = ' auto '
2018-03-05 04:35:31 +02:00
qlr = BrotherQLRaster ( CONFIG [ ' PRINTER ' ] [ ' MODEL ' ] )
2020-01-30 15:47:59 +02:00
red = False
if ' red ' in context [ ' label_size ' ] :
red = True
2023-05-13 16:48:43 +03:00
compress = False
if CONFIG [ ' PRINTER ' ] [ ' MODEL ' ] == ' PT-P750W ' :
compress = True
create_label ( qlr , im , context [ ' label_size ' ] , red = red , threshold = context [ ' threshold ' ] , cut = True , rotate = rotate , compress = compress )
2016-11-20 15:57:39 +02:00
if not DEBUG :
try :
2018-03-05 04:35:31 +02:00
be = BACKEND_CLASS ( CONFIG [ ' PRINTER ' ] [ ' PRINTER ' ] )
2016-11-20 15:57:39 +02:00
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 ( ) :
2018-03-05 04:35:31 +02:00
global DEBUG , FONTS , BACKEND_CLASS , CONFIG
2016-11-20 16:10:07 +02:00
parser = argparse . ArgumentParser ( description = __doc__ )
2018-03-07 22:28:32 +02:00
parser . add_argument ( ' --port ' , default = False )
2018-03-05 04:35:31 +02:00
parser . add_argument ( ' --loglevel ' , type = lambda x : getattr ( logging , x . upper ( ) ) , default = False )
parser . add_argument ( ' --font-folder ' , default = False , help = ' folder for additional .ttf/.otf fonts ' )
parser . add_argument ( ' --default-label-size ' , default = False , help = ' Label size inserted in your printer. Defaults to 62. ' )
parser . add_argument ( ' --default-orientation ' , default = False , choices = ( ' standard ' , ' rotated ' ) , help = ' Label orientation, defaults to " standard " . To turn your text by 90°, state " rotated " . ' )
parser . add_argument ( ' --model ' , default = False , choices = models , help = ' The model of your printer (default: QL-500) ' )
parser . add_argument ( ' printer ' , nargs = ' ? ' , default = False , help = ' String descriptor for the printer to use (like tcp://192.168.0.23:9100 or file:///dev/usb/lp0) ' )
2016-11-20 15:57:39 +02:00
args = parser . parse_args ( )
2018-03-05 04:35:31 +02:00
if args . printer :
CONFIG [ ' PRINTER ' ] [ ' PRINTER ' ] = args . printer
if args . port :
PORT = args . port
else :
PORT = CONFIG [ ' SERVER ' ] [ ' PORT ' ]
if args . loglevel :
LOGLEVEL = args . loglevel
else :
LOGLEVEL = CONFIG [ ' SERVER ' ] [ ' LOGLEVEL ' ]
if LOGLEVEL == ' DEBUG ' :
DEBUG = True
else :
DEBUG = False
if args . model :
CONFIG [ ' PRINTER ' ] [ ' MODEL ' ] = args . model
if args . default_label_size :
CONFIG [ ' LABEL ' ] [ ' DEFAULT_SIZE ' ] = args . default_label_size
if args . default_orientation :
CONFIG [ ' LABEL ' ] [ ' DEFAULT_ORIENTATION ' ] = args . default_orientation
if args . font_folder :
ADDITIONAL_FONT_FOLDER = args . font_folder
else :
ADDITIONAL_FONT_FOLDER = CONFIG [ ' SERVER ' ] [ ' ADDITIONAL_FONT_FOLDER ' ]
logging . basicConfig ( level = LOGLEVEL )
2016-11-20 15:57:39 +02:00
try :
2018-03-05 04:35:31 +02:00
selected_backend = guess_backend ( CONFIG [ ' PRINTER ' ] [ ' PRINTER ' ] )
except ValueError :
2016-11-20 15:57:39 +02:00
parser . error ( " Couln ' t guess the backend to use from the printer string descriptor " )
BACKEND_CLASS = backend_factory ( selected_backend ) [ ' backend_class ' ]
2018-03-05 04:35:31 +02:00
if CONFIG [ ' LABEL ' ] [ ' DEFAULT_SIZE ' ] not in label_sizes :
2017-02-06 15:10:13 +02:00
parser . error ( " Invalid --default-label-size. Please choose on of the following: \n : " + " " . join ( label_sizes ) )
2016-11-20 15:57:39 +02:00
FONTS = get_fonts ( )
2018-03-05 04:35:31 +02:00
if ADDITIONAL_FONT_FOLDER :
FONTS . update ( get_fonts ( ADDITIONAL_FONT_FOLDER ) )
2018-01-15 14:16:48 +02:00
if not FONTS :
sys . stderr . write ( " Not a single font was found on your system. Please install some or use the \" --font-folder \" argument. \n " )
sys . exit ( 2 )
2016-11-20 15:57:39 +02:00
2018-03-05 04:35:31 +02:00
for font in CONFIG [ ' LABEL ' ] [ ' DEFAULT_FONTS ' ] :
2016-11-20 15:57:39 +02:00
try :
FONTS [ font [ ' family ' ] ] [ font [ ' style ' ] ]
2018-03-05 04:35:31 +02:00
CONFIG [ ' LABEL ' ] [ ' DEFAULT_FONTS ' ] = font
2016-11-20 15:57:39 +02:00
logger . debug ( " Selected the following default font: {} " . format ( font ) )
break
except : pass
2018-03-05 04:35:31 +02:00
if CONFIG [ ' LABEL ' ] [ ' DEFAULT_FONTS ' ] is None :
2018-01-15 14:17:50 +02:00
sys . stderr . write ( ' Could not find any of the default fonts. Choosing a random one. \n ' )
family = random . choice ( list ( FONTS . keys ( ) ) )
style = random . choice ( list ( FONTS [ family ] . keys ( ) ) )
2018-03-05 04:35:31 +02:00
CONFIG [ ' LABEL ' ] [ ' DEFAULT_FONTS ' ] = { ' family ' : family , ' style ' : style }
sys . stderr . write ( ' The default font is now set to: {family} ( {style} ) \n ' . format ( * * CONFIG [ ' LABEL ' ] [ ' DEFAULT_FONTS ' ] ) )
2016-11-20 15:57:39 +02:00
2018-03-05 04:35:31 +02:00
run ( host = CONFIG [ ' SERVER ' ] [ ' HOST ' ] , port = PORT , debug = DEBUG )
2016-11-20 15:57:39 +02:00
if __name__ == " __main__ " :
main ( )