Improve the label printing experimental tool

Now it should be safer to print on die-cut labels,
or even any supported label at all.
This commit is contained in:
Přemysl Eric Janouch 2019-04-12 19:22:27 +02:00
parent 8c0a5195fc
commit b763c2d362
Signed by: p
GPG Key ID: A0420B94F92B9493
2 changed files with 196 additions and 113 deletions

View File

@ -168,11 +168,14 @@ func printStatusInformation(d []byte) {
} }
// genLabelData converts an image to the printer's raster format. // genLabelData converts an image to the printer's raster format.
func genLabelData(src image.Image, offset int) (data []byte) { func genLabelData(src image.Image, offset, length int) (data []byte) {
// TODO: Margins? For 29mm, it's 6 pins from the start, 306 printing pins.
bounds := src.Bounds() bounds := src.Bounds()
pixels := make([]bool, 720) pixels := make([]bool, 720)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
length--
if length <= 0 {
break
}
off := offset off := offset
for x := bounds.Max.X - 1; x >= bounds.Min.X; x-- { for x := bounds.Max.X - 1; x >= bounds.Min.X; x-- {
// TODO: Anything to do with the ColorModel? // TODO: Anything to do with the ColorModel?
@ -193,10 +196,15 @@ func genLabelData(src image.Image, offset int) (data []byte) {
data = append(data, b) data = append(data, b)
} }
} }
for ; length > 0; length-- {
data = append(data, 'g', 0x00, 90)
data = append(data, make([]byte, 90)...)
}
return return
} }
func printLabel(src image.Image) error { func printLabel(printer *ql.Printer, src image.Image,
status *ql.Status, mediaInfo *ql.MediaInfo) error {
data := []byte(nil) data := []byte(nil)
// Raster mode. // Raster mode.
@ -208,7 +216,17 @@ func printLabel(src image.Image) error {
// Print information command. // Print information command.
dy := src.Bounds().Dy() dy := src.Bounds().Dy()
data = append(data, 0x1b, 0x69, 0x7a, 0x02|0x04|0x40|0x80, 0x0a, 29, 0, 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) byte(dy), byte(dy>>8), byte(dy>>16), byte(dy>>24), 0, 0x00)
// Auto cut, each 1 label. // Auto cut, each 1 label.
@ -219,41 +237,26 @@ func printLabel(src image.Image) error {
// Not sure what it means, doesn't seem to have any effect to turn it off. // Not sure what it means, doesn't seem to have any effect to turn it off.
data = append(data, 0x1b, 0x69, 0x4b, 0x08) data = append(data, 0x1b, 0x69, 0x4b, 0x08)
// 3mm margins along the direction of feed. 0x23 = 35 dots, the minimum. if status.MediaLengthMM != 0 {
data = append(data, 0x1b, 0x69, 0x64, 0x23, 0x00) // 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. // Compression mode: no compression.
// Should be the only supported mode for QL-800. // Should be the only supported mode for QL-800.
data = append(data, 0x4d, 0x00) data = append(data, 0x4d, 0x00)
// The graphics data itself. // The graphics data itself.
data = append(data, genLabelData(src, 6)...) data = append(data, genLabelData(src, mediaInfo.SideMarginPins, dy)...)
// Print command with feeding. // Print command with feeding.
data = append(data, 0x1a) data = append(data, 0x1a)
// --- // ---
printer, err := ql.Open()
if err != nil {
return err
}
if printer == nil {
return errors.New("no suitable printer found")
}
defer printer.Close()
if err := printer.Initialize(); err != nil {
return err
}
status, err := printer.GetStatus()
if err != nil {
return err
}
printStatusInformation(status)
// Print the prepared data. // Print the prepared data.
if _, err := printer.File.Write(data); err != nil { if _, err := printer.File.Write(data); err != nil {
return err return err
@ -263,19 +266,19 @@ func printLabel(src image.Image) error {
// state, and try to figure out something from the statuses. // state, and try to figure out something from the statuses.
// We may also receive an error status instead of the transition to // We may also receive an error status instead of the transition to
// the printing state. Or even after it. // the printing state. Or even after it.
start := time.Now() start, b := time.Now(), make([]byte, 32)
for { for {
if time.Now().Sub(start) > 3*time.Second { if time.Now().Sub(start) > 3*time.Second {
break break
} }
if n, err := printer.File.Read(status); err == io.EOF { if n, err := printer.File.Read(b); err == io.EOF {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} else if err != nil { } else if err != nil {
return err return err
} else if n < 32 { } else if n < 32 {
return errors.New("invalid read") return errors.New("invalid read")
} else { } else {
printStatusInformation(status) printStatusInformation(b)
} }
} }
return nil return nil
@ -285,47 +288,14 @@ func printLabel(src image.Image) error {
var font *bdf.Font var font *bdf.Font
func genLabel(text string, width int) image.Image { func genLabelForHeight(text string, height, scale int) image.Image {
// Create a scaled bitmap of the QR code.
qrImg, _ := qr.Encode(text, qr.H, qr.Auto)
qrImg, _ = barcode.Scale(qrImg, width, width)
qrRect := qrImg.Bounds()
// Create a scaled bitmap of the text label. // Create a scaled bitmap of the text label.
textRect, _ := font.BoundString(text) textRect, _ := font.BoundString(text)
textImg := image.NewRGBA(textRect) textImg := image.NewRGBA(textRect)
draw.Draw(textImg, textRect, image.White, image.ZP, draw.Src) draw.Draw(textImg, textRect, image.White, image.ZP, draw.Src)
font.DrawString(textImg, image.ZP, text) font.DrawString(textImg, image.ZP, text)
// TODO: We can scale as needed to make the text fit. scaledTextImg := scaler{image: textImg, scale: scale}
scaledTextImg := scaler{image: textImg, scale: 3}
scaledTextRect := scaledTextImg.Bounds()
// Combine.
combinedRect := qrRect
combinedRect.Max.Y += scaledTextRect.Dy() + 20
combinedImg := image.NewRGBA(combinedRect)
draw.Draw(combinedImg, combinedRect, image.White, image.ZP, draw.Src)
draw.Draw(combinedImg, combinedRect, qrImg, image.ZP, draw.Src)
target := image.Rect(
(width-scaledTextRect.Dx())/2, qrRect.Dy()+10,
combinedRect.Max.X, combinedRect.Max.Y)
draw.Draw(combinedImg, target, &scaledTextImg, scaledTextRect.Min, draw.Src)
return combinedImg
}
func genLabelForHeight(text string, height int) image.Image {
// Create a scaled bitmap of the text label.
textRect, _ := font.BoundString(text)
textImg := image.NewRGBA(textRect)
draw.Draw(textImg, textRect, image.White, image.ZP, draw.Src)
font.DrawString(textImg, image.ZP, text)
// TODO: Make it possible to choose scale, or use some heuristic.
scaledTextImg := scaler{image: textImg, scale: 3}
//scaledTextImg := scaler{image: textImg, scale: 3}
scaledTextRect := scaledTextImg.Bounds() scaledTextRect := scaledTextImg.Bounds()
remains := height - scaledTextRect.Dy() - 20 remains := height - scaledTextRect.Dy() - 20
@ -358,16 +328,48 @@ func genLabelForHeight(text string, height int) image.Image {
var tmpl = template.Must(template.New("form").Parse(` var tmpl = template.Must(template.New("form").Parse(`
<!DOCTYPE html> <!DOCTYPE html>
<html><body> <html><body>
<h1>PT-CBP label printing tool</h1>
<table><tr> <table><tr>
<td valign=top> <td valign=top>
<img border=1 src='?img&amp;width={{.Width}}&amp;text={{.Text}}'> <img border=1 src='?img&amp;scale={{.Scale}}&amp;text={{.Text}}'>
</td> </td>
<td valign=top> <td valign=top>
<fieldset>
{{ if .Printer }}
<p>Printer: {{ .Printer.Manufacturer }} {{ .Printer.Model }}
<p>Tape:
{{ if .Status }}
{{ .Status.MediaWidthMM }} mm &times;
{{ .Status.MediaLengthMM }} mm
{{ if .MediaInfo }}
(offset: {{ .MediaInfo.SideMarginPins }} pt,
print area: {{ .MediaInfo.PrintAreaPins }} pt)
{{ else }}
(unknown media)
{{ end }}
{{ if .Status.Errors }}
{{ range .Status.Errors }}
<p>Error: {{ . }}
{{ end }}
{{ end }}
{{ end }}
{{ if .InitErr }}
{{ .InitErr }}
{{ end }}
{{ else }}
<p>Error: {{ .PrinterErr }}
{{ end }}
</fieldset>
<form><fieldset> <form><fieldset>
<p><label for=width>Tape width in pt:</label>
<input id=width name=width value='{{.Width}}'>
<p><label for=text>Text:</label> <p><label for=text>Text:</label>
<input id=text name=text value='{{.Text}}'> <input id=text name=text value='{{.Text}}'>
<label for=scale>Scale:</label>
<input id=scale name=scale value='{{.Scale}}' size=1>
<p><input type=submit value='Update'> <p><input type=submit value='Update'>
<input type=submit name=print value='Update and Print'> <input type=submit name=print value='Update and Print'>
</fieldset></form> </fieldset></form>
@ -376,31 +378,82 @@ var tmpl = template.Must(template.New("form").Parse(`
</body></html> </body></html>
`)) `))
func getPrinter() (*ql.Printer, error) {
printer, err := ql.Open()
if err != nil {
return nil, err
}
if printer == nil {
return nil, errors.New("no suitable printer found")
}
return printer, nil
}
func getStatus(printer *ql.Printer) (*ql.Status, error) {
if err := printer.Initialize(); err != nil {
return nil, err
}
if data, err := printer.GetStatus(); err != nil {
return nil, err
} else {
printStatusInformation(data)
return ql.DecodeStatus(data), nil
}
}
func handle(w http.ResponseWriter, r *http.Request) { func handle(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
var (
status *ql.Status
initErr error
)
printer, printerErr := getPrinter()
if printerErr == nil {
defer printer.Close()
status, initErr = getStatus(printer)
}
var mediaInfo *ql.MediaInfo
if status != nil {
mediaInfo = ql.GetMediaInfo(status.MediaWidthMM, status.MediaLengthMM)
}
var params = struct { var params = struct {
Width int Printer *ql.Printer
Text string PrinterErr error
Status *ql.Status
InitErr error
MediaInfo *ql.MediaInfo
Text string
Scale int
}{ }{
Text: r.FormValue("text"), Printer: printer,
PrinterErr: printerErr,
Status: status,
InitErr: initErr,
MediaInfo: mediaInfo,
Text: r.FormValue("text"),
} }
var err error var err error
params.Width, err = strconv.Atoi(r.FormValue("width")) params.Scale, err = strconv.Atoi(r.FormValue("scale"))
if err != nil { if err != nil {
params.Width = 306 // Default to 29mm tape. params.Scale = 3
} }
// TODO: Possibly just remove the for-width mode. var label image.Image
label := genLabel(params.Text, params.Width) if mediaInfo != nil {
label = &leftRotate{image: genLabelForHeight(params.Text, params.Width)} label = &leftRotate{image: genLabelForHeight(
if r.FormValue("print") != "" { params.Text, mediaInfo.PrintAreaPins, params.Scale)}
if err := printLabel(label); err != nil { if r.FormValue("print") != "" {
log.Println("print error:", err) if err := printLabel(
printer, label, status, mediaInfo); err != nil {
log.Println("print error:", err)
}
} }
} }
@ -410,6 +463,11 @@ func handle(w http.ResponseWriter, r *http.Request) {
return return
} }
if mediaInfo == nil {
http.Error(w, "unknown media", 500)
return
}
w.Header().Set("Content-Type", "image/png") w.Header().Set("Content-Type", "image/png")
if err := png.Encode(w, label); err != nil { if err := png.Encode(w, label); err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)

View File

@ -5,6 +5,7 @@ package ql
// http://etc.nkadesign.com/Printers/QL550LabelPrinterProtocol // http://etc.nkadesign.com/Printers/QL550LabelPrinterProtocol
// https://github.com/torvalds/linux/blob/master/drivers/usb/class/usblp.c // 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/page_description_languages/brother_p-touch
// http://www.undocprint.org/formats/communication_protocols/ieee_1284
import ( import (
"errors" "errors"
@ -61,8 +62,6 @@ type deviceID map[string][]string
// parseIEEE1284DeviceID leniently parses an IEEE 1284 Device ID string // parseIEEE1284DeviceID leniently parses an IEEE 1284 Device ID string
// and returns a map containing a slice of values for each key. // and returns a map containing a slice of values for each key.
//
// See e.g. http://www.undocprint.org/formats/communication_protocols/ieee_1284
func parseIEEE1284DeviceID(id []byte) deviceID { func parseIEEE1284DeviceID(id []byte) deviceID {
m := make(deviceID) m := make(deviceID)
for _, kv := range deviceIDRegexp.FindAllStringSubmatch(string(id), -1) { for _, kv := range deviceIDRegexp.FindAllStringSubmatch(string(id), -1) {
@ -85,10 +84,19 @@ func (id deviceID) Find(key, abbreviation string) []string {
return nil return nil
} }
func (id deviceID) FindFirst(key, abbreviation string) string {
for _, s := range id.Find(key, abbreviation) {
return s
}
return ""
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
type Printer struct { type Printer struct {
File *os.File File *os.File
Manufacturer string
Model string
} }
func compatible(id deviceID) bool { func compatible(id deviceID) bool {
@ -119,12 +127,17 @@ func Open() (*Printer, error) {
f.Close() f.Close()
continue continue
} }
parsedID := parseIEEE1284DeviceID(deviceID)
// Filter out printers that wouldn't understand the protocol. // Filter out printers that wouldn't understand the protocol.
if !compatible(parseIEEE1284DeviceID(deviceID)) { if !compatible(parsedID) {
f.Close() f.Close()
continue continue
} }
return &Printer{File: f}, nil return &Printer{
File: f,
Manufacturer: parsedID.FindFirst("MANUFACTURER", "MFG"),
Model: parsedID.FindFirst("MODEL", "MDL"),
}, nil
} }
return nil, nil return nil, nil
} }
@ -194,52 +207,61 @@ func (p *Printer) Close() error {
type mediaSize struct { type mediaSize struct {
WidthMM int WidthMM int
HeightMM int LengthMM int
} }
type mediaInfo struct { type MediaInfo struct {
// Note that these are approximates, many pins within the margins will work // Note that these are approximates, many pins within the margins will work.
SideMarginPins int SideMarginPins int
PrintAreaPins int PrintAreaPins int
// If non-zero, length of the die-cut label print area in 300dpi pins.
PrintAreaLength int
} }
var media = map[mediaSize]mediaInfo{ var media = map[mediaSize]MediaInfo{
// Continuous length tape // Continuous length tape
{12, 0}: {29, 106}, {12, 0}: {29, 106, 0},
{29, 0}: {6, 306}, {29, 0}: {6, 306, 0},
{38, 0}: {12, 413}, {38, 0}: {12, 413, 0},
{50, 0}: {12, 554}, {50, 0}: {12, 554, 0},
{54, 0}: {0, 590}, {54, 0}: {0, 590, 0},
{62, 0}: {12, 696}, {62, 0}: {12, 696, 0},
// Die-cut labels // Die-cut labels
{17, 54}: {0, 165}, {17, 54}: {0, 165, 566},
{17, 87}: {0, 165}, {17, 87}: {0, 165, 956},
{23, 23}: {42, 236}, {23, 23}: {42, 236, 202},
{29, 42}: {6, 306}, {29, 42}: {6, 306, 425},
{29, 90}: {6, 306}, {29, 90}: {6, 306, 991},
{38, 90}: {12, 413}, {38, 90}: {12, 413, 991},
{39, 48}: {6, 425}, {39, 48}: {6, 425, 495},
{52, 29}: {0, 578}, {52, 29}: {0, 578, 271},
{54, 29}: {59, 602}, {54, 29}: {59, 602, 271},
{60, 86}: {24, 672}, {60, 86}: {24, 672, 954},
{62, 29}: {12, 696}, {62, 29}: {12, 696, 271},
{62, 100}: {12, 696}, {62, 100}: {12, 696, 1109},
// Die-cut diameter labels // Die-cut diameter labels
{12, 12}: {113, 94}, {12, 12}: {113, 94, 94},
{24, 24}: {42, 236}, {24, 24}: {42, 236, 236},
{58, 58}: {51, 618}, {58, 58}: {51, 618, 618},
} }
func GetMediaInfo(widthMM, lengthMM int) *MediaInfo {
if mi, ok := media[mediaSize{widthMM, lengthMM}]; ok {
return &mi
}
return nil
}
// -----------------------------------------------------------------------------
type Status struct { type Status struct {
MediaWidthMM int MediaWidthMM int
MediaLengthMM int MediaLengthMM int
Errors []string Errors []string
} }
// -----------------------------------------------------------------------------
func decodeBitfieldErrors(b byte, errors [8]string) []string { func decodeBitfieldErrors(b byte, errors [8]string) []string {
var result []string var result []string
for i := uint(0); i < 8; i++ { for i := uint(0); i < 8; i++ {
@ -251,8 +273,11 @@ func decodeBitfieldErrors(b byte, errors [8]string) []string {
} }
// TODO: What exactly do we need? Probably extend as needed. // TODO: What exactly do we need? Probably extend as needed.
func decodeStatusInformation(d []byte) Status { func DecodeStatus(d []byte) *Status {
var status Status var status Status
status.MediaWidthMM = int(d[10])
status.MediaLengthMM = int(d[17])
status.Errors = append(status.Errors, decodeBitfieldErrors(d[8], [8]string{ status.Errors = append(status.Errors, decodeBitfieldErrors(d[8], [8]string{
"no media", "end of media", "cutter jam", "?", "printer in use", "no media", "end of media", "cutter jam", "?", "printer in use",
"printer turned off", "high-voltage adapter", "fan motor error"})...) "printer turned off", "high-voltage adapter", "fan motor error"})...)
@ -260,5 +285,5 @@ func decodeStatusInformation(d []byte) Status {
"replace media", "expansion buffer full", "communication error", "replace media", "expansion buffer full", "communication error",
"communication buffer full", "cover open", "cancel key", "communication buffer full", "cover open", "cancel key",
"media cannot be fed", "system error"})...) "media cannot be fed", "system error"})...)
return status return &status
} }