You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

207 lines
4.1 KiB
Go

package main
import (
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"io"
"net/http"
"strconv"
"strings"
)
var (
white = color.RGBA{0xff, 0xff, 0xff, 0xff}
red = color.RGBA{0xff, 0, 0, 0xff}
green = color.RGBA{0, 0xff, 0, 0xff}
blue = color.RGBA{0, 0, 0xff, 0xff}
)
type server struct {
out io.Writer
errors io.Writer
}
func (s *server) logReceived(r *http.Request) {
fmt.Fprintf(s.out, "> %s\n", r.URL.String())
}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.logReceived(r)
encoder, err := getEncoding(r)
if err != nil {
s.writeError(w, err)
return
}
width := parseInt(r.URL.Query().Get("w"), 400)
height := parseInt(r.URL.Query().Get("h"), 400)
bg := parseColor(r.URL.Query().Get("bg"), white)
data := parseSeries(r.URL.Query().Get("s"), series{})
// create a new canvas
m := image.NewRGBA(image.Rect(0, 0, width, height))
// paint the background
draw.Draw(m, m.Bounds(), &image.Uniform{bg}, m.Bounds().Min, draw.Src)
// compute column dimensions
num_cols := data.len()
col_width := width
if num_cols > 0 {
col_width = width / num_cols // float here?
}
fg := &image.Uniform{blue}
for idx, val := range data {
col_height_n := norm(val, data.min(), data.max())
col_height := int(col_height_n * float64(height))
x := idx * col_width
rect := image.Rect(x, height, x+col_width, height-col_height)
draw.Draw(m, rect, fg, image.ZP, draw.Src)
}
encoder.WriteImage(w, m)
}
// parses an integer from a user string. If the user string is invalid, return
// the suuplied default int.
func parseInt(i_s string, i int) int {
n, err := strconv.Atoi(i_s)
if err != nil {
return i
}
return n
}
// parses a color from a user string. If the user string is invalid, return the
// supplied default color.
func parseColor(c_s string, d color.RGBA) color.RGBA {
var err error
f := func(s string) (n uint8) {
if len(s) != 2 {
err = coalesce(err, fmt.Errorf("input too long"))
return 0
}
i, e := strconv.ParseUint(s, 16, 0)
if e != nil {
err = coalesce(err, e)
return 0
}
return uint8(i)
}
var c color.RGBA
c_s = strings.ToLower(c_s)
switch len(c_s) {
case 2:
n := f(c_s[0:2])
c = color.RGBA{n, n, n, 0xff}
case 4:
n, a := f(c_s[0:2]), f(c_s[2:4])
c = color.RGBA{n, n, n, a}
case 6:
c = color.RGBA{f(c_s[0:2]), f(c_s[2:4]), f(c_s[4:6]), 0xff}
case 8:
c = color.RGBA{f(c_s[0:2]), f(c_s[2:4]), f(c_s[4:6]), f(c_s[6:8])}
default:
return d
}
if err != nil {
return d
}
return c
}
type series []int
// minimum value in the series
func (s series) min() int {
switch len(s) {
case 0:
return 0
case 1:
return s[0]
default:
}
m := s[0]
for _, i := range s[1:] {
if i < m {
m = i
}
}
return m
}
// maximum value in the series
func (s series) max() int {
switch len(s) {
case 0:
return 0
case 1:
return s[0]
default:
}
m := s[0]
for _, i := range s[1:] {
if i > m {
m = i
}
}
return m
}
func (s series) len() int {
return len(s)
}
// parses a user-supplied series
func parseSeries(s string, missing series) series {
parts := strings.Split(s, ",")
out := make(series, 0, len(parts))
for _, p := range parts {
n, err := strconv.Atoi(p)
if err != nil {
return missing
}
out = append(out, n)
}
return out
}
type imageWriter interface {
WriteImage(w http.ResponseWriter, m image.Image)
}
type pngWriter struct {
CompressionLevel png.CompressionLevel
}
func (w pngWriter) WriteImage(rw http.ResponseWriter, m image.Image) {
enc := png.Encoder{CompressionLevel: w.CompressionLevel}
rw.Header().Add("Content-Type", "image/png")
enc.Encode(rw, m)
}
func getEncoding(r *http.Request) (imageWriter, error) {
parts := strings.Split(r.URL.Path, ".")
if len(parts) == 0 {
return nil, fmt.Errorf("no encoding specified")
}
switch e := parts[len(parts)-1]; e {
case "png":
return pngWriter{CompressionLevel: png.DefaultCompression}, nil
default:
return nil, fmt.Errorf("invalid encoding: %s", e)
}
}
func (s *server) writeError(w http.ResponseWriter, err error) {
fmt.Fprintf(s.errors, "E %s\n", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
}