mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-10-11 04:32:29 +02:00
New imagedata interface (#1471)
This commit is contained in:
@@ -269,7 +269,7 @@ func (ab *AsyncBuffer) Error() error {
|
|||||||
// Chunk must be available when this method is called.
|
// Chunk must be available when this method is called.
|
||||||
// Returns the number of bytes copied to the slice or 0 if chunk has no data
|
// Returns the number of bytes copied to the slice or 0 if chunk has no data
|
||||||
// (eg. offset is beyond the end of the stream).
|
// (eg. offset is beyond the end of the stream).
|
||||||
func (ab *AsyncBuffer) readChunkAt(p []byte, off, rem int64) int {
|
func (ab *AsyncBuffer) readChunkAt(p []byte, off int64) int {
|
||||||
// If the chunk is not available, we return 0
|
// If the chunk is not available, we return 0
|
||||||
if off >= ab.len.Load() {
|
if off >= ab.len.Load() {
|
||||||
return 0
|
return 0
|
||||||
@@ -286,17 +286,9 @@ func (ab *AsyncBuffer) readChunkAt(p []byte, off, rem int64) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// How many bytes we could read from the chunk. No more than:
|
// Copy data to the target slice. The number of bytes to copy is limited by the
|
||||||
// - left to read totally
|
// size of the target slice and the size of the data in the chunk.
|
||||||
// - chunk size minus the start offset
|
return copy(p, chunk.data[startOffset:])
|
||||||
// - chunk has
|
|
||||||
size := min(rem, ChunkSize-startOffset, int64(len(chunk.data)))
|
|
||||||
|
|
||||||
if size == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return copy(p, chunk.data[startOffset:startOffset+size])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// readAt reads data from the AsyncBuffer at the given offset.
|
// readAt reads data from the AsyncBuffer at the given offset.
|
||||||
@@ -333,7 +325,7 @@ func (ab *AsyncBuffer) readAt(p []byte, off int64) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read data from the first chunk
|
// Read data from the first chunk
|
||||||
n := ab.readChunkAt(p, off, size)
|
n := ab.readChunkAt(p, off)
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return 0, io.EOF // Failed to read any data: means we tried to read beyond the end of the stream
|
return 0, io.EOF // Failed to read any data: means we tried to read beyond the end of the stream
|
||||||
}
|
}
|
||||||
@@ -350,7 +342,7 @@ func (ab *AsyncBuffer) readAt(p []byte, off int64) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read data from the next chunk
|
// Read data from the next chunk
|
||||||
nX := ab.readChunkAt(p[n:], off, size)
|
nX := ab.readChunkAt(p[n:], off)
|
||||||
n += nX
|
n += nX
|
||||||
size -= int64(nX)
|
size -= int64(nX)
|
||||||
off += int64(nX)
|
off += int64(nX)
|
||||||
@@ -402,13 +394,11 @@ func (ab *AsyncBuffer) Reader() *Reader {
|
|||||||
// Read reads data from the AsyncBuffer.
|
// Read reads data from the AsyncBuffer.
|
||||||
func (r *Reader) Read(p []byte) (int, error) {
|
func (r *Reader) Read(p []byte) (int, error) {
|
||||||
n, err := r.ab.readAt(p, r.pos)
|
n, err := r.ab.readAt(p, r.pos)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return n, err
|
r.pos += int64(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.pos += int64(n)
|
return n, err
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek sets the position of the reader to the given offset and returns the new position
|
// Seek sets the position of the reader to the given offset and returns the new position
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -346,3 +347,15 @@ func TestAsyncBufferReadAsync(t *testing.T) {
|
|||||||
require.ErrorIs(t, io.EOF, err)
|
require.ErrorIs(t, io.EOF, err)
|
||||||
assert.Equal(t, 0, n)
|
assert.Equal(t, 0, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAsyncBufferReadAllCompability tests that ReadAll methods works as expected
|
||||||
|
func TestAsyncBufferReadAllCompability(t *testing.T) {
|
||||||
|
source, err := os.ReadFile("../testdata/test1.jpg")
|
||||||
|
require.NoError(t, err)
|
||||||
|
asyncBuffer := FromReader(bytes.NewReader(source))
|
||||||
|
defer asyncBuffer.Close()
|
||||||
|
|
||||||
|
b, err := io.ReadAll(asyncBuffer.Reader())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, b, len(source))
|
||||||
|
}
|
||||||
|
15
etag/etag.go
15
etag/etag.go
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
|
"io"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -105,7 +106,7 @@ func (h *Handler) ImageEtagExpected() string {
|
|||||||
return h.imgEtagExpected
|
return h.imgEtagExpected
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) SetActualImageData(imgdata *imagedata.ImageData) bool {
|
func (h *Handler) SetActualImageData(imgdata *imagedata.ImageData) (bool, error) {
|
||||||
var haveActualImgETag bool
|
var haveActualImgETag bool
|
||||||
h.imgEtagActual, haveActualImgETag = imgdata.Headers["ETag"]
|
h.imgEtagActual, haveActualImgETag = imgdata.Headers["ETag"]
|
||||||
haveActualImgETag = haveActualImgETag && len(h.imgEtagActual) > 0
|
haveActualImgETag = haveActualImgETag && len(h.imgEtagActual) > 0
|
||||||
@@ -113,7 +114,7 @@ func (h *Handler) SetActualImageData(imgdata *imagedata.ImageData) bool {
|
|||||||
// Just in case server didn't check ETag properly and returned the same one
|
// Just in case server didn't check ETag properly and returned the same one
|
||||||
// as we expected
|
// as we expected
|
||||||
if haveActualImgETag && h.imgEtagExpected == h.imgEtagActual {
|
if haveActualImgETag && h.imgEtagExpected == h.imgEtagActual {
|
||||||
return true
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
haveExpectedImgHash := len(h.imgHashExpected) != 0
|
haveExpectedImgHash := len(h.imgHashExpected) != 0
|
||||||
@@ -123,14 +124,18 @@ func (h *Handler) SetActualImageData(imgdata *imagedata.ImageData) bool {
|
|||||||
defer eTagCalcPool.Put(c)
|
defer eTagCalcPool.Put(c)
|
||||||
|
|
||||||
c.hash.Reset()
|
c.hash.Reset()
|
||||||
c.hash.Write(imgdata.Data)
|
|
||||||
|
_, err := io.Copy(c.hash, imgdata.Reader())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
h.imgHashActual = base64.RawURLEncoding.EncodeToString(c.hash.Sum(nil))
|
h.imgHashActual = base64.RawURLEncoding.EncodeToString(c.hash.Sum(nil))
|
||||||
|
|
||||||
return haveExpectedImgHash && h.imgHashActual == h.imgHashExpected
|
return haveExpectedImgHash && h.imgHashActual == h.imgHashExpected, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) GenerateActualETag() string {
|
func (h *Handler) GenerateActualETag() string {
|
||||||
|
@@ -2,6 +2,7 @@ package etag
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -14,29 +15,36 @@ import (
|
|||||||
"github.com/imgproxy/imgproxy/v3/options"
|
"github.com/imgproxy/imgproxy/v3/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
po = options.NewProcessingOptions()
|
|
||||||
|
|
||||||
imgWithETag = imagedata.ImageData{
|
|
||||||
Data: []byte("Hello Test"),
|
|
||||||
Headers: map[string]string{"ETag": `"loremipsumdolor"`},
|
|
||||||
}
|
|
||||||
imgWithoutETag = imagedata.ImageData{
|
|
||||||
Data: []byte("Hello Test"),
|
|
||||||
}
|
|
||||||
|
|
||||||
etagReq = `"yj0WO6sFU4GCciYUBWjzvvfqrBh869doeOC2Pp5EI1Y/RImxvcmVtaXBzdW1kb2xvciI"`
|
etagReq = `"yj0WO6sFU4GCciYUBWjzvvfqrBh869doeOC2Pp5EI1Y/RImxvcmVtaXBzdW1kb2xvciI"`
|
||||||
etagData = `"yj0WO6sFU4GCciYUBWjzvvfqrBh869doeOC2Pp5EI1Y/DvyChhMNu_sFX7jrjoyrgQbnFwfoOVv7kzp_Fbs6hQBg"`
|
etagData = `"yj0WO6sFU4GCciYUBWjzvvfqrBh869doeOC2Pp5EI1Y/D3t8wWhX4piqDCV4ZMEZsKvOaIO6onhKjbf9f-ZfYUV0"`
|
||||||
)
|
)
|
||||||
|
|
||||||
type EtagTestSuite struct {
|
type EtagTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
|
|
||||||
|
po *options.ProcessingOptions
|
||||||
|
imgWithETag *imagedata.ImageData
|
||||||
|
imgWithoutETag *imagedata.ImageData
|
||||||
|
|
||||||
h Handler
|
h Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtagTestSuite) SetupSuite() {
|
func (s *EtagTestSuite) SetupSuite() {
|
||||||
logrus.SetOutput(io.Discard)
|
logrus.SetOutput(io.Discard)
|
||||||
|
s.po = options.NewProcessingOptions()
|
||||||
|
|
||||||
|
d, err := os.ReadFile("../testdata/test1.jpg")
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
imgWithETag, err := imagedata.NewFromBytes(d, http.Header{"ETag": []string{`"loremipsumdolor"`}})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
imgWithoutETag, err := imagedata.NewFromBytes(d, make(http.Header))
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
s.imgWithETag = imgWithETag
|
||||||
|
s.imgWithoutETag = imgWithoutETag
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtagTestSuite) TeardownSuite() {
|
func (s *EtagTestSuite) TeardownSuite() {
|
||||||
@@ -49,15 +57,15 @@ func (s *EtagTestSuite) SetupTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestGenerateActualReq() {
|
func (s *EtagTestSuite) TestGenerateActualReq() {
|
||||||
s.h.SetActualProcessingOptions(po)
|
s.h.SetActualProcessingOptions(s.po)
|
||||||
s.h.SetActualImageData(&imgWithETag)
|
s.h.SetActualImageData(s.imgWithETag)
|
||||||
|
|
||||||
s.Require().Equal(etagReq, s.h.GenerateActualETag())
|
s.Require().Equal(etagReq, s.h.GenerateActualETag())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestGenerateActualData() {
|
func (s *EtagTestSuite) TestGenerateActualData() {
|
||||||
s.h.SetActualProcessingOptions(po)
|
s.h.SetActualProcessingOptions(s.po)
|
||||||
s.h.SetActualImageData(&imgWithoutETag)
|
s.h.SetActualImageData(s.imgWithoutETag)
|
||||||
|
|
||||||
s.Require().Equal(etagData, s.h.GenerateActualETag())
|
s.Require().Equal(etagData, s.h.GenerateActualETag())
|
||||||
}
|
}
|
||||||
@@ -75,7 +83,7 @@ func (s *EtagTestSuite) TestGenerateExpectedData() {
|
|||||||
func (s *EtagTestSuite) TestProcessingOptionsCheckSuccess() {
|
func (s *EtagTestSuite) TestProcessingOptionsCheckSuccess() {
|
||||||
s.h.ParseExpectedETag(etagReq)
|
s.h.ParseExpectedETag(etagReq)
|
||||||
|
|
||||||
s.Require().True(s.h.SetActualProcessingOptions(po))
|
s.Require().True(s.h.SetActualProcessingOptions(s.po))
|
||||||
s.Require().True(s.h.ProcessingOptionsMatch())
|
s.Require().True(s.h.ProcessingOptionsMatch())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +93,7 @@ func (s *EtagTestSuite) TestProcessingOptionsCheckFailure() {
|
|||||||
|
|
||||||
s.h.ParseExpectedETag(wrongEtag)
|
s.h.ParseExpectedETag(wrongEtag)
|
||||||
|
|
||||||
s.Require().False(s.h.SetActualProcessingOptions(po))
|
s.Require().False(s.h.SetActualProcessingOptions(s.po))
|
||||||
s.Require().False(s.h.ProcessingOptionsMatch())
|
s.Require().False(s.h.ProcessingOptionsMatch())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +101,7 @@ func (s *EtagTestSuite) TestImageETagExpectedPresent() {
|
|||||||
s.h.ParseExpectedETag(etagReq)
|
s.h.ParseExpectedETag(etagReq)
|
||||||
|
|
||||||
//nolint:testifylint // False-positive expected-actual
|
//nolint:testifylint // False-positive expected-actual
|
||||||
s.Require().Equal(imgWithETag.Headers["ETag"], s.h.ImageEtagExpected())
|
s.Require().Equal(s.imgWithETag.Headers["ETag"], s.h.ImageEtagExpected())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageETagExpectedBlank() {
|
func (s *EtagTestSuite) TestImageETagExpectedBlank() {
|
||||||
@@ -104,7 +112,7 @@ func (s *EtagTestSuite) TestImageETagExpectedBlank() {
|
|||||||
|
|
||||||
func (s *EtagTestSuite) TestImageDataCheckDataToDataSuccess() {
|
func (s *EtagTestSuite) TestImageDataCheckDataToDataSuccess() {
|
||||||
s.h.ParseExpectedETag(etagData)
|
s.h.ParseExpectedETag(etagData)
|
||||||
s.Require().True(s.h.SetActualImageData(&imgWithoutETag))
|
s.Require().True(s.h.SetActualImageData(s.imgWithoutETag))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageDataCheckDataToDataFailure() {
|
func (s *EtagTestSuite) TestImageDataCheckDataToDataFailure() {
|
||||||
@@ -112,12 +120,12 @@ func (s *EtagTestSuite) TestImageDataCheckDataToDataFailure() {
|
|||||||
wrongEtag := etagData[:i] + `/Dwrongimghash"`
|
wrongEtag := etagData[:i] + `/Dwrongimghash"`
|
||||||
|
|
||||||
s.h.ParseExpectedETag(wrongEtag)
|
s.h.ParseExpectedETag(wrongEtag)
|
||||||
s.Require().False(s.h.SetActualImageData(&imgWithoutETag))
|
s.Require().False(s.h.SetActualImageData(s.imgWithoutETag))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageDataCheckDataToReqSuccess() {
|
func (s *EtagTestSuite) TestImageDataCheckDataToReqSuccess() {
|
||||||
s.h.ParseExpectedETag(etagData)
|
s.h.ParseExpectedETag(etagData)
|
||||||
s.Require().True(s.h.SetActualImageData(&imgWithETag))
|
s.Require().True(s.h.SetActualImageData(s.imgWithETag))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageDataCheckDataToReqFailure() {
|
func (s *EtagTestSuite) TestImageDataCheckDataToReqFailure() {
|
||||||
@@ -125,19 +133,19 @@ func (s *EtagTestSuite) TestImageDataCheckDataToReqFailure() {
|
|||||||
wrongEtag := etagData[:i] + `/Dwrongimghash"`
|
wrongEtag := etagData[:i] + `/Dwrongimghash"`
|
||||||
|
|
||||||
s.h.ParseExpectedETag(wrongEtag)
|
s.h.ParseExpectedETag(wrongEtag)
|
||||||
s.Require().False(s.h.SetActualImageData(&imgWithETag))
|
s.Require().False(s.h.SetActualImageData(s.imgWithETag))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageDataCheckReqToDataFailure() {
|
func (s *EtagTestSuite) TestImageDataCheckReqToDataFailure() {
|
||||||
s.h.ParseExpectedETag(etagReq)
|
s.h.ParseExpectedETag(etagReq)
|
||||||
s.Require().False(s.h.SetActualImageData(&imgWithoutETag))
|
s.Require().False(s.h.SetActualImageData(s.imgWithoutETag))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestETagBusterFailure() {
|
func (s *EtagTestSuite) TestETagBusterFailure() {
|
||||||
config.ETagBuster = "busted"
|
config.ETagBuster = "busted"
|
||||||
|
|
||||||
s.h.ParseExpectedETag(etagReq)
|
s.h.ParseExpectedETag(etagReq)
|
||||||
s.Require().False(s.h.SetActualImageData(&imgWithoutETag))
|
s.Require().False(s.h.SetActualImageData(s.imgWithoutETag))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEtag(t *testing.T) {
|
func TestEtag(t *testing.T) {
|
||||||
|
31
imagedata/factory.go
Normal file
31
imagedata/factory.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package imagedata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v3/imagemeta"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewFromBytes creates a new ImageData instance from the provided byte slice.
|
||||||
|
func NewFromBytes(b []byte, headers http.Header) (*ImageData, error) {
|
||||||
|
r := bytes.NewReader(b)
|
||||||
|
|
||||||
|
meta, err := imagemeta.DecodeMeta(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary workaround for the old ImageData interface
|
||||||
|
h := make(map[string]string, len(headers))
|
||||||
|
for k, v := range headers {
|
||||||
|
h[k] = strings.Join(v, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ImageData{
|
||||||
|
data: b,
|
||||||
|
meta: meta,
|
||||||
|
Headers: h,
|
||||||
|
}, nil
|
||||||
|
}
|
@@ -1,15 +1,18 @@
|
|||||||
package imagedata
|
package imagedata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/imagemeta"
|
||||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||||
"github.com/imgproxy/imgproxy/v3/security"
|
"github.com/imgproxy/imgproxy/v3/security"
|
||||||
)
|
)
|
||||||
@@ -20,20 +23,43 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ImageData struct {
|
type ImageData struct {
|
||||||
Type imagetype.Type
|
meta imagemeta.Meta
|
||||||
Data []byte
|
data []byte
|
||||||
Headers map[string]string
|
Headers map[string]string
|
||||||
|
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
cancelOnce sync.Once
|
cancelOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ImageData) Close() {
|
func (d *ImageData) Close() error {
|
||||||
d.cancelOnce.Do(func() {
|
d.cancelOnce.Do(func() {
|
||||||
if d.cancel != nil {
|
if d.cancel != nil {
|
||||||
d.cancel()
|
d.cancel()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meta returns the image metadata
|
||||||
|
func (d *ImageData) Meta() imagemeta.Meta {
|
||||||
|
return d.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format returns the image format based on the metadata
|
||||||
|
func (d *ImageData) Format() imagetype.Type {
|
||||||
|
return d.meta.Format()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader returns an io.ReadSeeker for the image data
|
||||||
|
func (d *ImageData) 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) {
|
||||||
|
return len(d.data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ImageData) SetCancel(cancel context.CancelFunc) {
|
func (d *ImageData) SetCancel(cancel context.CancelFunc) {
|
||||||
|
@@ -94,8 +94,8 @@ func (s *ImageDataTestSuite) TestDownloadStatusOK() {
|
|||||||
|
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
s.Require().NotNil(imgdata)
|
s.Require().NotNil(imgdata)
|
||||||
s.Require().Equal(s.defaultData, imgdata.Data)
|
s.Require().Equal(s.defaultData, imgdata.data)
|
||||||
s.Require().Equal(imagetype.JPEG, imgdata.Type)
|
s.Require().Equal(imagetype.JPEG, imgdata.Format())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ImageDataTestSuite) TestDownloadStatusPartialContent() {
|
func (s *ImageDataTestSuite) TestDownloadStatusPartialContent() {
|
||||||
@@ -165,8 +165,8 @@ func (s *ImageDataTestSuite) TestDownloadStatusPartialContent() {
|
|||||||
} else {
|
} else {
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
s.Require().NotNil(imgdata)
|
s.Require().NotNil(imgdata)
|
||||||
s.Require().Equal(s.defaultData, imgdata.Data)
|
s.Require().Equal(s.defaultData, imgdata.data)
|
||||||
s.Require().Equal(imagetype.JPEG, imgdata.Type)
|
s.Require().Equal(imagetype.JPEG, imgdata.Format())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -278,8 +278,8 @@ func (s *ImageDataTestSuite) TestDownloadGzip() {
|
|||||||
|
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
s.Require().NotNil(imgdata)
|
s.Require().NotNil(imgdata)
|
||||||
s.Require().Equal(s.defaultData, imgdata.Data)
|
s.Require().Equal(s.defaultData, imgdata.data)
|
||||||
s.Require().Equal(imagetype.JPEG, imgdata.Type)
|
s.Require().Equal(imagetype.JPEG, imgdata.Format())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ImageDataTestSuite) TestFromFile() {
|
func (s *ImageDataTestSuite) TestFromFile() {
|
||||||
@@ -287,8 +287,8 @@ func (s *ImageDataTestSuite) TestFromFile() {
|
|||||||
|
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
s.Require().NotNil(imgdata)
|
s.Require().NotNil(imgdata)
|
||||||
s.Require().Equal(s.defaultData, imgdata.Data)
|
s.Require().Equal(s.defaultData, imgdata.data)
|
||||||
s.Require().Equal(imagetype.JPEG, imgdata.Type)
|
s.Require().Equal(imagetype.JPEG, imgdata.Format())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ImageDataTestSuite) TestFromBase64() {
|
func (s *ImageDataTestSuite) TestFromBase64() {
|
||||||
@@ -298,8 +298,8 @@ func (s *ImageDataTestSuite) TestFromBase64() {
|
|||||||
|
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
s.Require().NotNil(imgdata)
|
s.Require().NotNil(imgdata)
|
||||||
s.Require().Equal(s.defaultData, imgdata.Data)
|
s.Require().Equal(s.defaultData, imgdata.data)
|
||||||
s.Require().Equal(imagetype.JPEG, imgdata.Type)
|
s.Require().Equal(imagetype.JPEG, imgdata.Format())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImageData(t *testing.T) {
|
func TestImageData(t *testing.T) {
|
||||||
|
@@ -50,8 +50,8 @@ func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &ImageData{
|
return &ImageData{
|
||||||
Data: buf.Bytes(),
|
data: buf.Bytes(),
|
||||||
Type: meta.Format(),
|
meta: meta,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
21
imagedatanew/image_data.go
Normal file
21
imagedatanew/image_data.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package imagedatanew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v3/imagemeta"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImageData is an interface that defines methods for reading image data and metadata
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
@@ -251,7 +251,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
|
pctx.imgtype = imagetype.Unknown
|
||||||
if imgdata != nil {
|
if imgdata != nil {
|
||||||
pctx.imgtype = imgdata.Type
|
pctx.imgtype = imgdata.Format()
|
||||||
}
|
}
|
||||||
|
|
||||||
pctx.srcWidth, pctx.srcHeight, pctx.angle, pctx.flip = extractMeta(img, po.Rotate, po.AutoRotate)
|
pctx.srcWidth, pctx.srcHeight, pctx.angle, pctx.flip = extractMeta(img, po.Rotate, po.AutoRotate)
|
||||||
@@ -266,7 +266,7 @@ func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptio
|
|||||||
|
|
||||||
// The size of a vector image is not checked during download, yet it can be very large.
|
// The size of a vector image is not checked during download, yet it can be very large.
|
||||||
// So we should scale it down to the maximum allowed resolution
|
// So we should scale it down to the maximum allowed resolution
|
||||||
if !pctx.trimmed && imgdata != nil && imgdata.Type.IsVector() && !po.Enlarge {
|
if !pctx.trimmed && imgdata != nil && imgdata.Format().IsVector() && !po.Enlarge {
|
||||||
resolution := imath.Round((float64(img.Width()*img.Height()) * pctx.wscale * pctx.hscale))
|
resolution := imath.Round((float64(img.Width()*img.Height()) * pctx.wscale * pctx.hscale))
|
||||||
if resolution > po.SecurityOptions.MaxSrcResolution {
|
if resolution > po.SecurityOptions.MaxSrcResolution {
|
||||||
scale := math.Sqrt(float64(po.SecurityOptions.MaxSrcResolution) / float64(resolution))
|
scale := math.Sqrt(float64(po.SecurityOptions.MaxSrcResolution) / float64(resolution))
|
||||||
|
@@ -217,7 +217,16 @@ func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
imgdata, err := img.Save(po.Format, quality)
|
imgdata, err := img.Save(po.Format, quality)
|
||||||
if err != nil || len(imgdata.Data) <= po.MaxBytes || quality <= 10 {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := imgdata.Size()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if size <= po.MaxBytes || quality <= 10 {
|
||||||
return imgdata, err
|
return imgdata, err
|
||||||
}
|
}
|
||||||
imgdata.Close()
|
imgdata.Close()
|
||||||
@@ -226,7 +235,7 @@ func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
delta := float64(len(imgdata.Data)) / float64(po.MaxBytes)
|
delta := float64(size) / float64(po.MaxBytes)
|
||||||
switch {
|
switch {
|
||||||
case delta > 3:
|
case delta > 3:
|
||||||
diff = 0.25
|
diff = 0.25
|
||||||
@@ -247,7 +256,7 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options
|
|||||||
|
|
||||||
animationSupport :=
|
animationSupport :=
|
||||||
po.SecurityOptions.MaxAnimationFrames > 1 &&
|
po.SecurityOptions.MaxAnimationFrames > 1 &&
|
||||||
imgdata.Type.SupportsAnimationLoad() &&
|
imgdata.Format().SupportsAnimationLoad() &&
|
||||||
(po.Format == imagetype.Unknown || po.Format.SupportsAnimationSave())
|
(po.Format == imagetype.Unknown || po.Format.SupportsAnimationSave())
|
||||||
|
|
||||||
pages := 1
|
pages := 1
|
||||||
@@ -258,7 +267,7 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options
|
|||||||
img := new(vips.Image)
|
img := new(vips.Image)
|
||||||
defer img.Clear()
|
defer img.Clear()
|
||||||
|
|
||||||
if po.EnforceThumbnail && imgdata.Type.SupportsThumbnail() {
|
if po.EnforceThumbnail && imgdata.Format().SupportsThumbnail() {
|
||||||
if err := img.LoadThumbnail(imgdata); err != nil {
|
if err := img.LoadThumbnail(imgdata); err != nil {
|
||||||
log.Debugf("Can't load thumbnail: %s", err)
|
log.Debugf("Can't load thumbnail: %s", err)
|
||||||
// Failed to load thumbnail, rollback to the full image
|
// Failed to load thumbnail, rollback to the full image
|
||||||
@@ -286,10 +295,10 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options
|
|||||||
po.Format = imagetype.AVIF
|
po.Format = imagetype.AVIF
|
||||||
case po.PreferWebP:
|
case po.PreferWebP:
|
||||||
po.Format = imagetype.WEBP
|
po.Format = imagetype.WEBP
|
||||||
case isImageTypePreferred(imgdata.Type):
|
case isImageTypePreferred(imgdata.Format()):
|
||||||
po.Format = imgdata.Type
|
po.Format = imgdata.Format()
|
||||||
default:
|
default:
|
||||||
po.Format = findBestFormat(imgdata.Type, animated, expectAlpha)
|
po.Format = findBestFormat(imgdata.Format(), animated, expectAlpha)
|
||||||
}
|
}
|
||||||
case po.EnforceJxl && !animated:
|
case po.EnforceJxl && !animated:
|
||||||
po.Format = imagetype.JXL
|
po.Format = imagetype.JXL
|
||||||
|
@@ -18,7 +18,7 @@ func canScaleOnLoad(pctx *pipelineContext, imgdata *imagedata.ImageData, scale f
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if imgdata.Type.IsVector() {
|
if imgdata.Format().IsVector() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,10 +26,10 @@ func canScaleOnLoad(pctx *pipelineContext, imgdata *imagedata.ImageData, scale f
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return imgdata.Type == imagetype.JPEG ||
|
return imgdata.Format() == imagetype.JPEG ||
|
||||||
imgdata.Type == imagetype.WEBP ||
|
imgdata.Format() == imagetype.WEBP ||
|
||||||
imgdata.Type == imagetype.HEIC ||
|
imgdata.Format() == imagetype.HEIC ||
|
||||||
imgdata.Type == imagetype.AVIF
|
imgdata.Format() == imagetype.AVIF
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcJpegShink(shrink float64) int {
|
func calcJpegShink(shrink float64) int {
|
||||||
@@ -57,7 +57,7 @@ func scaleOnLoad(pctx *pipelineContext, img *vips.Image, po *options.ProcessingO
|
|||||||
|
|
||||||
var newWidth, newHeight int
|
var newWidth, newHeight int
|
||||||
|
|
||||||
if imgdata.Type.SupportsThumbnail() {
|
if imgdata.Format().SupportsThumbnail() {
|
||||||
thumbnail := new(vips.Image)
|
thumbnail := new(vips.Image)
|
||||||
defer thumbnail.Clear()
|
defer thumbnail.Clear()
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@ func trim(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions,
|
|||||||
|
|
||||||
// The size of a vector image is not checked during download, yet it can be very large.
|
// The size of a vector image is not checked during download, yet it can be very large.
|
||||||
// So we should scale it down to the maximum allowed resolution
|
// So we should scale it down to the maximum allowed resolution
|
||||||
if imgdata != nil && imgdata.Type.IsVector() {
|
if imgdata != nil && imgdata.Format().IsVector() {
|
||||||
if resolution := img.Width() * img.Height(); resolution > po.SecurityOptions.MaxSrcResolution {
|
if resolution := img.Width() * img.Height(); resolution > po.SecurityOptions.MaxSrcResolution {
|
||||||
scale := math.Sqrt(float64(po.SecurityOptions.MaxSrcResolution) / float64(resolution))
|
scale := math.Sqrt(float64(po.SecurityOptions.MaxSrcResolution) / float64(resolution))
|
||||||
if err := img.Load(imgdata, 1, scale, 1); err != nil {
|
if err := img.Load(imgdata, 1, scale, 1); err != nil {
|
||||||
|
@@ -29,7 +29,7 @@ func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options
|
|||||||
po.ResizingType = options.ResizeFit
|
po.ResizingType = options.ResizeFit
|
||||||
po.Dpr = 1
|
po.Dpr = 1
|
||||||
po.Enlarge = true
|
po.Enlarge = true
|
||||||
po.Format = wmData.Type
|
po.Format = wmData.Format()
|
||||||
|
|
||||||
if opts.Scale > 0 {
|
if opts.Scale > 0 {
|
||||||
po.Width = imath.Max(imath.ScaleToEven(imgWidth, opts.Scale), 1)
|
po.Width = imath.Max(imath.ScaleToEven(imgWidth, opts.Scale), 1)
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -121,12 +122,12 @@ 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
|
var contentDisposition string
|
||||||
if len(po.Filename) > 0 {
|
if len(po.Filename) > 0 {
|
||||||
contentDisposition = resultData.Type.ContentDisposition(po.Filename, po.ReturnAttachment)
|
contentDisposition = resultData.Format().ContentDisposition(po.Filename, po.ReturnAttachment)
|
||||||
} else {
|
} else {
|
||||||
contentDisposition = resultData.Type.ContentDispositionFromURL(originURL, po.ReturnAttachment)
|
contentDisposition = resultData.Format().ContentDispositionFromURL(originURL, po.ReturnAttachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
rw.Header().Set("Content-Type", resultData.Type.Mime())
|
rw.Header().Set("Content-Type", resultData.Format().Mime())
|
||||||
rw.Header().Set("Content-Disposition", contentDisposition)
|
rw.Header().Set("Content-Disposition", contentDisposition)
|
||||||
|
|
||||||
setCacheControl(rw, po.Expires, originData.Headers)
|
setCacheControl(rw, po.Expires, originData.Headers)
|
||||||
@@ -135,7 +136,12 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
|
|||||||
setCanonical(rw, originURL)
|
setCanonical(rw, originURL)
|
||||||
|
|
||||||
if config.EnableDebugHeaders {
|
if config.EnableDebugHeaders {
|
||||||
rw.Header().Set("X-Origin-Content-Length", strconv.Itoa(len(originData.Data)))
|
originSize, err := originData.Size()
|
||||||
|
if err != nil {
|
||||||
|
checkErr(r.Context(), "image_data_size", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.Header().Set("X-Origin-Content-Length", strconv.Itoa(originSize))
|
||||||
rw.Header().Set("X-Origin-Width", resultData.Headers["X-Origin-Width"])
|
rw.Header().Set("X-Origin-Width", resultData.Headers["X-Origin-Width"])
|
||||||
rw.Header().Set("X-Origin-Height", resultData.Headers["X-Origin-Height"])
|
rw.Header().Set("X-Origin-Height", resultData.Headers["X-Origin-Height"])
|
||||||
rw.Header().Set("X-Result-Width", resultData.Headers["X-Result-Width"])
|
rw.Header().Set("X-Result-Width", resultData.Headers["X-Result-Width"])
|
||||||
@@ -144,9 +150,15 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
|
|||||||
|
|
||||||
rw.Header().Set("Content-Security-Policy", "script-src 'none'")
|
rw.Header().Set("Content-Security-Policy", "script-src 'none'")
|
||||||
|
|
||||||
rw.Header().Set("Content-Length", strconv.Itoa(len(resultData.Data)))
|
resultSize, err := resultData.Size()
|
||||||
|
if err != nil {
|
||||||
|
checkErr(r.Context(), "image_data_size", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.Header().Set("Content-Length", strconv.Itoa(resultSize))
|
||||||
rw.WriteHeader(statusCode)
|
rw.WriteHeader(statusCode)
|
||||||
_, err := rw.Write(resultData.Data)
|
|
||||||
|
_, err = io.Copy(rw, resultData.Reader())
|
||||||
|
|
||||||
var ierr *ierrors.Error
|
var ierr *ierrors.Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -404,8 +416,8 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
|||||||
checkErr(ctx, "timeout", router.CheckTimeout(ctx))
|
checkErr(ctx, "timeout", router.CheckTimeout(ctx))
|
||||||
|
|
||||||
if config.ETagEnabled && statusCode == http.StatusOK {
|
if config.ETagEnabled && statusCode == http.StatusOK {
|
||||||
imgDataMatch := etagHandler.SetActualImageData(originData)
|
imgDataMatch, terr := etagHandler.SetActualImageData(originData)
|
||||||
|
if terr == nil {
|
||||||
rw.Header().Set("ETag", etagHandler.GenerateActualETag())
|
rw.Header().Set("ETag", etagHandler.GenerateActualETag())
|
||||||
|
|
||||||
if imgDataMatch && etagHandler.ProcessingOptionsMatch() {
|
if imgDataMatch && etagHandler.ProcessingOptionsMatch() {
|
||||||
@@ -413,18 +425,19 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
checkErr(ctx, "timeout", router.CheckTimeout(ctx))
|
checkErr(ctx, "timeout", router.CheckTimeout(ctx))
|
||||||
|
|
||||||
// Skip processing svg with unknown or the same destination imageType
|
// Skip processing svg with unknown or the same destination imageType
|
||||||
// if it's not forced by AlwaysRasterizeSvg option
|
// if it's not forced by AlwaysRasterizeSvg option
|
||||||
// Also skip processing if the format is in SkipProcessingFormats
|
// Also skip processing if the format is in SkipProcessingFormats
|
||||||
shouldSkipProcessing := (originData.Type == po.Format || po.Format == imagetype.Unknown) &&
|
shouldSkipProcessing := (originData.Format() == po.Format || po.Format == imagetype.Unknown) &&
|
||||||
(slices.Contains(po.SkipProcessingFormats, originData.Type) ||
|
(slices.Contains(po.SkipProcessingFormats, originData.Format()) ||
|
||||||
originData.Type == imagetype.SVG && !config.AlwaysRasterizeSvg)
|
originData.Format() == imagetype.SVG && !config.AlwaysRasterizeSvg)
|
||||||
|
|
||||||
if shouldSkipProcessing {
|
if shouldSkipProcessing {
|
||||||
if originData.Type == imagetype.SVG && config.SanitizeSvg {
|
if originData.Format() == imagetype.SVG && config.SanitizeSvg {
|
||||||
sanitized, svgErr := svg.Sanitize(originData)
|
sanitized, svgErr := svg.Sanitize(originData)
|
||||||
checkErr(ctx, "svg_processing", svgErr)
|
checkErr(ctx, "svg_processing", svgErr)
|
||||||
|
|
||||||
@@ -438,10 +451,10 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !vips.SupportsLoad(originData.Type) {
|
if !vips.SupportsLoad(originData.Format()) {
|
||||||
sendErrAndPanic(ctx, "processing", newInvalidURLErrorf(
|
sendErrAndPanic(ctx, "processing", newInvalidURLErrorf(
|
||||||
http.StatusUnprocessableEntity,
|
http.StatusUnprocessableEntity,
|
||||||
"Source image format is not supported: %s", originData.Type,
|
"Source image format is not supported: %s", originData.Format(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -25,6 +24,7 @@ import (
|
|||||||
"github.com/imgproxy/imgproxy/v3/options"
|
"github.com/imgproxy/imgproxy/v3/options"
|
||||||
"github.com/imgproxy/imgproxy/v3/router"
|
"github.com/imgproxy/imgproxy/v3/router"
|
||||||
"github.com/imgproxy/imgproxy/v3/svg"
|
"github.com/imgproxy/imgproxy/v3/svg"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||||
"github.com/imgproxy/imgproxy/v3/vips"
|
"github.com/imgproxy/imgproxy/v3/vips"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,8 +86,21 @@ func (s *ProcessingHandlerTestSuite) readTestFile(name string) []byte {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) readBody(res *http.Response) []byte {
|
func (s *ProcessingHandlerTestSuite) readTestImageData(name string) *imagedata.ImageData {
|
||||||
data, err := io.ReadAll(res.Body)
|
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))
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
return imgdata
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) readImageData(imgdata *imagedata.ImageData) []byte {
|
||||||
|
data, err := io.ReadAll(imgdata.Reader())
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@@ -100,10 +113,7 @@ func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, *im
|
|||||||
po.Width = 4
|
po.Width = 4
|
||||||
po.Height = 4
|
po.Height = 4
|
||||||
|
|
||||||
imgdata := imagedata.ImageData{
|
imgdata := s.readTestImageData("test1.png")
|
||||||
Type: imagetype.PNG,
|
|
||||||
Data: s.readTestFile("test1.png"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(imgETag) != 0 {
|
if len(imgETag) != 0 {
|
||||||
imgdata.Headers = map[string]string{"ETag": imgETag}
|
imgdata.Headers = map[string]string{"ETag": imgETag}
|
||||||
@@ -112,8 +122,8 @@ func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, *im
|
|||||||
var h etag.Handler
|
var h etag.Handler
|
||||||
|
|
||||||
h.SetActualProcessingOptions(po)
|
h.SetActualProcessingOptions(po)
|
||||||
h.SetActualImageData(&imgdata)
|
h.SetActualImageData(imgdata)
|
||||||
return poStr, &imgdata, h.GenerateActualETag()
|
return poStr, imgdata, h.GenerateActualETag()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestRequest() {
|
func (s *ProcessingHandlerTestSuite) TestRequest() {
|
||||||
@@ -262,10 +272,9 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
|
|||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(200, res.StatusCode)
|
||||||
|
|
||||||
actual := s.readBody(res)
|
expected := s.readTestImageData("test1.png")
|
||||||
expected := s.readTestFile("test1.png")
|
|
||||||
|
|
||||||
s.Require().True(bytes.Equal(expected, actual))
|
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
|
||||||
@@ -274,10 +283,9 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
|
|||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(200, res.StatusCode)
|
||||||
|
|
||||||
actual := s.readBody(res)
|
expected := s.readTestImageData("test1.png")
|
||||||
expected := s.readTestFile("test1.png")
|
|
||||||
|
|
||||||
s.Require().True(bytes.Equal(expected, actual))
|
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
|
||||||
@@ -288,10 +296,9 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
|
|||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(200, res.StatusCode)
|
||||||
|
|
||||||
actual := s.readBody(res)
|
expected := s.readTestImageData("test1.png")
|
||||||
expected := s.readTestFile("test1.png")
|
|
||||||
|
|
||||||
s.Require().True(bytes.Equal(expected, actual))
|
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
|
||||||
@@ -302,10 +309,9 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
|
|||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(200, res.StatusCode)
|
||||||
|
|
||||||
actual := s.readBody(res)
|
expected := s.readTestImageData("test1.png")
|
||||||
expected := s.readTestFile("test1.png")
|
|
||||||
|
|
||||||
s.Require().False(bytes.Equal(expected, actual))
|
s.Require().False(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
|
||||||
@@ -314,12 +320,10 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
|
|||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(200, res.StatusCode)
|
||||||
|
|
||||||
actual := s.readBody(res)
|
expected, err := svg.Sanitize(s.readTestImageData("test1.svg"))
|
||||||
expected, err := svg.Sanitize(&imagedata.ImageData{Data: s.readTestFile("test1.svg")})
|
|
||||||
|
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
s.Require().True(bytes.Equal(expected.Data, actual))
|
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
|
func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
|
||||||
@@ -328,10 +332,9 @@ func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
|
|||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(200, res.StatusCode)
|
||||||
|
|
||||||
actual := s.readBody(res)
|
expected := s.readTestImageData("test1.svg")
|
||||||
expected := s.readTestFile("test1.svg")
|
|
||||||
|
|
||||||
s.Require().False(bytes.Equal(expected, actual))
|
s.Require().False(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestErrorSavingToSVG() {
|
func (s *ProcessingHandlerTestSuite) TestErrorSavingToSVG() {
|
||||||
@@ -435,7 +438,7 @@ func (s *ProcessingHandlerTestSuite) TestETagDataNoIfNotModified() {
|
|||||||
s.Empty(r.Header.Get("If-None-Match"))
|
s.Empty(r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(imgdata.Data)
|
rw.Write(s.readImageData(imgdata))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
@@ -477,7 +480,7 @@ func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
|
|||||||
s.Empty(r.Header.Get("If-None-Match"))
|
s.Empty(r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(imgdata.Data)
|
rw.Write(s.readImageData(imgdata))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
@@ -502,7 +505,7 @@ func (s *ProcessingHandlerTestSuite) TestETagReqNotMatch() {
|
|||||||
|
|
||||||
rw.Header().Set("ETag", imgdata.Headers["ETag"])
|
rw.Header().Set("ETag", imgdata.Headers["ETag"])
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(imgdata.Data)
|
rw.Write(s.readImageData(imgdata))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
@@ -527,7 +530,7 @@ func (s *ProcessingHandlerTestSuite) TestETagDataNotMatch() {
|
|||||||
s.Empty(r.Header.Get("If-None-Match"))
|
s.Empty(r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(imgdata.Data)
|
rw.Write(s.readImageData(imgdata))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
@@ -553,7 +556,7 @@ func (s *ProcessingHandlerTestSuite) TestETagProcessingOptionsNotMatch() {
|
|||||||
|
|
||||||
rw.Header().Set("ETag", imgdata.Headers["ETag"])
|
rw.Header().Set("ETag", imgdata.Headers["ETag"])
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(imgdata.Data)
|
rw.Write(s.readImageData(imgdata))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
|
25
svg/svg.go
25
svg/svg.go
@@ -1,8 +1,8 @@
|
|||||||
package svg
|
package svg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tdewolff/parse/v2"
|
"github.com/tdewolff/parse/v2"
|
||||||
@@ -11,21 +11,17 @@ import (
|
|||||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func cloneHeaders(src map[string]string) map[string]string {
|
func cloneHeaders(src map[string]string) http.Header {
|
||||||
if src == nil {
|
h := make(http.Header, len(src))
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dst := make(map[string]string, len(src))
|
|
||||||
for k, v := range src {
|
for k, v := range src {
|
||||||
dst[k] = v
|
h.Set(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dst
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func Sanitize(data *imagedata.ImageData) (*imagedata.ImageData, error) {
|
func Sanitize(data *imagedata.ImageData) (*imagedata.ImageData, error) {
|
||||||
r := bytes.NewReader(data.Data)
|
r := data.Reader()
|
||||||
l := xml.NewLexer(parse.NewInput(r))
|
l := xml.NewLexer(parse.NewInput(r))
|
||||||
|
|
||||||
buf, cancel := imagedata.BorrowBuffer()
|
buf, cancel := imagedata.BorrowBuffer()
|
||||||
@@ -58,14 +54,13 @@ func Sanitize(data *imagedata.ImageData) (*imagedata.ImageData, error) {
|
|||||||
return nil, l.Err()
|
return nil, l.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
newData := imagedata.ImageData{
|
newData, err := imagedata.NewFromBytes(buf.Bytes(), cloneHeaders(data.Headers))
|
||||||
Data: buf.Bytes(),
|
if err != nil {
|
||||||
Type: data.Type,
|
return nil, err
|
||||||
Headers: cloneHeaders(data.Headers),
|
|
||||||
}
|
}
|
||||||
newData.SetCancel(cancel)
|
newData.SetCancel(cancel)
|
||||||
|
|
||||||
return &newData, nil
|
return newData, nil
|
||||||
case xml.StartTagToken:
|
case xml.StartTagToken:
|
||||||
curTagName = strings.ToLower(string(l.Text()))
|
curTagName = strings.ToLower(string(l.Text()))
|
||||||
|
|
||||||
|
@@ -1,15 +1,17 @@
|
|||||||
package svg
|
package svg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
"go.withmatt.com/httpheaders"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SvgTestSuite struct {
|
type SvgTestSuite struct {
|
||||||
@@ -30,24 +32,23 @@ func (s *SvgTestSuite) readTestFile(name string) *imagedata.ImageData {
|
|||||||
data, err := os.ReadFile(filepath.Join(wd, "..", "testdata", name))
|
data, err := os.ReadFile(filepath.Join(wd, "..", "testdata", name))
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
return &imagedata.ImageData{
|
h := make(http.Header)
|
||||||
Type: imagetype.SVG,
|
h.Set(httpheaders.ContentType, "image/svg+xml")
|
||||||
Data: data,
|
h.Set(httpheaders.CacheControl, "public, max-age=12345")
|
||||||
Headers: map[string]string{
|
|
||||||
"Content-Type": "image/svg+xml",
|
d, err := imagedata.NewFromBytes(data, h)
|
||||||
"Cache-Control": "public, max-age=12345",
|
s.Require().NoError(err)
|
||||||
},
|
|
||||||
}
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SvgTestSuite) TestSanitize() {
|
func (s *SvgTestSuite) TestSanitize() {
|
||||||
origin := s.readTestFile("test1.svg")
|
origin := s.readTestFile("test1.svg")
|
||||||
expected := s.readTestFile("test1.sanitized.svg")
|
expected := s.readTestFile("test1.sanitized.svg")
|
||||||
|
|
||||||
actual, err := Sanitize(origin)
|
actual, err := Sanitize(origin)
|
||||||
|
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
s.Require().Equal(string(expected.Data), string(actual.Data))
|
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), actual.Reader()))
|
||||||
s.Require().Equal(origin.Headers, actual.Headers)
|
s.Require().Equal(origin.Headers, actual.Headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
38
testutil/testutil.go
Normal file
38
testutil/testutil.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const bufSize = 4096
|
||||||
|
|
||||||
|
// RequireReadersEqual compares two io.Reader contents in a streaming manner.
|
||||||
|
// It fails the test if contents differ or if reading fails.
|
||||||
|
func ReadersEqual(t require.TestingT, expected, actual io.Reader) bool {
|
||||||
|
if h, ok := t.(interface{ Helper() }); ok {
|
||||||
|
h.Helper()
|
||||||
|
}
|
||||||
|
|
||||||
|
buf1 := make([]byte, bufSize)
|
||||||
|
buf2 := make([]byte, bufSize)
|
||||||
|
|
||||||
|
for {
|
||||||
|
n1, err1 := expected.Read(buf1)
|
||||||
|
n2, err2 := actual.Read(buf2)
|
||||||
|
|
||||||
|
if n1 != n2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, buf1[:n1], buf2[:n1])
|
||||||
|
|
||||||
|
if err1 == io.EOF && err2 == io.EOF {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err1)
|
||||||
|
require.NoError(t, err2)
|
||||||
|
}
|
||||||
|
}
|
22
vips/vips.go
22
vips/vips.go
@@ -9,7 +9,6 @@ package vips
|
|||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
@@ -359,11 +358,11 @@ func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64,
|
|||||||
|
|
||||||
err := C.int(0)
|
err := C.int(0)
|
||||||
|
|
||||||
reader := bytes.NewReader(imgdata.Data)
|
reader := imgdata.Reader()
|
||||||
source := newVipsImgproxySource(reader)
|
source := newVipsImgproxySource(reader)
|
||||||
defer C.unref_imgproxy_source(source)
|
defer C.unref_imgproxy_source(source)
|
||||||
|
|
||||||
switch imgdata.Type {
|
switch imgdata.Format() {
|
||||||
case imagetype.JPEG:
|
case imagetype.JPEG:
|
||||||
err = C.vips_jpegload_source_go(source, C.int(shrink), &tmp)
|
err = C.vips_jpegload_source_go(source, C.int(shrink), &tmp)
|
||||||
case imagetype.JXL:
|
case imagetype.JXL:
|
||||||
@@ -393,7 +392,7 @@ func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64,
|
|||||||
|
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
|
|
||||||
if imgdata.Type == imagetype.TIFF {
|
if imgdata.Format() == imagetype.TIFF {
|
||||||
if C.vips_fix_float_tiff(img.VipsImage, &tmp) == 0 {
|
if C.vips_fix_float_tiff(img.VipsImage, &tmp) == 0 {
|
||||||
C.swap_and_clear(&img.VipsImage, tmp)
|
C.swap_and_clear(&img.VipsImage, tmp)
|
||||||
} else {
|
} else {
|
||||||
@@ -405,13 +404,13 @@ func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (img *Image) LoadThumbnail(imgdata *imagedata.ImageData) error {
|
func (img *Image) LoadThumbnail(imgdata *imagedata.ImageData) error {
|
||||||
if imgdata.Type != imagetype.HEIC && imgdata.Type != imagetype.AVIF {
|
if imgdata.Format() != imagetype.HEIC && imgdata.Format() != imagetype.AVIF {
|
||||||
return newVipsError("Usupported image type to load thumbnail")
|
return newVipsError("Usupported image type to load thumbnail")
|
||||||
}
|
}
|
||||||
|
|
||||||
var tmp *C.VipsImage
|
var tmp *C.VipsImage
|
||||||
|
|
||||||
reader := bytes.NewReader(imgdata.Data)
|
reader := imgdata.Reader()
|
||||||
source := newVipsImgproxySource(reader)
|
source := newVipsImgproxySource(reader)
|
||||||
defer C.unref_imgproxy_source(source)
|
defer C.unref_imgproxy_source(source)
|
||||||
|
|
||||||
@@ -469,14 +468,17 @@ func (img *Image) Save(imgtype imagetype.Type, quality int) (*imagedata.ImageDat
|
|||||||
var blob_ptr = C.vips_blob_get(target.blob, &imgsize)
|
var blob_ptr = C.vips_blob_get(target.blob, &imgsize)
|
||||||
var ptr unsafe.Pointer = unsafe.Pointer(blob_ptr)
|
var ptr unsafe.Pointer = unsafe.Pointer(blob_ptr)
|
||||||
|
|
||||||
imgdata := imagedata.ImageData{
|
b := ptrToBytes(ptr, int(imgsize))
|
||||||
Type: imgtype,
|
|
||||||
Data: ptrToBytes(ptr, int(imgsize)),
|
imgdata, ierr := imagedata.NewFromBytes(b, make(http.Header))
|
||||||
|
if ierr != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, ierr
|
||||||
}
|
}
|
||||||
|
|
||||||
imgdata.SetCancel(cancel)
|
imgdata.SetCancel(cancel)
|
||||||
|
|
||||||
return &imgdata, nil
|
return imgdata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (img *Image) Clear() {
|
func (img *Image) Clear() {
|
||||||
|
Reference in New Issue
Block a user