Skip to content

Instantly share code, notes, and snippets.

@Bougakov
Last active March 25, 2021 13:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Bougakov/cb2501e4ed358873805a861e3c555086 to your computer and use it in GitHub Desktop.
Save Bougakov/cb2501e4ed358873805a861e3c555086 to your computer and use it in GitHub Desktop.
A wrapper for python-escpos to print Unicode text labels with the TrueType font of your choice from command line. Written in Python 3. Also - an additional wrapper written in Parser3, to allow printing notes from web browser.
#!/usr/bin/env python3
debug = 1
import argparse
parser = argparse.ArgumentParser(description="Prints provided image on a POS printer")
parser.add_argument("-i", "--image", type=str, help="path to image")
parser.add_argument("-c", "--crop", type=str, help="Cut paper roll after printing (default: yes)",
choices=["yes","no"])
args = parser.parse_args()
image_toprint = args.image # works with Unicode!
image_toprint = str(image_toprint)
if debug == 1:
print("Received parameter: '{}'".format(image_toprint))
# load the image
from PIL import Image, ImageDraw, ImageFont
img = Image.open(open(image_toprint, 'rb'))
# summarize some details about the image
print("Image format: \'{}\'".format(img.format))
print("Image mode: \'{}\'".format(img.mode))
print("Image size: \'{}\'".format(img.size))
prn_dpi = 203 # check out printer's specifications
inches_width = 2.83465 # width of the adhesive paper roll, in inches (72mm for 80mm roll)
# convert to grayscale
img = img.convert('1')
# resize to fit the paper width:
maxwidth = int(prn_dpi * inches_width)
currwidth = img.size[0]
currheight = img.size[1]
print("Max allowed width is: {}px, actual width is {}px".format(maxwidth, currwidth))
scaling_ratio = maxwidth / currwidth
print("Scaling factor needs to be {}%".format(int(scaling_ratio*100)))
if scaling_ratio < 1:
img = img.resize((maxwidth,int(currheight * scaling_ratio)), Image.BILINEAR)
print("Resized to: {}px × {}px".format(maxwidth,int(currheight * scaling_ratio)))
else:
print("No downscaling was required, will leave image as-is.")
# fixes issue with poorly printed top margin (adds spare 5px on top)
nwidth, nheight = img.size
margin = 5
new_height = nheight + margin
print("Size with margin: {}px × {}px".format(nwidth,new_height))
fix = Image.new("L", (nwidth, new_height), (255))
fix.paste(img, (0, margin))
img = fix
# converts canvas to BW (better we do it here than rely on printer's firmware)
img = img.convert('1')
img.save(image_toprint, dpi=(prn_dpi, prn_dpi) )
# sends the image to printer
from escpos.printer import Usb
p = Usb(0x2730, 0x0fff, in_ep=0x81, out_ep=0x02)
p.set(align=u'center')
print("Printing image..")
p.image(image_toprint)
if args.crop != "no":
print("Cropping paper: {}".format(str(args.crop)))
p.cut(mode='FULL')
else:
print("Cropping paper is disabled.")
print("Finished.")
#!/usr/bin/env python3
debug = 0
import argparse
parser = argparse.ArgumentParser(description="Renders provided Unicode string as image and prints it on a POS printer")
parser.add_argument("-t", "--text", type=str, help="The text to print. For a multi-line text, prefix the parameter with the dollar sign: $'Мама\\nмыла раму' ")
parser.add_argument("-s", "--size", type=int, help="Override the auto font size (80 pt is recommended)")
parser.add_argument("-w", "--wrap", type=int, help="Force-wrap text lines at a given position (default: 50)")
parser.add_argument("-c", "--crop", type=str, help="Cut paper roll after printing (default: yes)",
choices=["yes","no"])
parser.add_argument("-f", "--font", type=str, help="The font to use (if omitted, the default font is Oswald)",
choices=["Caveat","Roboto","JetBrainsMono","Oswald","Lato","OpenSans","Yanone"])
args = parser.parse_args()
text_toprint = args.text # works with Unicode!
word_wrap = 50
if args.wrap:
try:
word_wrap = int(args.wrap) # value from command line overrides default
except ValueError:
word_wrap = 50
import textwrap
if debug ==1:
print("Initial text: ")
print(str(text_toprint))
text_toprint = '\n'.join(['\n'.join(textwrap.wrap(line, word_wrap, break_long_words=True, replace_whitespace=False, expand_tabs=True)) for line in text_toprint.splitlines() if line.strip() != ''])
# Misc. cleanup:
text_toprint = text_toprint.replace("❏","*")
if debug ==1:
print("Text processed by textwrap:")
print(str(text_toprint))
prn_dpi = 203 # check out printer's specifications
inches_width = 2.83465 # width of the adhesive paper roll, in inches (72mm for 80mm roll)
length_list = text_toprint.split('\n')
max_line_length = 0
for i in length_list:
max_line_length = max(max_line_length, len(i))
print("Longest line: {} chars".format(max_line_length))
# For the longer labels we'll reduce the font size, to bring the image size down and to improve the speed of the subsequent image manipulations.
# Tune those values for the font you are using:
if(max_line_length <= 2): font_size = 500
if(max_line_length > 2 and max_line_length <= 10): font_size = int(400 - max_line_length * 10.47)
if(max_line_length >= 11 and max_line_length < 20): font_size = int(200 - max_line_length * 5.21)
if(max_line_length >= 20 and max_line_length < 80): font_size = int(80 - max_line_length * 0.5)
if(max_line_length >= 80): font_size = 50 # font under 30 points is unreadable on my printer
if debug == 1:
workdir = "/var/www/html" # saves image to the root dir of the web server, which allows debugging via web browser (install Nginx to easily access generated images)
else:
workdir = "/var/www/tmp"
if args.size:
try:
font_size = int(args.size) # value from command line overrides auto font size
print("Font size will be force-set by user to: {}pt".format(font_size))
except ValueError:
font_size = 80
print("Font size reset to: {}pt".format(font_size))
else:
print("Font size was automatically set to: {}pt".format(font_size))
print("Length of text: {} chars".format(len(text_toprint)))
print("{} lines in total".format(text_toprint.count('\n') + 1))
from PIL import Image, ImageDraw, ImageFont
if args.font == "JetBrainsMono":
ttf=ImageFont.truetype('/usr/share/fonts/truetype/JetBrainsMono-1.0.3/ttf/JetBrainsMono-Regular.ttf', font_size) # get it from https://www.jetbrains.com/lp/mono/
elif args.font == "Lato":
ttf=ImageFont.truetype('/usr/share/fonts/truetype/google-fonts/Lato-Regular.ttf', font_size) # get it from https://github.com/google/fonts
elif args.font == "Roboto":
ttf=ImageFont.truetype('/usr/share/fonts/truetype/google-fonts/Roboto-Regular.ttf', font_size)
elif args.font == "OpenSans":
ttf=ImageFont.truetype('/usr/share/fonts/truetype/google-fonts/OpenSansCondensed-Light.ttf', font_size)
elif args.font == "Caveat":
ttf=ImageFont.truetype('/usr/share/fonts/truetype/google-fonts/Caveat-Regular.ttf', font_size)
elif args.font == "Yanone":
ttf=ImageFont.truetype('/usr/share/fonts/truetype/google-fonts/YanoneKaffeesatz-Regular.ttf', font_size)
else:
ttf=ImageFont.truetype('/usr/share/fonts/truetype/google-fonts/Oswald-Regular.ttf', font_size)
# Determine text size using a scratch image. Initially it is in grayscale, we'll convert it into B/W later.
img = Image.new("L", (1,1))
draw = ImageDraw.Draw(img)
textsize = draw.textsize(text_toprint.strip(), spacing=4, font=ttf)
# There is a bug in PIL that causes clipping of the fonts,
# it is described in https://stackoverflow.com/questions/1933766/fonts-clipping-with-pil
# To avoid it, we'll add a generous +25% margin on the bottom:
temp = list(textsize)
temp[1] = int(temp[1] * 1.25)
textsize = tuple(temp)
img = Image.new("L", textsize, (255))
draw = ImageDraw.Draw(img)
draw.text((0, 0), text_toprint.strip(), (0), ttf)
print("Result is: {}px × {}px".format(img.size[0], img.size[1]))
if debug == 1: img.save(workdir + '/p1.png' , dpi=(prn_dpi, prn_dpi) )
# To get rid of the unnecessary white space we've added earlier, we scan it pixel by pixel,
# and leave only non-white rows and columns. Sadly, it is a compute-intensive task
# for a single board PC with ARM processor
print("Determining unused blank margins (this can take a while)..")
nonwhite_positions = [(x,y) for x in range(img.size[0]) for y in range(img.size[1]) if img.getdata()[x+y*img.size[0]] != (255)]
rect = (min([x for x,y in nonwhite_positions]), min([y for x,y in nonwhite_positions]), max([x for x,y in nonwhite_positions]), max([y for x,y in nonwhite_positions])) # scans for unused margins of canvas
print("Cropping image..")
img = img.crop(rect) # crops margins
if debug == 1: img.save(workdir + '/p2.png' , dpi=(prn_dpi, prn_dpi) )
print("Result is: {}px × {}px".format(img.size[0], img.size[1]))
# resize to fit the paper width:
maxwidth = int(prn_dpi * inches_width)
currwidth = img.size[0]
currheight = img.size[1]
print("Max allowed width is: {}px, actual width is {}px".format(maxwidth, currwidth))
scaling_ratio = maxwidth / currwidth
print("Scaling factor needs to be {}%".format(int(scaling_ratio*100)))
if scaling_ratio < 1:
img = img.resize((maxwidth,int(currheight * scaling_ratio)), Image.BILINEAR)
print("Resized to: {}px × {}px".format(maxwidth,int(currheight * scaling_ratio)))
else:
print("No downscaling was required, will leave image as-is.")
if debug == 1: img.save(workdir + '/p3.png' , dpi=(prn_dpi, prn_dpi) )
# fixes issue with poorly printed top margin (adds spare 5px on top)
nwidth, nheight = img.size
margin = 5
new_height = nheight + margin
print("Size with margin: {}px × {}px".format(nwidth,new_height))
fix = Image.new("L", (nwidth, new_height), (255))
fix.paste(img, (0, margin))
img = fix
# converts canvas to BW (better we do it here than rely on printer's firmware)
img = img.convert('1')
img.save(workdir + '/p4.png' , dpi=(prn_dpi, prn_dpi) )
# sends the image to printer
from escpos.printer import Usb
p = Usb(0x2730, 0x0fff, in_ep=0x81, out_ep=0x02)
p.set(align=u'center')
print("Printing..")
p.image(workdir + '/p4.png')
# p.qr("https://yandex.ru/maps/-/CShNUNmr", size=12)
if args.crop != "no":
print("Cropping paper: {}".format(str(args.crop)))
p.cut(mode='FULL')
else:
print("Cropping paper is disabled.")
print("Finished.")
<!DOCTYPE html>
<html lang="ru" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>escpos</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<section id="print">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
<h2 class="section-heading">Citizen CT-S2000</h2>
<div class="mb-5">
<form method="post">
<p><textarea name="text_toprint" rows="10" cols="30" style="font-family: Courier New, Courier, monospace" />^if(def $form:text_toprint){$form:text_toprint}</textarea>
<p>Font: <select name="font">
<option value="Yanone" ^if($form:font eq "Yanone"){selected="selected"}>Yanone Kaffeesatz Regular</option>
<option value="Oswald" ^if($form:font eq "Oswald"){selected="selected"}>Oswald Regular (narrow)</option>
<option value="Caveat" ^if($form:font eq "Caveat"){selected="selected"}>Caveat Regular (handwritten)</option>
<option value="Roboto" ^if($form:font eq "Roboto"){selected="selected"}>Roboto Regular</option>
<option value="JetBrainsMono" ^if($form:font eq "JetBrainsMono"){selected="selected"}>JetBrains Mono</option>
<option value="Lato" ^if($form:font eq "Lato"){selected="selected"}>Lato Regular</option>
<option value="OpenSans" ^if($form:font eq "OpenSans"){selected="selected"}>OpenSans Condensed Regular</option>
</select></p>
<p>Cut paper after printing: <select name="crop">
<option value="yes" ^if($form:crop eq "yes"){selected="selected"}>yes</option>
<option value="no" ^if($form:crop eq "no"){selected="selected"}>no</option>
</select></p>
<p>
<input type="checkbox" name="custom" value="1" ^if($form:custom eq "1"){checked="checked"}> Override auto font size, set to:
<input type="text" name="font_size" size="3" value="^if($form:font_size){$form:font_size}{60}">
</p>
<p>
Force-wrap the long lines at:
<input type="text" name="wrap" size="3" value="^if($form:wrap){$form:wrap}{40}">
</p>
<p><input type="submit" name="action"/> <a href="/">Reset form</a></p>
</form>
</div>
</div>
</div>
</section>
^if(def $form:text_toprint){
<section id="print_out">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
^switch[$form:font]{
^case[Roboto]{$font[Roboto]}
^case[Caveat]{$font[Caveat]}
^case[JetBrainsMono]{$font[JetBrainsMono]}
^case[Lato]{$font[Lato]}
^case[Yanone]{$font[Yanone]}
^case[OpenSans]{$font[OpenSans]}
^case[DEFAULT]{$font[Oswald]}
}
^if($form:custom eq "1" && def $form:font_size){
$script[^file::exec[/../../../var/www/cgi/label.py;;-t;$form:text_toprint;-f;$font;-c;$form:crop;-w;^form:wrap.int(50);-s;^form:font_size.int(80)]]
}{
$script[^file::exec[/../../../var/www/cgi/label.py;;-t;$form:text_toprint;-f;$font;-c;$form:crop;-w;^form:wrap.int(50)]]
}
<div><b>Printer's output:</b> <pre style="text-align: left">$script.text</pre></div>
^if($script.status ne "0"){
<div><b>Script status:</b> <pre style="text-align: left">$script.status</pre></div>
}
^if($script.stderr ne ""){
<div><b>Stderr output:</b> <pre style="text-align: left">$script.stderr</pre></div>
}
</div>
</div>
</div>
</section>
}
<section id="printimg">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
<div class="mb-5">
<h3 class="section-heading">Print image</h2>
<form method="post" enctype="multipart/form-data">
<p>
<input type="file" name="pimage">
</p>
<p>Cut paper after printing: <select name="crop">
<option value="yes" ^if($form:crop eq "yes"){selected="selected"}>yes</option>
<option value="no" ^if($form:crop eq "no"){selected="selected"}>no</option>
</select></p>
<p>
<input type="submit" name="action"/>
<a href="/">Reset form</a>
</p>
</form>
</div>
</div>
</div>
</div>
</section>
^if(def $form:pimage){
<section id="printimg_out">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
^form:pimage.save[binary;/../../../var/www/tmp/print.^file:justext[$form:pimage.name]]
<p>File $form:pimage.name was uploaded.</p>
$script[^file::exec[/../../../var/www/cgi/image.py;;-c;$form:crop;-i;/var/www/tmp/print.^file:justext[$form:pimage.name]]]
<div><b>Printer's output:</b> <pre style="text-align: left">$script.text</pre></div>
^if($script.status ne "0"){
<div><b>Script status:</b> <pre style="text-align: left">$script.status</pre></div>
}
^if($script.stderr ne ""){
<div><b>Stderr output:</b> <pre style="text-align: left">$script.stderr</pre></div>
}
<p><a href="/">Reset form</a></p>
</div>
</div>
</div>
</section>
}
<section id="printqr">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
<div class="mb-5">
<h3 class="section-heading">Print QR code</h2>
<form method="post">
<p>
Text to encode in QR:
<input type="text" name="pqr">
</p>
<p>Cut paper after printing: <select name="crop">
<option value="yes" ^if($form:crop eq "yes"){selected="selected"}>yes</option>
<option value="no" ^if($form:crop eq "no"){selected="selected"}>no</option>
</select>
Size: <select name="scale">
<option value="1" ^if($form:scale eq "1"){selected="selected"} >1 (smallest)</option>
<option value="2" ^if($form:scale eq "2"){selected="selected"} >2</option>
<option value="3" ^if($form:scale eq "3"){selected="selected"} >3</option>
<option value="4" ^if($form:scale eq "4"){selected="selected"} >4</option>
<option value="5" ^if($form:scale eq "5"){selected="selected"} >5</option>
<option value="6" ^if($form:scale eq "6"){selected="selected"} >6</option>
<option value="7" ^if($form:scale eq "7"){selected="selected"} >7</option>
<option value="8" ^if($form:scale eq "8"){selected="selected"} >8</option>
<option value="9" ^if($form:scale eq "9"){selected="selected"} >9</option>
<option value="10" ^if($form:scale eq "10"){selected="selected"}>10</option>
<option value="11" ^if($form:scale eq "11"){selected="selected"}>11</option>
<option value="12" ^if($form:scale eq "12"){selected="selected"}>12</option>
<option value="13" ^if($form:scale eq "13"){selected="selected"}>13</option>
<option value="14" ^if($form:scale eq "14"){selected="selected"}>14</option>
<option value="15" ^if($form:scale eq "15"){selected="selected"}>15</option>
<option value="16" ^if($form:scale eq "16"){selected="selected"}>16 (largest)</option>
</select>
</p>
<p>
<input type="submit" name="action"/>
<a href="/">Reset form</a>
</p>
</form>
</div>
</div>
</div>
</div>
</section>
^if(def $form:pqr){
<section id="printqr_out">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
$script[^file::exec[/../../../var/www/cgi/qr.py;;-t;$form:pqr;-s;$form:scale;-c;$form:crop]]
<div><b>Printer's output:</b> <pre style="text-align: left">$script.text</pre></div>
^if($script.status ne "0"){
<div><b>Script status:</b> <pre style="text-align: left">$script.status</pre></div>
}
^if($script.stderr ne ""){
<div><b>Stderr output:</b> <pre style="text-align: left">$script.stderr</pre></div>
}
<p><a href="/">Reset form</a></p>
</div>
</div>
</div>
</section>
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.4.1/jquery.easing.min.js"></script>
</body>
</html>
#!/usr/bin/env python3
debug = 1
import argparse
parser = argparse.ArgumentParser(description="Prints QR on a POS printer")
parser.add_argument("-t", "--text", type=str, help="The text to embed in QR. For a multi-line text, prefix the parameter with the dollar sign: $'Мама\\nмыла раму' ")
parser.add_argument("-c", "--crop", type=str, help="Cut paper roll after printing (default: yes)",
choices=["yes","no"])
parser.add_argument("-s", "--scale", type=int, help="QR size (default: 12)")
args = parser.parse_args()
text_toprint = args.text # works with Unicode!
if debug ==1:
print("Initial text: ")
print(str(text_toprint))
qrsize = 12
if args.scale:
try:
qrsize = int(args.scale) # value from command line overrides default
except ValueError:
qrsize = 12
print("QR size: {}".format(qrsize))
from escpos.printer import Usb
p = Usb(0x2730, 0x0fff, in_ep=0x81, out_ep=0x02)
p.set(align=u'center')
print("Printing QR..")
p.qr(text_toprint, size=qrsize)
if args.crop != "no":
print("Cropping paper: {}".format(str(args.crop)))
p.cut(mode='FULL')
else:
print("Cropping paper is disabled.")
print("Finished.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment