mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-09-28 20:43:54 +02:00
IMG-23: split imagetype (#1484)
* Image type (new) * bufreader, RegisterFormat updated * Remove StemExt * Unknown = 0 * Joined imagedetect and imagetype * Use regular []byte in bufreader
This commit is contained in:
@@ -1,104 +1,79 @@
|
|||||||
|
// bufreader provides a buffered reader that reads from io.Reader, but caches
|
||||||
|
// the data in a bytes.Buffer to allow peeking and discarding without re-reading.
|
||||||
package bufreader
|
package bufreader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/imath"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reader struct {
|
// ReadPeeker is an interface that combines io.Reader and a method to peek at the next n bytes
|
||||||
r io.Reader
|
type ReadPeeker interface {
|
||||||
buf *bytes.Buffer
|
io.Reader
|
||||||
cur int
|
Peek(n int) ([]byte, error) // Peek returns the next n bytes without advancing
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(r io.Reader, buf *bytes.Buffer) *Reader {
|
// Reader is a buffered reader that reads from an io.Reader and caches the data.
|
||||||
|
type Reader struct {
|
||||||
|
r io.Reader
|
||||||
|
buf []byte
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates new buffered reader
|
||||||
|
func New(r io.Reader) *Reader {
|
||||||
br := Reader{
|
br := Reader{
|
||||||
r: r,
|
r: r,
|
||||||
buf: buf,
|
buf: nil,
|
||||||
}
|
}
|
||||||
return &br
|
return &br
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read reads data into p from the buffered reader.
|
||||||
func (br *Reader) Read(p []byte) (int, error) {
|
func (br *Reader) Read(p []byte) (int, error) {
|
||||||
if err := br.fill(br.cur + len(p)); err != nil {
|
if err := br.fetch(br.pos + len(p)); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
n := copy(p, br.buf.Bytes()[br.cur:])
|
n := copy(p, br.buf[br.pos:])
|
||||||
br.cur += n
|
br.pos += n
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *Reader) ReadByte() (byte, error) {
|
|
||||||
if err := br.fill(br.cur + 1); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
b := br.buf.Bytes()[br.cur]
|
|
||||||
br.cur++
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *Reader) Discard(n int) (int, error) {
|
|
||||||
if n < 0 {
|
|
||||||
return 0, bufio.ErrNegativeCount
|
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := br.fill(br.cur + n); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n = imath.Min(n, br.buf.Len()-br.cur)
|
|
||||||
br.cur += n
|
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Peek returns the next n bytes from the buffered reader without advancing the position.
|
||||||
func (br *Reader) Peek(n int) ([]byte, error) {
|
func (br *Reader) Peek(n int) ([]byte, error) {
|
||||||
if n < 0 {
|
err := br.fetch(br.pos + n)
|
||||||
return []byte{}, bufio.ErrNegativeCount
|
if err != nil && err != io.EOF {
|
||||||
}
|
return nil, err
|
||||||
if n == 0 {
|
|
||||||
return []byte{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := br.fill(br.cur + n); err != nil {
|
// Return slice of buffered data without advancing position
|
||||||
return []byte{}, err
|
available := br.buf[br.pos:]
|
||||||
|
if len(available) == 0 && err == io.EOF {
|
||||||
|
return nil, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
if n > br.buf.Len()-br.cur {
|
return available[:min(len(available), n)], nil
|
||||||
return br.buf.Bytes()[br.cur:], io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
return br.buf.Bytes()[br.cur : br.cur+n], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *Reader) Flush() error {
|
// Rewind seeks buffer to the beginning
|
||||||
_, err := br.buf.ReadFrom(br.r)
|
func (br *Reader) Rewind() {
|
||||||
return err
|
br.pos = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *Reader) fill(need int) error {
|
// fetch ensures the buffer contains at least 'need' bytes
|
||||||
n := need - br.buf.Len()
|
func (br *Reader) fetch(need int) error {
|
||||||
if n <= 0 {
|
if need-len(br.buf) <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
n = imath.Max(4096, n)
|
b := make([]byte, need)
|
||||||
|
n, err := io.ReadFull(br.r, b)
|
||||||
if _, err := br.buf.ReadFrom(io.LimitReader(br.r, int64(n))); err != nil {
|
if err != nil && err != io.ErrUnexpectedEOF {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing was read, it's EOF
|
// append only those which we read in fact
|
||||||
if br.cur == br.buf.Len() {
|
br.buf = append(br.buf, b[:n]...)
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
97
bufreader/bufreader_test.go
Normal file
97
bufreader/bufreader_test.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package bufreader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BufferedReaderTestSuite defines the test suite for the buffered reader
|
||||||
|
type BufferedReaderTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BufferedReaderTestSuite) TestRead() {
|
||||||
|
data := "hello world"
|
||||||
|
br := New(strings.NewReader(data))
|
||||||
|
|
||||||
|
// First read
|
||||||
|
p1 := make([]byte, 5)
|
||||||
|
n1, err1 := br.Read(p1)
|
||||||
|
s.Require().NoError(err1)
|
||||||
|
s.Equal(5, n1)
|
||||||
|
s.Equal("hello", string(p1))
|
||||||
|
|
||||||
|
// Second read
|
||||||
|
p2 := make([]byte, 6)
|
||||||
|
n2, err2 := br.Read(p2)
|
||||||
|
s.Require().NoError(err2)
|
||||||
|
s.Equal(6, n2)
|
||||||
|
s.Equal(" world", string(p2))
|
||||||
|
|
||||||
|
// Verify position
|
||||||
|
s.Equal(11, br.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BufferedReaderTestSuite) TestEOF() {
|
||||||
|
data := "hello"
|
||||||
|
br := New(strings.NewReader(data))
|
||||||
|
|
||||||
|
// Read all data
|
||||||
|
p1 := make([]byte, 5)
|
||||||
|
n1, err1 := br.Read(p1)
|
||||||
|
s.Require().NoError(err1)
|
||||||
|
s.Equal(5, n1)
|
||||||
|
s.Equal("hello", string(p1))
|
||||||
|
|
||||||
|
// Try to read more - should get EOF
|
||||||
|
p2 := make([]byte, 5)
|
||||||
|
n2, err2 := br.Read(p2)
|
||||||
|
s.Equal(io.EOF, err2)
|
||||||
|
s.Equal(0, n2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BufferedReaderTestSuite) TestEOF_WhenDataExhausted() {
|
||||||
|
data := "hello"
|
||||||
|
br := New(strings.NewReader(data))
|
||||||
|
|
||||||
|
// Try to read more than available
|
||||||
|
p := make([]byte, 10)
|
||||||
|
n, err := br.Read(p)
|
||||||
|
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(5, n)
|
||||||
|
s.Equal("hello", string(p[:n]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BufferedReaderTestSuite) TestPeek() {
|
||||||
|
data := "hello world"
|
||||||
|
br := New(strings.NewReader(data))
|
||||||
|
|
||||||
|
// Peek at first 5 bytes
|
||||||
|
peeked, err := br.Peek(5)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal("hello", string(peeked))
|
||||||
|
s.Equal(0, br.pos) // Position should not change
|
||||||
|
|
||||||
|
// Read the same data to verify peek didn't consume it
|
||||||
|
p := make([]byte, 5)
|
||||||
|
n, err := br.Read(p)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(5, n)
|
||||||
|
s.Equal("hello", string(p))
|
||||||
|
s.Equal(5, br.pos) // Position should now be updated
|
||||||
|
|
||||||
|
// Peek at the next 7 bytes (which are beyond the EOF)
|
||||||
|
peeked2, err := br.Peek(7)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(" world", string(peeked2))
|
||||||
|
s.Equal(5, br.pos) // Position should still be 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBufferedReaderSuite runs the test suite
|
||||||
|
func TestBufferedReader(t *testing.T) {
|
||||||
|
suite.Run(t, new(BufferedReaderTestSuite))
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
// Code generated by gen_imagetype.go; DO NOT EDIT.
|
||||||
|
|
||||||
package imagetype
|
package imagetype
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -74,15 +76,6 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func ByMime(mime string) Type {
|
|
||||||
for k, v := range mimes {
|
|
||||||
if v == mime {
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it Type) String() string {
|
func (it Type) String() string {
|
||||||
// JPEG has two names, we should use only the full one
|
// JPEG has two names, we should use only the full one
|
||||||
if it == JPEG {
|
if it == JPEG {
|
||||||
|
191
imagetype_new/defs.go
Normal file
191
imagetype_new/defs.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package imagetype_new
|
||||||
|
|
||||||
|
var (
|
||||||
|
JPEG = RegisterType(&TypeDesc{
|
||||||
|
String: "jpeg",
|
||||||
|
Ext: ".jpg",
|
||||||
|
Mime: "image/jpeg",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: false,
|
||||||
|
SupportsColourProfile: true,
|
||||||
|
SupportsQuality: true,
|
||||||
|
SupportsAnimationLoad: false,
|
||||||
|
SupportsAnimationSave: false,
|
||||||
|
SupportsThumbnail: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
JXL = RegisterType(&TypeDesc{
|
||||||
|
String: "jxl",
|
||||||
|
Ext: ".jxl",
|
||||||
|
Mime: "image/jxl",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: true,
|
||||||
|
SupportsQuality: true,
|
||||||
|
SupportsAnimationLoad: true,
|
||||||
|
SupportsAnimationSave: false,
|
||||||
|
SupportsThumbnail: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
PNG = RegisterType(&TypeDesc{
|
||||||
|
String: "png",
|
||||||
|
Ext: ".png",
|
||||||
|
Mime: "image/png",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: true,
|
||||||
|
SupportsQuality: false,
|
||||||
|
SupportsAnimationLoad: false,
|
||||||
|
SupportsAnimationSave: false,
|
||||||
|
SupportsThumbnail: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
WEBP = RegisterType(&TypeDesc{
|
||||||
|
String: "webp",
|
||||||
|
Ext: ".webp",
|
||||||
|
Mime: "image/webp",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: true,
|
||||||
|
SupportsQuality: true,
|
||||||
|
SupportsAnimationLoad: true,
|
||||||
|
SupportsAnimationSave: true,
|
||||||
|
SupportsThumbnail: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
GIF = RegisterType(&TypeDesc{
|
||||||
|
String: "gif",
|
||||||
|
Ext: ".gif",
|
||||||
|
Mime: "image/gif",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: false,
|
||||||
|
SupportsQuality: false,
|
||||||
|
SupportsAnimationLoad: true,
|
||||||
|
SupportsAnimationSave: true,
|
||||||
|
SupportsThumbnail: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
ICO = RegisterType(&TypeDesc{
|
||||||
|
String: "ico",
|
||||||
|
Ext: ".ico",
|
||||||
|
Mime: "image/x-icon",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: false,
|
||||||
|
SupportsQuality: false,
|
||||||
|
SupportsAnimationLoad: false,
|
||||||
|
SupportsAnimationSave: false,
|
||||||
|
SupportsThumbnail: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
SVG = RegisterType(&TypeDesc{
|
||||||
|
String: "svg",
|
||||||
|
Ext: ".svg",
|
||||||
|
Mime: "image/svg+xml",
|
||||||
|
IsVector: true,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: false,
|
||||||
|
SupportsQuality: false,
|
||||||
|
SupportsAnimationLoad: false,
|
||||||
|
SupportsAnimationSave: false,
|
||||||
|
SupportsThumbnail: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
HEIC = RegisterType(&TypeDesc{
|
||||||
|
String: "heic",
|
||||||
|
Ext: ".heic",
|
||||||
|
Mime: "image/heif",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: true,
|
||||||
|
SupportsQuality: true,
|
||||||
|
SupportsAnimationLoad: false,
|
||||||
|
SupportsAnimationSave: false,
|
||||||
|
SupportsThumbnail: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
AVIF = RegisterType(&TypeDesc{
|
||||||
|
String: "avif",
|
||||||
|
Ext: ".avif",
|
||||||
|
Mime: "image/avif",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: true,
|
||||||
|
SupportsQuality: true,
|
||||||
|
SupportsAnimationLoad: false,
|
||||||
|
SupportsAnimationSave: false,
|
||||||
|
SupportsThumbnail: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
BMP = RegisterType(&TypeDesc{
|
||||||
|
String: "bmp",
|
||||||
|
Ext: ".bmp",
|
||||||
|
Mime: "image/bmp",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: false,
|
||||||
|
SupportsQuality: false,
|
||||||
|
SupportsAnimationLoad: false,
|
||||||
|
SupportsAnimationSave: false,
|
||||||
|
SupportsThumbnail: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
TIFF = RegisterType(&TypeDesc{
|
||||||
|
String: "tiff",
|
||||||
|
Ext: ".tiff",
|
||||||
|
Mime: "image/tiff",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: false,
|
||||||
|
SupportsQuality: true,
|
||||||
|
SupportsAnimationLoad: false,
|
||||||
|
SupportsAnimationSave: false,
|
||||||
|
SupportsThumbnail: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// init registers default magic bytes for common image formats
|
||||||
|
func init() {
|
||||||
|
// NOTE: we cannot be 100% sure of image type until we fully decode it. This is especially true
|
||||||
|
// for "naked" jxl (0xff 0x0a). There is no other way to ensure this is a JXL file, except to fully
|
||||||
|
// decode it. Two bytes are too few to reliably identify the format. The same applies to ICO.
|
||||||
|
|
||||||
|
// JPEG magic bytes
|
||||||
|
RegisterMagicBytes(JPEG, []byte("\xff\xd8"))
|
||||||
|
|
||||||
|
// JXL magic bytes
|
||||||
|
RegisterMagicBytes(JXL, []byte{0xff, 0x0a}) // JXL codestream (can't use string due to 0x0a)
|
||||||
|
RegisterMagicBytes(JXL, []byte{0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A}) // JXL container (has null bytes)
|
||||||
|
|
||||||
|
// PNG magic bytes
|
||||||
|
RegisterMagicBytes(PNG, []byte("\x89PNG\r\n\x1a\n"))
|
||||||
|
|
||||||
|
// WEBP magic bytes (RIFF container with WEBP fourcc) - using wildcard for size
|
||||||
|
RegisterMagicBytes(WEBP, []byte("RIFF????WEBP"))
|
||||||
|
|
||||||
|
// GIF magic bytes
|
||||||
|
RegisterMagicBytes(GIF, []byte("GIF8?a"))
|
||||||
|
|
||||||
|
// ICO magic bytes
|
||||||
|
RegisterMagicBytes(ICO, []byte{0, 0, 1, 0}) // ICO (has null bytes)
|
||||||
|
|
||||||
|
// HEIC/HEIF magic bytes with wildcards for size
|
||||||
|
RegisterMagicBytes(HEIC, []byte("????ftypheic"),
|
||||||
|
[]byte("????ftypheix"),
|
||||||
|
[]byte("????ftyphevc"),
|
||||||
|
[]byte("????ftypheim"),
|
||||||
|
[]byte("????ftypheis"),
|
||||||
|
[]byte("????ftyphevm"),
|
||||||
|
[]byte("????ftyphevs"),
|
||||||
|
[]byte("????ftypmif1"))
|
||||||
|
|
||||||
|
// AVIF magic bytes
|
||||||
|
RegisterMagicBytes(AVIF, []byte("????ftypavif"))
|
||||||
|
|
||||||
|
// BMP magic bytes
|
||||||
|
RegisterMagicBytes(BMP, []byte("BM"))
|
||||||
|
|
||||||
|
// TIFF magic bytes (little-endian and big-endian)
|
||||||
|
RegisterMagicBytes(TIFF, []byte("II*\x00"), []byte("MM\x00*")) // Big-Endian, Little-endian
|
||||||
|
}
|
23
imagetype_new/errors.go
Normal file
23
imagetype_new/errors.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package imagetype_new
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
UnknownFormatError struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
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" }
|
143
imagetype_new/registry.go
Normal file
143
imagetype_new/registry.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package imagetype_new
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v3/bufreader"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// maxDetectionLimit is maximum bytes detectors allowed to read from the source
|
||||||
|
maxDetectionLimit = 32 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
// TypeDesc is used to store metadata about an image type.
|
||||||
|
// It represents the minimal information needed to make imgproxy to
|
||||||
|
// work with the type.
|
||||||
|
type TypeDesc struct {
|
||||||
|
String string
|
||||||
|
Ext string
|
||||||
|
Mime string
|
||||||
|
IsVector bool
|
||||||
|
SupportsAlpha bool
|
||||||
|
SupportsColourProfile bool
|
||||||
|
SupportsQuality bool
|
||||||
|
SupportsAnimationLoad bool
|
||||||
|
SupportsAnimationSave bool
|
||||||
|
SupportsThumbnail bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectFunc is a function that detects the image type from byte data
|
||||||
|
type DetectFunc func(r bufreader.ReadPeeker) (Type, error)
|
||||||
|
|
||||||
|
// Registry holds the type registry
|
||||||
|
type Registry struct {
|
||||||
|
detectors []DetectFunc
|
||||||
|
types []*TypeDesc
|
||||||
|
}
|
||||||
|
|
||||||
|
// globalRegistry is the default registry instance
|
||||||
|
var globalRegistry = &Registry{}
|
||||||
|
|
||||||
|
// RegisterType registers a new image type in the global registry.
|
||||||
|
// It panics if the type already exists (i.e., if a TypeDesc is already registered for this Type).
|
||||||
|
func RegisterType(desc *TypeDesc) Type {
|
||||||
|
return globalRegistry.RegisterType(desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTypeDesc returns the TypeDesc for the given Type.
|
||||||
|
// Returns nil if the type is not registered.
|
||||||
|
func GetTypeDesc(t Type) *TypeDesc {
|
||||||
|
return globalRegistry.GetTypeDesc(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterType registers a new image type in this registry.
|
||||||
|
// It panics if the type already exists (i.e., if a TypeDesc is already registered for this Type).
|
||||||
|
func (r *Registry) RegisterType(desc *TypeDesc) Type {
|
||||||
|
r.types = append(r.types, desc)
|
||||||
|
return Type(len(r.types)) // 0 is unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTypeDesc returns the TypeDesc for the given Type.
|
||||||
|
// Returns nil if the type is not registered.
|
||||||
|
func (r *Registry) GetTypeDesc(t Type) *TypeDesc {
|
||||||
|
if t <= 0 { // This would be "default" type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(t-1) >= len(r.types) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.types[t-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterDetector registers a custom detector function
|
||||||
|
// Detectors are tried in the order they were registered
|
||||||
|
func RegisterDetector(detector DetectFunc) {
|
||||||
|
globalRegistry.RegisterDetector(detector)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterMagicBytes registers magic bytes for a specific image type
|
||||||
|
// Magic byte detectors are always tried before custom detectors
|
||||||
|
func RegisterMagicBytes(typ Type, signature ...[]byte) {
|
||||||
|
globalRegistry.RegisterMagicBytes(typ, signature...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect attempts to detect the image type from a reader.
|
||||||
|
// It first tries magic byte detection, then custom detectors in registration order
|
||||||
|
func Detect(r io.Reader) (Type, error) {
|
||||||
|
return globalRegistry.Detect(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterDetector registers a custom detector function on this registry instance
|
||||||
|
func (r *Registry) RegisterDetector(detector DetectFunc) {
|
||||||
|
r.detectors = append(r.detectors, detector)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterMagicBytes registers magic bytes for a specific image type on this registry instance
|
||||||
|
func (r *Registry) RegisterMagicBytes(typ Type, signature ...[]byte) {
|
||||||
|
r.detectors = append(r.detectors, func(r bufreader.ReadPeeker) (Type, error) {
|
||||||
|
for _, sig := range signature {
|
||||||
|
b, err := r.Peek(len(sig))
|
||||||
|
if err != nil {
|
||||||
|
return Unknown, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasMagicBytes(b, sig) {
|
||||||
|
return typ, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unknown, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect runs image format detection
|
||||||
|
func (r *Registry) Detect(re io.Reader) (Type, error) {
|
||||||
|
br := bufreader.New(io.LimitReader(re, maxDetectionLimit))
|
||||||
|
|
||||||
|
for _, fn := range globalRegistry.detectors {
|
||||||
|
br.Rewind()
|
||||||
|
if typ, err := fn(br); err == nil && typ != Unknown {
|
||||||
|
return typ, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unknown, newUnknownFormatError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasMagicBytes checks if the data matches a magic byte signature
|
||||||
|
// Supports '?' characters in signature which match any byte
|
||||||
|
func hasMagicBytes(data []byte, magic []byte) bool {
|
||||||
|
if len(data) < len(magic) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, c := range magic {
|
||||||
|
if c != data[i] && c != '?' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
178
imagetype_new/registry_test.go
Normal file
178
imagetype_new/registry_test.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package imagetype_new
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v3/bufreader"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegisterType(t *testing.T) {
|
||||||
|
// Create a separate registry for testing to avoid conflicts with global registry
|
||||||
|
testRegistry := &Registry{}
|
||||||
|
|
||||||
|
// Register a custom type
|
||||||
|
customDesc := &TypeDesc{
|
||||||
|
String: "custom",
|
||||||
|
Ext: ".custom",
|
||||||
|
Mime: "image/custom",
|
||||||
|
IsVector: false,
|
||||||
|
SupportsAlpha: true,
|
||||||
|
SupportsColourProfile: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
customType := testRegistry.RegisterType(customDesc)
|
||||||
|
|
||||||
|
// Verify the type is now registered
|
||||||
|
result := testRegistry.GetTypeDesc(customType)
|
||||||
|
require.NotNil(t, result)
|
||||||
|
require.Equal(t, customDesc.String, result.String)
|
||||||
|
require.Equal(t, customDesc.Ext, result.Ext)
|
||||||
|
require.Equal(t, customDesc.Mime, result.Mime)
|
||||||
|
require.Equal(t, customDesc.IsVector, result.IsVector)
|
||||||
|
require.Equal(t, customDesc.SupportsAlpha, result.SupportsAlpha)
|
||||||
|
require.Equal(t, customDesc.SupportsColourProfile, result.SupportsColourProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeProperties(t *testing.T) {
|
||||||
|
// Test that Type methods use TypeDesc fields correctly
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
typ Type
|
||||||
|
expectVector bool
|
||||||
|
expectAlpha bool
|
||||||
|
expectColourProfile bool
|
||||||
|
expectQuality bool
|
||||||
|
expectAnimationLoad bool
|
||||||
|
expectAnimationSave bool
|
||||||
|
expectThumbnail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "JPEG",
|
||||||
|
typ: JPEG,
|
||||||
|
expectVector: false,
|
||||||
|
expectAlpha: false,
|
||||||
|
expectColourProfile: true,
|
||||||
|
expectQuality: true,
|
||||||
|
expectAnimationLoad: false,
|
||||||
|
expectAnimationSave: false,
|
||||||
|
expectThumbnail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PNG",
|
||||||
|
typ: PNG,
|
||||||
|
expectVector: false,
|
||||||
|
expectAlpha: true,
|
||||||
|
expectColourProfile: true,
|
||||||
|
expectQuality: false,
|
||||||
|
expectAnimationLoad: false,
|
||||||
|
expectAnimationSave: false,
|
||||||
|
expectThumbnail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WEBP",
|
||||||
|
typ: WEBP,
|
||||||
|
expectVector: false,
|
||||||
|
expectAlpha: true,
|
||||||
|
expectColourProfile: true,
|
||||||
|
expectQuality: true,
|
||||||
|
expectAnimationLoad: true,
|
||||||
|
expectAnimationSave: true,
|
||||||
|
expectThumbnail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SVG",
|
||||||
|
typ: SVG,
|
||||||
|
expectVector: true,
|
||||||
|
expectAlpha: true,
|
||||||
|
expectColourProfile: false,
|
||||||
|
expectQuality: false,
|
||||||
|
expectAnimationLoad: false,
|
||||||
|
expectAnimationSave: false,
|
||||||
|
expectThumbnail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GIF",
|
||||||
|
typ: GIF,
|
||||||
|
expectVector: false,
|
||||||
|
expectAlpha: true,
|
||||||
|
expectColourProfile: false,
|
||||||
|
expectQuality: false,
|
||||||
|
expectAnimationLoad: true,
|
||||||
|
expectAnimationSave: true,
|
||||||
|
expectThumbnail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HEIC",
|
||||||
|
typ: HEIC,
|
||||||
|
expectVector: false,
|
||||||
|
expectAlpha: true,
|
||||||
|
expectColourProfile: true,
|
||||||
|
expectQuality: true,
|
||||||
|
expectAnimationLoad: false,
|
||||||
|
expectAnimationSave: false,
|
||||||
|
expectThumbnail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AVIF",
|
||||||
|
typ: AVIF,
|
||||||
|
expectVector: false,
|
||||||
|
expectAlpha: true,
|
||||||
|
expectColourProfile: true,
|
||||||
|
expectQuality: true,
|
||||||
|
expectAnimationLoad: false,
|
||||||
|
expectAnimationSave: false,
|
||||||
|
expectThumbnail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
require.Equal(t, tt.expectVector, tt.typ.IsVector())
|
||||||
|
require.Equal(t, tt.expectAlpha, tt.typ.SupportsAlpha())
|
||||||
|
require.Equal(t, tt.expectColourProfile, tt.typ.SupportsColourProfile())
|
||||||
|
require.Equal(t, tt.expectQuality, tt.typ.SupportsQuality())
|
||||||
|
require.Equal(t, tt.expectAnimationLoad, tt.typ.SupportsAnimationLoad())
|
||||||
|
require.Equal(t, tt.expectAnimationSave, tt.typ.SupportsAnimationSave())
|
||||||
|
require.Equal(t, tt.expectThumbnail, tt.typ.SupportsThumbnail())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterDetector(t *testing.T) {
|
||||||
|
// Create a test registry to avoid interfering with global state
|
||||||
|
testRegistry := &Registry{}
|
||||||
|
|
||||||
|
// Create a test detector function
|
||||||
|
testDetector := func(r bufreader.ReadPeeker) (Type, error) {
|
||||||
|
b, err := r.Peek(2)
|
||||||
|
if err != nil {
|
||||||
|
return Unknown, err
|
||||||
|
}
|
||||||
|
if len(b) >= 2 && b[0] == 0xFF && b[1] == 0xD8 {
|
||||||
|
return JPEG, nil
|
||||||
|
}
|
||||||
|
return Unknown, newUnknownFormatError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the detector using the method
|
||||||
|
testRegistry.RegisterDetector(testDetector)
|
||||||
|
|
||||||
|
// Verify the detector is registered
|
||||||
|
require.Len(t, testRegistry.detectors, 1)
|
||||||
|
require.NotNil(t, testRegistry.detectors[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterMagicBytes(t *testing.T) {
|
||||||
|
// Create a test registry to avoid interfering with global state
|
||||||
|
testRegistry := &Registry{}
|
||||||
|
|
||||||
|
require.Empty(t, testRegistry.detectors)
|
||||||
|
|
||||||
|
// Register magic bytes for JPEG using the method
|
||||||
|
jpegMagic := []byte{0xFF, 0xD8}
|
||||||
|
testRegistry.RegisterMagicBytes(JPEG, jpegMagic)
|
||||||
|
|
||||||
|
// Verify the magic bytes are registered
|
||||||
|
require.Len(t, testRegistry.detectors, 1)
|
||||||
|
}
|
34
imagetype_new/svg.go
Normal file
34
imagetype_new/svg.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package imagetype_new
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v3/bufreader"
|
||||||
|
|
||||||
|
"github.com/tdewolff/parse/v2"
|
||||||
|
"github.com/tdewolff/parse/v2/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Register SVG detector (needs at least 1000 bytes to reliably detect SVG)
|
||||||
|
RegisterDetector(IsSVG)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsSVG(r bufreader.ReadPeeker) (Type, error) {
|
||||||
|
l := xml.NewLexer(parse.NewInput(r))
|
||||||
|
|
||||||
|
for {
|
||||||
|
tt, _ := l.Next()
|
||||||
|
|
||||||
|
switch tt {
|
||||||
|
case xml.ErrorToken:
|
||||||
|
return Unknown, nil
|
||||||
|
|
||||||
|
case xml.StartTagToken:
|
||||||
|
tag := strings.ToLower(string(l.Text()))
|
||||||
|
if tag == "svg" || tag == "svg:svg" {
|
||||||
|
return SVG, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
imagetype_new/type.go
Normal file
121
imagetype_new/type.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package imagetype_new
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type represents an image type
|
||||||
|
type (
|
||||||
|
// Type represents an image type.
|
||||||
|
Type int
|
||||||
|
)
|
||||||
|
|
||||||
|
// Supported image types
|
||||||
|
var (
|
||||||
|
// Unknown is a reserved type, it has index 0. We guarantee that index 0 won't be used
|
||||||
|
// for any other type. This way, Unknown is a zero value for Type.
|
||||||
|
Unknown Type = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mime returns the MIME type for the image type.
|
||||||
|
func (t Type) Mime() string {
|
||||||
|
desc := GetTypeDesc(t)
|
||||||
|
if desc != nil {
|
||||||
|
return desc.Mime
|
||||||
|
}
|
||||||
|
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the image type.
|
||||||
|
func (t Type) String() string {
|
||||||
|
desc := GetTypeDesc(t)
|
||||||
|
if desc != nil {
|
||||||
|
return desc.String
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ext returns the file extension for the image type.
|
||||||
|
func (t Type) Ext() string {
|
||||||
|
desc := GetTypeDesc(t)
|
||||||
|
if desc != nil {
|
||||||
|
return desc.Ext
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for Type.
|
||||||
|
func (t Type) MarshalJSON() ([]byte, error) {
|
||||||
|
s := t.String()
|
||||||
|
if s == "" {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Appendf(nil, "%q", s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsVector checks if the image type is a vector format.
|
||||||
|
func (t Type) IsVector() bool {
|
||||||
|
desc := GetTypeDesc(t)
|
||||||
|
if desc != nil {
|
||||||
|
return desc.IsVector
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsAlpha checks if the image type supports alpha transparency.
|
||||||
|
func (t Type) SupportsAlpha() bool {
|
||||||
|
desc := GetTypeDesc(t)
|
||||||
|
if desc != nil {
|
||||||
|
return desc.SupportsAlpha
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsAnimationLoad checks if the image type supports animation.
|
||||||
|
func (t Type) SupportsAnimationLoad() bool {
|
||||||
|
desc := GetTypeDesc(t)
|
||||||
|
if desc != nil {
|
||||||
|
return desc.SupportsAnimationLoad
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsAnimationSave checks if the image type supports saving animations.
|
||||||
|
func (t Type) SupportsAnimationSave() bool {
|
||||||
|
desc := GetTypeDesc(t)
|
||||||
|
if desc != nil {
|
||||||
|
return desc.SupportsAnimationSave
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsColourProfile checks if the image type supports colour profiles.
|
||||||
|
func (t Type) SupportsColourProfile() bool {
|
||||||
|
desc := GetTypeDesc(t)
|
||||||
|
if desc != nil {
|
||||||
|
return desc.SupportsColourProfile
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsQuality checks if the image type supports quality adjustments.
|
||||||
|
func (t Type) SupportsQuality() bool {
|
||||||
|
desc := GetTypeDesc(t)
|
||||||
|
if desc != nil {
|
||||||
|
return desc.SupportsQuality
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsThumbnail checks if the image type supports thumbnails.
|
||||||
|
func (t Type) SupportsThumbnail() bool {
|
||||||
|
desc := GetTypeDesc(t)
|
||||||
|
if desc != nil {
|
||||||
|
return desc.SupportsThumbnail
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
59
imagetype_new/type_test.go
Normal file
59
imagetype_new/type_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package imagetype_new
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultTypesRegistered(t *testing.T) {
|
||||||
|
// Test that all default types are properly registered by init()
|
||||||
|
defaultTypes := []Type{
|
||||||
|
JPEG, JXL, PNG, WEBP, GIF, ICO, SVG, HEIC, AVIF, BMP, TIFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, typ := range defaultTypes {
|
||||||
|
t.Run(typ.String(), func(t *testing.T) {
|
||||||
|
desc := GetTypeDesc(typ)
|
||||||
|
require.NotNil(t, desc)
|
||||||
|
|
||||||
|
// Verify that the description has non-empty fields
|
||||||
|
require.NotEmpty(t, desc.String)
|
||||||
|
require.NotEmpty(t, desc.Ext)
|
||||||
|
require.NotEqual(t, "application/octet-stream", desc.Mime)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetect(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
file string
|
||||||
|
want Type
|
||||||
|
}{
|
||||||
|
{"JPEG", "../testdata/test-images/jpg/jpg.jpg", JPEG},
|
||||||
|
{"JXL", "../testdata/test-images/jxl/jxl.jxl", JXL},
|
||||||
|
{"PNG", "../testdata/test-images/png/png.png", PNG},
|
||||||
|
{"WEBP", "../testdata/test-images/webp/webp.webp", WEBP},
|
||||||
|
{"GIF", "../testdata/test-images/gif/gif.gif", GIF},
|
||||||
|
{"ICO", "../testdata/test-images/ico/png-256x256.ico", ICO},
|
||||||
|
{"SVG", "../testdata/test-images/svg/svg.svg", SVG},
|
||||||
|
{"HEIC", "../testdata/test-images/heif/heif.heif", HEIC},
|
||||||
|
{"BMP", "../testdata/test-images/bmp/24-bpp.bmp", BMP},
|
||||||
|
{"TIFF", "../testdata/test-images/tiff/tiff.tiff", TIFF},
|
||||||
|
{"SVG", "../testdata/test-images/svg/svg.svg", SVG},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
f, err := os.Open(tt.file)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
got, err := Detect(f)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -41,7 +41,7 @@ func buildRouter() *router.Router {
|
|||||||
func startServer(cancel context.CancelFunc) (*http.Server, error) {
|
func startServer(cancel context.CancelFunc) (*http.Server, error) {
|
||||||
l, err := reuseport.Listen(config.Network, config.Bind)
|
l, err := reuseport.Listen(config.Network, config.Bind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Can't start server: %s", err)
|
return nil, fmt.Errorf("can't start server: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.MaxClients > 0 {
|
if config.MaxClients > 0 {
|
||||||
|
Reference in New Issue
Block a user