Backend: Prepare database for advanced filtering and grouping #154

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2019-12-28 20:24:20 +01:00
parent de6503646c
commit ca8cfffc24
26 changed files with 927 additions and 518 deletions

View file

@ -60,6 +60,9 @@ class Config {
case "countries":
this.values.count.countries += data.count;
break;
case "places":
this.values.count.places += data.count;
break;
case "labels":
this.values.count.labels += data.count;
break;

View file

@ -49,7 +49,7 @@
{text: '', value: '', align: 'center', sortable: false, class: 'p-col-select'},
{text: this.$gettext('Title'), value: 'PhotoTitle'},
{text: this.$gettext('Taken At'), value: 'TakenAt'},
{text: this.$gettext('Location'), value: 'LocPlace'},
{text: this.$gettext('Location'), value: 'LocLabel'},
{text: this.$gettext('Camera'), value: 'CameraModel'},
{text: this.$gettext('Favorite'), value: 'PhotoFavorite', align: 'left'},
],

View file

@ -104,8 +104,8 @@ class Photo extends Abstract {
}
getLocation() {
if (this.LocPlace) {
return this.LocPlace;
if (this.LocLabel) {
return this.LocLabel;
}
return "Unknown";

View file

@ -135,14 +135,14 @@ describe("model/photo", () => {
});
it("should get location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocPlace: "Cape Point, South Africa", LocCountry: "South Africa"};
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocLabel: "Cape Point, South Africa", LocCountry: "South Africa"};
const photo = new Photo(values);
const result = photo.getLocation();
assert.equal(result, "Cape Point, South Africa");
});
it("should get location", () => {
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocPlace: "Cape Point, State, South Africa", LocCountry: "South Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"};
const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocLabel: "Cape Point, State, South Africa", LocCountry: "South Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"};
const photo = new Photo(values);
const result = photo.getLocation();
assert.equal(result, "Cape Point, State, South Africa");

462
internal/colors/colors.go Normal file
View file

@ -0,0 +1,462 @@
/*
This package encapsulates session storage.
Additional information can be found in our Developer Guide:
https://github.com/photoprism/photoprism/wiki
*/
package colors
import (
"fmt"
"image/color"
"strings"
"github.com/lucasb-eyer/go-colorful"
)
type ColorPerception struct {
Colors Colors
MainColor Color
Luminance LightMap
Chroma Chroma
}
type Color uint16
type Colors []Color
type Chroma uint8
type Luminance uint8
type LightMap []Luminance
const (
Black Color = iota
Brown
Grey
White
Purple
Gold
Blue
Cyan
Teal
Green
Lime
Yellow
Magenta
Orange
Red
Pink
)
var All = Colors{
Red,
Magenta,
Pink,
Orange,
Gold,
Yellow,
Lime,
Green,
Teal,
Cyan,
Blue,
Purple,
Brown,
White,
Grey,
Black,
}
var Names = map[Color]string{
Black: "dark", // 0
Brown: "brown", // 1
Grey: "grey", // 2
White: "bright", // 3
Purple: "purple", // 4
Gold: "gold", // 5
Blue: "blue", // 6
Cyan: "cyan", // 7
Teal: "teal", // 8
Green: "green", // 9
Lime: "lime", // A
Yellow: "yellow", // B
Magenta: "magenta", // C
Orange: "orange", // D
Red: "red", // E
Pink: "pink", // F
}
var Weights = map[Color]uint16{
Black: 2,
Brown: 2,
Grey: 1,
White: 2,
Purple: 4,
Gold: 4,
Blue: 3,
Cyan: 4,
Teal: 4,
Green: 3,
Lime: 5,
Yellow: 5,
Magenta: 5,
Orange: 4,
Red: 4,
Pink: 4,
}
func (c Color) Name() string {
return Names[c]
}
func (c Color) Hex() string {
return fmt.Sprintf("%X", c)
}
func (c Colors) Hex() (result string) {
for _, indexedColor := range c {
result += indexedColor.Hex()
}
return result
}
func (c Colors) List() []map[string]string {
result := make([]map[string]string, 0, len(c))
for _, c := range c {
result = append(result, map[string]string{"name": c.Name(), "label": strings.Title(c.Name()), "example": ColorExamples[c]})
}
return result
}
func (c Chroma) Hex() string {
return fmt.Sprintf("%X", c)
}
func (c Chroma) Uint() uint {
return uint(c)
}
func (c Chroma) Int() int {
return int(c)
}
func (l Luminance) Hex() string {
return fmt.Sprintf("%X", l)
}
func (m LightMap) Hex() (result string) {
for _, luminance := range m {
result += luminance.Hex()
}
return result
}
var ColorExamples = map[Color]string{
Red: "#E57373",
Magenta: "#FF00FF",
Pink: "#F06292",
Orange: "#FFB74D",
Brown: "#A1887F",
Gold: "#FFD54F",
Yellow: "#FFF176",
Lime: "#DCE775",
Green: "#81C784",
Teal: "#4DB6AC",
Cyan: "#4DD0E1",
Blue: "#64B5F6",
Purple: "#BA68C8",
White: "#F5F5F5",
Grey: "#BDBDBD",
Black: "#333333",
}
var ColorMap = map[color.RGBA]Color{
{0x00, 0x00, 0x00, 0xff}: Black,
{0xa1, 0x88, 0x7f, 0xff}: Brown,
{0x8d, 0x6e, 0x63, 0xff}: Brown,
{0xa0, 0x7f, 0x6c, 0xff}: Brown,
{0x9b, 0x7b, 0x5b, 0xff}: Brown,
{0x75, 0x64, 0x5b, 0xff}: Brown,
{0x79, 0x55, 0x48, 0xff}: Brown,
{0x6d, 0x4c, 0x41, 0xff}: Brown,
{0x5d, 0x40, 0x37, 0xff}: Brown,
{0x9b, 0x61, 0x36, 0xff}: Brown,
{0xc1, 0xa4, 0x87, 0xff}: Brown,
{0xaa, 0x80, 0x62, 0xff}: Brown,
{0x6b, 0x55, 0x46, 0xff}: Brown,
{0xb4, 0xb5, 0x9c, 0xff}: Brown,
{0xb2, 0xb4, 0x9b, 0xff}: Green,
{0xe0, 0xe0, 0xe0, 0xff}: Grey,
{0x9E, 0x9E, 0x9E, 0xff}: Grey,
{0x75, 0x75, 0x75, 0xff}: Grey,
{0x61, 0x61, 0x61, 0xff}: Grey,
{0x42, 0x42, 0x42, 0xff}: Grey,
{0x84, 0x7a, 0x72, 0xff}: Grey,
{0xdf, 0xe0, 0xe1, 0xff}: Grey,
{0xFF, 0xFF, 0xFF, 0xff}: White,
{0xe4, 0xe4, 0xe4, 0xff}: White,
{0xe7, 0xe7, 0xe7, 0xff}: White,
{0xf3, 0xe5, 0xf5, 0xff}: Purple,
{0xe1, 0xbe, 0xe7, 0xff}: Purple,
{0xce, 0x93, 0xd8, 0xff}: Purple,
{0xba, 0x68, 0xc8, 0xff}: Purple,
{0xab, 0x47, 0xbc, 0xff}: Purple,
{0x9c, 0x27, 0xb0, 0xff}: Purple,
{0x9b, 0x31, 0x8f, 0xff}: Purple,
{0x86, 0x00, 0x7e, 0xff}: Purple,
{0x8e, 0x24, 0xaa, 0xff}: Purple,
{0x7b, 0x1f, 0xa2, 0xff}: Purple,
{0x6a, 0x1b, 0x9a, 0xff}: Purple,
{0x4a, 0x14, 0x8c, 0xff}: Purple,
{0xaa, 0x00, 0xff, 0xff}: Purple,
{0xed, 0xe7, 0xf6, 0xff}: Purple,
{0xd1, 0xc4, 0xe9, 0xff}: Purple,
{0xb3, 0x9d, 0xdb, 0xff}: Purple,
{0x95, 0x75, 0xcd, 0xff}: Purple,
{0x7e, 0x57, 0xc2, 0xff}: Purple,
{0x5e, 0x35, 0xb1, 0xff}: Purple,
{0x67, 0x3a, 0xb7, 0xff}: Purple,
{0x51, 0x2d, 0xa8, 0xff}: Purple,
{0x45, 0x27, 0xa0, 0xff}: Purple,
{0x31, 0x1b, 0x92, 0xff}: Purple,
{0xb3, 0x88, 0xff, 0xff}: Purple,
{0x7c, 0x4d, 0xff, 0xff}: Purple,
{0x8e, 0x64, 0x93, 0xff}: Purple,
{0x5e, 0x3a, 0x5e, 0xff}: Purple,
{0x44, 0x0e, 0x79, 0xff}: Purple,
{0x48, 0x36, 0x78, 0xff}: Purple,
{0x4e, 0x38, 0x80, 0xff}: Purple,
{0x3b, 0x0e, 0x79, 0xff}: Purple,
{0x3F, 0x51, 0xB5, 0xff}: Blue,
{0xc5, 0xca, 0xe9, 0xff}: Blue,
{0x5c, 0x6b, 0xc0, 0xff}: Blue,
{0x39, 0x49, 0xab, 0xff}: Blue,
{0x30, 0x3f, 0x9f, 0xff}: Blue,
{0x28, 0x35, 0x93, 0xff}: Blue,
{0x1a, 0x23, 0x7e, 0xff}: Blue,
{0x53, 0x6d, 0xfe, 0xff}: Blue,
{0x3d, 0x5a, 0xfe, 0xff}: Blue,
{0x30, 0x4f, 0xfe, 0xff}: Blue,
{0x21, 0x96, 0xF3, 0xff}: Blue,
{0xbb, 0xde, 0xfb, 0xff}: Blue,
{0x90, 0xca, 0xf9, 0xff}: Blue,
{0x64, 0xb5, 0xf6, 0xff}: Blue,
{0x42, 0xa5, 0xf5, 0xff}: Blue,
{0x1e, 0x88, 0xe5, 0xff}: Blue,
{0x19, 0x76, 0xd2, 0xff}: Blue,
{0x15, 0x65, 0xc0, 0xff}: Blue,
{0x0d, 0x47, 0xa1, 0xff}: Blue,
{0x82, 0xb1, 0xff, 0xff}: Blue,
{0x44, 0x8a, 0xff, 0xff}: Blue,
{0x29, 0x79, 0xff, 0xff}: Blue,
{0x29, 0x62, 0xff, 0xff}: Blue,
{0x03, 0xa9, 0xf6, 0xff}: Blue,
{0xb3, 0xe5, 0xfc, 0xff}: Blue,
{0x81, 0xd4, 0xfa, 0xff}: Blue,
{0x4f, 0xc3, 0xf7, 0xff}: Blue,
{0x29, 0xb6, 0xf6, 0xff}: Blue,
{0x03, 0x9b, 0xe5, 0xff}: Blue,
{0x02, 0x88, 0xd1, 0xff}: Blue,
{0x02, 0x77, 0xbd, 0xff}: Blue,
{0x01, 0x57, 0x9b, 0xff}: Blue,
{0x80, 0xd8, 0xff, 0xff}: Blue,
{0x40, 0xc4, 0xff, 0xff}: Blue,
{0x00, 0xb0, 0xff, 0xff}: Blue,
{0x00, 0x91, 0xea, 0xff}: Blue,
{0x60, 0x7d, 0x8b, 0xff}: Blue,
{0x78, 0x90, 0x9c, 0xff}: Blue,
{0x54, 0x6e, 0x7a, 0xff}: Blue,
{0x37, 0x47, 0x4f, 0xff}: Blue,
{0xe4, 0xeb, 0xfd, 0xff}: Blue,
{0x7d, 0xd3, 0xea, 0xff}: Blue,
{0x07, 0x63, 0x99, 0xff}: Blue,
{0x28, 0x44, 0x6b, 0xff}: Blue,
{0x4a, 0xc8, 0xf5, 0xff}: Blue,
{0x08, 0x00, 0xf4, 0xff}: Blue,
{0x01, 0x2d, 0x5f, 0xff}: Blue,
{0xb2, 0xeb, 0xf2, 0xff}: Cyan,
{0x80, 0xde, 0xea, 0xff}: Cyan,
{0x4d, 0xd0, 0xe1, 0xff}: Cyan,
{0x26, 0xc6, 0xda, 0xff}: Cyan,
{0x00, 0xb8, 0xd4, 0xff}: Cyan,
{0x00, 0xBC, 0xD4, 0xff}: Cyan,
{0x00, 0xac, 0xc1, 0xff}: Cyan,
{0x00, 0x97, 0xa7, 0xff}: Cyan,
{0x00, 0x83, 0x8f, 0xff}: Cyan,
{0x00, 0x60, 0x64, 0xff}: Cyan,
{0x84, 0xff, 0xff, 0xff}: Cyan,
{0x18, 0xff, 0xff, 0xff}: Cyan,
{0x00, 0xe5, 0xff, 0xff}: Cyan,
{0x00, 0x96, 0x88, 0xff}: Teal,
{0x00, 0x89, 0x7b, 0xff}: Teal,
{0x00, 0x79, 0x6b, 0xff}: Teal,
{0x00, 0x69, 0x5c, 0xff}: Teal,
{0x04, 0x5d, 0x5c, 0xff}: Teal,
{0x24, 0x5a, 0x5f, 0xff}: Teal,
{0x03, 0x45, 0x4f, 0xff}: Teal,
{0x2c, 0x54, 0x5e, 0xff}: Teal,
{0x17, 0x47, 0x41, 0xff}: Teal,
{0xe8, 0xf5, 0xe9, 0xff}: Green,
{0xc8, 0xe6, 0xc9, 0xff}: Green,
{0xab, 0xc7, 0xb0, 0xff}: Green,
{0xa5, 0xd6, 0xa7, 0xff}: Green,
{0x81, 0xc7, 0x84, 0xff}: Green,
{0x66, 0xbb, 0x6a, 0xff}: Green,
{0x4C, 0xAF, 0x50, 0xff}: Green,
{0x43, 0xa0, 0x47, 0xff}: Green,
{0x38, 0x8e, 0x3c, 0xff}: Green,
{0x2e, 0x7d, 0x32, 0xff}: Green,
{0x1b, 0x5e, 0x20, 0xff}: Green,
{0xf1, 0xf8, 0xe9, 0xff}: Green,
{0xdc, 0xed, 0xc8, 0xff}: Green,
{0xc5, 0xe1, 0xa5, 0xff}: Green,
{0xae, 0xd5, 0x81, 0xff}: Green,
{0x8b, 0xc3, 0x4a, 0xff}: Green,
{0x9c, 0xcc, 0x65, 0xff}: Green,
{0x7c, 0xb3, 0x42, 0xff}: Green,
{0x68, 0x9f, 0x38, 0xff}: Green,
{0x55, 0x8b, 0x2f, 0xff}: Green,
{0x33, 0x69, 0x1e, 0xff}: Green,
{0xb9, 0xf6, 0xca, 0xff}: Green,
{0x69, 0xf0, 0xae, 0xff}: Green,
{0x00, 0xc8, 0x53, 0xff}: Green,
{0x00, 0xe6, 0x76, 0xff}: Green,
{0xcc, 0xff, 0x90, 0xff}: Green,
{0xb2, 0xff, 0x59, 0xff}: Green,
{0x76, 0xff, 0x03, 0xff}: Green,
{0x64, 0xdd, 0x17, 0xff}: Green,
{0xdd, 0xd5, 0x79, 0xff}: Green,
{0xee, 0xec, 0xa2, 0xff}: Green,
{0x24, 0x4e, 0x3b, 0xff}: Green,
{0x9a, 0x9d, 0x47, 0xff}: Green,
{0xbe, 0xbd, 0x76, 0xff}: Green,
{0x5c, 0x5a, 0x30, 0xff}: Green,
{0xb3, 0xc1, 0x6c, 0xff}: Green,
{0xac, 0xa7, 0x83, 0xff}: Green,
{0x47, 0x4c, 0x25, 0xff}: Green,
{0xcd, 0xd0, 0x87, 0xff}: Green,
{0x79, 0x6d, 0x41, 0xff}: Green,
{0xf0, 0xf4, 0xc3, 0xff}: Lime,
{0xe6, 0xee, 0x9c, 0xff}: Lime,
{0xdc, 0xe7, 0x75, 0xff}: Lime,
{0xd4, 0xe1, 0x57, 0xff}: Lime,
{0xCD, 0xDC, 0x39, 0xff}: Lime,
{0xc0, 0xca, 0x33, 0xff}: Lime,
{0xaf, 0xb4, 0x2b, 0xff}: Lime,
{0xee, 0xff, 0x41, 0xff}: Lime,
{0xc6, 0xff, 0x00, 0xff}: Lime,
{0xae, 0xea, 0x00, 0xff}: Lime,
{0xff, 0xf9, 0xc4, 0xff}: Yellow,
{0xff, 0xf5, 0x9d, 0xff}: Yellow,
{0xff, 0xf1, 0x76, 0xff}: Yellow,
{0xff, 0xee, 0x58, 0xff}: Yellow,
{0xff, 0xff, 0x8d, 0xff}: Yellow,
{0xff, 0xff, 0x00, 0xff}: Yellow,
{0xff, 0xd5, 0x4f, 0xff}: Yellow,
{0xff, 0xca, 0x28, 0xff}: Yellow,
{0xe3, 0xce, 0x81, 0xff}: Yellow,
{0xd1, 0xaf, 0x52, 0xff}: Yellow,
{0xee, 0xbb, 0x2b, 0xff}: Yellow,
{0xd3, 0xa8, 0x3a, 0xff}: Yellow,
{0xc5, 0xa7, 0x02, 0xff}: Yellow,
{0x9f, 0x82, 0x01, 0xff}: Yellow,
{0xe8, 0xce, 0x03, 0xff}: Yellow,
{0xf9, 0xa8, 0x25, 0xff}: Orange,
{0xFF, 0x98, 0x00, 0xff}: Orange,
{0xff, 0xa7, 0x26, 0xff}: Orange,
{0xfb, 0x8c, 0x00, 0xff}: Orange,
{0xf5, 0x7c, 0x00, 0xff}: Orange,
{0xef, 0x6c, 0x00, 0xff}: Orange,
{0xff, 0x91, 0x00, 0xff}: Orange,
{0xff, 0x6d, 0x00, 0xff}: Orange,
{0xfd, 0x9a, 0x31, 0xff}: Orange,
{0x7d, 0x27, 0x04, 0xff}: Orange,
{0xfd, 0x57, 0x1f, 0xff}: Orange,
{0xf8, 0x67, 0x04, 0xff}: Orange,
{0xfd, 0x9a, 0x00, 0xff}: Orange,
{0xfe, 0x8a, 0x00, 0xff}: Orange,
{0xf1, 0x96, 0x52, 0xff}: Orange,
{0xe5, 0x83, 0x47, 0xff}: Orange,
{0xc9, 0x4c, 0x30, 0xff}: Orange,
{0x9f, 0x56, 0x01, 0xff}: Orange,
{0xfa, 0x68, 0x01, 0xff}: Orange,
{0xbb, 0x72, 0x3d, 0xff}: Orange,
{0xff, 0x52, 0x52, 0xff}: Red,
{0xf4, 0x43, 0x36, 0xff}: Red,
{0xef, 0x53, 0x50, 0xff}: Red,
{0xe5, 0x39, 0x35, 0xff}: Red,
{0xf6, 0x29, 0x2e, 0xff}: Red,
{0xfc, 0x25, 0x2d, 0xff}: Red,
{0xd3, 0x2f, 0x2f, 0xff}: Red,
{0xc6, 0x28, 0x28, 0xff}: Red,
{0xba, 0x28, 0x30, 0xff}: Red,
{0xb7, 0x1c, 0x1c, 0xff}: Red,
{0xd5, 0x00, 0x00, 0xff}: Red,
{0xdb, 0x08, 0x06, 0xff}: Red,
{0xcf, 0x09, 0x04, 0xff}: Red,
{0xd8, 0x1a, 0x14, 0xff}: Red,
{0xcc, 0x17, 0x08, 0xff}: Red,
{0xd8, 0x0a, 0x07, 0xff}: Red,
{0xde, 0x26, 0x16, 0xff}: Red,
{0xee, 0x24, 0x0f, 0xff}: Red,
{0xa1, 0x21, 0x1f, 0xff}: Red,
{0x70, 0x12, 0x19, 0xff}: Red,
{0x51, 0x12, 0x18, 0xff}: Red,
{0x49, 0x11, 0x14, 0xff}: Red,
{0xfc, 0xe4, 0xec, 0xff}: Pink,
{0xfd, 0xc8, 0xeb, 0xff}: Pink,
{0xe7, 0x9f, 0xa6, 0xff}: Pink,
{0xf8, 0xbb, 0xd0, 0xff}: Pink,
{0xf4, 0x8f, 0xb1, 0xff}: Pink,
{0xff, 0x80, 0xab, 0xff}: Pink,
{0xff, 0x40, 0x81, 0xff}: Pink,
{0xf5, 0x00, 0x57, 0xff}: Pink,
{0xf0, 0x62, 0x92, 0xff}: Pink,
{0xec, 0x40, 0x7a, 0xff}: Pink,
{0xe9, 0x1e, 0x63, 0xff}: Pink,
{0xd8, 0x1b, 0x60, 0xff}: Pink,
{0xc2, 0x18, 0x5b, 0xff}: Pink,
{0xff, 0x00, 0xff, 0xff}: Magenta,
{0xe5, 0x00, 0xe5, 0xff}: Magenta,
{0xf0, 0x00, 0xb5, 0xff}: Magenta,
{0xce, 0x00, 0x9b, 0xff}: Magenta,
{0xc0, 0x05, 0x5b, 0xff}: Magenta,
{0xb0, 0x00, 0x85, 0xff}: Magenta,
{0xa8, 0x28, 0x63, 0xff}: Magenta,
{0x5b, 0x00, 0x2f, 0xff}: Magenta,
{0x4b, 0x01, 0x21, 0xff}: Magenta,
{0x86, 0x02, 0x25, 0xff}: Magenta,
{0xcb, 0x02, 0x3d, 0xff}: Magenta,
{0x64, 0x07, 0x1a, 0xff}: Magenta,
{0x9e, 0x00, 0x47, 0xff}: Magenta,
{0xdc, 0x7a, 0xcf, 0xff}: Magenta,
{0xed, 0xde, 0xac, 0xff}: Gold,
{0xe8, 0xb4, 0x51, 0xff}: Gold,
{0xc0, 0x8a, 0x3e, 0xff}: Gold,
{0xa2, 0x7d, 0x4b, 0xff}: Gold,
{0x75, 0x55, 0x31, 0xff}: Gold,
{0xd1, 0x93, 0x27, 0xff}: Gold,
{0xde, 0xa2, 0x53, 0xff}: Gold,
{0xd5, 0xaa, 0x6f, 0xff}: Gold,
{0xf5, 0xea, 0xd4, 0xff}: Gold,
}
func Colorful(actualColor colorful.Color) (result Color) {
var distance = 1.0
for rgba, i := range ColorMap {
colorColorful, _ := colorful.MakeColor(rgba)
currentDistance := colorColorful.DistanceLab(actualColor)
if distance >= currentDistance {
distance = currentDistance
result = i
}
}
return result
}

View file

@ -0,0 +1,11 @@
package colors
import (
"testing"
)
func TestColors_List(t *testing.T) {
allColors := All.List()
t.Logf("colors: %+v", allColors)
}

View file

@ -6,12 +6,14 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
gc "github.com/patrickmn/go-cache"
"github.com/photoprism/photoprism/internal/colors"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/tidb"
@ -549,6 +551,7 @@ func (c *Config) MigrateDb() {
&entity.File{},
&entity.Photo{},
&entity.Event{},
&entity.Place{},
&entity.Location{},
&entity.Camera{},
&entity.Lens{},
@ -563,6 +566,9 @@ func (c *Config) MigrateDb() {
&entity.Keyword{},
&entity.PhotoKeyword{},
)
entity.CreateUnknownPlace(db)
entity.CreateUnknownCountry(db)
}
// ClientConfig returns a loaded and set configuration entity.
@ -570,6 +576,7 @@ func (c *Config) ClientConfig() ClientConfig {
db := c.Db()
var cameras []*entity.Camera
var lenses []*entity.Lens
var albums []*entity.Album
var position struct {
@ -614,11 +621,11 @@ func (c *Config) ClientConfig() ClientConfig {
Take(&count)
db.Table("countries").
Select("COUNT(*) AS countries").
Select("(COUNT(*) - 1) AS countries").
Take(&count)
db.Table("locations").
Select("COUNT(DISTINCT loc_place) AS places").
db.Table("places").
Select("(COUNT(*) - 1) AS places").
Take(&count)
type country struct {
@ -637,10 +644,39 @@ func (c *Config) ClientConfig() ClientConfig {
Limit(1000).Order("camera_model").
Find(&cameras)
db.Where("deleted_at IS NULL").
Limit(1000).Order("lens_model").
Find(&lenses)
db.Where("deleted_at IS NULL AND album_favorite = 1").
Limit(20).Order("album_name").
Find(&albums)
var years []string
db.Table("photos").
Order("photo_year DESC").
Pluck("DISTINCT photo_year", &years)
type CategoryLabel struct {
LabelName string
Title string
}
var categories []CategoryLabel
db.Table("categories").
Select("l.label_name").
Joins("JOIN labels l ON categories.category_id = l.id").
Group("l.label_name").
Order("l.label_name").
Limit(1000).Offset(0).
Scan(&categories)
for i, l := range categories {
categories[i].Title = strings.Title(l.LabelName)
}
jsHash := util.Hash(c.HttpStaticBuildPath() + "/app.js")
cssHash := util.Hash(c.HttpStaticBuildPath() + "/app.css")
@ -660,6 +696,7 @@ func (c *Config) ClientConfig() ClientConfig {
"public": c.Public(),
"albums": albums,
"cameras": cameras,
"lenses": lenses,
"countries": countries,
"thumbnails": Thumbnails,
"jsHash": jsHash,
@ -667,6 +704,9 @@ func (c *Config) ClientConfig() ClientConfig {
"settings": c.Settings(),
"count": count,
"pos": position,
"years": years,
"colors": colors.All.List(),
"categories": categories,
}
return result

View file

@ -3,6 +3,7 @@ package entity
import (
"github.com/gosimple/slug"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/maps"
)
var altCountryNames = map[string]string{
@ -22,6 +23,12 @@ type Country struct {
New bool `gorm:"-"`
}
var UnknownCountry = NewCountry("zz", maps.CountryNames["zz"])
func CreateUnknownCountry(db *gorm.DB) {
UnknownCountry.FirstOrCreate(db)
}
// Create a new country
func NewCountry(countryCode string, countryName string) *Country {
if countryCode == "" {
@ -54,3 +61,11 @@ func (m *Country) FirstOrCreate(db *gorm.DB) *Country {
func (m *Country) AfterCreate(scope *gorm.Scope) error {
return scope.SetColumn("New", true)
}
func (m *Country) Code() string {
return m.ID
}
func (m *Country) Name() string {
return m.CountryName
}

View file

@ -12,18 +12,14 @@ import (
// Photo location
type Location struct {
ID uint64 `gorm:"type:BIGINT;primary_key;auto_increment:false;"`
PlaceID uint64 `gorm:"type:BIGINT;"`
Place *Place
LocLat float64
LocLng float64
LocName string `gorm:"type:varchar(100);"`
LocCategory string `gorm:"type:varchar(50);"`
LocSuburb string `gorm:"type:varchar(100);"`
LocPlace string `gorm:"type:varbinary(500);index;"`
LocCity string `gorm:"type:varchar(100);"`
LocState string `gorm:"type:varchar(100);"`
LocCountry string `gorm:"type:binary(2);"`
LocSource string `gorm:"type:varbinary(16);"`
LocNotes string `gorm:"type:text;"`
LocFavorite bool
CreatedAt time.Time
UpdatedAt time.Time
}
@ -40,6 +36,7 @@ func NewLocation(lat, lng float64) *Location {
func (m *Location) Find(db *gorm.DB) error {
if err := db.First(m, "id = ?", m.ID).Error; err == nil {
m.Place = FindPlace(m.PlaceID, db)
return nil
}
@ -53,13 +50,19 @@ func (m *Location) Find(db *gorm.DB) error {
return err
}
m.Place = FindPlaceByLabel(l.ID, l.LocLabel, db)
if m.Place.NoID() {
m.Place.ID = l.ID
m.Place.LocLabel = l.LocLabel
m.Place.LocCity = l.LocCity
m.Place.LocState = l.LocState
m.Place.LocCountry = l.LocCountry
}
m.LocName = l.LocName
m.LocCategory = l.LocCategory
m.LocSuburb = l.LocSuburb
m.LocPlace = l.LocPlace
m.LocCity = l.LocCity
m.LocState = l.LocState
m.LocCountry = l.LocCountry
m.LocSource = l.LocSource
if err := db.Create(m).Error; err != nil {
@ -72,16 +75,16 @@ func (m *Location) Find(db *gorm.DB) error {
func (m *Location) Keywords() []string {
result := []string{
strings.ToLower(m.LocCity),
strings.ToLower(m.LocSuburb),
strings.ToLower(m.LocState),
strings.ToLower(m.City()),
strings.ToLower(m.Suburb()),
strings.ToLower(m.State()),
strings.ToLower(m.CountryName()),
strings.ToLower(m.LocCategory),
strings.ToLower(m.Category()),
}
result = append(result, util.Keywords(m.LocName)...)
result = append(result, util.Keywords(m.LocPlace)...)
result = append(result, util.Keywords(m.LocNotes)...)
result = append(result, util.Keywords(m.Name())...)
result = append(result, util.Keywords(m.Label())...)
result = append(result, util.Keywords(m.Notes())...)
return result
}
@ -102,32 +105,64 @@ func (m *Location) Name() string {
return m.LocName
}
func (m *Location) NoName() bool {
return m.LocName == ""
}
func (m *Location) Category() string {
return m.LocCategory
}
func (m *Location) NoCategory() bool {
return m.LocCategory == ""
}
func (m *Location) Suburb() string {
return m.LocSuburb
}
func (m *Location) Place() string {
return m.LocPlace
func (m *Location) NoSuburb() bool {
return m.LocSuburb == ""
}
func (m *Location) Label() string {
return m.Place.Label()
}
func (m *Location) City() string {
return m.LocCity
return m.Place.City()
}
func (m *Location) LongCity() bool {
return len(m.City()) > 16
}
func (m *Location) NoCity() bool {
return m.City() == ""
}
func (m *Location) CityContains(text string) bool {
return strings.Contains(text, m.City())
}
func (m *Location) State() string {
return m.LocState
return m.Place.State()
}
func (m *Location) NoState() bool {
return m.Place.State() == ""
}
func (m *Location) CountryCode() string {
return m.LocCountry
return m.Place.CountryCode()
}
func (m *Location) CountryName() string {
return maps.CountryNames[m.LocCountry]
return m.Place.CountryName()
}
func (m *Location) Notes() string {
return m.Place.Notes()
}
func (m *Location) Source() string {

View file

@ -32,16 +32,19 @@ type Photo struct {
PhotoExposure string `gorm:"type:varbinary(16);"`
PhotoViews uint
Camera *Camera
CameraID uint `gorm:"index;"`
CameraID uint `gorm:"index:idx_photos_camera_lens;"`
Lens *Lens
LensID uint `gorm:"index;"`
Country *Country
CountryID string `gorm:"type:binary(2);index;"`
LensID uint `gorm:"index:idx_photos_camera_lens;"`
CountryChanged bool
Location *Location
LocationID uint64 `gorm:"type:BIGINT;index;"`
Place *Place
PlaceID uint64 `gorm:"type:BIGINT;index;"`
LocationChanged bool
LocationEstimated bool
PhotoCountry string `gorm:"index:idx_photos_country_year_month;"`
PhotoYear int `gorm:"index:idx_photos_country_year_month;"`
PhotoMonth int `gorm:"index:idx_photos_country_year_month;"`
TakenAt time.Time `gorm:"type:datetime;index;"`
TakenAtLocal time.Time `gorm:"type:datetime;"`
TakenAtChanged bool
@ -146,7 +149,22 @@ func (m *Photo) NoLocation() bool {
return m.LocationID == 0
}
func (m *Photo) HasLocation() bool {
return m.LocationID != 0
}
func (m *Photo) NoPlace() bool {
return m.PlaceID < 5
}
func (m *Photo) HasPlace() bool {
return m.PlaceID >= 5
}
func (m *Photo) NoTitle() bool {
return m.PhotoTitle == ""
}
func (m *Photo) HasTitle() bool {
return m.PhotoTitle != ""
}

108
internal/entity/place.go Normal file
View file

@ -0,0 +1,108 @@
package entity
import (
"time"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/maps"
)
// Photo place
type Place struct {
ID uint64 `gorm:"type:BIGINT;primary_key;auto_increment:false;"`
LocLabel string `gorm:"type:varbinary(500);unique_index;"`
LocCity string `gorm:"type:varchar(100);"`
LocState string `gorm:"type:varchar(100);"`
LocCountry string `gorm:"type:binary(2);"`
LocNotes string `gorm:"type:text;"`
LocFavorite bool
CreatedAt time.Time
UpdatedAt time.Time
New bool `gorm:"-"`
}
var UnknownPlace = NewPlace(1, "Unknown", "Unknown", "Unknown", "zz")
func CreateUnknownPlace(db *gorm.DB) {
UnknownPlace.FirstOrCreate(db)
}
func (m *Place) AfterCreate(scope *gorm.Scope) error {
return scope.SetColumn("New", true)
}
func FindPlace(id uint64, db *gorm.DB) *Place {
place := &Place{}
if err := db.First(place, "id = ?", id).Error; err != nil {
log.Debugf("place: %s for id %d", err.Error(), id)
}
return place
}
func FindPlaceByLabel(id uint64, label string, db *gorm.DB) *Place {
place := &Place{}
if err := db.First(place, "id = ? OR loc_label = ?", id, label).Error; err != nil {
log.Debugf("place: %s for id %d or label \"%s\"", err.Error(), id, label)
}
return place
}
func NewPlace(id uint64, label, city, state, countryCode string) *Place {
result := &Place{
ID: id,
LocLabel: label,
LocCity: city,
LocState: state,
LocCountry: countryCode,
}
return result
}
func (m *Place) Find(db *gorm.DB) error {
if err := db.First(m, "id = ?", m.ID).Error; err != nil {
return err
}
return nil
}
func (m *Place) FirstOrCreate(db *gorm.DB) *Place {
if err := db.FirstOrCreate(m, "id = ? OR loc_label = ?", m.ID, m.LocLabel).Error; err != nil {
log.Debugf("place: %s for id %d or label \"%s\"", err.Error(), m.ID, m.LocLabel)
}
return m
}
func (m *Place) NoID() bool {
return m.ID == 0
}
func (m *Place) Label() string {
return m.LocLabel
}
func (m *Place) City() string {
return m.LocCity
}
func (m *Place) State() string {
return m.LocState
}
func (m *Place) CountryCode() string {
return m.LocCountry
}
func (m *Place) CountryName() string {
return maps.CountryNames[m.LocCountry]
}
func (m *Place) Notes() string {
return m.LocNotes
}

View file

@ -251,4 +251,5 @@ var CountryNames = map[string]string{
"ye": "Yemen",
"zm": "Zambia",
"zw": "Zimbabwe",
"zz": "Unknown",
}

View file

@ -994,5 +994,9 @@
{
"Code": "ZW",
"Name": "Zimbabwe"
},
{
"Code": "ZZ",
"Name": "Unknown"
}
]

View file

@ -7,6 +7,21 @@ import (
"github.com/photoprism/photoprism/internal/maps/osm"
)
/* TODO
(SELECT pl.loc_label as album_name, pl.loc_country, YEAR(ph.taken_at) as taken_year, round(count(ph.id)) as photo_count FROM photos ph
JOIN places pl ON ph.place_id = pl.id AND pl.id <> 1
GROUP BY album_name, taken_year HAVING photo_count > 5) UNION (
SELECT c.country_name AS album_name, pl.loc_country, YEAR(ph.taken_at) as taken_year, round(count(ph.id)) as photo_count FROM photos ph
JOIN places pl ON ph.place_id = pl.id AND pl.id <> 1
JOIN countries c ON c.id = pl.loc_country
GROUP BY album_name, taken_year
HAVING photo_count > 10)
ORDER BY loc_country, album_name, taken_year;
*/
// Photo location
type Location struct {
ID uint64
@ -15,7 +30,7 @@ type Location struct {
LocName string
LocCategory string
LocSuburb string
LocPlace string
LocLabel string
LocCity string
LocState string
LocCountry string
@ -81,7 +96,7 @@ func (l *Location) Assign(s LocationSource) error {
l.LocState = s.State()
l.LocCountry = s.CountryCode()
l.LocCategory = s.Category()
l.LocPlace = l.place()
l.LocLabel = l.label()
return nil
}
@ -94,7 +109,7 @@ func (l *Location) Unknown() bool {
return false
}
func (l *Location) place() string {
func (l *Location) label() string {
if l.Unknown() {
return "Unknown"
}
@ -103,9 +118,8 @@ func (l *Location) place() string {
var loc []string
shortCountry := len([]rune(countryName)) <= 20
shortCity := len([]rune(l.LocCity)) <= 20
if shortCity && l.LocCity != "" {
if l.LocCity != "" {
loc = append(loc, l.LocCity)
}
@ -140,8 +154,8 @@ func (l Location) Suburb() string {
return l.LocSuburb
}
func (l Location) Place() string {
return l.LocPlace
func (l Location) Label() string {
return l.LocLabel
}
func (l Location) City() string {

View file

@ -19,7 +19,7 @@ func TestLocation_Query(t *testing.T) {
}
assert.Equal(t, "Fernsehturm Berlin", l.LocName)
assert.Equal(t, "Berlin, Germany", l.LocPlace)
assert.Equal(t, "Berlin, Germany", l.LocLabel)
})
}
@ -48,7 +48,7 @@ func TestLocation_Assign(t *testing.T) {
}
assert.Equal(t, "Fernsehturm Berlin", l.LocName)
assert.Equal(t, "Berlin, Germany", l.LocPlace)
assert.Equal(t, "Berlin, Germany", l.LocLabel)
})
t.Run("SantaMonica", func(t *testing.T) {
@ -76,7 +76,7 @@ func TestLocation_Assign(t *testing.T) {
}
assert.Equal(t, "Santa Monica Pier", l.LocName)
assert.Equal(t, "Santa Monica, California, USA", l.LocPlace)
assert.Equal(t, "Santa Monica, California, USA", l.LocLabel)
})
t.Run("AirportZurich", func(t *testing.T) {
@ -105,7 +105,7 @@ func TestLocation_Assign(t *testing.T) {
}
assert.Equal(t, "Airport", l.LocName)
assert.Equal(t, "Kloten, Zurich, Switzerland", l.LocPlace)
assert.Equal(t, "Kloten, Zurich, Switzerland", l.LocLabel)
})
t.Run("AirportTegel", func(t *testing.T) {
@ -134,7 +134,7 @@ func TestLocation_Assign(t *testing.T) {
}
assert.Equal(t, "Airport", l.LocName)
assert.Equal(t, "Berlin, Germany", l.LocPlace)
assert.Equal(t, "Berlin, Germany", l.LocLabel)
})
t.Run("PinkBeach", func(t *testing.T) {
@ -164,7 +164,7 @@ func TestLocation_Assign(t *testing.T) {
assert.Equal(t, uint64(0x149ce78540000000), l.ID)
assert.Equal(t, "Pink Beach", l.LocName)
assert.Equal(t, "Chrisoskalitissa, Crete, Greece", l.LocPlace)
assert.Equal(t, "Chrisoskalitissa, Crete, Greece", l.LocLabel)
})
t.Run("NewJersey", func(t *testing.T) {
@ -194,7 +194,7 @@ func TestLocation_Assign(t *testing.T) {
assert.Equal(t, uint64(0x9c25741c0000000), l.ID)
assert.Equal(t, "", l.LocName)
assert.Equal(t, "Jersey City, New Jersey, USA", l.LocPlace)
assert.Equal(t, "Jersey City, New Jersey, USA", l.LocLabel)
})
t.Run("SouthAfrica", func(t *testing.T) {
@ -224,7 +224,7 @@ func TestLocation_Assign(t *testing.T) {
assert.Equal(t, uint64(0x1e5e4205c0000000), l.ID)
assert.Equal(t, "R411", l.LocName)
assert.Equal(t, "Eastern Cape, South Africa", l.LocPlace)
assert.Equal(t, "Eastern Cape, South Africa", l.LocLabel)
})
t.Run("Unknown", func(t *testing.T) {
@ -272,7 +272,7 @@ func TestLocation_place(t *testing.T) {
l := NewLocation(lat, lng)
assert.Equal(t, "Unknown", l.place())
assert.Equal(t, "Unknown", l.label())
})
t.Run("Nürnberg, Bayern, Germany", func(t *testing.T) {
lat := -31.976301666666668
@ -280,7 +280,7 @@ func TestLocation_place(t *testing.T) {
l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern"}
assert.Equal(t, "Nürnberg, Bayern, Germany", l.place())
assert.Equal(t, "Nürnberg, Bayern, Germany", l.label())
})
}
@ -373,13 +373,13 @@ func TestLocation_Source(t *testing.T) {
}
func TestLocation_Place(t *testing.T) {
t.Run("test-place", func(t *testing.T) {
t.Run("test-label", func(t *testing.T) {
lat := -31.976301666666668
lng := 29.148046666666666
l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocPlace: "test-place", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
assert.Equal(t, "test-place", l.Place())
assert.Equal(t, "test-label", l.Label())
})
}
@ -388,7 +388,7 @@ func TestLocation_CountryCode(t *testing.T) {
lat := -31.976301666666668
lng := 29.148046666666666
l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocPlace: "test-place", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
assert.Equal(t, "de", l.CountryCode())
})
@ -399,7 +399,7 @@ func TestLocation_CountryName(t *testing.T) {
lat := -31.976301666666668
lng := 29.148046666666666
l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocPlace: "test-place", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
assert.Equal(t, "Germany", l.CountryName())
})

View file

@ -85,6 +85,10 @@ func (o Location) City() (result string) {
result = o.Address.State
}
if len([]rune(result)) > 19 {
result = ""
}
return strings.TrimSpace(result)
}

View file

@ -86,7 +86,7 @@ func TestOSM_State(t *testing.T) {
assert.Equal(t, "Berlin", l.State())
})
}
/*
func TestOSM_City(t *testing.T) {
t.Run("Berlin", func(t *testing.T) {
@ -113,13 +113,12 @@ func TestOSM_City(t *testing.T) {
assert.Equal(t, "Wiesbaden", l.City())
})
t.Run("Frankfurt", func(t *testing.T) {
a := Address{CountryCode: "de", City: "", State: "Frankfurt", HouseNumber: "63", Suburb: "Neukölln", Town: "", Village: "", County: ""}
a := Address{CountryCode: "de", City: "Frankfurt", State: "", HouseNumber: "63", Suburb: "Neukölln", Town: "", Village: "", County: ""}
l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name", Address: a}
assert.Equal(t, "Frankfurt", l.City())
})
}
*/
func TestOSM_Suburb(t *testing.T) {
t.Run("Neukölln", func(t *testing.T) {

View file

@ -2,413 +2,15 @@ package photoprism
import (
"errors"
"fmt"
"image/color"
"math"
"github.com/lucasb-eyer/go-colorful"
"github.com/photoprism/photoprism/internal/colors"
)
type ColorPerception struct {
Colors IndexedColors
MainColor IndexedColor
Luminance LightMap
Chroma Chroma
}
type IndexedColor uint16
type IndexedColors []IndexedColor
type Chroma uint8
type Luminance uint8
type LightMap []Luminance
const (
Black IndexedColor = iota
Brown
Grey
White
Purple
Gold
Blue
Cyan
Teal
Green
Lime
Yellow
Magenta
Orange
Red
Pink
)
var IndexedColorNames = map[IndexedColor]string{
Black: "dark", // 0
Brown: "brown", // 1
Grey: "grey", // 2
White: "bright", // 3
Purple: "purple", // 4
Gold: "gold", // 5
Blue: "blue", // 6
Cyan: "cyan", // 7
Teal: "teal", // 8
Green: "green", // 9
Lime: "lime", // A
Yellow: "yellow", // B
Magenta: "magenta", // C
Orange: "orange", // D
Red: "red", // E
Pink: "pink", // F
}
var IndexedColorWeight = map[IndexedColor]uint16{
Black: 2,
Brown: 2,
Grey: 1,
White: 2,
Purple: 4,
Gold: 4,
Blue: 3,
Cyan: 4,
Teal: 4,
Green: 3,
Lime: 5,
Yellow: 5,
Magenta: 5,
Orange: 4,
Red: 4,
Pink: 4,
}
func (c IndexedColor) Name() string {
return IndexedColorNames[c]
}
func (c IndexedColor) Hex() string {
return fmt.Sprintf("%X", c)
}
func (c IndexedColors) Hex() (result string) {
for _, indexedColor := range c {
result += indexedColor.Hex()
}
return result
}
func (c Chroma) Hex() string {
return fmt.Sprintf("%X", c)
}
func (c Chroma) Uint() uint {
return uint(c)
}
func (c Chroma) Int() int {
return int(c)
}
func (l Luminance) Hex() string {
return fmt.Sprintf("%X", l)
}
func (m LightMap) Hex() (result string) {
for _, luminance := range m {
result += luminance.Hex()
}
return result
}
var IndexedColorMap = map[color.RGBA]IndexedColor{
{0x00, 0x00, 0x00, 0xff}: Black,
{0xa1, 0x88, 0x7f, 0xff}: Brown,
{0x8d, 0x6e, 0x63, 0xff}: Brown,
{0xa0, 0x7f, 0x6c, 0xff}: Brown,
{0x9b, 0x7b, 0x5b, 0xff}: Brown,
{0x75, 0x64, 0x5b, 0xff}: Brown,
{0x79, 0x55, 0x48, 0xff}: Brown,
{0x6d, 0x4c, 0x41, 0xff}: Brown,
{0x5d, 0x40, 0x37, 0xff}: Brown,
{0x9b, 0x61, 0x36, 0xff}: Brown,
{0xc1, 0xa4, 0x87, 0xff}: Brown,
{0xaa, 0x80, 0x62, 0xff}: Brown,
{0x6b, 0x55, 0x46, 0xff}: Brown,
{0xb4, 0xb5, 0x9c, 0xff}: Brown,
{0xb2, 0xb4, 0x9b, 0xff}: Green,
{0xe0, 0xe0, 0xe0, 0xff}: Grey,
{0x9E, 0x9E, 0x9E, 0xff}: Grey,
{0x75, 0x75, 0x75, 0xff}: Grey,
{0x61, 0x61, 0x61, 0xff}: Grey,
{0x42, 0x42, 0x42, 0xff}: Grey,
{0x84, 0x7a, 0x72, 0xff}: Grey,
{0xdf, 0xe0, 0xe1, 0xff}: Grey,
{0xFF, 0xFF, 0xFF, 0xff}: White,
{0xe4, 0xe4, 0xe4, 0xff}: White,
{0xe7, 0xe7, 0xe7, 0xff}: White,
{0xf3, 0xe5, 0xf5, 0xff}: Purple,
{0xe1, 0xbe, 0xe7, 0xff}: Purple,
{0xce, 0x93, 0xd8, 0xff}: Purple,
{0xba, 0x68, 0xc8, 0xff}: Purple,
{0xab, 0x47, 0xbc, 0xff}: Purple,
{0x9c, 0x27, 0xb0, 0xff}: Purple,
{0x9b, 0x31, 0x8f, 0xff}: Purple,
{0x86, 0x00, 0x7e, 0xff}: Purple,
{0x8e, 0x24, 0xaa, 0xff}: Purple,
{0x7b, 0x1f, 0xa2, 0xff}: Purple,
{0x6a, 0x1b, 0x9a, 0xff}: Purple,
{0x4a, 0x14, 0x8c, 0xff}: Purple,
{0xaa, 0x00, 0xff, 0xff}: Purple,
{0xed, 0xe7, 0xf6, 0xff}: Purple,
{0xd1, 0xc4, 0xe9, 0xff}: Purple,
{0xb3, 0x9d, 0xdb, 0xff}: Purple,
{0x95, 0x75, 0xcd, 0xff}: Purple,
{0x7e, 0x57, 0xc2, 0xff}: Purple,
{0x5e, 0x35, 0xb1, 0xff}: Purple,
{0x67, 0x3a, 0xb7, 0xff}: Purple,
{0x51, 0x2d, 0xa8, 0xff}: Purple,
{0x45, 0x27, 0xa0, 0xff}: Purple,
{0x31, 0x1b, 0x92, 0xff}: Purple,
{0xb3, 0x88, 0xff, 0xff}: Purple,
{0x7c, 0x4d, 0xff, 0xff}: Purple,
{0x8e, 0x64, 0x93, 0xff}: Purple,
{0x5e, 0x3a, 0x5e, 0xff}: Purple,
{0x44, 0x0e, 0x79, 0xff}: Purple,
{0x48, 0x36, 0x78, 0xff}: Purple,
{0x4e, 0x38, 0x80, 0xff}: Purple,
{0x3b, 0x0e, 0x79, 0xff}: Purple,
{0x3F, 0x51, 0xB5, 0xff}: Blue,
{0xc5, 0xca, 0xe9, 0xff}: Blue,
{0x5c, 0x6b, 0xc0, 0xff}: Blue,
{0x39, 0x49, 0xab, 0xff}: Blue,
{0x30, 0x3f, 0x9f, 0xff}: Blue,
{0x28, 0x35, 0x93, 0xff}: Blue,
{0x1a, 0x23, 0x7e, 0xff}: Blue,
{0x53, 0x6d, 0xfe, 0xff}: Blue,
{0x3d, 0x5a, 0xfe, 0xff}: Blue,
{0x30, 0x4f, 0xfe, 0xff}: Blue,
{0x21, 0x96, 0xF3, 0xff}: Blue,
{0xbb, 0xde, 0xfb, 0xff}: Blue,
{0x90, 0xca, 0xf9, 0xff}: Blue,
{0x64, 0xb5, 0xf6, 0xff}: Blue,
{0x42, 0xa5, 0xf5, 0xff}: Blue,
{0x1e, 0x88, 0xe5, 0xff}: Blue,
{0x19, 0x76, 0xd2, 0xff}: Blue,
{0x15, 0x65, 0xc0, 0xff}: Blue,
{0x0d, 0x47, 0xa1, 0xff}: Blue,
{0x82, 0xb1, 0xff, 0xff}: Blue,
{0x44, 0x8a, 0xff, 0xff}: Blue,
{0x29, 0x79, 0xff, 0xff}: Blue,
{0x29, 0x62, 0xff, 0xff}: Blue,
{0x03, 0xa9, 0xf6, 0xff}: Blue,
{0xb3, 0xe5, 0xfc, 0xff}: Blue,
{0x81, 0xd4, 0xfa, 0xff}: Blue,
{0x4f, 0xc3, 0xf7, 0xff}: Blue,
{0x29, 0xb6, 0xf6, 0xff}: Blue,
{0x03, 0x9b, 0xe5, 0xff}: Blue,
{0x02, 0x88, 0xd1, 0xff}: Blue,
{0x02, 0x77, 0xbd, 0xff}: Blue,
{0x01, 0x57, 0x9b, 0xff}: Blue,
{0x80, 0xd8, 0xff, 0xff}: Blue,
{0x40, 0xc4, 0xff, 0xff}: Blue,
{0x00, 0xb0, 0xff, 0xff}: Blue,
{0x00, 0x91, 0xea, 0xff}: Blue,
{0x60, 0x7d, 0x8b, 0xff}: Blue,
{0x78, 0x90, 0x9c, 0xff}: Blue,
{0x54, 0x6e, 0x7a, 0xff}: Blue,
{0x37, 0x47, 0x4f, 0xff}: Blue,
{0xe4, 0xeb, 0xfd, 0xff}: Blue,
{0x7d, 0xd3, 0xea, 0xff}: Blue,
{0x07, 0x63, 0x99, 0xff}: Blue,
{0x28, 0x44, 0x6b, 0xff}: Blue,
{0x4a, 0xc8, 0xf5, 0xff}: Blue,
{0x08, 0x00, 0xf4, 0xff}: Blue,
{0x01, 0x2d, 0x5f, 0xff}: Blue,
{0xb2, 0xeb, 0xf2, 0xff}: Cyan,
{0x80, 0xde, 0xea, 0xff}: Cyan,
{0x4d, 0xd0, 0xe1, 0xff}: Cyan,
{0x26, 0xc6, 0xda, 0xff}: Cyan,
{0x00, 0xb8, 0xd4, 0xff}: Cyan,
{0x00, 0xBC, 0xD4, 0xff}: Cyan,
{0x00, 0xac, 0xc1, 0xff}: Cyan,
{0x00, 0x97, 0xa7, 0xff}: Cyan,
{0x00, 0x83, 0x8f, 0xff}: Cyan,
{0x00, 0x60, 0x64, 0xff}: Cyan,
{0x84, 0xff, 0xff, 0xff}: Cyan,
{0x18, 0xff, 0xff, 0xff}: Cyan,
{0x00, 0xe5, 0xff, 0xff}: Cyan,
{0x00, 0x96, 0x88, 0xff}: Teal,
{0x00, 0x89, 0x7b, 0xff}: Teal,
{0x00, 0x79, 0x6b, 0xff}: Teal,
{0x00, 0x69, 0x5c, 0xff}: Teal,
{0x04, 0x5d, 0x5c, 0xff}: Teal,
{0x24, 0x5a, 0x5f, 0xff}: Teal,
{0x03, 0x45, 0x4f, 0xff}: Teal,
{0x2c, 0x54, 0x5e, 0xff}: Teal,
{0x17, 0x47, 0x41, 0xff}: Teal,
{0xe8, 0xf5, 0xe9, 0xff}: Green,
{0xc8, 0xe6, 0xc9, 0xff}: Green,
{0xab, 0xc7, 0xb0, 0xff}: Green,
{0xa5, 0xd6, 0xa7, 0xff}: Green,
{0x81, 0xc7, 0x84, 0xff}: Green,
{0x66, 0xbb, 0x6a, 0xff}: Green,
{0x4C, 0xAF, 0x50, 0xff}: Green,
{0x43, 0xa0, 0x47, 0xff}: Green,
{0x38, 0x8e, 0x3c, 0xff}: Green,
{0x2e, 0x7d, 0x32, 0xff}: Green,
{0x1b, 0x5e, 0x20, 0xff}: Green,
{0xf1, 0xf8, 0xe9, 0xff}: Green,
{0xdc, 0xed, 0xc8, 0xff}: Green,
{0xc5, 0xe1, 0xa5, 0xff}: Green,
{0xae, 0xd5, 0x81, 0xff}: Green,
{0x8b, 0xc3, 0x4a, 0xff}: Green,
{0x9c, 0xcc, 0x65, 0xff}: Green,
{0x7c, 0xb3, 0x42, 0xff}: Green,
{0x68, 0x9f, 0x38, 0xff}: Green,
{0x55, 0x8b, 0x2f, 0xff}: Green,
{0x33, 0x69, 0x1e, 0xff}: Green,
{0xb9, 0xf6, 0xca, 0xff}: Green,
{0x69, 0xf0, 0xae, 0xff}: Green,
{0x00, 0xc8, 0x53, 0xff}: Green,
{0x00, 0xe6, 0x76, 0xff}: Green,
{0xcc, 0xff, 0x90, 0xff}: Green,
{0xb2, 0xff, 0x59, 0xff}: Green,
{0x76, 0xff, 0x03, 0xff}: Green,
{0x64, 0xdd, 0x17, 0xff}: Green,
{0xdd, 0xd5, 0x79, 0xff}: Green,
{0xee, 0xec, 0xa2, 0xff}: Green,
{0x24, 0x4e, 0x3b, 0xff}: Green,
{0x9a, 0x9d, 0x47, 0xff}: Green,
{0xbe, 0xbd, 0x76, 0xff}: Green,
{0x5c, 0x5a, 0x30, 0xff}: Green,
{0xb3, 0xc1, 0x6c, 0xff}: Green,
{0xac, 0xa7, 0x83, 0xff}: Green,
{0x47, 0x4c, 0x25, 0xff}: Green,
{0xcd, 0xd0, 0x87, 0xff}: Green,
{0x79, 0x6d, 0x41, 0xff}: Green,
{0xf0, 0xf4, 0xc3, 0xff}: Lime,
{0xe6, 0xee, 0x9c, 0xff}: Lime,
{0xdc, 0xe7, 0x75, 0xff}: Lime,
{0xd4, 0xe1, 0x57, 0xff}: Lime,
{0xCD, 0xDC, 0x39, 0xff}: Lime,
{0xc0, 0xca, 0x33, 0xff}: Lime,
{0xaf, 0xb4, 0x2b, 0xff}: Lime,
{0xee, 0xff, 0x41, 0xff}: Lime,
{0xc6, 0xff, 0x00, 0xff}: Lime,
{0xae, 0xea, 0x00, 0xff}: Lime,
{0xff, 0xf9, 0xc4, 0xff}: Yellow,
{0xff, 0xf5, 0x9d, 0xff}: Yellow,
{0xff, 0xf1, 0x76, 0xff}: Yellow,
{0xff, 0xee, 0x58, 0xff}: Yellow,
{0xff, 0xff, 0x8d, 0xff}: Yellow,
{0xff, 0xff, 0x00, 0xff}: Yellow,
{0xff, 0xd5, 0x4f, 0xff}: Yellow,
{0xff, 0xca, 0x28, 0xff}: Yellow,
{0xe3, 0xce, 0x81, 0xff}: Yellow,
{0xd1, 0xaf, 0x52, 0xff}: Yellow,
{0xee, 0xbb, 0x2b, 0xff}: Yellow,
{0xd3, 0xa8, 0x3a, 0xff}: Yellow,
{0xc5, 0xa7, 0x02, 0xff}: Yellow,
{0x9f, 0x82, 0x01, 0xff}: Yellow,
{0xe8, 0xce, 0x03, 0xff}: Yellow,
{0xf9, 0xa8, 0x25, 0xff}: Orange,
{0xFF, 0x98, 0x00, 0xff}: Orange,
{0xff, 0xa7, 0x26, 0xff}: Orange,
{0xfb, 0x8c, 0x00, 0xff}: Orange,
{0xf5, 0x7c, 0x00, 0xff}: Orange,
{0xef, 0x6c, 0x00, 0xff}: Orange,
{0xff, 0x91, 0x00, 0xff}: Orange,
{0xff, 0x6d, 0x00, 0xff}: Orange,
{0xfd, 0x9a, 0x31, 0xff}: Orange,
{0x7d, 0x27, 0x04, 0xff}: Orange,
{0xfd, 0x57, 0x1f, 0xff}: Orange,
{0xf8, 0x67, 0x04, 0xff}: Orange,
{0xfd, 0x9a, 0x00, 0xff}: Orange,
{0xfe, 0x8a, 0x00, 0xff}: Orange,
{0xf1, 0x96, 0x52, 0xff}: Orange,
{0xe5, 0x83, 0x47, 0xff}: Orange,
{0xc9, 0x4c, 0x30, 0xff}: Orange,
{0x9f, 0x56, 0x01, 0xff}: Orange,
{0xfa, 0x68, 0x01, 0xff}: Orange,
{0xbb, 0x72, 0x3d, 0xff}: Orange,
{0xff, 0x52, 0x52, 0xff}: Red,
{0xf4, 0x43, 0x36, 0xff}: Red,
{0xef, 0x53, 0x50, 0xff}: Red,
{0xe5, 0x39, 0x35, 0xff}: Red,
{0xf6, 0x29, 0x2e, 0xff}: Red,
{0xfc, 0x25, 0x2d, 0xff}: Red,
{0xd3, 0x2f, 0x2f, 0xff}: Red,
{0xc6, 0x28, 0x28, 0xff}: Red,
{0xba, 0x28, 0x30, 0xff}: Red,
{0xb7, 0x1c, 0x1c, 0xff}: Red,
{0xd5, 0x00, 0x00, 0xff}: Red,
{0xdb, 0x08, 0x06, 0xff}: Red,
{0xcf, 0x09, 0x04, 0xff}: Red,
{0xd8, 0x1a, 0x14, 0xff}: Red,
{0xcc, 0x17, 0x08, 0xff}: Red,
{0xd8, 0x0a, 0x07, 0xff}: Red,
{0xde, 0x26, 0x16, 0xff}: Red,
{0xee, 0x24, 0x0f, 0xff}: Red,
{0xa1, 0x21, 0x1f, 0xff}: Red,
{0x70, 0x12, 0x19, 0xff}: Red,
{0x51, 0x12, 0x18, 0xff}: Red,
{0x49, 0x11, 0x14, 0xff}: Red,
{0xfc, 0xe4, 0xec, 0xff}: Pink,
{0xfd, 0xc8, 0xeb, 0xff}: Pink,
{0xe7, 0x9f, 0xa6, 0xff}: Pink,
{0xf8, 0xbb, 0xd0, 0xff}: Pink,
{0xf4, 0x8f, 0xb1, 0xff}: Pink,
{0xff, 0x80, 0xab, 0xff}: Pink,
{0xff, 0x40, 0x81, 0xff}: Pink,
{0xf5, 0x00, 0x57, 0xff}: Pink,
{0xf0, 0x62, 0x92, 0xff}: Pink,
{0xec, 0x40, 0x7a, 0xff}: Pink,
{0xe9, 0x1e, 0x63, 0xff}: Pink,
{0xd8, 0x1b, 0x60, 0xff}: Pink,
{0xc2, 0x18, 0x5b, 0xff}: Pink,
{0xff, 0x00, 0xff, 0xff}: Magenta,
{0xe5, 0x00, 0xe5, 0xff}: Magenta,
{0xf0, 0x00, 0xb5, 0xff}: Magenta,
{0xce, 0x00, 0x9b, 0xff}: Magenta,
{0xc0, 0x05, 0x5b, 0xff}: Magenta,
{0xb0, 0x00, 0x85, 0xff}: Magenta,
{0xa8, 0x28, 0x63, 0xff}: Magenta,
{0x5b, 0x00, 0x2f, 0xff}: Magenta,
{0x4b, 0x01, 0x21, 0xff}: Magenta,
{0x86, 0x02, 0x25, 0xff}: Magenta,
{0xcb, 0x02, 0x3d, 0xff}: Magenta,
{0x64, 0x07, 0x1a, 0xff}: Magenta,
{0x9e, 0x00, 0x47, 0xff}: Magenta,
{0xdc, 0x7a, 0xcf, 0xff}: Magenta,
{0xed, 0xde, 0xac, 0xff}: Gold,
{0xe8, 0xb4, 0x51, 0xff}: Gold,
{0xc0, 0x8a, 0x3e, 0xff}: Gold,
{0xa2, 0x7d, 0x4b, 0xff}: Gold,
{0x75, 0x55, 0x31, 0xff}: Gold,
{0xd1, 0x93, 0x27, 0xff}: Gold,
{0xde, 0xa2, 0x53, 0xff}: Gold,
{0xd5, 0xaa, 0x6f, 0xff}: Gold,
{0xf5, 0xea, 0xd4, 0xff}: Gold,
}
func ColorfulToIndexedColor(actualColor colorful.Color) (result IndexedColor) {
var distance = 1.0
for rgba, i := range IndexedColorMap {
colorColorful, _ := colorful.MakeColor(rgba)
currentDistance := colorColorful.DistanceLab(actualColor)
if distance >= currentDistance {
distance = currentDistance
result = i
}
}
return result
}
// Colors returns color information for a media file.
func (m *MediaFile) Colors(thumbPath string) (perception ColorPerception, err error) {
func (m *MediaFile) Colors(thumbPath string) (perception colors.ColorPerception, err error) {
if !m.IsJpeg() {
return perception, errors.New("no color information: not a JPEG file")
}
@ -426,20 +28,20 @@ func (m *MediaFile) Colors(thumbPath string) (perception ColorPerception, err er
pixels := float64(width * height)
chromaSum := 0.0
colorCount := make(map[IndexedColor]uint16)
colorCount := make(map[colors.Color]uint16)
var mainColorCount uint16
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
r, g, b, a := img.At(x, y).RGBA()
rgb, _ := colorful.MakeColor(color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)})
i := ColorfulToIndexedColor(rgb)
i := colors.Colorful(rgb)
perception.Colors = append(perception.Colors, i)
if _, ok := colorCount[i]; ok == true {
colorCount[i] += IndexedColorWeight[i]
colorCount[i] += colors.Weights[i]
} else {
colorCount[i] = IndexedColorWeight[i]
colorCount[i] = colors.Weights[i]
}
if colorCount[i] > mainColorCount {
@ -451,11 +53,11 @@ func (m *MediaFile) Colors(thumbPath string) (perception ColorPerception, err er
chromaSum += c
perception.Luminance = append(perception.Luminance, Luminance(math.Round(l*15)))
perception.Luminance = append(perception.Luminance, colors.Luminance(math.Round(l*15)))
}
}
perception.Chroma = Chroma(math.Round((chromaSum / pixels) * 100))
perception.Chroma = colors.Chroma(math.Round((chromaSum / pixels) * 100))
return perception, nil
}

View file

@ -8,6 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/colors"
)
func TestMediaFile_Colors_Testdata(t *testing.T) {
@ -20,35 +21,35 @@ func TestMediaFile_Colors_Testdata(t *testing.T) {
/*
TODO: Add and compare other images in "testdata/"
*/
expected := map[string]ColorPerception{
expected := map[string]colors.ColorPerception{
"elephant_mono.jpg": {
Colors: IndexedColors{0x2, 0x2, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0},
Colors: colors.Colors{0x2, 0x2, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0},
MainColor: 0,
Luminance: LightMap{0xa, 0x9, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0},
Luminance: colors.LightMap{0xa, 0x9, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0},
Chroma: 0,
},
"sharks_blue.jpg": {
Colors: IndexedColors{0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x4, 0x4, 0x6},
Colors: colors.Colors{0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x4, 0x4, 0x6},
MainColor: 6,
Luminance: LightMap{0x9, 0x7, 0x5, 0x4, 0x3, 0x4, 0x3, 0x3, 0x3},
Luminance: colors.LightMap{0x9, 0x7, 0x5, 0x4, 0x3, 0x4, 0x3, 0x3, 0x3},
Chroma: 89,
},
"cat_black.jpg": {
Colors: IndexedColors{0x2, 0x1, 0x1, 0x1, 0x2, 0x1, 0x2, 0x5, 0x2},
Colors: colors.Colors{0x2, 0x1, 0x1, 0x1, 0x2, 0x1, 0x2, 0x5, 0x2},
MainColor: 1,
Luminance: LightMap{0x8, 0xc, 0x9, 0x4, 0x2, 0x7, 0xd, 0xd, 0x3},
Luminance: colors.LightMap{0x8, 0xc, 0x9, 0x4, 0x2, 0x7, 0xd, 0xd, 0x3},
Chroma: 9,
},
"cat_brown.jpg": {
Colors: IndexedColors{0x9, 0x5, 0x1, 0x2, 0x2, 0x1, 0x0, 0x6, 0x2},
Colors: colors.Colors{0x9, 0x5, 0x1, 0x2, 0x2, 0x1, 0x0, 0x6, 0x2},
MainColor: 5,
Luminance: LightMap{0x4, 0x5, 0xb, 0x4, 0x7, 0x3, 0x2, 0x5, 0x7},
Luminance: colors.LightMap{0x4, 0x5, 0xb, 0x4, 0x7, 0x3, 0x2, 0x5, 0x7},
Chroma: 13,
},
"cat_yellow_grey.jpg": {
Colors: IndexedColors{0x2, 0x1, 0x1, 0x9, 0x0, 0x5, 0xb, 0x0, 0x5},
Colors: colors.Colors{0x2, 0x1, 0x1, 0x9, 0x0, 0x5, 0xb, 0x0, 0x5},
MainColor: 5,
Luminance: LightMap{0x9, 0x5, 0xb, 0x6, 0x1, 0x6, 0xa, 0x1, 0x8},
Luminance: colors.LightMap{0x9, 0x5, 0xb, 0x6, 0x1, 0x6, 0xa, 0x1, 0x8},
Chroma: 20,
},
}
@ -105,10 +106,10 @@ func TestMediaFile_Colors(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 13, p.Chroma.Int())
assert.Equal(t, "D", p.Chroma.Hex())
assert.IsType(t, IndexedColors{}, p.Colors)
assert.IsType(t, colors.Colors{}, p.Colors)
assert.Equal(t, "gold", p.MainColor.Name())
assert.Equal(t, IndexedColors{0x9, 0x5, 0x1, 0x2, 0x2, 0x1, 0x0, 0x6, 0x2}, p.Colors)
assert.Equal(t, LightMap{0x4, 0x5, 0xb, 0x4, 0x7, 0x3, 0x2, 0x5, 0x7}, p.Luminance)
assert.Equal(t, colors.Colors{0x9, 0x5, 0x1, 0x2, 0x2, 0x1, 0x0, 0x6, 0x2}, p.Colors)
assert.Equal(t, colors.LightMap{0x4, 0x5, 0xb, 0x4, 0x7, 0x3, 0x2, 0x5, 0x7}, p.Luminance)
} else {
t.Error(err)
}
@ -123,10 +124,10 @@ func TestMediaFile_Colors(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 51, p.Chroma.Int())
assert.Equal(t, "33", p.Chroma.Hex())
assert.IsType(t, IndexedColors{}, p.Colors)
assert.IsType(t, colors.Colors{}, p.Colors)
assert.Equal(t, "lime", p.MainColor.Name())
assert.Equal(t, IndexedColors{0xa, 0x9, 0xa, 0x9, 0xa, 0xa, 0x9, 0x9, 0x9}, p.Colors)
assert.Equal(t, LightMap{0xb, 0x4, 0xa, 0x6, 0x9, 0x8, 0x2, 0x3, 0x4}, p.Luminance)
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)
} else {
t.Error(err)
}
@ -141,9 +142,9 @@ func TestMediaFile_Colors(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 7, p.Chroma.Int())
assert.Equal(t, "7", p.Chroma.Hex())
assert.IsType(t, IndexedColors{}, p.Colors)
assert.IsType(t, colors.Colors{}, p.Colors)
assert.Equal(t, "blue", p.MainColor.Name())
assert.Equal(t, IndexedColors{0x2, 0x6, 0x6, 0x2, 0x2, 0x9, 0x2, 0x0, 0x0}, p.Colors)
assert.Equal(t, colors.Colors{0x2, 0x6, 0x6, 0x2, 0x2, 0x9, 0x2, 0x0, 0x0}, p.Colors)
} else {
t.Error(err)
}
@ -158,10 +159,10 @@ func TestMediaFile_Colors(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 16, p.Chroma.Int())
assert.Equal(t, "10", p.Chroma.Hex())
assert.IsType(t, IndexedColors{}, p.Colors)
assert.IsType(t, colors.Colors{}, p.Colors)
assert.Equal(t, "gold", p.MainColor.Name())
assert.Equal(t, IndexedColors{0x0, 0x0, 0x1, 0x5, 0x5, 0x0, 0x1, 0x5, 0x0}, p.Colors)
assert.Equal(t, colors.Colors{0x0, 0x0, 0x1, 0x5, 0x5, 0x0, 0x1, 0x5, 0x0}, p.Colors)
} else {
t.Error(err)
}

View file

@ -158,6 +158,9 @@ func (i *Indexer) indexMediaFile(m *MediaFile, o IndexerOptions) IndexResult {
}
}
photo.PhotoYear = photo.TakenAt.Year()
photo.PhotoMonth = int(photo.TakenAt.Month())
if photoExists {
// Estimate location
if o.UpdateLocation && photo.NoLocation() {
@ -369,21 +372,27 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
return keywords, labels
}
if location.Place.New {
event.Publish("count.places", event.Data{
"count": 1,
})
}
photo.Location = location
photo.LocationID = location.ID
photo.Place = location.Place
photo.PlaceID = location.PlaceID
photo.LocationEstimated = false
photo.Country = entity.NewCountry(location.CountryCode(), location.CountryName()).FirstOrCreate(i.db)
country := entity.NewCountry(location.CountryCode(), location.CountryName()).FirstOrCreate(i.db)
if photo.Country.New {
if country.New {
event.Publish("count.countries", event.Data{
"count": 1,
})
}
countryName := photo.Country.CountryName
locCategory := location.Category()
keywords = append(keywords, location.Keywords()...)
// Append category from reverse location lookup
@ -395,10 +404,10 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
if (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false {
if title := labels.Title(location.Name()); title != "" { // TODO: User defined title format
log.Infof("index: using label \"%s\" to create photo title", title)
if location.LocCity == "" || len(location.LocCity) > 16 || strings.Contains(title, location.LocCity) {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), countryName, photo.TakenAt.Format("2006"))
if location.NoCity() || location.LongCity() || location.CityContains(title) {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), location.CountryName(), photo.TakenAt.Format("2006"))
} else {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), location.LocCity, photo.TakenAt.Format("2006"))
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), location.City(), photo.TakenAt.Format("2006"))
}
} else if location.Name() != "" && location.City() != "" {
if len(location.Name()) > 45 {
@ -406,13 +415,13 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
} else if len(location.Name()) > 20 || len(location.City()) > 16 || strings.Contains(location.Name(), location.City()) {
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.Name(), photo.TakenAt.Format("2006"))
} else {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.Name(), location.LocCity, photo.TakenAt.Format("2006"))
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.Name(), location.City(), photo.TakenAt.Format("2006"))
}
} else if location.City() != "" && countryName != "" {
} else if location.City() != "" && location.CountryName() != "" {
if len(location.City()) > 20 {
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.LocCity, photo.TakenAt.Format("2006"))
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.City(), photo.TakenAt.Format("2006"))
} else {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCity, countryName, photo.TakenAt.Format("2006"))
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.City(), location.CountryName(), photo.TakenAt.Format("2006"))
}
}
@ -424,19 +433,24 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
}
} else {
log.Debugf("index: location cannot be determined precisely (%s)", err.Error())
photo.Place = entity.UnknownPlace
photo.PlaceID = entity.UnknownPlace.ID
}
photo.PhotoCountry = photo.Place.LocCountry
return keywords, labels
}
func (i *Indexer) estimateLocation(photo *entity.Photo) {
var recentPhoto entity.Photo
if result := i.db.Unscoped().Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", photo.TakenAt)).Preload("Country").First(&recentPhoto); result.Error == nil {
if recentPhoto.Country != nil {
photo.Country = recentPhoto.Country
if result := i.db.Unscoped().Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", photo.TakenAt)).Preload("Place").First(&recentPhoto); result.Error == nil {
if recentPhoto.HasPlace() {
photo.Place = recentPhoto.Place
photo.PhotoCountry = photo.Place.LocCountry
photo.LocationEstimated = true
log.Debugf("index: approximate location is \"%s\"", recentPhoto.Country.CountryName)
log.Debugf("index: approximate location is \"%s\"", recentPhoto.Place.Label())
}
}
}

View file

@ -43,8 +43,8 @@ func TestMediaFile_Location(t *testing.T) {
t.Fatal(err)
}
assert.Equal(t, "Himeji", location2.LocCity)
assert.Equal(t, "Kinki Region", location2.LocState)
assert.Equal(t, "Himeji", location2.City())
assert.Equal(t, "Kinki Region", location2.State())
})
t.Run("cat_brown.jpg", func(t *testing.T) {
conf := config.TestConfig()

33
internal/repo/category.go Normal file
View file

@ -0,0 +1,33 @@
package repo
import (
"strings"
)
type CategoryLabel struct {
Name string
Title string
}
func (s *Repo) CategoryLabels(limit, offset int) (results []CategoryLabel) {
q := s.db.NewScope(nil).DB()
// q.LogMode(true)
q = q.Table("categories").
Select("label_name AS name").
Joins("JOIN labels l ON categories.category_id = l.id").
Group("label_name").
Limit(limit).Offset(offset)
if err := q.Scan(&results).Error; err != nil {
log.Errorf("categories: %s", err.Error())
return results
}
for i, l := range results {
results[i].Title = strings.Title(l.Name)
}
return results
}

View file

@ -0,0 +1,17 @@
package repo
import (
"testing"
"github.com/photoprism/photoprism/internal/config"
)
func TestRepo_CategoryLabels(t *testing.T) {
conf := config.TestConfig()
search := New(conf.OriginalsPath(), conf.Db())
categories := search.CategoryLabels(1000, 0)
t.Logf("categories: %+v", categories)
}

View file

@ -26,6 +26,9 @@ type PhotoResult struct {
PhotoName string
PhotoTitle string
PhotoDescription string
PhotoYear int
PhotoMonth int
PhotoCountry string
PhotoArtist string
PhotoKeywords string
PhotoColors string
@ -52,18 +55,13 @@ type PhotoResult struct {
LensModel string
LensMake string
// Country
CountryID string
// Location
LocationID uint64
LocName string
LocPlace string
PlaceID uint64
LocLabel string
LocCity string
LocSuburb string
LocState string
LocCategory string
LocSource string
LocCountry string
LocationChanged bool
LocationEstimated bool
@ -118,12 +116,12 @@ func (s *Repo) Photos(f form.PhotoSearch) (results []PhotoResult, err error) {
files.file_orientation, files.file_main_color, files.file_colors, files.file_luminance, files.file_chroma,
cameras.camera_make, cameras.camera_model,
lenses.lens_make, lenses.lens_model,
locations.loc_name, locations.loc_place, locations.loc_city, locations.loc_suburb, locations.loc_state,
locations.loc_category, locations.loc_source`).
places.loc_label, places.loc_city, places.loc_state, places.loc_country
`).
Joins("JOIN files ON files.photo_id = photos.id AND files.file_primary AND files.deleted_at IS NULL").
Joins("JOIN cameras ON cameras.id = photos.camera_id").
Joins("JOIN lenses ON lenses.id = photos.lens_id").
Joins("LEFT JOIN locations ON locations.id = photos.location_id").
Joins("JOIN places ON photos.place_id = places.id").
Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id").
Where("photos.deleted_at IS NULL AND files.file_missing = 0").
Group("photos.id, files.id")
@ -220,7 +218,7 @@ func (s *Repo) Photos(f form.PhotoSearch) (results []PhotoResult, err error) {
}
if f.Country != "" {
q = q.Where("photos.country_id = ?", f.Country)
q = q.Where("photos.photo_country = ?", f.Country)
}
if f.Title != "" {

17
internal/util/date.go Normal file
View file

@ -0,0 +1,17 @@
package util
var Months = [...]string{
"Unknown",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
}

View file

@ -0,0 +1,13 @@
package util
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMonths(t *testing.T) {
assert.Equal(t, "Unknown", Months[0])
assert.Equal(t, "January", Months[1])
assert.Equal(t, "December", Months[12])
}