Personal warehouse management system
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

277 lines
7.2 KiB

// Package ql is a Linux driver for Brother QL-series printers.
package ql
// Resources:
// http://etc.nkadesign.com/Printers/QL550LabelPrinterProtocol
// https://github.com/torvalds/linux/blob/master/drivers/usb/class/usblp.c
// http://www.undocprint.org/formats/page_description_languages/brother_p-touch
// http://www.undocprint.org/formats/communication_protocols/ieee_1284
import (
"image"
"regexp"
"strings"
)
// -----------------------------------------------------------------------------
var deviceIDRegexp = regexp.MustCompile(
`(?s:\s*([^:,;]+?)\s*:\s*([^:;]*)\s*(?:;|$))`)
type deviceID map[string][]string
// parseIEEE1284DeviceID leniently parses an IEEE 1284 Device ID string
// and returns a map containing a slice of values for each key.
func parseIEEE1284DeviceID(id []byte) deviceID {
m := make(deviceID)
for _, kv := range deviceIDRegexp.FindAllStringSubmatch(string(id), -1) {
var values []string
for _, v := range strings.Split(kv[2], ",") {
values = append(values, strings.Trim(v, "\t\n\v\f\r "))
}
m[kv[1]] = values
}
return m
}
func (id deviceID) Find(key, abbreviation string) []string {
if values, ok := id[key]; ok {
return values
}
if values, ok := id[abbreviation]; ok {
return values
}
return nil
}
func (id deviceID) FindFirst(key, abbreviation string) string {
for _, s := range id.Find(key, abbreviation) {
return s
}
return ""
}
// -----------------------------------------------------------------------------
func compatible(id deviceID) bool {
for _, commandSet := range id.Find("COMMAND SET", "CMD") {
if commandSet == "PT-CBP" {
return true
}
}
return false
}
// -----------------------------------------------------------------------------
type mediaSize struct {
WidthMM int
LengthMM int
}
type MediaInfo struct {
// Note that these are approximates, many pins within the margins will work.
SideMarginPins int
PrintAreaPins int
// If non-zero, length of the die-cut label print area in 300dpi pins.
PrintAreaLength int
}
var media = map[mediaSize]MediaInfo{
// Continuous length tape
{12, 0}: {29, 106, 0},
{29, 0}: {6, 306, 0},
{38, 0}: {12, 413, 0},
{50, 0}: {12, 554, 0},
{54, 0}: {0, 590, 0},
{62, 0}: {12, 696, 0},
// Die-cut labels
{17, 54}: {0, 165, 566},
{17, 87}: {0, 165, 956},
{23, 23}: {42, 236, 202},
{29, 42}: {6, 306, 425},
{29, 90}: {6, 306, 991},
{38, 90}: {12, 413, 991},
{39, 48}: {6, 425, 495},
{52, 29}: {0, 578, 271},
{54, 29}: {59, 602, 271},
{60, 86}: {24, 672, 954},
{62, 29}: {12, 696, 271},
{62, 100}: {12, 696, 1109},
// Die-cut diameter labels
{12, 12}: {113, 94, 94},
{24, 24}: {42, 236, 236},
{58, 58}: {51, 618, 618},
}
func GetMediaInfo(widthMM, lengthMM int) *MediaInfo {
if mi, ok := media[mediaSize{widthMM, lengthMM}]; ok {
return &mi
}
return nil
}
// -----------------------------------------------------------------------------
const (
printBytes = 90
printPins = printBytes * 8
)
// pack packs a bool array into a byte array for the printer to print out.
func pack(data [printPins]bool, out *[]byte) {
for i := 0; i < printBytes; i++ {
var b byte
for j := 0; j < 8; j++ {
b <<= 1
if data[i*8+j] {
b |= 1
}
}
*out = append(*out, b)
}
}
// makeBitmapDataRB converts an image to the printer's red-black raster format.
func makeBitmapDataRB(src image.Image, margin, length int) []byte {
data, bounds := []byte{}, src.Bounds()
if bounds.Dy() > length {
bounds.Max.Y = bounds.Min.Y + length
}
if bounds.Dx() > printPins-margin {
bounds.Max.X = bounds.Min.X + printPins
}
redcells, blackcells := [printPins]bool{}, [printPins]bool{}
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
length--
// The graphics needs to be inverted horizontally, iterating backwards.
offset := margin
for x := bounds.Max.X - 1; x >= bounds.Min.X; x-- {
r, g, b, a := src.At(x, y).RGBA()
redcells[offset] = r >= 0xc000 && g < 0x4000 && b < 0x4000 &&
a >= 0x8000
blackcells[offset] = r < 0x4000 && g < 0x4000 && b < 0x4000 &&
a >= 0x8000
offset++
}
data = append(data, 'w', 0x01, printBytes)
pack(blackcells, &data)
data = append(data, 'w', 0x02, printBytes)
pack(redcells, &data)
}
for ; length > 0; length-- {
data = append(data, 'w', 0x01, printBytes)
data = append(data, make([]byte, printBytes)...)
data = append(data, 'w', 0x02, printBytes)
data = append(data, make([]byte, printBytes)...)
}
return data
}
// makeBitmapData converts an image to the printer's raster format.
func makeBitmapData(src image.Image, rb bool, margin, length int) []byte {
// It's a necessary nuisance, so just copy and paste.
if rb {
return makeBitmapDataRB(src, margin, length)
}
data, bounds := []byte{}, src.Bounds()
if bounds.Dy() > length {
bounds.Max.Y = bounds.Min.Y + length
}
if bounds.Dx() > printPins-margin {
bounds.Max.X = bounds.Min.X + printPins
}
pixels := [printPins]bool{}
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
length--
// The graphics needs to be inverted horizontally, iterating backwards.
offset := margin
for x := bounds.Max.X - 1; x >= bounds.Min.X; x-- {
r, g, b, a := src.At(x, y).RGBA()
pixels[offset] = r < 0x4000 && g < 0x4000 && b < 0x4000 &&
a >= 0x8000
offset++
}
data = append(data, 'g', 0x00, printBytes)
pack(pixels, &data)
}
for ; length > 0; length-- {
data = append(data, 'g', 0x00, printBytes)
data = append(data, make([]byte, printBytes)...)
}
return data
}
// XXX: It would be preferrable to know for certain if this is a red-black tape,
// because the printer refuses to print on a mismatch.
func makePrintData(status *Status, image image.Image, rb bool) (data []byte) {
mediaInfo := GetMediaInfo(
status.MediaWidthMM(),
status.MediaLengthMM(),
)
if mediaInfo == nil {
return nil
}
// Raster mode.
// Should be the only supported mode for QL-800.
data = append(data, 0x1b, 0x69, 0x61, 0x01)
// Automatic status mode (though it's the default).
data = append(data, 0x1b, 0x69, 0x21, 0x00)
// Print information command.
dy := image.Bounds().Dy()
if mediaInfo.PrintAreaLength != 0 {
dy = mediaInfo.PrintAreaLength
}
mediaType := byte(0x0a)
if status.MediaLengthMM() != 0 {
mediaType = byte(0x0b)
}
data = append(data, 0x1b, 0x69, 0x7a, 0x02|0x04|0x40|0x80, mediaType,
byte(status.MediaWidthMM()), byte(status.MediaLengthMM()),
byte(dy), byte(dy>>8), byte(dy>>16), byte(dy>>24), 0, 0x00)
// Auto cut, each 1 label.
data = append(data, 0x1b, 0x69, 0x4d, 0x40)
data = append(data, 0x1b, 0x69, 0x41, 0x01)
// Cut at end (though it's the default).
// Not sure what it means, doesn't seem to have any effect to turn it off.
if rb {
data = append(data, 0x1b, 0x69, 0x4b, 0x08|0x01)
} else {
data = append(data, 0x1b, 0x69, 0x4b, 0x08)
}
if status.MediaLengthMM() != 0 {
// 3mm margins along the direction of feed. 0x23 = 35 dots, the minimum.
data = append(data, 0x1b, 0x69, 0x64, 0x23, 0x00)
} else {
// May not set anything other than zero.
data = append(data, 0x1b, 0x69, 0x64, 0x00, 0x00)
}
// Compression mode: no compression.
// Should be the only supported mode for QL-800.
data = append(data, 0x4d, 0x00)
// The graphics data itself.
bitmapData := makeBitmapData(image, rb, mediaInfo.SideMarginPins, dy)
data = append(data, bitmapData...)
// Print command with feeding.
return append(data, 0x1a)
}