diff --git a/README.md b/README.md index ebe8af4..294c6c1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ Now available as a native Mac application! https://primitive.lol/ +### Primitive for your Web Server + +Process images using primitive as a route [in your web server](https://github.com/fogleman/primitive/tree/master/server/README.md). + ### Twitter Follow [@PrimitivePic](https://twitter.com/PrimitivePic) on Twitter to see a new primitive picture every 30 minutes! diff --git a/build/build.go b/build/build.go new file mode 100644 index 0000000..5ec2c4d --- /dev/null +++ b/build/build.go @@ -0,0 +1,62 @@ +package build + +import ( + "image" + "runtime" + "time" + + "github.com/fogleman/primitive/primitive" + "github.com/nfnt/resize" +) + +// Config are options needed to build a primitive image +type Config struct { + Input image.Image + Background string + Alpha int + Count int + OutputSize int + Mode int + Repeat int + V, VV bool +} + +// Build builds a primitive image from available options +func Build(conf Config, stepCallback func(*primitive.Model, int, int)) *primitive.Model { + start := time.Now() + + // set log level + if conf.V { + primitive.LogLevel = 1 + } + if conf.VV { + primitive.LogLevel = 2 + } + + // determine background color + var bg primitive.Color + if conf.Background == "" { + bg = primitive.MakeColor(primitive.AverageImageColor(conf.Input)) + } else { + bg = primitive.MakeHexColor(conf.Background) + } + + input := resize.Thumbnail(uint(256), uint(256), conf.Input, resize.Bilinear) + model := primitive.NewModel(input, bg, conf.OutputSize, runtime.NumCPU()) + primitive.Log(1, "%d: t=%.3f, score=%.6f\n", 0, 0.0, model.Score) + + frame := 0 + for i := 0; i < conf.Count; i++ { + frame++ + + // find optimal shape and add it to the model + t := time.Now() + n := model.Step(primitive.ShapeType(conf.Mode), conf.Alpha, conf.Repeat) + nps := primitive.NumberString(float64(n) / time.Since(t).Seconds()) + elapsed := time.Since(start).Seconds() + primitive.Log(1, "%d: t=%.3f, score=%.6f, n=%d, n/s=%s\n", frame, elapsed, model.Score, n, nps) + stepCallback(model, frame, i) + } + + return model +} diff --git a/main.go b/main.go index 59e3345..d2d15cc 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/fogleman/primitive/build" "github.com/fogleman/primitive/primitive" "github.com/nfnt/resize" ) @@ -144,33 +145,24 @@ func main() { input = resize.Thumbnail(size, size, input, resize.Bilinear) } - // determine background color - var bg primitive.Color - if Background == "" { - bg = primitive.MakeColor(primitive.AverageImageColor(input)) - } else { - bg = primitive.MakeHexColor(Background) - } - // run algorithm - model := primitive.NewModel(input, bg, OutputSize, Workers) - primitive.Log(1, "%d: t=%.3f, score=%.6f\n", 0, 0.0, model.Score) - start := time.Now() - frame := 0 for j, config := range Configs { primitive.Log(1, "count=%d, mode=%d, alpha=%d, repeat=%d\n", config.Count, config.Mode, config.Alpha, config.Repeat) - for i := 0; i < config.Count; i++ { - frame++ - - // find optimal shape and add it to the model - t := time.Now() - n := model.Step(primitive.ShapeType(config.Mode), config.Alpha, config.Repeat) - nps := primitive.NumberString(float64(n) / time.Since(t).Seconds()) - elapsed := time.Since(start).Seconds() - primitive.Log(1, "%d: t=%.3f, score=%.6f, n=%d, n/s=%s\n", frame, elapsed, model.Score, n, nps) + buildConfig := build.Config{ + Input: input, + Background: Background, + Alpha: config.Alpha, + Count: config.Count, + OutputSize: OutputSize, + Mode: config.Mode, + Repeat: config.Repeat, + V: V, + VV: VV, + } + build.Build(buildConfig, func(model *primitive.Model, frame int, i int) { // write output image(s) for _, output := range Outputs { ext := strings.ToLower(filepath.Ext(output)) @@ -202,6 +194,6 @@ func main() { } } } - } + }) } } diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..71858c2 --- /dev/null +++ b/server/README.md @@ -0,0 +1,43 @@ +# Primitive for your Web Server + +## Add a route to your existing web server + +The following will bind a new route to your server where users can +POST files to `/primitive` with the `file` form key and process them with +primitive. The response returns a raw PNG buffer with Content-Type `image/png`. + +```go +import ( + "net/http" + + pr "github.com/fogleman/primitive/server/route" +) + +handler := pr.PrimitiveRoute(pr.Config{ + MaxUploadMb: 10, + FileKey: "file", +}) +http.HandleFunc("/primitive", handler) +``` + +## Example web server + +An examples web server you can start that has a `/primitive` route +bound that accepts file uploads to be processed with primitive +can be started with the following instructions. + +``` +$ go get github.com/fogleman/primitive/server + +$ # run the web server on port :8080 +$ go run $(go env GOPATH)/src/github.com/fogleman/primitive/server + +$ # in another terminal window, make sure the server is running +$ curl localhost:8080 +We're running! +``` + +## TODOS + +- Make all parameters configurable (Background, Alpha, Mode, etc.) +- Dockerfile for example web server \ No newline at end of file diff --git a/server/route/route.go b/server/route/route.go new file mode 100644 index 0000000..e9fd739 --- /dev/null +++ b/server/route/route.go @@ -0,0 +1,64 @@ +package route + +import ( + "image" + "image/png" + "net/http" + + "github.com/fogleman/primitive/build" + "github.com/fogleman/primitive/primitive" +) + +// Config contains core settings one can +// configure to setup the primitive file processing route. +type Config struct { + MaxUploadMb int64 + FileKey string +} + +// PrimitiveRoute is an http route than can be used to consume +// an uploaded file and execute primitive on it. +func PrimitiveRoute(conf Config) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse our multipart form, 10 << 20 specifies a maximum upload of 10 MB files. + var maxUpload int64 = 10 + if conf.MaxUploadMb > 0 { + maxUpload = conf.MaxUploadMb + } + r.ParseMultipartForm(maxUpload << 20) + + // FormFile returns the first file for the given key `myFile` + // it also returns the FileHeader so we can get the Filename, + // the Header and the size of the file + fileKey := "file" + if conf.FileKey != "" { + fileKey = conf.FileKey + } + file, _, err := r.FormFile(fileKey) + if err != nil { + http.Error(w, err.Error(), 400) + return + } + defer file.Close() + + img, _, err := image.Decode(file) + if err != nil { + http.Error(w, err.Error(), 400) + return + } + + buildConfig := build.Config{ + Input: img, + Alpha: 128, + Count: 100, + OutputSize: 1024, + Mode: 4, + } + model := build.Build(buildConfig, func(_ *primitive.Model, _ int, _ int) {}) + finalImg := model.Context.Image() + + // return final primitive image + w.Header().Set("Content-Type", "image/png") + png.Encode(w, finalImg) + } +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..6df795b --- /dev/null +++ b/server/server.go @@ -0,0 +1,33 @@ +// main.go +package main + +import ( + "log" + "net/http" + "os" + + pr "github.com/fogleman/primitive/server/route" +) + +func setupRoutes() { + handler := pr.PrimitiveRoute(pr.Config{ + MaxUploadMb: 10, + }) + http.HandleFunc("/primitive", handler) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("We're running!")) + }) +} + +func main() { + setupRoutes() + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + err := http.ListenAndServe(":"+port, nil) + if err != nil { + log.Fatal("Error starting server - ", err) + } +}