mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-10-06 18:03:29 +02:00
IMG-23: Replace imagemeta with imagedetect (#1483)
* Replace imagemeta with imagedetect * Removed imagemeta package * 1 page in the check
This commit is contained in:
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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"))
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
}
|
@@ -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) }
|
@@ -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)
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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) },
|
||||
)
|
||||
}
|
@@ -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()
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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))
|
||||
}
|
253
imagemeta/jxl.go
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)
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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)
|
||||
|
@@ -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))
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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))
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user