mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-09-30 14:06:48 +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
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/imath"
|
||||
)
|
||||
|
||||
type Reader struct {
|
||||
r io.Reader
|
||||
buf *bytes.Buffer
|
||||
cur int
|
||||
// ReadPeeker is an interface that combines io.Reader and a method to peek at the next n bytes
|
||||
type ReadPeeker interface {
|
||||
io.Reader
|
||||
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{
|
||||
r: r,
|
||||
buf: buf,
|
||||
buf: nil,
|
||||
}
|
||||
return &br
|
||||
}
|
||||
|
||||
// Read reads data into p from the buffered reader.
|
||||
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
|
||||
}
|
||||
|
||||
n := copy(p, br.buf.Bytes()[br.cur:])
|
||||
br.cur += 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
|
||||
n := copy(p, br.buf[br.pos:])
|
||||
br.pos += n
|
||||
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) {
|
||||
if n < 0 {
|
||||
return []byte{}, bufio.ErrNegativeCount
|
||||
}
|
||||
if n == 0 {
|
||||
return []byte{}, nil
|
||||
err := br.fetch(br.pos + n)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := br.fill(br.cur + n); err != nil {
|
||||
return []byte{}, err
|
||||
// Return slice of buffered data without advancing position
|
||||
available := br.buf[br.pos:]
|
||||
if len(available) == 0 && err == io.EOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if n > br.buf.Len()-br.cur {
|
||||
return br.buf.Bytes()[br.cur:], io.EOF
|
||||
}
|
||||
|
||||
return br.buf.Bytes()[br.cur : br.cur+n], nil
|
||||
return available[:min(len(available), n)], nil
|
||||
}
|
||||
|
||||
func (br *Reader) Flush() error {
|
||||
_, err := br.buf.ReadFrom(br.r)
|
||||
return err
|
||||
// Rewind seeks buffer to the beginning
|
||||
func (br *Reader) Rewind() {
|
||||
br.pos = 0
|
||||
}
|
||||
|
||||
func (br *Reader) fill(need int) error {
|
||||
n := need - br.buf.Len()
|
||||
if n <= 0 {
|
||||
// fetch ensures the buffer contains at least 'need' bytes
|
||||
func (br *Reader) fetch(need int) error {
|
||||
if need-len(br.buf) <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
n = imath.Max(4096, n)
|
||||
|
||||
if _, err := br.buf.ReadFrom(io.LimitReader(br.r, int64(n))); err != nil {
|
||||
b := make([]byte, need)
|
||||
n, err := io.ReadFull(br.r, b)
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
return err
|
||||
}
|
||||
|
||||
// Nothing was read, it's EOF
|
||||
if br.cur == br.buf.Len() {
|
||||
return io.EOF
|
||||
}
|
||||
// append only those which we read in fact
|
||||
br.buf = append(br.buf, b[:n]...)
|
||||
|
||||
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
|
||||
|
||||
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 {
|
||||
// JPEG has two names, we should use only the full one
|
||||
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) {
|
||||
l, err := reuseport.Listen(config.Network, config.Bind)
|
||||
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 {
|
||||
|
Reference in New Issue
Block a user