Skip to content

Commit

Permalink
Merge pull request #5 from SmilyOrg/embedded
Browse files Browse the repository at this point in the history
Support embedded jpeg thumbs + fix debug modes
  • Loading branch information
SmilyOrg authored Dec 1, 2021
2 parents d7a28f3 + 9aa66f8 commit e95e1c3
Show file tree
Hide file tree
Showing 17 changed files with 482 additions and 127 deletions.
22 changes: 22 additions & 0 deletions defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,26 @@ media:
# width
# height: Predefined thumbnail dimensions used to pick the most
# appropriately-sized thumbnail for the zoom level.
#
# extra_cost: Additional weight to add when picking the closest thumbnail.
# Higher number means that other thumbnails will be preferred
# if they exist.

#
# Embedded JPEG thumbnail
#
- name: exif-thumb
exif: ThumbnailImage
fit: INSIDE
width: 120
height: 120
# It's expensive to extract, so this makes it more of a last resort, but
# still a lot better than loading the original photo.
extra_cost: 10

#
# Synology Moments / Photo Station thumbnails
#
- name: S
path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_THUMB_S.jpg"
fit: INSIDE
Expand Down Expand Up @@ -122,6 +141,9 @@ media:
videos:
extensions: [".mp4"]
thumbnails:
#
# Synology Moments / Photo Station video variants
#
- name: M
path: "{{.Dir}}@eaDir/{{.Filename}}/SYNOPHOTO_FILM_M.mp4"
fit: OUTSIDE
Expand Down
48 changes: 45 additions & 3 deletions internal/image/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"image"
"photofield/internal/metrics"
"reflect"
"time"
"unsafe"

"github.com/dgraph-io/ristretto"
Expand Down Expand Up @@ -47,7 +48,16 @@ func newInfoCache() InfoCache {
}
}

func newImageCache(caches Caches) *ristretto.Cache {
type ImageCache struct {
cache *ristretto.Cache
}

type ImageLoader interface {
Acquire(key string, path string, thumbnail *Thumbnail) (image.Image, Info, error)
Release(key string)
}

func newImageCache(caches Caches) ImageCache {
cache, err := ristretto.NewCache(&ristretto.Config{
NumCounters: 1e6, // number of keys to track frequency of
MaxCost: caches.Image.MaxSizeBytes(), // maximum cost of cache
Expand All @@ -59,7 +69,7 @@ func newImageCache(caches Caches) *ristretto.Cache {
if img == nil {
return 1
}
switch img := (*img).(type) {
switch img := img.(type) {

case *image.YCbCr:
return int64(unsafe.Sizeof(*img)) +
Expand Down Expand Up @@ -95,7 +105,39 @@ func newImageCache(caches Caches) *ristretto.Cache {
panic(err)
}
metrics.AddRistretto("image_cache", cache)
return cache
return ImageCache{
cache: cache,
}
}

func (c *ImageCache) GetOrLoad(path string, thumbnail *Thumbnail, loader ImageLoader) (image.Image, Info, error) {
tries := 10
key := path
if thumbnail != nil {
key += "|thumbnail|" + thumbnail.Name
}
for try := 0; try < tries; try++ {
value, found := c.cache.Get(key)
if found {
imageRef := value.(imageRef)
return imageRef.image, imageRef.info, imageRef.err
} else {
image, info, err := loader.Acquire(key, path, thumbnail)
imageRef := imageRef{
image: image,
info: info,
err: err,
}
c.cache.SetWithTTL(key, imageRef, 0, 10*time.Minute)
loader.Release(key)
return image, info, err
}
}
return nil, Info{}, fmt.Errorf("unable to get image after %v tries", tries)
}

func (c *ImageCache) Delete(path string) {
c.cache.Del(path)
}

func newFileExistsCache() *ristretto.Cache {
Expand Down
27 changes: 24 additions & 3 deletions internal/image/decoder.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
package image

import (
"bytes"
goimage "image"
"image/jpeg"
"io"
"log"
"strconv"
"time"
)

type Decoder struct {
loader metadataLoader
loader metadataLoader
goexifLoader *GoExifRwcarlsenLoader
}

type metadataLoader interface {
DecodeInfo(path string, info *Info) error
DecodeBytes(path string, tagName string) ([]byte, error)
Close()
}

func NewDecoder(exifToolCount int) *Decoder {
decoder := Decoder{}
decoder.goexifLoader = NewGoExifRwcarlsenLoader()
if exifToolCount > 0 {
var err error
decoder.loader, err = NewExifToolMostlyGeekLoader(exifToolCount)
if err != nil {
log.Printf("unable to use exiftool, defaulting to goexif - no video metadata support (%v)\n", err.Error())
decoder.loader = NewGoExifRwcarlsenLoader()
decoder.loader = decoder.goexifLoader
}
} else {
decoder.loader = NewGoExifRwcarlsenLoader()
decoder.loader = decoder.goexifLoader
}
return &decoder
}
Expand Down Expand Up @@ -55,6 +62,20 @@ func (decoder *Decoder) DecodeInfo(path string, info *Info) error {
return err
}

func (decoder *Decoder) DecodeImage(path string, tagName string) (goimage.Image, Info, error) {
imageBytes, err := decoder.loader.DecodeBytes(path, tagName)
if err != nil {
return nil, Info{}, err
}
info := Info{}
r := bytes.NewReader(imageBytes)
decoder.goexifLoader.DecodeInfoReader(r, &info)

r.Seek(0, io.SeekStart)
img, err := jpeg.Decode(r)
return img, info, err
}

func parseOrientation(orientation string) Orientation {
n, err := strconv.Atoi(orientation)
if err != nil || n < 1 || n > 8 {
Expand Down
45 changes: 33 additions & 12 deletions internal/image/exiftool-mostlygeek.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package image
import (
"bufio"
"errors"
"regexp"
"strconv"
"strings"

"github.com/mostlygeek/go-exiftool"
)

var previewValueMatcher = regexp.MustCompile(`Binary data (\d+) bytes`)

type ExifToolMostlyGeekLoader struct {
exifTool *exiftool.Pool
}
Expand All @@ -21,6 +24,19 @@ func NewExifToolMostlyGeekLoader(exifToolCount int) (*ExifToolMostlyGeekLoader,
decoder := &ExifToolMostlyGeekLoader{}
decoder.exifTool, err = exiftool.NewPool(
"exiftool", exifToolCount,
"-n", // Machine-readable values
"-S", // Short tag names with no padding
)
return decoder, err
}

func (decoder *ExifToolMostlyGeekLoader) DecodeInfo(path string, info *Info) error {

if decoder == nil {
return errors.New("unable to decode, exiftool missing")
}

bytes, err := decoder.exifTool.ExtractFlags(path,
"-Orientation",
"-Rotation",
"-ImageWidth",
Expand All @@ -37,19 +53,7 @@ func NewExifToolMostlyGeekLoader(exifToolCount int) (*ExifToolMostlyGeekLoader,
"-TimeStamp",
"-FileModifyDate",
"-FileCreateDate",
"-n", // Machine-readable values
"-S", // Short tag names with no padding
)
return decoder, err
}

func (decoder *ExifToolMostlyGeekLoader) DecodeInfo(path string, info *Info) error {

if decoder == nil {
return errors.New("unable to decode, exiftool missing")
}

bytes, err := decoder.exifTool.Extract(path)
if err != nil {
return err
}
Expand Down Expand Up @@ -87,6 +91,11 @@ func (decoder *ExifToolMostlyGeekLoader) DecodeInfo(path string, info *Info) err
if info.DateTime.IsZero() &&
(strings.Contains(name, "Date") || strings.Contains(name, "Time")) {
info.DateTime, _ = parseDateTime(value)
} else if strings.HasSuffix(name, "Image") {
match := previewValueMatcher.FindStringSubmatch(value)
if len(match) >= 2 {
println(name, match[1])
}
}
}
}
Expand Down Expand Up @@ -129,6 +138,18 @@ func (decoder *ExifToolMostlyGeekLoader) DecodeInfo(path string, info *Info) err
return nil
}

func (decoder *ExifToolMostlyGeekLoader) DecodeBytes(path string, tagName string) ([]byte, error) {

bytes, err := decoder.exifTool.ExtractFlags(path, "-b", "-"+tagName)

if err != nil {
println(path, tagName, err.Error())
return nil, err
}

return bytes, nil
}

func (decoder *ExifToolMostlyGeekLoader) Close() {
if decoder.exifTool != nil {
decoder.exifTool.Stop()
Expand Down
29 changes: 26 additions & 3 deletions internal/image/goexif-rwcarlsen.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@ func (decoder *GoExifRwcarlsenLoader) DecodeInfo(path string, info *Info) error
return err
}
defer file.Close()
return decoder.DecodeInfoReader(file, info)
}

x, err := exif.Decode(file)
func (decoder *GoExifRwcarlsenLoader) DecodeInfoReader(r io.ReadSeeker, info *Info) error {
x, err := exif.Decode(r)
if err == nil {
info.DateTime, _ = x.DateTime()
}

orientation := parseOrientation(getOrientationFromExif(x))

file.Seek(0, io.SeekStart)
conf, _, err := image.DecodeConfig(file)
r.Seek(0, io.SeekStart)
conf, _, err := image.DecodeConfig(r)
if err != nil {
return err
}
Expand All @@ -58,4 +61,24 @@ func (decoder *GoExifRwcarlsenLoader) DecodeInfo(path string, info *Info) error
return nil
}

func (decoder *GoExifRwcarlsenLoader) DecodeBytes(path string, tagName string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()

x, err := exif.Decode(file)
if err != nil {
return nil, err
}

tag, err := x.Get(exif.FieldName(tagName))
if err != nil {
return nil, err
}

return tag.Val, nil
}

func (decoder *GoExifRwcarlsenLoader) Close() {}
Loading

0 comments on commit e95e1c3

Please sign in to comment.