package main import ( "context" "embed" "flag" "fmt" "image" "image/color" "image/png" "io" "math" "net/http" "runtime/debug" "text/template" "time" "golang.org/x/image/draw" "git.chandlerswift.com/chandlerswift/bannergen/internal/datafetchers" "git.chandlerswift.com/chandlerswift/bannergen/internal/datafetchers/espn" "git.chandlerswift.com/chandlerswift/bannergen/internal/types" ) //go:embed static templates var frontend embed.FS func main() { port := flag.Int("port", 8000, "Port to listen on") flag.Parse() // preload teams data fetchers := []datafetchers.DataFetcher{ espn.ESPNDataFetcher{}, } var sports []types.Sport for _, fetcher := range fetchers { sports = append(sports, fetcher.Fetch()...) } rawBuildInfo, _ := debug.ReadBuildInfo() var revision, time string var dirty bool for _, buildSetting := range rawBuildInfo.Settings { if buildSetting.Key == "vcs.revision" { revision = buildSetting.Value } else if buildSetting.Key == "vcs.time" { time = buildSetting.Value } else if buildSetting.Key == "vcs.modified" { dirty = buildSetting.Value == "true" } } buildInfo := map[string]interface{}{ "GitRev": revision, "GitDirty": dirty, "GitTime": time, "GoVersion": rawBuildInfo.GoVersion, "AllBuildInfo": rawBuildInfo.String(), } http.Handle("GET /static/", http.FileServer(http.FS(frontend))) tmpls := template.Must(template.ParseFS(frontend, "templates/*")) http.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { err := tmpls.ExecuteTemplate(w, "index.html", map[string]interface{}{}) if err != nil { panic(err) } }) http.HandleFunc("GET /about.html", func(w http.ResponseWriter, r *http.Request) { err := tmpls.ExecuteTemplate(w, "about.html", map[string]interface{}{ "buildInfo": buildInfo, }) if err != nil { panic(err) } }) http.HandleFunc("/generate/{image_name}", func(w http.ResponseWriter, r *http.Request) { r.ParseForm() colorLeftHex := r.Form.Get("left_color") if colorLeftHex == "" { http.Error(w, "no left_color provided", 400) } colorRightHex := r.Form.Get("right_color") if colorRightHex == "" { http.Error(w, "no right_color provided", 400) } logoLeftPath := r.Form.Get("left_logo") if logoLeftPath == "" { http.Error(w, "no left_logo provided", 400) } logoRightPath := r.Form.Get("right_logo") if logoRightPath == "" { http.Error(w, "no right_logo provided", 400) } size := 400 // TODO: param // Set image dimensions; TODO: param const width, height = 1280, 720 // Create a blank RGBA image img := image.NewRGBA(image.Rect(0, 0, width, height)) // Convert hex colors to RGBA colorLeft := hexToRGBA(colorLeftHex) colorRight := hexToRGBA(colorRightHex) // Fill the image with colors split by angle centerX, centerY := float64(width)/2, float64(height)/2 for x := 0; x < width; x++ { for y := 0; y < height; y++ { dx, dy := float64(x)-centerX, float64(y)-centerY rotatedAngle := math.Atan2(dy, dx) - 16.0*math.Pi/180 if math.Cos(rotatedAngle) >= 0 { img.Set(x, y, colorRight) } else { img.Set(x, y, colorLeft) } } } // Load logos logoLeft, err := loadPNG(logoLeftPath) if err != nil { panic(err) } logoRight, err := loadPNG(logoRightPath) if err != nil { panic(err) } // Resize logos using golang.org/x/image/draw logoLeftResized := resizeImage(logoLeft, size, size) logoRightResized := resizeImage(logoRight, size, size) // Place logos logoLeftX, logoLeftY := (width/4)-(size/2), (height/2)-(size/2) logoRightX, logoRightY := (3*width/4)-(size/2), (height/2)-(size/2) draw.Draw(img, image.Rect(logoLeftX, logoLeftY, logoLeftX+size, logoLeftY+size), logoLeftResized, image.Point{}, draw.Over) draw.Draw(img, image.Rect(logoRightX, logoRightY, logoRightX+size, logoRightY+size), logoRightResized, image.Point{}, draw.Over) png.Encode(w, img) }) panic(http.ListenAndServe(fmt.Sprintf(":%v", *port), nil)) } func resizeImage(src *image.RGBA, newWidth, newHeight int) *image.RGBA { dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight)) draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil) return dst } func hexToRGBA(hex string) color.RGBA { var r, g, b int var err error if hex[0] == '#' { _, err = fmt.Sscanf(hex, "#%02x%02x%02x", &r, &g, &b) } else { _, err = fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b) } if err != nil { panic(err) } return color.RGBA{uint8(r), uint8(g), uint8(b), 255} } func loadPNG(path string) (*image.RGBA, error) { var reader io.ReadCloser var err error // TODO: smarter limits here ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", path, nil) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { resp.Body.Close() return nil, fmt.Errorf("failed to fetch image: %s", resp.Status) } reader = resp.Body defer reader.Close() img, err := png.Decode(reader) if err != nil { return nil, err } // Convert to RGBA rgbaImg := image.NewRGBA(img.Bounds()) draw.Draw(rgbaImg, rgbaImg.Bounds(), img, image.Point{}, draw.Src) return rgbaImg, nil }