Files
imgproxy/imagetype/registry.go
2025-08-16 02:28:56 +06:00

196 lines
5.5 KiB
Go

package imagetype
import (
"io"
"slices"
"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)
// detector is a struct that holds a detection function and its priority
type detector struct {
priority int // priority of the detector, lower is better
fn DetectFunc // function that detects the image type
}
// Registry holds the type registry
type registry struct {
detectors []detector
types []*TypeDesc
typesByName map[string]Type // maps type string to Type
}
// globalRegistry is the default registry instance
var globalRegistry = NewRegistry()
// NewRegistry creates a new image type registry.
func NewRegistry() *registry {
return &registry{
types: nil,
typesByName: make(map[string]Type),
}
}
// 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)
}
// GetTypes returns all registered image types.
func GetTypeByName(name string) (Type, bool) {
return globalRegistry.GetTypeByName(name)
}
// 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)
typ := Type(len(r.types)) // 0 is unknown
r.typesByName[desc.String] = typ
// NOTE: this is a special case for JPEG. The problem is that JPEG is using
// several alternative extensions and processing_options.go is using extension to
// find a type by key. There might be not the only case (e.g. ".tif/.tiff").
// We need to handle this case in a more generic way.
if desc.String == "jpeg" {
// JPEG is a special case, we need to alias it
r.typesByName["jpg"] = typ
}
return typ
}
// 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]
}
// GetTypeByName returns Type by it's name
func (r *registry) GetTypeByName(name string) (Type, bool) {
typ, ok := r.typesByName[name]
return typ, ok
}
// RegisterDetector registers a custom detector function
// Detectors are tried in the order they were registered
func RegisterDetector(priority int, fn DetectFunc) {
globalRegistry.RegisterDetector(priority, fn)
}
// 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(priority int, fn DetectFunc) {
r.detectors = append(r.detectors, detector{
priority: priority,
fn: fn,
})
// Sort detectors by priority.
// We don't expect a huge number of detectors, and detectors should be registered at startup,
// so sorting them at each registration is okay.
slices.SortStableFunc(r.detectors, func(a, b detector) int {
return a.priority - b.priority
})
}
// RegisterMagicBytes registers magic bytes for a specific image type on this registry instance
func (r *registry) RegisterMagicBytes(typ Type, signature ...[]byte) {
r.RegisterDetector(-1, 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 _, d := range r.detectors {
br.Rewind()
typ, err := d.fn(br)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return Unknown, newTypeDetectionError(err)
}
if 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
}