From 4344e895d18cb637eb94dcf739c7976ce96529da Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sat, 27 Mar 2021 21:14:42 +0100 Subject: [PATCH] Support .heic images + refactor EncodeMediaData --- api/go.mod | 1 + api/go.sum | 2 + api/scanner/encode_photo.go | 101 ++++++++++++++++++++--------------- api/scanner/media_type.go | 3 ++ api/scanner/process_photo.go | 4 +- 5 files changed, 66 insertions(+), 45 deletions(-) diff --git a/api/go.mod b/api/go.mod index 6aa9063b..32895c18 100644 --- a/api/go.mod +++ b/api/go.mod @@ -14,6 +14,7 @@ require ( github.com/joho/godotenv v1.3.0 github.com/pkg/errors v0.9.1 github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f + github.com/strukturag/libheif v1.11.0 // indirect github.com/vektah/gqlparser/v2 v2.1.0 github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 github.com/xor-gate/goexif2 v1.1.0 diff --git a/api/go.sum b/api/go.sum index 6bc5e982..f7bfa582 100644 --- a/api/go.sum +++ b/api/go.sum @@ -198,6 +198,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/strukturag/libheif v1.11.0 h1:HaWu5re98INSXNq7C8o5AwLcv2qD8+U7a+jVCpGWemI= +github.com/strukturag/libheif v1.11.0/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks= github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg= diff --git a/api/scanner/encode_photo.go b/api/scanner/encode_photo.go index faf9e0bc..6b1f9458 100644 --- a/api/scanner/encode_photo.go +++ b/api/scanner/encode_photo.go @@ -11,34 +11,29 @@ import ( "github.com/photoview/photoview/api/utils" "github.com/pkg/errors" "gopkg.in/vansante/go-ffprobe.v2" - "gorm.io/gorm" + + _ "github.com/strukturag/libheif/go/heif" ) -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() +func EncodeThumbnail(inputPath string, outputPath string) (*image_helpers.PhotoDimensions, error) { - image, err := imaging.Decode(file, imaging.AutoOrientation(true)) + inputImage, err := imaging.Open(inputPath, imaging.AutoOrientation(true)) if err != nil { - return nil, errors.Wrapf(err, "failed to decode image (%s)", imagePath) + return nil, err } - return image, nil + dimensions := image_helpers.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 } -// EncodeMediaData is used to easily decode media data, with a cache so expensive operations are not repeated -type EncodeMediaData struct { - media *models.Media - _photoImage image.Image - _thumbnailImage image.Image - _contentType *MediaType - _videoMetadata *ffprobe.ProbeData -} - -func EncodeImageJPEG(image image.Image, outputPath string, jpegQuality int) error { +func encodeImageJPEG(image image.Image, outputPath string, jpegQuality int) error { photo_file, err := os.Create(outputPath) if err != nil { return errors.Wrapf(err, "could not create file: %s", outputPath) @@ -53,6 +48,14 @@ func EncodeImageJPEG(image image.Image, outputPath string, jpegQuality int) erro return nil } +// EncodeMediaData is used to easily decode media data, with a cache so expensive operations are not repeated +type EncodeMediaData struct { + media *models.Media + _photoImage image.Image + _contentType *MediaType + _videoMetadata *ffprobe.ProbeData +} + // ContentType reads the image to determine its content type func (img *EncodeMediaData) ContentType() (*MediaType, error) { if img._contentType != nil { @@ -68,7 +71,7 @@ func (img *EncodeMediaData) ContentType() (*MediaType, error) { return imgType, nil } -func (img *EncodeMediaData) EncodeHighRes(tx *gorm.DB, outputPath string) error { +func (img *EncodeMediaData) EncodeHighRes(outputPath string) error { contentType, err := img.ContentType() if err != nil { return err @@ -89,36 +92,19 @@ func (img *EncodeMediaData) EncodeHighRes(tx *gorm.DB, outputPath string) error return errors.New("could not convert photo as no RAW converter was found") } } else { - image, err := img.photoImage(tx) + image, err := img.photoImage() if err != nil { return err } - EncodeImageJPEG(image, outputPath, 70) + encodeImageJPEG(image, outputPath, 70) } return nil } -func EncodeThumbnail(inputPath string, outputPath string) (*image_helpers.PhotoDimensions, error) { - inputImage, err := DecodeImage(inputPath) - if err != nil { - return nil, err - } - - dimensions := image_helpers.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 -} - -// PhotoImage reads and decodes the image file and saves it in a cache so the photo in only decoded once -func (img *EncodeMediaData) photoImage(tx *gorm.DB) (image.Image, error) { +// photoImage reads and decodes the image file and saves it in a cache so the photo in only decoded once +func (img *EncodeMediaData) photoImage() (image.Image, error) { if img._photoImage != nil { return img._photoImage, nil } @@ -130,7 +116,7 @@ func (img *EncodeMediaData) photoImage(tx *gorm.DB) (image.Image, error) { photoPath = img.media.Path } - photoImg, err := DecodeImage(photoPath) + photoImg, err := img.decodeImage(photoPath) if err != nil { return nil, utils.HandleError("image decoding", err) } @@ -138,3 +124,32 @@ func (img *EncodeMediaData) photoImage(tx *gorm.DB) (image.Image, error) { img._photoImage = photoImg return img._photoImage, nil } + +func (img *EncodeMediaData) 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() + + mediaType, err := img.ContentType() + if err != nil { + return nil, errors.Wrapf(err, "failed to get media content type needed to decode it (%s)", imagePath) + } + + var decodedImage image.Image + + if *mediaType == TypeHeic { + decodedImage, _, err = image.Decode(file) + if err != nil { + return nil, errors.Wrapf(err, "failed to decode HEIF image (%s)", imagePath) + } + } else { + decodedImage, err = imaging.Decode(file, imaging.AutoOrientation(true)) + if err != nil { + return nil, errors.Wrapf(err, "failed to decode image (%s)", imagePath) + } + } + + return decodedImage, nil +} diff --git a/api/scanner/media_type.go b/api/scanner/media_type.go index a1dfce6a..60d0e4eb 100644 --- a/api/scanner/media_type.go +++ b/api/scanner/media_type.go @@ -20,6 +20,7 @@ const ( TypeTiff MediaType = "image/tiff" TypeWebp MediaType = "image/webp" TypeBmp MediaType = "image/bmp" + TypeHeic MediaType = "image/heic" // Raw formats TypeDNG MediaType = "image/x-adobe-dng" @@ -73,6 +74,7 @@ var SupportedMimetypes = [...]MediaType{ TypeTiff, TypeWebp, TypeBmp, + TypeHeic, } var WebMimetypes = [...]MediaType{ @@ -144,6 +146,7 @@ var fileExtensions = map[string]MediaType{ ".tif": TypeTiff, ".tiff": TypeTiff, ".bmp": TypeBmp, + ".heic": TypeHeic, // RAW formats ".dng": TypeDNG, diff --git a/api/scanner/process_photo.go b/api/scanner/process_photo.go index 389d25cf..47baa032 100644 --- a/api/scanner/process_photo.go +++ b/api/scanner/process_photo.go @@ -149,7 +149,7 @@ func processPhoto(tx *gorm.DB, imageData *EncodeMediaData, photoCachePath *strin fmt.Printf("High-res photo found in database but not in cache, re-encoding photo to cache: %s\n", highResURL.MediaName) didProcess = true - err = imageData.EncodeHighRes(tx, baseImagePath) + err = imageData.EncodeHighRes(baseImagePath) if err != nil { return false, errors.Wrap(err, "creating high-res cached image") } @@ -275,7 +275,7 @@ func saveOriginalPhotoToDB(tx *gorm.DB, photo *models.Media, imageData *EncodeMe func generateSaveHighResJPEG(tx *gorm.DB, media *models.Media, imageData *EncodeMediaData, highres_name string, imagePath string, mediaURL *models.MediaURL) (*models.MediaURL, error) { - err := imageData.EncodeHighRes(tx, imagePath) + err := imageData.EncodeHighRes(imagePath) if err != nil { return nil, errors.Wrap(err, "creating high-res cached image") }