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:
Victor Sokolov
2025-08-13 11:29:06 +02:00
committed by GitHub
parent e14e054471
commit 2cb9aea995
11 changed files with 889 additions and 75 deletions

View File

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

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

View File

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

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

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

View File

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