package images import ( "bytes" "context" "encoding/base64" "encoding/binary" "errors" "fmt" "hash/fnv" "image" "image/jpeg" _ "image/png" "io" "strings" _ "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 } return } func (s *Service) getImage(path string) (file []byte, mimeType string, err error) { 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(path string) (img Image, err error) { rawImage, _, err := s.getImage(path) if err != nil { return } img.Path = path img.Image, _, err = image.Decode(bytes.NewBuffer(rawImage)) return } func (s *Service) GetScaledImage(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) path, err := PathFromUID(uid) if err != nil { return } rawImage, mimeType, err := s.getImage(path) 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(s.cfg.CacheLifetime).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 }