/* * Citizen CW-01 Photo Printer CUPS backend -- libusb-1.0 version * * (c) 2014-2016 Solomon Peachy * * The latest version of this program can be found at: * * http://git.shaftnet.org/cgit/selphy_print.git * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * [http://www.gnu.org/licenses/gpl-3.0.html] * */ #include #include #include #include #include #include #include #include #define BACKEND cw01_backend #include "backend_common.h" #define USB_VID_CITIZEN 0x1343 #define USB_PID_CITIZEN_CW01 0x0002 // Maybe others? //#define USB_PID_OLMEC_OP900 XXXX /* Private data stucture */ struct cw01_spool_hdr { uint8_t type; /* 0x00 -> 0x06 */ uint8_t res; /* vertical resolution; 0x00 == 334dpi, 0x01 == 600dpi */ uint8_t copies; /* number of prints */ uint8_t null0; uint32_t plane_len; /* LE */ uint8_t null1[4]; }; #define DPI_334 0 #define DPI_600 1 #define TYPE_DSC 0 #define TYPE_L 1 #define TYPE_PC 2 #define TYPE_2DSC 3 #define TYPE_3L 4 #define TYPE_A5 5 #define TYPE_A6 6 #define SPOOL_PLANE_HDR_LEN 1064 #define PRINTER_PLANE_HDR_LEN 1088 struct cw01_ctx { struct libusb_device_handle *dev; uint8_t endp_up; uint8_t endp_down; int type; uint8_t *databuf; struct cw01_spool_hdr hdr; }; struct cw01_cmd { uint8_t esc; /* Fixed at ascii ESC, aka 0x1B */ uint8_t p; /* Fixed at ascii 'P' aka 0x50 */ uint8_t arg1[6]; uint8_t arg2[16]; uint8_t arg3[8]; /* Decimal value of arg4's length, or empty */ uint8_t arg4[0]; /* Extra payload if arg3 is non-empty Doesn't have to be sent in the same URB */ /* All unused elements are set to 0x20 (ie ascii space) */ }; #define min(__x, __y) ((__x) < (__y)) ? __x : __y static void cw01_build_cmd(struct cw01_cmd *cmd, char *arg1, char *arg2, uint32_t arg3_len) { memset(cmd, 0x20, sizeof(*cmd)); cmd->esc = 0x1b; cmd->p = 0x50; memcpy(cmd->arg1, arg1, min(strlen(arg1), sizeof(cmd->arg1))); memcpy(cmd->arg2, arg2, min(strlen(arg2), sizeof(cmd->arg2))); if (arg3_len) { char buf[9]; snprintf(buf, sizeof(buf), "%08u", arg3_len); memcpy(cmd->arg3, buf, 8); } } static void cw01_cleanup_string(char *start, int len) { char *ptr = strchr(start, 0x0d); if (ptr && (ptr - start < len)) { *ptr = 0x00; /* If there is a , terminate there */ len = ptr - start; } else { start[--len] = 0x00; /* force null-termination */ } /* Trim trailing spaces */ while (len && start[len-1] == ' ') { start[--len] = 0; } } static char *cw01_media_types(char *str) { char tmp[4]; int i; memcpy(tmp, str + 4, 3); tmp[3] = 0; i = atoi(tmp); switch (i) { case 100: return "UNK 100"; case 110: return "UNK 110"; case 200: return "?? 5x3.5 (L)"; case 210: return "?? 5x7 (2L)"; case 300: return "?? 6x4 (PC)"; case 400: return "?? 6x9 (A5W)"; default: break; } return "Unknown type"; } static char *cw01_statuses(char *str) { char tmp[6]; int i; memcpy(tmp, str, 5); tmp[5] = 0; i = atoi(tmp); switch (i) { case 0: return "Idle"; case 1: return "Printing"; case 500: return "Cooling Print Head"; case 510: return "Cooling Paper Motor"; case 1000: return "Cover Open"; case 1010: return "No Scrap Box"; case 1100: return "Paper End"; case 1200: return "Ribbon End"; case 1300: return "Paper Jam"; case 1400: return "Ribbon Error"; case 1500: return "Paper Definition Error"; case 1600: return "Data Error"; case 2000: return "Head Voltage Error"; case 2100: return "Head Position Error"; case 2200: return "Power Supply Fan Error"; case 2300: return "Cutter Error"; case 2400: return "Pinch Roller Error"; case 2500: return "Abnormal Head Temperature"; case 2600: return "Abnormal Media Temperature"; case 2610: return "Abnormal Paper Motor Temperature"; case 2700: return "Ribbon Tension Error"; case 2800: return "RF-ID Module Error"; case 3000: return "System Error"; default: break; } return "Unkown Error"; } static int cw01_do_cmd(struct cw01_ctx *ctx, struct cw01_cmd *cmd, uint8_t *data, int len) { int ret; if ((ret = send_data(ctx->dev, ctx->endp_down, (uint8_t*)cmd, sizeof(*cmd)))) return ret; if (data && len) if ((ret = send_data(ctx->dev, ctx->endp_down, data, len))) return ret; return CUPS_BACKEND_OK; } static uint8_t *cw01_resp_cmd(struct cw01_ctx *ctx, struct cw01_cmd *cmd, int *len) { char tmp[9]; uint8_t *respbuf; int ret, i, num = 0; memset(tmp, 0, sizeof(tmp)); if ((ret = cw01_do_cmd(ctx, cmd, NULL, 0))) return NULL; /* Read in the response header */ ret = read_data(ctx->dev, ctx->endp_up, (uint8_t*)tmp, 8, &num); if (ret < 0) return NULL; if (num != 8) { ERROR("Short read! (%d/%d)\n", num, 8); return NULL; } i = atoi(tmp); /* Length of payload in bytes, possibly padded */ respbuf = malloc(i); if (!respbuf) { ERROR("Memory Allocation Failure!\n"); return NULL; } /* Read in the actual response */ ret = read_data(ctx->dev, ctx->endp_up, respbuf, i, &num); if (ret < 0) { free(respbuf); return NULL; } if (num != i) { ERROR("Short read! (%d/%d)\n", num, i); free(respbuf); return NULL; } *len = num; return respbuf; } static int cw01_query_serno(struct libusb_device_handle *dev, uint8_t endp_up, uint8_t endp_down, char *buf, int buf_len) { struct cw01_cmd cmd; uint8_t *resp; int len = 0; struct cw01_ctx ctx = { .dev = dev, .endp_up = endp_up, .endp_down = endp_down, }; /* Get Serial Number */ cw01_build_cmd(&cmd, "INFO", "SERIAL_NUMBER", 0); resp = cw01_resp_cmd(&ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); strncpy(buf, (char*)resp, buf_len); buf[buf_len-1] = 0; free(resp); return CUPS_BACKEND_OK; } static void *cw01_init(void) { struct cw01_ctx *ctx = malloc(sizeof(struct cw01_ctx)); if (!ctx) { ERROR("Memory Allocation Failure!\n"); return NULL; } memset(ctx, 0, sizeof(struct cw01_ctx)); return ctx; } static void cw01_attach(void *vctx, struct libusb_device_handle *dev, uint8_t endp_up, uint8_t endp_down, uint8_t jobid) { struct cw01_ctx *ctx = vctx; struct libusb_device *device; struct libusb_device_descriptor desc; UNUSED(jobid); ctx->dev = dev; ctx->endp_up = endp_up; ctx->endp_down = endp_down; device = libusb_get_device(dev); libusb_get_device_descriptor(device, &desc); ctx->type = lookup_printer_type(&cw01_backend, desc.idVendor, desc.idProduct); } static void cw01_teardown(void *vctx) { struct cw01_ctx *ctx = vctx; if (!ctx) return; if (ctx->databuf) free(ctx->databuf); free(ctx); } static int cw01_read_parse(void *vctx, int data_fd) { struct cw01_ctx *ctx = vctx; int i, j, remain; if (!ctx) return CUPS_BACKEND_FAILED; if (ctx->databuf) { free(ctx->databuf); ctx->databuf = NULL; } i = read(data_fd, (uint8_t*) &ctx->hdr, sizeof(struct cw01_spool_hdr)); if (i < 0) return i; if (i == 0) return CUPS_BACKEND_CANCEL; if (i < (int)sizeof(struct cw01_spool_hdr)) return CUPS_BACKEND_CANCEL; if (ctx->hdr.type > 0x06 || ctx->hdr.res > 0x01) { ERROR("Unrecognized header data format!\n"); return CUPS_BACKEND_CANCEL; } ctx->hdr.plane_len = le32_to_cpu(ctx->hdr.plane_len); remain = ctx->hdr.plane_len * 3; ctx->databuf = malloc(remain); if (!ctx->databuf) { ERROR("Memory allocation failure!\n"); return CUPS_BACKEND_CANCEL; } j = 0; while (remain) { i = read(data_fd, ctx->databuf + j, remain); if (i < 0) return i; remain -= i; j += i; } return CUPS_BACKEND_OK; } static int cw01_main_loop(void *vctx, int copies) { struct cw01_ctx *ctx = vctx; int ret; struct cw01_cmd cmd; uint8_t *resp = NULL; int len = 0; uint32_t tmp; uint8_t *ptr; char buf[9]; uint8_t plane_hdr[PRINTER_PLANE_HDR_LEN]; if (!ctx) return CUPS_BACKEND_FAILED; top: if (resp) free(resp); /* Query status */ cw01_build_cmd(&cmd, "STATUS", "", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); /* If we're not idle */ if (strcmp("00000", (char*)resp)) { if (!strcmp("00001", (char*)resp)) { free(resp); /* Query buffer state */ cw01_build_cmd(&cmd, "INFO", "FREE_PBUFFER", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); /* Check to see if we have sufficient buffers */ // XXX audit these rules...? if (!strcmp("FBP00", (char*)resp) || (ctx->hdr.res == DPI_600 && !strcmp("FBP01", (char*)resp))) { INFO("Insufficient printer buffers, retrying...\n"); sleep(1); goto top; } } else { ERROR("Printer Status: %s\n", cw01_statuses((char*)resp)); free(resp); return CUPS_BACKEND_RETRY_CURRENT; } } free(resp); /* Get Vertical resolution */ cw01_build_cmd(&cmd, "INFO", "RESOLUTION_V", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); #if 0 if (ctx->hdr.res == DPI_600 && strcmp("RV0334", *char*)resp) { ERROR("600DPI prints not yet supported, need 600DPI CWD load"); return CUPS_BACKEND_CANCEL; } #endif free(resp); resp = NULL; /* Set print quantity */ // XXX check against remaining print count cw01_build_cmd(&cmd, "CNTRL", "QTY", 8); snprintf(buf, sizeof(buf), "%07d\r", copies); ret = cw01_do_cmd(ctx, &cmd, (uint8_t*) buf, 8); if (ret) return CUPS_BACKEND_FAILED; /* Cutter control. ??? */ // cw01_build_cmd(&cmd, "CNTRL", "CUTTER", 8); //snprintf(buf, sizeof(buf), "%08d", ???); //ret = cw01_do_cmd(ctx, &cmd, (uint8_t*) buf, 8); //if (ret) // return CUPS_BACKEND_FAILED; /* Start sending image data */ ptr = ctx->databuf; /* Generate plane header (same for all planes) */ tmp = cpu_to_le32(ctx->hdr.plane_len) + 24; memset(plane_hdr, 0, PRINTER_PLANE_HDR_LEN); plane_hdr[0] = 0x42; plane_hdr[1] = 0x4d; memcpy(plane_hdr + 2, &tmp, sizeof(tmp)); plane_hdr[10] = 0x40; plane_hdr[11] = 0x04; memcpy(plane_hdr + 14, ptr, SPOOL_PLANE_HDR_LEN); /******** Plane 1 */ cw01_build_cmd(&cmd, "IMAGE", "YPLANE", ctx->hdr.plane_len - SPOOL_PLANE_HDR_LEN + PRINTER_PLANE_HDR_LEN); ret = cw01_do_cmd(ctx, &cmd, plane_hdr, PRINTER_PLANE_HDR_LEN); if (ret) return CUPS_BACKEND_FAILED; /* Send plane data */ if ((ret = send_data(ctx->dev, ctx->endp_down, ptr + SPOOL_PLANE_HDR_LEN, ctx->hdr.plane_len - SPOOL_PLANE_HDR_LEN))) return CUPS_BACKEND_FAILED; ptr += ctx->hdr.plane_len; /******** Plane 2 */ cw01_build_cmd(&cmd, "IMAGE", "MPLANE", ctx->hdr.plane_len - SPOOL_PLANE_HDR_LEN + PRINTER_PLANE_HDR_LEN); ret = cw01_do_cmd(ctx, &cmd, plane_hdr, PRINTER_PLANE_HDR_LEN); if (ret) return CUPS_BACKEND_FAILED; /* Send plane data */ if ((ret = send_data(ctx->dev, ctx->endp_down, ptr + SPOOL_PLANE_HDR_LEN, ctx->hdr.plane_len - SPOOL_PLANE_HDR_LEN))) return CUPS_BACKEND_FAILED; ptr += ctx->hdr.plane_len; /******** Plane 3 */ cw01_build_cmd(&cmd, "IMAGE", "CPLANE", ctx->hdr.plane_len - SPOOL_PLANE_HDR_LEN + PRINTER_PLANE_HDR_LEN); ret = cw01_do_cmd(ctx, &cmd, plane_hdr, PRINTER_PLANE_HDR_LEN); if (ret) return CUPS_BACKEND_FAILED; /* Send plane data */ if ((ret = send_data(ctx->dev, ctx->endp_down, ptr + SPOOL_PLANE_HDR_LEN, ctx->hdr.plane_len - SPOOL_PLANE_HDR_LEN))) return CUPS_BACKEND_FAILED; /* ptr += ctx->hdr.plane_len; */ /* Start print */ cw01_build_cmd(&cmd, "CNTRL", "START", 0); ret = cw01_do_cmd(ctx, &cmd, NULL, 0); if (ret) return CUPS_BACKEND_FAILED; INFO("Print complete\n"); if (resp) free(resp); return CUPS_BACKEND_OK; } static int cw01_get_info(struct cw01_ctx *ctx) { struct cw01_cmd cmd; uint8_t *resp; int len = 0; /* Get Serial Number */ cw01_build_cmd(&cmd, "INFO", "SERIAL_NUMBER", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Serial Number: '%s'\n", (char*)resp); free(resp); /* Get Firmware Version */ cw01_build_cmd(&cmd, "INFO", "FVER", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Firmware Version: '%s'\n", (char*)resp); free(resp); /* Get Sensor Info */ cw01_build_cmd(&cmd, "INFO", "SENSOR", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Sensor Info: '%s'\n", (char*)resp); // XXX parse this out. Each token is 'XXX-###' delimited by '; ' free(resp); /* Get Horizonal resolution */ cw01_build_cmd(&cmd, "INFO", "RESOLUTION_H", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Horizontal Resolution: '%s' dpi\n", (char*)resp + 3); free(resp); /* Get Vertical resolution */ cw01_build_cmd(&cmd, "INFO", "RESOLUTION_V", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Vertical Resolution: '%s' dpi\n", (char*)resp + 3); free(resp); /* Get Media Color offset */ cw01_build_cmd(&cmd, "INFO", "MCOLOR", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Media Color Offset: '%02x%02x%02x%02x'\n", *(resp+2), *(resp+3), *(resp+4), *(resp+5)); free(resp); /* Get Media Lot */ cw01_build_cmd(&cmd, "INFO", "MLOT", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Media Lot Code: '%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x'\n", *(resp+2), *(resp+3), *(resp+4), *(resp+5), *(resp+6), *(resp+7), *(resp+8), *(resp+9), *(resp+10), *(resp+11), *(resp+12), *(resp+13)); free(resp); /* Get Media ID Set (?) */ cw01_build_cmd(&cmd, "MNT_RD", "MEDIA_ID_SET", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Media ID(?): '%s'\n", (char*)resp+4); free(resp); /* Get Color Control Data Version */ cw01_build_cmd(&cmd, "TBL_RD", "Version", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Color Data Version: '%s'\n", (char*)resp); free(resp); /* Get Color Control Data Checksum */ cw01_build_cmd(&cmd, "MNT_RD", "CTRLD_CHKSUM", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Color Data Checksum: '%s'\n", (char*)resp); free(resp); return CUPS_BACKEND_OK; } static int cw01_get_status(struct cw01_ctx *ctx) { struct cw01_cmd cmd; uint8_t *resp; int len = 0; /* Generate command */ cw01_build_cmd(&cmd, "STATUS", "", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Printer Status: %s => %s\n", (char*)resp, cw01_statuses((char*)resp)); free(resp); /* Get remaining prints in this job */ cw01_build_cmd(&cmd, "INFO", "PQTY", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Prints Remaining in job: '%s'\n", (char*)resp + 4); free(resp); /* Generate command */ cw01_build_cmd(&cmd, "INFO", "FREE_PBUFFER", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Free Buffers: '%s'\n", (char*)resp + 3); free(resp); /* Get Media Info */ cw01_build_cmd(&cmd, "INFO", "MEDIA", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Media Type: '%s'\n", cw01_media_types((char*)resp)); free(resp); /* Get Media remaining */ cw01_build_cmd(&cmd, "INFO", "MQTY", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Prints Remaining: '%s'\n", (char*)resp + 4); free(resp); return 0; } static int cw01_get_counters(struct cw01_ctx *ctx) { struct cw01_cmd cmd; uint8_t *resp; int len = 0; /* Generate command */ cw01_build_cmd(&cmd, "MNT_RD", "COUNTER_LIFE", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("Lifetime Counter: '%s'\n", (char*)resp+2); free(resp); /* Generate command */ cw01_build_cmd(&cmd, "MNT_RD", "COUNTER_A", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("A Counter: '%s'\n", (char*)resp+2); free(resp); /* Generate command */ cw01_build_cmd(&cmd, "MNT_RD", "COUNTER_B", 0); resp = cw01_resp_cmd(ctx, &cmd, &len); if (!resp) return CUPS_BACKEND_FAILED; cw01_cleanup_string((char*)resp, len); INFO("B Counter: '%s'\n", (char*)resp+2); free(resp); return CUPS_BACKEND_OK; } static int cw01_clear_counter(struct cw01_ctx *ctx, char counter) { struct cw01_cmd cmd; char msg[4]; int ret; /* Generate command */ cw01_build_cmd(&cmd, "MNT_WT", "COUNTER_CLEAR", 4); msg[0] = 'C'; msg[1] = counter; msg[2] = 0x0d; /* ie carriage return, ASCII '\r' */ msg[3] = 0x00; if ((ret = cw01_do_cmd(ctx, &cmd, (uint8_t*)msg, 4))) return ret; return 0; } static void cw01_cmdline(void) { DEBUG("\t\t[ -i ] # Query printer info\n"); DEBUG("\t\t[ -s ] # Query status\n"); DEBUG("\t\t[ -n ] # Query counters\n"); DEBUG("\t\t[ -N A|B|M ] # Clear counter A/B/M\n"); } static int cw01_cmdline_arg(void *vctx, int argc, char **argv) { struct cw01_ctx *ctx = vctx; int i, j = 0; if (!ctx) return -1; while ((i = getopt(argc, argv, GETOPT_LIST_GLOBAL "inN:s")) >= 0) { switch(i) { GETOPT_PROCESS_GLOBAL case 'i': j = cw01_get_info(ctx); break; case 'n': j = cw01_get_counters(ctx); break; case 'N': if (optarg[0] != 'A' && optarg[0] != 'B') return CUPS_BACKEND_FAILED; j = cw01_clear_counter(ctx, optarg[0]); break; case 's': j = cw01_get_status(ctx); break; default: break; /* Ignore completely */ } if (j) return j; } return 0; } /* Exported */ struct dyesub_backend cw01_backend = { .name = "Citizen CW-01", .version = "0.12", .uri_prefix = "citizencw01", .cmdline_usage = cw01_cmdline, .cmdline_arg = cw01_cmdline_arg, .init = cw01_init, .attach = cw01_attach, .teardown = cw01_teardown, .read_parse = cw01_read_parse, .main_loop = cw01_main_loop, .query_serno = cw01_query_serno, .devices = { { USB_VID_CITIZEN, USB_PID_CITIZEN_CW01, P_CITIZEN_CW01, ""}, // { USB_VID_CITIZEN, USB_PID_OLMEC_OP900, P_CITIZEN_CW01, ""}, { 0, 0, 0, ""} } }; /* Basic spool file format: TT RR NN 00 XX XX XX XX 00 00 00 00 <- FILE header. NN : copies (0x01 or more) RR : resolution; 0 == 334 dpi, 1 == 600dpi TT : type 0x02 == 4x6, 0x01 == 5x3.5 XX XX XX XX : plane length (LE) plane length * 3 + 12 == file length. Followed by three planes, each with this header: 28 00 00 00 00 08 00 00 RR RR 00 00 01 00 08 00 00 00 00 00 00 00 00 00 5a 33 00 00 YY YY 00 00 00 01 00 00 00 00 00 00 RR RR : rows in LE format YY YY : 0x335a (334dpi) or 0x5c40 (600dpi) Followed by 1024 bytes of color tables: ff ff ff 00 ... 00 00 00 00 1024+40 = 1064 bytes of header per plane. Always have 2048 columns of data. followed by (2048 * rows) bytes of data. */