add text rendering via repl

This commit is contained in:
Norwin Roosen 2020-02-14 22:29:58 +01:00
parent 8a22c7bf29
commit 470115a126
7 changed files with 267 additions and 115 deletions

44
io.go
View File

@ -1,44 +0,0 @@
package main
import (
"image"
_ "image/gif"
_ "image/jpeg"
"image/png"
"log"
"os"
)
func readImage(path string) (img image.Image) {
reader, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
img, _, err2 := image.Decode(reader)
if err2 != nil {
log.Fatal(err2)
}
return img
}
func writeImage(path string, img image.Image) {
f, err := os.Create(path)
if err != nil {
log.Fatal(err)
}
if err := png.Encode(f, img); err != nil {
f.Close()
log.Fatal(err)
}
}
func imgToNRGBA(img image.Image) *image.NRGBA {
b := img.Bounds()
r := image.NewNRGBA(b)
for x := b.Min.X; x < b.Max.X; x++ {
for y := b.Min.Y; y < b.Max.Y; y++ {
r.Set(x, y, img.At(x, y))
}
}
return r
}

10
main.go
View File

@ -11,6 +11,7 @@ import (
"sync"
"github.com/SpeckiJ/Hochwasser/pixelflut"
"github.com/SpeckiJ/Hochwasser/render"
"github.com/SpeckiJ/Hochwasser/rpc"
)
@ -50,7 +51,12 @@ func main() {
if *imgPath != "" {
offset := image.Pt(*x, *y)
img := imgToNRGBA(readImage(*imgPath))
imgTmp, err := render.ReadImage(*imgPath)
if err != nil {
log.Println(err)
return
}
img := render.ImgToNRGBA(imgTmp)
// check connectivity by opening one test connection
conn, err := net.Dial("tcp", *address)
@ -71,7 +77,7 @@ func main() {
if *fetchImgPath != "" {
fetchedImg := pixelflut.FetchImage(img.Bounds().Add(offset), *address, 1, stopChan)
*connections -= 1
defer writeImage(*fetchImgPath, fetchedImg)
defer render.WriteImage(*fetchImgPath, fetchedImg)
}
// local 🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊🌊

54
render/image.go Normal file
View File

@ -0,0 +1,54 @@
package render
import (
"image"
_ "image/gif" // register gif, jpeg, png format handlers
_ "image/jpeg"
"image/png"
"os"
"golang.org/x/image/draw"
)
func ReadImage(path string) (image.Image, error) {
reader, err := os.Open(path)
if err != nil {
return nil, err
}
img, _, err := image.Decode(reader)
if err != nil {
return nil, err
}
return img, nil
}
func WriteImage(path string, img image.Image) error {
f, err := os.Create(path)
if err != nil {
return err
}
if err := png.Encode(f, img); err != nil {
f.Close()
return err
}
return nil
}
func ImgToNRGBA(img image.Image) *image.NRGBA {
b := img.Bounds()
r := image.NewNRGBA(b)
for x := b.Min.X; x < b.Max.X; x++ {
for y := b.Min.Y; y < b.Max.Y; y++ {
r.Set(x, y, img.At(x, y))
}
}
return r
}
func ScaleImage(img image.Image, factor int) (scaled *image.NRGBA) {
b := img.Bounds()
scaledBounds := image.Rect(0, 0, b.Max.X*factor, b.Max.Y*factor)
scaledImg := image.NewNRGBA(scaledBounds)
draw.NearestNeighbor.Scale(scaledImg, scaledBounds, img, b, draw.Src, nil)
return scaledImg
}

42
render/text.go Normal file
View File

@ -0,0 +1,42 @@
package render
import (
"image"
"image/color"
"golang.org/x/image/draw"
"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
"golang.org/x/image/math/fixed"
)
func pt(p fixed.Point26_6) image.Point {
return image.Point{
X: int(p.X+32) >> 6,
Y: int(p.Y+32) >> 6,
}
}
func RenderText(text string, scale int, col color.Color) *image.NRGBA {
// @incomplete: draw with texture via Drawer.Src
face := basicfont.Face7x13
stringBounds, _ := font.BoundString(face, text)
b := image.Rectangle{pt(stringBounds.Min), pt(stringBounds.Max)}
img := image.NewNRGBA(b)
draw.Draw(img, b, image.Black, image.Point{}, draw.Src) // fill with black bg
d := font.Drawer{
Dst: img,
Src: image.NewUniform(col),
Face: face,
}
d.DrawString(text)
// normalize bounds to start at 0,0
img.Rect = img.Bounds().Sub(img.Bounds().Min)
// scale up, as this font is quite small
return ScaleImage(img, scale)
}

View File

@ -46,11 +46,9 @@ type FlutStatus struct {
}
func (h *Hevring) Flut(task FlutTask, reply *FlutAck) error {
if (h.task != FlutTask{}) {
// @incomplete: stop old task if new task is received
fmt.Println("[hevring] already have a task")
reply.Ok = false
return nil
// stop old task if new task is received
if h.taskQuit != nil {
close(h.taskQuit)
}
fmt.Printf("[hevring] Rán gave us /w o r k/! %v\n", task)
@ -71,7 +69,7 @@ func (h *Hevring) Status(metrics bool, reply *FlutStatus) error {
}
func (h *Hevring) Stop(x int, reply *FlutAck) error {
if (h.task != FlutTask{}) {
if h.taskQuit != nil {
fmt.Println("[hevring] stopping task")
h.task = FlutTask{}
close(h.taskQuit)
@ -82,7 +80,9 @@ func (h *Hevring) Stop(x int, reply *FlutAck) error {
}
func (h *Hevring) Die(x int, reply *FlutAck) error {
go func() { // @cleanup: hacky
// @robustness: waiting for reply to be sent via timeout
// @incomplete: should try to reconnect for a bit first
go func() {
time.Sleep(100 * time.Millisecond)
fmt.Println("[hevring] Rán disconnected, stopping")
os.Exit(0)

View File

@ -1,20 +1,19 @@
package rpc
import (
"bufio"
"fmt"
"image"
"log"
"net"
"net/rpc"
"os"
"strings"
"sync"
"time"
"github.com/SpeckiJ/Hochwasser/pixelflut"
)
// Rán represents the RPC hub, used to coordinate `Hevring` clients.
// Implements `Fluter`
type Rán struct {
clients []*rpc.Client
task FlutTask
@ -93,70 +92,23 @@ func SummonRán(address string, stopChan chan bool, wg *sync.WaitGroup) *Rán {
}
}()
// REPL to change tasks without loosing clients
go func() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
input := strings.Split(scanner.Text(), " ")
cmd := strings.ToLower(input[0])
args := input[1:]
if cmd == "stop" {
for _, c := range r.clients {
ack := FlutAck{}
c.Call("Hevring.Stop", 0, &ack) // @speed: async
}
} else if cmd == "start" {
if (r.task != FlutTask{}) {
for _, c := range r.clients {
ack := FlutAck{}
// @speed: should send tasks async
err := c.Call("Hevring.Flut", r.task, &ack)
if err != nil || !ack.Ok {
log.Printf("[rán] client didn't accept task")
}
}
}
} else if cmd == "img" && len(args) > 0 {
// // @incomplete
// path := args[0]
// img := readImage(path)
// offset := image.Pt(0, 0)
// if len(args) == 3 {
// x := strconv.Atoi(args[1])
// y := strconv.Atoi(args[2])
// offset = image.Pt(x, y)
// }
// task := FlutTask{}
} else if cmd == "metrics" {
r.metrics.Enabled = !r.metrics.Enabled
}
}
}()
// kill clients on exit
go func() {
<-stopChan
for _, c := range r.clients {
ack := FlutAck{}
c.Call("Hevring.Die", 0, &ack) // @speed: async
}
wg.Done()
}()
go RunREPL(r)
go r.killClients(stopChan, wg)
return r
}
// SetTask assigns a FlutTask to Rán, distributing it to all clients
func (r *Rán) SetTask(img *image.NRGBA, offset image.Point, address string, maxConns int) {
// @incomplete: smart task creation:
// fetch server state & sample foreign activity in image regions. assign
// subregions to clients (per connection), considering their bandwidth.
func (r *Rán) getTask() FlutTask { return r.task }
r.task = FlutTask{address, maxConns, img, offset, true}
func (r *Rán) toggleMetrics() {
r.metrics.Enabled = !r.metrics.Enabled
}
func (r *Rán) applyTask(t FlutTask) {
if (t == FlutTask{}) { // @robustness: FlutTask should provide .IsValid()
return
}
r.task = t
for _, c := range r.clients {
ack := FlutAck{}
// @speed: should send tasks async
@ -166,3 +118,29 @@ func (r *Rán) SetTask(img *image.NRGBA, offset image.Point, address string, max
}
}
}
func (r *Rán) stopTask() {
// @robustness: errorchecking
for _, c := range r.clients {
ack := FlutAck{}
c.Call("Hevring.Stop", 0, &ack) // @speed: async
}
}
func (r *Rán) killClients(stopChan <-chan bool, wg *sync.WaitGroup) {
<-stopChan
for _, c := range r.clients {
ack := FlutAck{}
c.Call("Hevring.Die", 0, &ack) // @speed: async
}
wg.Done()
}
// SetTask assigns a FlutTask to Rán, distributing it to all clients
func (r *Rán) SetTask(img *image.NRGBA, offset image.Point, address string, maxConns int) {
// @incomplete: smart task creation:
// fetch server state & sample foreign activity in image regions. assign
// subregions to clients (per connection), considering their bandwidth.
r.applyTask(FlutTask{address, maxConns, img, offset, true})
}

116
rpc/repl.go Normal file
View File

@ -0,0 +1,116 @@
package rpc
import (
"bufio"
"encoding/hex"
"fmt"
"image"
"image/color"
"os"
"strconv"
"strings"
"github.com/SpeckiJ/Hochwasser/render"
)
// Fluter implements flut operations that can be triggered via a REPL
type Fluter interface {
getTask() FlutTask
applyTask(FlutTask)
stopTask()
toggleMetrics()
}
const commandMode = "CMD"
const textMode = "TXT"
// RunREPL starts reading os.Stdin for commands to apply to the given Fluter
func RunREPL(f Fluter) {
mode := commandMode
textSize := 4
textCol := color.NRGBA{0xff, 0xff, 0xff, 0xff}
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
inputStr := scanner.Text()
switch mode {
case textMode:
if inputStr == commandMode {
fmt.Println("[rán] command mode")
mode = commandMode
continue
}
t := f.getTask()
t.Img = render.RenderText(inputStr, textSize, textCol)
f.applyTask(t)
case commandMode:
input := strings.Split(inputStr, " ")
cmd := strings.ToLower(input[0])
args := input[1:]
switch cmd {
case "stop":
f.stopTask()
case "start":
f.applyTask(f.getTask())
case "offset":
if len(args) == 2 {
x, err := strconv.Atoi(args[0])
y, err2 := strconv.Atoi(args[1])
if err == nil && err2 == nil {
t := f.getTask()
t.Offset = image.Pt(x, y)
f.applyTask(t)
}
}
case "conns":
if len(args) == 1 {
if conns, err := strconv.Atoi(args[0]); err == nil {
t := f.getTask()
t.MaxConns = conns
f.applyTask(t)
}
}
case "shuffle":
t := f.getTask()
t.Shuffle = !t.Shuffle
f.applyTask(t)
case "txt":
fmt.Printf("[rán] text mode, return via %v\n", commandMode)
mode = textMode
if len(args) > 0 {
if size, err := strconv.Atoi(args[0]); err == nil {
textSize = size
}
}
if len(args) > 1 {
if col, err := hex.DecodeString(args[1]); err == nil {
textCol = color.NRGBA{col[0], col[1], col[2], 0xff}
}
}
case "img":
if len(args) > 0 {
path := strings.Join(args, " ")
t := f.getTask()
if img, err := render.ReadImage(path); err != nil {
fmt.Println(err)
} else {
t.Img = render.ImgToNRGBA(img)
f.applyTask(t)
}
}
case "metrics":
f.toggleMetrics()
}
}
}
}