243 lines
5.6 KiB
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 * 2
|
|
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()
|
|
}
|