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

* Replace imagemeta with imagedetect

* Removed imagemeta package

* 1 page in the check
This commit is contained in:
Victor Sokolov
2025-08-13 11:49:48 +02:00
committed by GitHub
parent 2236bd8170
commit 09a25f8966
23 changed files with 96 additions and 1458 deletions

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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"))
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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) }

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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) },
)
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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"
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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
}