refactor pixelflut package into proper API

This commit is contained in:
Norwin Roosen 2020-02-06 13:39:24 +01:00
parent 7283fac957
commit 3f0acd9694
6 changed files with 131 additions and 123 deletions

31
main.go
View File

@ -49,38 +49,15 @@ func main() {
offset := image.Pt(*image_offsetx, *image_offsety) offset := image.Pt(*image_offsetx, *image_offsety)
img := readImage(*image_path) img := readImage(*image_path)
fetchedImg := image.NewNRGBA(img.Bounds().Add(offset))
var fetchedImg *image.NRGBA
if *fetchImgPath != "" { if *fetchImgPath != "" {
fetchCmds := pixelflut.CmdsFetchImage(fetchedImg.Bounds()) fetchedImg = pixelflut.FetchImage(img.Bounds().Add(offset), *address, 1)
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)
}
*connections -= 1 *connections -= 1
} }
// Generate and split messages into equal chunks // 🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊
commands := pixelflut.CommandsFromImage(img, offset) pixelflut.Flut(img, offset, *shuffle, *address, *connections)
if *shuffle {
commands.Shuffle()
}
if *connections > 0 {
commandGroups := commands.Chunk(*connections)
for _, messages := range commandGroups {
go pixelflut.Bomb(messages, *address)
}
}
// Terminate after timeout to save resources // Terminate after timeout to save resources
timer, err := time.ParseDuration(*runtime) timer, err := time.ParseDuration(*runtime)

77
pixelflut/api.go Normal file
View File

@ -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,
})
}
}

View File

@ -1,15 +1,18 @@
package pixelflut package pixelflut
import ( import (
"fmt"
"image"
"image/color"
"math/rand" "math/rand"
) )
// Commands represent a list of messages to be sent to a pixelflut server. // 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 // Chunk splits commands into equally sized chunks, while flattening each chunk
// so that all commands are concatenated as a single `[]byte`. // 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) chunks := make([][]byte, numChunks)
chunkLength := len(c) / numChunks chunkLength := len(c) / numChunks
for i := 0; i < numChunks; i++ { for i := 0; i < numChunks; i++ {
@ -22,9 +25,49 @@ func (c Commands) Chunk(numChunks int) [][]byte {
} }
// Shuffle reorders commands randomly, in place. // Shuffle reorders commands randomly, in place.
func (c Commands) Shuffle() { func (c commands) Shuffle() {
for i := range c { for i := range c {
j := rand.Intn(i + 1) j := rand.Intn(i + 1)
c[i], c[j] = c[j], c[i] 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
}

View File

@ -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
}

View File

@ -1,34 +1,26 @@
package pixelflut package pixelflut
import ( import (
"bufio"
"encoding/hex"
"image"
"image/color"
"log" "log"
"net" "net"
"net/textproto"
"strconv"
"strings"
) )
// @speed: add some performance reporting mechanism on these functions when // @speed: add some performance reporting mechanism on these functions when
// called as goroutines // 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. // forever, as fast as possible.
func Bomb(message []byte, address string) { func bombAddress(message []byte, address string) {
conn, err := net.Dial("tcp", address) conn, err := net.Dial("tcp", address)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer conn.Close() defer conn.Close()
Bomb2(message, conn) bombConn(message, conn)
} }
// @cleanup: find common interface instead of Bomb2 func bombConn(message []byte, conn net.Conn) {
func Bomb2(message []byte, conn net.Conn) {
for { for {
_, err := conn.Write(message) _, err := conn.Write(message)
if err != nil { 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,
})
}
}

View File

@ -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]
}