@@ -0,0 +1,86 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"github.com/InfinityTools/go-binpack2d"
|
||||
)
|
||||
|
||||
type Gallery struct {
|
||||
GridWith int
|
||||
GridHeight int
|
||||
Images []Image
|
||||
}
|
||||
type Set struct {
|
||||
SM Gallery
|
||||
LG Gallery
|
||||
XLG Gallery
|
||||
}
|
||||
|
||||
func (s *Service) GetGallerySet(ctx context.Context) (gallerySet Set, err error) {
|
||||
gallerySet.SM, err = s.GetGallery(ctx, 50)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
gallerySet.LG, err = s.GetGallery(ctx, 80)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gallerySet.XLG, err = s.GetGallery(ctx, 120)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Service) GetGallery(ctx context.Context, width int) (gallery Gallery, err error) {
|
||||
imgs, err := s.GetImages(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
packer := binpack2d.Create(width, 999999)
|
||||
|
||||
for len(imgs) > 0 {
|
||||
unused := make([]Image, 0, len(imgs))
|
||||
for _, img := range imgs {
|
||||
adjustImageSize(&img, 200)
|
||||
rect, ok := packer.Insert(img.Width, img.Height, binpack2d.RULE_BEST_SHORT_SIDE_FIT)
|
||||
if !ok {
|
||||
unused = append(unused, img)
|
||||
continue
|
||||
}
|
||||
|
||||
img.X = rect.X
|
||||
img.Y = rect.Y
|
||||
|
||||
gallery.Images = append(gallery.Images, img)
|
||||
}
|
||||
imgs = unused
|
||||
}
|
||||
|
||||
packer.ShrinkBin(false)
|
||||
|
||||
gallery.GridHeight = packer.GetHeight()
|
||||
gallery.GridWith = packer.GetWidth()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// adjustImageSize adjusts the image size so that the aspect ratio stays the same
|
||||
// but the image area is the given parameter.
|
||||
func adjustImageSize(img *Image, area int) {
|
||||
// Calculate the original aspect ratio (W / H)
|
||||
aspectRatio := float64(img.Width) / float64(img.Height)
|
||||
|
||||
// Calculate new height and width using the derived formulas
|
||||
newHeight := math.Sqrt(float64(area) / aspectRatio)
|
||||
newWidth := newHeight * aspectRatio
|
||||
|
||||
// Round to the nearest integer to minimize precision loss
|
||||
img.Width = int(math.Round(newWidth))
|
||||
img.Height = int(math.Round(newHeight))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
type fileWithPath struct {
|
||||
os.FileInfo
|
||||
path string
|
||||
}
|
||||
|
||||
func (f fileWithPath) Name() string {
|
||||
return path.Join(f.path, f.FileInfo.Name())
|
||||
}
|
||||
|
||||
func (s *Service) readDirRecursive(path string) (files []os.FileInfo, err error) {
|
||||
return s.readDirRecursiveTo([]os.FileInfo{}, path)
|
||||
}
|
||||
|
||||
func (s *Service) readDirRecursiveTo(files []os.FileInfo, p ...string) ([]os.FileInfo, error) {
|
||||
localFiles, err := s.fs.ReadDir(path.Join(p...))
|
||||
if err != nil {
|
||||
return files, err
|
||||
}
|
||||
|
||||
for _, file := range localFiles {
|
||||
if file.IsDir() {
|
||||
newP := append(p, file.Name())
|
||||
files, err = s.readDirRecursiveTo(files, newP...)
|
||||
if err != nil {
|
||||
return files, err
|
||||
}
|
||||
} else {
|
||||
files = append(files, fileWithPath{file, path.Join(p[1:]...)})
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/valkey-io/valkey-go"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
UID string
|
||||
X int
|
||||
Y int
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
const (
|
||||
imagesKey = "gallery:images:list"
|
||||
hashKey = "gallery:images:hash"
|
||||
)
|
||||
|
||||
func (s *Service) autoRefresh() {
|
||||
t := time.NewTicker(time.Minute)
|
||||
for {
|
||||
ctx, cancle := context.WithTimeout(context.Background(), time.Minute)
|
||||
|
||||
if err := s.assembleImageList(ctx); err != nil {
|
||||
slog.Error("error while refreshing gallery", "err", err)
|
||||
}
|
||||
cancle()
|
||||
<-t.C
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) assembleImageList(ctx context.Context) error {
|
||||
files, err := s.readDirRecursive(s.cfg.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read contents of %s: %w", s.cfg.Path, err)
|
||||
}
|
||||
slog.Debug("loaded files", "files", files)
|
||||
|
||||
savedHash, _ := s.cache.Do(ctx, s.cache.B().Get().Key(hashKey).Build()).AsBytes()
|
||||
|
||||
newHash := hashImageList(files)
|
||||
|
||||
if bytes.Equal(savedHash, newHash) {
|
||||
slog.Debug("images up to date no refreshing of gallery", "savedHash", savedHash, "newHash", newHash)
|
||||
return nil
|
||||
}
|
||||
|
||||
slog.Info("starting gallery refresh")
|
||||
defer slog.Info("gallery refresh completed")
|
||||
|
||||
imgs := make([]Image, 0, len(files))
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(file.Name(), ".md") {
|
||||
continue
|
||||
}
|
||||
|
||||
imagePath := path.Join(s.cfg.Path, file.Name())
|
||||
img, err := s.img.GetImage(imagePath)
|
||||
if err != nil || img.Image == nil {
|
||||
slog.Warn("could not get image for gallery", "err", err, "img", img)
|
||||
continue
|
||||
}
|
||||
|
||||
uid, err := img.UID()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
imgs = append(imgs, Image{
|
||||
UID: uid,
|
||||
Width: img.Bounds().Dx(),
|
||||
Height: img.Bounds().Dy(),
|
||||
})
|
||||
}
|
||||
|
||||
if err = s.cache.Do(ctx, s.cache.B().Set().Key(hashKey).Value(valkey.BinaryString(newHash)).Build()).Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.cache.Do(ctx, s.cache.B().Set().Key(imagesKey).Value(valkey.JSON(imgs)).Build()).Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hashImageList(files []os.FileInfo) []byte {
|
||||
h := fnv.New128()
|
||||
|
||||
for _, file := range files {
|
||||
fmt.Fprint(h, file.ModTime())
|
||||
fmt.Fprint(h, file.Size())
|
||||
fmt.Fprint(h, file.Name())
|
||||
}
|
||||
|
||||
return h.Sum([]byte{})
|
||||
}
|
||||
|
||||
func (s *Service) GetImages(ctx context.Context) (images []Image, err error) {
|
||||
err = s.cache.Do(ctx, s.cache.B().Get().Key(imagesKey).Build()).DecodeJSON(&images)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem"
|
||||
"github.com/valkey-io/valkey-go"
|
||||
)
|
||||
|
||||
type getImager interface {
|
||||
GetImage(path string) (img images.Image, err error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
fs filesystem.FS
|
||||
cfg *config.Gallery
|
||||
img getImager
|
||||
cache valkey.Client
|
||||
|
||||
refreshIntervall time.Duration
|
||||
}
|
||||
|
||||
func New(fs filesystem.FS, cfg *config.Gallery, img getImager, cache valkey.Client) *Service {
|
||||
s := &Service{
|
||||
fs: fs,
|
||||
cfg: cfg,
|
||||
img: img,
|
||||
cache: cache,
|
||||
}
|
||||
go s.autoRefresh()
|
||||
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user