Skip to content

Commit

Permalink
Thumb: Skip left_224 and right_224 if the original is square #1474
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Mayer <michael@photoprism.app>
  • Loading branch information
lastzero committed May 17, 2024
1 parent 107cb2e commit a54a46b
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 50 deletions.
2 changes: 1 addition & 1 deletion internal/photoprism/index_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func (ind *Index) Labels(jpeg *MediaFile) (results classify.Labels) {

var sizes []thumb.Name

if jpeg.AspectRatio() == 1 {
if jpeg.Square() {
sizes = []thumb.Name{thumb.Tile224}
} else {
sizes = []thumb.Name{thumb.Tile224, thumb.Left224, thumb.Right224}
Expand Down
12 changes: 12 additions & 0 deletions internal/photoprism/mediafile.go
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,18 @@ func (m *MediaFile) AspectRatio() float32 {
return aspectRatio
}

// Square checks if the width and height of this media file are the same.
func (m *MediaFile) Square() bool {
width := m.Width()
height := m.Height()

if width <= 0 || height <= 0 {
return false
}

return width == height
}

// Portrait tests if the image is a portrait.
func (m *MediaFile) Portrait() bool {
return m.Width() < m.Height()
Expand Down
3 changes: 3 additions & 0 deletions internal/photoprism/mediafile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2032,6 +2032,7 @@ func TestMediaFile_AspectRatio(t *testing.T) {

ratio := mediaFile.AspectRatio()
assert.Equal(t, float32(0.75), ratio)
assert.False(t, mediaFile.Square())
})
t.Run("fern_green.jpg", func(t *testing.T) {
conf := config.TestConfig()
Expand All @@ -2044,6 +2045,7 @@ func TestMediaFile_AspectRatio(t *testing.T) {

ratio := mediaFile.AspectRatio()
assert.Equal(t, float32(1), ratio)
assert.True(t, mediaFile.Square())
})
t.Run("elephants.jpg", func(t *testing.T) {
conf := config.TestConfig()
Expand All @@ -2056,6 +2058,7 @@ func TestMediaFile_AspectRatio(t *testing.T) {

ratio := mediaFile.AspectRatio()
assert.Equal(t, float32(1.5), ratio)
assert.False(t, mediaFile.Square())
})
}

Expand Down
12 changes: 6 additions & 6 deletions internal/thumb/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ func (n Name) String() string {

// Names of thumbnail sizes.
const (
Colors Name = "colors"
Tile50 Name = "tile_50"
Tile100 Name = "tile_100"
Tile224 Name = "tile_224"
Tile500 Name = "tile_500"
Tile1080 Name = "tile_1080"
Colors Name = "colors"
Left224 Name = "left_224"
Right224 Name = "right_224"
Tile224 Name = "tile_224"
Fit720 Name = "fit_720"
Tile500 Name = "tile_500"
Tile1080 Name = "tile_1080"
Fit1280 Name = "fit_1280"
Fit1600 Name = "fit_1600"
Fit1920 Name = "fit_1920"
Expand All @@ -45,12 +45,12 @@ var Names = []Name{
Fit1280,
Tile500,
Fit720,
Tile224,
Right224,
Left224,
Colors,
Tile224,
Tile100,
Tile50,
Colors,
}

// Find returns the largest default thumbnail type for the given size limit.
Expand Down
19 changes: 17 additions & 2 deletions internal/thumb/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"github.com/photoprism/photoprism/pkg/fs"
)

type ResampleOption int

const (
ResampleFillCenter ResampleOption = iota
ResampleFillTopLeft
Expand All @@ -25,6 +23,23 @@ var ResampleMethods = map[ResampleOption]string{
ResampleResize: "resize",
}

// ResampleOption represents a thumbnail rendering option.
type ResampleOption int

// Options represents a list of thumbnail rendering options.
type Options []ResampleOption

// Contains checks if the specified option is set.
func (o Options) Contains(option ResampleOption) bool {
for _, v := range o {
if v == option {
return true
}
}

return false
}

// ResampleOptions extracts filter, format, and method from resample options.
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter ResampleFilter, format fs.Type) {
method = ResampleFit
Expand Down
40 changes: 40 additions & 0 deletions internal/thumb/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package thumb

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestOptions_Contains(t *testing.T) {
t.Run("Left224", func(t *testing.T) {
options := SizeLeft224.Options
assert.True(t, options.Contains(ResampleFillTopLeft))
assert.False(t, options.Contains(ResampleFillBottomRight))
assert.False(t, options.Contains(ResampleFillCenter))
})
t.Run("Right224", func(t *testing.T) {
options := SizeRight224.Options
assert.False(t, options.Contains(ResampleFillTopLeft))
assert.True(t, options.Contains(ResampleFillBottomRight))
assert.False(t, options.Contains(ResampleFillCenter))
})
t.Run("Tile224", func(t *testing.T) {
options := SizeTile224.Options
assert.False(t, options.Contains(ResampleFillTopLeft))
assert.False(t, options.Contains(ResampleFillBottomRight))
assert.True(t, options.Contains(ResampleFillCenter))
})
t.Run("Tile500", func(t *testing.T) {
options := SizeTile500.Options
assert.False(t, options.Contains(ResampleFillTopLeft))
assert.False(t, options.Contains(ResampleFillBottomRight))
assert.True(t, options.Contains(ResampleFillCenter))
})
t.Run("Fit1600", func(t *testing.T) {
options := SizeFit1600.Options
assert.False(t, options.Contains(ResampleFillTopLeft))
assert.False(t, options.Contains(ResampleFillBottomRight))
assert.False(t, options.Contains(ResampleFillCenter))
})
}
47 changes: 30 additions & 17 deletions internal/thumb/size.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import (

// Size represents a standard media resolution.
type Size struct {
Name Name `json:"name"` // Name of the thumbnail size.
Source Name `json:"-"` // Larger size this size can be generated from.
Usage string `json:"usage"` // Usage example.
Width int `json:"w"` // Width in pixels.
Height int `json:"h"` // Height in pixels.
Public bool `json:"-"` // Size is visible in client applications.
Fit bool `json:"-"` // Image is fitted to fill this size.
Optional bool `json:"-"` // Size must not be generated by default.
Required bool `json:"-"` // Size must always be generated.
Options []ResampleOption `json:"-"`
Name Name `json:"name"` // Name of the thumbnail size.
Source Name `json:"-"` // Larger size this size can be generated from.
Usage string `json:"usage"` // Usage example.
Width int `json:"w"` // Width in pixels.
Height int `json:"h"` // Height in pixels.
Public bool `json:"-"` // Size is visible in client applications.
Fit bool `json:"-"` // Image is fitted to fill this size.
Optional bool `json:"-"` // Size must not be generated by default.
Required bool `json:"-"` // Size must always be generated.
Options Options `json:"-"`
}

// Bounds returns the thumb size as image.Rectangle.
Expand Down Expand Up @@ -65,17 +65,30 @@ func (s Size) Skip(img image.Image) bool {

// Skip tests if the size can be skipped when generating thumbnails, e.g. because it is larger than the original.
func Skip(s Size, bounds image.Rectangle) bool {
// Always return false if this thumbnail size is always required.
if s.Required {
// This thumbnail size is always required.
return false
} else if s.Optional {
// Size can be omitted by default, e.g. because it is deprecated or uncommon.
}

// Optional sizes can be skipped by default.
if s.Optional {
return true
} else if !s.Fit || !bounds.In(s.Bounds()) {
// Image is within the bounds of this thumbnail size or is fitted to it.
}

// Skip square thumbnails that show a crop on the left or right if the image is square as well.
if bounds.Max.X == bounds.Max.Y && s.Width == s.Height {
if s.Options.Contains(ResampleFillTopLeft) || s.Options.Contains(ResampleFillBottomRight) {
return true
}
}

// Check if image is within the bounds of this thumbnail size or is fitted to it.
if !s.Fit || !bounds.In(s.Bounds()) {
return false
} else if newSize := FitBounds(bounds); newSize.Width < s.Width {
// Image is smaller than this thumbnail size.
}

// Skip if the image is smaller than this thumbnail size.
if newSize := FitBounds(bounds); newSize.Width < s.Width {
return true
}

Expand Down
6 changes: 6 additions & 0 deletions internal/thumb/size_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import (
)

func TestSkip(t *testing.T) {
t.Run("Tile224", func(t *testing.T) {
bounds := image.Rectangle{Min: image.Point{}, Max: image.Point{X: 1024, Y: 1024}}
assert.False(t, Skip(SizeTile224, bounds))
assert.True(t, Skip(SizeLeft224, bounds))
assert.True(t, Skip(SizeRight224, bounds))
})
t.Run("Tile500", func(t *testing.T) {
bounds := image.Rectangle{Min: image.Point{}, Max: image.Point{X: 1024, Y: 1024}}
assert.False(t, Skip(SizeTile500, bounds))
Expand Down
48 changes: 24 additions & 24 deletions internal/thumb/sizes.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,42 +37,42 @@ func (m SizeMap) All() SizeList {
}

var (
SizeTile50 = Size{Tile50, Fit720, "List View", 50, 50, false, false, false, true, []ResampleOption{ResampleFillCenter, ResampleDefault}}
SizeTile100 = Size{Tile100, Fit720, "Places View", 100, 100, false, false, false, true, []ResampleOption{ResampleFillCenter, ResampleDefault}}
SizeTile224 = Size{Tile224, Fit720, "TensorFlow, Mosaic View", 224, 224, false, false, false, true, []ResampleOption{ResampleFillCenter, ResampleDefault}}
SizeColors = Size{Colors, Fit720, "Color Detection", 3, 3, false, false, false, true, []ResampleOption{ResampleResize, ResampleNearestNeighbor, ResamplePng}}
SizeLeft224 = Size{Left224, Fit720, "TensorFlow", 224, 224, false, false, false, true, []ResampleOption{ResampleFillTopLeft, ResampleDefault}}
SizeRight224 = Size{Right224, Fit720, "TensorFlow", 224, 224, false, false, false, true, []ResampleOption{ResampleFillBottomRight, ResampleDefault}}
SizeFit720 = Size{Fit720, "", "SD TV, Mobile", 720, 720, true, true, false, true, []ResampleOption{ResampleFit, ResampleDefault}}
SizeTile500 = Size{Tile500, Fit1920, "Cards View", 500, 500, false, false, false, true, []ResampleOption{ResampleFillCenter, ResampleDefault}}
SizeFit1280 = Size{Fit1280, Fit1920, "HD TV, SXGA", 1280, 1024, true, true, false, false, []ResampleOption{ResampleFit, ResampleDefault}}
SizeFit1600 = Size{Fit1600, Fit1920, "Social Sharing", 1600, 900, false, true, true, false, []ResampleOption{ResampleFit, ResampleDefault}}
SizeFit1920 = Size{Fit1920, "", "Full HD", 1920, 1200, true, true, false, false, []ResampleOption{ResampleFit, ResampleDefault}}
SizeTile1080 = Size{Tile1080, Fit4096, "Instagram", 1080, 1080, false, false, true, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}
SizeFit2048 = Size{Fit2048, Fit4096, "DCI 2K, Tablets", 2048, 2048, false, true, true, false, []ResampleOption{ResampleFit, ResampleDefault}}
SizeFit2560 = Size{Fit2560, Fit4096, "Quad HD, Notebooks", 2560, 1600, true, true, false, false, []ResampleOption{ResampleFit, ResampleDefault}}
SizeFit3840 = Size{Fit3840, Fit4096, "4K Ultra HD", 3840, 2400, false, true, true, false, []ResampleOption{ResampleFit, ResampleDefault}}
SizeFit4096 = Size{Fit4096, "", "DCI 4K, Retina 4K", 4096, 4096, true, true, false, false, []ResampleOption{ResampleFit, ResampleDefault}}
SizeFit7680 = Size{Fit7680, "", "8K Ultra HD 2", 7680, 4320, true, true, false, false, []ResampleOption{ResampleFit, ResampleDefault}}
SizeColors = Size{Colors, Fit720, "Color Detection", 3, 3, false, false, false, true, Options{ResampleResize, ResampleNearestNeighbor, ResamplePng}}
SizeTile50 = Size{Tile50, Fit720, "List View", 50, 50, false, false, false, true, Options{ResampleFillCenter, ResampleDefault}}
SizeTile100 = Size{Tile100, Fit720, "Places View", 100, 100, false, false, false, true, Options{ResampleFillCenter, ResampleDefault}}
SizeTile224 = Size{Tile224, Fit720, "TensorFlow, Mosaic View", 224, 224, false, false, false, true, Options{ResampleFillCenter, ResampleDefault}}
SizeLeft224 = Size{Left224, Fit720, "TensorFlow", 224, 224, false, false, false, false, Options{ResampleFillTopLeft, ResampleDefault}}
SizeRight224 = Size{Right224, Fit720, "TensorFlow", 224, 224, false, false, false, false, Options{ResampleFillBottomRight, ResampleDefault}}
SizeFit720 = Size{Fit720, "", "SD TV, Mobile", 720, 720, true, true, false, true, Options{ResampleFit, ResampleDefault}}
SizeTile500 = Size{Tile500, Fit1920, "Cards View", 500, 500, false, false, false, true, Options{ResampleFillCenter, ResampleDefault}}
SizeTile1080 = Size{Tile1080, Fit1920, "Instagram", 1080, 1080, false, false, true, false, Options{ResampleFillCenter, ResampleDefault}}
SizeFit1280 = Size{Fit1280, Fit1920, "HD TV, SXGA", 1280, 1024, true, true, false, false, Options{ResampleFit, ResampleDefault}}
SizeFit1600 = Size{Fit1600, Fit1920, "Social Media", 1600, 900, false, true, true, false, Options{ResampleFit, ResampleDefault}}
SizeFit1920 = Size{Fit1920, "", "Full HD", 1920, 1200, true, true, false, false, Options{ResampleFit, ResampleDefault}}
SizeFit2048 = Size{Fit2048, Fit4096, "DCI 2K, Tablets", 2048, 2048, false, true, true, false, Options{ResampleFit, ResampleDefault}}
SizeFit2560 = Size{Fit2560, Fit4096, "Quad HD, Notebooks", 2560, 1600, true, true, false, false, Options{ResampleFit, ResampleDefault}}
SizeFit3840 = Size{Fit3840, Fit4096, "4K Ultra HD", 3840, 2400, false, true, true, false, Options{ResampleFit, ResampleDefault}}
SizeFit4096 = Size{Fit4096, "", "DCI 4K, Retina 4K", 4096, 4096, true, true, false, false, Options{ResampleFit, ResampleDefault}}
SizeFit7680 = Size{Fit7680, "", "8K Ultra HD 2", 7680, 4320, true, true, false, false, Options{ResampleFit, ResampleDefault}}
)

// Sizes contains the properties of all thumbnail sizes.
var Sizes = SizeMap{
Colors: SizeColors,
Tile50: SizeTile50,
Tile100: SizeTile100,
Tile224: SizeTile224,
Tile500: SizeTile500,
Tile1080: SizeTile1080,
Colors: SizeColors,
Left224: SizeLeft224,
Right224: SizeRight224,
Tile224: SizeTile224,
Fit720: SizeFit720,
Tile500: SizeTile500,
Tile1080: SizeTile1080, // Optional
Fit1280: SizeFit1280,
Fit1600: SizeFit1600,
Fit1600: SizeFit1600, // Optional
Fit1920: SizeFit1920,
Fit2048: SizeFit2048,
Fit2048: SizeFit2048, // Deprecated in favor of Fit1920
Fit2560: SizeFit2560,
Fit3840: SizeFit3840, // Deprecated in favor of fit_4096
Fit3840: SizeFit3840, // Deprecated in favor of Fit4096
Fit4096: SizeFit4096,
Fit7680: SizeFit7680,
}

0 comments on commit a54a46b

Please sign in to comment.