2020-05-14 14:35:08 +02:00
|
|
|
package scanner
|
|
|
|
|
|
|
|
import (
|
|
|
|
"image"
|
|
|
|
"image/jpeg"
|
|
|
|
"os"
|
|
|
|
|
|
|
|
"github.com/disintegration/imaging"
|
2020-05-14 15:17:23 +02:00
|
|
|
"github.com/pkg/errors"
|
2020-05-14 14:35:08 +02:00
|
|
|
"github.com/viktorstrate/photoview/api/graphql/models"
|
|
|
|
"github.com/viktorstrate/photoview/api/utils"
|
2020-07-11 13:13:31 +02:00
|
|
|
"gopkg.in/vansante/go-ffprobe.v2"
|
2020-11-24 11:46:49 +01:00
|
|
|
"gorm.io/gorm"
|
2020-05-14 14:35:08 +02:00
|
|
|
)
|
|
|
|
|
2020-05-15 16:36:02 +02:00
|
|
|
type PhotoDimensions struct {
|
|
|
|
Width int
|
|
|
|
Height int
|
|
|
|
}
|
|
|
|
|
2020-08-15 12:24:54 +02:00
|
|
|
func DecodeImage(imagePath string) (image.Image, error) {
|
|
|
|
file, err := os.Open(imagePath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "failed to open file to decode image (%s)", imagePath)
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
image, err := imaging.Decode(file, imaging.AutoOrientation(true))
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "failed to decode image (%s)", imagePath)
|
|
|
|
}
|
|
|
|
|
|
|
|
return image, nil
|
|
|
|
}
|
|
|
|
|
2020-05-15 16:36:02 +02:00
|
|
|
func PhotoDimensionsFromRect(rect image.Rectangle) PhotoDimensions {
|
|
|
|
return PhotoDimensions{
|
|
|
|
Width: rect.Bounds().Max.X,
|
|
|
|
Height: rect.Bounds().Max.Y,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dimensions *PhotoDimensions) ThumbnailScale() PhotoDimensions {
|
|
|
|
aspect := float64(dimensions.Width) / float64(dimensions.Height)
|
|
|
|
|
|
|
|
var width, height int
|
|
|
|
|
|
|
|
if aspect > 1 {
|
|
|
|
width = 1024
|
|
|
|
height = int(1024 / aspect)
|
|
|
|
} else {
|
|
|
|
width = int(1024 * aspect)
|
|
|
|
height = 1024
|
|
|
|
}
|
|
|
|
|
|
|
|
return PhotoDimensions{
|
|
|
|
Width: width,
|
|
|
|
Height: height,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-11 13:13:31 +02:00
|
|
|
// EncodeMediaData is used to easily decode media data, with a cache so expensive operations are not repeated
|
|
|
|
type EncodeMediaData struct {
|
2020-07-10 14:26:19 +02:00
|
|
|
media *models.Media
|
2020-05-14 15:17:23 +02:00
|
|
|
_photoImage image.Image
|
|
|
|
_thumbnailImage image.Image
|
2020-07-09 15:07:39 +02:00
|
|
|
_contentType *MediaType
|
2020-07-11 13:39:11 +02:00
|
|
|
_videoMetadata *ffprobe.ProbeData
|
2020-05-14 15:17:23 +02:00
|
|
|
}
|
|
|
|
|
2020-05-15 15:23:21 +02:00
|
|
|
func EncodeImageJPEG(image image.Image, outputPath string, jpegQuality int) error {
|
|
|
|
photo_file, err := os.Create(outputPath)
|
2020-05-14 14:35:08 +02:00
|
|
|
if err != nil {
|
2020-05-15 15:23:21 +02:00
|
|
|
return errors.Wrapf(err, "could not create file: %s", outputPath)
|
2020-05-14 14:35:08 +02:00
|
|
|
}
|
|
|
|
defer photo_file.Close()
|
|
|
|
|
2020-05-14 15:17:23 +02:00
|
|
|
err = jpeg.Encode(photo_file, image, &jpeg.Options{Quality: jpegQuality})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-05-14 14:35:08 +02:00
|
|
|
|
2020-05-14 15:17:23 +02:00
|
|
|
return nil
|
2020-05-14 14:35:08 +02:00
|
|
|
}
|
|
|
|
|
2020-05-15 16:36:02 +02:00
|
|
|
func GetPhotoDimensions(imagePath string) (*PhotoDimensions, error) {
|
|
|
|
photoFile, err := os.Open(imagePath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer photoFile.Close()
|
|
|
|
|
|
|
|
config, _, err := image.DecodeConfig(photoFile)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &PhotoDimensions{
|
|
|
|
Width: config.Width,
|
|
|
|
Height: config.Height,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2020-05-14 14:35:08 +02:00
|
|
|
// ContentType reads the image to determine its content type
|
2020-07-11 13:13:31 +02:00
|
|
|
func (img *EncodeMediaData) ContentType() (*MediaType, error) {
|
2020-05-14 14:35:08 +02:00
|
|
|
if img._contentType != nil {
|
|
|
|
return img._contentType, nil
|
|
|
|
}
|
|
|
|
|
2020-07-11 14:05:06 +02:00
|
|
|
imgType, err := getMediaType(img.media.Path)
|
2020-05-14 14:35:08 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-05-14 15:17:23 +02:00
|
|
|
img._contentType = imgType
|
|
|
|
return imgType, nil
|
2020-05-14 14:35:08 +02:00
|
|
|
}
|
|
|
|
|
2020-11-24 11:46:49 +01:00
|
|
|
func (img *EncodeMediaData) EncodeHighRes(tx *gorm.DB, outputPath string) error {
|
2020-05-15 16:36:02 +02:00
|
|
|
contentType, err := img.ContentType()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !contentType.isSupported() {
|
|
|
|
return errors.New("could not convert photo as file format is not supported")
|
|
|
|
}
|
|
|
|
|
|
|
|
if contentType.isRaw() {
|
2020-05-17 21:49:29 +02:00
|
|
|
if DarktableCli.IsInstalled() {
|
2020-07-10 14:26:19 +02:00
|
|
|
err := DarktableCli.EncodeJpeg(img.media.Path, outputPath, 70)
|
2020-05-15 16:36:02 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return errors.New("could not convert photo as no RAW converter was found")
|
|
|
|
}
|
2020-05-17 16:08:58 +02:00
|
|
|
} else {
|
|
|
|
image, err := img.photoImage(tx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-05-15 16:36:02 +02:00
|
|
|
|
2020-05-17 16:08:58 +02:00
|
|
|
EncodeImageJPEG(image, outputPath, 70)
|
2020-05-15 16:36:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func EncodeThumbnail(inputPath string, outputPath string) (*PhotoDimensions, error) {
|
2020-08-15 12:24:54 +02:00
|
|
|
inputImage, err := DecodeImage(inputPath)
|
2020-05-15 16:36:02 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
dimensions := PhotoDimensionsFromRect(inputImage.Bounds())
|
|
|
|
dimensions = dimensions.ThumbnailScale()
|
|
|
|
|
|
|
|
thumbImage := imaging.Resize(inputImage, dimensions.Width, dimensions.Height, imaging.NearestNeighbor)
|
|
|
|
if err = EncodeImageJPEG(thumbImage, outputPath, 60); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &dimensions, nil
|
|
|
|
}
|
|
|
|
|
2020-05-14 14:35:08 +02:00
|
|
|
// PhotoImage reads and decodes the image file and saves it in a cache so the photo in only decoded once
|
2020-11-24 11:46:49 +01:00
|
|
|
func (img *EncodeMediaData) photoImage(tx *gorm.DB) (image.Image, error) {
|
2020-05-14 14:35:08 +02:00
|
|
|
if img._photoImage != nil {
|
|
|
|
return img._photoImage, nil
|
|
|
|
}
|
|
|
|
|
2020-08-15 12:24:54 +02:00
|
|
|
photoImg, err := DecodeImage(img.media.Path)
|
2020-05-14 14:35:08 +02:00
|
|
|
if err != nil {
|
2020-05-15 16:36:02 +02:00
|
|
|
return nil, utils.HandleError("image decoding", err)
|
2020-05-14 14:35:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
img._photoImage = photoImg
|
|
|
|
return img._photoImage, nil
|
|
|
|
}
|