2020-05-14 14:35:08 +02:00
package scanner
import (
"database/sql"
"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-05-15 16:36:02 +02:00
type PhotoDimensions struct {
Width int
Height int
}
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-05-14 15:17:23 +02:00
// EncodeImageData is used to easily decode image data, with a cache so expensive operations are not repeated
type EncodeImageData struct {
photo * models . Photo
_photoImage image . Image
_thumbnailImage image . Image
_contentType * ImageType
}
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
func ( img * EncodeImageData ) ContentType ( ) ( * ImageType , error ) {
if img . _contentType != nil {
return img . _contentType , nil
}
2020-05-14 15:17:23 +02:00
imgType , err := getImageType ( img . photo . 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-05-15 16:36:02 +02:00
func ( img * EncodeImageData ) EncodeHighRes ( tx * sql . Tx , outputPath string ) error {
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 ( ) {
if DarktableCli . isInstalled ( ) {
err := DarktableCli . EncodeJpeg ( img . photo . Path , outputPath , 70 )
if err != nil {
return err
}
} else {
return errors . New ( "could not convert photo as no RAW converter was found" )
}
}
image , err := img . photoImage ( tx )
if err != nil {
return err
}
EncodeImageJPEG ( image , outputPath , 70 )
return nil
}
func EncodeThumbnail ( inputPath string , outputPath string ) ( * PhotoDimensions , error ) {
inputFile , err := os . Open ( inputPath )
if err != nil {
return nil , err
}
defer inputFile . Close ( )
inputImage , _ , err := image . Decode ( inputFile )
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-05-15 16:36:02 +02:00
func ( img * EncodeImageData ) photoImage ( tx * sql . Tx ) ( image . Image , error ) {
2020-05-14 14:35:08 +02:00
if img . _photoImage != nil {
return img . _photoImage , nil
}
photoFile , err := os . Open ( img . photo . Path )
if err != nil {
return nil , err
}
defer photoFile . Close ( )
2020-05-15 16:36:02 +02:00
photoImg , _ , err := image . Decode ( photoFile )
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
}
// Get orientation from exif data
row := tx . QueryRow ( "SELECT photo_exif.orientation FROM photo JOIN photo_exif WHERE photo.exif_id = photo_exif.exif_id AND photo.photo_id = ?" , img . photo . PhotoID )
var orientation * int
if err = row . Scan ( & orientation ) ; err != nil {
// If not found use default orientation (not rotate)
if err == sql . ErrNoRows {
orientation = nil
} else {
return nil , err
}
}
if orientation == nil {
defaultOrientation := 0
orientation = & defaultOrientation
}
switch * orientation {
case 2 :
photoImg = imaging . FlipH ( photoImg )
break
case 3 :
photoImg = imaging . Rotate180 ( photoImg )
break
case 4 :
photoImg = imaging . FlipV ( photoImg )
break
case 5 :
photoImg = imaging . Transpose ( photoImg )
break
case 6 :
photoImg = imaging . Rotate270 ( photoImg )
break
case 7 :
photoImg = imaging . Transverse ( photoImg )
break
case 8 :
photoImg = imaging . Rotate90 ( photoImg )
break
default :
break
}
img . _photoImage = photoImg
return img . _photoImage , nil
}