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 {
@@ -13,43 +13,26 @@ type layout struct {
}
func layoutFor(source image.Point, settings domain.VariantSettings) layout {
outputWidth, outputHeight := domain.OutputSize(source.X, source.Y, settings)
output := image.Pt(outputWidth, outputHeight)
if settings.Width == nil || settings.Height == nil {
return proportionalLayout(source, settings)
return wholeSourceLayout(source, output)
}
box := image.Pt(*settings.Width, *settings.Height)
switch settings.Fit {
case domain.FitContain:
return containLayout(source, box)
return containLayout(source, output)
case domain.FitInside:
return insideLayout(source, box)
return wholeSourceLayout(source, output)
default:
return coverLayout(source, box)
return coverLayout(source, output)
}
}
func proportionalLayout(source image.Point, settings domain.VariantSettings) layout {
scale := 1.0
if settings.Width != nil {
scale = math.Min(scale, ratio(*settings.Width, source.X))
}
if settings.Height != nil {
scale = math.Min(scale, ratio(*settings.Height, source.Y))
}
return wholeSourceLayout(source, scaled(source, scale))
}
func insideLayout(source image.Point, box image.Point) layout {
scale := math.Min(1, math.Min(ratio(box.X, source.X), ratio(box.Y, source.Y)))
return wholeSourceLayout(source, scaled(source, scale))
}
func wholeSourceLayout(source image.Point, output image.Point) layout {
return layout{canvas: output, source: image.Rectangle{Max: source}, content: image.Rectangle{Max: output}}
}
func coverLayout(source image.Point, box image.Point) layout {
shrink := math.Min(1, math.Min(ratio(source.X, box.X), ratio(source.Y, box.Y)))
output := scaled(box, shrink)
func coverLayout(source image.Point, output image.Point) layout {
return layout{canvas: output, source: centeredCrop(source, output), content: image.Rectangle{Max: output}}
}
@@ -70,6 +70,26 @@ func (v *VariantFileRepository) Delete(id uint32) error {
return v.db.Delete(&entity.ImageVariantFile{}, id).Error
}
func (v *VariantFileRepository) TotalsBySource() (map[string]domain.VariantFileTotals, error) {
var rows []struct {
SourceName string
Files int
Bytes int64
}
err := v.db.Model(&entity.ImageVariantFile{}).
Select("source_name, COUNT(*) AS files, COALESCE(SUM(size), 0) AS bytes").
Group("source_name").
Scan(&rows).Error
if err != nil {
return nil, err
}
totals := make(map[string]domain.VariantFileTotals, len(rows))
for _, row := range rows {
totals[row.SourceName] = domain.VariantFileTotals{Files: row.Files, Bytes: row.Bytes}
}
return totals, nil
}
func (v *VariantFileRepository) Totals() (map[string]domain.VariantFileTotals, error) {
var rows []struct {
VariantName string
@@ -0,0 +1,100 @@
package useCases
import (
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/image"
"context"
"slices"
)
type GenerateAllResult struct {
Generated int
Failed int
Failures []VariantFailure
Items []ImageVariantView
}
type GenerateAllImageVariantsUseCase struct {
images image.Repository
variants image.VariantRepository
files image.VariantFileRepository
renderer image.VariantRenderer
generator *ServeVariantUseCase
recorder audit.Recorder
}
func NewGenerateAllImageVariantsUseCase(images image.Repository, variants image.VariantRepository, files image.VariantFileRepository, renderer image.VariantRenderer, generator *ServeVariantUseCase, recorder audit.Recorder) *GenerateAllImageVariantsUseCase {
return &GenerateAllImageVariantsUseCase{images, variants, files, renderer, generator, recorder}
}
func (u *GenerateAllImageVariantsUseCase) Generate(ctx context.Context, id uint32) (GenerateAllResult, error) {
img, err := u.images.Get(id)
if err != nil {
return GenerateAllResult{}, err
}
if isVectorSource(img) {
return GenerateAllResult{}, image.ErrVariantSourceUnsupported
}
enabled, err := enabledVariants(u.variants)
if err != nil {
return GenerateAllResult{}, err
}
storedName := image.StoredName(img.Path)
rows, err := currentFiles(u.files, storedName)
if err != nil {
return GenerateAllResult{}, err
}
result := GenerateAllResult{Failures: []VariantFailure{}}
var firstFailure error
for _, variant := range enabled {
if isCurrent(variant, rows) {
continue
}
if _, err := u.generator.Generate(ctx, variant, storedName); err != nil {
result.Failed++
result.Failures = append(result.Failures, VariantFailure{Name: variant.Name, Cause: generationCause(err)})
if firstFailure == nil {
firstFailure = err
}
continue
}
result.Generated++
}
refreshed, err := currentFiles(u.files, storedName)
if err != nil {
return GenerateAllResult{}, err
}
result.Items = viewsOf(enabled, img, refreshed, u.renderer.EncodableFormats())
u.record(ctx, img, result, firstFailure)
return result, nil
}
func (u *GenerateAllImageVariantsUseCase) record(ctx context.Context, img image.Image, result GenerateAllResult, firstFailure error) {
if result.Generated == 0 && result.Failed == 0 {
return
}
event := audit.NewEvent(audit.ActionVariantGenerateAll, audit.CategoryMedia)
event.Target = imageTarget(img)
event.Params = map[string]any{
audit.ParamGenerated: result.Generated,
audit.ParamFailed: result.Failed,
audit.ParamCauses: distinctCauses(result.Failures),
}
if result.Generated == 0 {
failFrom(ctx, u.recorder, event, reasonFor(firstFailure))
return
}
u.recorder.Record(ctx, event)
}
func distinctCauses(failures []VariantFailure) []string {
causes := []string{}
for _, failure := range failures {
if !slices.Contains(causes, failure.Cause) {
causes = append(causes, failure.Cause)
}
}
return causes
}
@@ -0,0 +1,44 @@
package useCases
import (
"RenewCMS/internal/domain/image"
"context"
)
type GenerateImageVariantUseCase struct {
images image.Repository
variants image.VariantRepository
files image.VariantFileRepository
renderer image.VariantRenderer
generator *ServeVariantUseCase
}
func NewGenerateImageVariantUseCase(images image.Repository, variants image.VariantRepository, files image.VariantFileRepository, renderer image.VariantRenderer, generator *ServeVariantUseCase) *GenerateImageVariantUseCase {
return &GenerateImageVariantUseCase{images, variants, files, renderer, generator}
}
func (u *GenerateImageVariantUseCase) Generate(ctx context.Context, id uint32, name string) (ImageVariantView, error) {
img, err := u.images.Get(id)
if err != nil {
return ImageVariantView{}, err
}
variant, err := u.variants.Get(name)
if err != nil {
return ImageVariantView{}, err
}
if !variant.Enabled || variant.Name != name {
return ImageVariantView{}, image.ErrVariantNotFound
}
if isVectorSource(img) {
return ImageVariantView{}, image.ErrVariantSourceUnsupported
}
storedName := image.StoredName(img.Path)
if _, err := u.generator.Generate(ctx, variant, storedName); err != nil {
return ImageVariantView{}, err
}
rows, err := currentFiles(u.files, storedName)
if err != nil {
return ImageVariantView{}, err
}
return viewOf(variant, img, rows, u.renderer.EncodableFormats()), nil
}
@@ -0,0 +1,37 @@
package useCases
import (
"RenewCMS/internal/domain/image"
"context"
"log"
)
type GenerateVariantsOnUploadUseCase struct {
variants image.VariantRepository
generator *ServeVariantUseCase
logger *log.Logger
}
func NewGenerateVariantsOnUploadUseCase(variants image.VariantRepository, generator *ServeVariantUseCase, logger *log.Logger) *GenerateVariantsOnUploadUseCase {
return &GenerateVariantsOnUploadUseCase{variants, generator, logger}
}
func (u *GenerateVariantsOnUploadUseCase) Generate(ctx context.Context, img image.Image) {
if isVectorSource(img) {
return
}
enabled, err := enabledVariants(u.variants)
if err != nil {
u.logger.Printf("listing variants to generate on upload failed: image=%d: %v", img.ID, err)
return
}
storedName := image.StoredName(img.Path)
for _, variant := range enabled {
if !variant.GenerateOnUpload {
continue
}
if _, err := u.generator.Generate(ctx, variant, storedName); err != nil {
u.logger.Printf("generating variant on upload failed: variant=%s image=%d: %v", variant.Name, img.ID, err)
}
}
}
@@ -0,0 +1,33 @@
package useCases
import "RenewCMS/internal/domain/image"
type ListImageVariantsUseCase struct {
images image.Repository
variants image.VariantRepository
files image.VariantFileRepository
renderer image.VariantRenderer
}
func NewListImageVariantsUseCase(images image.Repository, variants image.VariantRepository, files image.VariantFileRepository, renderer image.VariantRenderer) *ListImageVariantsUseCase {
return &ListImageVariantsUseCase{images, variants, files, renderer}
}
func (u *ListImageVariantsUseCase) List(id uint32) (ImageVariantList, error) {
img, err := u.images.Get(id)
if err != nil {
return ImageVariantList{}, err
}
if isVectorSource(img) {
return ImageVariantList{Unsupported: unsupportedSVG, Items: []ImageVariantView{}}, nil
}
enabled, err := enabledVariants(u.variants)
if err != nil {
return ImageVariantList{}, err
}
rows, err := currentFiles(u.files, image.StoredName(img.Path))
if err != nil {
return ImageVariantList{}, err
}
return ImageVariantList{Items: viewsOf(enabled, img, rows, u.renderer.EncodableFormats())}, nil
}
+48 -9
View File
@@ -16,6 +16,9 @@ type MediaItem struct {
UploadedBy string
UsedIn int
Duplicate bool
VariantCount int
VariantSize int64
}
type MediaCounts struct {
@@ -34,10 +37,11 @@ type ListMediaUseCase struct {
imageRepository image.Repository
articleRepository article.Repository
userRepository user.Repository
variantFiles image.VariantFileRepository
}
func NewListMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, userRepository user.Repository) *ListMediaUseCase {
return &ListMediaUseCase{imageRepository, articleRepository, userRepository}
func NewListMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, userRepository user.Repository, variantFiles image.VariantFileRepository) *ListMediaUseCase {
return &ListMediaUseCase{imageRepository, articleRepository, userRepository, variantFiles}
}
func (l *ListMediaUseCase) List(query image.ListQuery) (MediaPage, error) {
@@ -61,16 +65,20 @@ func (l *ListMediaUseCase) List(query image.ListQuery) (MediaPage, error) {
if err != nil {
return MediaPage{}, err
}
variantTotals, err := l.variantFiles.TotalsBySource()
if err != nil {
return MediaPage{}, err
}
images, err := l.imageRepository.List(image.SearchQuery{Search: strings.TrimSpace(query.Search), Hash: query.Hash})
if err != nil {
return MediaPage{}, err
}
image.SortImages(images, query.Sort)
sortImages(images, query.Sort, variantTotals)
matching := make([]image.Image, 0, len(images))
for _, img := range images {
if matchesFilter(query.Filter, usage(img), duplicateHashes[img.SHA256] && img.SHA256 != "") {
if matchesVariants(query.Variants, variantTotals[image.StoredName(img.Path)].Files) && matchesFilter(query.Filter, usage(img), duplicateHashes[img.SHA256] && img.SHA256 != "") {
matching = append(matching, img)
}
}
@@ -84,11 +92,14 @@ func (l *ListMediaUseCase) List(query image.ListQuery) (MediaPage, error) {
if err != nil {
return MediaPage{}, err
}
own := variantTotals[image.StoredName(img.Path)]
items = append(items, MediaItem{
Image: img,
UploadedBy: uploadedBy,
UsedIn: usage(img),
Duplicate: img.SHA256 != "" && duplicateHashes[img.SHA256],
Image: img,
UploadedBy: uploadedBy,
UsedIn: usage(img),
Duplicate: img.SHA256 != "" && duplicateHashes[img.SHA256],
VariantCount: own.Files,
VariantSize: own.Bytes,
})
}
@@ -118,7 +129,16 @@ func (l *ListMediaUseCase) Describe(img image.Image) (MediaItem, error) {
duplicate = len(sameContent) > 1
}
return MediaItem{Image: img, UploadedBy: uploadedBy, UsedIn: len(articles), Duplicate: duplicate}, nil
rows, err := l.variantFiles.ListBySource(image.StoredName(img.Path))
if err != nil {
return MediaItem{}, err
}
var variantSize int64
for _, row := range rows {
variantSize += row.Size
}
return MediaItem{Image: img, UploadedBy: uploadedBy, UsedIn: len(articles), Duplicate: duplicate, VariantCount: len(rows), VariantSize: variantSize}, nil
}
func (l *ListMediaUseCase) usageCounter() (func(image.Image) int, error) {
@@ -168,6 +188,25 @@ func (l *ListMediaUseCase) counts(usage func(image.Image) int) (MediaCounts, err
return MediaCounts{All: libraryCounts.All, Unused: unused, Duplicates: libraryCounts.DuplicateGroups}, nil
}
func sortImages(images []image.Image, order image.Sort, totals map[string]image.VariantFileTotals) {
if order == image.SortTotalSize {
image.SortImagesByTotalSize(images, totals)
return
}
image.SortImages(images, order)
}
func matchesVariants(filter image.VariantsFilter, variantFiles int) bool {
switch filter {
case image.VariantsWith:
return variantFiles > 0
case image.VariantsWithout:
return variantFiles == 0
default:
return true
}
}
func matchesFilter(filter image.Filter, usedIn int, duplicate bool) bool {
switch filter {
case image.FilterUnused:
@@ -0,0 +1,52 @@
package useCases
import (
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/image"
"context"
)
type PurgeImageVariantUseCase struct {
images image.Repository
variants image.VariantRepository
files image.VariantFileRepository
cache variantCache
recorder audit.Recorder
}
func NewPurgeImageVariantUseCase(images image.Repository, variants image.VariantRepository, files image.VariantFileRepository, storage image.Storage, recorder audit.Recorder) *PurgeImageVariantUseCase {
return &PurgeImageVariantUseCase{images, variants, files, variantCache{files, storage}, recorder}
}
func (u *PurgeImageVariantUseCase) Purge(ctx context.Context, id uint32, name string) (VariantPurgeResult, error) {
img, err := u.images.Get(id)
if err != nil {
return VariantPurgeResult{}, err
}
variant, err := u.variants.Get(name)
if err != nil {
return VariantPurgeResult{}, err
}
if variant.Name != name {
return VariantPurgeResult{}, image.ErrVariantNotFound
}
event := audit.NewEvent(audit.ActionVariantPurgeImage, audit.CategoryMedia)
event.Target = imageTarget(img)
row, found, err := u.files.Find(name, image.StoredName(img.Path))
if err != nil {
recordFailure(ctx, u.recorder, event, err)
return VariantPurgeResult{}, err
}
if !found {
return VariantPurgeResult{}, nil
}
purged, err := u.cache.purge([]image.VariantFile{row})
if err != nil {
recordFailure(ctx, u.recorder, event, err)
return VariantPurgeResult{}, err
}
event.Params = variantPurgeParams(purged)
u.recorder.Record(ctx, event)
return purged, nil
}
@@ -5,6 +5,7 @@ import (
"bytes"
"context"
"errors"
"fmt"
"io"
"path"
"slices"
@@ -263,7 +264,7 @@ func (u *ServeVariantUseCase) render(ctx context.Context, settings image.Variant
func (u *ServeVariantUseCase) checkDeclaredPixels(data []byte) error {
width, height, err := u.renderer.DeclaredSize(bytes.NewReader(data))
if err != nil {
return err
return fmt.Errorf("%w: %v", image.ErrVariantSourceUnreadable, err)
}
if int64(width)*int64(height) > u.maxSourcePixels {
return image.ErrSourceTooLarge
@@ -0,0 +1,33 @@
package useCases
import (
"RenewCMS/internal/domain/image"
"context"
)
func (u *ServeVariantUseCase) Generate(ctx context.Context, variant image.Variant, storedName string) (image.VariantFile, error) {
source, err := u.storage.Open(storedName)
if err != nil {
return image.VariantFile{}, err
}
defer source.Content.Close()
if servedAsIs(storedName, source.Content) {
return image.VariantFile{}, image.ErrVariantSourceUnsupported
}
if source.Size > u.maxSourceBytes {
return image.VariantFile{}, image.ErrSourceTooLarge
}
settings := variant.Settings()
fingerprint := settings.Fingerprint()
served, existing, hit, err := u.lookupCached(variant.Name, storedName, fingerprint, image.VariantETag(settings, storedName))
if err != nil {
return image.VariantFile{}, err
}
if hit {
_ = served.Content.Close()
return existing, nil
}
return u.generated(ctx, variant, storedName, fingerprint, existing)
}
@@ -29,6 +29,9 @@ var failureReasons = []failureReason{
{image.ErrInvalidMergeRequest, "The merge request is not valid."},
{image.ErrMergeCopyMissing, "One of the copies no longer exists."},
{image.ErrMergeHashMismatch, "The images are not identical copies."},
{image.ErrSourceTooLarge, "The source image is too large to generate this variant."},
{image.ErrVariantSourceUnsupported, "Variants aren't generated for this format."},
{image.ErrVariantSourceUnreadable, "The source image can't be read, so this variant can't be generated."},
{image.ErrVariantExists, "A variant with that name already exists."},
{purge.ErrPurgeConflict, "The library changed since the preview. Review it again."},
{purge.ErrPurgeBusy, "A purge is already running."},
@@ -0,0 +1,108 @@
package useCases
import (
"RenewCMS/internal/domain/image"
"errors"
"slices"
"strings"
)
const (
svgContentType = "image/svg+xml"
unsupportedSVG = "svg"
causeTooLarge = "too_large"
causeUnsupported = "unsupported_format"
causeFailed = "failed"
variantURLQuery = "?" + image.VariantQueryParameter + "="
)
type ImageVariantView struct {
Variant image.Variant
OutputWidth int
OutputHeight int
OutputFormat image.VariantFormat
Generated bool
Size int64
URL string
}
type ImageVariantList struct {
Unsupported string
Items []ImageVariantView
}
type VariantFailure struct {
Name string
Cause string
}
func isVectorSource(img image.Image) bool {
return img.ContentType == svgContentType
}
func enabledVariants(variants image.VariantRepository) ([]image.Variant, error) {
listed, err := variants.List()
if err != nil {
return nil, err
}
enabled := make([]image.Variant, 0, len(listed))
for _, variant := range listed {
if variant.Enabled {
enabled = append(enabled, variant)
}
}
slices.SortFunc(enabled, func(a, b image.Variant) int { return strings.Compare(a.Name, b.Name) })
return enabled, nil
}
func currentFiles(files image.VariantFileRepository, storedName string) (map[string]image.VariantFile, error) {
rows, err := files.ListBySource(storedName)
if err != nil {
return nil, err
}
byVariant := make(map[string]image.VariantFile, len(rows))
for _, row := range rows {
byVariant[row.VariantName] = row
}
return byVariant, nil
}
func isCurrent(variant image.Variant, rows map[string]image.VariantFile) bool {
row, found := rows[variant.Name]
return found && row.Fingerprint == variant.Settings().Fingerprint()
}
func viewOf(variant image.Variant, img image.Image, rows map[string]image.VariantFile, encodable []image.VariantFormat) ImageVariantView {
width, height := image.OutputSize(img.Width, img.Height, variant.Settings())
view := ImageVariantView{
Variant: variant,
OutputWidth: width,
OutputHeight: height,
OutputFormat: image.OutputFormat(variant.Format, img.ContentType, encodable),
URL: image.PublicPath(image.StoredName(img.Path)) + variantURLQuery + variant.Name,
}
if isCurrent(variant, rows) {
view.Generated = true
view.Size = rows[variant.Name].Size
}
return view
}
func viewsOf(variants []image.Variant, img image.Image, rows map[string]image.VariantFile, encodable []image.VariantFormat) []ImageVariantView {
views := make([]ImageVariantView, 0, len(variants))
for _, variant := range variants {
views = append(views, viewOf(variant, img, rows, encodable))
}
return views
}
func generationCause(err error) string {
switch {
case errors.Is(err, image.ErrSourceTooLarge):
return causeTooLarge
case errors.Is(err, image.ErrVariantSourceUnreadable):
return causeUnsupported
default:
return causeFailed
}
}