QL-800 series: printing black/red/white labels (CLI: --red)
This commit is contained in:
@@ -15,7 +15,7 @@ The following printers are claimed to be supported (✓ means verified by the au
|
|||||||
|
|
||||||
* QL-500 (✓), QL-550 (✓), QL-560, QL-570 (✓), QL-580N, QL-650TD, QL-700 (✓), QL-710W (✓), QL-720NW (✓), QL-800, QL-810W, QL-820NWB (✓), QL-1050, and QL-1060N.
|
* QL-500 (✓), QL-550 (✓), QL-560, QL-570 (✓), QL-580N, QL-650TD, QL-700 (✓), QL-710W (✓), QL-720NW (✓), QL-800, QL-810W, QL-820NWB (✓), QL-1050, and QL-1060N.
|
||||||
|
|
||||||
The new QL-800 series can print labels with two colors (black and red) on DK-22251 labels. This is not yet supported by this package.
|
The new QL-800 series can print labels with two colors (black and red) on DK-22251 labels.
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ giving:
|
|||||||
|
|
||||||
usage: brother_ql_create [-h] [--model MODEL] [--label-size LABEL_SIZE]
|
usage: brother_ql_create [-h] [--model MODEL] [--label-size LABEL_SIZE]
|
||||||
[--rotate {0,90,180,270}] [--threshold THRESHOLD]
|
[--rotate {0,90,180,270}] [--threshold THRESHOLD]
|
||||||
[--dither] [--compress] [--no-cut]
|
[--dither] [--compress] [--red] [--no-cut]
|
||||||
[--loglevel LOGLEVEL]
|
[--loglevel LOGLEVEL]
|
||||||
image [outfile]
|
image [outfile]
|
||||||
|
|
||||||
@@ -90,6 +90,8 @@ giving:
|
|||||||
set, --threshold is meaningless.
|
set, --threshold is meaningless.
|
||||||
--compress, -c Enable compression (if available with the model).
|
--compress, -c Enable compression (if available with the model).
|
||||||
Takes more time but results in smaller file size.
|
Takes more time but results in smaller file size.
|
||||||
|
--red Create a label to be printed in black/red/white (only
|
||||||
|
with QL-800, QL-810W, QL-820NWB on DK-22251 labels).
|
||||||
--no-cut Don't cut the tape after printing the label.
|
--no-cut Don't cut the tape after printing the label.
|
||||||
--loglevel LOGLEVEL Set to DEBUG for verbose debugging output to stderr.
|
--loglevel LOGLEVEL Set to DEBUG for verbose debugging output to stderr.
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ from __future__ import division
|
|||||||
import sys, argparse, logging
|
import sys, argparse, logging
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import PIL.ImageOps
|
import PIL.ImageOps, PIL.ImageChops
|
||||||
|
|
||||||
from brother_ql.raster import BrotherQLRaster
|
from brother_ql.raster import BrotherQLRaster
|
||||||
from brother_ql.devicedependent import models, label_type_specs, ENDLESS_LABEL, DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL
|
from brother_ql.devicedependent import models, label_type_specs, ENDLESS_LABEL, DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL
|
||||||
from brother_ql import BrotherQLError, BrotherQLUnsupportedCmd, BrotherQLUnknownModel
|
from brother_ql import BrotherQLError, BrotherQLUnsupportedCmd, BrotherQLUnknownModel
|
||||||
|
from brother_ql.image_trafos import filtered_hls
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stdout = sys.stdout.buffer
|
stdout = sys.stdout.buffer
|
||||||
@@ -33,6 +34,7 @@ def main():
|
|||||||
parser.add_argument('--threshold', '-t', type=float, default=70.0, help='The threshold value (in percent) to discriminate between black and white pixels.')
|
parser.add_argument('--threshold', '-t', type=float, default=70.0, help='The threshold value (in percent) to discriminate between black and white pixels.')
|
||||||
parser.add_argument('--dither', '-d', action='store_true', help='Enable dithering when converting the image to b/w. If set, --threshold is meaningless.')
|
parser.add_argument('--dither', '-d', action='store_true', help='Enable dithering when converting the image to b/w. If set, --threshold is meaningless.')
|
||||||
parser.add_argument('--compress', '-c', action='store_true', help='Enable compression (if available with the model). Takes more time but results in smaller file size.')
|
parser.add_argument('--compress', '-c', action='store_true', help='Enable compression (if available with the model). Takes more time but results in smaller file size.')
|
||||||
|
parser.add_argument('--red', action='store_true', help='Create a label to be printed in black/red/white (only with QL-800, QL-810W, QL-820NWB on DK-22251 labels).')
|
||||||
parser.add_argument('--no-cut', dest='cut', action='store_false', help="Don't cut the tape after printing the label.")
|
parser.add_argument('--no-cut', dest='cut', action='store_false', help="Don't cut the tape after printing the label.")
|
||||||
parser.add_argument('--loglevel', type=lambda x: getattr(logging, x), default=logging.WARNING, help='Set to DEBUG for verbose debugging output to stderr.')
|
parser.add_argument('--loglevel', type=lambda x: getattr(logging, x), default=logging.WARNING, help='Set to DEBUG for verbose debugging output to stderr.')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -54,11 +56,11 @@ def main():
|
|||||||
|
|
||||||
qlr.exception_on_warning = True
|
qlr.exception_on_warning = True
|
||||||
|
|
||||||
create_label(qlr, args.image, args.label_size, threshold=args.threshold, cut=args.cut, rotate=args.rotate, dither=args.dither, compress=args.compress)
|
create_label(qlr, args.image, args.label_size, threshold=args.threshold, cut=args.cut, rotate=args.rotate, dither=args.dither, compress=args.compress, red=args.red)
|
||||||
|
|
||||||
args.outfile.write(qlr.data)
|
args.outfile.write(qlr.data)
|
||||||
|
|
||||||
def create_label(qlr, image, label_size, threshold=70, cut=True, dither=False, compress=False, **kwargs):
|
def create_label(qlr, image, label_size, threshold=70, cut=True, dither=False, compress=False, red=False, **kwargs):
|
||||||
|
|
||||||
label_specs = label_type_specs[label_size]
|
label_specs = label_type_specs[label_size]
|
||||||
dots_printable = label_specs['dots_printable']
|
dots_printable = label_specs['dots_printable']
|
||||||
@@ -101,15 +103,33 @@ def create_label(qlr, image, label_size, threshold=70, cut=True, dither=False, c
|
|||||||
new_im.paste(im, (device_pixel_width-im.size[0]-right_margin_dots, 0))
|
new_im.paste(im, (device_pixel_width-im.size[0]-right_margin_dots, 0))
|
||||||
im = new_im
|
im = new_im
|
||||||
|
|
||||||
im = im.convert("L")
|
if red:
|
||||||
im = PIL.ImageOps.invert(im)
|
filter_h = lambda h: 255 if h < 60 or h > 240 else 0
|
||||||
|
filter_l = lambda l: 255 if l < 220 else 0
|
||||||
|
filter_s = lambda s: 255 if s > 100 else 0
|
||||||
|
red_im = filtered_hls(im, filter_h, filter_l, filter_s)
|
||||||
|
red_im = red_im.convert("L")
|
||||||
|
red_im = PIL.ImageOps.invert(red_im)
|
||||||
|
red_im = red_im.convert("1", dither=Image.NONE)
|
||||||
|
|
||||||
if dither:
|
filter_h = lambda h: 255
|
||||||
im = im.convert("1", dither=Image.FLOYDSTEINBERG)
|
filter_l = lambda l: 255 if l < 120 else 0
|
||||||
|
filter_s = lambda s: 255
|
||||||
|
black_im = filtered_hls(im, filter_h, filter_l, filter_s)
|
||||||
|
black_im = black_im.convert("L")
|
||||||
|
black_im = PIL.ImageOps.invert(black_im)
|
||||||
|
black_im = black_im.convert("1", dither=Image.NONE)
|
||||||
|
black_im = PIL.ImageChops.subtract(black_im, red_im)
|
||||||
else:
|
else:
|
||||||
threshold = 100.0 - threshold
|
im = im.convert("L")
|
||||||
threshold = min(255, max(0, int(threshold/100.0 * 255))) # from percent to pixel val
|
im = PIL.ImageOps.invert(im)
|
||||||
im = im.point(lambda x: 0 if x < threshold else 255, mode="1")
|
|
||||||
|
if dither:
|
||||||
|
im = im.convert("1", dither=Image.FLOYDSTEINBERG)
|
||||||
|
else:
|
||||||
|
threshold = 100.0 - threshold
|
||||||
|
threshold = min(255, max(0, int(threshold/100.0 * 255))) # from percent to pixel val
|
||||||
|
im = im.point(lambda x: 0 if x < threshold else 255, mode="1")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
qlr.add_switch_mode()
|
qlr.add_switch_mode()
|
||||||
@@ -142,6 +162,7 @@ def create_label(qlr, image, label_size, threshold=70, cut=True, dither=False, c
|
|||||||
try:
|
try:
|
||||||
qlr.dpi_600 = False
|
qlr.dpi_600 = False
|
||||||
qlr.cut_at_end = cut
|
qlr.cut_at_end = cut
|
||||||
|
qlr.two_color_printing = True if red else False
|
||||||
qlr.add_expanded_mode()
|
qlr.add_expanded_mode()
|
||||||
except BrotherQLUnsupportedCmd:
|
except BrotherQLUnsupportedCmd:
|
||||||
pass
|
pass
|
||||||
@@ -150,7 +171,10 @@ def create_label(qlr, image, label_size, threshold=70, cut=True, dither=False, c
|
|||||||
if compress: qlr.add_compression(True)
|
if compress: qlr.add_compression(True)
|
||||||
except BrotherQLUnsupportedCmd:
|
except BrotherQLUnsupportedCmd:
|
||||||
pass
|
pass
|
||||||
qlr.add_raster_data(im)
|
if red:
|
||||||
|
qlr.add_raster_data(black_im, red_im)
|
||||||
|
else:
|
||||||
|
qlr.add_raster_data(im)
|
||||||
qlr.add_print()
|
qlr.add_print()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from PIL import Image
|
||||||
|
import colorsys
|
||||||
|
|
||||||
|
def HLSColor(img):
|
||||||
|
""" https://stackoverflow.com/a/22237709/183995 """
|
||||||
|
if isinstance(img,Image.Image):
|
||||||
|
img = img.convert('RGB')
|
||||||
|
r,g,b = img.split()
|
||||||
|
Hdat = []
|
||||||
|
Ldat = []
|
||||||
|
Sdat = []
|
||||||
|
for rd,gn,bl in zip(r.getdata(),g.getdata(),b.getdata()) :
|
||||||
|
h,l,s = colorsys.rgb_to_hls(rd/255.,gn/255.,bl/255.)
|
||||||
|
Hdat.append(int(h*255.))
|
||||||
|
Ldat.append(int(l*255.))
|
||||||
|
Sdat.append(int(s*255.))
|
||||||
|
r.putdata(Hdat)
|
||||||
|
g.putdata(Ldat)
|
||||||
|
b.putdata(Sdat)
|
||||||
|
return Image.merge('RGB',(r,g,b))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def filtered_hls(im, filter_h, filter_l, filter_s, default_col=(255,255,255)):
|
||||||
|
hls_im = HLSColor(im)
|
||||||
|
|
||||||
|
H, L, S = 0, 1, 2
|
||||||
|
hls = hls_im.split()
|
||||||
|
mask_h = hls[H].point(filter_h)
|
||||||
|
mask_l = hls[L].point(filter_l)
|
||||||
|
mask_s = hls[S].point(filter_s)
|
||||||
|
|
||||||
|
Mdat = []
|
||||||
|
# for debugging:
|
||||||
|
#mask_h, mask_l, mask_s = hls_im.split()
|
||||||
|
seen = []
|
||||||
|
for h, l, s in zip(mask_h.getdata(), mask_l.getdata(), mask_s.getdata()):
|
||||||
|
if (h, l, s) not in seen:
|
||||||
|
seen.append((h, l, s))
|
||||||
|
#print((h, l, s))
|
||||||
|
Mdat.append(255 if (h and l and s) else 0)
|
||||||
|
|
||||||
|
mask = mask_h
|
||||||
|
mask.putdata(Mdat)
|
||||||
|
|
||||||
|
filtered_im = Image.new("RGB", hls_im.size, color=default_col)
|
||||||
|
filtered_im.paste(im, None, mask)
|
||||||
|
return filtered_im
|
||||||
|
|
||||||
+26
-12
@@ -38,6 +38,7 @@ class BrotherQLRaster(object):
|
|||||||
self.page_number = 0
|
self.page_number = 0
|
||||||
self.cut_at_end = True
|
self.cut_at_end = True
|
||||||
self.dpi_600 = False
|
self.dpi_600 = False
|
||||||
|
self.two_color_printing = False
|
||||||
self._compression = False
|
self._compression = False
|
||||||
self.exception_on_warning = False
|
self.exception_on_warning = False
|
||||||
|
|
||||||
@@ -153,6 +154,7 @@ class BrotherQLRaster(object):
|
|||||||
flags = 0x00
|
flags = 0x00
|
||||||
flags |= self.cut_at_end << 3
|
flags |= self.cut_at_end << 3
|
||||||
flags |= self.dpi_600 << 6
|
flags |= self.dpi_600 << 6
|
||||||
|
flags |= self.two_color_printing << 0
|
||||||
self.data += bytes([flags])
|
self.data += bytes([flags])
|
||||||
|
|
||||||
def add_margins(self, dots=0x23):
|
def add_margins(self, dots=0x23):
|
||||||
@@ -174,27 +176,39 @@ class BrotherQLRaster(object):
|
|||||||
nbpr = number_bytes_per_row['default']
|
nbpr = number_bytes_per_row['default']
|
||||||
return nbpr*8
|
return nbpr*8
|
||||||
|
|
||||||
def add_raster_data(self, image):
|
def add_raster_data(self, image, second_image=None):
|
||||||
""" image: Pillow Image() """
|
""" image: Pillow Image() """
|
||||||
logger.info("raster_image_size: {0}x{1}".format(*image.size))
|
logger.info("raster_image_size: {0}x{1}".format(*image.size))
|
||||||
image = image.transpose(Image.FLIP_LEFT_RIGHT)
|
|
||||||
image = image.convert("1")
|
|
||||||
if image.size[0] != self.get_pixel_width():
|
if image.size[0] != self.get_pixel_width():
|
||||||
fmt = 'Wrong pixel width: {}, expected {}'
|
fmt = 'Wrong pixel width: {}, expected {}'
|
||||||
raise BrotherQLRasterError(fmt.format(image.size[0], self.get_pixel_width()))
|
raise BrotherQLRasterError(fmt.format(image.size[0], self.get_pixel_width()))
|
||||||
frame = bytes(image.tobytes(encoder_name='raw'))
|
images = [image]
|
||||||
frame_len = len(frame)
|
if second_image:
|
||||||
row_len = image.size[0]//8
|
if image.size != second_image.size:
|
||||||
|
fmt = "First and second image don't have the same dimesions: {} vs {}."
|
||||||
|
raise BrotherQLRasterError(fmt.format(image.size, second_image.size))
|
||||||
|
images.append(second_image)
|
||||||
|
frames = []
|
||||||
|
for image in images:
|
||||||
|
image = image.transpose(Image.FLIP_LEFT_RIGHT)
|
||||||
|
image = image.convert("1")
|
||||||
|
frames.append(bytes(image.tobytes(encoder_name='raw')))
|
||||||
|
frame_len = len(frames[0])
|
||||||
|
row_len = images[0].size[0]//8
|
||||||
start = 0
|
start = 0
|
||||||
file_str = BytesIO()
|
file_str = BytesIO()
|
||||||
while start + row_len <= frame_len:
|
while start + row_len <= frame_len:
|
||||||
row = frame[start:start+row_len]
|
for i, frame in enumerate(frames):
|
||||||
|
row = frame[start:start+row_len]
|
||||||
|
if second_image:
|
||||||
|
file_str.write(b'\x77\x01' if i == 0 else b'\x77\x02')
|
||||||
|
else:
|
||||||
|
file_str.write(b'\x67\x00')
|
||||||
|
if self._compression:
|
||||||
|
row = packbits.encode(row)
|
||||||
|
file_str.write(bytes([len(row)]))
|
||||||
|
file_str.write(row)
|
||||||
start += row_len
|
start += row_len
|
||||||
file_str.write(b'\x67\x00')
|
|
||||||
if self._compression:
|
|
||||||
row = packbits.encode(row)
|
|
||||||
file_str.write(bytes([len(row)]))
|
|
||||||
file_str.write(row)
|
|
||||||
self.data += file_str.getvalue()
|
self.data += file_str.getvalue()
|
||||||
|
|
||||||
def add_print(self, last_page=True):
|
def add_print(self, last_page=True):
|
||||||
|
|||||||
Reference in New Issue
Block a user