Added stdin, refactored a lot
This commit is contained in:
parent
0baf4b85cb
commit
452c70401e
8
go.mod
8
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
|
||||
|
||||
5
go.sum
5
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=
|
||||
|
||||
127
main.go
127
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] <image_path>")
|
||||
fmt.Println("Usage: catprinter-ble [flags] <image_path or ->")
|
||||
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)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user