From 452c70401e0dbef51b0ee4c1ba522f6d25b94dcb Mon Sep 17 00:00:00 2001 From: Ignacio Rivero Date: Fri, 20 Jun 2025 19:09:47 -0300 Subject: [PATCH] Added stdin, refactored a lot --- go.mod | 8 ++-- go.sum | 5 +-- main.go | 127 ++++++++++++++++++++++++++++++-------------------------- 3 files changed, 74 insertions(+), 66 deletions(-) diff --git a/go.mod b/go.mod index 0ef8bc5..55e5173 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,13 @@ module catprinter-ble go 1.22 -require github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333 +require ( + github.com/disintegration/imaging v1.6.2 + github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333 + github.com/makeworld-the-better-one/dither v1.0.0 +) require ( - github.com/disintegration/imaging v1.6.2 // indirect - github.com/makeworld-the-better-one/dither v1.0.0 // indirect github.com/mattn/go-colorable v0.1.6 // indirect github.com/mattn/go-isatty v0.0.12 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect diff --git a/go.sum b/go.sum index 522bf3f..8eee520 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= -github.com/esimov/dithergo v0.0.0-20210215145655-7f9ddf55e848 h1:4mfWkM1T8YZsWyuuIQcV0lskFRCkCMPnKBWKcBOTJUs= -github.com/esimov/dithergo v0.0.0-20210215145655-7f9ddf55e848/go.mod h1:KQYJwxnA4taJBkCet7blGupIXRROaNk+5mvpcIRSjZA= github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333 h1:bQK6D51cNzMSTyAf0HtM30V2IbljHTDam7jru9JNlJA= github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333/go.mod h1:fFJl/jD/uyILGBeD5iQ8tYHrPlJafyqCJzAyTHNJ1Uk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -31,7 +29,6 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -45,6 +42,6 @@ golang.org/x/sys v0.0.0-20211204120058-94396e421777 h1:QAkhGVjOxMa+n4mlsAWeAU+BM golang.org/x/sys v0.0.0-20211204120058-94396e421777/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 485d23c..6521197 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,12 @@ import ( "context" "flag" "fmt" + "image" "image/color" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "io" "log" "os" "os/signal" @@ -119,25 +124,37 @@ const ( bytesPerLine = linePixels / 8 ) -// Convert image to 1bpp, dithered, and packed bytes -func loadImageMono(path string, ditherType string) ([]byte, int, error) { +// decodeImage loads an image from a given path or stdin ("-") +func decodeImage(path string) (image.Image, error) { + if path == "-" { + return decodeImageFromReader(os.Stdin) + } img, err := imaging.Open(path, imaging.AutoOrientation(true)) if err != nil { - return nil, 0, fmt.Errorf("image open error: %v", err) + return nil, fmt.Errorf("failed to open image %q: %v", path, err) } + return img, nil +} +// decodeImageFromReader reads and decodes an image from any io.Reader +func decodeImageFromReader(r io.Reader) (image.Image, error) { + img, _, err := image.Decode(r) + if err != nil { + return nil, fmt.Errorf("decode error: %v", err) + } + return img, nil +} + +// loadImageMonoFromImage processes an image.Image to 1bpp packed byte format +func loadImageMonoFromImage(img image.Image, ditherType string) ([]byte, int, error) { ratio := float64(img.Bounds().Dx()) / float64(img.Bounds().Dy()) height := int(float64(linePixels) / ratio) img = imaging.Resize(img, linePixels, height, imaging.Lanczos) img = imaging.Grayscale(img) if ditherType != "none" { - palette := []color.Color{ - color.RGBA{0, 0, 0, 0xFF}, - color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}, - } + palette := []color.Color{color.Black, color.White} d := dither.NewDitherer(palette) - switch ditherType { case "floyd": d.Matrix = dither.FloydSteinberg @@ -156,11 +173,7 @@ func loadImageMono(path string, ditherType string) ([]byte, int, error) { default: return nil, 0, fmt.Errorf("unknown dither type: %s", ditherType) } - - img := d.DitherCopy(img) - if img == nil { - return nil, 0, fmt.Errorf("d.Dither(img) returned nil") - } + img = d.DitherCopy(img) } else { img = imaging.AdjustContrast(img, 10) } @@ -171,8 +184,7 @@ func loadImageMono(path string, ditherType string) ([]byte, int, error) { gray := color.GrayModel.Convert(img.At(x, y)).(color.Gray) if gray.Y < 128 { idx := (y*linePixels + x) / 8 - bit := x % 8 - pixels[idx] |= 1 << bit + pixels[idx] |= 1 << (x % 8) } } } @@ -180,14 +192,10 @@ func loadImageMono(path string, ditherType string) ([]byte, int, error) { return pixels, height, nil } -func loadImage4Bit(path string) ([]byte, int, error) { - img, err := imaging.Open(path, imaging.AutoOrientation(true)) - if err != nil { - return nil, 0, fmt.Errorf("image open error: %v", err) - } - - aspectRatio := float64(img.Bounds().Dx()) / float64(img.Bounds().Dy()) - height := int(float64(linePixels) / aspectRatio) +// loadImage4BitFromImage processes an image.Image to 4bpp packed byte format +func loadImage4BitFromImage(img image.Image) ([]byte, int, error) { + ratio := float64(img.Bounds().Dx()) / float64(img.Bounds().Dy()) + height := int(float64(linePixels) / ratio) img = imaging.Resize(img, linePixels, height, imaging.Lanczos) img = imaging.Grayscale(img) @@ -195,12 +203,10 @@ func loadImage4Bit(path string) ([]byte, int, error) { for y := 0; y < height; y++ { for x := 0; x < linePixels; x++ { gray := color.GrayModel.Convert(img.At(x, y)).(color.Gray) - // Direct port: invert and quantize level := (255 - gray.Y) >> 4 idx := (y*linePixels + x) >> 1 - shift := uint(((x & 1) ^ 1) << 2) // 4 if even, 0 if odd (matches C#) - + shift := uint(((x & 1) ^ 1) << 2) // 4 if even, 0 if odd pixels[idx] |= level << shift } } @@ -216,26 +222,7 @@ const ( Mode4bpp PrintMode = 0x02 ) -func sendImageToPrinter(client ble.Client, dataChr, printChr *ble.Characteristic, path string, mode PrintMode, intensity byte) error { - var ( - pixels []byte - height int - err error - bytesPerLine int - ) - - if mode == Mode4bpp { - pixels, height, err = loadImage4Bit(path) - bytesPerLine = linePixels / 2 - } else { - pixels, height, err = loadImageMono(path, *ditherType) - bytesPerLine = linePixels / 8 - } - - if err != nil { - return err - } - +func sendImageBufferToPrinter(client ble.Client, dataChr, printChr *ble.Characteristic, pixels []byte, height int, mode PrintMode, intensity byte) error { fmt.Printf("Sending image: %dx%d lines\n", linePixels, height) cmd := buildCommand(0xA2, []byte{intensity}) @@ -243,9 +230,8 @@ func sendImageToPrinter(client ble.Client, dataChr, printChr *ble.Characteristic return fmt.Errorf("intensity set failed: %v", err) } - lines := height param := []byte{ - byte(lines & 0xFF), byte(lines >> 8), + byte(height & 0xFF), byte(height >> 8), 0x30, byte(mode), } @@ -254,15 +240,20 @@ func sendImageToPrinter(client ble.Client, dataChr, printChr *ble.Characteristic return fmt.Errorf("print command failed: %v", err) } - mtu := 20 // BLE default payload size; adjust if you've negotiated a higher MTU - for y := 0; y < lines; y++ { + bytesPerLine := linePixels / 8 + if mode == Mode4bpp { + bytesPerLine = linePixels / 2 + } + + mtu := 20 + for y := 0; y < height; y++ { slice := pixels[y*bytesPerLine : (y+1)*bytesPerLine] - - // Split into MTU-friendly chunks for offset := 0; offset < len(slice); offset += mtu { - end := min(offset+mtu, len(slice)) + end := offset + mtu + if end > len(slice) { + end = len(slice) + } chunk := slice[offset:end] - if err := client.WriteCharacteristic(dataChr, chunk, true); err != nil { return fmt.Errorf("line %d chunk write failed: %v", y, err) } @@ -281,7 +272,7 @@ func sendImageToPrinter(client ble.Client, dataChr, printChr *ble.Characteristic func main() { flag.Parse() if flag.NArg() < 1 { - fmt.Println("Usage: catprinter-ble [flags] ") + fmt.Println("Usage: catprinter-ble [flags] ") flag.PrintDefaults() return } @@ -307,6 +298,24 @@ func main() { return } + img, err := decodeImage(imagePath) + if err != nil { + log.Fatalf("Image load error: %v", err) + } + + var pixels []byte + var height int + + switch printMode { + case Mode1bpp: + pixels, height, err = loadImageMonoFromImage(img, *ditherType) + case Mode4bpp: + pixels, height, err = loadImage4BitFromImage(img) + } + if err != nil { + log.Fatalf("Image conversion error: %v", err) + } + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() @@ -343,7 +352,7 @@ func main() { mtu, err := client.ExchangeMTU(100) if err != nil { - log.Printf("MTU negotiation failed, continuing with default MTU. Reason: %v", err) + log.Printf("MTU negotiation failed: %v", err) } else { log.Printf("Negotiated ATT MTU: %d", mtu) } @@ -355,7 +364,7 @@ func main() { svc := services[0] chars, err := client.DiscoverCharacteristics(nil, svc) if err != nil { - log.Fatalf("Char discovery failed: %v", err) + log.Fatalf("Characteristic discovery failed: %v", err) } var printChr, dataChr *ble.Characteristic @@ -368,10 +377,10 @@ func main() { } } if printChr == nil || dataChr == nil { - log.Fatalf("Required characteristics missing") + log.Fatalf("Missing required BLE characteristics") } - err = sendImageToPrinter(client, dataChr, printChr, imagePath, printMode, intensityByte) + err = sendImageBufferToPrinter(client, dataChr, printChr, pixels, height, printMode, intensityByte) if err != nil { log.Fatalf("Failed to print image: %v", err) }