From 8c849b650ed534362b107e5ac4d12fdee81908e1 Mon Sep 17 00:00:00 2001 From: David Lamparter Date: Thu, 28 Nov 2013 20:13:35 +0100 Subject: initial code --- .gitignore | 1 + ptgui.glade | 214 +++++++++++++++++++++++++++++++++++++++++ ptgui.py | 233 +++++++++++++++++++++++++++++++++++++++++++++ ptlayout.py | 170 +++++++++++++++++++++++++++++++++ pttarget.py | 308 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 926 insertions(+) create mode 100644 .gitignore create mode 100644 ptgui.glade create mode 100644 ptgui.py create mode 100644 ptlayout.py create mode 100644 pttarget.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/ptgui.glade b/ptgui.glade new file mode 100644 index 0000000..f1fc0fa --- /dev/null +++ b/ptgui.glade @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + False + + + True + False + vertical + + + True + False + devlist + + + + 0 + + + + + False + True + 0 + + + + + True + False + + + True + False + 0 + 5 + + + True + True + 3 + 0 + + + + + gtk-print + True + True + True + True + + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 5 + + + True + True + never + in + + + True + False + + + 128 + True + False + + + + + + + True + True + 0 + + + + + True + False + vertical + + + True + True + True + Text Color + #000000000000 + + + + False + True + 5 + 0 + + + + + True + True + True + Background Color + #ffffffffffff + + + + False + True + 5 + 1 + + + + + False + True + 1 + + + + + False + True + 2 + + + + + True + True + in + + + True + False + + + True + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + 3 + + + + + + diff --git a/ptgui.py b/ptgui.py new file mode 100644 index 0000000..032dbe2 --- /dev/null +++ b/ptgui.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python +# vim: set expandtab ts=4: + +from gi.repository import GLib, Gtk +import cairo, pango, pangocairo +ml = GLib.MainLoop() +import pttarget, ptlayout +from ptlayout import PTLText +import time + +class MainWindow(object): + def __init__(self): + builder = Gtk.Builder() + builder.add_from_file('ptgui.glade') + builder.connect_signals(self) + + self.tape = None + self.tgtwidth = 192 + + self.wnd = builder.get_object('mainwnd') + self.wnd.connect("delete-event", Gtk.main_quit) + self.statuslabel = builder.get_object('statuslabel') + self.ctlgrid = builder.get_object('ctlgrid') + self.printbtn = builder.get_object('printbtn') + self.color_fg = builder.get_object('color_fg') + self.color_bg = builder.get_object('color_bg') + + self.layout = ptlayout.PTLHSeq() + self.layout.spacing = 0 + # vs1 = ptlayout.PTLVStack() + # vs1.add(PTLText()) + # vs1.add(PTLText()) + # vs2 = ptlayout.PTLVStack() + # vs2.add(PTLText()) + # vs2.add(PTLText()) + t = PTLText() + t.font = 'FrutigerNextLT Medium 20' + t.mode = 'left' + self.layout.add(t) + t = PTLText() + t.font = 'FrutigerNextLT Heavy 60' + t.mode = 'left' + self.layout.add(t) + + self.layout.add(PTLText()) + + t = PTLText() + t.font = 'FrutigerNextLT Heavy 60' + t.mode = 'right' + self.layout.add(t) + t = PTLText() + t.font = 'FrutigerNextLT Medium 20' + t.mode = 'right' + self.layout.add(t) + self.layout.add(PTLText()) + self.layout_put_controls() + + self.img = builder.get_object('resultimg') + self.sizing_apply() + self.img.connect('draw', self.draw) + # help(self.img) + + self.devlist = Gtk.ListStore(str, object) + self.devbox = builder.get_object('devbox') + self.devbox.set_model(self.devlist) + self.scan() + self.dev_select() + self.display_refresh() + self.wnd.show_all() + + GLib.timeout_add(100, self.status_update) + + def scan(self): + pttarget.PTUSB.scan() + for dev in pttarget.PTUSB.printers: + print str(dev) + self.devlist.append([str(dev), dev]) + + if len(self.devlist) > 0: + itr = self.devlist.get_iter((0, )) + self.devbox.set_active_iter(itr) + + def dev_select(self): + itr = self.devbox.get_active_iter() + dev = self.devlist[itr][1] + dev.setup() + self.status_update() + + def status_update(self): + itr = self.devbox.get_active_iter() + dev = self.devlist[itr][1] + + if dev.last_status is None or time.time() - dev.last_status >= 1: + dev.refresh_status() + else: + dev.check_status() + + if dev.err1 == 0 and dev.err2 == 0: + self.printbtn.set_sensitive(True) + if dev.find_tape() != self.tape: + self.tape = dev.find_tape() + self.sizing_apply() + self.layout_update() + else: + self.printbtn.set_sensitive(False) + + mtype = pttarget.media_types.get(dev.media_type, 'unknown (%02x)' % (dev.media_type)) + msize = '%dx%dmm' % (dev.media_width, dev.media_length) if dev.media_length != 0 else \ + '%dmm continuous tape' % (dev.media_width) + self.statuslabel.set_text('Status: %s, %s %s' % (dev.get_errstr(), mtype, msize)) + return True + + def sizing_apply(self): + if self.tape is None: + self.label_surface = None + tapeh = 16 + else: + self.label_surface = cairo.ImageSurface(cairo.FORMAT_A1, self.tgtwidth, self.tape.pixels) + tapeh = self.tape.pixels + + self.display_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, + self.tgtwidth + 40, tapeh + 40) + self.img.set_size_request(self.display_surface.get_width(), + self.display_surface.get_height()) + + #ctx = cairo.Context(self.label_surface) + # for i in range(0, 128 / 16): + # x = 16 * (i & 1) + # ctx.rectangle(x, i * 16, 12, 12) + # ctx.fill() + # for i in range(0, 128 / 16): + # x = 32 + 16 * (i & 1) + # ctx.rectangle(x, i * 16, 12, 12) + # ctx.fill() + + # pctx = pangocairo.CairoContext(ctx) + # font = pango.FontDescription('Delicious 24') + # layout = pctx.create_layout() + # layout.set_font_description(font) + # layout.set_text(u'Hello') + # pctx.update_layout(layout) + # pctx.show_layout(layout) + + # self.display_refresh() + + def display_refresh(self, *args): + ctx = cairo.Context(self.display_surface) + w = self.display_surface.get_width() + h = self.display_surface.get_height() + for x in range(0, w, 16): + for y in range(0, h, 16): + col = 0.3 + 0.1 * (((x + y) / 16) & 1) + ctx.rectangle(x, y, 16, 16) + ctx.set_source_rgb(col, col, col) + ctx.fill() + + if self.label_surface is None: + return + + pw = self.label_surface.get_width() + ph = self.label_surface.get_height() + px, py = (w - pw) / 2, (h - ph) / 2 + ctx.rectangle(px, py, pw, ph) + bg = self.color_bg.get_rgba() + ctx.set_source_rgb(bg.red, bg.green, bg.blue) + ctx.fill() + + fg = self.color_fg.get_rgba() + ctx.set_source_rgb(fg.red, fg.green, fg.blue) + ctx.mask_surface(self.label_surface, px, py) + + self.img.queue_draw() + + def draw(self, widget, ctx): + ctx.set_source_surface(self.display_surface) + w = self.display_surface.get_width() + h = self.display_surface.get_height() + ctx.rectangle(0, 0, w, h) + ctx.fill() + + def layout_update(self): + w, h = self.layout.prep_size(self.label_surface.get_height()) + if w < 32: + w = 32 + if w != self.tgtwidth: + self.tgtwidth = w + self.sizing_apply() + + if self.label_surface is None: + return + + self.label_surface = cairo.ImageSurface(cairo.FORMAT_A1, self.tgtwidth, self.tape.pixels) + ctx = cairo.Context(self.label_surface) + ctx.set_operator(cairo.OPERATOR_SOURCE) + finw = self.layout.render(ctx, self.label_surface.get_height()) + self.display_refresh() + + def layout_control(self, node, name, spec): + elem = Gtk.Entry() + buf = elem.get_buffer() + def update(buf, *args): + node.__dict__[name] = buf.get_text() + self.layout_update() + + buf.set_text(node.__dict__[name], -1) + buf.connect('deleted-text', update) + buf.connect('inserted-text', update) + return elem + + def layout_put_controls(self): + nodes = [self.layout] + while len(nodes) > 0: + node = nodes.pop(0) + nodes.extend(reversed(node.children())) + for p in reversed(node.properties()): + elem = self.layout_control(node, p[0], p[1]) + self.ctlgrid.insert_row(0) + self.ctlgrid.attach(Gtk.Label(p[0]), 0, 0, 1, 1) + self.ctlgrid.attach(elem, 1, 0, 1, 1) + + def on_print(self, *args): + if self.label_surface is None: + return + + data = pttarget.PTLabelData() + data.addcairo(self.label_surface, offset = self.tape.offset) + itr = self.devbox.get_active_iter() + dev = self.devlist[itr][1] + dev.send(data.get()) + +mw = MainWindow() +Gtk.main() +# ml.run() diff --git a/ptlayout.py b/ptlayout.py new file mode 100644 index 0000000..f9518b7 --- /dev/null +++ b/ptlayout.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# vim: set expandtab ts=4: + +import cairo, pango, pangocairo +import qrcode +from math import pi + +dummysurface = cairo.ImageSurface(cairo.FORMAT_A1, 1, 1) +dummyctx = cairo.Context(dummysurface) + +class PTLayoutElem(object): + def __init__(self): + super(PTLayoutElem, self).__init__() + + def prep_size(self, h): + return (0, 0) + + def render(self, cctx, h): + return 0 + + def children(self): + return [] + + def properties(self): + return [] + +class PTLText(PTLayoutElem): + def __init__(self): + super(PTLText, self).__init__() + self.text = '' + self.font = '' + self.mode = '' + + def properties(self): + return [ + ('text', 'text'), + ('font', 'text'), + ('mode', 'text'), + ] + + def prep_size(self, h): + return self._render(dummyctx, True, h) + + def render(self, cctx, h): + return self._render(cctx, False, h) + + def _render(self, cctx, sizeonly, h): + pctx = pangocairo.CairoContext(cctx) + font = pango.FontDescription(self.font if self.font != '' else 'Delicious 24') + layout = pctx.create_layout() + layout.set_font_description(font) + layout.set_text(unicode(self.text)) + size = layout.get_pixel_size() + if sizeonly: + if self.mode in ['left', 'right']: + return (size[1], size[0]) + return size + w = size[0] + + matrix = cctx.get_matrix() + if self.mode == 'left': + cctx.rotate(90. * pi / 180.) + cctx.translate((h - size[0]) / 2, -size[1]) + w = size[1] + elif self.mode == 'right': + cctx.rotate(-90. * pi / 180.) + cctx.translate(-h + (h - size[0]) / 2, 0) + w = size[1] + + pctx.update_layout(layout) + pctx.show_layout(layout) + + cctx.set_matrix(matrix) + return w + +class PTLQRCode(PTLayoutElem): + def __init__(self): + super(PTLQRCode, self).__init__() + self.qrcontent = '' + self.hborder = 4 + self.invert = True + + def properties(self): + return [ + ('qrcontent', 'text'), + ] + + def prep_size(self, h): + return self._render(dummyctx, True, h) + + def render(self, cctx, h): + return self._render(cctx, False, h) + + def _render(self, cctx, sizeonly, h): + qr = qrcode.QRCode(border = 0) + qr.add_data(self.qrcontent) + qr.make(fit = True) + qm = qr.get_matrix() + qmlen = len(qm) + bpp = h / qmlen + if sizeonly: return (bpp * qmlen + self.hborder * 2, bpp * qmlen) + + if self.invert: + cctx.rectangle(0, 0, bpp * qmlen + self.hborder * 2, h) + cctx.fill() + cctx.set_source_rgba(1.0, 1.0, 1.0, 0.0) + + yoffs = (h - bpp * qmlen) / 2 + for y, line in enumerate(qm): + for x, dot in enumerate(line): + if dot: + cctx.rectangle(bpp * x + self.hborder, bpp * y + yoffs, bpp, bpp) + cctx.fill() + + cctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) + return bpp * qmlen + +class PTLContainer(PTLayoutElem): + def __init__(self): + super(PTLContainer, self).__init__() + self._children = [] + + def children(self): + return self._children + + def add(self, child): + self._children.append(child) + +class PTLHSeq(PTLContainer): + def __init__(self): + super(PTLHSeq, self).__init__() + self.spacing = 5 + + def prep_size(self, hh): + w, h = len(self._children) * self.spacing, 0 + for k in self._children: + cw, ch = k.prep_size(hh) + w += cw + h = max(h, ch) + return (w, h) + + def render(self, cctx, h): + wpos = 0 + for k in self._children: + cw = k.render(cctx, h) + wpos += cw + self.spacing + cctx.translate(cw + self.spacing, 0) + cctx.translate(-wpos, 0) + return wpos - self.spacing + +class PTLVStack(PTLContainer): + def __init__(self): + super(PTLVStack, self).__init__() + + def prep_size(self, hh): + w = 0 + for k in self._children: + cw, ch = k.prep_size(hh / len(self._children)) + w = max(w, cw) + return (w, hh) + + def render(self, cctx, h): + w = 0 + for i, k in enumerate(self._children): + vpos = int(h / float(len(self._children)) * i) + cctx.translate(0, vpos) + cw = k.render(cctx, h) + w = max(cw, w) + cctx.translate(0, -vpos) + return w diff --git a/pttarget.py b/pttarget.py new file mode 100644 index 0000000..d8f9eb2 --- /dev/null +++ b/pttarget.py @@ -0,0 +1,308 @@ +# import cups +import usb.core, usb.util + +import cairo, pango, pangocairo +import tempfile, os, struct, sys, array, time +import qrcode + +class PTPrinter(object): + def __init__(self): + pass + def setup(self): + pass + def is_interactive(self): + return False + +media_types = { + 0x01: 'Laminated', + 0x02: 'Lettering', + 0x03: 'Non-laminated', + 0x08: 'AV tape', + 0x09: 'HG tape', + 0x0a: 'Paper tape, white', + 0x0b: 'Address labels', +} +codes_err1 = { + 0x01: 'No media', + 0x02: 'End of media', + 0x04: 'Cutter jammed', +} +codes_err2 = { + 0x01: 'Replace media', + 0x02: 'Expansion buffer full', + 0x04: 'Transmission error', + 0x08: 'Transmission buffer full', + 0x10: 'Cover open', +} + +class PTUSB(PTPrinter): + def __init__(self, usbdev): + self.usbdev = usbdev + self.last_status = None + + def __str__(self): + return '%s %s (%04x:%04x @ %d.%d)' % ( + usb.util.get_string(self.usbdev, 256, self.usbdev.iManufacturer), + usb.util.get_string(self.usbdev, 256, self.usbdev.iProduct), + self.usbdev.idVendor, self.usbdev.idProduct, + self.usbdev.bus, self.usbdev.address) + + def setup(self): + for cfg in self.usbdev: + iface = usb.util.find_descriptor(cfg, bInterfaceClass = 7) + if iface is not None: + cfg.set() + break + else: + raise RuntimeError, 'no configuration/interface found' + usb.util.claim_interface(self.usbdev, iface) + iface.set_altsetting() + self.iface = iface + self.if_rd = iface[0] + self.if_wr = iface[1] + + self.if_wr.write(64*'\x00') + self.if_wr.write(64*'\x00') + self.if_wr.write(64*'\x00') + self.if_wr.write(8*'\x00') + self.if_wr.write('\x1b@') + + self.refresh_status() + + def refresh_status(self, data = None): + if data is None: + self.if_wr.write('\x1biS') + ar = array.array('B') + start = time.time() + while len(ar) < 32 or time.time() > start + 0.1: + ar += self.if_rd.read(16) + if len(ar) != 32: + raise IOError, 'failed to get status' + if ar[0] != 0x80 or ar[1] != 32: + raise IOError, 'invalid status (hdr: %02x %02x)' % (ar[0], ar[1]) + + self.err1 = ar[8] + self.err2 = ar[9] + self.media_width = ar[10] + self.media_type = ar[11] + self.media_length = ar[17] + self.status_type = ar[18] + self.phase_type = ar[19] + self.phase_bytes = ar[20] * 256 + ar[21] + self.notify_num = ar[22] + self.last_status = time.time() + + def find_tape(self): + for tape in PTTape.tapes: + if int(tape.sizemm) == self.media_width: + return tape + return None + + def check_status(self): + ar = self.if_rd.read(16) + if len(ar) == 0: + return + start = time.time() + while len(ar) < 32 or time.time() > start + 0.1: + ar += self.if_rd.read(16) + self.refresh_status(ar) + + def get_errstr(self): + errs = [] + for i in range(0, 8): + if self.err1 & (1 << i): + errs.append(codes_err1.get(1 << i, '<1:%02x>' % (1 << i))) + for i in range(0, 8): + if self.err2 & (1 << i): + errs.append(codes_err2.get(1 << i, '<2:%02x>' % (1 << i))) + if len(errs) == 0: + return 'OK' + return ', '.join(errs) + + def send(self, data): + offset = 0 + size = len(data) + while offset < size: + wsize = self.if_wr.wMaxPacketSize + if wsize > size - offset: + wsize = size - offset + written = self.if_wr.write(data[offset:offset+wsize]) + if written <= 0: + raise IOError, 'failed to write' + offset += written + + @classmethod + def scan(klass): + klass.printers = [] + + def match(dev): + if dev.idVendor != 0x04f9: return False + for cfg in dev: + if usb.util.find_descriptor(cfg, bInterfaceClass = 7) is not None: + return True + return False + + printers = usb.core.find(find_all = True, custom_match = match) + for printer in printers: + klass.printers.append(klass(printer)) + +# class PTCups(PTPrinter): +# try: +# conn = cups.Connection() +# except RuntimeError: +# conn = None +# +# def __init__(self, pname, printer): +# self.pname = pname +# self.printer = printer +# +# def __str__(self): +# return '%s (%s)' % (self.pname, self.printer['device-uri']) +# +# def send(self, data): +# fd, fn = tempfile.mkstemp() +# os.fdopen(fd, 'w').write(data) +# PTCups.conn.printFile(self.pname, fn, 'label', {}) +# os.unlink(fn) +# +# @classmethod +# def scan(klass): +# printers = klass.conn.getPrinters() +# +# klass.printers = [] +# for printer in printers: +# devuri = printers[printer]["device-uri"] +# if devuri.startswith('usb://Brother/PT'): +# klass.printers.append(klass(printer, printers[printer])) + +class PTLabelData(object): + def __init__(self): + # margins: '\x1bid\x00\x00' + + self.init = '\x1biR\x01' + '\x1biM\x00' + self.raw = '' + # self.fini = '\x1a' # Z / feed ? + self.fini = '\x0c' + + def addpixels(self, raw): + row = 'G' + struct.pack(' 128: + pt_height = 128 + # pt_bytes = (pt_height + 7) / 8 + pt_bytes = 128 / 8 + + for x in range(0, surface.get_width()): + pixels = bytearray(pt_bytes * '\x00') + for y in range(0, pt_height - offset): + py = y + offset + cbit = ord(data[y * stride + (x / 8)]) \ + & (1 << (x & 7)) + if cbit != 0: + pixels[py / 8] |= 1 << (7 - (py & 7)) + self.addpixels(pixels) + + def get(self): + return self.init + self.raw + self.fini + +class PTTape(object): + tapes = [] + def __init__(self, sizemm, pixels, offset): + self.sizemm = sizemm + self.pixels = pixels + self.offset = offset + PTTape.tapes.append(self) + +PTTape(24.0, 128, 0) +PTTape(12.0, 83, 22) +PTTape(9.0, 63, 34) + +if __name__ == '__main__': + klass = PTUSB + klass.scan() + for p in klass.printers: + print p + p.setup() + print 'Status: ', p.get_errstr() + print 'Media: ', \ + media_types.get(p.media_type, 'unknown (%02x)' % (p.media_type)), \ + '%dx%dmm' % (p.media_width, p.media_length) if p.media_length != 0 else \ + '%dmm continuous tape' % (p.media_width) + + data = PTLabelData() + tape = PTTape.tapes[2] + + sys.exit(0) + + surface = cairo.ImageSurface(cairo.FORMAT_A1, 80, tape.pixels) + + ctx = cairo.Context(surface) + # ctx.translate(0, 64) + pctx = pangocairo.CairoContext(ctx) + + #for i in range(0, 128 / 16): + # x = 16 * (i & 1) + # ctx.rectangle(x, i * 16, 16, 16) + # ctx.fill() + #for i in range(0, 128 / 16): + # x = 32 + 16 * (i & 1) + # ctx.rectangle(x, i * 16, 16, 16) + # ctx.fill() + + #font = pango.FontDescription('Delicious 24') + # + #layout = pctx.create_layout() + #layout.set_font_description(font) + #layout.set_text(u'Hello') + #pctx.update_layout(layout) + #pctx.show_layout(layout) + + qr = qrcode.QRCode(border = 0) + qr.add_data('test') + qr.make(fit = True) + qm = qr.get_matrix() + print 'QR:', len(qm) + for y, line in enumerate(qm): + for x, dot in enumerate(line): + if dot: + ctx.rectangle(3 * x, 3 * y, 3, 3) + ctx.fill() + + ctx.rectangle(70, 0, 10, 1) + ctx.fill() + ctx.rectangle(70, 2, 10, 1) + ctx.fill() + ctx.rectangle(70, tape.pixels - 3, 10, 1) + ctx.fill() + ctx.rectangle(70, tape.pixels - 1, 10, 1) + ctx.fill() + + data.addcairo(surface, offset = tape.offset) + + #data.addpixels( + # bytearray(((w + 7) / 8) * '\xff')) + #data.addpixels( + # bytearray(((w + 7) / 8) * '\x00')) + #data.addpixels( + # bytearray(((w + 7) / 8) * '\x55')) + #for i in range(0, w): + # pixels = bytearray(((w + 7) / 8) * '\x00') + # if i & 7 == 0: + # pixels[i / 8] = chr(0xff) + # else: + # pixels[i / 8] = chr(1 << (7 - (i & 7))) + # data.addpixels(pixels) + #data.addpixels( + # bytearray(((w + 7) / 8) * '\xff')) + p.send(data.get()) + -- cgit v1.2.1