Initial Release
This commit is contained in:
Vendored
+264
@@ -0,0 +1,264 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user