package image import ( "crypto/sha256" "encoding/hex" "errors" "regexp" "strconv" "strings" "time" ) var ErrVariantNotFound = errors.New("variant not found") var ErrVariantExists = errors.New("variant already exists") var ErrVariantInvalid = errors.New("variant is not valid") var ErrVariantFormatUnavailable = errors.New("variant format cannot be encoded") var ErrUnknownVariant = errors.New("unknown variant") var ErrUnsupportedParameter = errors.New("unsupported parameter") var ErrSourceTooLarge = errors.New("source image is too large to generate this variant") type VariantFit string const ( FitCover VariantFit = "cover" FitContain VariantFit = "contain" FitInside VariantFit = "inside" ) type VariantFormat string const ( FormatOriginal VariantFormat = "original" FormatJPEG VariantFormat = "jpeg" FormatPNG VariantFormat = "png" FormatWebP VariantFormat = "webp" ) const ( MinVariantNameLength = 2 MaxVariantNameLength = 32 MinVariantDimension = 1 MaxVariantDimension = 8000 MinVariantQuality = 1 MaxVariantQuality = 100 DefaultVariantQuality = 82 DefaultVariantFit = FitCover DefaultVariantFormat = FormatOriginal VariantQueryParameter = "v" ) var variantNamePattern = regexp.MustCompile(`^[a-z0-9-]{2,32}$`) var unsupportedQueryParameters = []string{"width", "height", "format", "quality"} func VariantFits() []VariantFit { return []VariantFit{FitCover, FitContain, FitInside} } func VariantFormats() []VariantFormat { return []VariantFormat{FormatOriginal, FormatJPEG, FormatPNG, FormatWebP} } func UnsupportedQueryParameters() []string { return append([]string(nil), unsupportedQueryParameters...) } func IsValidVariantName(name string) bool { return variantNamePattern.MatchString(name) } type VariantSettings struct { Width *int Height *int Fit VariantFit Format VariantFormat Quality *int } func (s VariantSettings) Normalized() (VariantSettings, error) { normalized := s if normalized.Fit == "" { normalized.Fit = DefaultVariantFit } if normalized.Format == "" { normalized.Format = DefaultVariantFormat } if !isKnownFit(normalized.Fit) || !isKnownFormat(normalized.Format) { return VariantSettings{}, ErrVariantInvalid } if !hasValidDimensions(normalized.Width, normalized.Height) { return VariantSettings{}, ErrVariantInvalid } quality, err := normalizedQuality(normalized.Format, normalized.Quality) if err != nil { return VariantSettings{}, err } normalized.Quality = quality return normalized, nil } func (s VariantSettings) Fingerprint() string { parts := []string{ "w=" + DimensionText(s.Width), "h=" + DimensionText(s.Height), "fit=" + string(s.Fit), "format=" + string(s.Format), "q=" + QualityText(s.Quality), } sum := sha256.Sum256([]byte(strings.Join(parts, ";"))) return hex.EncodeToString(sum[:]) } func (s VariantSettings) IsSameAs(other VariantSettings) bool { return s.Fingerprint() == other.Fingerprint() } func isKnownFit(fit VariantFit) bool { for _, known := range VariantFits() { if fit == known { return true } } return false } func isKnownFormat(format VariantFormat) bool { for _, known := range VariantFormats() { if format == known { return true } } return false } func hasValidDimensions(width *int, height *int) bool { if width == nil && height == nil { return false } return isValidDimension(width) && isValidDimension(height) } func isValidDimension(dimension *int) bool { return dimension == nil || (*dimension >= MinVariantDimension && *dimension <= MaxVariantDimension) } func normalizedQuality(format VariantFormat, quality *int) (*int, error) { if format == FormatPNG { return nil, nil } if quality == nil { defaultQuality := DefaultVariantQuality return &defaultQuality, nil } if *quality < MinVariantQuality || *quality > MaxVariantQuality { return nil, ErrVariantInvalid } kept := *quality return &kept, nil } func DimensionText(dimension *int) string { if dimension == nil { return "auto" } return strconv.Itoa(*dimension) } func QualityText(quality *int) string { if quality == nil { return "none" } return strconv.Itoa(*quality) } type Variant struct { Name string `json:"name"` Width *int `json:"width"` Height *int `json:"height"` Fit VariantFit `json:"fit"` Format VariantFormat `json:"format"` Quality *int `json:"quality"` GenerateOnUpload bool `json:"generate_on_upload"` Enabled bool `json:"enabled"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func NewVariant(name string, settings VariantSettings, generateOnUpload bool, enabled bool, now time.Time) (Variant, error) { if !IsValidVariantName(name) { return Variant{}, ErrVariantInvalid } normalized, err := settings.Normalized() if err != nil { return Variant{}, err } variant := Variant{Name: name, GenerateOnUpload: generateOnUpload, Enabled: enabled, CreatedAt: now, UpdatedAt: now} return variant.WithSettings(normalized), nil } func (v Variant) Settings() VariantSettings { return VariantSettings{Width: v.Width, Height: v.Height, Fit: v.Fit, Format: v.Format, Quality: v.Quality} } func (v Variant) WithSettings(settings VariantSettings) Variant { v.Width, v.Height, v.Fit, v.Format, v.Quality = settings.Width, settings.Height, settings.Fit, settings.Format, settings.Quality return v } type VariantFile struct { ID uint32 VariantName string SourceName string CachedName string ContentType string Size int64 Fingerprint string } type VariantFileTotals struct { Files int Bytes int64 }