leech/thumbnail/thumbnail.go

243 lines
5.6 KiB
Go

package thumbnail
import (
"bytes"
"fmt"
"image"
"image/color"
"image/png"
"leech/config"
"os"
"path/filepath"
"slices"
"sync"
"github.com/disintegration/imaging"
"github.com/fogleman/fauxgl"
"github.com/gen2brain/go-fitz"
"github.com/gofiber/fiber/v2"
ffmpeg "github.com/u2takey/ffmpeg-go"
)
var SupportedFileTypes = []string{".png", ".PNG", ".jpg", ".JPG", ".jpeg", ".JPEG", ".webp", ".webP", ".WEBP", ".pdf", ".PDF", ".mp4", ".MP4", ".webm", ".WEBM", ".mkv", ".MKV", ".obj", ".OBJ", ".stl", ".STL", ".ply", ".PLY", ".3ds", ".3DS"}
var FileTypesMap = map[string]imaging.Format{
".png": imaging.PNG,
".PNG": imaging.PNG,
".jpg": imaging.JPEG,
".JPG": imaging.JPEG,
".jpeg": imaging.JPEG,
".JPEG": imaging.JPEG,
".pdf": imaging.PNG,
".PDF": imaging.PNG,
".webp": imaging.PNG,
".WEBP": imaging.PNG,
".mp4": imaging.PNG,
".MP4": imaging.PNG,
".webm": imaging.PNG,
".WEBM": imaging.PNG,
".mkv": imaging.PNG,
".MKV": imaging.PNG,
".obj": imaging.PNG,
".OBJ": imaging.PNG,
".stl": imaging.PNG,
".STL": imaging.PNG,
".ply": imaging.PNG,
".PLY": imaging.PNG,
".3ds": imaging.PNG,
".3DS": imaging.PNG,
}
var FFMPEGFormats = []string{
".webp",
".webP",
".WEBP",
".mp4",
".MP4",
".webm",
".WEBM",
".mkv",
".MKV",
}
var FauxGLFormats = []string{
".obj",
".OBJ",
".stl",
".STL",
".ply",
".PLY",
".3ds",
".3DS",
}
var thumbnailSize = 48
var AA = 2
var thumbnailCache = map[string][]byte{}
var thumbnailCacheMutex = &sync.RWMutex{}
var memLimiterMutex = &sync.RWMutex{}
var jobCounter = 0
var checkAgain = make(chan bool, 5)
func IsSupportedFileType(completePath string) bool {
fileExt := filepath.Ext(completePath)
return slices.Contains(SupportedFileTypes, fileExt)
}
func WaitForAvailable() {
memLimiterMutex.RLock()
for config.Config.ThumbnailJobLimit == jobCounter {
memLimiterMutex.RUnlock()
<-checkAgain
memLimiterMutex.RLock()
}
memLimiterMutex.RUnlock()
memLimiterMutex.Lock()
jobCounter += 1
memLimiterMutex.Unlock()
}
func Free() {
memLimiterMutex.Lock()
jobCounter -= 1
memLimiterMutex.Unlock()
select {
case checkAgain <- true:
return
default:
return
}
}
func GetThumbnail(c *fiber.Ctx, completePath string) {
c.Set(fiber.HeaderContentType, "image")
thumbnailCacheMutex.RLock()
bytesThumb, ok := thumbnailCache[completePath]
thumbnailCacheMutex.RUnlock()
if ok {
c.Write(bytesThumb)
return
}
WaitForAvailable()
/* While waiting some other thread could have generated the thumbnail */
thumbnailCacheMutex.RLock()
bytesThumb, ok = thumbnailCache[completePath]
thumbnailCacheMutex.RUnlock()
if ok {
c.Write(bytesThumb)
Free()
return
}
fileExt := filepath.Ext(completePath)
if !slices.Contains(SupportedFileTypes, fileExt) {
Free()
return
}
f, err := os.Open(completePath)
if err != nil {
Free()
return
}
var img image.Image
if fileExt == ".pdf" || fileExt == ".PDF" {
doc, err := fitz.New(completePath)
if err != nil {
Free()
return
}
img, err = doc.Image(0)
if err != nil {
Free()
return
}
doc.Close()
} else if slices.Contains(FFMPEGFormats, fileExt) {
buf := bytes.NewBuffer(nil)
err := ffmpeg.Input(completePath).
Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", 0)}).
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
WithOutput(buf, nil). //os.Stdout
Silent(true).
Run()
if err != nil {
Free()
return
}
img, err = imaging.Decode(buf)
if err != nil {
Free()
return
}
} else if slices.Contains(FauxGLFormats, fileExt) {
// load a mesh
var mesh *fauxgl.Mesh
if fileExt == ".obj" || fileExt == ".OBJ" {
mesh, err = fauxgl.LoadOBJ(completePath)
} else if fileExt == ".stl" || fileExt == ".STL" {
mesh, err = fauxgl.LoadSTL(completePath)
} else if fileExt == ".ply" || fileExt == ".PLY" {
mesh, err = fauxgl.LoadPLY(completePath)
} else {
mesh, err = fauxgl.Load3DS(completePath)
}
if err != nil {
Free()
return
}
// fit mesh in a bi-unit cube centered at the origin
mesh.BiUnitCube()
// smooth the normals
mesh.SmoothNormalsThreshold(fauxgl.Radians(30))
// create a rendering context
context := fauxgl.NewContext(thumbnailSize*AA, thumbnailSize*AA)
/*context.ClearColorBufferWith(fauxgl.HexColor("#FFF8E3"))*/
// create transformation matrix and light direction
aspect := 1.
matrix := fauxgl.LookAt(fauxgl.V(-4, 1.5, -2) /* eye */, fauxgl.V(0, -0.07, 0) /* center */, fauxgl.V(0, 1, 0) /* up */).Perspective(33 /*fovy*/, aspect, 1 /* near */, 30 /* far */)
// use builtin phong shader
shader := fauxgl.NewPhongShader(matrix, fauxgl.V(-0.75, 1, 0.25).Normalize() /* light */, fauxgl.V(-4, 1.5, -2) /* eye */)
shader.ObjectColor = fauxgl.HexColor("#9e5272")
context.Shader = shader
// render
context.DrawMesh(mesh)
img = context.Image()
} else {
img, _, err = image.Decode(f)
if err != nil {
Free()
return
}
f.Close()
}
// load images and make 64x64 thumbnails of them
thumbnail := imaging.Thumbnail(img, thumbnailSize, thumbnailSize, imaging.CatmullRom)
// create a new blank image
dst := imaging.New(thumbnailSize, thumbnailSize, color.NRGBA{0, 0, 0, 0})
// paste thumbnails into the new image side by side
dst = imaging.Paste(dst, thumbnail, image.Pt(0, 0))
// save the combined image to buffer
var buf bytes.Buffer
if FileTypesMap[fileExt] == imaging.PNG {
err = png.Encode(&buf, dst)
} else {
err = imaging.Encode(&buf, dst, FileTypesMap[fileExt])
}
if err != nil {
Free()
return
}
thumbnailCacheMutex.Lock()
thumbnailCache[completePath] = buf.Bytes()
thumbnailCacheMutex.Unlock()
c.Write(buf.Bytes())
Free()
}