Albums: Regenerate share preview after one hour and after changes #3658
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
6463fe03be
commit
a30cbb19b7
|
@ -160,6 +160,10 @@ func UpdateAlbum(router *gin.RouterGroup) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush album cover cache.
|
||||||
|
RemoveFromAlbumCoverCache(uid)
|
||||||
|
|
||||||
|
// Update client.
|
||||||
UpdateClientConfig()
|
UpdateClientConfig()
|
||||||
|
|
||||||
// Update album YAML backup.
|
// Update album YAML backup.
|
||||||
|
|
|
@ -2,6 +2,8 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
@ -9,6 +11,8 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/internal/thumb"
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
"github.com/photoprism/photoprism/internal/ttl"
|
"github.com/photoprism/photoprism/internal/ttl"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
"github.com/photoprism/photoprism/pkg/rnd"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ThumbCache struct {
|
type ThumbCache struct {
|
||||||
|
@ -42,8 +46,13 @@ func RemoveFromFolderCache(rootName string) {
|
||||||
|
|
||||||
// RemoveFromAlbumCoverCache removes covers by album UID e.g. after adding or removing photos.
|
// RemoveFromAlbumCoverCache removes covers by album UID e.g. after adding or removing photos.
|
||||||
func RemoveFromAlbumCoverCache(uid string) {
|
func RemoveFromAlbumCoverCache(uid string) {
|
||||||
|
if !rnd.IsAlnum(uid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cache := get.CoverCache()
|
cache := get.CoverCache()
|
||||||
|
|
||||||
|
// Flush album cover cache.
|
||||||
for thumbName := range thumb.Sizes {
|
for thumbName := range thumb.Sizes {
|
||||||
cacheKey := CacheKey(albumCover, uid, string(thumbName))
|
cacheKey := CacheKey(albumCover, uid, string(thumbName))
|
||||||
|
|
||||||
|
@ -52,6 +61,12 @@ func RemoveFromAlbumCoverCache(uid string) {
|
||||||
log.Debugf("removed %s from cache", cacheKey)
|
log.Debugf("removed %s from cache", cacheKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete share preview, if exists.
|
||||||
|
if sharePreview := path.Join(get.Config().ThumbCachePath(), "share", uid+fs.ExtJPEG); fs.FileExists(sharePreview) {
|
||||||
|
_ = os.Remove(sharePreview)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update album cover images.
|
||||||
if err := query.UpdateAlbumCovers(); err != nil {
|
if err := query.UpdateAlbumCovers(); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
|
@ -14,24 +13,26 @@ import (
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
|
"github.com/photoprism/photoprism/internal/frame"
|
||||||
"github.com/photoprism/photoprism/internal/get"
|
"github.com/photoprism/photoprism/internal/get"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/internal/search"
|
"github.com/photoprism/photoprism/internal/search"
|
||||||
"github.com/photoprism/photoprism/internal/thumb"
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SharePreview returns a link share preview image.
|
// SharePreview returns a preview image for the given share uid if the token is valid.
|
||||||
//
|
//
|
||||||
// GET /s/:token/:uid/preview
|
// GET /s/:token/:shared/preview
|
||||||
// TODO: Proof of concept, needs refactoring.
|
// TODO: Proof of concept, needs refactoring.
|
||||||
func SharePreview(router *gin.RouterGroup) {
|
func SharePreview(router *gin.RouterGroup) {
|
||||||
router.GET("/:token/:shared/preview", func(c *gin.Context) {
|
router.GET("/:token/:shared/preview", func(c *gin.Context) {
|
||||||
conf := get.Config()
|
conf := get.Config()
|
||||||
|
|
||||||
token := clean.Token(c.Param("token"))
|
token := clean.Token(c.Param("token"))
|
||||||
shared := clean.Token(c.Param("shared"))
|
shared := clean.UID(c.Param("shared"))
|
||||||
links := entity.FindLinks(token, shared)
|
links := entity.FindLinks(token, shared)
|
||||||
|
|
||||||
if len(links) != 1 {
|
if len(links) != 1 {
|
||||||
|
@ -48,12 +49,13 @@ func SharePreview(router *gin.RouterGroup) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
previewFilename := fmt.Sprintf("%s/%s.jpg", thumbPath, shared)
|
previewFilename := filepath.Join(thumbPath, shared+fs.ExtJPEG)
|
||||||
yesterday := time.Now().Add(-24 * time.Hour)
|
|
||||||
|
expires := entity.TimeStamp().Add(-1 * time.Hour)
|
||||||
|
|
||||||
if info, err := os.Stat(previewFilename); err != nil {
|
if info, err := os.Stat(previewFilename); err != nil {
|
||||||
log.Debugf("share: creating new preview for %s", clean.Log(shared))
|
log.Debugf("share: creating new preview for %s", clean.Log(shared))
|
||||||
} else if info.ModTime().After(yesterday) {
|
} else if info.ModTime().After(expires) {
|
||||||
log.Debugf("share: using cached preview for %s", clean.Log(shared))
|
log.Debugf("share: using cached preview for %s", clean.Log(shared))
|
||||||
c.File(previewFilename)
|
c.File(previewFilename)
|
||||||
return
|
return
|
||||||
|
@ -63,6 +65,14 @@ func SharePreview(router *gin.RouterGroup) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a, err := query.AlbumByUID(shared)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var f form.SearchPhotos
|
var f form.SearchPhotos
|
||||||
|
|
||||||
// Covers may only contain public content in shared albums.
|
// Covers may only contain public content in shared albums.
|
||||||
|
@ -75,11 +85,11 @@ func SharePreview(router *gin.RouterGroup) {
|
||||||
f.Primary = true
|
f.Primary = true
|
||||||
|
|
||||||
// Get first 12 album entries.
|
// Get first 12 album entries.
|
||||||
f.Count = 12
|
f.Count = 6
|
||||||
f.Order = "relevance"
|
f.Order = a.AlbumOrder
|
||||||
|
|
||||||
if err := f.ParseQueryString(); err != nil {
|
if parseErr := f.ParseQueryString(); parseErr != nil {
|
||||||
log.Errorf("preview: %s", err)
|
log.Errorf("preview: %s", parseErr)
|
||||||
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -95,75 +105,43 @@ func SharePreview(router *gin.RouterGroup) {
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
||||||
return
|
return
|
||||||
} else if count < 12 {
|
}
|
||||||
f := p[0]
|
|
||||||
size, _ := thumb.Sizes[thumb.Fit720]
|
|
||||||
|
|
||||||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
size, _ := thumb.Sizes[thumb.Tile500]
|
||||||
|
|
||||||
|
images := make([]image.Image, 0, len(p))
|
||||||
|
|
||||||
|
// Get thumbnail images to create album preview.
|
||||||
|
for _, file := range p {
|
||||||
|
fileName := photoprism.FileName(file.FileRoot, file.FileName)
|
||||||
|
|
||||||
if !fs.FileExists(fileName) {
|
if !fs.FileExists(fileName) {
|
||||||
log.Errorf("share: file %s is missing (preview)", clean.Log(f.FileName))
|
log.Errorf("share: file %s is missing (preview)", clean.Log(file.FileName))
|
||||||
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, f.FileOrientation, size.Options...)
|
thumbnail, imgErr := thumb.FromFile(fileName, file.FileHash, conf.ThumbCachePath(), size.Width, size.Height, file.FileOrientation, size.Options...)
|
||||||
|
|
||||||
if err != nil {
|
if imgErr != nil {
|
||||||
log.Error(err)
|
log.Warn(imgErr)
|
||||||
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
continue
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.File(thumbnail)
|
img, imgErr := imaging.Open(thumbnail)
|
||||||
|
|
||||||
return
|
if imgErr != nil {
|
||||||
|
log.Warn(imgErr)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
width := 908
|
images = append(images, img)
|
||||||
height := 680
|
|
||||||
x := 0
|
|
||||||
y := 0
|
|
||||||
|
|
||||||
preview := imaging.New(width, height, color.NRGBA{255, 255, 255, 255})
|
|
||||||
size, _ := thumb.Sizes[thumb.Tile224]
|
|
||||||
|
|
||||||
for _, f := range p {
|
|
||||||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
|
||||||
|
|
||||||
if !fs.FileExists(fileName) {
|
|
||||||
log.Errorf("share: file %s is missing (preview)", clean.Log(f.FileName))
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbCachePath(), size.Width, size.Height, f.FileOrientation, size.Options...)
|
// Create album preview from thumbnail images.
|
||||||
|
preview, err := frame.Collage(frame.Polaroid, images)
|
||||||
|
|
||||||
if err != nil {
|
// Save the resulting album preview as JPEG.
|
||||||
log.Error(err)
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
src, err := imaging.Open(thumbnail)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
preview = imaging.Paste(preview, src, image.Pt(x, y))
|
|
||||||
|
|
||||||
x += 228
|
|
||||||
|
|
||||||
if x > width {
|
|
||||||
x = 0
|
|
||||||
y += 228
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the resulting image as JPEG.
|
|
||||||
err = imaging.Save(preview, previewFilename)
|
err = imaging.Save(preview, previewFilename)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
24
internal/frame/angle.go
Normal file
24
internal/frame/angle.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package frame
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomAngle returns a random angle between -max and max.
|
||||||
|
func RandomAngle(max float64) float64 {
|
||||||
|
if max == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if max < 0 {
|
||||||
|
max = -1 * max
|
||||||
|
}
|
||||||
|
|
||||||
|
if max > 180 {
|
||||||
|
max = 180
|
||||||
|
}
|
||||||
|
|
||||||
|
r := 2 * max
|
||||||
|
|
||||||
|
return (rand.Float64() - 0.5) * r
|
||||||
|
}
|
19
internal/frame/angle_test.go
Normal file
19
internal/frame/angle_test.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package frame
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRandomAngle(t *testing.T) {
|
||||||
|
t.Run("Valid", func(t *testing.T) {
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
e := float64(i)
|
||||||
|
a := RandomAngle(e)
|
||||||
|
t.Logf("%f => %f", e, a)
|
||||||
|
assert.LessOrEqual(t, a, e)
|
||||||
|
assert.GreaterOrEqual(t, a, -1*e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
77
internal/frame/collage.go
Normal file
77
internal/frame/collage.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package frame
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CollageBackground is the default background for image collages.
|
||||||
|
var CollageBackground = color.NRGBA{32, 33, 36, 255}
|
||||||
|
|
||||||
|
// Collage embeds images into a collage and returns the resulting image.
|
||||||
|
func Collage(t Type, images []image.Image) (collage image.Image, err error) {
|
||||||
|
width := 1600
|
||||||
|
height := 900
|
||||||
|
|
||||||
|
collage = imaging.New(width, height, CollageBackground)
|
||||||
|
|
||||||
|
if len(images) == 0 {
|
||||||
|
return collage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case Polaroid:
|
||||||
|
collage, err = polaroidCollage(collage, images)
|
||||||
|
default:
|
||||||
|
return collage, fmt.Errorf("unknown collage type %s", clean.Log(string(t)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return collage, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// polaroidCollage embeds images into a Polaroid collage and returns the resulting image.
|
||||||
|
func polaroidCollage(collage image.Image, images []image.Image) (image.Image, error) {
|
||||||
|
n := len(images) - 1
|
||||||
|
|
||||||
|
if n == 1 {
|
||||||
|
if framed, err := polaroid(images[0], RandomAngle(20)); err != nil {
|
||||||
|
return collage, err
|
||||||
|
} else {
|
||||||
|
collage = imaging.Overlay(collage, framed, image.Pt(50, -80), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if framed, err := polaroid(images[1], RandomAngle(20)); err != nil {
|
||||||
|
return collage, err
|
||||||
|
} else {
|
||||||
|
collage = imaging.Overlay(collage, framed, image.Pt(500, -30), 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dl := 1500 / n
|
||||||
|
dr := 1350 / n
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
img := images[i+1]
|
||||||
|
|
||||||
|
framed, err := polaroid(img, RandomAngle(30))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return collage, err
|
||||||
|
}
|
||||||
|
|
||||||
|
collage = imaging.Overlay(collage, framed, RandomPoint(850-i*dl, -150-((i%2)*50), 950-i*dr, 125-((i%2)*125)), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if framed, err := polaroid(images[0], RandomAngle(20)); err != nil {
|
||||||
|
return collage, err
|
||||||
|
} else {
|
||||||
|
collage = imaging.Overlay(collage, framed, image.Pt(275, -50), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collage, nil
|
||||||
|
}
|
62
internal/frame/collage_test.go
Normal file
62
internal/frame/collage_test.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package frame
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCollage(t *testing.T) {
|
||||||
|
t.Run("Polaroid", func(t *testing.T) {
|
||||||
|
var images []image.Image
|
||||||
|
|
||||||
|
img, err := imaging.Open("testdata/500x500.jpg")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for i := 0; i <= 5; i++ {
|
||||||
|
images = append(images, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveName := "testdata/test-polaroid-collage.jpg"
|
||||||
|
preview, err := Collage(Polaroid, images)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = imaging.Save(preview, saveName)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
mimeType := fs.MimeType(saveName)
|
||||||
|
assert.Equal(t, fs.MimeTypeJPEG, mimeType)
|
||||||
|
|
||||||
|
_ = os.Remove(saveName)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Two", func(t *testing.T) {
|
||||||
|
var images []image.Image
|
||||||
|
|
||||||
|
img, err := imaging.Open("testdata/500x500.jpg")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for i := 0; i <= 1; i++ {
|
||||||
|
images = append(images, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveName := "testdata/test-polaroid-collage-two.jpg"
|
||||||
|
preview, err := Collage(Polaroid, images)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = imaging.Save(preview, saveName)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
mimeType := fs.MimeType(saveName)
|
||||||
|
assert.Equal(t, fs.MimeTypeJPEG, mimeType)
|
||||||
|
|
||||||
|
_ = os.Remove(saveName)
|
||||||
|
})
|
||||||
|
}
|
29
internal/frame/frame.go
Normal file
29
internal/frame/frame.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
Package frame provides helper functions to embed images into frames.
|
||||||
|
|
||||||
|
Copyright (c) 2018 - 2023 PhotoPrism UG. All rights reserved.
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||||
|
<https://docs.photoprism.app/license/agpl>
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||||
|
which describe how our Brand Assets may be used:
|
||||||
|
<https://www.photoprism.app/trademark>
|
||||||
|
|
||||||
|
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||||
|
want to support our work, or just want to say hello.
|
||||||
|
|
||||||
|
Additional information can be found in our Developer Guide:
|
||||||
|
<https://docs.photoprism.app/developer-guide/>
|
||||||
|
*/
|
||||||
|
package frame
|
||||||
|
|
||||||
|
type Type string
|
||||||
|
|
||||||
|
const Polaroid = "polaroid"
|
18
internal/frame/image.go
Normal file
18
internal/frame/image.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package frame
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Image embeds the specified image file into a frame and returns the resulting image.
|
||||||
|
func Image(t Type, img image.Image, rotate float64) (image.Image, error) {
|
||||||
|
switch t {
|
||||||
|
case Polaroid:
|
||||||
|
return polaroid(img, rotate)
|
||||||
|
default:
|
||||||
|
return img, fmt.Errorf("unknown collage type %s", clean.Log(string(t)))
|
||||||
|
}
|
||||||
|
}
|
32
internal/frame/image_test.go
Normal file
32
internal/frame/image_test.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package frame
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestImage(t *testing.T) {
|
||||||
|
t.Run("Polaroid", func(t *testing.T) {
|
||||||
|
img, err := imaging.Open("testdata/500x500.jpg")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
saveName := "testdata/test-image.png"
|
||||||
|
|
||||||
|
out, err := Image(Polaroid, img, RandomAngle(30))
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = imaging.Save(out, saveName)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
mimeType := fs.MimeType(saveName)
|
||||||
|
assert.Equal(t, fs.MimeTypePNG, mimeType)
|
||||||
|
|
||||||
|
_ = os.Remove(saveName)
|
||||||
|
})
|
||||||
|
}
|
29
internal/frame/point.go
Normal file
29
internal/frame/point.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package frame
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomPoint returns a random image position within the specified range.
|
||||||
|
func RandomPoint(xMin, yMin, xMax, yMax int) image.Point {
|
||||||
|
if xMin == 0 && yMin == 0 && xMax == 0 && yMax == 0 {
|
||||||
|
return image.Pt(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if xMin > xMax {
|
||||||
|
xMin = xMax
|
||||||
|
}
|
||||||
|
|
||||||
|
xDiff := float64(xMax - xMin)
|
||||||
|
x := xMin + int(rand.Float64()*xDiff)
|
||||||
|
|
||||||
|
if yMin > yMax {
|
||||||
|
yMin = yMax
|
||||||
|
}
|
||||||
|
|
||||||
|
yDiff := float64(yMax - yMin)
|
||||||
|
y := yMin + int(rand.Float64()*yDiff)
|
||||||
|
|
||||||
|
return image.Pt(x, y)
|
||||||
|
}
|
33
internal/frame/polaroid.go
Normal file
33
internal/frame/polaroid.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package frame
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed polaroid.png
|
||||||
|
var polaroidPng []byte
|
||||||
|
|
||||||
|
// Polaroid embeds the specified image file into a Polaroid frame and returns the resulting image.
|
||||||
|
func polaroid(img image.Image, rotate float64) (image.Image, error) {
|
||||||
|
// Create image frame.
|
||||||
|
frm, err := imaging.Decode(bytes.NewReader(polaroidPng))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paste image into frame.
|
||||||
|
out := imaging.Paste(frm, img, image.Pt(200, 152))
|
||||||
|
|
||||||
|
// Rotate image before returning it?
|
||||||
|
if rotate != 0.0 {
|
||||||
|
out = imaging.Rotate(out, rotate, color.NRGBA{255, 255, 255, 0})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
BIN
internal/frame/polaroid.png
Normal file
BIN
internal/frame/polaroid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 134 KiB |
32
internal/frame/polaroid_test.go
Normal file
32
internal/frame/polaroid_test.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package frame
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPolaroid(t *testing.T) {
|
||||||
|
t.Run("RandomAngle", func(t *testing.T) {
|
||||||
|
img, err := imaging.Open("testdata/500x500.jpg")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
saveName := "testdata/test-polaroid.png"
|
||||||
|
|
||||||
|
out, err := polaroid(img, RandomAngle(30))
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = imaging.Save(out, saveName)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
mimeType := fs.MimeType(saveName)
|
||||||
|
assert.Equal(t, fs.MimeTypePNG, mimeType)
|
||||||
|
|
||||||
|
_ = os.Remove(saveName)
|
||||||
|
})
|
||||||
|
}
|
BIN
internal/frame/testdata/500x500.jpg
vendored
Normal file
BIN
internal/frame/testdata/500x500.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
Loading…
Reference in a new issue