192 lines
4.3 KiB
Go
192 lines
4.3 KiB
Go
package crop
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/photoprism/photoprism/pkg/rnd"
|
|
)
|
|
|
|
// Areas represents a list of relative crop areas.
|
|
type Areas []Area
|
|
|
|
// Area represents a relative crop area.
|
|
type Area struct {
|
|
Name string `json:"name,omitempty"`
|
|
X float32 `json:"x,omitempty"`
|
|
Y float32 `json:"y,omitempty"`
|
|
W float32 `json:"w,omitempty"`
|
|
H float32 `json:"h,omitempty"`
|
|
}
|
|
|
|
// Empty tests if the area is empty.
|
|
func (a Area) Empty() bool {
|
|
return a.X == 0 && a.Y == 0 && a.W == 0 && a.H == 0
|
|
}
|
|
|
|
// String returns a string identifying the crop area.
|
|
func (a Area) String() string {
|
|
if a.Empty() {
|
|
return ""
|
|
}
|
|
|
|
return fmt.Sprintf("%03x%03x%03x%03x", int(a.X*1000), int(a.Y*1000), int(a.W*1000), int(a.H*1000))
|
|
}
|
|
|
|
// Thumb returns a string identifying the file and crop area to create a thumb.
|
|
func (a Area) Thumb(fileHash string) string {
|
|
if len(fileHash) < 40 {
|
|
return a.String()
|
|
}
|
|
|
|
return fmt.Sprintf("%040s-%012s", fileHash, a.String())
|
|
}
|
|
|
|
// Bounds returns absolute coordinates and dimension.
|
|
func (a Area) Bounds(img image.Image) (min, max image.Point, dim int) {
|
|
size := img.Bounds().Max
|
|
|
|
min = image.Point{X: int(float32(size.X) * a.X), Y: int(float32(size.Y) * a.Y)}
|
|
max = image.Point{X: int(float32(size.X) * (a.X + a.W)), Y: int(float32(size.Y) * (a.Y + a.H))}
|
|
dim = int(float32(size.X) * a.W)
|
|
|
|
return min, max, dim
|
|
}
|
|
|
|
// FileWidth returns the ideal file width based on the crop size.
|
|
func (a Area) FileWidth(size Size) int {
|
|
return int(float32(size.Width) / a.W)
|
|
}
|
|
|
|
// Top returns the top Y coordinate as float64.
|
|
func (a Area) Top() float64 {
|
|
return float64(a.Y)
|
|
}
|
|
|
|
// Left returns the left X coordinate as float64.
|
|
func (a Area) Left() float64 {
|
|
return float64(a.X)
|
|
}
|
|
|
|
// Right returns the right X coordinate as float64.
|
|
func (a Area) Right() float64 {
|
|
return float64(a.X + a.W)
|
|
}
|
|
|
|
// Bottom returns the bottom Y coordinate as float64.
|
|
func (a Area) Bottom() float64 {
|
|
return float64(a.Y + a.H)
|
|
}
|
|
|
|
// Surface returns the surface area.
|
|
func (a Area) Surface() float64 {
|
|
return float64(a.W * a.H)
|
|
}
|
|
|
|
// SurfaceRatio returns the surface ratio.
|
|
func (a Area) SurfaceRatio(area float64) float64 {
|
|
if area <= 0 {
|
|
return 0
|
|
}
|
|
|
|
if s := a.Surface(); s <= 0 {
|
|
return 0
|
|
} else if area > s {
|
|
return s / area
|
|
} else {
|
|
return area / s
|
|
}
|
|
}
|
|
|
|
// Overlap calculates the overlap of two areas.
|
|
func (a Area) Overlap(other Area) (x, y float64) {
|
|
x = math.Max(0, math.Min(a.Right(), other.Right())-math.Max(a.Left(), other.Left()))
|
|
y = math.Max(0, math.Min(a.Bottom(), other.Bottom())-math.Max(a.Top(), other.Top()))
|
|
|
|
return x, y
|
|
}
|
|
|
|
// OverlapArea calculates the overlap area of two areas.
|
|
func (a Area) OverlapArea(other Area) (area float64) {
|
|
x, y := a.Overlap(other)
|
|
|
|
return x * y
|
|
}
|
|
|
|
// OverlapPercent calculates the overlap ratio of two areas in percent.
|
|
func (a Area) OverlapPercent(other Area) int {
|
|
return int(math.Round(other.SurfaceRatio(a.OverlapArea(other)) * 100))
|
|
}
|
|
|
|
// clipVal ensures the relative size is within a valid range.
|
|
func clipVal(f float32) float32 {
|
|
if f > 1 {
|
|
f = 1
|
|
} else if f < 0 {
|
|
f = 0
|
|
}
|
|
|
|
return f
|
|
}
|
|
|
|
// NewArea returns new relative image area.
|
|
func NewArea(name string, x, y, w, h float32) Area {
|
|
return Area{
|
|
Name: name,
|
|
X: clipVal(x),
|
|
Y: clipVal(y),
|
|
W: clipVal(w),
|
|
H: clipVal(h),
|
|
}
|
|
}
|
|
|
|
// AreaFromString returns an image area.
|
|
func AreaFromString(s string) Area {
|
|
if len(s) != 12 || !rnd.IsHex(s) {
|
|
return Area{}
|
|
}
|
|
|
|
x, _ := strconv.ParseInt(s[0:3], 16, 32)
|
|
y, _ := strconv.ParseInt(s[3:6], 16, 32)
|
|
w, _ := strconv.ParseInt(s[6:9], 16, 32)
|
|
h, _ := strconv.ParseInt(s[9:12], 16, 32)
|
|
|
|
return NewArea("crop", float32(x)/1000, float32(y)/1000, float32(w)/1000, float32(h)/1000)
|
|
}
|
|
|
|
// IsCroppedThumb tests if the string represents a cropped thumbnail and returns the split position if true.
|
|
func IsCroppedThumb(thumb string) int {
|
|
if thumb == "" || len(thumb) < 41 {
|
|
return -1
|
|
}
|
|
|
|
if i := strings.IndexRune(thumb, '-'); i >= 40 && i < len(thumb)-1 {
|
|
return i
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
// ParseThumb splits a thumbnail string into the crop area and file hash.
|
|
func ParseThumb(thumb string) (fileHash, area string) {
|
|
if len(thumb) == 12 {
|
|
return "", thumb
|
|
} else if len(thumb) < 41 {
|
|
return thumb, ""
|
|
}
|
|
|
|
s := strings.SplitN(strings.Trim(thumb, "/ -"), "-", 2)
|
|
|
|
fileHash = s[0]
|
|
|
|
if len(s) < 2 {
|
|
// Do nothing.
|
|
} else if len(s[1]) >= 12 {
|
|
area = s[1]
|
|
}
|
|
|
|
return fileHash, area
|
|
}
|