diff --git a/.gitignore b/.gitignore index 16a33ed..396995d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -epos-server/epos-server +/cmd/epos-server/epos-server +/cmd/epos-server/epos-server.exe diff --git a/README.md b/README.md index 16e98ad..da1776d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # About escpos # -This is a simple [Golang](http://www.golang.org/project) package that provides -[ESC-POS](https://en.wikipedia.org/wiki/ESC/P) library functions to help with -sending control codes to a ESC-POS capable printer such as an Epson TM-T82 or -similar. +This is a simple [Go][1] package that provides [ESC-POS][2] library functions +to help with sending control codes to a ESC-POS capable printer such as an +Epson TM-T82 or similar. These printers are often used in retail environments in conjunction with a point-of-sale (POS) system. @@ -12,14 +11,13 @@ point-of-sale (POS) system. Install the package via the following: - go get -u github.com/knq/escpos + go get -u github.com/kenshaw/escpos ## Example epos-server ## -An example EPOS server implementation is available in the -[epos-server](epos-server) subdirectory of this project. This example -server is more or less compatible with [Epson TM-Intelligent](https://c4b.epson-biz.com) -printers and print server implementations. +An example EPOS server implementation is available in the [cmd/epos-server][3] +subdirectory of this project. This example server is more or less compatible +with [Epson TM-Intelligent][4] printers and print server implementations. ## Usage ## @@ -32,7 +30,7 @@ import ( "bufio" "os" - "github.com/knq/escpos" + "github.com/kenshaw/escpos" ) func main() { @@ -86,5 +84,8 @@ func main() { ## TODO - Fix barcode/image support -- Update code to be idiomatic Go -- Fix example server implementation + +[1]: http://www.golang.org/project +[2]: https://en.wikipedia.org/wiki/ESC/P +[3]: cmd/epos-server +[4]: https://c4b.epson-biz.com diff --git a/epos-server/README.md b/cmd/epos-server/README.md similarity index 71% rename from epos-server/README.md rename to cmd/epos-server/README.md index 46ff4cc..7774618 100644 --- a/epos-server/README.md +++ b/cmd/epos-server/README.md @@ -1,10 +1,9 @@ # About epos-server # -This is a quick and dirty Golang implementation of a [Epson TM-Intelligent](https://c4b.epson-biz.com/) -print server. This also serves as example code for the -[escpos](https://github.com/knq/escpos) package. This is "more-or-less" -compatible with the ePOS-Print API and shows how ePOS-XML is translated into -simple ESCPOS data. +This is a quick and dirty Golang implementation of a [Epson TM-Intelligent][1] +print server. This also serves as example code for the [escpos][2] package. +This is "more-or-less" compatible with the ePOS-Print API and shows how +ePOS-XML is translated into simple ESCPOS data. This has been tested and works as expected on Linux. @@ -20,12 +19,11 @@ like the following for your system: Then install via the following: - go get -u github.com/knq/escpos/epos-server - + go get -u github.com/kenshaw/escpos/epos-server You should then be able to build the epos-server like this: - go build github.com/knq/escpos/epos-server + go build github.com/kenshaw/escpos/epos-server ## Usage ## @@ -46,3 +44,6 @@ printer: The following still needs to be implemented: * Fix image decoding and printing + +[1]: https://c4b.epson-biz.com/ +[2]: https://github.com/kenshaw/escpos diff --git a/cmd/epos-server/main.go b/cmd/epos-server/main.go new file mode 100644 index 0000000..c16774b --- /dev/null +++ b/cmd/epos-server/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "flag" + "log" + "net/http" + "os" + + "github.com/kenshaw/escpos" +) + +var ( + flagListen = flag.String("l", "127.0.22.8:80", "listen") + flagEndpoint = flag.String("endpoint", escpos.DefaultEndpoint, "endpoint") + flagPrinter = flag.String("p", "", "path to printer") +) + +func main() { + flag.Parse() + + if *flagPrinter == "" { + log.Fatal("must specify path to printer via -p") + } + + // open printer + f, err := os.Create(*flagPrinter) + if err != nil { + panic(err) + } + defer f.Close() + + // create printer + ep, err := escpos.NewPrinter(f) + if err != nil { + log.Fatal(err) + } + + // create server + s, err := escpos.NewServer(ep) + if err != nil { + log.Fatal(err) + } + + // set up mux + mux := http.NewServeMux() + mux.Handle(*flagEndpoint, s) + mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { + http.Error(res, http.StatusText(http.StatusNotFound), http.StatusNotFound) + }) + + log.Fatal(http.ListenAndServe(*flagListen, mux)) +} diff --git a/epos-server/epos-server.go b/epos-server/epos-server.go deleted file mode 100644 index e108591..0000000 --- a/epos-server/epos-server.go +++ /dev/null @@ -1,159 +0,0 @@ -package main - -import ( - "bufio" - "errors" - "flag" - "fmt" - "io/ioutil" - "log" - "net/http" - "os" - - "github.com/gorilla/mux" - "github.com/knq/escpos" - "github.com/moovweb/gokogiri" - "github.com/moovweb/gokogiri/xml" - "github.com/moovweb/gokogiri/xpath" -) - -var listenAddr = flag.String("l", "127.0.22.8", "Address to listen on") -var port = flag.Int("port", 80, "Port to listen on") -var printerPath = flag.String("p", "/dev/usb/lp0", "Path to printer") - -type EposServer struct { - r *mux.Router - printer *escpos.Escpos - printerWriter *bufio.Writer -} - -func writeSoapResponse(rw http.ResponseWriter, req *http.Request, code string) { - success_str := "false" - if code == "" { - success_str = "true" - } - - response := fmt.Sprintf(` - - - - -`, success_str, code) - - log.Printf("Sending:\n%s\n", response) - - // inject response - rw.Header().Set("Content-Type", req.Header.Get("Content-Type")) - rw.Write([]byte(response)) -} - -func getEposNodes(doc *xml.XmlDocument) (retnodes []xml.Node, err error) { - // grab the 'Body' element - path := xpath.Compile("*[local-name()='Body']") - nodes, e := doc.Root().Search(path) - if e != nil { - err = e - return - } - - // check that the data is present - if len(nodes) < 1 || nodes[0].CountChildren() < 1 { - err = errors.New("bad data") - return - } - - // get epos data - return nodes[0].FirstChild().Search("./*") -} - -func (s *EposServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - // send origin headers - if origin := req.Header.Get("Origin"); origin != "" { - rw.Header().Set("Access-Control-Allow-Origin", origin) - rw.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") - rw.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, If-Modified-Since, SOAPAction") - } - - // stop if its options - if req.Method == "OPTIONS" { - log.Printf("OPTIONS %s\n", req.URL) - return - } - - // handle crappy soap action - if req.Method == "POST" { - // grab posted body - data, _ := ioutil.ReadAll(req.Body) - log.Printf("POST %s:\n%s\n\n", req.URL, string(data)) - - // parse xml with gokogiri - doc, _ := gokogiri.ParseXml(data) - defer doc.Free() - - // load print nodes from xml doc - epos_nodes, err := getEposNodes(doc) - if err != nil { - rw.WriteHeader(503) - log.Fatal(err) - return - } - - // init printer - s.printer.Init() - - // loop over nodes - for _, en := range epos_nodes { - // grab name and inner text - name := en.Name() - content := en.Content() - - // grab parameters - params := make(map[string]string) - for _, attr := range en.Attributes() { - params[attr.Name()] = attr.Value() - } - - // write data to printer - s.printer.WriteNode(name, params, content) - } - - // end - s.printer.End() - - // flush writer - s.printerWriter.Flush() - - //rw.WriteHeader(402) - // write soap response - writeSoapResponse(rw, req, "") - - return - } - - // force an error for everything else - rw.WriteHeader(403) - - // Lets Gorilla work - s.r.ServeHTTP(rw, req) -} - -func main() { - flag.Parse() - - // open printer - f, err := os.Create(*printerPath) - if err != nil { - panic(err) - } - defer f.Close() - - // setup buffered writer - w := bufio.NewWriter(f) - ep := escpos.New(w) - - // set up service router - r := mux.NewRouter() - http.Handle("/cgi-bin/epos/service.cgi", &EposServer{r, ep, w}) - - log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *listenAddr, *port), nil)) -} diff --git a/escpos.go b/escpos.go index df73667..57ebb3a 100644 --- a/escpos.go +++ b/escpos.go @@ -2,134 +2,112 @@ package escpos import ( "encoding/base64" + "errors" "fmt" "io" "log" "strconv" "strings" + "sync" ) -// text replacement map -var textReplaceMap = map[string]string{ - // horizontal tab - " ": "\x09", - " ": "\x09", - - // linefeed - " ": "\n", - " ": "\n", - - // xml stuff - "'": "'", - """: `"`, - ">": ">", - "<": "<", - - // ampersand must be last to avoid double decoding - "&": "&", -} - -// replace text from the above map -func textReplace(data string) string { - for k, v := range textReplaceMap { - data = strings.Replace(data, k, v, -1) - } - return data -} - -type Escpos struct { +// Printer wraps sending ESC-POS commands to a io.Writer. +type Printer struct { // destination - dst io.Writer + w io.Writer // font metrics - width, height uint8 + width, height byte // state toggles ESC[char] - underline uint8 - emphasize uint8 - upsidedown uint8 - rotate uint8 + underline byte + emphasize byte + upsidedown byte + rotate byte // state toggles GS[char] - reverse, smooth uint8 + reverse, smooth byte + + sync.Mutex } -// reset toggles -func (e *Escpos) reset() { - e.width = 1 - e.height = 1 +// NewPrinter creates a new printer using the specified writer. +func NewPrinter(w io.Writer /*, opts ...PrinterOption*/) (*Printer, error) { + if w == nil { + return nil, errors.New("must supply valid writer") + } - e.underline = 0 - e.emphasize = 0 - e.upsidedown = 0 - e.rotate = 0 + p := &Printer{ + w: w, + width: 1, + height: 1, + } - e.reverse = 0 - e.smooth = 0 + return p, nil } -// create Escpos printer -func New(dst io.Writer) (e *Escpos) { - e = &Escpos{dst: dst} - e.reset() - return -} +// Reset resets the printer state. +func (p *Printer) Reset() { + p.width = 1 + p.height = 1 -// write raw bytes to printer -func (e *Escpos) WriteRaw(data []byte) (n int, err error) { - if len(data) > 0 { - log.Printf("Writing %d bytes\n", len(data)) - e.dst.Write(data) - } else { - log.Printf("Wrote NO bytes\n") - } + p.underline = 0 + p.emphasize = 0 + p.upsidedown = 0 + p.rotate = 0 - return 0, nil + p.reverse = 0 + p.smooth = 0 } -// write a string to the printer -func (e *Escpos) Write(data string) (int, error) { - return e.WriteRaw([]byte(data)) +// Write writes buf to printer. +func (p *Printer) Write(buf []byte) (int, error) { + return p.w.Write(buf) } -// init/reset printer settings -func (e *Escpos) Init() { - e.reset() - e.Write("\x1B@") +// WriteString writes a string to the printer. +func (p *Printer) WriteString(s string) (int, error) { + return p.w.Write([]byte(s)) } -// end output -func (e *Escpos) End() { - e.Write("\xFA") +// Init resets the state of the printer, and writes the initialize code. +func (p *Printer) Init() { + p.Reset() + p.WriteString("\x1B@") } -// send cut -func (e *Escpos) Cut() { - e.Write("\x1DVA0") +// End terminates the printer session. +func (p *Printer) End() { + p.WriteString("\xFA") } -// send cash -func (e *Escpos) Cash() { - e.Write("\x1B\x70\x00\x0A\xFF") +// Cut writes the cut code to the printer. +func (p *Printer) Cut() { + p.WriteString("\x1DVA0") } -// send linefeed -func (e *Escpos) Linefeed() { - e.Write("\n") +// Cash writes the cash code to the printer. +func (p *Printer) Cash() { + p.WriteString("\x1B\x70\x00\x0A\xFF") } -// send N formfeeds -func (e *Escpos) FormfeedN(n int) { - e.Write(fmt.Sprintf("\x1Bd%c", n)) +// Linefeed writes a line end to the printer. +func (p *Printer) Linefeed() { + p.WriteString("\n") } -// send formfeed -func (e *Escpos) Formfeed() { - e.FormfeedN(1) +// FormfeedN writes N formfeeds to the printer. +func (p *Printer) FormfeedN(n int) { + p.WriteString(fmt.Sprintf("\x1Bd%c", n)) } -// set font -func (e *Escpos) SetFont(font string) { +// Formfeed writes 1 formfeed to the printer. +func (p *Printer) Formfeed() { + p.FormfeedN(1) +} + +// SetFont sets the font on the printer. +func (p *Printer) SetFont(font string) { f := 0 switch font { @@ -144,108 +122,108 @@ func (e *Escpos) SetFont(font string) { f = 0 } - e.Write(fmt.Sprintf("\x1BM%c", f)) + p.WriteString(fmt.Sprintf("\x1BM%c", f)) } -func (e *Escpos) SendFontSize() { - e.Write(fmt.Sprintf("\x1D!%c", ((e.width-1)<<4)|(e.height-1))) +// SendFontSize sends the font size command to the printer. +func (p *Printer) SendFontSize() { + p.WriteString(fmt.Sprintf("\x1D!%c", ((p.width-1)<<4)|(p.height-1))) } -// set font size -func (e *Escpos) SetFontSize(width, height uint8) { +// SetFontSize sets the font size state and sends the command to the printer. +func (p *Printer) SetFontSize(width, height byte) { if width > 0 && height > 0 && width <= 8 && height <= 8 { - e.width = width - e.height = height - e.SendFontSize() + p.width, p.height = width, height + p.SendFontSize() } else { log.Fatalf("Invalid font size passed: %d x %d", width, height) } } -// send underline -func (e *Escpos) SendUnderline() { - e.Write(fmt.Sprintf("\x1B-%c", e.underline)) +// SendUnderline sends the underline command to the printer. +func (p *Printer) SendUnderline() { + p.WriteString(fmt.Sprintf("\x1B-%c", p.underline)) } -// send emphasize / doublestrike -func (e *Escpos) SendEmphasize() { - e.Write(fmt.Sprintf("\x1BG%c", e.emphasize)) +// SendEmphasize sends the emphasize / doublestrike command to the printer. +func (p *Printer) SendEmphasize() { + p.WriteString(fmt.Sprintf("\x1BG%c", p.emphasize)) } -// send upsidedown -func (e *Escpos) SendUpsidedown() { - e.Write(fmt.Sprintf("\x1B{%c", e.upsidedown)) +// SendUpsidedown sends the upsidedown command to the printer. +func (p *Printer) SendUpsidedown() { + p.WriteString(fmt.Sprintf("\x1B{%c", p.upsidedown)) } -// send rotate -func (e *Escpos) SendRotate() { - e.Write(fmt.Sprintf("\x1BR%c", e.rotate)) +// SendRotate sends the rotate command to the printer. +func (p *Printer) SendRotate() { + p.WriteString(fmt.Sprintf("\x1BR%c", p.rotate)) } -// send reverse -func (e *Escpos) SendReverse() { - e.Write(fmt.Sprintf("\x1DB%c", e.reverse)) +// SendReverse sends the reverse command to the printer. +func (p *Printer) SendReverse() { + p.WriteString(fmt.Sprintf("\x1DB%c", p.reverse)) } -// send smooth -func (e *Escpos) SendSmooth() { - e.Write(fmt.Sprintf("\x1Db%c", e.smooth)) +// SendSmooth sends the smooth command to the printer. +func (p *Printer) SendSmooth() { + p.WriteString(fmt.Sprintf("\x1Db%c", p.smooth)) } -// send move x -func (e *Escpos) SendMoveX(x uint16) { - e.Write(string([]byte{0x1b, 0x24, byte(x % 256), byte(x / 256)})) +// SendMoveX sends the move x command to the printer. +func (p *Printer) SendMoveX(x uint16) { + p.Write([]byte{0x1b, 0x24, byte(x % 256), byte(x / 256)}) } -// send move y -func (e *Escpos) SendMoveY(y uint16) { - e.Write(string([]byte{0x1d, 0x24, byte(y % 256), byte(y / 256)})) +// SendMoveY sends the move y command to the printer. +func (p *Printer) SendMoveY(y uint16) { + p.Write([]byte{0x1d, 0x24, byte(y % 256), byte(y / 256)}) } -// set underline -func (e *Escpos) SetUnderline(v uint8) { - e.underline = v - e.SendUnderline() +// SetUnderline sets the underline state and sends it to the printer. +func (p *Printer) SetUnderline(v byte) { + p.underline = v + p.SendUnderline() } -// set emphasize -func (e *Escpos) SetEmphasize(u uint8) { - e.emphasize = u - e.SendEmphasize() +// SetEmphasize sets the emphasize state and sends it to the printer. +func (p *Printer) SetEmphasize(u byte) { + p.emphasize = u + p.SendEmphasize() } -// set upsidedown -func (e *Escpos) SetUpsidedown(v uint8) { - e.upsidedown = v - e.SendUpsidedown() +// SetUpsidedown sets the upsidedown state and sends it to the printer. +func (p *Printer) SetUpsidedown(v byte) { + p.upsidedown = v + p.SendUpsidedown() } -// set rotate -func (e *Escpos) SetRotate(v uint8) { - e.rotate = v - e.SendRotate() +// SetRotate sets the rotate state and sends it to the printer. +func (p *Printer) SetRotate(v byte) { + p.rotate = v + p.SendRotate() } -// set reverse -func (e *Escpos) SetReverse(v uint8) { - e.reverse = v - e.SendReverse() +// SetReverse sets the reverse state and sends it to the printer. +func (p *Printer) SetReverse(v byte) { + p.reverse = v + p.SendReverse() } -// set smooth -func (e *Escpos) SetSmooth(v uint8) { - e.smooth = v - e.SendSmooth() +// SetSmooth sets the smooth state and sends it to the printer. +func (p *Printer) SetSmooth(v byte) { + p.smooth = v + p.SendSmooth() } -// pulse (open the drawer) -func (e *Escpos) Pulse() { +// Pulse sends the pulse (open drawer) code to the printer. +func (p *Printer) Pulse() { // with t=2 -- meaning 2*2msec - e.Write("\x1Bp\x02") + p.WriteString("\x1Bp\x02") } -// set alignment -func (e *Escpos) SetAlign(align string) { +// SetAlign sets the alignment state and sends it to the printer. +func (p *Printer) SetAlign(align string) { a := 0 switch align { case "left": @@ -257,11 +235,11 @@ func (e *Escpos) SetAlign(align string) { default: log.Fatalf("Invalid alignment: %s", align) } - e.Write(fmt.Sprintf("\x1Ba%c", a)) + p.WriteString(fmt.Sprintf("\x1Ba%c", a)) } -// set language -- ESC R -func (e *Escpos) SetLang(lang string) { +// SetLang sets the language state and sends it to the printer. +func (p *Printer) SetLang(lang string) { l := 0 switch lang { @@ -288,66 +266,66 @@ func (e *Escpos) SetLang(lang string) { default: log.Fatalf("Invalid language: %s", lang) } - e.Write(fmt.Sprintf("\x1BR%c", l)) -} -// do a block of text -func (e *Escpos) Text(params map[string]string, data string) { + p.WriteString(fmt.Sprintf("\x1BR%c", l)) +} +// Text sends a block of text to the printer using the formatting parameters in params. +func (p *Printer) Text(params map[string]string, text string) { // send alignment to printer if align, ok := params["align"]; ok { - e.SetAlign(align) + p.SetAlign(align) } // set lang if lang, ok := params["lang"]; ok { - e.SetLang(lang) + p.SetLang(lang) } // set smooth if smooth, ok := params["smooth"]; ok && (smooth == "true" || smooth == "1") { - e.SetSmooth(1) + p.SetSmooth(1) } // set emphasize if em, ok := params["em"]; ok && (em == "true" || em == "1") { - e.SetEmphasize(1) + p.SetEmphasize(1) } // set underline if ul, ok := params["ul"]; ok && (ul == "true" || ul == "1") { - e.SetUnderline(1) + p.SetUnderline(1) } // set reverse if reverse, ok := params["reverse"]; ok && (reverse == "true" || reverse == "1") { - e.SetReverse(1) + p.SetReverse(1) } // set rotate if rotate, ok := params["rotate"]; ok && (rotate == "true" || rotate == "1") { - e.SetRotate(1) + p.SetRotate(1) } // set font if font, ok := params["font"]; ok { - e.SetFont(strings.ToUpper(font[5:6])) + p.SetFont(strings.ToUpper(font[5:6])) } // do dw (double font width) if dw, ok := params["dw"]; ok && (dw == "true" || dw == "1") { - e.SetFontSize(2, e.height) + p.SetFontSize(2, p.height) } // do dh (double font height) if dh, ok := params["dh"]; ok && (dh == "true" || dh == "1") { - e.SetFontSize(e.width, 2) + p.SetFontSize(p.width, 2) } // do font width if width, ok := params["width"]; ok { if i, err := strconv.Atoi(width); err == nil { - e.SetFontSize(uint8(i), e.height) + p.SetFontSize(byte(i), p.height) } else { log.Fatalf("Invalid font width: %s", width) } @@ -356,7 +334,7 @@ func (e *Escpos) Text(params map[string]string, data string) { // do font height if height, ok := params["height"]; ok { if i, err := strconv.Atoi(height); err == nil { - e.SetFontSize(e.width, uint8(i)) + p.SetFontSize(p.width, byte(i)) } else { log.Fatalf("Invalid font height: %s", height) } @@ -365,7 +343,7 @@ func (e *Escpos) Text(params map[string]string, data string) { // do y positioning if x, ok := params["x"]; ok { if i, err := strconv.Atoi(x); err == nil { - e.SendMoveX(uint16(i)) + p.SendMoveX(uint16(i)) } else { log.Fatalf("Invalid x param %s", x) } @@ -374,25 +352,24 @@ func (e *Escpos) Text(params map[string]string, data string) { // do y positioning if y, ok := params["y"]; ok { if i, err := strconv.Atoi(y); err == nil { - e.SendMoveY(uint16(i)) + p.SendMoveY(uint16(i)) } else { log.Fatalf("Invalid y param %s", y) } } // do text replace, then write data - data = textReplace(data) - if len(data) > 0 { - e.Write(data) + if len(text) > 0 { + p.WriteString(textReplacer.Replace(text)) } } -// feed the printer -func (e *Escpos) Feed(params map[string]string) { +// Feed feeds the printer, applying the supplied params as necessary. +func (p *Printer) Feed(params map[string]string) { // handle lines (form feed X lines) if l, ok := params["line"]; ok { if i, err := strconv.Atoi(l); err == nil { - e.FormfeedN(i) + p.FormfeedN(i) } else { log.Fatalf("Invalid line number %s", l) } @@ -401,40 +378,41 @@ func (e *Escpos) Feed(params map[string]string) { // handle units (dots) if u, ok := params["unit"]; ok { if i, err := strconv.Atoi(u); err == nil { - e.SendMoveY(uint16(i)) + p.SendMoveY(uint16(i)) } else { log.Fatalf("Invalid unit number %s", u) } } // send linefeed - e.Linefeed() + p.Linefeed() // reset variables - e.reset() + p.Reset() // reset printer - e.SendEmphasize() - e.SendRotate() - e.SendSmooth() - e.SendReverse() - e.SendUnderline() - e.SendUpsidedown() - e.SendFontSize() - e.SendUnderline() -} - -// feed and cut based on parameters -func (e *Escpos) FeedAndCut(params map[string]string) { + p.SendEmphasize() + p.SendRotate() + p.SendSmooth() + p.SendReverse() + p.SendUnderline() + p.SendUpsidedown() + p.SendFontSize() + p.SendUnderline() +} + +// FeedAndCut feeds the printer using the supplied params and then sends a cut +// command. +func (p *Printer) FeedAndCut(params map[string]string) { if t, ok := params["type"]; ok && t == "feed" { - e.Formfeed() + p.Formfeed() } - e.Cut() + p.Cut() } // Barcode sends a barcode to the printer. -func (e *Escpos) Barcode(barcode string, format int) { +func (p *Printer) Barcode(barcode string, format int) { code := "" switch format { case 0: @@ -452,34 +430,34 @@ func (e *Escpos) Barcode(barcode string, format int) { } // reset settings - e.reset() + p.Reset() // set align - e.SetAlign("center") + p.SetAlign("center") // write barcode if format > 69 { - e.Write(fmt.Sprintf("\x1dk"+code+"%v%v", len(barcode), barcode)) + p.WriteString(fmt.Sprintf("\x1dk"+code+"%v%v", len(barcode), barcode)) } else if format < 69 { - e.Write(fmt.Sprintf("\x1dk"+code+"%v\x00", barcode)) + p.WriteString(fmt.Sprintf("\x1dk"+code+"%v\x00", barcode)) } - e.Write(fmt.Sprintf("%v", barcode)) + p.WriteString(barcode) } -// used to send graphics headers -func (e *Escpos) gSend(m byte, fn byte, data []byte) { +// gSendsend graphics headers. +func (p *Printer) gSend(m byte, fn byte, data []byte) { l := len(data) + 2 - e.Write("\x1b(L") - e.WriteRaw([]byte{byte(l % 256), byte(l / 256), m, fn}) - e.WriteRaw(data) + p.WriteString("\x1b(L") + p.Write([]byte{byte(l % 256), byte(l / 256), m, fn}) + p.Write(data) } -// write an image -func (e *Escpos) Image(params map[string]string, data string) { +// Image writes an image using the supplied params. +func (p *Printer) Image(params map[string]string, data string) { // send alignment to printer if align, ok := params["align"]; ok { - e.SetAlign(align) + p.SetAlign(align) } // get width @@ -530,16 +508,16 @@ func (e *Escpos) Image(params map[string]string, data string) { a := append(header, dec...) - e.gSend(byte('0'), byte('p'), a) - e.gSend(byte('0'), byte('2'), []byte{}) - + p.gSend(byte('0'), byte('p'), a) + p.gSend(byte('0'), byte('2'), []byte{}) } -// write a "node" to the printer -func (e *Escpos) WriteNode(name string, params map[string]string, data string) { +// WriteNode writes a node of type name with the supplied params and data to +// the printer. +func (p *Printer) WriteNode(name string, params map[string]string, data string) { cstr := "" if data != "" { - str := data[:] + str := data if len(data) > 40 { str = fmt.Sprintf("%s ...", data[0:40]) } @@ -549,14 +527,39 @@ func (e *Escpos) WriteNode(name string, params map[string]string, data string) { switch name { case "text": - e.Text(params, data) + p.Text(params, data) + case "feed": - e.Feed(params) + p.Feed(params) + case "cut": - e.FeedAndCut(params) + p.FeedAndCut(params) + case "pulse": - e.Pulse() + p.Pulse() + case "image": - e.Image(params, data) + p.Image(params, data) } } + +// textReplacer is a simple text replacer for the only valid XML encoded +// entities for escpos printers. +var textReplacer = strings.NewReplacer( + // horizontal tab + " ", "\x09", + " ", "\x09", + + // linefeed + " ", "\n", + " ", "\n", + + // xml entities + "'", "'", + """, `"`, + ">", ">", + "<", "<", + + // & (ampersand) must be last to avoid double decoding + "&", "&", +) diff --git a/example/all-in-one-print/README.md b/example/all-in-one-print/README.md new file mode 100644 index 0000000..af89e47 --- /dev/null +++ b/example/all-in-one-print/README.md @@ -0,0 +1 @@ +Must be run from this folder to get the image. diff --git a/example/all-in-one-print/crab_nebula.jpg b/example/all-in-one-print/crab_nebula.jpg new file mode 100644 index 0000000..61d156e Binary files /dev/null and b/example/all-in-one-print/crab_nebula.jpg differ diff --git a/example/all-in-one-print/main.go b/example/all-in-one-print/main.go new file mode 100644 index 0000000..5269437 --- /dev/null +++ b/example/all-in-one-print/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "flag" + "fmt" + "image" + "log" + "os" + + _ "image/jpeg" + + "github.com/kenshaw/escpos" + "github.com/kenshaw/escpos/raster" +) + +var ( + lpDev = flag.String("p", "/dev/usb/lp0", "Printer dev file") + maxWidth = flag.Int("printer-max-width", 512, "Printer max width in pixels") + + ep *escpos.Printer +) + +func main() { + flag.Parse() + + f, err := os.OpenFile(*lpDev, os.O_WRONLY, 0) + if err != nil { + log.Fatal(err) + } + + ep, err = escpos.NewPrinter(f) + if err != nil { + log.Fatal(err) + } + + ep.Init() + + defer func() { + ep.Cut() + ep.End() + }() + + ep.Text(nil, "sample text...\n\n") + + for _, font := range []string{"A", "B", "C"} { + ep.SetFont(font) + ep.Text(nil, fmt.Sprintf("sample text, font %s...\n\n", font)) + } + ep.SetFont("B") + + for _, format := range []int{0, 1, 2, 3, 4, 73} { + ep.Text(nil, fmt.Sprintf("sample barcode, format %d:\n", format)) + ep.Barcode("123456", format) + ep.Linefeed() + } + + ep.Text(nil, "sample image:\n") + rasterImage() + + ep.Text(nil, "cash code:\n\n") + ep.Cash() +} + +func rasterImage() { + imgFile, err := os.Open("crab_nebula.jpg") + if err != nil { + log.Fatal(err) + } + + img, imgFormat, err := image.Decode(imgFile) + imgFile.Close() + if err != nil { + log.Fatal(err) + } + + log.Print("Loaded image, format: ", imgFormat) + + rasterConv := &raster.Converter{ + MaxWidth: *maxWidth, + Threshold: 0.5, + } + + rasterConv.Print(img, ep) +} diff --git a/example/all-in-one-print/outputs/Epson_TM-T88V_M244A.jpg b/example/all-in-one-print/outputs/Epson_TM-T88V_M244A.jpg new file mode 100644 index 0000000..5709a21 Binary files /dev/null and b/example/all-in-one-print/outputs/Epson_TM-T88V_M244A.jpg differ diff --git a/example/image2pos/main.go b/example/image2pos/main.go new file mode 100644 index 0000000..b139abc --- /dev/null +++ b/example/image2pos/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "flag" + "image" + "log" + "os" + + _ "image/gif" + _ "image/jpeg" + _ "image/png" + + "github.com/kenshaw/escpos" + "github.com/kenshaw/escpos/raster" +) + +var ( + lpDev = flag.String("p", "/dev/usb/lp0", "Printer dev file") + imgPath = flag.String("i", "image.png", "Input image") + threshold = flag.Float64("t", 0.5, "Black/white threshold") + align = flag.String("a", "center", "Alignment (left, center, right)") + doCut = flag.Bool("c", false, "Cut after print") + maxWidth = flag.Int("printer-max-width", 512, "Printer max width in pixels") +) + +func main() { + flag.Parse() + + imgFile, err := os.Open(*imgPath) + if err != nil { + log.Fatal(err) + } + + img, imgFormat, err := image.Decode(imgFile) + imgFile.Close() + if err != nil { + log.Fatal(err) + } + + log.Print("Loaded image, format: ", imgFormat) + + // ---------------------------------------------------------------------- + + f, err := os.OpenFile(*lpDev, os.O_WRONLY, 0) + if err != nil { + log.Fatal(err) + } + + defer f.Close() + log.Print(*lpDev, " open.") + + ep, err := escpos.NewPrinter(f) + if err != nil { + log.Fatal(err) + } + + ep.Init() + + ep.SetAlign(*align) + + rasterConv := &raster.Converter{ + MaxWidth: *maxWidth, + Threshold: *threshold, + } + + rasterConv.Print(img, ep) + + if *doCut { + ep.Cut() + } + ep.End() +} diff --git a/opts.go b/opts.go new file mode 100644 index 0000000..609057d --- /dev/null +++ b/opts.go @@ -0,0 +1,12 @@ +package escpos + +// ServerOption is a server option. +type ServerOption func(*Server) error + +// WithLog is a server option to set a logging func. +func WithLog(f func(string, ...interface{})) ServerOption { + return func(s *Server) error { + s.logger = f + return nil + } +} diff --git a/raster.go b/raster.go index 2f0957f..84953ce 100644 --- a/raster.go +++ b/raster.go @@ -1,50 +1,44 @@ package escpos const ( - GS8L_MAX_Y = 1662 + gs8lMaxY = 1662 ) -func (e *Escpos) Raster(width, height, bytesWidth int, img_bw []byte) { - flushCmd := []byte{ - /* GS ( L, Print the graphics data in the print buffer, - p. 241 Moves print position to the left side of the - print area after printing of graphics data is - completed */ - 0x1d, 0x28, 0x4c, 0x02, 0x00, 0x30, - /* Fn 50 */ - 0x32, - } - +// Raster writes a rasterized version of a black and white image to the printer +// with the specified width, height, and lineWidth bytes per line. +func (p *Printer) Raster(width, height, lineWidth int, imgBw []byte) { for l := 0; l < height; { - n_lines := GS8L_MAX_Y - if n_lines > height-l { - n_lines = height - l + lines := gs8lMaxY + if lines > height-l { + lines = height - l } - f112_p := 10 + n_lines*bytesWidth - storeCmd := []byte{ - /* GS 8 L, Store the graphics data in the print buffer - (raster format), p. 252 */ - 0x1d, 0x38, 0x4c, - /* p1 p2 p3 p4 */ - byte(f112_p), byte(f112_p >> 8), - byte(f112_p >> 16), byte(f112_p >> 24), - /* Function 112 */ - 0x30, 0x70, 0x30, - /* bx by, zoom */ - 0x01, 0x01, - /* c, single-color printing model */ - 0x31, - /* xl, xh, number of dots in the horizontal direction */ - byte(width), byte(width >> 8), - /* yl, yh, number of dots in the vertical direction */ - byte(n_lines), byte(n_lines >> 8), - } + f112P := 10 + lines*lineWidth + + p.Write([]byte{ + 0x1d, 0x38, 0x4c, // GS 8 L, Store the graphics data in the print buffer -- (raster format), p. 252 + byte(f112P), byte(f112P >> 8), byte(f112P >> 16), byte(f112P >> 24), // p1 p2 p3 p4 + 0x30, 0x70, 0x30, // function 112 + 0x01, 0x01, // bx, by -- zoom + 0x31, // c -- single-color printing model + byte(width), byte(width >> 8), // xl, xh -- number of dots in the horizontal direction + byte(lines), byte(lines >> 8), // yl, yh -- number of dots in the vertical direction + }) + + // write line + p.Write(imgBw[l*lineWidth : (l+lines)*lineWidth]) - e.WriteRaw(storeCmd) - e.WriteRaw(img_bw[l*bytesWidth : (l+n_lines)*bytesWidth]) - e.WriteRaw(flushCmd) + // flush + // + // GS ( L, Print the graphics data in the print buffer, + // p. 241 Moves print position to the left side of the + // print area after printing of graphics data is + // completed + p.Write([]byte{ + 0x1d, 0x28, 0x4c, 0x02, 0x00, 0x30, + 0x32, // Fn 50 + }) - l += n_lines + l += lines } } diff --git a/server.go b/server.go new file mode 100644 index 0000000..f0b62b2 --- /dev/null +++ b/server.go @@ -0,0 +1,134 @@ +package escpos + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "net/http" + + "github.com/moovweb/gokogiri" +) + +const ( + // DefaultEndpoint is the default server endpoint for ePOS printers. + DefaultEndpoint = "/cgi-bin/epos/service.cgi" +) + +// Server wrap +type Server struct { + p *Printer + w *bufio.Writer + logger func(string, ...interface{}) +} + +// NewServer creates a new ePOS server. +func NewServer(w io.Writer, opts ...ServerOption) (*Server, error) { + var err error + + // create printer + p, err := NewPrinter(w) + if err != nil { + return nil, err + } + + s := &Server{ + p: p, + w: bufio.NewWriter(p), + } + + // apply opts + for _, o := range opts { + err = o(s) + if err != nil { + return nil, err + } + } + + if s.logger == nil { + s.logger = func(string, ...interface{}) {} + } + + return s, nil +} + +// ServeHTTP handles OPTIONS, Origin, and POST for an ePOS server. +func (s *Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { + s.logger("%s %s", req.Method, req.URL) + + // send origin headers + if origin := req.Header.Get("Origin"); origin != "" { + res.Header().Set("Access-Control-Allow-Origin", origin) + res.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + res.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, If-Modified-Since, SOAPAction") + } + + // stop if its options + if req.Method == "OPTIONS" { + return + } + + // bail if not POST + if req.Method != "POST" { + http.Error(res, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + // grab posted body + body, err := ioutil.ReadAll(req.Body) + if err != nil { + http.Error(res, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + defer req.Body.Close() + + // parse xml with gokogiri + doc, err := gokogiri.ParseXml(body) + if err != nil { + http.Error(res, "cannot load XML", http.StatusBadRequest) + return + } + defer doc.Free() + + // load print nodes from xml doc + nodes, err := getBodyChildren(doc) + if err != nil { + http.Error(res, "cannot find SOAP request Body", http.StatusBadRequest) + return + } + + // init printer + s.p.Init() + + // loop over nodes + for _, n := range nodes { + // grab parameters + params := make(map[string]string) + for _, attr := range n.Attributes() { + params[attr.Name()] = attr.Value() + } + + // write data to printer + s.p.WriteNode(n.Name(), params, n.Content()) + } + + // end + s.p.End() + + // flush writer + s.w.Flush() + + // write soap response + res.Header().Set("Content-Type", req.Header.Get("Content-Type")) + fmt.Fprintf(res, soapBody, true, "") +} + +const ( + // soapBody is a basic SOAP response body for an ePOS server response. + soapBody = ` + + + + +` +) diff --git a/util.go b/util.go new file mode 100644 index 0000000..4980d0e --- /dev/null +++ b/util.go @@ -0,0 +1,33 @@ +package escpos + +import ( + "errors" + + "github.com/moovweb/gokogiri/xml" + "github.com/moovweb/gokogiri/xpath" +) + +var ( + // ErrBodyElementEmpty is the body element empty error. + ErrBodyElementEmpty = errors.New("Body element empty") + + // bodyPath is the xpath selector for the + bodyPath = xpath.Compile("*[local-name()='Body']") +) + +// getBodyChildren returns the child nodes contained in the Body element in a XML document. +func getBodyChildren(doc *xml.XmlDocument) ([]xml.Node, error) { + // grab nodes + nodes, err := doc.Root().Search(bodyPath) + if err != nil { + return nil, err + } + + // check that the data is present + if len(nodes) < 1 || nodes[0].CountChildren() < 1 { + return nil, ErrBodyElementEmpty + } + + // get body children + return nodes[0].FirstChild().Search("./*") +}