chore: custom error
This commit is contained in:
@@ -3,10 +3,20 @@ package images
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/valkey-io/valkey-go"
|
||||
)
|
||||
|
||||
var ErrNotAnImage = errors.New("not an image")
|
||||
@@ -24,7 +34,7 @@ func (s *Service) getMime(path string) (mimeType string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Service) GetImage(ctx context.Context, uid string) (img io.Reader, mimeType string, err error) {
|
||||
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)
|
||||
@@ -40,10 +50,69 @@ func (s *Service) GetImage(ctx context.Context, uid string) (img io.Reader, mime
|
||||
return
|
||||
}
|
||||
|
||||
file, err := s.fs.Read(path)
|
||||
file, err = s.fs.Read(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("image file could not be fetched")
|
||||
}
|
||||
|
||||
return bytes.NewBuffer(file), mimeType, err
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Service) GetImage(ctx context.Context, uid string, options Options) (img io.Reader, mimeType string, err error) {
|
||||
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 = 80
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
package images
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"os"
|
||||
|
||||
type Service struct {
|
||||
fs fileSystem
|
||||
"github.com/valkey-io/valkey-go"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Width int // max width, 0 is unlimited
|
||||
Height int // max Height, 0 is unlimited
|
||||
Quality int // quality of the output jpeg
|
||||
}
|
||||
|
||||
func New(fs fileSystem) *Service {
|
||||
type Service struct {
|
||||
fs fileSystem
|
||||
cache valkey.Client
|
||||
}
|
||||
|
||||
func New(fs fileSystem, cache valkey.Client) *Service {
|
||||
return &Service{
|
||||
fs: fs,
|
||||
fs: fs,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package images
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
func scale(src image.Image, options Options) image.Image {
|
||||
// Set the expected size that you want:
|
||||
dst := image.NewRGBA(
|
||||
calculateFit(src.Bounds(), options),
|
||||
)
|
||||
|
||||
// Resize:
|
||||
draw.BiLinear.Scale(dst, dst.Rect, src, src.Bounds(), draw.Over, nil)
|
||||
|
||||
// Encode to `output`:
|
||||
return dst
|
||||
}
|
||||
|
||||
// calculateFit returns the new dimensions (width, height) that fit within the
|
||||
// provided Options without upscaling and while maintaining the aspect ratio.
|
||||
func calculateFit(bounds image.Rectangle, opt Options) image.Rectangle {
|
||||
// If no limits are set, or the image is already smaller than the limits,
|
||||
// return original dimensions (No Upscaling).
|
||||
if (opt.Width == 0 || bounds.Max.X <= opt.Width) && (opt.Height == 0 || bounds.Max.Y <= opt.Height) {
|
||||
return bounds
|
||||
}
|
||||
|
||||
// Calculate ratios for width and height
|
||||
ratioW := float64(opt.Width) / float64(bounds.Max.X)
|
||||
ratioH := float64(opt.Height) / float64(bounds.Max.Y)
|
||||
|
||||
// Determine the scale factor
|
||||
var scale float64
|
||||
if opt.Width > 0 && opt.Height > 0 {
|
||||
// Use the smaller ratio to ensure it fits in both bounds
|
||||
scale = math.Min(ratioW, ratioH)
|
||||
} else if opt.Width > 0 {
|
||||
scale = ratioW
|
||||
} else if opt.Height > 0 {
|
||||
scale = ratioH
|
||||
} else {
|
||||
return bounds
|
||||
}
|
||||
|
||||
return image.Rect(
|
||||
0,
|
||||
0,
|
||||
int(math.Round(float64(bounds.Max.X)*scale)),
|
||||
int(math.Round(float64(bounds.Max.Y)*scale)),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user