summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile35
-rw-r--r--README.md113
-rw-r--r--build/.gitignore1
-rw-r--r--build/.keepme0
-rw-r--r--example.pngbin0 -> 542 bytes
-rw-r--r--include/loadpng.h15
-rw-r--r--include/ql.h155
-rw-r--r--src/loadpng.c99
-rw-r--r--src/main.c205
-rw-r--r--src/ql.c333
10 files changed, 956 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d170eb3
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,35 @@
+default: build/qlprint
+
+# Get rid of most of the implicit rules by clearing the .SUFFIXES target
+.SUFFIXES:
+# Get rid of the auto-checkout from old version control systems rules
+%: %,v
+%: RCS/%,v
+%: RCS/%
+%: s.%
+%: SCCS/s.%
+
+
+CFLAGS=-std=c11 -Wall -Wextra -g -Iinclude -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809 $(shell pkg-config --cflags libpng)
+LDFLAGS=$(shell pkg-config --libs libpng)
+
+OBJS=$(addprefix build/, \
+ main.o \
+ ql.o \
+ loadpng.o \
+)
+
+vpath %.c src
+
+build/%.o: %.c
+ $(CC) $(CFLAGS) -c $< -o $@
+
+build/qlprint: $(OBJS)
+ $(CC) $(LDFLAGS) $^ -o $@
+
+$(OBJS): $(wildcard include/*) Makefile
+
+.PHONY: clean
+clean:
+ -rm -f build/*
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..425cf39
--- /dev/null
+++ b/README.md
@@ -0,0 +1,113 @@
+# qlprint
+Command-line utility for printing to Brother QL series label printers.
+
+Tested with:
+ * QL-570
+
+Hopefully also works with:
+ * QL-500/550
+ * QL-560
+ * QL-580N
+ * QL-650TD
+ * QL-700
+ * QL-710W
+ * QL-720W
+ * QL-1050
+ * QL-1060N
+
+Inspired, though not derived from, the [ql570](https://github.com/sudomesh/ql570.git) tool. Licensed under the GPLv3 nevertheless.
+
+## Building
+Requires `GNU make` and `libpng` (with development headers), with `pkg-config` to locate libpng headers & libs.
+
+Simply run `make` in this directory, e.g.:
+```
+$ make
+cc -std=c11 -Wall -Wextra -g -Iinclude -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809 -I/usr/include/libpng12 -c src/main.c -o build/main.o
+cc -std=c11 -Wall -Wextra -g -Iinclude -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809 -I/usr/include/libpng12 -c src/ql.c -o build/ql.o
+cc -std=c11 -Wall -Wextra -g -Iinclude -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809 -I/usr/include/libpng12 -c src/loadpng.c -o build/loadpng.o
+cc -lpng12 build/main.o build/ql.o build/loadpng.o -o build/qlprint
+$
+```
+
+## Running
+```
+Syntax:
+ qlprint [-p lp] -i
+ [-p lp] [-m margin] [-a] [-C|-D] [-W width] [-L length] [-Q] [-n num] [-t threshold] png...
+Where:
+ -p lp Printer port (default /dev/usb/lp0)
+ -i Print status information only, then exit
+ -m margin Margin (dots)
+ -a Enable auto-cut
+ -C Request continuous-length-tape when printing (error if not)
+ -D Request die-cut-labels when printing (error if not)
+ -W width Request particular width media when printing (error if not)
+ -L length Request particular length media when printing (error if not)
+ -Q Prioritise quality of speed
+ -n num Print num copies
+ -t threshold Threshold for black-vs-white (default 128, i.e. 0-127=black)
+ png... One or more png files to print
+
+```
+
+The PNG files are converted to monochrome internally. The black-vs-white
+threshold for this conversion may be tuned with the `-t threshold` argument.
+
+Image height is limited to the capability of the printer (720 for most, 1296
+for 1050/1060N models). Attempting to print larger images will fail.
+
+On successful printing, the exit code is zero; in case of any error, the exit
+code is non-zero and an error message is printed to stderr.
+
+## Examples
+
+### Show printer status information
+Here with a narrow continuous-length-tape cartridge loaded.
+```
+$ ./build/qlprint -i
+ Printer: QL-570
+ Mode: no-auto-cut
+ Errors: none
+ Media type: continuous-length-tape
+ Media width (mm): 29
+$
+```
+
+### Printing with auto-cutter enabled:
+```
+$ ./build/qlprint -a example.png
+example.png (135x135) OK
+$
+```
+
+### Printing two images, cutting only once
+```
+$ ./build/qlprint -a example.png example.png
+example.png (135x135) OK
+example.png (135x135) OK
+$
+```
+
+### Printing two copies of the one image, cutting after each
+```
+$ ./build/qlprint -a -n 2 example.png
+example.png (135x135) OK
+example.png (135x135) OK
+$
+```
+
+### Print only on the correct media type
+Assuming a continuous-length-tape cartridge is installed:
+```
+$ ./build/qlprint -C example.png
+example.png (135x135) OK
+$
+```
+...otherwise:
+```
+$ ./build/qlprint -C example.png
+Printer reported error(s): replace-media
+$
+```
+
diff --git a/build/.gitignore b/build/.gitignore
new file mode 100644
index 0000000..72e8ffc
--- /dev/null
+++ b/build/.gitignore
@@ -0,0 +1 @@
+*
diff --git a/build/.keepme b/build/.keepme
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/build/.keepme
diff --git a/example.png b/example.png
new file mode 100644
index 0000000..f5f8c41
--- /dev/null
+++ b/example.png
Binary files differ
diff --git a/include/loadpng.h b/include/loadpng.h
new file mode 100644
index 0000000..cace43a
--- /dev/null
+++ b/include/loadpng.h
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2017 DiUS Computing Pty Ltd. All rights reserved.
+ *
+ * Released under GPLv3, see LICENSE for details.
+ *
+ * @author Johny Mattsson <jmattsson@dius.com.au>
+ */
+#ifndef _LOADPNG_H_
+#define _LOADPNG_H_
+
+#include "ql.h"
+
+ql_raster_image_t *loadpng(const char *path);
+
+#endif
diff --git a/include/ql.h b/include/ql.h
new file mode 100644
index 0000000..b854c06
--- /dev/null
+++ b/include/ql.h
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2017 DiUS Computing Pty Ltd. All rights reserved.
+ *
+ * Released under GPLv3, see LICENSE for details.
+ *
+ * @author Johny Mattsson <jmattsson@dius.com.au>
+ */
+#ifndef _QL_H_
+#define _QL_H_
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <unistd.h>
+
+/* The QL module is based on an amalgamation of information from:
+ *
+ * - Brother QL-500/550/560/570/580N/650TD/700/1050/1060N Command Reference
+ * http://download.brother.com/welcome/docp000678/cv_qlseries_eng_raster_600.pdf
+ * - Software Developer's Manual Raster Command Reference QL-710W/720NW
+ * http://download.brother.com/welcome/docp000698/cv_ql710720_eng_raster_100.pdf
+ * - Actual experience communicating with a QL-570
+ *
+ */
+
+typedef struct
+{
+ uint8_t print_head_mark;
+ uint8_t sz;
+ uint8_t rsvd_2; // 'B'
+ uint8_t model_class;
+ uint8_t model_code;
+ uint8_t rsvd_5; // '0'
+ uint8_t rsvd_6; // '0'
+ uint8_t rsvd_7; // 0x00
+ uint8_t err_info_1;
+ uint8_t err_info_2;
+ uint8_t media_width_mm;
+ uint8_t media_type;
+ uint8_t rsvd_12; // 0x00
+ uint8_t rsvd_13; // 0x00
+ uint8_t rsvd_14; // 0x3f
+ uint8_t mode;
+ uint8_t rsvd_16; // 0x00
+ uint8_t media_length_mm;
+ uint8_t status_type;
+ uint8_t phase_type;
+ uint8_t phase_hi;
+ uint8_t phase_lo;
+ uint8_t notification;
+ uint8_t rsvd_23; // 0x00
+ uint8_t rsvd_24[8]; // 0x00...
+} ql_status_t;
+
+#define QL_ERR_1_NO_MEDIA 0x01
+#define QL_ERR_1_END_OF_MEDIA 0x02
+#define QL_ERR_1_CUTTER_JAM 0x04
+// 0x08 not defined
+#define QL_ERR_1_PRINTER_IN_USE 0x10
+#define QL_ERR_1_PRINTER_TURNED_OFF 0x20
+#define QL_ERR_1_HIGH_VOLTAGE_ADAPTER 0x40
+#define QL_ERR_1_FAN_MOTOR_ERROR 0x80
+
+#define QL_ERR_2_REPLACE_MEDIA 0x01
+#define QL_ERR_2_EXPANSION_BUFFER_FULL 0x02
+#define QL_ERR_2_COMMUNICATION_ERROR 0x04
+#define QL_ERR_2_COMMUNICATION_BUFFER_FULL 0x08
+#define QL_ERR_2_COVER_OPEN 0x10
+#define QL_ERR_2_CANCEL_KEY 0x20
+#define QL_ERR_2_MEDIA_CANNOT_BE_FED 0x40
+#define QL_ERR_2_SYSTEM_ERROR 0x80
+
+#define QL_MEDIA_LENGTH_CONTINUOUS 0x00
+
+#define QL_MEDIA_TYPE_NO_MEDIA 0x00
+#define QL_MEDIA_TYPE_CONTINUOUS 0x0a
+#define QL_MEDIA_TYPE_DIECUT_LABELS 0x0b
+// The 710/720 might report these instead
+#define QL_MEDIA_TYPE_CONTINUOUS_ALT 0x4a
+#define QL_MEDIA_TYPE_DIECUT_LABELS_ALT 0x4b
+
+// Flags for mode
+#define QL_MODE_NO_AUTOCUT 0x00
+#define QL_MODE_AUTOCUT 0x40
+
+#define QL_STATUS_TYPE_REPLY 0x00
+#define QL_STATUS_TYPE_PRINTING_DONE 0x01
+#define QL_STATUS_TYPE_ERROR_OCCURRED 0x02
+#define QL_STATUS_TYPE_TURNED_OFF 0x04
+#define QL_STATUS_TYPE_NOTIFICATION 0x05
+#define QL_STATUS_TYPE_PHASE_CHANGE 0x06
+
+#define QL_PHASE_TYPE_RECEIVING 0x00
+#define QL_PHASE_TYPE_PRINTING 0x01
+
+#define QL_NOTIFICATION_NONE 0x00
+#define QL_NOTIFICATION_COOLING_STARTED 0x03
+#define QL_NOTIFICATION_COOLING_DONE 0x04
+
+// Flags for expanded mode
+#define QL_EXPANDED_MODE_CUT_AT_END 0x10 /* Gah, 710 doc claims 0x08! */
+#define QL_EXPANDED_MODE_HIGH_RES 0x40 /* QL-570/580N/700 */
+
+typedef struct {
+ uint16_t width;
+ uint16_t height;
+ uint8_t data[];
+} ql_raster_image_t;
+
+typedef struct {
+ uint8_t threshold; // pixel values below threshold deemed black
+ uint8_t flags; // QL_PRINT_CFG_xxx flags, indicating which other fields valid
+ uint8_t media_type;
+ uint8_t media_width;
+ uint8_t media_length;
+ bool first_page; // used for autocut pagination
+} ql_print_cfg_t;
+
+#define QL_PRINT_CFG_MEDIA_TYPE 0x02
+#define QL_PRINT_CFG_MEDIA_WIDTH 0x04
+#define QL_PRINT_CFG_MEDIA_LENGTH 0x08
+#define QL_PRINT_CFG_QUALITY_PRIO 0x40
+
+typedef struct ql_ctx *ql_ctx_t;
+
+ql_ctx_t ql_open(const char *printer);
+void ql_close(ql_ctx_t ctx);
+
+bool ql_init(ql_ctx_t ctx); // also cancel
+bool ql_request_status(ql_ctx_t ctx);
+bool ql_read_status(ql_ctx_t ctx, ql_status_t *status);
+
+bool ql_needs_mode_switch(const ql_status_t *status);
+bool ql_switch_to_raster_mode(ql_ctx_t ctx);
+
+bool ql_set_mode(ql_ctx_t ctx, unsigned mode);
+bool ql_set_expanded_mode(ql_ctx_t ctx, unsigned mode);
+bool ql_set_autocut_every_n(ql_ctx_t ctx, uint8_t n);
+bool ql_set_margin(ql_ctx_t ctx, uint16_t dots);
+
+// Note: status needed for 1050/1060N detection to adjust command format
+bool ql_print_raster_image(ql_ctx_t ctx, const ql_status_t *status, const ql_raster_image_t *img, const ql_print_cfg_t *cfg);
+
+// Caution: ql_decode_*() are *not* multi-thread safe
+const char *ql_decode_mode(const ql_status_t *status);
+const char *ql_decode_errors(const ql_status_t *status);
+const char *ql_decode_model(const ql_status_t *status);
+const char *ql_decode_media_type(const ql_status_t *status);
+#define QL_DECODE_MODEL 0x01
+#define QL_DECODE_ERROR 0x02
+#define QL_DECODE_MEDIA 0x04
+#define QL_DECODE_MODE 0x08
+void ql_decode_print_status(FILE *out, const ql_status_t *status, unsigned flags);
+
+#endif
diff --git a/src/loadpng.c b/src/loadpng.c
new file mode 100644
index 0000000..c986ce2
--- /dev/null
+++ b/src/loadpng.c
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2017 DiUS Computing Pty Ltd. All rights reserved.
+ *
+ * Released under GPLv3, see LICENSE for details.
+ *
+ * @author Johny Mattsson <jmattsson@dius.com.au>
+ */
+#include "loadpng.h"
+#include <stdlib.h>
+#include <assert.h>
+#include <png.h>
+
+static_assert(sizeof(png_byte) == sizeof( ((ql_raster_image_t *)0)->data[0]), "Code relies on png_byte being compatible with ql_raster_image_t data ");
+
+ql_raster_image_t *loadpng(const char *path)
+{
+ ql_raster_image_t *ret = NULL;
+
+ if (!path)
+ goto out;
+
+ FILE *f = fopen(path, "rb");
+ if (!f)
+ goto out;
+
+ uint8_t header[8];
+ if (fread(header, 1, sizeof(header), f) != 8)
+ goto close_out;
+
+ if (!png_check_sig(header, sizeof(header)))
+ goto close_out;
+
+ png_infop info_ptr = NULL, end_ptr = NULL;
+ png_structp png_ptr =
+ png_create_read_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+ if (!png_ptr)
+ goto close_out;
+
+ info_ptr = png_create_info_struct(png_ptr);
+ if (!info_ptr)
+ goto destroy_read_out;
+
+ end_ptr = png_create_info_struct(png_ptr);
+ if (!end_ptr)
+ goto destroy_read_out;
+
+ if (setjmp(png_jmpbuf(png_ptr)))
+ goto destroy_read_out;
+
+ png_init_io(png_ptr, f);
+ png_set_sig_bytes(png_ptr, sizeof(header));
+
+ png_read_info(png_ptr, info_ptr);
+
+ png_set_strip_alpha(png_ptr);
+ png_set_rgb_to_gray_fixed(png_ptr, 1, -1, -1); // force into grayscale
+ png_set_expand_gray_1_2_4_to_8(png_ptr); // get us a known output format
+
+ png_read_update_info(png_ptr, info_ptr);
+
+ png_uint_32 width = png_get_image_width(png_ptr, info_ptr);
+ png_uint_32 height = png_get_image_height(png_ptr, info_ptr);
+
+ png_bytepp row_ptrs = calloc(height, sizeof(png_bytep));
+ if (!row_ptrs)
+ goto destroy_read_out;
+
+ const unsigned row_bytes = width * sizeof(png_byte);
+ ql_raster_image_t *img =
+ calloc(1, sizeof(ql_raster_image_t) + height * row_bytes);
+ if (!img)
+ goto free_image_out;
+
+ if (setjmp(png_jmpbuf(png_ptr)))
+ goto free_image_out;
+
+ for (png_uint_32 i = 0; i < height; ++i)
+ row_ptrs[i] = (png_bytep)(img->data + (i * row_bytes));
+
+ png_read_image(png_ptr, row_ptrs);
+ png_read_end(png_ptr, end_ptr);
+
+ img->height = height;
+ img->width = width;
+
+ ret = img;
+ img = NULL; // don't free it, we're returning it now
+
+free_image_out:
+ free(img);
+ free(row_ptrs);
+destroy_read_out:
+ png_destroy_read_struct(
+ &png_ptr, info_ptr ? &info_ptr : NULL, end_ptr ? &end_ptr : NULL);
+close_out:
+ fclose(f);
+out:
+ return ret;
+}
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..4b34ec1
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2017 DiUS Computing Pty Ltd. All rights reserved.
+ *
+ * Released under GPLv3, see LICENSE for details.
+ *
+ * @author Johny Mattsson <jmattsson@dius.com.au>
+ */
+#include "ql.h"
+#include "loadpng.h"
+#include <errno.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <signal.h>
+
+static bool timed_out = false;
+void on_alarm(int ignored)
+{
+ (void)ignored;
+ timed_out = true;
+}
+
+
+void syntax(void)
+{
+ fprintf(stderr,
+"Syntax:\n"
+" qlprint [-p lp] -i\n"
+" [-p lp] [-m margin] [-a] [-C|-D] [-W width] [-L length] [-Q] [-n num] [-t threshold] png...\n"
+"Where:\n"
+" -p lp Printer port (default /dev/usb/lp0)\n"
+" -i Print status information only, then exit\n"
+" -m margin Margin (dots)\n"
+" -a Enable auto-cut\n"
+" -C Request continuous-length-tape when printing (error if not)\n"
+" -D Request die-cut-labels when printing (error if not)\n"
+" -W width Request particular width media when printing (error if not)\n"
+" -L length Request particular length media when printing (error if not)\n"
+" -Q Prioritise quality of speed\n"
+" -n num Print num copies\n"
+" -t threshold Threshold for black-vs-white (default 128, i.e. 0-127=black)\n"
+" png... One or more png files to print\n"
+"\n");
+
+ exit(EXIT_FAILURE);
+}
+
+int main (int argc, char *argv[])
+{
+ bool info_only = false;
+ int32_t margin = -1;
+ bool autocut = false;
+ int num = 1;
+ ql_print_cfg_t cfg = {
+ .threshold = 0x80,
+ .flags = 0
+ };
+ int opt;
+ const char *printer = "/dev/usb/lp0";
+ while ((opt = getopt(argc, argv, "ip:m:an:CDW:L:Q")) != -1)
+ {
+ switch(opt)
+ {
+ case 'i': info_only = true; break;
+ case 'p': printer = optarg; break;
+ case 'm': margin = atoi(optarg); break;
+ case 'a': autocut = true; break;
+ case 'n': num = atoi(optarg); break;
+ case 'C': cfg.media_type = QL_MEDIA_TYPE_CONTINUOUS;
+ cfg.flags |= QL_PRINT_CFG_MEDIA_TYPE; break;
+ case 'D': cfg.media_type = QL_MEDIA_TYPE_DIECUT_LABELS;
+ cfg.flags |= QL_PRINT_CFG_MEDIA_TYPE; break;
+ case 'W': cfg.media_width = atoi(optarg);
+ cfg.flags |= QL_PRINT_CFG_MEDIA_WIDTH; break;
+ case 'L': cfg.media_length = atoi(optarg);
+ cfg.flags |= QL_PRINT_CFG_MEDIA_LENGTH; break;
+ case 'Q': cfg.flags |= QL_PRINT_CFG_QUALITY_PRIO; break;
+ default: syntax();
+ }
+ }
+
+ if (optind >= argc && !info_only)
+ syntax();
+
+ ql_ctx_t ctx = ql_open(printer);
+ if (!ctx)
+ {
+ fprintf(stderr, "Unable to open '%s': %s\n", printer, strerror(errno));
+ return EXIT_FAILURE;
+ }
+
+ if (!ql_init(ctx))
+ {
+ fprintf(stderr, "Failed to send initialisation sequence to printer: %s\n",
+ strerror(errno));
+ return EXIT_FAILURE;
+ }
+
+ if (!ql_request_status(ctx))
+ {
+ fprintf(stderr, "Failed to request status from printer: %s\n",
+ strerror(errno));
+ return EXIT_FAILURE;
+ }
+
+ ql_status_t status = { 0, };
+ if (!ql_read_status(ctx, &status))
+ {
+ fprintf(stderr, "Failed to read status from printer: %s\n",
+ strerror(errno));
+ return EXIT_FAILURE;
+ }
+
+/*
+for (int i = 0; i < 32; ++i)
+ printf("%02hhx ", ((char *)&status)[i]);
+printf("\n");
+*/
+
+ if (info_only)
+ {
+ ql_decode_print_status(stdout, &status,
+ QL_DECODE_MODEL | QL_DECODE_MEDIA | QL_DECODE_ERROR | QL_DECODE_MODE);
+ return EXIT_SUCCESS;
+ }
+
+ if (margin >= 0 && !ql_set_margin(ctx, (uint16_t)margin))
+ {
+ fprintf(stderr, "Failed to set margin: %s\n", strerror(errno));
+ return EXIT_FAILURE;
+ }
+ if (autocut &&
+ (!ql_set_mode(ctx, QL_MODE_AUTOCUT) ||
+ !ql_set_autocut_every_n(ctx, argc - optind)))
+ {
+ fprintf(stderr, "Failed to set autocut: %s\n", strerror(errno));
+ return EXIT_FAILURE;
+ }
+
+ if (ql_needs_mode_switch(&status) && !ql_switch_to_raster_mode(ctx))
+ {
+ fprintf(stderr, "Failed to set raster mode: %s\n", strerror(errno));
+ return EXIT_FAILURE;
+ }
+
+ signal(SIGALRM, on_alarm);
+ while (num--)
+ {
+ cfg.first_page = true;
+ for (int i = optind; i < argc; ++i)
+ {
+ ql_raster_image_t *img = loadpng(argv[i]);
+ if (!img)
+ {
+ fprintf(stderr, "Failed to load image '%s'\n", argv[i]);
+ return EXIT_FAILURE;
+ }
+/*
+for(int i = 0; i < img->height; ++i)
+{
+ for(int j = 0; j < img->width; ++j)
+ printf("%c", img->data[i*img->width + j] < cfg.threshold ? '#' : '.');
+ printf("\n");
+}
+*/
+ if (!ql_print_raster_image(ctx, &status, img, &cfg))
+ {
+ fprintf(stderr, "Failed to print '%s' (%ux%u)\n",
+ argv[i], img->width, img->height);
+ return EXIT_FAILURE;
+ }
+ alarm(5);
+ do {
+ if (!ql_read_status(ctx, &status))
+ {
+ if (!timed_out) // try again, soon
+ {
+ usleep(50);
+ continue;
+ }
+ fprintf(stderr, "Printer stopped responding!\n");
+ return EXIT_FAILURE;
+ }
+ if (status.err_info_1 || status.err_info_2)
+ {
+ fprintf(stderr, "Printer reported error(s): %s\n",
+ ql_decode_errors(&status));
+ return EXIT_FAILURE;
+ }
+ } while (status.status_type != QL_STATUS_TYPE_PRINTING_DONE);
+ alarm(0);
+
+ printf("%s (%ux%u) OK\n", argv[i], img->width, img->height);
+
+ free(img);
+ cfg.first_page = false;
+ }
+ }
+
+ ql_close(ctx);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/ql.c b/src/ql.c
new file mode 100644
index 0000000..f98a5f4
--- /dev/null
+++ b/src/ql.c
@@ -0,0 +1,333 @@
+/*
+ * Copyright 2017 DiUS Computing Pty Ltd. All rights reserved.
+ *
+ * Released under GPLv3, see LICENSE for details.
+ *
+ * @author Johny Mattsson <jmattsson@dius.com.au>
+ */
+#include "ql.h"
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct ql_ctx
+{
+ char *printer;
+ int fd;
+};
+
+#define ESC 0x1b
+
+#define NUM_STATUS_READ_RETRIES 100
+
+#define full_write(fd, buf) (retry_write(fd, buf, sizeof(buf)) == (ssize_t)sizeof(buf))
+
+ssize_t retry_write(int fd, const char *buf, size_t len)
+{
+ size_t written = 0;
+ while (written != len)
+ {
+ ssize_t n = write(fd, buf + written, len - written);
+ if (n == -1)
+ {
+ if (errno == EAGAIN || errno == EINTR)
+ continue;
+ else
+ break;
+ }
+ else
+ written += n;
+ }
+ return written;
+}
+
+
+ql_ctx_t ql_open(const char *printer)
+{
+ int fd = open(printer, O_RDWR);
+ if (fd < 0)
+ return NULL;
+
+ ql_ctx_t ctx = malloc(sizeof(struct ql_ctx));
+ if (!ctx)
+ return NULL;
+ ctx->printer = strdup(printer);
+ ctx->fd = fd;
+
+ const char clear[200] = { 0, };
+ (void)full_write(fd, clear); // recommended to clear old/errored jobs
+
+ return ctx;
+}
+
+void ql_close(ql_ctx_t ctx)
+{
+ free(ctx->printer);
+ close(ctx->fd);
+ free(ctx);
+}
+
+bool ql_init(ql_ctx_t ctx)
+{
+ const char init[] = { ESC, '@' };
+ return full_write(ctx->fd, init);
+}
+
+
+bool ql_request_status(ql_ctx_t ctx)
+{
+ const char status_req[] = { ESC, 'i', 'S' };
+ return full_write(ctx->fd, status_req);
+}
+
+
+bool ql_read_status(ql_ctx_t ctx, ql_status_t *status)
+{
+ for (int i = 0; i < NUM_STATUS_READ_RETRIES; ++i)
+ {
+ int ret = read(ctx->fd, status, sizeof(*status));
+ if (ret == 32)
+ return true;
+ else if ( (ret == 0) // "no data yet, too bad we just eof'd your fd, sucker"
+ || (ret == -1 && errno == EBADF)) // in case we messed up, somehow
+ {
+ close(ctx->fd);
+ ctx->fd = open(ctx->printer, O_RDWR);
+ if (ctx->fd < 0)
+ return false;
+ }
+ else if (ret == -1 && (errno != EAGAIN || errno != EINTR))
+ return false; // non-recoverable
+ }
+ errno = ETIME;
+ return false;
+}
+
+
+bool ql_set_mode(ql_ctx_t ctx, unsigned mode)
+{
+ char cmd[] = { ESC, 'i', 'M', mode };
+ return full_write(ctx->fd, cmd);
+}
+
+
+bool ql_set_expanded_mode(ql_ctx_t ctx, unsigned mode)
+{
+ char cmd[] = { ESC, 'i', 'K', mode };
+ return full_write(ctx->fd, cmd);
+}
+
+
+bool ql_set_autocut_every_n(ql_ctx_t ctx, uint8_t n)
+{
+ char cmd[] = { ESC, 'i', 'A', n };
+ return full_write(ctx->fd, cmd);
+}
+
+
+bool ql_set_margin(ql_ctx_t ctx, uint16_t dots)
+{
+ char cmd[] = { ESC, 'i', 'd', dots & 0xff, dots >> 8};
+ return full_write(ctx->fd, cmd);
+}
+
+
+bool ql_needs_mode_switch(const ql_status_t *status)
+{
+ switch(status->model_code)
+ {
+ case '3': case '4':
+ case 'P': case 'Q':
+ return true;
+ default: break;
+ }
+ return false;
+}
+
+
+bool ql_switch_to_raster_mode(ql_ctx_t ctx)
+{
+ #define MODE_ESC_P 0
+ #define MODE_RASTER 1
+ #define MODE_P_TOUCH_TEMPLATE 3
+ char cmd[] = { ESC, 'i', 'a', MODE_RASTER };
+ return full_write(ctx->fd, cmd);
+}
+
+
+static void pack_column(uint8_t *out, uint16_t bytes, uint16_t colno, const ql_raster_image_t *img, uint8_t black_below_v)
+{
+ for (unsigned n = 0; n < bytes; ++n, ++out)
+ {
+ *out = 0;
+ for (unsigned i = 0; i < 8; ++i)
+ {
+ unsigned img_row = n * 8 + i;
+ if (img_row < img->height)
+ if (img->data[img_row * img->width + colno] < black_below_v)
+ *out |= 1 << (7 - i);
+ }
+//for(int i = 7; i >= 0; --i) fprintf(stderr, "%c", (*out & (1<<i)) ? '#' : '.');
+ }
+//fprintf(stderr,"\n");
+}
+
+
+bool ql_print_raster_image(ql_ctx_t ctx, const ql_status_t *status, const ql_raster_image_t *img, const ql_print_cfg_t *cfg)
+{
+ unsigned dn = 90; // default raster transmission block size (720 pixels)
+ if (status->model_code == 'P' || status->model_code == '4')
+ dn = 162; // 1296 pixels
+
+ if (img->width > dn * 8)
+ return false; // image too wide for printer
+
+ char print_info[] = { ESC, 'i', 'z',
+ cfg->flags | 0x80,
+ (cfg->flags & QL_PRINT_CFG_MEDIA_TYPE) ? cfg->media_type : 0,
+ (cfg->flags & QL_PRINT_CFG_MEDIA_WIDTH) ? cfg->media_width : 0,
+ (cfg->flags & QL_PRINT_CFG_MEDIA_LENGTH) ? cfg->media_length : 0,
+ img->width & 0xff, img->width >> 8, 0, 0,
+ cfg->first_page ? 0 : 1, 0 };
+ if (!full_write(ctx->fd, print_info))
+ return false;
+
+ for (unsigned w = 0; w < img->width; ++w)
+ {
+ char block[dn + 3];
+ block[0] = 'g'; block[1] = 0; block[2] = dn;
+ pack_column((uint8_t *)block+3, dn, w, img, cfg->threshold);
+ if (!full_write(ctx->fd, block))
+ return false;
+ }
+
+ char done[] = { 0x1a }; // print with feeding
+ return full_write(ctx->fd, done);
+}
+
+
+const char *ql_decode_model(const ql_status_t *status)
+{
+ switch(status->model_code)
+ {
+ case '1': return "QL-560";
+ case '2': return "QL-570";
+ case '3': return "QL-580N";
+ case '4': return "QL-1060N";
+ case '5': return "QL-700";
+ case '6': return "QL-710W";
+ case '7': return "QL-720NW";
+ case 'O': return "QL-500/550";
+ case 'P': return "QL-1050";
+ case 'Q': return "QL-650TD";
+ default:
+ {
+ static char buf[] = "unrecognised (type code 0x$$)";
+ char *q = strchr(buf, '$');
+ sprintf(q, "%02hhx)", status->model_code);
+ return buf;
+ }
+ }
+}
+
+const char *ql_decode_mode(const ql_status_t *status)
+{
+ if (status->mode & QL_MODE_AUTOCUT)
+ return "auto-cut";
+ else
+ return "no-auto-cut";
+}
+
+const char *ql_decode_errors(const ql_status_t *status)
+{
+ typedef struct {
+ uint16_t bit;
+ const char *str;
+ } strmap_t;
+
+ #define ERR1(x) (x << 0)
+ #define ERR2(x) (x << 8)
+ const strmap_t strmap[] = {
+ { ERR1(QL_ERR_1_NO_MEDIA), "no-media " },
+ { ERR1(QL_ERR_1_END_OF_MEDIA), "end-of-media " },
+ { ERR1(QL_ERR_1_CUTTER_JAM), "cutter-jam " },
+ { ERR1(QL_ERR_1_PRINTER_IN_USE), "printer-in-use " },
+ { ERR1(QL_ERR_1_PRINTER_TURNED_OFF), "printer-turned-off " },
+ { ERR1(QL_ERR_1_HIGH_VOLTAGE_ADAPTER), "high-voltage-adapter " },
+ { ERR1(QL_ERR_1_FAN_MOTOR_ERROR), "fan-motor-error " },
+
+ { ERR2(QL_ERR_2_REPLACE_MEDIA), "replace-media " },
+ { ERR2(QL_ERR_2_EXPANSION_BUFFER_FULL), "expansion-buffer-full " },
+ { ERR2(QL_ERR_2_COMMUNICATION_ERROR), "communication-error " },
+ { ERR2(QL_ERR_2_COMMUNICATION_BUFFER_FULL), "communication-buffer-full " },
+ { ERR2(QL_ERR_2_COVER_OPEN), "cover-open " },
+ { ERR2(QL_ERR_2_CANCEL_KEY), "cancel-key-pressed " },
+ { ERR2(QL_ERR_2_MEDIA_CANNOT_BE_FED), "media-cannot-be-fed " },
+ { ERR2(QL_ERR_2_SYSTEM_ERROR), "system-error " }
+ };
+
+ static char *buf = 0;
+ if (!buf)
+ {
+ int len = 0;
+ for (unsigned i = 0; i < (sizeof(strmap)/sizeof(strmap[0])); ++i)
+ len += strlen(strmap[i].str);
+ buf = malloc(len + 1);
+ if (!buf)
+ return "<host-out-of-memory>";
+ }
+
+ uint16_t errs = ERR1(status->err_info_1) | ERR2(status->err_info_2);
+ buf[0] = 0;
+ for (unsigned i = 0; i < (sizeof(strmap)/sizeof(strmap[0])); ++i)
+ {
+ if (errs & strmap[i].bit)
+ strcat(buf, strmap[i].str);
+ }
+ return (buf[0] == 0) ? "none" : buf;
+}
+
+const char *ql_decode_media_type(const ql_status_t *status)
+{
+ switch(status->media_type)
+ {
+ case QL_MEDIA_TYPE_NO_MEDIA: return "no-media";
+ case QL_MEDIA_TYPE_CONTINUOUS:
+ case QL_MEDIA_TYPE_CONTINUOUS_ALT:
+ return "continuous-length-tape";
+ case QL_MEDIA_TYPE_DIECUT_LABELS:
+ case QL_MEDIA_TYPE_DIECUT_LABELS_ALT:
+ return "die-cut-labels";
+ default: {
+ static char buf[] = "unknown (code 0x$$)";
+ char *q = strchr(buf, '$');
+ sprintf(q, "%02hhx)", status->media_type);
+ return buf;
+ }
+ }
+}
+
+void ql_decode_print_status(FILE *f, const ql_status_t *status, unsigned flags)
+{
+ if (!status)
+ return;
+
+ const char *fmt_s = "%17s: %s\n";
+ const char *fmt_u = "%17s: %u\n";
+ if (flags & QL_DECODE_MODEL)
+ fprintf(f, fmt_s, "Printer", ql_decode_model(status));
+ if (flags & QL_DECODE_MODE)
+ fprintf(f, fmt_s, "Mode", ql_decode_mode(status));
+ if (flags & QL_DECODE_ERROR)
+ fprintf(f, fmt_s, "Errors", ql_decode_errors(status));
+ if (flags & QL_DECODE_MEDIA)
+ {
+ fprintf(f, fmt_s, "Media type", ql_decode_media_type(status));
+ fprintf(f, fmt_u, "Media width (mm)", status->media_width_mm);
+ if (status->media_type != QL_MEDIA_TYPE_CONTINUOUS)
+ fprintf(f, fmt_u, "Media length (mm)", status->media_length_mm);
+ }
+}
+