diff --git a/main.go b/main.go index 6cd7b95..9531666 100644 --- a/main.go +++ b/main.go @@ -49,38 +49,15 @@ func main() { offset := image.Pt(*image_offsetx, *image_offsety) img := readImage(*image_path) - fetchedImg := image.NewNRGBA(img.Bounds().Add(offset)) + var fetchedImg *image.NRGBA if *fetchImgPath != "" { - fetchCmds := pixelflut.CmdsFetchImage(fetchedImg.Bounds()) - fetchMessages := fetchCmds.Chunk(1) - - { - // @cleanup: encapsulate this in separate function exported from pixelflut - conn, err := net.Dial("tcp", *address) - if err != nil { - log.Fatal(err) - } - // defer conn.Close() - - go pixelflut.FetchPixels(fetchedImg, conn) - go pixelflut.Bomb2(fetchMessages[0], conn) - } + fetchedImg = pixelflut.FetchImage(img.Bounds().Add(offset), *address, 1) *connections -= 1 } - // Generate and split messages into equal chunks - commands := pixelflut.CommandsFromImage(img, offset) - if *shuffle { - commands.Shuffle() - } - - if *connections > 0 { - commandGroups := commands.Chunk(*connections) - for _, messages := range commandGroups { - go pixelflut.Bomb(messages, *address) - } - } + // 🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊 + pixelflut.Flut(img, offset, *shuffle, *address, *connections) // Terminate after timeout to save resources timer, err := time.ParseDuration(*runtime) diff --git a/pixelflut/api.go b/pixelflut/api.go new file mode 100644 index 0000000..e53a8c2 --- /dev/null +++ b/pixelflut/api.go @@ -0,0 +1,77 @@ +package pixelflut + +import ( + "bufio" + "encoding/hex" + "image" + "image/color" + "log" + "net" + "net/textproto" + "strconv" + "strings" +) + +// Flut asynchronously sends the given image to pixelflut server at `address` +// using `conns` connections. Pixels are sent row wise, unless `shuffle` is set. +func Flut(img image.Image, position image.Point, shuffle bool, address string, conns int) { + cmds := commandsFromImage(img, position) + if shuffle { + cmds.Shuffle() + } + + messages := cmds.Chunk(conns) + for _, msg := range messages { + go bombAddress(msg, address) + } +} + +// FetchImage asynchronously uses `conns` to fetch pixels within `bounds` from +// a pixelflut server at `address`, and writes them into the returned Image. +func FetchImage(bounds image.Rectangle, address string, conns int) (img *image.NRGBA) { + img = image.NewNRGBA(bounds) + cmds := cmdsFetchImage(bounds).Chunk(conns) + + for i := 0; i < conns; i++ { + conn, err := net.Dial("tcp", address) + if err != nil { + log.Fatal(err) + } + + // @cleanup: parsePixels calls conn.Close(), as deferring it here would + // instantly close it + go parsePixels(img, conn) + go bombConn(cmds[i], conn) + } + + return img +} + +func parsePixels(target *image.NRGBA, conn net.Conn) { + reader := bufio.NewReader(conn) + tp := textproto.NewReader(reader) + defer conn.Close() + + for { + // @speed: textproto seems not the fastest, buffer text manually & split at \n ? + res, err := tp.ReadLine() + if err != nil { + log.Fatal(err) + } + + // @speed: Split is ridiculously slow due to mallocs! + // chunk last 6 chars off -> color, remove first 3 chars, find space in + // remainder, then Atoi() xy? + res2 := strings.Split(res, " ") + x, _ := strconv.Atoi(res2[1]) + y, _ := strconv.Atoi(res2[2]) + col, _ := hex.DecodeString(res2[3]) + + target.Set(x, y, color.NRGBA{ + uint8(col[0]), + uint8(col[1]), + uint8(col[2]), + 255, + }) + } +} diff --git a/pixelflut/commands.go b/pixelflut/commands.go index 04fb8de..0c3c6c7 100644 --- a/pixelflut/commands.go +++ b/pixelflut/commands.go @@ -1,15 +1,18 @@ package pixelflut import ( + "fmt" + "image" + "image/color" "math/rand" ) // Commands represent a list of messages to be sent to a pixelflut server. -type Commands [][]byte +type commands [][]byte // Chunk splits commands into equally sized chunks, while flattening each chunk // so that all commands are concatenated as a single `[]byte`. -func (c Commands) Chunk(numChunks int) [][]byte { +func (c commands) Chunk(numChunks int) [][]byte { chunks := make([][]byte, numChunks) chunkLength := len(c) / numChunks for i := 0; i < numChunks; i++ { @@ -22,9 +25,49 @@ func (c Commands) Chunk(numChunks int) [][]byte { } // Shuffle reorders commands randomly, in place. -func (c Commands) Shuffle() { +func (c commands) Shuffle() { for i := range c { j := rand.Intn(i + 1) c[i], c[j] = c[j], c[i] } } + +// CommandsFromImage converts an image to the respective pixelflut commands +func commandsFromImage(img image.Image, offset image.Point) (cmds commands) { + b := img.Bounds() + cmds = make([][]byte, b.Size().X*b.Size().Y) + numCmds := 0 + + for x := b.Min.X; x < b.Max.X; x++ { + for y := b.Min.Y; y < b.Max.Y; y++ { + // ensure we're working with RGBA colors (non-alpha-pre-multiplied) + c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) + + // ignore transparent pixels + if c.A == 0 { + continue + } + // @incomplete: also send alpha? -> bandwidth tradeoff + // @speed: this sprintf call is quite slow.. + cmd := fmt.Sprintf("PX %d %d %.2x%.2x%.2x\n", + x+offset.X, y+offset.Y, c.R, c.G, c.B) + cmds[numCmds] = []byte(cmd) + numCmds++ + } + } + + return cmds[:numCmds] +} + +func cmdsFetchImage(bounds image.Rectangle) (cmds commands) { + cmds = make([][]byte, bounds.Size().X*bounds.Size().Y) + numCmds := 0 + for x := bounds.Min.X; x < bounds.Max.X; x++ { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + cmd := fmt.Sprintf("PX %d %d\n", x, y) + cmds[numCmds] = []byte(cmd) + numCmds++ + } + } + return cmds +} diff --git a/pixelflut/fetch.go b/pixelflut/fetch.go deleted file mode 100644 index b8fcc2e..0000000 --- a/pixelflut/fetch.go +++ /dev/null @@ -1,19 +0,0 @@ -package pixelflut - -import ( - "fmt" - "image" -) - -func CmdsFetchImage(bounds image.Rectangle) (cmds Commands) { - cmds = make([][]byte, bounds.Size().X*bounds.Size().Y) - numCmds := 0 - for x := bounds.Min.X; x < bounds.Max.X; x++ { - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - cmd := fmt.Sprintf("PX %d %d\n", x, y) - cmds[numCmds] = []byte(cmd) - numCmds++ - } - } - return cmds -} diff --git a/pixelflut/net.go b/pixelflut/net.go index 00041ea..a09aaf6 100644 --- a/pixelflut/net.go +++ b/pixelflut/net.go @@ -1,34 +1,26 @@ package pixelflut import ( - "bufio" - "encoding/hex" - "image" - "image/color" "log" "net" - "net/textproto" - "strconv" - "strings" ) // @speed: add some performance reporting mechanism on these functions when // called as goroutines -// Bomb writes the given message via plain TCP to the given address, +// bombAddress writes the given message via plain TCP to the given address, // forever, as fast as possible. -func Bomb(message []byte, address string) { +func bombAddress(message []byte, address string) { conn, err := net.Dial("tcp", address) if err != nil { log.Fatal(err) } defer conn.Close() - Bomb2(message, conn) + bombConn(message, conn) } -// @cleanup: find common interface instead of Bomb2 -func Bomb2(message []byte, conn net.Conn) { +func bombConn(message []byte, conn net.Conn) { for { _, err := conn.Write(message) if err != nil { @@ -36,31 +28,3 @@ func Bomb2(message []byte, conn net.Conn) { } } } - -func FetchPixels(target *image.NRGBA, conn net.Conn) { - reader := bufio.NewReader(conn) - tp := textproto.NewReader(reader) - - for { - // @speed: textproto seems not the fastest, buffer text manually & split at \n ? - res, err := tp.ReadLine() - if err != nil { - log.Fatal(err) - } - - // @speed: Split is ridiculously slow due to mallocs! - // chunk last 6 chars off -> color, remove first 3 chars, find space in - // remainder, then Atoi() xy? - res2 := strings.Split(res, " ") - x, _ := strconv.Atoi(res2[1]) - y, _ := strconv.Atoi(res2[2]) - col, _ := hex.DecodeString(res2[3]) - - target.Set(x, y, color.NRGBA{ - uint8(col[0]), - uint8(col[1]), - uint8(col[2]), - 255, - }) - } -} diff --git a/pixelflut/send.go b/pixelflut/send.go deleted file mode 100644 index 6a240da..0000000 --- a/pixelflut/send.go +++ /dev/null @@ -1,34 +0,0 @@ -package pixelflut - -import ( - "fmt" - "image" - "image/color" -) - -// CommandsFromImage converts an image to the respective pixelflut commands -func CommandsFromImage(img image.Image, offset image.Point) (commands Commands) { - b := img.Bounds() - commands = make([][]byte, b.Size().X*b.Size().Y) - numCmds := 0 - - for x := b.Min.X; x < b.Max.X; x++ { - for y := b.Min.Y; y < b.Max.Y; y++ { - // ensure we're working with RGBA colors (non-alpha-pre-multiplied) - c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) - - // ignore transparent pixels - if c.A == 0 { - continue - } - // @incomplete: also send alpha? -> bandwidth tradeoff - // @speed: this sprintf call is quite slow.. - cmd := fmt.Sprintf("PX %d %d %.2x%.2x%.2x\n", - x+offset.X, y+offset.Y, c.R, c.G, c.B) - commands[numCmds] = []byte(cmd) - numCmds++ - } - } - - return commands[:numCmds] -}