1
0
This repository has been archived on 2026-05-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
pancakz-stickerboard/env/Stickerboard.go
T
2026-05-24 19:58:20 -07:00

265 lines
6.5 KiB
Go

package env
import (
"bytes"
"fmt"
"image"
"image/color"
"image/gif"
"image/jpeg"
"image/png"
"log"
"os"
"os/exec"
"path"
"runtime"
"sync"
"sync/atomic"
"time"
"golang.org/x/image/draw"
"golang.org/x/image/webp"
)
const (
CANVAS_WIDTH = 854 // Canvas Width
CANVAS_HEIGHT = 480 // Canvas Height
CANVAS_STICKER_MAX_HEIGHT = 240 // Canvas Max Sticker Height in Pixels
CANVAS_FRAMES = 100
CANVAS_FPS = 20
CANVAS_DELAY = 100 / CANVAS_FPS
)
var (
StickerboardReady atomic.Bool
StickerboardPath = path.Join(DATA_DIRECTORY, STICKERBOARD_FILENAME)
StickerboardBack *image.RGBA
StickerboardMtx sync.RWMutex
Stickerboard []byte
)
func init() {
// Initialize Background to Black to Prevent Coalescing
base := image.NewRGBA(image.Rect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT))
for x := 0; x < CANVAS_WIDTH; x++ {
for y := 0; y < CANVAS_HEIGHT; y++ {
base.Set(x, y, color.Black)
}
}
StickerboardBack = base
// Load Custom Background (if any)
p := path.Join(DATA_DIRECTORY, "background.png")
if f, err := os.Open(p); err == nil {
if o, err := png.Decode(f); err == nil {
b := base.Bounds()
draw.CatmullRom.Scale(base, b.Bounds(), o, o.Bounds(), draw.Over, nil)
}
f.Close()
}
}
func stickerboardCopy() {
b, err := os.ReadFile(StickerboardPath)
if err != nil {
log.Println("[stickerboard] Read Image Error:", err)
return
}
StickerboardMtx.Lock()
Stickerboard = b
StickerboardReady.Store(true)
StickerboardMtx.Unlock()
}
func StickerboardRender() (int, error) {
t := time.Now()
// Mass Decode and Resizing of all Stickers
type DecodedSticker struct {
Position image.Rectangle
Frames []*image.RGBA
Delays []int
}
stickers := make([]DecodedSticker, len(Database.Stickers))
DatabaseMtx.RLock()
if err := Multithread(len(stickers), func(i int) error {
// Read Sticker from Disk
info := &Database.Stickers[i]
path := path.Join(DATA_DIRECTORY, info.ImageHash)
reader, err := os.Open(path)
if err != nil {
return err
}
defer reader.Close()
var images = make([]image.Image, 1)
var delays = make([]int, 1)
// Decode Sticker Frames
var decodeGIF *gif.GIF
var decodeImage image.Image
var decodeError error
switch info.ImageType {
case IMAGE_GIF:
decodeGIF, decodeError = gif.DecodeAll(reader)
case IMAGE_WEBP:
decodeImage, decodeError = webp.Decode(reader)
case IMAGE_JPEG:
decodeImage, decodeError = jpeg.Decode(reader)
case IMAGE_PNG:
decodeImage, decodeError = png.Decode(reader)
default:
return fmt.Errorf("decoder available")
}
if decodeError != nil {
return err
}
if info.ImageType == IMAGE_GIF {
// This section here properly layers GIF frames
delays = decodeGIF.Delay
images = make([]image.Image, len(decodeGIF.Image))
var imageBase = image.NewRGBA(image.Rect(0, 0, decodeGIF.Config.Width, decodeGIF.Config.Height))
var imagePrev *image.RGBA
var disposal = byte(gif.DisposalNone)
for i, frame := range decodeGIF.Image {
if disposal == gif.DisposalPrevious {
imagePrev = image.NewRGBA(imageBase.Bounds())
copy(imageBase.Pix, imagePrev.Pix)
}
if i > 0 {
switch disposal {
case gif.DisposalBackground:
draw.Draw(imageBase, decodeGIF.Image[i-1].Bounds(), image.Transparent, image.Point{}, draw.Src)
case gif.DisposalPrevious:
if imagePrev != nil {
draw.Draw(imageBase, imageBase.Bounds(), imagePrev, image.Point{}, draw.Src)
}
}
}
// Save Composited Frame
draw.Draw(imageBase, frame.Bounds(), frame, frame.Bounds().Min, draw.Over)
imageCopy := image.NewRGBA(imageBase.Bounds())
copy(imageCopy.Pix, imageBase.Pix)
images[i] = imageCopy
disposal = decodeGIF.Disposal[i]
}
} else {
// Copy Static Frame
images[0] = decodeImage
}
// Resize Decoded Frames
var (
stickerFrames = make([]*image.RGBA, len(images))
stickerPosition image.Rectangle
stickerBounds = images[0].Bounds()
stickerWidth = int(float64(stickerBounds.Dx()) * info.ImageScale)
stickerHeight = int(float64(stickerBounds.Dy()) * info.ImageScale)
)
for j := range images {
// Nice and Smooth Scaling
source := images[j]
scaled := image.NewRGBA(image.Rect(0, 0, stickerWidth, stickerHeight))
draw.CatmullRom.Scale(scaled, scaled.Bounds(), source, source.Bounds(), draw.Over, nil)
// Invert Y position because browsers placment origin is bottom-left but server is top-left
y := CANVAS_HEIGHT - info.OffsetY - stickerHeight
stickerPosition = image.Rect(info.OffsetX, y, info.OffsetX+stickerWidth, y+stickerHeight)
stickerFrames[j] = scaled
}
// Store Resized Frame
stickers[i] = DecodedSticker{
Frames: stickerFrames,
Delays: delays,
Position: stickerPosition,
}
return nil
}); err != nil {
DatabaseMtx.RUnlock()
return 0, err
}
DatabaseMtx.RUnlock()
// Startup Encoder
var outputLogs bytes.Buffer
cmd := exec.Command(
"ffmpeg", "-y",
"-threads", fmt.Sprint(runtime.NumCPU()),
"-f", "rawvideo",
"-pix_fmt", "rgba",
"-s", fmt.Sprintf("%dx%d", CANVAS_WIDTH, CANVAS_HEIGHT),
"-framerate", fmt.Sprint(CANVAS_FPS),
"-i", "pipe:0",
"-vcodec", "libwebp",
"-compression_level", "4",
"-q:v", "75",
"-loop", "0",
StickerboardPath,
)
cmdStdin, err := cmd.StdinPipe()
if err != nil {
return 0, err
}
cmd.Stdout = &outputLogs
cmd.Stderr = &outputLogs
if err := cmd.Start(); err != nil {
return 0, err
}
// Generate Frames
for i := 0; i < CANVAS_FRAMES; i++ {
// Generate Frame
canvas := image.NewRGBA(StickerboardBack.Rect)
copy(canvas.Pix, StickerboardBack.Pix)
for j := range stickers {
decode := &stickers[j]
index := 0
if len(decode.Frames) > 1 {
offset := 0
for {
idx := index % len(decode.Frames)
offset += decode.Delays[idx]
if offset > (i * CANVAS_DELAY) {
index = idx
break
}
index++
}
}
draw.Draw(canvas, decode.Position, decode.Frames[index], image.Point{}, draw.Over)
}
// Submit Encoder
for y := 0; y < canvas.Rect.Dy(); y++ {
start := y * canvas.Stride
end := start + canvas.Rect.Dx()*4
cmdStdin.Write(canvas.Pix[start:end])
}
}
// Await Encoding
cmdStdin.Close()
if err := cmd.Wait(); err != nil {
exitCode := -1
if cmd.ProcessState != nil {
exitCode = cmd.ProcessState.ExitCode()
}
log.Println("[sticker] FFMPEG Exited With Code %d\n%s\n", exitCode, outputLogs.String())
return 0, err
}
log.Printf("[sticker] Rendered %d Images in %s", len(stickers), time.Since(t))
stickerboardCopy()
return len(stickers), nil
}