2024-06-10 21:36:26 +02:00
package thumbnail
import (
"bytes"
2024-06-11 14:23:11 +02:00
"fmt"
2024-06-10 21:36:26 +02:00
"image"
"image/color"
"image/png"
2024-06-10 21:56:24 +02:00
"leech/config"
2024-06-10 21:36:26 +02:00
"os"
"path/filepath"
"slices"
"sync"
"github.com/disintegration/imaging"
2024-06-11 17:18:45 +02:00
"github.com/fogleman/fauxgl"
2024-06-11 14:23:11 +02:00
"github.com/gen2brain/go-fitz"
2024-06-10 21:36:26 +02:00
"github.com/gofiber/fiber/v2"
2024-06-11 14:23:11 +02:00
ffmpeg "github.com/u2takey/ffmpeg-go"
2024-06-10 21:36:26 +02:00
)
2024-06-11 17:18:45 +02:00
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" }
2024-06-10 21:36:26 +02:00
var FileTypesMap = map [ string ] imaging . Format {
".png" : imaging . PNG ,
".PNG" : imaging . PNG ,
".jpg" : imaging . JPEG ,
".JPG" : imaging . JPEG ,
".jpeg" : imaging . JPEG ,
".JPEG" : imaging . JPEG ,
2024-06-11 14:23:11 +02:00
".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 ,
2024-06-11 17:18:45 +02:00
".obj" : imaging . PNG ,
".OBJ" : imaging . PNG ,
".stl" : imaging . PNG ,
".STL" : imaging . PNG ,
".ply" : imaging . PNG ,
".PLY" : imaging . PNG ,
".3ds" : imaging . PNG ,
".3DS" : imaging . PNG ,
2024-06-10 21:36:26 +02:00
}
2024-06-11 14:23:11 +02:00
var FFMPEGFormats = [ ] string {
".webp" ,
2024-06-11 14:30:50 +02:00
".webP" ,
2024-06-11 14:23:11 +02:00
".WEBP" ,
".mp4" ,
".MP4" ,
".webm" ,
".WEBM" ,
".mkv" ,
".MKV" ,
}
2024-06-11 17:18:45 +02:00
var FauxGLFormats = [ ] string {
".obj" ,
".OBJ" ,
".stl" ,
".STL" ,
".ply" ,
".PLY" ,
".3ds" ,
".3DS" ,
}
2025-01-01 04:56:13 +01:00
var thumbnailSize = 48 * 2
2024-06-11 17:18:45 +02:00
var AA = 2
2024-06-10 21:36:26 +02:00
var thumbnailCache = map [ string ] [ ] byte { }
var thumbnailCacheMutex = & sync . RWMutex { }
2024-06-10 21:55:03 +02:00
var memLimiterMutex = & sync . RWMutex { }
var jobCounter = 0
var checkAgain = make ( chan bool , 5 )
2024-06-10 21:36:26 +02:00
func IsSupportedFileType ( completePath string ) bool {
fileExt := filepath . Ext ( completePath )
return slices . Contains ( SupportedFileTypes , fileExt )
}
2024-06-10 21:55:03 +02:00
func WaitForAvailable ( ) {
memLimiterMutex . RLock ( )
2024-06-10 21:56:24 +02:00
for config . Config . ThumbnailJobLimit == jobCounter {
2024-06-10 21:55:03 +02:00
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
}
}
2024-06-10 21:36:26 +02:00
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
}
2024-06-10 22:21:27 +02:00
WaitForAvailable ( )
2024-06-10 22:25:05 +02:00
/* While waiting some other thread could have generated the thumbnail */
thumbnailCacheMutex . RLock ( )
bytesThumb , ok = thumbnailCache [ completePath ]
thumbnailCacheMutex . RUnlock ( )
if ok {
c . Write ( bytesThumb )
2024-06-13 16:40:58 +02:00
Free ( )
2024-06-10 22:25:05 +02:00
return
}
2024-06-10 21:36:26 +02:00
fileExt := filepath . Ext ( completePath )
if ! slices . Contains ( SupportedFileTypes , fileExt ) {
2024-06-10 21:55:03 +02:00
Free ( )
2024-06-10 21:36:26 +02:00
return
}
f , err := os . Open ( completePath )
if err != nil {
2024-06-10 21:55:03 +02:00
Free ( )
2024-06-10 21:36:26 +02:00
return
}
2024-06-11 14:23:11 +02:00
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
}
2024-06-11 17:18:45 +02:00
} 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 ( )
2024-06-11 14:23:11 +02:00
} else {
img , _ , err = image . Decode ( f )
if err != nil {
Free ( )
return
}
f . Close ( )
2024-06-10 21:36:26 +02:00
}
// 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 {
2024-06-10 21:55:03 +02:00
Free ( )
2024-06-10 21:36:26 +02:00
return
}
thumbnailCacheMutex . Lock ( )
thumbnailCache [ completePath ] = buf . Bytes ( )
thumbnailCacheMutex . Unlock ( )
c . Write ( buf . Bytes ( ) )
2024-06-11 14:23:11 +02:00
2024-06-10 21:55:03 +02:00
Free ( )
2024-06-10 21:36:26 +02:00
}