commit ab253ce768c410f6c996fd3df98292df0554c2f3 Author: Přemysl Janouch Date: Sat Apr 6 21:31:11 2019 +0200 Initial commit 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(` + + + + + + + +{{range $k, $v := . }} + + + + +{{end}} +
NamePreview
{{$k}}
+ +`)) + +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< 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<= 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)) +}