//go:generate go-enum --sql --marshal --file $GOFILE package img import ( "bytes" "context" "errors" "fmt" "image" "io" "github.com/disintegration/imaging" "github.com/marusama/semaphore/v2" ) // ErrUnsupportedFormat means the given image format is not supported. var ErrUnsupportedFormat = errors.New("unsupported image format") // Service type Service struct { sem semaphore.Semaphore } func New(workers int) *Service { return &Service{ sem: semaphore.New(workers), } } // Format is an image file format. /* ENUM( jpeg png gif tiff bmp ) */ type Format int func (x Format) toImaging() imaging.Format { switch x { case FormatJpeg: return imaging.JPEG case FormatPng: return imaging.PNG case FormatGif: return imaging.GIF case FormatTiff: return imaging.TIFF case FormatBmp: return imaging.BMP default: return imaging.JPEG } } /* ENUM( high medium low ) */ type Quality int func (x Quality) resampleFilter() imaging.ResampleFilter { switch x { case QualityHigh: return imaging.Lanczos case QualityMedium: return imaging.Box case QualityLow: return imaging.NearestNeighbor default: return imaging.Box } } /* ENUM( fit fill ) */ type ResizeMode int func (s *Service) FormatFromExtension(ext string) (Format, error) { format, err := imaging.FormatFromExtension(ext) if err != nil { return -1, ErrUnsupportedFormat } switch format { case imaging.JPEG: return FormatJpeg, nil case imaging.PNG: return FormatPng, nil case imaging.GIF: return FormatGif, nil case imaging.TIFF: return FormatTiff, nil case imaging.BMP: return FormatBmp, nil } return -1, ErrUnsupportedFormat } type resizeConfig struct { format Format resizeMode ResizeMode quality Quality } type Option func(*resizeConfig) func WithFormat(format Format) Option { return func(config *resizeConfig) { config.format = format } } func WithMode(mode ResizeMode) Option { return func(config *resizeConfig) { config.resizeMode = mode } } func WithQuality(quality Quality) Option { return func(config *resizeConfig) { config.quality = quality } } func (s *Service) Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...Option) error { if err := s.sem.Acquire(ctx, 1); err != nil { return err } defer s.sem.Release(1) format, wrappedReader, err := s.detectFormat(in) if err != nil { return err } config := resizeConfig{ format: format, resizeMode: ResizeModeFit, quality: QualityMedium, } for _, option := range options { option(&config) } img, err := imaging.Decode(wrappedReader, imaging.AutoOrientation(true)) if err != nil { return err } switch config.resizeMode { case ResizeModeFill: img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter()) default: img = imaging.Fit(img, width, height, config.quality.resampleFilter()) } return imaging.Encode(out, img, config.format.toImaging()) } func (s *Service) detectFormat(in io.Reader) (Format, io.Reader, error) { buf := &bytes.Buffer{} r := io.TeeReader(in, buf) _, imgFormat, err := image.DecodeConfig(r) if err != nil { return 0, nil, fmt.Errorf("%s: %w", err.Error(), ErrUnsupportedFormat) } format, err := ParseFormat(imgFormat) if err != nil { return 0, nil, ErrUnsupportedFormat } return format, io.MultiReader(buf, in), nil }