feat: image variants engine
This commit is contained in:
1 parent
f0845de643
commit
f1d56ef50e
80 files changed
+7312
-84
No files matched your search
@@ -0,0 +1,222 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user