2019-04-12 02:15:28 +02:00
|
|
|
// Package ql is a Linux driver for Brother QL-series printers.
|
|
|
|
package ql
|
|
|
|
|
|
|
|
// Resources:
|
2019-04-12 03:00:17 +02:00
|
|
|
// http://etc.nkadesign.com/Printers/QL550LabelPrinterProtocol
|
|
|
|
// https://github.com/torvalds/linux/blob/master/drivers/usb/class/usblp.c
|
2019-04-12 02:15:28 +02:00
|
|
|
// http://www.undocprint.org/formats/page_description_languages/brother_p-touch
|
2019-04-12 19:22:27 +02:00
|
|
|
// http://www.undocprint.org/formats/communication_protocols/ieee_1284
|
2019-04-12 02:15:28 +02:00
|
|
|
|
|
|
|
import (
|
2019-04-12 21:50:36 +02:00
|
|
|
"image"
|
2019-04-12 02:15:28 +02:00
|
|
|
"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
|
|
|
|
}
|
|
|
|
|
2019-04-12 19:22:27 +02:00
|
|
|
func (id deviceID) FindFirst(key, abbreviation string) string {
|
|
|
|
for _, s := range id.Find(key, abbreviation) {
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2019-04-12 02:15:28 +02:00
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
func compatible(id deviceID) bool {
|
|
|
|
for _, commandSet := range id.Find("COMMAND SET", "CMD") {
|
|
|
|
if commandSet == "PT-CBP" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
2019-04-12 04:52:03 +02:00
|
|
|
type mediaSize struct {
|
|
|
|
WidthMM int
|
2019-04-12 19:22:27 +02:00
|
|
|
LengthMM int
|
2019-04-12 04:52:03 +02:00
|
|
|
}
|
|
|
|
|
2019-04-12 19:22:27 +02:00
|
|
|
type MediaInfo struct {
|
|
|
|
// Note that these are approximates, many pins within the margins will work.
|
2019-04-12 04:52:03 +02:00
|
|
|
SideMarginPins int
|
|
|
|
PrintAreaPins int
|
2019-04-12 19:22:27 +02:00
|
|
|
// If non-zero, length of the die-cut label print area in 300dpi pins.
|
|
|
|
PrintAreaLength int
|
2019-04-12 04:52:03 +02:00
|
|
|
}
|
|
|
|
|
2019-04-12 19:22:27 +02:00
|
|
|
var media = map[mediaSize]MediaInfo{
|
2019-04-12 04:52:03 +02:00
|
|
|
// Continuous length tape
|
2019-04-12 19:22:27 +02:00
|
|
|
{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},
|
2019-04-12 04:52:03 +02:00
|
|
|
|
|
|
|
// Die-cut labels
|
2019-04-12 19:22:27 +02:00
|
|
|
{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},
|
2019-04-12 04:52:03 +02:00
|
|
|
|
|
|
|
// Die-cut diameter labels
|
2019-04-12 19:22:27 +02:00
|
|
|
{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
|
2019-04-12 04:52:03 +02:00
|
|
|
}
|
|
|
|
|
2019-04-12 19:22:27 +02:00
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
2019-04-12 23:10:42 +02:00
|
|
|
const (
|
|
|
|
printBytes = 90
|
|
|
|
printPins = printBytes * 8
|
|
|
|
)
|
|
|
|
|
2019-04-12 21:50:36 +02:00
|
|
|
// makeBitmapData converts an image to the printer's raster format.
|
2019-04-12 23:10:42 +02:00
|
|
|
func makeBitmapData(src image.Image, margin, length int) (data []byte) {
|
2019-04-12 21:50:36 +02:00
|
|
|
bounds := src.Bounds()
|
2019-04-12 23:10:42 +02:00
|
|
|
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{}
|
2019-04-12 21:50:36 +02:00
|
|
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
|
|
|
length--
|
|
|
|
|
2019-04-12 23:10:42 +02:00
|
|
|
// The graphics needs to be inverted horizontally, iterating backwards.
|
|
|
|
offset := margin
|
2019-04-12 21:50:36 +02:00
|
|
|
for x := bounds.Max.X - 1; x >= bounds.Min.X; x-- {
|
|
|
|
r, g, b, a := src.At(x, y).RGBA()
|
2019-04-12 23:10:42 +02:00
|
|
|
pixels[offset] = r == 0 && g == 0 && b == 0 && a >= 0x8000
|
|
|
|
offset++
|
2019-04-12 21:50:36 +02:00
|
|
|
}
|
|
|
|
|
2019-04-12 23:10:42 +02:00
|
|
|
data = append(data, 'g', 0x00, printBytes)
|
|
|
|
for i := 0; i < printBytes; i++ {
|
2019-04-12 21:50:36 +02:00
|
|
|
var b byte
|
|
|
|
for j := 0; j < 8; j++ {
|
|
|
|
b <<= 1
|
|
|
|
if pixels[i*8+j] {
|
|
|
|
b |= 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
data = append(data, b)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for ; length > 0; length-- {
|
2019-04-12 23:10:42 +02:00
|
|
|
data = append(data, 'g', 0x00, printBytes)
|
|
|
|
data = append(data, make([]byte, printBytes)...)
|
2019-04-12 21:50:36 +02:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-04-12 23:10:42 +02:00
|
|
|
func makePrintData(status *Status, image image.Image) (data []byte) {
|
2019-04-12 21:50:36 +02:00
|
|
|
mediaInfo := GetMediaInfo(
|
2019-04-12 23:10:42 +02:00
|
|
|
status.MediaWidthMM(),
|
|
|
|
status.MediaLengthMM(),
|
2019-04-12 21:50:36 +02:00
|
|
|
)
|
2019-04-12 23:10:42 +02:00
|
|
|
if mediaInfo == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2019-04-12 21:50:36 +02:00
|
|
|
|
|
|
|
// 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)
|
2019-04-12 23:10:42 +02:00
|
|
|
if status.MediaLengthMM() != 0 {
|
2019-04-12 21:50:36 +02:00
|
|
|
mediaType = byte(0x0b)
|
|
|
|
}
|
|
|
|
|
|
|
|
data = append(data, 0x1b, 0x69, 0x7a, 0x02|0x04|0x40|0x80, mediaType,
|
2019-04-12 23:10:42 +02:00
|
|
|
byte(status.MediaWidthMM()), byte(status.MediaLengthMM()),
|
2019-04-12 21:50:36 +02:00
|
|
|
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.
|
|
|
|
data = append(data, 0x1b, 0x69, 0x4b, 0x08)
|
|
|
|
|
2019-04-12 23:10:42 +02:00
|
|
|
if status.MediaLengthMM() != 0 {
|
2019-04-12 21:50:36 +02:00
|
|
|
// 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.
|
|
|
|
data = append(data, makeBitmapData(image, mediaInfo.SideMarginPins, dy)...)
|
|
|
|
|
|
|
|
// Print command with feeding.
|
|
|
|
return append(data, 0x1a)
|
2019-04-12 02:15:28 +02:00
|
|
|
}
|