Forráskód Böngészése

IMG-23: Replace imagemeta with imagedetect (#1483)

* Replace imagemeta with imagedetect

* Removed imagemeta package

* 1 page in the check
Victor Sokolov 1 hónapja
szülő
commit
09a25f8966

+ 12 - 2
imagedata/download.go

@@ -3,6 +3,7 @@ package imagedata
 import (
 	"net/http"
 
+	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagefetcher"
 	"github.com/imgproxy/imgproxy/v3/transport"
@@ -17,8 +18,17 @@ var (
 )
 
 type DownloadOptions struct {
-	Header    http.Header
-	CookieJar http.CookieJar
+	Header         http.Header
+	CookieJar      http.CookieJar
+	MaxSrcFileSize int
+}
+
+func DefaultDownloadOptions() DownloadOptions {
+	return DownloadOptions{
+		Header:         nil,
+		CookieJar:      nil,
+		MaxSrcFileSize: config.MaxSrcFileSize,
+	}
 }
 
 func initDownloading() error {

+ 19 - 65
imagedata/factory.go

@@ -12,7 +12,6 @@ import (
 	"github.com/imgproxy/imgproxy/v3/asyncbuffer"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagefetcher"
-	"github.com/imgproxy/imgproxy/v3/imagemeta"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/security"
 )
@@ -31,42 +30,23 @@ func NewFromBytesWithFormat(format imagetype.Type, b []byte) ImageData {
 func NewFromBytes(b []byte) (ImageData, error) {
 	r := bytes.NewReader(b)
 
-	meta, err := imagemeta.DecodeMeta(r)
+	format, err := imagetype.Detect(r)
 	if err != nil {
 		return nil, err
 	}
 
-	return NewFromBytesWithFormat(meta.Format(), b), nil
+	return NewFromBytesWithFormat(format, b), nil
 }
 
 // NewFromPath creates a new ImageData from an os.File
-func NewFromPath(path string, secopts security.Options) (ImageData, error) {
+func NewFromPath(path string) (ImageData, error) {
 	fl, err := os.Open(path)
 	if err != nil {
 		return nil, err
 	}
 	defer fl.Close()
 
-	fr, err := security.LimitFileSize(fl, secopts)
-	if err != nil {
-		return nil, err
-	}
-
-	b, err := io.ReadAll(fr)
-	if err != nil {
-		return nil, err
-	}
-
-	r := bytes.NewReader(b)
-
-	// NOTE: This will be removed in the future in favor of VIPS metadata extraction
-	// It's here temporarily to maintain compatibility with existing code
-	meta, err := imagemeta.DecodeMeta(r)
-	if err != nil {
-		return nil, err
-	}
-
-	err = security.CheckMeta(meta, secopts)
+	b, err := io.ReadAll(fl)
 	if err != nil {
 		return nil, err
 	}
@@ -75,31 +55,17 @@ func NewFromPath(path string, secopts security.Options) (ImageData, error) {
 }
 
 // NewFromBase64 creates a new ImageData from a base64 encoded byte slice
-func NewFromBase64(encoded string, secopts security.Options) (ImageData, error) {
+func NewFromBase64(encoded string) (ImageData, error) {
 	b, err := base64.StdEncoding.DecodeString(encoded)
 	if err != nil {
 		return nil, err
 	}
 
-	r := bytes.NewReader(b)
-
-	// NOTE: This will be removed in the future in favor of VIPS metadata extraction
-	// It's here temporarily to maintain compatibility with existing code
-	meta, err := imagemeta.DecodeMeta(r)
-	if err != nil {
-		return nil, err
-	}
-
-	err = security.CheckMeta(meta, secopts)
-	if err != nil {
-		return nil, err
-	}
-
 	return NewFromBytes(b)
 }
 
 // sendRequest is a common logic between sync and async download.
-func sendRequest(ctx context.Context, url string, opts DownloadOptions, secopts security.Options) (*imagefetcher.Request, *http.Response, http.Header, error) {
+func sendRequest(ctx context.Context, url string, opts DownloadOptions) (*imagefetcher.Request, *http.Response, http.Header, error) {
 	h := make(http.Header)
 
 	// NOTE: This will be removed in the future when our test context gets better isolation
@@ -125,7 +91,7 @@ func sendRequest(ctx context.Context, url string, opts DownloadOptions, secopts
 		return req, nil, h, err
 	}
 
-	res, err = security.LimitResponseSize(res, secopts)
+	res, err = security.LimitResponseSize(res, opts.MaxSrcFileSize)
 	if err != nil {
 		if res != nil {
 			res.Body.Close()
@@ -139,8 +105,8 @@ func sendRequest(ctx context.Context, url string, opts DownloadOptions, secopts
 }
 
 // DownloadSync downloads the image synchronously and returns the ImageData and HTTP headers.
-func downloadSync(ctx context.Context, imageURL string, opts DownloadOptions, secopts security.Options) (ImageData, http.Header, error) {
-	req, res, h, err := sendRequest(ctx, imageURL, opts, secopts)
+func downloadSync(ctx context.Context, imageURL string, opts DownloadOptions) (ImageData, http.Header, error) {
+	req, res, h, err := sendRequest(ctx, imageURL, opts)
 	if res != nil {
 		defer res.Body.Close()
 	}
@@ -158,40 +124,28 @@ func downloadSync(ctx context.Context, imageURL string, opts DownloadOptions, se
 		return nil, h, err
 	}
 
-	meta, err := imagemeta.DecodeMeta(bytes.NewReader(b))
+	format, err := imagetype.Detect(bytes.NewReader(b))
 	if err != nil {
 		return nil, h, err
 	}
 
-	err = security.CheckMeta(meta, secopts)
-	if err != nil {
-		return nil, h, err
-	}
-
-	d := NewFromBytesWithFormat(meta.Format(), b)
+	d := NewFromBytesWithFormat(format, b)
 	return d, h, err
 }
 
 // downloadAsync downloads the image asynchronously and returns the ImageData
 // backed by AsyncBuffer and HTTP headers.
-func downloadAsync(ctx context.Context, imageURL string, opts DownloadOptions, secopts security.Options) (ImageData, http.Header, error) {
+func downloadAsync(ctx context.Context, imageURL string, opts DownloadOptions) (ImageData, http.Header, error) {
 	// We pass this responsibility to AsyncBuffer
 	//nolint:bodyclose
-	req, res, h, err := sendRequest(ctx, imageURL, opts, secopts)
+	req, res, h, err := sendRequest(ctx, imageURL, opts)
 	if err != nil {
 		return nil, h, err
 	}
 
 	b := asyncbuffer.New(res.Body)
 
-	meta, err := imagemeta.DecodeMeta(b.Reader())
-	if err != nil {
-		b.Close()
-		req.Cancel()
-		return nil, h, err
-	}
-
-	err = security.CheckMeta(meta, secopts)
+	format, err := imagetype.Detect(b.Reader())
 	if err != nil {
 		b.Close()
 		req.Cancel()
@@ -200,7 +154,7 @@ func downloadAsync(ctx context.Context, imageURL string, opts DownloadOptions, s
 
 	d := &imageDataAsyncBuffer{
 		b:      b,
-		format: meta.Format(),
+		format: format,
 		cancel: nil,
 	}
 	d.AddCancel(req.Cancel) // request will be closed when the image data is consumed
@@ -210,8 +164,8 @@ func downloadAsync(ctx context.Context, imageURL string, opts DownloadOptions, s
 
 // DownloadSyncWithDesc downloads the image synchronously and returns the ImageData, but
 // wraps errors with desc.
-func DownloadSync(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (ImageData, http.Header, error) {
-	imgdata, h, err := downloadSync(ctx, imageURL, opts, secopts)
+func DownloadSync(ctx context.Context, imageURL, desc string, opts DownloadOptions) (ImageData, http.Header, error) {
+	imgdata, h, err := downloadSync(ctx, imageURL, opts)
 	if err != nil {
 		return nil, h, ierrors.Wrap(
 			err, 0,
@@ -224,8 +178,8 @@ func DownloadSync(ctx context.Context, imageURL, desc string, opts DownloadOptio
 
 // DownloadSyncWithDesc downloads the image synchronously and returns the ImageData, but
 // wraps errors with desc.
-func DownloadAsync(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (ImageData, http.Header, error) {
-	imgdata, h, err := downloadAsync(ctx, imageURL, opts, secopts)
+func DownloadAsync(ctx context.Context, imageURL, desc string, opts DownloadOptions) (ImageData, http.Header, error) {
+	imgdata, h, err := downloadAsync(ctx, imageURL, opts)
 	if err != nil {
 		return nil, h, ierrors.Wrap(
 			err, 0,

+ 7 - 7
imagedata/image_data.go

@@ -11,7 +11,6 @@ import (
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
-	"github.com/imgproxy/imgproxy/v3/security"
 )
 
 var (
@@ -48,6 +47,7 @@ type imageDataAsyncBuffer struct {
 	cancelOnce sync.Once
 }
 
+// Close closes the image data and releases any resources held by it
 func (d *imageDataBytes) Close() error {
 	d.cancelOnce.Do(func() {
 		for _, cancel := range d.cancel {
@@ -143,7 +143,7 @@ func loadWatermark() error {
 
 	switch {
 	case len(config.WatermarkData) > 0:
-		Watermark, err = NewFromBase64(config.WatermarkData, security.DefaultOptions())
+		Watermark, err = NewFromBase64(config.WatermarkData)
 
 		// NOTE: this should be something like err = ierrors.Wrap(err).WithStackDeep(0).WithPrefix("watermark")
 		// In the NewFromBase64 all errors should be wrapped to something like
@@ -153,13 +153,13 @@ func loadWatermark() error {
 		}
 
 	case len(config.WatermarkPath) > 0:
-		Watermark, err = NewFromPath(config.WatermarkPath, security.DefaultOptions())
+		Watermark, err = NewFromPath(config.WatermarkPath)
 		if err != nil {
 			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't read watermark from file"))
 		}
 
 	case len(config.WatermarkURL) > 0:
-		Watermark, _, err = DownloadSync(context.Background(), config.WatermarkURL, "watermark", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
+		Watermark, _, err = DownloadSync(context.Background(), config.WatermarkURL, "watermark", DefaultDownloadOptions())
 		if err != nil {
 			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
 		}
@@ -174,19 +174,19 @@ func loadWatermark() error {
 func loadFallbackImage() (err error) {
 	switch {
 	case len(config.FallbackImageData) > 0:
-		FallbackImage, err = NewFromBase64(config.FallbackImageData, security.DefaultOptions())
+		FallbackImage, err = NewFromBase64(config.FallbackImageData)
 		if err != nil {
 			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't load fallback image from Base64"))
 		}
 
 	case len(config.FallbackImagePath) > 0:
-		FallbackImage, err = NewFromPath(config.FallbackImagePath, security.DefaultOptions())
+		FallbackImage, err = NewFromPath(config.FallbackImagePath)
 		if err != nil {
 			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't read fallback image from file"))
 		}
 
 	case len(config.FallbackImageURL) > 0:
-		FallbackImage, FallbackImageHeaders, err = DownloadSync(context.Background(), config.FallbackImageURL, "fallback image", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
+		FallbackImage, FallbackImageHeaders, err = DownloadSync(context.Background(), config.FallbackImageURL, "fallback image", DefaultDownloadOptions())
 		if err != nil {
 			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
 		}

+ 12 - 23
imagedata/image_data_test.go

@@ -19,7 +19,6 @@ import (
 	"github.com/imgproxy/imgproxy/v3/config"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
-	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/testutil"
 )
 
@@ -91,7 +90,7 @@ func (s *ImageDataTestSuite) SetupTest() {
 }
 
 func (s *ImageDataTestSuite) TestDownloadStatusOK() {
-	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DefaultDownloadOptions())
 
 	s.Require().NoError(err)
 	s.Require().NotNil(imgdata)
@@ -158,7 +157,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusPartialContent() {
 		s.Run(tc.name, func() {
 			s.header.Set("Content-Range", tc.contentRange)
 
-			imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+			imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DefaultDownloadOptions())
 
 			if tc.expectErr {
 				s.Require().Error(err)
@@ -178,7 +177,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusNotFound() {
 	s.data = []byte("Not Found")
 	s.header.Set("Content-Type", "text/plain")
 
-	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DefaultDownloadOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
@@ -190,7 +189,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusForbidden() {
 	s.data = []byte("Forbidden")
 	s.header.Set("Content-Type", "text/plain")
 
-	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DefaultDownloadOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
@@ -202,7 +201,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusInternalServerError() {
 	s.data = []byte("Internal Server Error")
 	s.header.Set("Content-Type", "text/plain")
 
-	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DefaultDownloadOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(500, ierrors.Wrap(err, 0).StatusCode())
@@ -216,7 +215,7 @@ func (s *ImageDataTestSuite) TestDownloadUnreachable() {
 
 	serverURL := fmt.Sprintf("http://%s", l.Addr().String())
 
-	imgdata, _, err := DownloadSync(context.Background(), serverURL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := DownloadSync(context.Background(), serverURL, "Test image", DefaultDownloadOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(500, ierrors.Wrap(err, 0).StatusCode())
@@ -226,7 +225,7 @@ func (s *ImageDataTestSuite) TestDownloadUnreachable() {
 func (s *ImageDataTestSuite) TestDownloadInvalidImage() {
 	s.data = []byte("invalid")
 
-	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DefaultDownloadOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
@@ -236,27 +235,17 @@ func (s *ImageDataTestSuite) TestDownloadInvalidImage() {
 func (s *ImageDataTestSuite) TestDownloadSourceAddressNotAllowed() {
 	config.AllowLoopbackSourceAddresses = false
 
-	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DefaultDownloadOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
 	s.Require().Nil(imgdata)
 }
 
-func (s *ImageDataTestSuite) TestDownloadImageTooLarge() {
-	config.MaxSrcResolution = 1
-
-	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
-
-	s.Require().Error(err)
-	s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
-	s.Require().Nil(imgdata)
-}
-
 func (s *ImageDataTestSuite) TestDownloadImageFileTooLarge() {
 	config.MaxSrcFileSize = 1
 
-	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DefaultDownloadOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
@@ -275,7 +264,7 @@ func (s *ImageDataTestSuite) TestDownloadGzip() {
 	s.data = buf.Bytes()
 	s.header.Set("Content-Encoding", "gzip")
 
-	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := DownloadSync(context.Background(), s.server.URL, "Test image", DefaultDownloadOptions())
 
 	s.Require().NoError(err)
 	s.Require().NotNil(imgdata)
@@ -284,7 +273,7 @@ func (s *ImageDataTestSuite) TestDownloadGzip() {
 }
 
 func (s *ImageDataTestSuite) TestFromFile() {
-	imgdata, err := NewFromPath("../testdata/test1.jpg", security.DefaultOptions())
+	imgdata, err := NewFromPath("../testdata/test1.jpg")
 
 	s.Require().NoError(err)
 	s.Require().NotNil(imgdata)
@@ -295,7 +284,7 @@ func (s *ImageDataTestSuite) TestFromFile() {
 func (s *ImageDataTestSuite) TestFromBase64() {
 	b64 := base64.StdEncoding.EncodeToString(s.defaultData)
 
-	imgdata, err := NewFromBase64(b64, security.DefaultOptions())
+	imgdata, err := NewFromBase64(b64)
 
 	s.Require().NoError(err)
 	s.Require().NotNil(imgdata)

+ 0 - 51
imagemeta/bmp.go

@@ -1,51 +0,0 @@
-package imagemeta
-
-import (
-	"bytes"
-	"encoding/binary"
-	"io"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-var bmpMagick = []byte("BM")
-
-func DecodeBmpMeta(r io.Reader) (Meta, error) {
-	var tmp [26]byte
-
-	if _, err := io.ReadFull(r, tmp[:]); err != nil {
-		return nil, err
-	}
-
-	if !bytes.Equal(tmp[:2], bmpMagick) {
-		return nil, newFormatError("BMP", "malformed header")
-	}
-
-	infoSize := binary.LittleEndian.Uint32(tmp[14:18])
-
-	var width, height int
-
-	if infoSize >= 40 {
-		width = int(binary.LittleEndian.Uint32(tmp[18:22]))
-		height = int(int32(binary.LittleEndian.Uint32(tmp[22:26])))
-	} else {
-		// CORE
-		width = int(binary.LittleEndian.Uint16(tmp[18:20]))
-		height = int(int16(binary.LittleEndian.Uint16(tmp[20:22])))
-	}
-
-	// height can be negative in Windows bitmaps
-	if height < 0 {
-		height = -height
-	}
-
-	return &meta{
-		format: imagetype.BMP,
-		width:  width,
-		height: height,
-	}, nil
-}
-
-func init() {
-	RegisterFormat(string(bmpMagick), DecodeBmpMeta)
-}

+ 0 - 37
imagemeta/errors.go

@@ -1,37 +0,0 @@
-package imagemeta
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/imgproxy/imgproxy/v3/ierrors"
-)
-
-type (
-	UnknownFormatError struct{}
-	FormatError        string
-)
-
-func newUnknownFormatError() error {
-	return ierrors.Wrap(
-		UnknownFormatError{},
-		1,
-		ierrors.WithStatusCode(http.StatusUnprocessableEntity),
-		ierrors.WithPublicMessage("Invalid source image"),
-		ierrors.WithShouldReport(false),
-	)
-}
-
-func (e UnknownFormatError) Error() string { return "Source image type not supported" }
-
-func newFormatError(format, msg string) error {
-	return ierrors.Wrap(
-		FormatError(fmt.Sprintf("Invalid %s file: %s", format, msg)),
-		1,
-		ierrors.WithStatusCode(http.StatusUnprocessableEntity),
-		ierrors.WithPublicMessage("Invalid source image"),
-		ierrors.WithShouldReport(false),
-	)
-}
-
-func (e FormatError) Error() string { return string(e) }

+ 0 - 26
imagemeta/gif.go

@@ -1,26 +0,0 @@
-package imagemeta
-
-import (
-	"io"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-func DecodeGifMeta(r io.Reader) (Meta, error) {
-	var tmp [10]byte
-
-	_, err := io.ReadFull(r, tmp[:])
-	if err != nil {
-		return nil, err
-	}
-
-	return &meta{
-		format: imagetype.GIF,
-		width:  int(tmp[6]) + int(tmp[7])<<8,
-		height: int(tmp[8]) + int(tmp[9])<<8,
-	}, nil
-}
-
-func init() {
-	RegisterFormat("GIF8?a", DecodeGifMeta)
-}

+ 0 - 280
imagemeta/heif.go

@@ -1,280 +0,0 @@
-package imagemeta
-
-import (
-	"bytes"
-	"cmp"
-	"encoding/binary"
-	"errors"
-	"fmt"
-	"io"
-	"math"
-	"slices"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-const heifBoxHeaderSize = uint64(8)
-
-var heicBrand = []byte("heic")
-var heixBrand = []byte("heix")
-var avifBrand = []byte("avif")
-var heifPict = []byte("pict")
-
-type heifDiscarder interface {
-	Discard(n int) (discarded int, err error)
-}
-
-type heifSize struct {
-	Width, Height int64
-}
-
-type heifData struct {
-	Format imagetype.Type
-	Sizes  []heifSize
-}
-
-func (d *heifData) Meta() (*meta, error) {
-	if d.Format == imagetype.Unknown {
-		return nil, newFormatError("HEIF", "format data wasn't found")
-	}
-
-	if len(d.Sizes) == 0 {
-		return nil, newFormatError("HEIF", "dimensions data wasn't found")
-	}
-
-	bestSize := slices.MaxFunc(d.Sizes, func(a, b heifSize) int {
-		return cmp.Compare(a.Width*a.Height, b.Width*b.Height)
-	})
-
-	return &meta{
-		format: d.Format,
-		width:  int(bestSize.Width),
-		height: int(bestSize.Height),
-	}, nil
-}
-
-func heifReadN(r io.Reader, n uint64) (b []byte, err error) {
-	if buf, ok := r.(*bytes.Buffer); ok {
-		b = buf.Next(int(n))
-		if len(b) == 0 {
-			err = io.EOF
-		}
-		return
-	}
-
-	b = make([]byte, n)
-	_, err = io.ReadFull(r, b)
-
-	return
-}
-
-func heifDiscardN(r io.Reader, n uint64) error {
-	if buf, ok := r.(*bytes.Buffer); ok {
-		_ = buf.Next(int(n))
-		return nil
-	}
-
-	if rd, ok := r.(heifDiscarder); ok {
-		_, err := rd.Discard(int(n))
-		return err
-	}
-
-	_, err := io.CopyN(io.Discard, r, int64(n))
-	return err
-}
-
-func heifReadBoxHeader(r io.Reader) (boxType string, boxDataSize uint64, err error) {
-	var b []byte
-
-	b, err = heifReadN(r, heifBoxHeaderSize)
-	if err != nil {
-		return
-	}
-
-	headerSize := heifBoxHeaderSize
-
-	boxDataSize = uint64(binary.BigEndian.Uint32(b[0:4]))
-	boxType = string(b[4:8])
-
-	if boxDataSize == 1 {
-		b, err = heifReadN(r, 8)
-		if err != nil {
-			return
-		}
-
-		boxDataSize = (uint64(binary.BigEndian.Uint32(b[0:4])) << 32) |
-			uint64(binary.BigEndian.Uint32(b[4:8]))
-		headerSize += 8
-	}
-
-	if boxDataSize < heifBoxHeaderSize || boxDataSize > math.MaxInt64 {
-		return "", 0, newFormatError("HEIF", "invalid box data size")
-	}
-
-	boxDataSize -= headerSize
-
-	return
-}
-
-func heifAssignFormat(d *heifData, brand []byte) bool {
-	if bytes.Equal(brand, heicBrand) || bytes.Equal(brand, heixBrand) {
-		d.Format = imagetype.HEIC
-		return true
-	}
-
-	if bytes.Equal(brand, avifBrand) {
-		d.Format = imagetype.AVIF
-		return true
-	}
-
-	return false
-}
-
-func heifReadFtyp(d *heifData, r io.Reader, boxDataSize uint64) error {
-	if boxDataSize < 8 {
-		return newFormatError("HEIF", "invalid ftyp data")
-	}
-
-	data, err := heifReadN(r, boxDataSize)
-	if err != nil {
-		return err
-	}
-
-	if heifAssignFormat(d, data[0:4]) {
-		return nil
-	}
-
-	if boxDataSize >= 12 {
-		for i := uint64(8); i < boxDataSize; i += 4 {
-			if heifAssignFormat(d, data[i:i+4]) {
-				return nil
-			}
-		}
-	}
-
-	return newFormatError("HEIF", "image is not compatible with heic/avif")
-}
-
-func heifReadMeta(d *heifData, r io.Reader, boxDataSize uint64) error {
-	if boxDataSize < 4 {
-		return newFormatError("HEIF", "invalid meta data")
-	}
-
-	data, err := heifReadN(r, boxDataSize)
-	if err != nil {
-		return err
-	}
-
-	if boxDataSize > 4 {
-		if err := heifReadBoxes(d, bytes.NewBuffer(data[4:])); err != nil && !errors.Is(err, io.EOF) {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func heifReadHldr(r io.Reader, boxDataSize uint64) error {
-	if boxDataSize < 12 {
-		return newFormatError("HEIF", "invalid hdlr data")
-	}
-
-	data, err := heifReadN(r, boxDataSize)
-	if err != nil {
-		return err
-	}
-
-	if !bytes.Equal(data[8:12], heifPict) {
-		return newFormatError("HEIF", fmt.Sprintf("Invalid handler. Expected: pict, actual: %s", data[8:12]))
-	}
-
-	return nil
-}
-
-func heifReadIspe(r io.Reader, boxDataSize uint64) (w, h int64, err error) {
-	if boxDataSize < 12 {
-		return 0, 0, newFormatError("HEIF", "invalid ispe data")
-	}
-
-	data, err := heifReadN(r, boxDataSize)
-	if err != nil {
-		return 0, 0, err
-	}
-
-	w = int64(binary.BigEndian.Uint32(data[4:8]))
-	h = int64(binary.BigEndian.Uint32(data[8:12]))
-
-	return
-}
-
-func heifReadBoxes(d *heifData, r io.Reader) error {
-	for {
-		boxType, boxDataSize, err := heifReadBoxHeader(r)
-		if err != nil {
-			return err
-		}
-
-		switch boxType {
-		case "ftyp":
-			if err := heifReadFtyp(d, r, boxDataSize); err != nil {
-				return err
-			}
-		case "meta":
-			return heifReadMeta(d, r, boxDataSize)
-		case "hdlr":
-			if err := heifReadHldr(r, boxDataSize); err != nil {
-				return err
-			}
-		case "iprp", "ipco":
-			data, err := heifReadN(r, boxDataSize)
-			if err != nil {
-				return err
-			}
-
-			if err := heifReadBoxes(d, bytes.NewBuffer(data)); err != nil && !errors.Is(err, io.EOF) {
-				return err
-			}
-		case "ispe":
-			w, h, err := heifReadIspe(r, boxDataSize)
-			if err != nil {
-				return err
-			}
-			d.Sizes = append(d.Sizes, heifSize{Width: w, Height: h})
-		case "irot":
-			data, err := heifReadN(r, boxDataSize)
-			if err != nil {
-				return err
-			}
-			if len(d.Sizes) > 0 && len(data) > 0 && (data[0] == 1 || data[0] == 3) {
-				lastSize := d.Sizes[len(d.Sizes)-1]
-				d.Sizes[len(d.Sizes)-1] = heifSize{Width: lastSize.Height, Height: lastSize.Width}
-			}
-		default:
-			if err := heifDiscardN(r, boxDataSize); err != nil {
-				return err
-			}
-		}
-	}
-}
-
-func DecodeHeifMeta(r io.Reader) (Meta, error) {
-	d := new(heifData)
-
-	if err := heifReadBoxes(d, r); err != nil {
-		return nil, err
-	}
-
-	return d.Meta()
-}
-
-func init() {
-	RegisterFormat("????ftypheic", DecodeHeifMeta)
-	RegisterFormat("????ftypheix", DecodeHeifMeta)
-	RegisterFormat("????ftyphevc", DecodeHeifMeta)
-	RegisterFormat("????ftypheim", DecodeHeifMeta)
-	RegisterFormat("????ftypheis", DecodeHeifMeta)
-	RegisterFormat("????ftyphevm", DecodeHeifMeta)
-	RegisterFormat("????ftyphevs", DecodeHeifMeta)
-	RegisterFormat("????ftypmif1", DecodeHeifMeta)
-	RegisterFormat("????ftypavif", DecodeHeifMeta)
-}

+ 0 - 87
imagemeta/ico.go

@@ -1,87 +0,0 @@
-package imagemeta
-
-import (
-	"encoding/binary"
-	"io"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-type IcoMeta struct {
-	Meta
-	offset int
-	size   int
-}
-
-func (m *IcoMeta) BestImageOffset() int {
-	return m.offset
-}
-
-func (m *IcoMeta) BestImageSize() int {
-	return m.size
-}
-
-func icoBestSize(r io.Reader) (width, height byte, offset uint32, size uint32, err error) {
-	var tmp [16]byte
-
-	if _, err = io.ReadFull(r, tmp[:6]); err != nil {
-		return
-	}
-
-	count := binary.LittleEndian.Uint16(tmp[4:6])
-
-	for i := uint16(0); i < count; i++ {
-		if _, err = io.ReadFull(r, tmp[:]); err != nil {
-			return
-		}
-
-		if tmp[0] > width || tmp[1] > height || tmp[0] == 0 || tmp[1] == 0 {
-			width = tmp[0]
-			height = tmp[1]
-			size = binary.LittleEndian.Uint32(tmp[8:12])
-			offset = binary.LittleEndian.Uint32(tmp[12:16])
-		}
-	}
-
-	return
-}
-
-func BestIcoPage(r io.Reader) (int, int, error) {
-	_, _, offset, size, err := icoBestSize(r)
-	return int(offset), int(size), err
-}
-
-func DecodeIcoMeta(r io.Reader) (*IcoMeta, error) {
-	bwidth, bheight, offset, size, err := icoBestSize(r)
-	if err != nil {
-		return nil, err
-	}
-
-	width := int(bwidth)
-	height := int(bheight)
-
-	if width == 0 {
-		width = 256
-	}
-
-	if height == 0 {
-		height = 256
-	}
-
-	return &IcoMeta{
-		Meta: &meta{
-			format: imagetype.ICO,
-			width:  width,
-			height: height,
-		},
-		offset: int(offset),
-		size:   int(size),
-	}, nil
-}
-
-func init() {
-	RegisterFormat(
-		"\x00\x00\x01\x00",
-		func(r io.Reader) (Meta, error) { return DecodeIcoMeta(r) },
-	)
-}

+ 0 - 99
imagemeta/image_meta.go

@@ -1,99 +0,0 @@
-package imagemeta
-
-import (
-	"bufio"
-	"errors"
-	"io"
-	"sync"
-	"sync/atomic"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-type Meta interface {
-	Format() imagetype.Type
-	Width() int
-	Height() int
-}
-
-type DecodeMetaFunc func(io.Reader) (Meta, error)
-
-type meta struct {
-	format        imagetype.Type
-	width, height int
-}
-
-func (m *meta) Format() imagetype.Type {
-	return m.format
-}
-
-func (m *meta) Width() int {
-	return m.width
-}
-
-func (m *meta) Height() int {
-	return m.height
-}
-
-type format struct {
-	magic      string
-	decodeMeta DecodeMetaFunc
-}
-
-type reader interface {
-	io.Reader
-	Peek(int) ([]byte, error)
-}
-
-var (
-	formatsMu     sync.Mutex
-	atomicFormats atomic.Value
-)
-
-func asReader(r io.Reader) reader {
-	if rr, ok := r.(reader); ok {
-		return rr
-	}
-	return bufio.NewReader(r)
-}
-
-func matchMagic(magic string, b []byte) bool {
-	if len(magic) != len(b) {
-		return false
-	}
-	for i, c := range b {
-		if magic[i] != c && magic[i] != '?' {
-			return false
-		}
-	}
-	return true
-}
-
-func RegisterFormat(magic string, decodeMeta DecodeMetaFunc) {
-	formatsMu.Lock()
-	defer formatsMu.Unlock()
-
-	formats, _ := atomicFormats.Load().([]format)
-	atomicFormats.Store(append(formats, format{magic, decodeMeta}))
-}
-
-func DecodeMeta(r io.Reader) (Meta, error) {
-	rr := asReader(r)
-	formats, _ := atomicFormats.Load().([]format)
-
-	for _, f := range formats {
-		if b, err := rr.Peek(len(f.magic)); err == nil || errors.Is(err, io.EOF) {
-			if matchMagic(f.magic, b) {
-				return f.decodeMeta(rr)
-			}
-		} else {
-			return nil, err
-		}
-	}
-
-	if IsSVG(rr) {
-		return &meta{format: imagetype.SVG, width: 1, height: 1}, nil
-	}
-
-	return nil, newUnknownFormatError()
-}

+ 0 - 139
imagemeta/jpeg.go

@@ -1,139 +0,0 @@
-package imagemeta
-
-import (
-	"bufio"
-	"io"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-const (
-	// https://www.disktuna.com/list-of-jpeg-markers/
-	jpegSof0Marker  = 0xc0 // Start Of Frame (Baseline Sequential).
-	jpegSof1Marker  = 0xc1 // Start Of Frame (Extended Sequential DCT)
-	jpegSof2Marker  = 0xc2 // Start Of Frame (Progressive DCT )
-	jpegSof3Marker  = 0xc3 // Start Of Frame (Lossless sequential)
-	jpegSof5Marker  = 0xc5 // Start Of Frame (Differential sequential DCT)
-	jpegSof6Marker  = 0xc6 // Start Of Frame (Differential progressive DCT)
-	jpegSof7Marker  = 0xc7 // Start Of Frame (Differential lossless sequential)
-	jpegSof9Marker  = 0xc9 // Start Of Frame (Extended sequential DCT, Arithmetic coding)
-	jpegSof10Marker = 0xca // Start Of Frame (Progressive DCT, Arithmetic coding)
-	jpegSof11Marker = 0xcb // Start Of Frame (Lossless sequential, Arithmetic coding)
-	jpegSof13Marker = 0xcd // Start Of Frame (Differential sequential DCT, Arithmetic coding)
-	jpegSof14Marker = 0xce // Start Of Frame (Differential progressive DCT, Arithmetic coding)
-	jpegSof15Marker = 0xcf // Start Of Frame (Differential lossless sequential, Arithmetic coding).
-	jpegRst0Marker  = 0xd0 // ReSTart (0).
-	jpegRst7Marker  = 0xd7 // ReSTart (7).
-	jpegSoiMarker   = 0xd8 // Start Of Image.
-	jpegEoiMarker   = 0xd9 // End Of Image.
-	jpegSosMarker   = 0xda // Start Of Scan.
-)
-
-type jpegReader interface {
-	io.Reader
-	ReadByte() (byte, error)
-	Discard(n int) (discarded int, err error)
-}
-
-func asJpegReader(r io.Reader) jpegReader {
-	if rr, ok := r.(jpegReader); ok {
-		return rr
-	}
-	return bufio.NewReader(r)
-}
-
-func DecodeJpegMeta(rr io.Reader) (Meta, error) {
-	var tmp [512]byte
-
-	r := asJpegReader(rr)
-
-	if _, err := io.ReadFull(r, tmp[:2]); err != nil {
-		return nil, err
-	}
-	if tmp[0] != 0xff || tmp[1] != jpegSoiMarker {
-		return nil, newFormatError("JPEG", "missing SOI marker")
-	}
-
-	for {
-		_, err := io.ReadFull(r, tmp[:2])
-		if err != nil {
-			return nil, err
-		}
-
-		// This is not a segment, continue searching
-		for tmp[0] != 0xff {
-			tmp[0] = tmp[1]
-			tmp[1], err = r.ReadByte()
-			if err != nil {
-				return nil, err
-			}
-		}
-
-		marker := tmp[1]
-
-		if marker == 0 {
-			// Treat "\xff\x00" as extraneous data.
-			continue
-		}
-
-		// Marker can be preceded by fill bytes
-		for marker == 0xff {
-			marker, err = r.ReadByte()
-			if err != nil {
-				return nil, err
-			}
-		}
-
-		if marker == jpegEoiMarker { // End Of Image.
-			return nil, newFormatError("JPEG", "missing SOF marker")
-		}
-
-		if marker == jpegSoiMarker {
-			return nil, newFormatError("JPEG", "two SOI markers")
-		}
-
-		if jpegRst0Marker <= marker && marker <= jpegRst7Marker {
-			continue
-		}
-
-		if _, err = io.ReadFull(r, tmp[:2]); err != nil {
-			return nil, err
-		}
-		n := int(tmp[0])<<8 + int(tmp[1]) - 2
-		if n <= 0 {
-			// We should fail here, but libvips is more tolerant to this, so, continue
-			continue
-		}
-
-		switch marker {
-		case jpegSof0Marker, jpegSof1Marker, jpegSof2Marker, jpegSof3Marker, jpegSof5Marker,
-			jpegSof6Marker, jpegSof7Marker, jpegSof9Marker, jpegSof10Marker, jpegSof11Marker,
-			jpegSof13Marker, jpegSof14Marker, jpegSof15Marker:
-			if _, err := io.ReadFull(r, tmp[:5]); err != nil {
-				return nil, err
-			}
-			// We only support 8-bit precision.
-			if tmp[0] != 8 {
-				return nil, newFormatError("JPEG", "unsupported precision")
-			}
-
-			return &meta{
-				format: imagetype.JPEG,
-				width:  int(tmp[3])<<8 + int(tmp[4]),
-				height: int(tmp[1])<<8 + int(tmp[2]),
-			}, nil
-
-		case jpegSosMarker:
-			return nil, newFormatError("JPEG", "missing SOF marker")
-		}
-
-		// Skip any other uninteresting segments
-		if _, err := r.Discard(n); err != nil {
-			return nil, err
-		}
-	}
-}
-
-func init() {
-	RegisterFormat("\xff\xd8", DecodeJpegMeta)
-}

+ 0 - 52
imagemeta/jpeg_test.go

@@ -1,52 +0,0 @@
-package imagemeta
-
-import (
-	"os"
-	"path/filepath"
-	"testing"
-
-	"github.com/stretchr/testify/suite"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-type JpegTestSuite struct {
-	suite.Suite
-}
-
-func (s *JpegTestSuite) openFile(name string) *os.File {
-	wd, err := os.Getwd()
-	s.Require().NoError(err)
-	path := filepath.Join(wd, "..", "testdata", name)
-	f, err := os.Open(path)
-	s.Require().NoError(err)
-	return f
-}
-
-func (s *JpegTestSuite) TestDecodeJpegMeta() {
-	files := []string{
-		"test1.jpg",
-		"test1.arith.jpg",
-	}
-
-	expectedMeta := &meta{
-		format: imagetype.JPEG,
-		width:  10,
-		height: 10,
-	}
-
-	for _, file := range files {
-		func() {
-			f := s.openFile(file)
-			defer f.Close()
-
-			metadata, err := DecodeJpegMeta(f)
-			s.Require().NoError(err)
-			s.Require().Equal(expectedMeta, metadata)
-		}()
-	}
-}
-
-func TestJpeg(t *testing.T) {
-	suite.Run(t, new(JpegTestSuite))
-}

+ 0 - 253
imagemeta/jxl.go

@@ -1,253 +0,0 @@
-package imagemeta
-
-import (
-	"bytes"
-	"encoding/binary"
-	"io"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-const (
-	jxlCodestreamHeaderMinSize = 4
-	jxlCodestreamHeaderMaxSize = 11
-)
-
-var jxlCodestreamMarker = []byte{0xff, 0x0a}
-var jxlISOBMFFMarker = []byte{0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A}
-
-var jxlSizeSizes = []uint64{9, 13, 18, 30}
-
-var jxlRatios = [][]uint64{
-	{1, 1},
-	{12, 10},
-	{4, 3},
-	{3, 2},
-	{16, 9},
-	{5, 4},
-	{2, 1},
-}
-
-type jxlBitReader struct {
-	buf    uint64
-	bufLen uint64
-}
-
-func NewJxlBitReader(data []byte) *jxlBitReader {
-	return &jxlBitReader{
-		buf:    binary.LittleEndian.Uint64(data),
-		bufLen: uint64(len(data) * 8),
-	}
-}
-
-func (br *jxlBitReader) Read(n uint64) (uint64, error) {
-	if n > br.bufLen {
-		return 0, io.EOF
-	}
-
-	mask := uint64(1<<n) - 1
-	res := br.buf & mask
-
-	br.buf >>= n
-	br.bufLen -= n
-
-	return res, nil
-}
-
-func jxlReadJxlc(r io.Reader, boxDataSize uint64) ([]byte, error) {
-	if boxDataSize < jxlCodestreamHeaderMinSize {
-		return nil, newFormatError("JPEG XL", "invalid codestream box")
-	}
-
-	toRead := boxDataSize
-	if toRead > jxlCodestreamHeaderMaxSize {
-		toRead = jxlCodestreamHeaderMaxSize
-	}
-
-	return heifReadN(r, toRead)
-}
-
-func jxlReadJxlp(r io.Reader, boxDataSize uint64, codestream []byte) ([]byte, bool, error) {
-	if boxDataSize < 4 {
-		return nil, false, newFormatError("JPEG XL", "invalid jxlp box")
-	}
-
-	jxlpInd, err := heifReadN(r, 4)
-	if err != nil {
-		return nil, false, err
-	}
-
-	last := jxlpInd[0] == 0x80
-
-	readLeft := jxlCodestreamHeaderMaxSize - len(codestream)
-	if readLeft <= 0 {
-		return codestream, last, nil
-	}
-
-	toRead := boxDataSize - 4
-	if uint64(readLeft) < toRead {
-		toRead = uint64(readLeft)
-	}
-
-	data, err := heifReadN(r, toRead)
-	if err != nil {
-		return nil, last, err
-	}
-
-	if codestream == nil {
-		codestream = make([]byte, 0, jxlCodestreamHeaderMaxSize)
-	}
-
-	return append(codestream, data...), last, nil
-}
-
-// We can reuse HEIF functions to read ISO BMFF boxes
-func jxlFindCodestream(r io.Reader) ([]byte, error) {
-	var (
-		codestream []byte
-		last       bool
-	)
-
-	for {
-		boxType, boxDataSize, err := heifReadBoxHeader(r)
-		if err != nil {
-			return nil, err
-		}
-
-		switch boxType {
-		// jxlc box contins full codestream.
-		// We can just read and return its header
-		case "jxlc":
-			codestream, err = jxlReadJxlc(r, boxDataSize)
-			return codestream, err
-
-		// jxlp partial codestream.
-		// We should read its data until we read jxlCodestreamHeaderSize bytes
-		case "jxlp":
-			codestream, last, err = jxlReadJxlp(r, boxDataSize, codestream)
-			if err != nil {
-				return nil, err
-			}
-
-			csLen := len(codestream)
-			if csLen >= jxlCodestreamHeaderMaxSize || (last && csLen >= jxlCodestreamHeaderMinSize) {
-				return codestream, nil
-			}
-
-			if last {
-				return nil, newFormatError("JPEG XL", "invalid codestream box")
-			}
-
-		// Skip other boxes
-		default:
-			if err := heifDiscardN(r, boxDataSize); err != nil {
-				return nil, err
-			}
-		}
-	}
-}
-
-func jxlParseSize(br *jxlBitReader, small bool) (uint64, error) {
-	if small {
-		size, err := br.Read(5)
-		return (size + 1) * 8, err
-	} else {
-		selector, err := br.Read(2)
-		if err != nil {
-			return 0, err
-		}
-
-		sizeSize := jxlSizeSizes[selector]
-		size, err := br.Read(sizeSize)
-
-		return size + 1, err
-	}
-}
-
-func jxlDecodeCodestreamHeader(buf []byte) (width, height uint64, err error) {
-	if len(buf) < jxlCodestreamHeaderMinSize {
-		return 0, 0, newFormatError("JPEG XL", "invalid codestream header")
-	}
-
-	if !bytes.Equal(buf[0:2], jxlCodestreamMarker) {
-		return 0, 0, newFormatError("JPEG XL", "missing codestream marker")
-	}
-
-	br := NewJxlBitReader(buf[2:])
-
-	smallBit, sbErr := br.Read(1)
-	if sbErr != nil {
-		return 0, 0, sbErr
-	}
-
-	small := smallBit == 1
-
-	height, err = jxlParseSize(br, small)
-	if err != nil {
-		return 0, 0, err
-	}
-
-	ratioIdx, riErr := br.Read(3)
-	if riErr != nil {
-		return 0, 0, riErr
-	}
-
-	if ratioIdx == 0 {
-		width, err = jxlParseSize(br, small)
-	} else {
-		ratio := jxlRatios[ratioIdx-1]
-		width = height * ratio[0] / ratio[1]
-	}
-
-	return
-}
-
-func DecodeJxlMeta(r io.Reader) (Meta, error) {
-	var (
-		tmp           [12]byte
-		codestream    []byte
-		width, height uint64
-		err           error
-	)
-
-	if _, err = io.ReadFull(r, tmp[:2]); err != nil {
-		return nil, err
-	}
-
-	if bytes.Equal(tmp[0:2], jxlCodestreamMarker) {
-		if _, err = io.ReadFull(r, tmp[2:]); err != nil {
-			return nil, err
-		}
-
-		codestream = tmp[:]
-	} else {
-		if _, err = io.ReadFull(r, tmp[2:12]); err != nil {
-			return nil, err
-		}
-
-		if !bytes.Equal(tmp[0:12], jxlISOBMFFMarker) {
-			return nil, newFormatError("JPEG XL", "invalid header")
-		}
-
-		codestream, err = jxlFindCodestream(r)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	width, height, err = jxlDecodeCodestreamHeader(codestream)
-	if err != nil {
-		return nil, err
-	}
-
-	return &meta{
-		format: imagetype.JXL,
-		width:  int(width),
-		height: int(height),
-	}, nil
-}
-
-func init() {
-	RegisterFormat(string(jxlCodestreamMarker), DecodeJxlMeta)
-	RegisterFormat(string(jxlISOBMFFMarker), DecodeJxlMeta)
-}

+ 0 - 37
imagemeta/png.go

@@ -1,37 +0,0 @@
-package imagemeta
-
-import (
-	"bytes"
-	"encoding/binary"
-	"io"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-var pngMagick = []byte("\x89PNG\r\n\x1a\n")
-
-func DecodePngMeta(r io.Reader) (Meta, error) {
-	var tmp [16]byte
-
-	if _, err := io.ReadFull(r, tmp[:8]); err != nil {
-		return nil, err
-	}
-
-	if !bytes.Equal(pngMagick, tmp[:8]) {
-		return nil, newFormatError("PNG", "not a PNG image")
-	}
-
-	if _, err := io.ReadFull(r, tmp[:]); err != nil {
-		return nil, err
-	}
-
-	return &meta{
-		format: imagetype.PNG,
-		width:  int(binary.BigEndian.Uint32(tmp[8:12])),
-		height: int(binary.BigEndian.Uint32(tmp[12:16])),
-	}, nil
-}
-
-func init() {
-	RegisterFormat(string(pngMagick), DecodePngMeta)
-}

+ 0 - 30
imagemeta/svg.go

@@ -1,30 +0,0 @@
-package imagemeta
-
-import (
-	"io"
-	"strings"
-
-	"github.com/imgproxy/imgproxy/v3/config"
-
-	"github.com/tdewolff/parse/v2"
-	"github.com/tdewolff/parse/v2/xml"
-)
-
-func IsSVG(r io.Reader) bool {
-	maxBytes := config.MaxSvgCheckBytes
-
-	l := xml.NewLexer(parse.NewInput(io.LimitReader(r, int64(maxBytes))))
-
-	for {
-		tt, _ := l.Next()
-
-		switch tt {
-		case xml.ErrorToken:
-			return false
-
-		case xml.StartTagToken:
-			tag := strings.ToLower(string(l.Text()))
-			return tag == "svg" || tag == "svg:svg"
-		}
-	}
-}

+ 0 - 119
imagemeta/tiff.go

@@ -1,119 +0,0 @@
-package imagemeta
-
-import (
-	"bufio"
-	"bytes"
-	"encoding/binary"
-	"io"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-var (
-	tiffLeHeader = []byte("II\x2A\x00")
-	tiffBeHeader = []byte("MM\x00\x2A")
-)
-
-const (
-	tiffDtByte  = 1
-	tiffDtShort = 3
-	tiffDtLong  = 4
-
-	tiffImageWidth  = 256
-	tiffImageLength = 257
-)
-
-type tiffReader interface {
-	io.Reader
-	Discard(n int) (discarded int, err error)
-}
-
-func asTiffReader(r io.Reader) tiffReader {
-	if rr, ok := r.(tiffReader); ok {
-		return rr
-	}
-	return bufio.NewReader(r)
-}
-
-func DecodeTiffMeta(rr io.Reader) (Meta, error) {
-	var (
-		tmp       [12]byte
-		byteOrder binary.ByteOrder
-	)
-
-	r := asTiffReader(rr)
-
-	if _, err := io.ReadFull(r, tmp[:8]); err != nil {
-		return nil, err
-	}
-
-	switch {
-	case bytes.Equal(tiffLeHeader, tmp[0:4]):
-		byteOrder = binary.LittleEndian
-	case bytes.Equal(tiffBeHeader, tmp[0:4]):
-		byteOrder = binary.BigEndian
-	default:
-		return nil, newFormatError("TIFF", "malformed header")
-	}
-
-	ifdOffset := int(byteOrder.Uint32(tmp[4:8]))
-
-	if _, err := r.Discard(ifdOffset - 8); err != nil {
-		return nil, err
-	}
-
-	if _, err := io.ReadFull(r, tmp[0:2]); err != nil {
-		return nil, err
-	}
-	numItems := int(byteOrder.Uint16(tmp[0:2]))
-
-	var width, height int
-
-	for i := 0; i < numItems; i++ {
-		if _, err := io.ReadFull(r, tmp[:]); err != nil {
-			return nil, err
-		}
-
-		tag := byteOrder.Uint16(tmp[0:2])
-
-		if tag != tiffImageWidth && tag != tiffImageLength {
-			continue
-		}
-
-		datatype := byteOrder.Uint16(tmp[2:4])
-
-		var value int
-
-		switch datatype {
-		case tiffDtByte:
-			value = int(tmp[8])
-		case tiffDtShort:
-			value = int(byteOrder.Uint16(tmp[8:10]))
-		case tiffDtLong:
-			value = int(byteOrder.Uint32(tmp[8:12]))
-		default:
-			return nil, newFormatError("TIFF", "unsupported IFD entry datatype")
-		}
-
-		if tag == tiffImageWidth {
-			width = value
-		} else {
-			height = value
-		}
-
-		if width > 0 && height > 0 {
-			return &meta{
-				format: imagetype.TIFF,
-				width:  width,
-				height: height,
-			}, nil
-		}
-	}
-
-	return nil, newFormatError("TIFF", "image dimensions are not specified")
-}
-
-func init() {
-	RegisterFormat(string(tiffLeHeader), DecodeTiffMeta)
-	RegisterFormat(string(tiffBeHeader), DecodeTiffMeta)
-}

+ 0 - 103
imagemeta/webp.go

@@ -1,103 +0,0 @@
-// Copyright 2011 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Original code was cropped and fixed by @DarthSim for imgproxy needs
-
-package imagemeta
-
-import (
-	"io"
-
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-	"golang.org/x/image/riff"
-	"golang.org/x/image/vp8"
-	"golang.org/x/image/vp8l"
-)
-
-var (
-	webpFccALPH = riff.FourCC{'A', 'L', 'P', 'H'}
-	webpFccVP8  = riff.FourCC{'V', 'P', '8', ' '}
-	webpFccVP8L = riff.FourCC{'V', 'P', '8', 'L'}
-	webpFccVP8X = riff.FourCC{'V', 'P', '8', 'X'}
-	webpFccWEBP = riff.FourCC{'W', 'E', 'B', 'P'}
-)
-
-func DecodeWebpMeta(r io.Reader) (Meta, error) {
-	formType, riffReader, err := riff.NewReader(r)
-	if err != nil {
-		return nil, err
-	}
-	if formType != webpFccWEBP {
-		return nil, newFormatError("WEBP", "invalid form type")
-	}
-
-	var buf [10]byte
-
-	for {
-		chunkID, chunkLen, chunkData, err := riffReader.Next()
-		if err == io.EOF {
-			err = newFormatError("WEBP", "no VP8, VP8L or VP8X chunk found")
-		}
-		if err != nil {
-			return nil, err
-		}
-
-		switch chunkID {
-		case webpFccALPH:
-			// Ignore
-		case webpFccVP8:
-			if int32(chunkLen) < 0 {
-				return nil, newFormatError("WEBP", "invalid chunk length")
-			}
-
-			d := vp8.NewDecoder()
-			d.Init(chunkData, int(chunkLen))
-
-			fh, err := d.DecodeFrameHeader()
-
-			return &meta{
-				format: imagetype.WEBP,
-				width:  fh.Width,
-				height: fh.Height,
-			}, err
-
-		case webpFccVP8L:
-			conf, err := vp8l.DecodeConfig(chunkData)
-			if err != nil {
-				return nil, err
-			}
-
-			return &meta{
-				format: imagetype.WEBP,
-				width:  conf.Width,
-				height: conf.Height,
-			}, nil
-
-		case webpFccVP8X:
-			if chunkLen != 10 {
-				return nil, newFormatError("WEBP", "invalid chunk length")
-			}
-
-			if _, err := io.ReadFull(chunkData, buf[:10]); err != nil {
-				return nil, err
-			}
-
-			widthMinusOne := uint32(buf[4]) | uint32(buf[5])<<8 | uint32(buf[6])<<16
-			heightMinusOne := uint32(buf[7]) | uint32(buf[8])<<8 | uint32(buf[9])<<16
-
-			return &meta{
-				format: imagetype.WEBP,
-				width:  int(widthMinusOne) + 1,
-				height: int(heightMinusOne) + 1,
-			}, nil
-
-		default:
-			return nil, newFormatError("WEBP", "unknown chunk")
-		}
-	}
-}
-
-func init() {
-	RegisterFormat("RIFF????WEBPVP8", DecodeWebpMeta)
-}

+ 3 - 0
processing/processing.go

@@ -289,6 +289,9 @@ func ProcessImage(ctx context.Context, imgdata imagedata.ImageData, po *options.
 	}
 
 	originWidth, originHeight := getImageSize(img)
+	if err := security.CheckDimensions(originWidth, originHeight, 1, po.SecurityOptions); err != nil {
+		return nil, err
+	}
 
 	animated := img.IsAnimated()
 	expectAlpha := !po.Flatten && (img.HasAlpha() || po.Padding.Enabled || po.Extend.Enabled)

+ 18 - 9
processing/processing_test.go

@@ -12,9 +12,9 @@ import (
 	"github.com/stretchr/testify/suite"
 
 	"github.com/imgproxy/imgproxy/v3/config"
+	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/options"
-	"github.com/imgproxy/imgproxy/v3/security"
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
@@ -25,6 +25,11 @@ type ProcessingTestSuite struct {
 func (s *ProcessingTestSuite) SetupSuite() {
 	config.Reset()
 
+	config.MaxSrcResolution = 10 * 1024 * 1024
+	config.MaxSrcFileSize = 10 * 1024 * 1024
+	config.MaxAnimationFrames = 100
+	config.MaxAnimationFrameResolution = 10 * 1024 * 1024
+
 	s.Require().NoError(imagedata.Init())
 	s.Require().NoError(vips.Init())
 
@@ -32,18 +37,11 @@ func (s *ProcessingTestSuite) SetupSuite() {
 }
 
 func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
-	secopts := security.Options{
-		MaxSrcResolution:            10 * 1024 * 1024,
-		MaxSrcFileSize:              10 * 1024 * 1024,
-		MaxAnimationFrames:          100,
-		MaxAnimationFrameResolution: 10 * 1024 * 1024,
-	}
-
 	wd, err := os.Getwd()
 	s.Require().NoError(err)
 	path := filepath.Join(wd, "..", "testdata", name)
 
-	imagedata, err := imagedata.NewFromPath(path, secopts)
+	imagedata, err := imagedata.NewFromPath(path)
 	s.Require().NoError(err)
 
 	return imagedata
@@ -986,6 +984,17 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
 	}
 }
 
+func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
+	po := options.NewProcessingOptions()
+	po.SecurityOptions.MaxSrcResolution = 1
+
+	imgdata := s.openFile("test2.jpg")
+	_, err := ProcessImage(context.Background(), imgdata, po)
+
+	s.Require().Error(err)
+	s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
+}
+
 func TestProcessing(t *testing.T) {
 	suite.Run(t, new(ProcessingTestSuite))
 }

+ 4 - 3
processing_handler.go

@@ -370,8 +370,9 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 		})()
 
 		downloadOpts := imagedata.DownloadOptions{
-			Header:    imgRequestHeader,
-			CookieJar: nil,
+			Header:         imgRequestHeader,
+			CookieJar:      nil,
+			MaxSrcFileSize: po.SecurityOptions.MaxSrcFileSize,
 		}
 
 		if config.CookiePassthrough {
@@ -379,7 +380,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 			checkErr(ctx, "download", err)
 		}
 
-		return imagedata.DownloadAsync(ctx, imageURL, "source image", downloadOpts, po.SecurityOptions)
+		return imagedata.DownloadAsync(ctx, imageURL, "source image", downloadOpts)
 	}()
 
 	var nmErr imagefetcher.NotModifiedError

+ 17 - 5
processing_handler_test.go

@@ -20,7 +20,6 @@ import (
 	"github.com/imgproxy/imgproxy/v3/etag"
 	"github.com/imgproxy/imgproxy/v3/httpheaders"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
-	"github.com/imgproxy/imgproxy/v3/imagemeta"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/options"
 	"github.com/imgproxy/imgproxy/v3/router"
@@ -135,12 +134,10 @@ func (s *ProcessingHandlerTestSuite) TestRequest() {
 	s.Require().Equal(200, res.StatusCode)
 	s.Require().Equal("image/png", res.Header.Get("Content-Type"))
 
-	meta, err := imagemeta.DecodeMeta(res.Body)
+	format, err := imagetype.Detect(res.Body)
 
 	s.Require().NoError(err)
-	s.Require().Equal(imagetype.PNG, meta.Format())
-	s.Require().Equal(4, meta.Width())
-	s.Require().Equal(4, meta.Height())
+	s.Require().Equal(imagetype.PNG, format)
 }
 
 func (s *ProcessingHandlerTestSuite) TestSignatureValidationFailure() {
@@ -770,6 +767,21 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
 	s.Require().Equal("image/svg+xml", res.Header.Get("Content-Type"))
 }
 
+func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
+	config.MaxSrcFileSize = 1
+
+	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		rw.WriteHeader(200)
+		rw.Write(s.readTestFile("test1.png"))
+	}))
+	defer ts.Close()
+
+	rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
+	res := rw.Result()
+
+	s.Require().Equal(422, res.StatusCode)
+}
+
 func TestProcessingHandler(t *testing.T) {
 	suite.Run(t, new(ProcessingHandlerTestSuite))
 }

+ 0 - 5
security/image_size.go

@@ -1,7 +1,6 @@
 package security
 
 import (
-	"github.com/imgproxy/imgproxy/v3/imagemeta"
 	"github.com/imgproxy/imgproxy/v3/imath"
 )
 
@@ -20,7 +19,3 @@ func CheckDimensions(width, height, frames int, opts Options) error {
 
 	return nil
 }
-
-func CheckMeta(meta imagemeta.Meta, opts Options) error {
-	return CheckDimensions(meta.Width(), meta.Height(), 1, opts)
-}

+ 4 - 26
security/response_limit.go

@@ -3,7 +3,6 @@ package security
 import (
 	"io"
 	"net/http"
-	"os"
 )
 
 // hardLimitReadCloser is a wrapper around io.ReadCloser
@@ -33,40 +32,19 @@ func (lr *hardLimitReadCloser) Close() error {
 // First, it tries to use Content-Length header to check the limit.
 // If Content-Length is not set, it limits the size of the response body by wrapping
 // body reader with hard limit reader.
-func LimitResponseSize(r *http.Response, opts Options) (*http.Response, error) {
-	if opts.MaxSrcFileSize == 0 {
+func LimitResponseSize(r *http.Response, limit int) (*http.Response, error) {
+	if limit == 0 {
 		return r, nil
 	}
 
 	// If Content-Length was set, limit the size of the response body before reading it
 	size := int(r.ContentLength)
-
-	if size > opts.MaxSrcFileSize {
+	if size > limit {
 		return nil, newFileSizeError()
 	}
 
 	// hard-limit the response body reader
-	r.Body = &hardLimitReadCloser{r: r.Body, left: opts.MaxSrcFileSize}
+	r.Body = &hardLimitReadCloser{r: r.Body, left: limit}
 
 	return r, nil
 }
-
-// LimitFileSize limits the size of the file to MaxSrcFileSize (if set).
-// It calls f.Stat() to get the file to get its size and returns an error
-// if the size exceeds MaxSrcFileSize.
-func LimitFileSize(f *os.File, opts Options) (*os.File, error) {
-	if opts.MaxSrcFileSize == 0 {
-		return f, nil
-	}
-
-	s, err := f.Stat()
-	if err != nil {
-		return nil, err
-	}
-
-	if int(s.Size()) > opts.MaxSrcFileSize {
-		return nil, newFileSizeError()
-	}
-
-	return f, nil
-}