feat: image variants per image

This commit is contained in:
fsylvain committed 2026-10-08 19:57:51 +02:00
1 parent f567866161
commit 951fef18ba
92 files changed
+8695 -413

No files matched your search

+5
View File
@@ -82,6 +82,8 @@ const (
ActionVariantEnable = "media.variant_enable"
ActionVariantDisable = "media.variant_disable"
ActionVariantPurge = "media.variant_purge"
ActionVariantGenerateAll = "media.variant_generate_all"
ActionVariantPurgeImage = "media.variant_purge_image"
ActionUserRegister = "user.register"
ActionUserVerify = "user.verify"
ActionUserUpdateProfile = "user.update_profile"
@@ -121,6 +123,9 @@ const (
ParamKind = "kind"
ParamPurgedFiles = "purged_files"
ParamPurgedBytes = "purged_bytes"
ParamGenerated = "generated"
ParamFailed = "failed"
ParamCauses = "causes"
ParamWidth = "width"
ParamHeight = "height"
ParamFit = "fit"
+39 -11
View File
@@ -99,10 +99,11 @@ func StoredName(imagePath string) string {
type Sort string
const (
SortNewest Sort = "newest"
SortOldest Sort = "oldest"
SortName Sort = "name"
SortLargest Sort = "largest"
SortNewest Sort = "newest"
SortOldest Sort = "oldest"
SortName Sort = "name"
SortLargest Sort = "largest"
SortTotalSize Sort = "total_size"
)
type Filter string
@@ -113,13 +114,22 @@ const (
FilterDuplicates Filter = "duplicates"
)
type VariantsFilter string
const (
VariantsAny VariantsFilter = ""
VariantsWith VariantsFilter = "with"
VariantsWithout VariantsFilter = "without"
)
type ListQuery struct {
Search string
Hash string
Filter Filter
Sort Sort
Limit int
Cursor string
Search string
Hash string
Filter Filter
Variants VariantsFilter
Sort Sort
Limit int
Cursor string
}
func NewListQuery() ListQuery {
@@ -133,7 +143,12 @@ func (q ListQuery) Validate() error {
return ErrInvalidQuery
}
switch q.Sort {
case SortNewest, SortOldest, SortName, SortLargest:
case SortNewest, SortOldest, SortName, SortLargest, SortTotalSize:
default:
return ErrInvalidQuery
}
switch q.Variants {
case VariantsAny, VariantsWith, VariantsWithout:
default:
return ErrInvalidQuery
}
@@ -175,6 +190,19 @@ func SortImages(images []Image, order Sort) {
})
}
func SortImagesByTotalSize(images []Image, totals map[string]VariantFileTotals) {
totalOf := func(img Image) int64 {
return img.Size + totals[StoredName(img.Path)].Bytes
}
sort.SliceStable(images, func(i, j int) bool {
a, b := images[i], images[j]
if totalOf(a) != totalOf(b) {
return totalOf(a) > totalOf(b)
}
return a.ID < b.ID
})
}
type Counts struct {
All int
DuplicateGroups int
+63
View File
@@ -4,7 +4,9 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
"math"
"regexp"
"slices"
"strconv"
"strings"
"time"
@@ -17,6 +19,8 @@ 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")
var ErrVariantSourceUnsupported = errors.New("variants are not generated for this format")
var ErrVariantSourceUnreadable = errors.New("the source image cannot be read")
type VariantFit string
@@ -220,3 +224,62 @@ type VariantFileTotals struct {
Files int
Bytes int64
}
func OutputSize(sourceWidth int, sourceHeight int, settings VariantSettings) (int, int) {
if sourceWidth < 1 || sourceHeight < 1 {
return 0, 0
}
if settings.Width == nil || settings.Height == nil {
return proportionalOutput(sourceWidth, sourceHeight, settings)
}
boxWidth, boxHeight := *settings.Width, *settings.Height
switch settings.Fit {
case FitContain:
return boxWidth, boxHeight
case FitInside:
return scaledOutput(sourceWidth, sourceHeight, math.Min(1, math.Min(ratio(boxWidth, sourceWidth), ratio(boxHeight, sourceHeight))))
default:
shrink := math.Min(1, math.Min(ratio(sourceWidth, boxWidth), ratio(sourceHeight, boxHeight)))
return scaledOutput(boxWidth, boxHeight, shrink)
}
}
func proportionalOutput(sourceWidth int, sourceHeight int, settings VariantSettings) (int, int) {
scale := 1.0
if settings.Width != nil {
scale = math.Min(scale, ratio(*settings.Width, sourceWidth))
}
if settings.Height != nil {
scale = math.Min(scale, ratio(*settings.Height, sourceHeight))
}
return scaledOutput(sourceWidth, sourceHeight, scale)
}
func scaledOutput(width int, height int, factor float64) (int, int) {
return scaledLength(width, factor), scaledLength(height, factor)
}
func scaledLength(length int, factor float64) int {
return max(1, int(math.Round(float64(length)*factor)))
}
func ratio(numerator int, denominator int) float64 {
return float64(numerator) / float64(denominator)
}
var sourceContentTypeFormats = map[string]VariantFormat{
"image/jpeg": FormatJPEG,
"image/png": FormatPNG,
"image/webp": FormatWebP,
"image/gif": FormatPNG,
}
func OutputFormat(format VariantFormat, sourceContentType string, encodable []VariantFormat) VariantFormat {
if format == FormatOriginal {
format = sourceContentTypeFormats[sourceContentType]
}
if slices.Contains(encodable, format) {
return format
}
return FormatPNG
}
@@ -17,6 +17,7 @@ type VariantFileRepository interface {
ListBySource(sourceName string) ([]VariantFile, error)
Delete(id uint32) error
Totals() (map[string]VariantFileTotals, error)
TotalsBySource() (map[string]VariantFileTotals, error)
}
type RenderedVariant struct {