From ab253ce768c410f6c996fd3df98292df0554c2f3 Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Sat, 6 Apr 2019 21:31:11 +0200 Subject: Initial commit --- label-exp/main.go | 402 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 label-exp/main.go (limited to 'label-exp') diff --git a/label-exp/main.go b/label-exp/main.go new file mode 100644 index 0000000..442c021 --- /dev/null +++ b/label-exp/main.go @@ -0,0 +1,402 @@ +package main + +import ( + "errors" + "html/template" + "image" + "image/color" + "image/draw" + "image/png" + "io" + "log" + "net/http" + "os" + "strconv" + "time" + + "github.com/boombuler/barcode" + "github.com/boombuler/barcode/qr" + + "janouch.name/sklad/bdf" +) + +// scaler is a scaling image.Image wrapper. +type scaler struct { + image image.Image + scale int +} + +// ColorModel implements image.Image. +func (s *scaler) ColorModel() color.Model { + return s.image.ColorModel() +} + +// Bounds implements image.Image. +func (s *scaler) Bounds() image.Rectangle { + r := s.image.Bounds() + return image.Rect(r.Min.X*s.scale, r.Min.Y*s.scale, + r.Max.X*s.scale, r.Max.Y*s.scale) +} + +// At implements image.Image. +func (s *scaler) At(x, y int) color.Color { + if x < 0 { + x = x - s.scale + 1 + } + if y < 0 { + y = y - s.scale + 1 + } + return s.image.At(x/s.scale, y/s.scale) +} + +// ----------------------------------------------------------------------------- + +func decodeBitfieldErrors(b byte, errors [8]string) []string { + var result []string + for i := uint(0); i < 8; i++ { + if b&(1<= bounds.Min.X; x-- { + // TODO: Anything to do with the ColorModel? + r, g, b, a := src.At(x, y).RGBA() + pixels[off] = r == 0 && g == 0 && b == 0 && a != 0 + off++ + } + + data = append(data, 'g', 0x00, 90) + for i := 0; i < 90; i++ { + var b byte + for j := 0; j < 8; j++ { + b <<= 1 + if pixels[i*8+j] { + b |= 1 + } + } + data = append(data, b) + } + } + return +} + +func printLabel(src image.Image) error { + data := []byte(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 := src.Bounds().Dy() + data = append(data, 0x1b, 0x69, 0x7a, 0x02|0x04|0x40|0x80, 0x0a, 29, 0, + 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) + + // 3mm margins along the direction of feed. 0x23 = 35 dots, the minimum. + data = append(data, 0x1b, 0x69, 0x64, 0x23, 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)...) + + // Print command with feeding. + data = append(data, 0x1a) + + // --- + + // Linux usblp module, located in /drivers/usb/class/usblp.c + // (at least that's where the trails go, I don't understand the code) + f, err := os.OpenFile("/dev/usb/lp0", os.O_RDWR, 0) + if err != nil { + return err + } + defer f.Close() + + // Flush any former responses in the printer's queue. + for { + dummy := make([]byte, 32) + if _, err := f.Read(dummy); err == io.EOF { + break + } + } + + // Clear the print buffer. + invalidate := make([]byte, 400) + if _, err := f.Write(invalidate); err != nil { + return err + } + + // Initialize. + if _, err := f.WriteString("\x1b\x40"); err != nil { + return err + } + + // Request status information. + if _, err := f.WriteString("\x1b\x69\x53"); err != nil { + return err + } + + // We need to poll the device. + status := make([]byte, 32) + for { + if n, err := f.Read(status); err == io.EOF { + time.Sleep(10 * time.Millisecond) + } else if err != nil { + return err + } else if n < 32 { + return errors.New("invalid read") + } else { + break + } + } + printStatusInformation(status) + + // Print the prepared data. + if _, err := f.Write(data); err != nil { + return err + } + + // TODO: We specifically need to wait for a transition to the receiving + // 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() + for { + if time.Now().Sub(start) > 3*time.Second { + break + } + if n, err := f.Read(status); 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) + } + } + return nil +} + +// ----------------------------------------------------------------------------- + +var font *bdf.Font + +// TODO: By rotating the label we can make it smaller, as an alternate mode. +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, 306, 306) + qrRect := qrImg.Bounds() + + // 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 +} + +var tmpl = template.Must(template.New("form").Parse(` + + + + + +
+ + +
+

+ +

+ +

+ + +

+
+ +`)) + +func handle(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), 500) + return + } + + var params = struct { + Width int + Text string + }{ + Text: r.FormValue("text"), + } + + var err error + params.Width, err = strconv.Atoi(r.FormValue("width")) + if err != nil { + params.Width = 306 // Default to 29mm tape. + } + + label := genLabel(params.Text, params.Width) + if r.FormValue("print") != "" { + if err := printLabel(label); err != nil { + log.Println("print error:", err) + } + } + + if _, ok := r.Form["img"]; !ok { + w.Header().Set("Content-Type", "text/html") + tmpl.Execute(w, ¶ms) + return + } + + w.Header().Set("Content-Type", "image/png") + if err := png.Encode(w, label); err != nil { + http.Error(w, err.Error(), 500) + return + } +} + +func main() { + var err error + fi, err := os.Open("../../ucs-fonts-75dpi100dpi/100dpi/luBS24.bdf") + if err != nil { + log.Fatalln(err) + } + font, err = bdf.NewFromBDF(fi) + if err != nil { + log.Fatalln(err) + } + if err := fi.Close(); err != nil { + log.Fatalln(err) + } + + log.Println("Starting server") + http.HandleFunc("/", handle) + log.Fatal(http.ListenAndServe(":8080", nil)) +} -- cgit v1.2.3-70-g09d2