Viktor Sokolov 2 месяцев назад
Родитель
Сommit
b1760061fe

+ 1 - 1
etag/etag.go

@@ -107,7 +107,7 @@ func (h *Handler) ImageEtagExpected() string {
 	return h.imgEtagExpected
 }
 
-func (h *Handler) SetActualImageData(imgdata *imagedata.ImageData) (bool, error) {
+func (h *Handler) SetActualImageData(imgdata imagedata.ImageData) (bool, error) {
 	var haveActualImgETag bool
 	h.imgEtagActual = imgdata.Headers().Get(httpheaders.Etag)
 	haveActualImgETag = len(h.imgEtagActual) > 0

+ 5 - 5
etag/etag_test.go

@@ -2,7 +2,6 @@ package etag
 
 import (
 	"io"
-	"net/http"
 	"os"
 	"strings"
 	"testing"
@@ -25,8 +24,8 @@ type EtagTestSuite struct {
 	suite.Suite
 
 	po             *options.ProcessingOptions
-	imgWithETag    *imagedata.ImageData
-	imgWithoutETag *imagedata.ImageData
+	imgWithETag    imagedata.ImageData
+	imgWithoutETag imagedata.ImageData
 
 	h Handler
 }
@@ -38,10 +37,11 @@ func (s *EtagTestSuite) SetupSuite() {
 	d, err := os.ReadFile("../testdata/test1.jpg")
 	s.Require().NoError(err)
 
-	imgWithETag, err := imagedata.NewFromBytes(d, http.Header{httpheaders.Etag: []string{`"loremipsumdolor"`}})
+	imgWithETag, err := imagedata.NewFromBytes(d)
 	s.Require().NoError(err)
+	imgWithETag.Headers().Add(httpheaders.Etag, `"loremipsumdolor"`)
 
-	imgWithoutETag, err := imagedata.NewFromBytes(d, make(http.Header))
+	imgWithoutETag, err := imagedata.NewFromBytes(d)
 	s.Require().NoError(err)
 
 	s.imgWithETag = imgWithETag

+ 7 - 2
imagedata/download.go

@@ -37,7 +37,7 @@ func initDownloading() error {
 	return nil
 }
 
-func download(ctx context.Context, imageURL string, opts DownloadOptions, secopts security.Options) (*ImageData, error) {
+func download(ctx context.Context, imageURL string, opts DownloadOptions, secopts security.Options) (ImageData, error) {
 	// We use this for testing
 	if len(redirectAllRequestsTo) > 0 {
 		imageURL = redirectAllRequestsTo
@@ -70,7 +70,12 @@ func download(ctx context.Context, imageURL string, opts DownloadOptions, secopt
 		return nil, ierrors.Wrap(err, 0)
 	}
 
-	imgdata.headers = res.Header.Clone()
+	// NOTE: This will be removed in the future in favor of headers/image data separation
+	for k, v := range res.Header {
+		for _, v := range v {
+			imgdata.Headers().Add(k, v)
+		}
+	}
 
 	return imgdata, nil
 }

+ 74 - 8
imagedata/factory.go

@@ -2,14 +2,30 @@ package imagedata
 
 import (
 	"bytes"
+	"context"
+	"encoding/base64"
+	"io"
 	"net/http"
+	"os"
 
 	"github.com/imgproxy/imgproxy/v3/imagemeta"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
+	"github.com/imgproxy/imgproxy/v3/security"
 )
 
+// NewFromBytesWithFormat creates a new ImageData instance from the provided format,
+// http headers and byte slice.
+func NewFromBytesWithFormat(format imagetype.Type, b []byte, headers http.Header) ImageData {
+	return &imageDataBytes{
+		data:    b,
+		format:  format,
+		headers: headers,
+		cancel:  make([]context.CancelFunc, 0),
+	}
+}
+
 // NewFromBytes creates a new ImageData instance from the provided byte slice.
-func NewFromBytes(b []byte, headers http.Header) (*ImageData, error) {
+func NewFromBytes(b []byte) (ImageData, error) {
 	r := bytes.NewReader(b)
 
 	meta, err := imagemeta.DecodeMeta(r)
@@ -17,14 +33,64 @@ func NewFromBytes(b []byte, headers http.Header) (*ImageData, error) {
 		return nil, err
 	}
 
-	return NewFromBytesWithFormat(meta.Format(), b, headers), nil
+	return NewFromBytesWithFormat(meta.Format(), b, make(http.Header)), nil
 }
 
-// NewFromBytesWithFormat creates a new ImageData instance from the provided format and byte slice.
-func NewFromBytesWithFormat(format imagetype.Type, b []byte, headers http.Header) *ImageData {
-	return &ImageData{
-		data:    b,
-		format:  format,
-		headers: headers,
+// NewFromPath creates a new ImageData from an os.File
+func NewFromPath(path string, secopts security.Options) (ImageData, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	fr, err := security.LimitFileSize(f, 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)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewFromBytes(b)
+}
+
+// NewFromBase64 creates a new ImageData from a base64 encoded byte slice
+func NewFromBase64(encoded string, secopts security.Options) (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)
 }

+ 56 - 57
imagedata/image_data.go

@@ -3,12 +3,9 @@ package imagedata
 import (
 	"bytes"
 	"context"
-	"encoding/base64"
 	"fmt"
 	"io"
 	"net/http"
-	"os"
-	"strings"
 	"sync"
 
 	"github.com/imgproxy/imgproxy/v3/config"
@@ -18,23 +15,35 @@ import (
 )
 
 var (
-	Watermark     *ImageData
-	FallbackImage *ImageData
+	Watermark     ImageData
+	FallbackImage ImageData
 )
 
-type ImageData struct {
+type ImageData interface {
+	io.Closer                                // Close closes the image data and releases any resources held by it
+	Reader() io.ReadSeeker                   // Reader returns a new ReadSeeker for the image data
+	Format() imagetype.Type                  // Format returns the image format from the metadata (shortcut)
+	Size() (int, error)                      // Size returns the size of the image data in bytes
+	WithCancel(context.CancelFunc) ImageData // WithCancel attaches a cancel function to the image data
+
+	// This will be removed in the future
+	Headers() http.Header // Headers returns the HTTP headers of the image data, will be removed in the future
+}
+
+// imageDataBytes represents image data stored in a byte slice in memory
+type imageDataBytes struct {
 	format  imagetype.Type
 	data    []byte
 	headers http.Header
 
-	cancel     context.CancelFunc
+	cancel     []context.CancelFunc
 	cancelOnce sync.Once
 }
 
-func (d *ImageData) Close() error {
+func (d *imageDataBytes) Close() error {
 	d.cancelOnce.Do(func() {
-		if d.cancel != nil {
-			d.cancel()
+		for _, cancel := range d.cancel {
+			cancel()
 		}
 	})
 
@@ -42,27 +51,28 @@ func (d *ImageData) Close() error {
 }
 
 // Format returns the image format based on the metadata
-func (d *ImageData) Format() imagetype.Type {
+func (d *imageDataBytes) Format() imagetype.Type {
 	return d.format
 }
 
 // Reader returns an io.ReadSeeker for the image data
-func (d *ImageData) Reader() io.ReadSeeker {
+func (d *imageDataBytes) Reader() io.ReadSeeker {
 	return bytes.NewReader(d.data)
 }
 
 // Size returns the size of the image data in bytes.
 // NOTE: asyncbuffer implementation will .Wait() for the data to be fully read
-func (d *ImageData) Size() (int, error) {
+func (d *imageDataBytes) Size() (int, error) {
 	return len(d.data), nil
 }
 
-func (d *ImageData) Headers() http.Header {
+func (d *imageDataBytes) Headers() http.Header {
 	return d.headers
 }
 
-func (d *ImageData) SetCancel(cancel context.CancelFunc) {
-	d.cancel = cancel
+func (d *imageDataBytes) WithCancel(cancel context.CancelFunc) ImageData {
+	d.cancel = append(d.cancel, cancel)
+	return d
 }
 
 func Init() error {
@@ -83,20 +93,32 @@ func Init() error {
 	return nil
 }
 
-func loadWatermark() (err error) {
+func loadWatermark() error {
+	var err error
+
 	if len(config.WatermarkData) > 0 {
-		Watermark, err = FromBase64(config.WatermarkData, "watermark", security.DefaultOptions())
-		return
+		Watermark, err = NewFromBase64(config.WatermarkData, security.DefaultOptions())
+
+		// 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
+		// .WithPrefix("load from base64")
+		if err != nil {
+			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't load watermark from Base64"))
+		}
 	}
 
 	if len(config.WatermarkPath) > 0 {
-		Watermark, err = FromFile(config.WatermarkPath, "watermark", security.DefaultOptions())
-		return
+		Watermark, err = NewFromPath(config.WatermarkPath, security.DefaultOptions())
+		if err != nil {
+			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't read watermark from file"))
+		}
 	}
 
 	if len(config.WatermarkURL) > 0 {
 		Watermark, err = Download(context.Background(), config.WatermarkURL, "watermark", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
-		return
+		if err != nil {
+			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
+		}
 	}
 
 	return nil
@@ -105,9 +127,17 @@ func loadWatermark() (err error) {
 func loadFallbackImage() (err error) {
 	switch {
 	case len(config.FallbackImageData) > 0:
-		FallbackImage, err = FromBase64(config.FallbackImageData, "fallback image", security.DefaultOptions())
+		FallbackImage, err = NewFromBase64(config.FallbackImageData, security.DefaultOptions())
+		if err != nil {
+			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't load fallback image from Base64"))
+		}
+
 	case len(config.FallbackImagePath) > 0:
-		FallbackImage, err = FromFile(config.FallbackImagePath, "fallback image", security.DefaultOptions())
+		FallbackImage, err = NewFromPath(config.FallbackImagePath, security.DefaultOptions())
+		if err != nil {
+			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't read fallback image from file"))
+		}
+
 	case len(config.FallbackImageURL) > 0:
 		FallbackImage, err = Download(context.Background(), config.FallbackImageURL, "fallback image", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
 	default:
@@ -115,44 +145,13 @@ func loadFallbackImage() (err error) {
 	}
 
 	if FallbackImage != nil && err == nil && config.FallbackImageTTL > 0 {
-		FallbackImage.headers.Set("Fallback-Image", "1")
+		FallbackImage.Headers().Set("Fallback-Image", "1")
 	}
 
 	return err
 }
 
-func FromBase64(encoded, desc string, secopts security.Options) (*ImageData, error) {
-	dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encoded))
-	size := 4 * (len(encoded)/3 + 1)
-
-	imgdata, err := readAndCheckImage(dec, size, secopts)
-	if err != nil {
-		return nil, fmt.Errorf("Can't decode %s: %s", desc, err)
-	}
-
-	return imgdata, nil
-}
-
-func FromFile(path, desc string, secopts security.Options) (*ImageData, error) {
-	f, err := os.Open(path)
-	if err != nil {
-		return nil, fmt.Errorf("Can't read %s: %s", desc, err)
-	}
-
-	fi, err := f.Stat()
-	if err != nil {
-		return nil, fmt.Errorf("Can't read %s: %s", desc, err)
-	}
-
-	imgdata, err := readAndCheckImage(f, int(fi.Size()), secopts)
-	if err != nil {
-		return nil, fmt.Errorf("Can't read %s: %s", desc, err)
-	}
-
-	return imgdata, nil
-}
-
-func Download(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (*ImageData, error) {
+func Download(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (ImageData, error) {
 	imgdata, err := download(ctx, imageURL, opts, secopts)
 	if err != nil {
 		return nil, ierrors.Wrap(

+ 0 - 22
imagedata/image_data_i.go

@@ -1,22 +0,0 @@
-package imagedata
-
-import (
-	"io"
-	"net/http"
-
-	"github.com/imgproxy/imgproxy/v3/imagemeta"
-	"github.com/imgproxy/imgproxy/v3/imagetype"
-)
-
-// NOTE: This is temporary naming, will be fixed in the subsequent PR
-// ImageDataI is an interface that defines methods for reading image data and metadata
-type ImageDataI interface {
-	io.Closer               // Close closes the image data and releases any resources held by it
-	Reader() io.ReadSeeker  // Reader returns a new ReadSeeker for the image data
-	Meta() imagemeta.Meta   // Meta returns the metadata of the image data
-	Format() imagetype.Type // Format returns the image format from the metadata (shortcut)
-	Size() (int, error)     // Size returns the size of the image data in bytes
-
-	// This will be removed in the future
-	Headers() http.Header // Headers returns the HTTP headers of the image data, will be removed in the future
-}

+ 8 - 7
imagedata/image_data_test.go

@@ -20,6 +20,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/security"
+	"github.com/imgproxy/imgproxy/v3/testutil"
 )
 
 type ImageDataTestSuite struct {
@@ -94,7 +95,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusOK() {
 
 	s.Require().NoError(err)
 	s.Require().NotNil(imgdata)
-	s.Require().Equal(s.defaultData, imgdata.data)
+	s.Require().True(testutil.ReadersEqual(s.T(), bytes.NewReader(s.defaultData), imgdata.Reader()))
 	s.Require().Equal(imagetype.JPEG, imgdata.Format())
 }
 
@@ -165,7 +166,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusPartialContent() {
 			} else {
 				s.Require().NoError(err)
 				s.Require().NotNil(imgdata)
-				s.Require().Equal(s.defaultData, imgdata.data)
+				s.Require().True(testutil.ReadersEqual(s.T(), bytes.NewReader(s.defaultData), imgdata.Reader()))
 				s.Require().Equal(imagetype.JPEG, imgdata.Format())
 			}
 		})
@@ -278,27 +279,27 @@ func (s *ImageDataTestSuite) TestDownloadGzip() {
 
 	s.Require().NoError(err)
 	s.Require().NotNil(imgdata)
-	s.Require().Equal(s.defaultData, imgdata.data)
+	s.Require().True(testutil.ReadersEqual(s.T(), bytes.NewReader(s.defaultData), imgdata.Reader()))
 	s.Require().Equal(imagetype.JPEG, imgdata.Format())
 }
 
 func (s *ImageDataTestSuite) TestFromFile() {
-	imgdata, err := FromFile("../testdata/test1.jpg", "Test image", security.DefaultOptions())
+	imgdata, err := NewFromPath("../testdata/test1.jpg", security.DefaultOptions())
 
 	s.Require().NoError(err)
 	s.Require().NotNil(imgdata)
-	s.Require().Equal(s.defaultData, imgdata.data)
+	s.Require().True(testutil.ReadersEqual(s.T(), bytes.NewReader(s.defaultData), imgdata.Reader()))
 	s.Require().Equal(imagetype.JPEG, imgdata.Format())
 }
 
 func (s *ImageDataTestSuite) TestFromBase64() {
 	b64 := base64.StdEncoding.EncodeToString(s.defaultData)
 
-	imgdata, err := FromBase64(b64, "Test image", security.DefaultOptions())
+	imgdata, err := NewFromBase64(b64, security.DefaultOptions())
 
 	s.Require().NoError(err)
 	s.Require().NotNil(imgdata)
-	s.Require().Equal(s.defaultData, imgdata.data)
+	s.Require().True(testutil.ReadersEqual(s.T(), bytes.NewReader(s.defaultData), imgdata.Reader()))
 	s.Require().Equal(imagetype.JPEG, imgdata.Format())
 }
 

+ 3 - 6
imagedata/read.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"io"
+	"net/http"
 
 	"github.com/imgproxy/imgproxy/v3/bufpool"
 	"github.com/imgproxy/imgproxy/v3/bufreader"
@@ -19,7 +20,7 @@ func initRead() {
 	downloadBufPool = bufpool.New("download", config.Workers, config.DownloadBufferSize)
 }
 
-func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options) (*ImageData, error) {
+func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options) (ImageData, error) {
 	buf := downloadBufPool.Get(contentLength, false)
 	cancel := func() { downloadBufPool.Put(buf) }
 
@@ -49,11 +50,7 @@ func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options)
 		return nil, imagefetcher.WrapError(err)
 	}
 
-	return &ImageData{
-		data:   buf.Bytes(),
-		format: meta.Format(),
-		cancel: cancel,
-	}, nil
+	return NewFromBytesWithFormat(meta.Format(), buf.Bytes(), make(http.Header)).WithCancel(cancel), nil
 }
 
 func BorrowBuffer() (*bytes.Buffer, context.CancelFunc) {

+ 1 - 1
processing/apply_filters.go

@@ -6,7 +6,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func applyFilters(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func applyFilters(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if po.Blur == 0 && po.Sharpen == 0 && po.Pixelate <= 1 {
 		return nil
 	}

+ 2 - 2
processing/crop.go

@@ -32,7 +32,7 @@ func cropImage(img *vips.Image, cropWidth, cropHeight int, gravity *options.Grav
 	return img.Crop(left, top, cropWidth, cropHeight)
 }
 
-func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	width, height := pctx.cropWidth, pctx.cropHeight
 
 	opts := pctx.cropGravity
@@ -47,6 +47,6 @@ func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions,
 	return cropImage(img, width, height, &opts, 1.0)
 }
 
-func cropToResult(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func cropToResult(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	return cropImage(img, pctx.resultCropWidth, pctx.resultCropHeight, &po.Gravity, pctx.dprScale)
 }

+ 1 - 1
processing/export_color_profile.go

@@ -6,7 +6,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func exportColorProfile(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func exportColorProfile(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	keepProfile := !po.StripColorProfile && po.Format.SupportsColourProfile()
 
 	if img.IsLinear() {

+ 2 - 2
processing/extend.go

@@ -25,7 +25,7 @@ func extendImage(img *vips.Image, width, height int, gravity *options.GravityOpt
 	return img.Embed(width, height, offX, offY)
 }
 
-func extend(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func extend(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if !po.Extend.Enabled {
 		return nil
 	}
@@ -34,7 +34,7 @@ func extend(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOption
 	return extendImage(img, width, height, &po.Extend.Gravity, pctx.dprScale)
 }
 
-func extendAspectRatio(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func extendAspectRatio(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if !po.ExtendAspectRatio.Enabled {
 		return nil
 	}

+ 1 - 1
processing/fix_size.go

@@ -91,7 +91,7 @@ func fixIcoSize(img *vips.Image) error {
 	return nil
 }
 
-func fixSize(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func fixSize(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	switch po.Format {
 	case imagetype.WEBP:
 		return fixWebpSize(img)

+ 1 - 1
processing/flatten.go

@@ -6,7 +6,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func flatten(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func flatten(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if !po.Flatten && po.Format.SupportsAlpha() {
 		return nil
 	}

+ 1 - 1
processing/import_color_profile.go

@@ -7,7 +7,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func importColorProfile(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func importColorProfile(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if img.ColourProfileImported() {
 		return nil
 	}

+ 1 - 1
processing/padding.go

@@ -7,7 +7,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func padding(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func padding(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if !po.Padding.Enabled {
 		return nil
 	}

+ 2 - 2
processing/pipeline.go

@@ -60,10 +60,10 @@ type pipelineContext struct {
 	extendAspectRatioHeight int
 }
 
-type pipelineStep func(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error
+type pipelineStep func(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error
 type pipeline []pipelineStep
 
-func (p pipeline) Run(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func (p pipeline) Run(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	pctx := pipelineContext{
 		ctx: ctx,
 

+ 1 - 1
processing/prepare.go

@@ -248,7 +248,7 @@ func (pctx *pipelineContext) limitScale(widthToScale, heightToScale int, po *opt
 	}
 }
 
-func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	pctx.imgtype = imagetype.Unknown
 	if imgdata != nil {
 		pctx.imgtype = imgdata.Format()

+ 4 - 4
processing/processing.go

@@ -97,7 +97,7 @@ func getImageSize(img *vips.Image) (int, int) {
 	return width, height
 }
 
-func transformAnimated(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func transformAnimated(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if po.Trim.Enabled {
 		log.Warning("Trim is not supported for animated images")
 		po.Trim.Enabled = false
@@ -207,7 +207,7 @@ func transformAnimated(ctx context.Context, img *vips.Image, po *options.Process
 	return nil
 }
 
-func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img *vips.Image) (*imagedata.ImageData, error) {
+func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img *vips.Image) (imagedata.ImageData, error) {
 	var diff float64
 	quality := po.GetQuality()
 
@@ -248,7 +248,7 @@ func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img
 	}
 }
 
-func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options.ProcessingOptions) (*imagedata.ImageData, error) {
+func ProcessImage(ctx context.Context, imgdata imagedata.ImageData, po *options.ProcessingOptions) (imagedata.ImageData, error) {
 	runtime.LockOSThread()
 	defer runtime.UnlockOSThread()
 
@@ -348,7 +348,7 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options
 	}
 
 	var (
-		outData *imagedata.ImageData
+		outData imagedata.ImageData
 		err     error
 	)
 

+ 3 - 3
processing/processing_test.go

@@ -31,7 +31,7 @@ func (s *ProcessingTestSuite) SetupSuite() {
 	logrus.SetOutput(io.Discard)
 }
 
-func (s *ProcessingTestSuite) openFile(name string) *imagedata.ImageData {
+func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
 	secopts := security.Options{
 		MaxSrcResolution:            10 * 1024 * 1024,
 		MaxSrcFileSize:              10 * 1024 * 1024,
@@ -43,13 +43,13 @@ func (s *ProcessingTestSuite) openFile(name string) *imagedata.ImageData {
 	s.Require().NoError(err)
 	path := filepath.Join(wd, "..", "testdata", name)
 
-	imagedata, err := imagedata.FromFile(path, "test image", secopts)
+	imagedata, err := imagedata.NewFromPath(path, secopts)
 	s.Require().NoError(err)
 
 	return imagedata
 }
 
-func (s *ProcessingTestSuite) checkSize(imgdata *imagedata.ImageData, width, height int) {
+func (s *ProcessingTestSuite) checkSize(imgdata imagedata.ImageData, width, height int) {
 	img := new(vips.Image)
 	err := img.Load(imgdata, 1, 1, 1)
 	s.Require().NoError(err)

+ 1 - 1
processing/rotate_and_flip.go

@@ -6,7 +6,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func rotateAndFlip(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func rotateAndFlip(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if pctx.angle%360 == 0 && po.Rotate%360 == 0 && !pctx.flip {
 		return nil
 	}

+ 1 - 1
processing/scale.go

@@ -6,7 +6,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func scale(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func scale(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if pctx.wscale == 1 && pctx.hscale == 1 {
 		return nil
 	}

+ 2 - 2
processing/scale_on_load.go

@@ -13,7 +13,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func canScaleOnLoad(pctx *pipelineContext, imgdata *imagedata.ImageData, scale float64) bool {
+func canScaleOnLoad(pctx *pipelineContext, imgdata imagedata.ImageData, scale float64) bool {
 	if imgdata == nil || pctx.trimmed || scale == 1 {
 		return false
 	}
@@ -45,7 +45,7 @@ func calcJpegShink(shrink float64) int {
 	return 1
 }
 
-func scaleOnLoad(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func scaleOnLoad(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	wshrink := float64(pctx.srcWidth) / float64(imath.Scale(pctx.srcWidth, pctx.wscale))
 	hshrink := float64(pctx.srcHeight) / float64(imath.Scale(pctx.srcHeight, pctx.hscale))
 	preshrink := math.Min(wshrink, hshrink)

+ 1 - 1
processing/strip_metadata.go

@@ -105,7 +105,7 @@ func stripXMP(img *vips.Image) []byte {
 	return xmpData
 }
 
-func stripMetadata(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func stripMetadata(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if !po.StripMetadata {
 		return nil
 	}

+ 1 - 1
processing/trim.go

@@ -8,7 +8,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/vips"
 )
 
-func trim(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func trim(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if !po.Trim.Enabled {
 		return nil
 	}

+ 3 - 3
processing/watermark.go

@@ -20,7 +20,7 @@ var watermarkPipeline = pipeline{
 	padding,
 }
 
-func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options.WatermarkOptions, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
+func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, opts *options.WatermarkOptions, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
 	if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
 		return err
 	}
@@ -81,7 +81,7 @@ func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options
 	return wm.StripAll()
 }
 
-func applyWatermark(img *vips.Image, wmData *imagedata.ImageData, opts *options.WatermarkOptions, offsetScale float64, framesCount int) error {
+func applyWatermark(img *vips.Image, wmData imagedata.ImageData, opts *options.WatermarkOptions, offsetScale float64, framesCount int) error {
 	wm := new(vips.Image)
 	defer wm.Clear()
 
@@ -162,7 +162,7 @@ func applyWatermark(img *vips.Image, wmData *imagedata.ImageData, opts *options.
 	return nil
 }
 
-func watermark(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
+func watermark(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
 	if !po.Watermark.Enabled || imagedata.Watermark == nil {
 		return nil
 	}

+ 3 - 3
processing_handler.go

@@ -120,7 +120,7 @@ func setCanonical(rw http.ResponseWriter, originURL string) {
 	}
 }
 
-func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData *imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData *imagedata.ImageData) {
+func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData imagedata.ImageData) {
 	var contentDisposition string
 	if len(po.Filename) > 0 {
 		contentDisposition = resultData.Format().ContentDisposition(po.Filename, po.ReturnAttachment)
@@ -343,7 +343,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 
 	statusCode := http.StatusOK
 
-	originData, err := func() (*imagedata.ImageData, error) {
+	originData, err := func() (imagedata.ImageData, error) {
 		defer metrics.StartDownloadingSegment(ctx, metrics.Meta{
 			metrics.MetaSourceImageURL:    metricsMeta[metrics.MetaSourceImageURL],
 			metrics.MetaSourceImageOrigin: metricsMeta[metrics.MetaSourceImageOrigin],
@@ -462,7 +462,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 		))
 	}
 
-	resultData, err := func() (*imagedata.ImageData, error) {
+	resultData, err := func() (imagedata.ImageData, error) {
 		defer metrics.StartProcessingSegment(ctx, metrics.Meta{
 			metrics.MetaProcessingOptions: metricsMeta[metrics.MetaProcessingOptions],
 		})()

+ 4 - 4
processing_handler_test.go

@@ -87,26 +87,26 @@ func (s *ProcessingHandlerTestSuite) readTestFile(name string) []byte {
 	return data
 }
 
-func (s *ProcessingHandlerTestSuite) readTestImageData(name string) *imagedata.ImageData {
+func (s *ProcessingHandlerTestSuite) readTestImageData(name string) imagedata.ImageData {
 	wd, err := os.Getwd()
 	s.Require().NoError(err)
 
 	data, err := os.ReadFile(filepath.Join(wd, "testdata", name))
 	s.Require().NoError(err)
 
-	imgdata, err := imagedata.NewFromBytes(data, make(http.Header))
+	imgdata, err := imagedata.NewFromBytes(data)
 	s.Require().NoError(err)
 
 	return imgdata
 }
 
-func (s *ProcessingHandlerTestSuite) readImageData(imgdata *imagedata.ImageData) []byte {
+func (s *ProcessingHandlerTestSuite) readImageData(imgdata imagedata.ImageData) []byte {
 	data, err := io.ReadAll(imgdata.Reader())
 	s.Require().NoError(err)
 	return data
 }
 
-func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, *imagedata.ImageData, string) {
+func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, imagedata.ImageData, string) {
 	poStr := "rs:fill:4:4"
 
 	po := options.NewProcessingOptions()

+ 5 - 0
security/image_size.go

@@ -1,6 +1,7 @@
 package security
 
 import (
+	"github.com/imgproxy/imgproxy/v3/imagemeta"
 	"github.com/imgproxy/imgproxy/v3/imath"
 )
 
@@ -19,3 +20,7 @@ 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)
+}

+ 21 - 0
security/response_limit.go

@@ -3,6 +3,7 @@ package security
 import (
 	"io"
 	"net/http"
+	"os"
 )
 
 // hardLimitReadCloser is a wrapper around io.ReadCloser
@@ -49,3 +50,23 @@ func LimitResponseSize(r *http.Response, opts Options) (*http.Response, error) {
 
 	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
+}

+ 2 - 3
svg/svg.go

@@ -11,7 +11,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 )
 
-func Sanitize(data *imagedata.ImageData) (*imagedata.ImageData, error) {
+func Sanitize(data imagedata.ImageData) (imagedata.ImageData, error) {
 	r := data.Reader()
 	l := xml.NewLexer(parse.NewInput(r))
 
@@ -49,8 +49,7 @@ func Sanitize(data *imagedata.ImageData) (*imagedata.ImageData, error) {
 				imagetype.SVG,
 				buf.Bytes(),
 				data.Headers().Clone(),
-			)
-			newData.SetCancel(cancel)
+			).WithCancel(cancel)
 
 			return newData, nil
 		case xml.StartTagToken:

+ 4 - 7
svg/svg_test.go

@@ -1,7 +1,6 @@
 package svg
 
 import (
-	"net/http"
 	"os"
 	"path/filepath"
 	"testing"
@@ -25,18 +24,16 @@ func (s *SvgTestSuite) SetupSuite() {
 	s.Require().NoError(err)
 }
 
-func (s *SvgTestSuite) readTestFile(name string) *imagedata.ImageData {
+func (s *SvgTestSuite) readTestFile(name string) imagedata.ImageData {
 	wd, err := os.Getwd()
 	s.Require().NoError(err)
 
 	data, err := os.ReadFile(filepath.Join(wd, "..", "testdata", name))
 	s.Require().NoError(err)
 
-	h := make(http.Header)
-	h.Set(httpheaders.ContentType, "image/svg+xml")
-	h.Set(httpheaders.CacheControl, "public, max-age=12345")
-
-	d, err := imagedata.NewFromBytes(data, h)
+	d, err := imagedata.NewFromBytes(data)
+	d.Headers().Set(httpheaders.ContentType, "image/svg+xml")
+	d.Headers().Set(httpheaders.CacheControl, "public, max-age=12345")
 	s.Require().NoError(err)
 
 	return d

+ 4 - 7
vips/vips.go

@@ -353,7 +353,7 @@ func (img *Image) Pages() int {
 	return p
 }
 
-func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64, pages int) error {
+func (img *Image) Load(imgdata imagedata.ImageData, shrink int, scale float64, pages int) error {
 	var tmp *C.VipsImage
 
 	err := C.int(0)
@@ -403,7 +403,7 @@ func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64,
 	return nil
 }
 
-func (img *Image) LoadThumbnail(imgdata *imagedata.ImageData) error {
+func (img *Image) LoadThumbnail(imgdata imagedata.ImageData) error {
 	if imgdata.Format() != imagetype.HEIC && imgdata.Format() != imagetype.AVIF {
 		return newVipsError("Usupported image type to load thumbnail")
 	}
@@ -423,7 +423,7 @@ func (img *Image) LoadThumbnail(imgdata *imagedata.ImageData) error {
 	return nil
 }
 
-func (img *Image) Save(imgtype imagetype.Type, quality int) (*imagedata.ImageData, error) {
+func (img *Image) Save(imgtype imagetype.Type, quality int) (imagedata.ImageData, error) {
 	target := C.vips_target_new_to_memory()
 
 	cancel := func() {
@@ -470,10 +470,7 @@ func (img *Image) Save(imgtype imagetype.Type, quality int) (*imagedata.ImageDat
 
 	b := ptrToBytes(ptr, int(imgsize))
 
-	imgdata := imagedata.NewFromBytesWithFormat(imgtype, b, make(http.Header))
-	imgdata.SetCancel(cancel)
-
-	return imgdata, nil
+	return imagedata.NewFromBytesWithFormat(imgtype, b, make(http.Header)).WithCancel(cancel), nil
 }
 
 func (img *Image) Clear() {