feat: filebrowser with encfs
This commit is contained in:
244
img/service.go
Normal file
244
img/service.go
Normal file
@@ -0,0 +1,244 @@
|
||||
//go:generate go-enum --sql --marshal --file $GOFILE
|
||||
package img
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/dsoprea/go-exif/v3"
|
||||
"github.com/marusama/semaphore/v2"
|
||||
|
||||
exifcommon "github.com/dsoprea/go-exif/v3/common"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if config.quality == QualityLow && format == FormatJpeg {
|
||||
thm, newWrappedReader, errThm := getEmbeddedThumbnail(wrappedReader)
|
||||
wrappedReader = newWrappedReader
|
||||
if errThm == nil {
|
||||
_, err = out.Write(thm)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
case ResizeModeFit:
|
||||
fallthrough //nolint:gocritic
|
||||
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
|
||||
}
|
||||
|
||||
func getEmbeddedThumbnail(in io.Reader) ([]byte, io.Reader, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
r := io.TeeReader(in, buf)
|
||||
wrappedReader := io.MultiReader(buf, in)
|
||||
|
||||
offset := 0
|
||||
offsets := []int{12, 30}
|
||||
head := make([]byte, 0xffff) //nolint:gomnd
|
||||
|
||||
_, err := r.Read(head)
|
||||
if err != nil {
|
||||
return nil, wrappedReader, err
|
||||
}
|
||||
|
||||
for _, offset = range offsets {
|
||||
if _, err = exif.ParseExifHeader(head[offset:]); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, wrappedReader, err
|
||||
}
|
||||
|
||||
im, err := exifcommon.NewIfdMappingWithStandard()
|
||||
if err != nil {
|
||||
return nil, wrappedReader, err
|
||||
}
|
||||
|
||||
_, index, err := exif.Collect(im, exif.NewTagIndex(), head[offset:])
|
||||
if err != nil {
|
||||
return nil, wrappedReader, err
|
||||
}
|
||||
|
||||
ifd := index.RootIfd.NextIfd()
|
||||
if ifd == nil {
|
||||
return nil, wrappedReader, exif.ErrNoThumbnail
|
||||
}
|
||||
|
||||
thm, err := ifd.Thumbnail()
|
||||
return thm, wrappedReader, err
|
||||
}
|
||||
Reference in New Issue
Block a user