Created
March 23, 2026 22:25
-
-
Save cbertelli/ab257fe02bd437f276ed34ef8f772e8b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ##---------------------------- backend.go ---------------------------- ## | |
| package main | |
| import ( | |
| "encoding/binary" | |
| "fmt" | |
| "image" | |
| "image/png" | |
| "log" | |
| "net" | |
| "os" | |
| "time" | |
| ) | |
| func removeHeaders(data []byte) [][]byte { | |
| log.Println("Removing headers from bytes...") | |
| pages := make([][]byte, 0) | |
| page := make([]byte, 0) | |
| currentPage := 1 | |
| i := 0 | |
| headersLoop: | |
| for { | |
| if data[i] == scanner.endPage { | |
| pages = append(pages, page) | |
| if len(data) > i+10 && data[i+10] == scanner.endScan { | |
| break headersLoop | |
| } | |
| page = make([]byte, 0) | |
| currentPage++ | |
| i += scanner.headerLen - 2 | |
| continue headersLoop | |
| } | |
| payloadLen := binary.LittleEndian.Uint16(data[i+scanner.headerLen-2 : i+scanner.headerLen]) | |
| chunkSize := int(payloadLen) + scanner.headerLen | |
| page = append(page, data[i+scanner.headerLen:i+chunkSize]...) | |
| i += chunkSize | |
| } | |
| return pages | |
| } | |
| func sendRequest(socket net.Conn, resolution int, _mode string) (int, int) { | |
| mode, compression := getCompressionMode(_mode) | |
| log.Println("Reading scanner status...") | |
| status := readPacket(socket)[:7] | |
| if status != scanner.ready { | |
| log.Fatalf("invalid reply from scanner: %s", status) | |
| } | |
| log.Println("Leasing options...") | |
| request := []byte(fmt.Sprintf(formats.leaseRequest, resolution, resolution, mode)) | |
| sendPacket(socket, request) | |
| offer := readPacket(socket) | |
| log.Println("Reading offer...") | |
| width, height := 0, 0 | |
| planeWidth, planeHeight := 0, 0 | |
| dpiX, dpiY := 0, 0 | |
| adfStatus := 0 | |
| fmt.Sscanf(offer[3:], "%d,%d,%d,%d,%d,%d,%d", &dpiX, &dpiY, &adfStatus, &planeWidth, &width, &planeHeight, &height) | |
| if adfStatus == scanner.adfEnabled { | |
| log.Println("Automatic document feeder is enabled") | |
| } | |
| if planeHeight == 0 { | |
| // Firmware bug | |
| planeHeight = scanner.A4height | |
| } | |
| width = mmToPixels(planeWidth, dpiX) | |
| height = mmToPixels(planeHeight, dpiY) | |
| request = []byte(fmt.Sprintf(formats.scanRequest, dpiX, dpiY, mode, compression, width, height)) | |
| sendPacket(socket, request) | |
| log.Println("Scanning started...") | |
| return width, height | |
| } | |
| func getScanBytes(socket net.Conn) ([]byte, error) { | |
| log.Println("Getting packets...") | |
| packet := make([]byte, 2048) | |
| scanBytes := make([]byte, 0) | |
| readPackets: | |
| for { | |
| socket.SetDeadline(time.Now().Add(time.Second * 10)) | |
| bytes, err := socket.Read(packet) | |
| switch err := err.(type) { | |
| case net.Error: | |
| if err.Timeout() { | |
| break readPackets | |
| } | |
| case nil: | |
| scanBytes = append(scanBytes, packet[:bytes]...) | |
| default: | |
| HandleError(err) | |
| } | |
| } | |
| if (len(scanBytes)) < 1 { | |
| return scanBytes, fmt.Errorf("no data received") | |
| } | |
| return scanBytes, nil | |
| } | |
| func Scan(brotherIP string, brotherPort int, resolution int, color string) ([][]byte, int, int) { | |
| log.Println("Valid IP address, opening socket...") | |
| socket, err := net.Dial("tcp", fmt.Sprintf("%s:%d", brotherIP, brotherPort)) | |
| HandleError(err) | |
| defer socket.Close() | |
| width, heigth := sendRequest(socket, resolution, color) | |
| bytes, err := getScanBytes(socket) | |
| HandleError(err) | |
| return removeHeaders(bytes), width, heigth | |
| } | |
| func SaveImage(data []byte, width int, height int, name string, color string) { | |
| log.Println("Saving image...") | |
| _, compression := getCompressionMode(color) | |
| if compression != scanner.compression.jpeg { | |
| img := image.NewGray(image.Rectangle{ | |
| image.Point{0, 0}, | |
| image.Point{width, height}, | |
| }) | |
| for y := 0; y < height; y++ { | |
| for x := 0; x < width; x++ { | |
| img.SetGray(x, y, colorToGray(data[(y*width+x)%len(data)])) | |
| } | |
| } | |
| file, err := os.Create(name) | |
| HandleError(err) | |
| png.Encode(file, img) | |
| } else { | |
| err := os.WriteFile(name, data, 0644) | |
| HandleError(err) | |
| } | |
| os.Exit(0) | |
| } | |
| ##---------------------------- definitions.go ---------------------------- ## | |
| package main | |
| type requests struct { | |
| leaseRequest string | |
| scanRequest string | |
| } | |
| type modes struct { | |
| color string | |
| grayscale string | |
| } | |
| type encode struct { | |
| jpeg string | |
| none string | |
| } | |
| type constants struct { | |
| ready string | |
| mode modes | |
| compression encode | |
| adfEnabled int | |
| headerLen int | |
| A4height int | |
| mmInch float32 | |
| endPage byte | |
| endScan byte | |
| } | |
| var scanner constants = constants{ | |
| ready: "+OK 200", | |
| mode: modes{ | |
| color: "CGRAY", | |
| grayscale: "GRAY64", | |
| }, | |
| compression: encode{ | |
| jpeg: "JPEG", | |
| none: "NONE", | |
| }, | |
| adfEnabled: 0x2, | |
| headerLen: 0xc, | |
| A4height: 294, | |
| mmInch: 25.4, | |
| endPage: 0x82, | |
| endScan: 0x80, | |
| } | |
| var formats requests = requests{ | |
| leaseRequest: "\x1bI\nR=%d,%d\nM=%s\n\x80", | |
| scanRequest: "\x1bX\nR=%v,%v\nM=%s\nC=%s\nJ=MID\nB=50\nN=50\nA=0,0,%d,%d\n\x80", | |
| } | |
| ##---------------------------- main.go ---------------------------- ## | |
| package main | |
| import ( | |
| "flag" | |
| "fmt" | |
| "log" | |
| "net" | |
| "sync" | |
| ) | |
| var wg sync.WaitGroup | |
| func main() { | |
| const brotherPort int = 54921 | |
| brotherIP := flag.String("a", "", "IP address of the Brother scanner") | |
| resolution := flag.Int("r", 300, "Resolution of the scan") | |
| color := flag.String("c", "CGRAY", "Color mode of the scan (CGRAY, GRAY64)") | |
| name := flag.String("n", "scan.jpg", "Name of the output file") | |
| flag.Parse() | |
| if net.ParseIP(*brotherIP) == nil { | |
| HandleError(fmt.Errorf("invalid IP address: %s", *brotherIP)) | |
| } | |
| rawImages, width, heigth := Scan(*brotherIP, brotherPort, *resolution, *color) | |
| wg.Add(len(rawImages)) | |
| log.Printf("Received %d images\n", len(rawImages)) | |
| for i, rawImage := range rawImages { | |
| go SaveImage(rawImage, width, heigth, fmt.Sprintf("%s(%d)", *name, i), *color) | |
| } | |
| wg.Wait() | |
| } | |
| ##---------------------------- utils.go ---------------------------- ## | |
| package main | |
| import ( | |
| "image/color" | |
| "log" | |
| "net" | |
| ) | |
| func sendPacket(socket net.Conn, packet []byte) { | |
| _, err := socket.Write(packet) | |
| HandleError(err) | |
| } | |
| func readPacket(socket net.Conn) string { | |
| reply := make([]byte, 64) | |
| _, err := socket.Read(reply) | |
| HandleError(err) | |
| return string(reply) | |
| } | |
| func HandleError(err error) { | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| } | |
| func getCompressionMode(_mode string) (string, string) { | |
| if _mode == scanner.mode.grayscale { | |
| return _mode, scanner.compression.none | |
| } else { | |
| return scanner.mode.color, scanner.compression.jpeg | |
| } | |
| } | |
| func mmToPixels(mm int, dpi int) int { | |
| return int(float32(mm*dpi) / scanner.mmInch) | |
| } | |
| func colorToGray(value byte) color.Gray { | |
| return color.Gray{Y: value} | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment