diff options
-rw-r--r-- | bdf-preview/main.go | 93 | ||||
-rw-r--r-- | bdf-sample/main.go | 41 | ||||
-rw-r--r-- | bdf/bdf.go | 353 | ||||
-rw-r--r-- | brother-info/main.go | 207 | ||||
-rw-r--r-- | go.mod | 5 | ||||
-rw-r--r-- | go.sum | 2 | ||||
-rw-r--r-- | label-exp/main.go | 402 |
7 files changed, 1103 insertions, 0 deletions
diff --git a/bdf-preview/main.go b/bdf-preview/main.go new file mode 100644 index 0000000..b1f2045 --- /dev/null +++ b/bdf-preview/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "html/template" + "image" + "image/draw" + "image/png" + "log" + "net/http" + "os" + + "janouch.name/sklad/bdf" +) + +type fontItem struct { + Font *bdf.Font + Preview image.Image +} + +var fonts = map[string]fontItem{} + +var tmpl = template.Must(template.New("list").Parse(` + <!DOCTYPE html> + <html><body> + <table border='1' cellpadding='3' style='border-collapse: collapse'> + <tr> + <th>Name</th> + <th>Preview</th> + <tr> +{{range $k, $v := . }} + <tr> + <td>{{$k}}</td> + <td><img src='?name={{$k}}'></td> + </tr> +{{end}} + </table> + </body></html> +`)) + +func handle(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), 500) + return + } + + name := r.FormValue("name") + if name == "" { + w.Header().Set("Content-Type", "text/html") + tmpl.Execute(w, fonts) + return + } + + item, ok := fonts[name] + if !ok { + http.Error(w, "No such font.", 400) + return + } + + w.Header().Set("Content-Type", "image/png") + if err := png.Encode(w, item.Preview); err != nil { + http.Error(w, err.Error(), 500) + return + } +} + +func main() { + for _, filename := range os.Args[1:] { + fi, err := os.Open(filename) + if err != nil { + log.Fatalln(err) + } + font, err := bdf.NewFromBDF(fi) + if err != nil { + log.Fatalf("%s: %s\n", filename, err) + } + if err := fi.Close(); err != nil { + log.Fatalln(err) + } + + r, _ := font.BoundString(font.Name) + super := r.Inset(-3) + + img := image.NewRGBA(super) + draw.Draw(img, super, image.White, image.ZP, draw.Src) + font.DrawString(img, image.ZP, font.Name) + + fonts[filename] = fontItem{Font: font, Preview: img} + } + + log.Println("Starting server") + http.HandleFunc("/", handle) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/bdf-sample/main.go b/bdf-sample/main.go new file mode 100644 index 0000000..ae9072f --- /dev/null +++ b/bdf-sample/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "image" + "image/draw" + "image/png" + "janouch.name/sklad/bdf" + "log" + "os" +) + +func main() { + fi, err := os.Open(os.Args[1]) + if err != nil { + log.Fatalln(err) + } + defer fi.Close() + font, err := bdf.NewFromBDF(fi) + if err != nil { + log.Fatalln(err) + } + + r, _ := font.BoundString(font.Name) + super := r.Inset(-20) + + img := image.NewRGBA(super) + draw.Draw(img, super, image.White, image.ZP, draw.Src) + font.DrawString(img, image.ZP, font.Name) + + fo, err := os.Create("out.png") + if err != nil { + log.Fatalln(err) + } + if err := png.Encode(fo, img); err != nil { + fo.Close() + log.Fatal(err) + } + if err := fo.Close(); err != nil { + log.Fatal(err) + } +} diff --git a/bdf/bdf.go b/bdf/bdf.go new file mode 100644 index 0000000..c02e31e --- /dev/null +++ b/bdf/bdf.go @@ -0,0 +1,353 @@ +package bdf + +import ( + "bufio" + "encoding/hex" + "fmt" + "image" + "image/color" + "image/draw" + "io" + "strconv" +) + +// glyph is a singular bitmap glyph to be used as a mask, assumed to directly +// correspond to a rune. A zero value is also valid and drawable. +type glyph struct { + // Coordinates are relative to the origin, on the baseline. + // The ascent is thus negative, unlike the usual model. + bounds image.Rectangle + bitmap []byte + advance int +} + +// ColorModel implements image.Image. +func (g *glyph) ColorModel() color.Model { return color.Alpha16Model } + +// Bounds implements image.Image. +func (g *glyph) Bounds() image.Rectangle { return g.bounds } + +// At implements image.Image. This is going to be somewhat slow. +func (g *glyph) At(x, y int) color.Color { + x -= g.bounds.Min.X + y -= g.bounds.Min.Y + + dx, dy := g.bounds.Dx(), g.bounds.Dy() + if x < 0 || y < 0 || x >= dx || y >= dy { + return color.Transparent + } + + stride, offset, bit := (dx+7)/8, x/8, byte(1<<uint(7-x%8)) + if g.bitmap[y*stride+offset]&bit == 0 { + return color.Transparent + } + return color.Opaque +} + +// ----------------------------------------------------------------------------- + +// Font represents a particular bitmap font. +type Font struct { + Name string + glyphs map[rune]glyph + fallback glyph +} + +// FindGlyph returns the best glyph to use for the given rune. +// The returned boolean indicates whether a fallback has been used. +func (f *Font) FindGlyph(r rune) (glyph, bool) { + if g, ok := f.glyphs[r]; ok { + return g, true + } + return f.fallback, false +} + +// DrawString draws the specified text string onto dst horizontally along +// the baseline starting at dp, using black color. +func (f *Font) DrawString(dst draw.Image, dp image.Point, s string) { + for _, r := range s { + g, _ := f.FindGlyph(r) + draw.DrawMask(dst, g.bounds.Add(dp), + image.Black, image.ZP, &g, g.bounds.Min, draw.Over) + dp.X += g.advance + } +} + +// BoundString measures the text's bounds when drawn along the X axis +// for the baseline. Also returns the total advance. +func (f *Font) BoundString(s string) (image.Rectangle, int) { + var ( + bounds image.Rectangle + dot image.Point + ) + for _, r := range s { + g, _ := f.FindGlyph(r) + bounds = bounds.Union(g.bounds.Add(dot)) + dot.X += g.advance + } + return bounds, dot.X +} + +// ----------------------------------------------------------------------------- + +func latin1ToUTF8(latin1 []byte) string { + buf := make([]rune, len(latin1)) + for i, b := range latin1 { + buf[i] = rune(b) + } + return string(buf) +} + +// tokenize splits a BDF line into tokens. Quoted strings may start anywhere +// on the line. We only enforce that they must end somewhere. +func tokenize(s string) (tokens []string, err error) { + token, quotes, escape := []rune{}, false, false + for _, r := range s { + switch { + case escape: + switch r { + case '"': + escape = false + token = append(token, r) + case ' ', '\t': + quotes, escape = false, false + tokens = append(tokens, string(token)) + token = nil + default: + quotes, escape = false, false + token = append(token, r) + } + case quotes: + switch r { + case '"': + escape = true + default: + token = append(token, r) + } + default: + switch r { + case '"': + // We could also enable quote processing on demand, + // so that it is only turned on in properties. + if len(tokens) < 1 || tokens[0] != "COMMENT" { + quotes = true + } else { + token = append(token, r) + } + case ' ', '\t': + if len(token) > 0 { + tokens = append(tokens, string(token)) + token = nil + } + default: + token = append(token, r) + } + } + } + if quotes && !escape { + return nil, fmt.Errorf("strings may not contain newlines") + } + if quotes || len(token) > 0 { + tokens = append(tokens, string(token)) + } + return tokens, nil +} + +// ----------------------------------------------------------------------------- + +// bdfParser is a basic and rather lenient parser of +// Bitmap Distribution Format (BDF) files. +type bdfParser struct { + scanner *bufio.Scanner // input reader + line int // current line number + tokens []string // tokens on the current line + font *Font // glyph storage + + defaultBounds image.Rectangle + defaultAdvance int + defaultChar int +} + +// readLine reads the next line and splits it into tokens. +// Panics on error, returns false if the end of file has been reached normally. +func (p *bdfParser) readLine() bool { + p.line++ + if !p.scanner.Scan() { + if err := p.scanner.Err(); err != nil { + panic(err) + } + p.line-- + return false + } + + var err error + if p.tokens, err = tokenize(latin1ToUTF8(p.scanner.Bytes())); err != nil { + panic(err) + } + + // Eh, it would be nicer iteratively, this may overrun the stack. + if len(p.tokens) == 0 { + return p.readLine() + } + return true +} + +func (p *bdfParser) readCharEncoding() int { + if len(p.tokens) < 2 { + panic("insufficient arguments") + } + if i, err := strconv.Atoi(p.tokens[1]); err != nil { + panic(err) + } else { + return i // Some fonts even use -1 for things outside the encoding. + } +} + +func (p *bdfParser) parseProperties() { + // The wording in the specification suggests that the argument + // with the number of properties to follow isn't reliable. + for p.readLine() && p.tokens[0] != "ENDPROPERTIES" { + switch p.tokens[0] { + case "DEFAULT_CHAR": + p.defaultChar = p.readCharEncoding() + } + } +} + +// XXX: Ignoring vertical advance since we only expect purely horizontal fonts. +func (p *bdfParser) readDwidth() int { + if len(p.tokens) < 2 { + panic("insufficient arguments") + } + if i, err := strconv.Atoi(p.tokens[1]); err != nil { + panic(err) + } else { + return i + } +} + +func (p *bdfParser) readBBX() image.Rectangle { + if len(p.tokens) < 5 { + panic("insufficient arguments") + } + w, e1 := strconv.Atoi(p.tokens[1]) + h, e2 := strconv.Atoi(p.tokens[2]) + x, e3 := strconv.Atoi(p.tokens[3]) + y, e4 := strconv.Atoi(p.tokens[4]) + if e1 != nil || e2 != nil || e3 != nil || e4 != nil { + panic("invalid arguments") + } + if w < 0 || h < 0 { + panic("bounding boxes may not have negative dimensions") + } + return image.Rectangle{ + Min: image.Point{x, -(y + h)}, + Max: image.Point{x + w, -y}, + } +} + +func (p *bdfParser) parseChar() { + g := glyph{bounds: p.defaultBounds, advance: p.defaultAdvance} + bitmap, rows, encoding := false, 0, -1 + for p.readLine() && p.tokens[0] != "ENDCHAR" { + if bitmap { + b, err := hex.DecodeString(p.tokens[0]) + if err != nil { + panic(err) + } + if len(b) != (g.bounds.Dx()+7)/8 { + panic("invalid bitmap data, width mismatch") + } + g.bitmap = append(g.bitmap, b...) + rows++ + } else { + switch p.tokens[0] { + case "ENCODING": + encoding = p.readCharEncoding() + case "DWIDTH": + g.advance = p.readDwidth() + case "BBX": + g.bounds = p.readBBX() + case "BITMAP": + bitmap = true + } + } + } + if rows != g.bounds.Dy() { + panic("invalid bitmap data, height mismatch") + } + + // XXX: We don't try to convert encodings, since we'd need x/text/encoding + // for the conversion tables, though most fonts are at least going to use + // supersets of ASCII. Use ISO10646-1 X11 fonts for proper Unicode support. + if encoding >= 0 { + p.font.glyphs[rune(encoding)] = g + } + if encoding == p.defaultChar { + p.font.fallback = g + } +} + +// https://en.wikipedia.org/wiki/Glyph_Bitmap_Distribution_Format +// https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5005.BDF_Spec.pdf +func (p *bdfParser) parse() { + if !p.readLine() || len(p.tokens) != 2 || p.tokens[0] != "STARTFONT" { + panic("invalid header") + } + if p.tokens[1] != "2.1" && p.tokens[1] != "2.2" { + panic("unsupported version number") + } + for p.readLine() && p.tokens[0] != "ENDFONT" { + switch p.tokens[0] { + case "FONT": + if len(p.tokens) < 2 { + panic("insufficient arguments") + } + p.font.Name = p.tokens[1] + case "FONTBOUNDINGBOX": + // There's no guarantee that this includes all BBXs. + p.defaultBounds = p.readBBX() + case "METRICSSET": + if len(p.tokens) < 2 { + panic("insufficient arguments") + } + if p.tokens[1] == "1" { + panic("purely vertical fonts are unsupported") + } + case "DWIDTH": + p.defaultAdvance = p.readDwidth() + case "STARTPROPERTIES": + p.parseProperties() + case "STARTCHAR": + p.parseChar() + } + } + if p.font.Name == "" { + panic("the font file doesn't contain the font's name") + } + if len(p.font.glyphs) == 0 { + panic("the font file doesn't seem to contain any glyphs") + } +} + +func NewFromBDF(r io.Reader) (f *Font, err error) { + p := bdfParser{ + scanner: bufio.NewScanner(r), + font: &Font{glyphs: make(map[rune]glyph)}, + defaultChar: -1, + } + defer func() { + if r := recover(); r != nil { + var ok bool + if err, ok = r.(error); !ok { + err = fmt.Errorf("%v", r) + } + } + if err != nil { + err = fmt.Errorf("line %d: %s", p.line, err) + } + }() + + p.parse() + return p.font, nil +} diff --git a/brother-info/main.go b/brother-info/main.go new file mode 100644 index 0000000..b65b1dc --- /dev/null +++ b/brother-info/main.go @@ -0,0 +1,207 @@ +package main + +import ( + "io" + "log" + "os" + "time" +) + +func decodeBitfieldErrors(b byte, errors [8]string) []string { + var result []string + for i := uint(0); i < 8; i++ { + if b&(1<<i) != 0 { + result = append(result, errors[i]) + } + } + return result +} + +// ----------------------------------------------------------------------------- + +type brotherStatus struct { + errors []string +} + +// TODO: What exactly do we need? Probably extend as needed. +func decodeStatusInformation(d []byte) brotherStatus { + var status brotherStatus + 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"})...) + status.errors = append(status.errors, decodeBitfieldErrors(d[9], [8]string{ + "replace media", "expansion buffer full", "communication error", + "communication buffer full", "cover open", "cancel key", + "media cannot be fed", "system error"})...) + return status +} + +// ----------------------------------------------------------------------------- + +func printStatusInformation(d []byte) { + if d[0] != 0x80 || d[1] != 0x20 || d[2] != 0x42 || d[3] != 0x34 { + log.Println("unexpected status fixed bytes") + } + + // Model code. + switch b := d[4]; b { + case 0x38: + log.Println("model: QL-800") + case 0x39: + log.Println("model: QL-810W") + case 0x41: + log.Println("model: QL-820NWB") + default: + log.Println("model:", b) + } + + // d[6] seems to be 0x00 in a real-world QL-800. + if d[5] != 0x30 || d[6] != 0x30 || d[7] != 0x00 { + log.Println("unexpected status fixed bytes") + } + + // Error information 1. + for _, e := range decodeBitfieldErrors(d[8], [8]string{ + "no media", "end of media", "cutter jam", "?", "printer in use", + "printer turned off", "high-voltage adapter", "fan motor error"}) { + log.Println("error:", e) + } + + // Error information 2. + for _, e := range decodeBitfieldErrors(d[9], [8]string{ + "replace media", "expansion buffer full", "communication error", + "communication buffer full", "cover open", "cancel key", + "media cannot be fed", "system error"}) { + log.Println("error:", e) + } + + // Media width. + log.Println("media width:", d[10], "mm") + + // Media type. + switch b := d[11]; b { + case 0x00: + log.Println("media: no media") + case 0x4a: + log.Println("media: continuous length tape") + case 0x4b: + log.Println("media: die-cut labels") + default: + log.Println("media:", b) + } + + // d[14] seems to be 0x14 in a real-world QL-800. + if d[12] != 0x00 || d[13] != 0x00 || d[14] != 0x3f { + log.Println("unexpected status fixed bytes") + } + + // Mode. + log.Println("mode:", d[15]) + + if d[16] != 0x00 { + log.Println("unexpected status fixed bytes") + } + + // Media length. + log.Println("media width:", d[17], "mm") + + // Status type. + switch b := d[18]; b { + case 0x00: + log.Println("status type: reply to status request") + case 0x01: + log.Println("status type: printing completed") + case 0x02: + log.Println("status type: error occurred") + case 0x04: + log.Println("status type: turned off") + case 0x05: + log.Println("status type: notification") + case 0x06: + log.Println("status type: phase change") + default: + log.Println("status type:", b) + } + + // Phase type. + switch b := d[19]; b { + case 0x00: + log.Println("phase state: receiving state") + case 0x01: + log.Println("phase state: printing state") + default: + log.Println("phase state:", b) + } + + // Phase number. + log.Println("phase number:", int(d[20])*256+int(d[21])) + + // Notification number. + switch b := d[22]; b { + case 0x00: + log.Println("notification number: not available") + case 0x03: + log.Println("notification number: cooling (started)") + case 0x04: + log.Println("notification number: cooling (finished)") + default: + log.Println("notification number:", b) + } + + // d[25] seems to be 0x01 in a real-world QL-800. + if d[23] != 0x00 || d[24] != 0x00 || d[25] != 0x00 || d[26] != 0x00 || + d[27] != 0x00 || d[28] != 0x00 || d[29] != 0x00 || d[30] != 0x00 || + d[31] != 0x00 { + log.Println("unexpected status fixed bytes") + } +} + +func main() { + // 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 { + log.Fatalln(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 { + log.Fatalln(err) + } + + // Initialize. + if _, err := f.WriteString("\x1b\x40"); err != nil { + log.Fatalln(err) + } + + // Request status information. + if _, err := f.WriteString("\x1b\x69\x53"); err != nil { + log.Fatalln(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 { + log.Fatalln(err) + } else if n < 32 { + log.Fatalln("invalid read") + } else { + break + } + } + + printStatusInformation(status) +} @@ -0,0 +1,5 @@ +module janouch.name/sklad + +go 1.12 + +require github.com/boombuler/barcode v1.0.0 @@ -0,0 +1,2 @@ +github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 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<<i) != 0 { + result = append(result, errors[i]) + } + } + return result +} + +func printStatusInformation(d []byte) { + log.Println("-- status") + + // Error information 1. + for _, e := range decodeBitfieldErrors(d[8], [8]string{ + "no media", "end of media", "cutter jam", "?", "printer in use", + "printer turned off", "high-voltage adapter", "fan motor error"}) { + log.Println("error:", e) + } + + // Error information 2. + for _, e := range decodeBitfieldErrors(d[9], [8]string{ + "replace media", "expansion buffer full", "communication error", + "communication buffer full", "cover open", "cancel key", + "media cannot be fed", "system error"}) { + log.Println("error:", e) + } + + // Media width. + log.Println("media width:", d[10], "mm") + + // Media type. + switch b := d[11]; b { + case 0x00: + log.Println("media: no media") + case 0x4a: + log.Println("media: continuous length tape") + case 0x4b: + log.Println("media: die-cut labels") + default: + log.Println("media:", b) + } + + // Mode. + log.Println("mode:", d[15]) + + // Media length. + log.Println("media width:", d[17], "mm") + + // Status type. + switch b := d[18]; b { + case 0x00: + log.Println("status type: reply to status request") + case 0x01: + log.Println("status type: printing completed") + case 0x02: + log.Println("status type: error occurred") + case 0x04: + log.Println("status type: turned off") + case 0x05: + log.Println("status type: notification") + case 0x06: + log.Println("status type: phase change") + default: + log.Println("status type:", b) + } + + // Phase type. + switch b := d[19]; b { + case 0x00: + log.Println("phase state: receiving state") + case 0x01: + log.Println("phase state: printing state") + default: + log.Println("phase state:", b) + } + + // Phase number. + log.Println("phase number:", int(d[20])*256+int(d[21])) + + // Notification number. + switch b := d[22]; b { + case 0x00: + log.Println("notification number: not available") + case 0x03: + log.Println("notification number: cooling (started)") + case 0x04: + log.Println("notification number: cooling (finished)") + default: + log.Println("notification number:", b) + } +} + +// 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. + bounds := src.Bounds() + pixels := make([]bool, 720) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + off := offset + for x := bounds.Max.X - 1; x >= 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(` + <!DOCTYPE html> + <html><body> + <table><tr> + <td valign=top> + <img border=1 src='?img&width={{.Width}}&text={{.Text}}'> + </td> + <td valign=top> + <form><fieldset> + <p><label for=width>Tape width in pt:</label> + <input id=width name=width value='{{.Width}}'> + <p><label for=text>Text:</label> + <input id=text name=text value='{{.Text}}'> + <p><input type=submit value='Update'> + <input type=checkbox id=print name=print> + <label for=print>Print label</label> + </fieldset></form> + </td> + </tr></table> + </body></html> +`)) + +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)) +} |