package summary import ( "image" "image/color" "image/draw" _ "image/jpeg" "os" "path" "golang.org/x/image/font" "golang.org/x/image/font/opentype" "golang.org/x/image/math/fixed" _ "golang.org/x/image/webp" ) var ( assetPath = path.Join("..", "..", "assets") titleFontPath = path.Join(assetPath, "LeagueSpartan-Medium.ttf") textFontPath = path.Join(assetPath, "Jost-Regular.ttf") paddingLg = 30 paddingMd = 20 paddingSm = 6 featuredImageSize = 180 titleFontSize = 48.0 textFontSize = 16.0 featureTextStart = paddingLg + paddingMd + featuredImageSize ) // lots of code borrowed from https://medium.com/@daniel.ruizcamacho/how-to-create-an-image-in-golang-step-by-step-4416affe088f // func GenerateImage(summary *Summary) error { // base := image.NewRGBA(image.Rect(0, 0, 750, 1100)) // draw.Draw(base, base.Bounds(), image.NewUniform(color.Black), image.Pt(0, 0), draw.Over) // file, err := os.Create(path.Join(cfg.ConfigDir(), "summary.png")) // if err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // defer file.Close() // // add title // if err := addText(base, summary.Title, "", image.Pt(paddingLg, 60), titleFontPath, titleFontSize); err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // // add images // if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120), featuredImageSize); err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120-(featuredImageSize+paddingLg)), featuredImageSize); err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // if err := addImage(base, summary.TopArtistImage, image.Pt(-paddingLg, -120-(featuredImageSize+paddingLg)*2), featuredImageSize); err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // // top artists text // if err := addText(base, "Top Artists", "", image.Pt(featureTextStart, 132), textFontPath, textFontSize); err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // for rank, artist := range summary.TopArtists { // if rank == 0 { // if err := addText(base, artist.Name, strconv.Itoa(artist.Plays)+" plays", image.Pt(featureTextStart, featuredImageSize+10), titleFontPath, titleFontSize); err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // } else { // if err := addText(base, artist.Name, strconv.Itoa(artist.Plays)+" plays", image.Pt(featureTextStart, 210+(rank*(int(textFontSize)+paddingSm))), textFontPath, textFontSize); err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // } // } // // top albums text // if err := addText(base, "Top Albums", "", image.Pt(featureTextStart, 132+featuredImageSize+paddingLg), textFontPath, textFontSize); err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // for rank, album := range summary.TopAlbums { // if rank == 0 { // if err := addText(base, album.Title, strconv.Itoa(album.Plays)+" plays", image.Pt(featureTextStart, featuredImageSize+10), titleFontPath, titleFontSize); err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // } else { // if err := addText(base, album.Title, strconv.Itoa(album.Plays)+" plays", image.Pt(featureTextStart, 210+(rank*(int(textFontSize)+paddingSm))), textFontPath, textFontSize); err != nil { // return fmt.Errorf("GenerateImage: %w", err) // } // } // } // // top tracks text // // stats text // if err := png.Encode(file, base); err != nil { // return fmt.Errorf("GenerateImage: png.Encode: %w", err) // } // return nil // } func addImage(baseImage *image.RGBA, path string, point image.Point, height int) error { templateFile, err := os.Open(path) if err != nil { return err } template, _, err := image.Decode(templateFile) if err != nil { return err } resized := resize(template, height, height) draw.Draw(baseImage, baseImage.Bounds(), resized, point, draw.Over) return nil } func addText(baseImage *image.RGBA, text, subtext string, point image.Point, fontFile string, fontSize float64) error { fontBytes, err := os.ReadFile(fontFile) if err != nil { return err } ttf, err := opentype.Parse(fontBytes) if err != nil { return err } face, err := opentype.NewFace(ttf, &opentype.FaceOptions{ Size: fontSize, DPI: 72, Hinting: font.HintingFull, }) if err != nil { return err } drawer := &font.Drawer{ Dst: baseImage, Src: image.NewUniform(color.White), Face: face, Dot: fixed.Point26_6{ X: fixed.I(point.X), Y: fixed.I(point.Y), }, } drawer.DrawString(text) if subtext != "" { face, err = opentype.NewFace(ttf, &opentype.FaceOptions{ Size: textFontSize, DPI: 72, Hinting: font.HintingFull, }) drawer.Face = face if err != nil { return err } drawer.Src = image.NewUniform(color.RGBA{200, 200, 200, 255}) drawer.DrawString(" - ") drawer.DrawString(subtext) } return nil } func resize(m image.Image, w, h int) *image.RGBA { if w < 0 || h < 0 { return nil } r := m.Bounds() if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 { return image.NewRGBA(image.Rect(0, 0, w, h)) } curw, curh := r.Dx(), r.Dy() img := image.NewRGBA(image.Rect(0, 0, w, h)) for y := range h { for x := range w { // Get a source pixel. subx := x * curw / w suby := y * curh / h r32, g32, b32, a32 := m.At(subx, suby).RGBA() r := uint8(r32 >> 8) g := uint8(g32 >> 8) b := uint8(b32 >> 8) a := uint8(a32 >> 8) img.SetRGBA(x, y, color.RGBA{r, g, b, a}) } } return img }