From ed97975b9c52228527fb2852d8515ea6971b8457 Mon Sep 17 00:00:00 2001 From: Philipp Klaus Date: Mon, 18 Sep 2017 23:09:39 +0200 Subject: [PATCH] QL-800 series: printing black/red/white labels (CLI: --red) --- README.md | 6 ++-- brother_ql/brother_ql_create.py | 46 +++++++++++++++++++++++-------- brother_ql/image_trafos.py | 49 +++++++++++++++++++++++++++++++++ brother_ql/raster.py | 38 +++++++++++++++++-------- 4 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 brother_ql/image_trafos.py diff --git a/README.md b/README.md index 7421e69..4f5262d 100644 --- a/README.md +++ b/README.md @@ -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. -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 @@ -63,7 +63,7 @@ giving: usage: brother_ql_create [-h] [--model MODEL] [--label-size LABEL_SIZE] [--rotate {0,90,180,270}] [--threshold THRESHOLD] - [--dither] [--compress] [--no-cut] + [--dither] [--compress] [--red] [--no-cut] [--loglevel LOGLEVEL] image [outfile] @@ -90,6 +90,8 @@ giving: set, --threshold is meaningless. --compress, -c Enable compression (if available with the model). 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. --loglevel LOGLEVEL Set to DEBUG for verbose debugging output to stderr. diff --git a/brother_ql/brother_ql_create.py b/brother_ql/brother_ql_create.py index 0104f88..57b49ce 100755 --- a/brother_ql/brother_ql_create.py +++ b/brother_ql/brother_ql_create.py @@ -5,11 +5,12 @@ from __future__ import division import sys, argparse, logging from PIL import Image -import PIL.ImageOps +import PIL.ImageOps, PIL.ImageChops 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 import BrotherQLError, BrotherQLUnsupportedCmd, BrotherQLUnknownModel +from brother_ql.image_trafos import filtered_hls try: 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('--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('--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('--loglevel', type=lambda x: getattr(logging, x), default=logging.WARNING, help='Set to DEBUG for verbose debugging output to stderr.') args = parser.parse_args() @@ -54,11 +56,11 @@ def main(): 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) -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] 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)) im = new_im - im = im.convert("L") - im = PIL.ImageOps.invert(im) + if red: + 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: - im = im.convert("1", dither=Image.FLOYDSTEINBERG) + filter_h = lambda h: 255 + 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: - 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") + im = im.convert("L") + im = PIL.ImageOps.invert(im) + + 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: qlr.add_switch_mode() @@ -142,6 +162,7 @@ def create_label(qlr, image, label_size, threshold=70, cut=True, dither=False, c try: qlr.dpi_600 = False qlr.cut_at_end = cut + qlr.two_color_printing = True if red else False qlr.add_expanded_mode() except BrotherQLUnsupportedCmd: 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) except BrotherQLUnsupportedCmd: 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() if __name__ == "__main__": diff --git a/brother_ql/image_trafos.py b/brother_ql/image_trafos.py new file mode 100644 index 0000000..c994dee --- /dev/null +++ b/brother_ql/image_trafos.py @@ -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 + diff --git a/brother_ql/raster.py b/brother_ql/raster.py index 7e90df1..77827ba 100644 --- a/brother_ql/raster.py +++ b/brother_ql/raster.py @@ -38,6 +38,7 @@ class BrotherQLRaster(object): self.page_number = 0 self.cut_at_end = True self.dpi_600 = False + self.two_color_printing = False self._compression = False self.exception_on_warning = False @@ -153,6 +154,7 @@ class BrotherQLRaster(object): flags = 0x00 flags |= self.cut_at_end << 3 flags |= self.dpi_600 << 6 + flags |= self.two_color_printing << 0 self.data += bytes([flags]) def add_margins(self, dots=0x23): @@ -174,27 +176,39 @@ class BrotherQLRaster(object): nbpr = number_bytes_per_row['default'] return nbpr*8 - def add_raster_data(self, image): + def add_raster_data(self, image, second_image=None): """ image: Pillow Image() """ 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(): fmt = 'Wrong pixel width: {}, expected {}' raise BrotherQLRasterError(fmt.format(image.size[0], self.get_pixel_width())) - frame = bytes(image.tobytes(encoder_name='raw')) - frame_len = len(frame) - row_len = image.size[0]//8 + images = [image] + if second_image: + 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 file_str = BytesIO() 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 - 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() def add_print(self, last_page=True):