265 lines
6.5 KiB
Go
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
|
||
|
|
}
|