Skip to content

Instantly share code, notes, and snippets.

@cbertelli
Created March 23, 2026 22:25
Show Gist options
  • Select an option

  • Save cbertelli/ab257fe02bd437f276ed34ef8f772e8b to your computer and use it in GitHub Desktop.

Select an option

Save cbertelli/ab257fe02bd437f276ed34ef8f772e8b to your computer and use it in GitHub Desktop.
##---------------------------- 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