diff --git a/label-exp/main.go b/label-exp/main.go index 1f15266..673ee65 100644 --- a/label-exp/main.go +++ b/label-exp/main.go @@ -168,11 +168,14 @@ func printStatusInformation(d []byte) { } // genLabelData converts an image to the printer's raster format. -func genLabelData(src image.Image, offset int) (data []byte) { - // TODO: Margins? For 29mm, it's 6 pins from the start, 306 printing pins. +func genLabelData(src image.Image, offset, length int) (data []byte) { bounds := src.Bounds() pixels := make([]bool, 720) for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + length-- + if length <= 0 { + break + } off := offset for x := bounds.Max.X - 1; x >= bounds.Min.X; x-- { // TODO: Anything to do with the ColorModel? @@ -193,10 +196,15 @@ func genLabelData(src image.Image, offset int) (data []byte) { data = append(data, b) } } + for ; length > 0; length-- { + data = append(data, 'g', 0x00, 90) + data = append(data, make([]byte, 90)...) + } 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) // Raster mode. @@ -208,7 +216,17 @@ func printLabel(src image.Image) error { // Print information command. 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) // 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. data = append(data, 0x1b, 0x69, 0x4b, 0x08) - // 3mm margins along the direction of feed. 0x23 = 35 dots, the minimum. - data = append(data, 0x1b, 0x69, 0x64, 0x23, 0x00) + 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. - data = append(data, genLabelData(src, 6)...) + data = append(data, genLabelData(src, mediaInfo.SideMarginPins, dy)...) // Print command with feeding. 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. if _, err := printer.File.Write(data); err != nil { return err @@ -263,19 +266,19 @@ func printLabel(src image.Image) error { // state, and try to figure out something from the statuses. // We may also receive an error status instead of the transition to // the printing state. Or even after it. - start := time.Now() + start, b := time.Now(), make([]byte, 32) for { if time.Now().Sub(start) > 3*time.Second { 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) } else if err != nil { return err } else if n < 32 { return errors.New("invalid read") } else { - printStatusInformation(status) + printStatusInformation(b) } } return nil @@ -285,47 +288,14 @@ func printLabel(src image.Image) error { var font *bdf.Font -func genLabel(text string, width 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() - +func genLabelForHeight(text string, height, scale 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: We can scale as needed to make the text fit. - 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} + scaledTextImg := scaler{image: textImg, scale: scale} scaledTextRect := scaledTextImg.Bounds() 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(` +

PT-CBP label printing tool

- + +
+ {{ if .Printer }} + +

Printer: {{ .Printer.Manufacturer }} {{ .Printer.Model }} +

Tape: + {{ if .Status }} + {{ .Status.MediaWidthMM }} mm × + {{ .Status.MediaLengthMM }} mm + + {{ if .MediaInfo }} + (offset: {{ .MediaInfo.SideMarginPins }} pt, + print area: {{ .MediaInfo.PrintAreaPins }} pt) + {{ else }} + (unknown media) + {{ end }} + + {{ if .Status.Errors }} + {{ range .Status.Errors }} +

Error: {{ . }} + {{ end }} + {{ end }} + + {{ end }} + {{ if .InitErr }} + {{ .InitErr }} + {{ end }} + + {{ else }} +

Error: {{ .PrinterErr }} + {{ end }} +

-

-

+ +

@@ -376,31 +378,82 @@ var tmpl = template.Must(template.New("form").Parse(` `)) +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) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), 500) 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 { - Width int - Text string + Printer *ql.Printer + 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 - params.Width, err = strconv.Atoi(r.FormValue("width")) + params.Scale, err = strconv.Atoi(r.FormValue("scale")) if err != nil { - params.Width = 306 // Default to 29mm tape. + params.Scale = 3 } - // TODO: Possibly just remove the for-width mode. - label := genLabel(params.Text, params.Width) - label = &leftRotate{image: genLabelForHeight(params.Text, params.Width)} - if r.FormValue("print") != "" { - if err := printLabel(label); err != nil { - log.Println("print error:", err) + var label image.Image + if mediaInfo != nil { + label = &leftRotate{image: genLabelForHeight( + params.Text, mediaInfo.PrintAreaPins, params.Scale)} + if r.FormValue("print") != "" { + 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 } + if mediaInfo == nil { + http.Error(w, "unknown media", 500) + return + } + w.Header().Set("Content-Type", "image/png") if err := png.Encode(w, label); err != nil { http.Error(w, err.Error(), 500) diff --git a/ql/ql.go b/ql/ql.go index 0d71551..bf532cb 100644 --- a/ql/ql.go +++ b/ql/ql.go @@ -5,6 +5,7 @@ package ql // 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 ( "errors" @@ -61,8 +62,6 @@ 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. -// -// See e.g. http://www.undocprint.org/formats/communication_protocols/ieee_1284 func parseIEEE1284DeviceID(id []byte) deviceID { m := make(deviceID) for _, kv := range deviceIDRegexp.FindAllStringSubmatch(string(id), -1) { @@ -85,10 +84,19 @@ func (id deviceID) Find(key, abbreviation string) []string { return nil } +func (id deviceID) FindFirst(key, abbreviation string) string { + for _, s := range id.Find(key, abbreviation) { + return s + } + return "" +} + // ----------------------------------------------------------------------------- type Printer struct { - File *os.File + File *os.File + Manufacturer string + Model string } func compatible(id deviceID) bool { @@ -119,12 +127,17 @@ func Open() (*Printer, error) { f.Close() continue } + parsedID := parseIEEE1284DeviceID(deviceID) // Filter out printers that wouldn't understand the protocol. - if !compatible(parseIEEE1284DeviceID(deviceID)) { + if !compatible(parsedID) { f.Close() continue } - return &Printer{File: f}, nil + return &Printer{ + File: f, + Manufacturer: parsedID.FindFirst("MANUFACTURER", "MFG"), + Model: parsedID.FindFirst("MODEL", "MDL"), + }, nil } return nil, nil } @@ -194,52 +207,61 @@ func (p *Printer) Close() error { type mediaSize struct { WidthMM int - HeightMM int + LengthMM int } -type mediaInfo struct { - // Note that these are approximates, many pins within the margins will work +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{ +var media = map[mediaSize]MediaInfo{ // Continuous length tape - {12, 0}: {29, 106}, - {29, 0}: {6, 306}, - {38, 0}: {12, 413}, - {50, 0}: {12, 554}, - {54, 0}: {0, 590}, - {62, 0}: {12, 696}, + {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}, - {17, 87}: {0, 165}, - {23, 23}: {42, 236}, - {29, 42}: {6, 306}, - {29, 90}: {6, 306}, - {38, 90}: {12, 413}, - {39, 48}: {6, 425}, - {52, 29}: {0, 578}, - {54, 29}: {59, 602}, - {60, 86}: {24, 672}, - {62, 29}: {12, 696}, - {62, 100}: {12, 696}, + {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}, - {24, 24}: {42, 236}, - {58, 58}: {51, 618}, + {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 +} + +// ----------------------------------------------------------------------------- + type Status struct { MediaWidthMM int MediaLengthMM int Errors []string } -// ----------------------------------------------------------------------------- - func decodeBitfieldErrors(b byte, errors [8]string) []string { var result []string 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. -func decodeStatusInformation(d []byte) Status { +func DecodeStatus(d []byte) *Status { var status Status + status.MediaWidthMM = int(d[10]) + status.MediaLengthMM = int(d[17]) + status.Errors = append(status.Errors, decodeBitfieldErrors(d[8], [8]string{ "no media", "end of media", "cutter jam", "?", "printer in use", "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", "communication buffer full", "cover open", "cancel key", "media cannot be fed", "system error"})...) - return status + return &status }