127 lines
2.7 KiB
Go
127 lines
2.7 KiB
Go
package images
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"image"
|
|
"image/jpeg"
|
|
_ "image/png"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "golang.org/x/image/webp"
|
|
|
|
"github.com/valkey-io/valkey-go"
|
|
)
|
|
|
|
var ErrNotAnImage = errors.New("not an image")
|
|
|
|
func (s *Service) getMime(path string) (mimeType string, err error) {
|
|
info, err := s.fs.Stat(path)
|
|
if err != nil {
|
|
err = fmt.Errorf("image file info could not be fetched: %w", err)
|
|
}
|
|
|
|
if mimer, ok := info.(interface{ ContentType() string }); ok {
|
|
mimeType = mimer.ContentType()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Service) getImage(uid string) (file []byte, mimeType string, err error) {
|
|
path, err := PathFromUID(uid)
|
|
if err != nil {
|
|
err = fmt.Errorf("could not get path: %w", err)
|
|
return
|
|
}
|
|
mimeType, err = s.getMime(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(mimeType, "image") {
|
|
err = ErrNotAnImage
|
|
return
|
|
}
|
|
|
|
file, err = s.fs.Read(path)
|
|
if err != nil {
|
|
err = fmt.Errorf("image file could not be fetched")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Service) GetImage(ctx context.Context, uid string, options Options) (img io.Reader, mimeType string, err error) {
|
|
if err = s.seph.Acquire(ctx, 1); err != nil {
|
|
err = fmt.Errorf("could not Acquire semaphore: %w", err)
|
|
return
|
|
|
|
}
|
|
defer s.seph.Release(1)
|
|
|
|
rawImage, mimeType, err := s.getImage(uid)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
hash, err := hash(rawImage, options)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if imgBytes, err := s.cache.Do(ctx, s.cache.B().Get().Key("img:"+hash).Build()).AsBytes(); err == nil {
|
|
return bytes.NewBuffer(imgBytes), mimeType, err
|
|
}
|
|
|
|
decImage, _, err := image.Decode(bytes.NewBuffer(rawImage))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
decImage = scale(decImage, options)
|
|
outBuff := bytes.NewBuffer(make([]byte, 0, 1024))
|
|
|
|
if options.Quality == 0 {
|
|
options.Quality = s.cfg.Quality
|
|
}
|
|
|
|
err = jpeg.Encode(outBuff, decImage, &jpeg.Options{Quality: options.Quality})
|
|
if err != nil {
|
|
return
|
|
}
|
|
if err = s.cache.Do(ctx, s.cache.B().Set().Key("img:"+hash).Value(valkey.BinaryString(outBuff.Bytes())).Ex(time.Hour*48).Build()).Error(); err == nil {
|
|
return outBuff, "image/jpeg", err
|
|
}
|
|
|
|
return outBuff, "image/jpeg", err
|
|
}
|
|
|
|
func hash(bytes []byte, options Options) (hash string, err error) {
|
|
h := fnv.New128()
|
|
|
|
if _, err = h.Write(bytes); err != nil {
|
|
return
|
|
}
|
|
if err = binary.Write(h, binary.LittleEndian, int64(options.Height)); err != nil {
|
|
return
|
|
}
|
|
if err = binary.Write(h, binary.LittleEndian, int64(options.Width)); err != nil {
|
|
return
|
|
}
|
|
if err = binary.Write(h, binary.LittleEndian, int64(options.Quality)); err != nil {
|
|
return
|
|
}
|
|
|
|
res := h.Sum(make([]byte, 0, 128/8))
|
|
|
|
return base64.RawStdEncoding.EncodeToString(res), nil
|
|
}
|