From 6ff747c3963445272c453acd494801f4ddc342be Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Mon, 29 Jan 2024 22:31:04 +0100 Subject: [PATCH] Colors: Enforce thumbnail size limit of 3x3 pixels #3976 Signed-off-by: Michael Mayer --- internal/photoprism/colors.go | 13 ++++ internal/photoprism/colors_test.go | 116 +++++++++++++++++------------ internal/thumb/sizes.go | 24 ++++-- pkg/colors/colors.go | 4 + pkg/colors/colors_test.go | 14 +++- pkg/colors/perception.go | 2 +- 6 files changed, 116 insertions(+), 57 deletions(-) diff --git a/internal/photoprism/colors.go b/internal/photoprism/colors.go index 4170d2df2..e08bde6eb 100644 --- a/internal/photoprism/colors.go +++ b/internal/photoprism/colors.go @@ -27,6 +27,19 @@ func (m *MediaFile) Colors(thumbPath string) (perception colors.ColorPerception, bounds := img.Bounds() width, height := bounds.Max.X, bounds.Max.Y + + // Enforce thumbnail width limit and warn if it is exceeded. + if maxWidth := thumb.SizeColors.Width; width > maxWidth { + log.Warnf("color: thumbnail width %d exceeds size limit of %d in %s", width, maxWidth, clean.Log(m.RootRelName())) + width = maxWidth + } + + // Enforce thumbnail height limit and warn if it is exceeded. + if maxHeight := thumb.SizeColors.Height; height > maxHeight { + log.Warnf("color: thumbnail height %d exceeds size limit of %d in %s", height, maxHeight, clean.Log(m.RootRelName())) + height = maxHeight + } + pixels := float64(width * height) chromaSum := 0.0 diff --git a/internal/photoprism/colors_test.go b/internal/photoprism/colors_test.go index 5b2b69df3..d47adf977 100644 --- a/internal/photoprism/colors_test.go +++ b/internal/photoprism/colors_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/thumb" "github.com/photoprism/photoprism/pkg/colors" "github.com/photoprism/photoprism/pkg/fastwalk" ) @@ -101,84 +102,105 @@ func TestMediaFile_Colors_Testdata(t *testing.T) { } func TestMediaFile_Colors(t *testing.T) { - conf := config.TestConfig() + c := config.TestConfig() t.Run("cat_brown.jpg", func(t *testing.T) { - if mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/cat_brown.jpg"); err == nil { - p, err := mediaFile.Colors(conf.ThumbCachePath()) + if mediaFile, err := NewMediaFile(c.ExamplesPath() + "/cat_brown.jpg"); err == nil { + file, fileErr := mediaFile.Colors(c.ThumbCachePath()) - t.Log(p, err) + t.Log(file, fileErr) - assert.Nil(t, err) - assert.Equal(t, 13, p.Chroma.Int()) - assert.Equal(t, "D", p.Chroma.Hex()) - assert.IsType(t, colors.Colors{}, p.Colors) - assert.Equal(t, "gold", p.MainColor.Name()) - assert.Equal(t, colors.Colors{0x9, 0x3, 0x2, 0x1, 0x1, 0x2, 0x0, 0x6, 0x1}, p.Colors) - assert.Equal(t, colors.LightMap{0x4, 0x5, 0xb, 0x4, 0x7, 0x3, 0x2, 0x5, 0x7}, p.Luminance) + assert.Nil(t, fileErr) + assert.Equal(t, 13, file.Chroma.Int()) + assert.Equal(t, "D", file.Chroma.Hex()) + assert.IsType(t, colors.Colors{}, file.Colors) + assert.Equal(t, "gold", file.MainColor.Name()) + assert.Equal(t, colors.Colors{0x9, 0x3, 0x2, 0x1, 0x1, 0x2, 0x0, 0x6, 0x1}, file.Colors) + assert.Equal(t, colors.LightMap{0x4, 0x5, 0xb, 0x4, 0x7, 0x3, 0x2, 0x5, 0x7}, file.Luminance) } else { t.Error(err) } }) + t.Run("FernRegular", func(t *testing.T) { + if mediaFile, err := NewMediaFile(c.ExamplesPath() + "/fern_green.jpg"); err == nil { + file, fileErr := mediaFile.Colors(c.ThumbCachePath()) - t.Run("fern_green.jpg", func(t *testing.T) { - if mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/fern_green.jpg"); err == nil { - p, err := mediaFile.Colors(conf.ThumbCachePath()) + t.Log(file, fileErr) - t.Log(p, err) - - assert.Nil(t, err) - assert.Equal(t, 51, p.Chroma.Int()) - assert.Equal(t, "33", p.Chroma.Hex()) - assert.IsType(t, colors.Colors{}, p.Colors) - assert.Equal(t, "lime", p.MainColor.Name()) - assert.Equal(t, colors.Colors{0xa, 0x9, 0xa, 0x9, 0xa, 0xa, 0x9, 0x9, 0x9}, p.Colors) - assert.Equal(t, colors.LightMap{0xb, 0x4, 0xa, 0x6, 0x9, 0x8, 0x2, 0x3, 0x4}, p.Luminance) + assert.Nil(t, fileErr) + assert.Equal(t, 51, file.Chroma.Int()) + assert.Equal(t, "33", file.Chroma.Hex()) + assert.IsType(t, colors.Colors{}, file.Colors) + assert.Equal(t, "lime", file.MainColor.Name()) + assert.Equal(t, colors.Colors{0xa, 0x9, 0xa, 0x9, 0xa, 0xa, 0x9, 0x9, 0x9}, file.Colors) + assert.Equal(t, colors.LightMap{0xb, 0x4, 0xa, 0x6, 0x9, 0x8, 0x2, 0x3, 0x4}, file.Luminance) } else { t.Error(err) } }) + t.Run("FernLarge", func(t *testing.T) { + if mediaFile, err := NewMediaFile(c.ExamplesPath() + "/fern_green.jpg"); err == nil { + thumbLarge := thumb.SizeColors + thumbLarge.Height = 16 + thumbLarge.Width = 16 + thumbLarge.Name = "color_large" + thumb.Sizes[thumb.Colors] = thumbLarge + + file, fileErr := mediaFile.Colors(c.ThumbCachePath()) + + thumb.Sizes[thumb.Colors] = thumb.SizeColors + + t.Log(file, fileErr) + + assert.Nil(t, fileErr) + assert.Equal(t, 67, file.Chroma.Int()) + assert.Equal(t, "43", file.Chroma.Hex()) + assert.IsType(t, colors.Colors{}, file.Colors) + assert.Equal(t, "lime", file.MainColor.Name()) + assert.Equal(t, colors.Colors{9, 10, 10, 10, 10, 10, 10, 10, 10}, file.Colors) + assert.Equal(t, colors.LightMap{5, 9, 9, 9, 9, 10, 10, 9, 11}, file.Luminance) + } else { + t.Error(err) + } + }) t.Run("IMG_4120.JPG", func(t *testing.T) { - if mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120.JPG"); err == nil { - p, err := mediaFile.Colors(conf.ThumbCachePath()) + if mediaFile, err := NewMediaFile(c.ExamplesPath() + "/IMG_4120.JPG"); err == nil { + file, fileErr := mediaFile.Colors(c.ThumbCachePath()) - t.Log(p, err) + t.Log(file, fileErr) - assert.Nil(t, err) - assert.Equal(t, 7, p.Chroma.Int()) - assert.Equal(t, "7", p.Chroma.Hex()) - assert.IsType(t, colors.Colors{}, p.Colors) - assert.Equal(t, "blue", p.MainColor.Name()) - assert.Equal(t, colors.Colors{0x1, 0x6, 0x6, 0x1, 0x1, 0x9, 0x1, 0x0, 0x0}, p.Colors) + assert.Nil(t, fileErr) + assert.Equal(t, 7, file.Chroma.Int()) + assert.Equal(t, "7", file.Chroma.Hex()) + assert.IsType(t, colors.Colors{}, file.Colors) + assert.Equal(t, "blue", file.MainColor.Name()) + assert.Equal(t, colors.Colors{0x1, 0x6, 0x6, 0x1, 0x1, 0x9, 0x1, 0x0, 0x0}, file.Colors) } else { t.Error(err) } }) - t.Run("leaves_gold.jpg", func(t *testing.T) { - if mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/leaves_gold.jpg"); err == nil { - p, err := mediaFile.Colors(conf.ThumbCachePath()) + if mediaFile, err := NewMediaFile(c.ExamplesPath() + "/leaves_gold.jpg"); err == nil { + file, fileErr := mediaFile.Colors(c.ThumbCachePath()) - t.Log(p, err) + t.Log(file, fileErr) - assert.Nil(t, err) - assert.Equal(t, 16, p.Chroma.Int()) - assert.Equal(t, "10", p.Chroma.Hex()) - assert.IsType(t, colors.Colors{}, p.Colors) - assert.Equal(t, "gold", p.MainColor.Name()) + assert.Nil(t, fileErr) + assert.Equal(t, 16, file.Chroma.Int()) + assert.Equal(t, "10", file.Chroma.Hex()) + assert.IsType(t, colors.Colors{}, file.Colors) + assert.Equal(t, "gold", file.MainColor.Name()) - assert.Equal(t, colors.Colors{0x0, 0x0, 0x2, 0x3, 0x3, 0x0, 0x2, 0x3, 0x0}, p.Colors) + assert.Equal(t, colors.Colors{0x0, 0x0, 0x2, 0x3, 0x3, 0x0, 0x2, 0x3, 0x0}, file.Colors) } else { t.Error(err) } }) - t.Run("Random.docx", func(t *testing.T) { - mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/Random.docx") - p, err := mediaFile.Colors(conf.ThumbCachePath()) - assert.Error(t, err, "no color information: not a JPEG file") + file, fileErr := NewMediaFile(c.ExamplesPath() + "/Random.docx") + p, fileErr := file.Colors(c.ThumbCachePath()) + assert.Error(t, fileErr, "no color information: not a JPEG file") t.Log(p) - }) } diff --git a/internal/thumb/sizes.go b/internal/thumb/sizes.go index 2e337510d..7eb11c63e 100644 --- a/internal/thumb/sizes.go +++ b/internal/thumb/sizes.go @@ -37,15 +37,25 @@ func (m SizeMap) All() SizeList { return result } +var ( + SizeTile50 = Size{Tile50, Tile500, "List View", 50, 50, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}} + SizeTile100 = Size{Tile100, Tile500, "Places View", 100, 100, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}} + SizeTile224 = Size{Tile224, Tile500, "TensorFlow, Mosaic View", 224, 224, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}} + SizeTile500 = Size{Tile500, "", "Cards View", 500, 500, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}} + SizeColors = Size{Colors, Fit720, "Color Detection", 3, 3, false, false, []ResampleOption{ResampleResize, ResampleNearestNeighbor, ResamplePng}} + SizeLeft224 = Size{Left224, Fit720, "TensorFlow", 224, 224, false, false, []ResampleOption{ResampleFillTopLeft, ResampleDefault}} + SizeRight224 = Size{Right224, Fit720, "TensorFlow", 224, 224, false, false, []ResampleOption{ResampleFillBottomRight, ResampleDefault}} +) + // Sizes contains the properties of all thumbnail sizes. var Sizes = SizeMap{ - Tile50: {Tile50, Tile500, "List View", 50, 50, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, - Tile100: {Tile100, Tile500, "Places View", 100, 100, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, - Tile224: {Tile224, Tile500, "TensorFlow, Mosaic View", 224, 224, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, - Tile500: {Tile500, "", "Cards View", 500, 500, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, - Colors: {Colors, Fit720, "Color Detection", 3, 3, false, false, []ResampleOption{ResampleResize, ResampleNearestNeighbor, ResamplePng}}, - Left224: {Left224, Fit720, "TensorFlow", 224, 224, false, false, []ResampleOption{ResampleFillTopLeft, ResampleDefault}}, - Right224: {Right224, Fit720, "TensorFlow", 224, 224, false, false, []ResampleOption{ResampleFillBottomRight, ResampleDefault}}, + Tile50: SizeTile50, + Tile100: SizeTile100, + Tile224: SizeTile224, + Tile500: SizeTile500, + Colors: SizeColors, + Left224: SizeLeft224, + Right224: SizeRight224, Fit720: {Fit720, "", "SD TV, Mobile", 720, 720, true, true, []ResampleOption{ResampleFit, ResampleDefault}}, Fit1280: {Fit1280, Fit2048, "HD TV, SXGA", 1280, 1024, true, true, []ResampleOption{ResampleFit, ResampleDefault}}, Fit1920: {Fit1920, Fit2048, "Full HD", 1920, 1200, true, true, []ResampleOption{ResampleFit, ResampleDefault}}, diff --git a/pkg/colors/colors.go b/pkg/colors/colors.go index d67af5065..981a5c868 100644 --- a/pkg/colors/colors.go +++ b/pkg/colors/colors.go @@ -118,6 +118,10 @@ func (c Color) ID() int16 { } func (c Color) Hex() string { + if c < 0 || c > 15 { + return "0" + } + return fmt.Sprintf("%X", c) } diff --git a/pkg/colors/colors_test.go b/pkg/colors/colors_test.go index ed6693c94..5d1262ad4 100644 --- a/pkg/colors/colors_test.go +++ b/pkg/colors/colors_test.go @@ -19,13 +19,23 @@ func TestColors_List(t *testing.T) { } func TestColor_Hex(t *testing.T) { + assert.Equal(t, "0", Color(-1).Hex()) + assert.Equal(t, "0", Black.Hex()) assert.Equal(t, "C", Magenta.Hex()) assert.Equal(t, "7", Cyan.Hex()) + assert.Equal(t, "F", Pink.Hex()) + assert.Equal(t, "F", Color(15).Hex()) + assert.Equal(t, "0", Color(16).Hex()) + assert.Equal(t, "0", Color(17).Hex()) } func TestColors_Hex(t *testing.T) { - result := Colors{Orange, Lime, Black}.Hex() - assert.Equal(t, "DA0", result) + t.Run("All", func(t *testing.T) { + assert.Equal(t, "5CFED3BA98762410", All.Hex()) + }) + t.Run("OrangeLimeBlack", func(t *testing.T) { + assert.Equal(t, "DA0", Colors{Orange, Lime, Black}.Hex()) + }) } func TestColor_ID(t *testing.T) { diff --git a/pkg/colors/perception.go b/pkg/colors/perception.go index 5be43206e..607f8b3f9 100644 --- a/pkg/colors/perception.go +++ b/pkg/colors/perception.go @@ -1,6 +1,6 @@ package colors -// Information on how an image looks like in terms of colors and light. +// ColorPerception provides information on how an image looks in terms of color and light. type ColorPerception struct { Colors Colors MainColor Color