Integrate executable worker, still working on darktable
This commit is contained in:
parent
76ade24041
commit
40265a18f7
|
@ -16,7 +16,6 @@ require (
|
|||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/h2non/filetype v1.0.12
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/nf/cr2 v0.0.0-20180623103828-4699471a17ed
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/urfave/cli v1.22.3 // indirect
|
||||
|
|
|
@ -7,12 +7,42 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
cr2Decoder "github.com/nf/cr2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||
"github.com/viktorstrate/photoview/api/utils"
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeImageData is used to easily decode image data, with a cache so expensive operations are not repeated
|
||||
type EncodeImageData struct {
|
||||
photo *models.Photo
|
||||
|
@ -36,6 +66,24 @@ func EncodeImageJPEG(image image.Image, outputPath string, jpegQuality int) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ContentType reads the image to determine its content type
|
||||
func (img *EncodeImageData) ContentType() (*ImageType, error) {
|
||||
if img._contentType != nil {
|
||||
|
@ -51,8 +99,62 @@ func (img *EncodeImageData) ContentType() (*ImageType, error) {
|
|||
return imgType, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// PhotoImage reads and decodes the image file and saves it in a cache so the photo in only decoded once
|
||||
func (img *EncodeImageData) PhotoImage(tx *sql.Tx) (image.Image, error) {
|
||||
func (img *EncodeImageData) photoImage(tx *sql.Tx) (image.Image, error) {
|
||||
if img._photoImage != nil {
|
||||
return img._photoImage, nil
|
||||
}
|
||||
|
@ -63,23 +165,10 @@ func (img *EncodeImageData) PhotoImage(tx *sql.Tx) (image.Image, error) {
|
|||
}
|
||||
defer photoFile.Close()
|
||||
|
||||
var photoImg image.Image
|
||||
contentType, err := img.ContentType()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if contentType != nil && *contentType == "image/x-canon-cr2" {
|
||||
photoImg, err = cr2Decoder.Decode(photoFile)
|
||||
if err != nil {
|
||||
return nil, utils.HandleError("cr2 raw image decoding", err)
|
||||
}
|
||||
} else {
|
||||
photoImg, _, err = image.Decode(photoFile)
|
||||
photoImg, _, err := image.Decode(photoFile)
|
||||
if err != nil {
|
||||
return nil, utils.HandleError("image decoding", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
@ -127,29 +216,3 @@ func (img *EncodeImageData) PhotoImage(tx *sql.Tx) (image.Image, error) {
|
|||
img._photoImage = photoImg
|
||||
return img._photoImage, nil
|
||||
}
|
||||
|
||||
// ThumbnailImage downsizes the image and returns it
|
||||
func (img *EncodeImageData) ThumbnailImage(tx *sql.Tx) (image.Image, error) {
|
||||
photoImage, err := img.PhotoImage(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dimensions := photoImage.Bounds().Max
|
||||
aspect := float64(dimensions.X) / float64(dimensions.Y)
|
||||
|
||||
var width, height int
|
||||
|
||||
if aspect > 1 {
|
||||
width = 1024
|
||||
height = int(1024 / aspect)
|
||||
} else {
|
||||
width = int(1024 * aspect)
|
||||
height = 1024
|
||||
}
|
||||
|
||||
thumbImage := imaging.Thumbnail(photoImage, width, height, imaging.NearestNeighbor)
|
||||
img._thumbnailImage = thumbImage
|
||||
|
||||
return img._thumbnailImage, nil
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ type ExecutableWorker struct {
|
|||
func newExecutableWorker(name string, argsFmt string) ExecutableWorker {
|
||||
path, err := exec.LookPath(name)
|
||||
if err != nil {
|
||||
log.Printf("WARN: Darktable was not found, RAW conversion will be disabled")
|
||||
log.Printf(fmt.Sprintf("WARN: %s was not found", name))
|
||||
}
|
||||
|
||||
return ExecutableWorker{
|
||||
|
@ -27,6 +27,10 @@ func newExecutableWorker(name string, argsFmt string) ExecutableWorker {
|
|||
}
|
||||
}
|
||||
|
||||
func (execWorker *ExecutableWorker) isInstalled() bool {
|
||||
return execWorker.Path != ""
|
||||
}
|
||||
|
||||
func (execWorker *ExecutableWorker) EncodeJpeg(inputPath string, outputPath string, jpegQuality int) error {
|
||||
args := fmt.Sprintf(execWorker.argsFmt, inputPath, outputPath, jpegQuality)
|
||||
cmd := exec.Command(execWorker.Path, args)
|
||||
|
|
|
@ -62,6 +62,16 @@ func (imgType *ImageType) isRaw() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (imgType *ImageType) isWebCompatible() bool {
|
||||
for _, web_mime := range WebMimetypes {
|
||||
if web_mime == *imgType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (imgType *ImageType) isSupported() bool {
|
||||
for _, supported_mime := range SupportedMimetypes {
|
||||
if supported_mime == *imgType {
|
||||
|
|
|
@ -51,11 +51,6 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
|||
photo: photo,
|
||||
}
|
||||
|
||||
photoName := path.Base(photo.Path)
|
||||
|
||||
photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
|
||||
photoBaseExt := path.Ext(photoName)
|
||||
|
||||
photoUrlFromDB, err := makePhotoURLChecker(tx, photo.PhotoID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -67,29 +62,6 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if origURL == nil {
|
||||
original_image_name := fmt.Sprintf("%s_%s", photoBaseName, utils.GenerateToken())
|
||||
original_image_name = strings.ReplaceAll(original_image_name, " ", "_") + photoBaseExt
|
||||
|
||||
photoImage, err := imageData.PhotoImage(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contentType, err := imageData.ContentType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
photoDimensions := photoImage.Bounds().Max
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, original_image_name, photoDimensions.X, photoDimensions.Y, models.PhotoOriginal, contentType)
|
||||
if err != nil {
|
||||
log.Printf("Could not insert original photo url: %d, %s\n", photo.PhotoID, photoName)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Thumbnail
|
||||
thumbURL, err := photoUrlFromDB(models.PhotoThumbnail)
|
||||
if err != nil {
|
||||
|
@ -108,25 +80,91 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
|||
return errors.Wrap(err, "cache directory error")
|
||||
}
|
||||
|
||||
// Save thumbnail to cache
|
||||
if thumbURL == nil {
|
||||
thumbnail_name := fmt.Sprintf("thumbnail_%s_%s", photoName, utils.GenerateToken())
|
||||
thumbnail_name = strings.ReplaceAll(thumbnail_name, ".", "_")
|
||||
thumbnail_name = strings.ReplaceAll(thumbnail_name, " ", "_")
|
||||
thumbnail_name = thumbnail_name + ".jpg"
|
||||
// Generate high res jpeg
|
||||
var photoDimensions *PhotoDimensions
|
||||
var baseImagePath string = photo.Path
|
||||
|
||||
thumbnailImage, err := imageData.ThumbnailImage(tx)
|
||||
if highResURL == nil {
|
||||
|
||||
contentType, err := imageData.ContentType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = EncodeImageJPEG(thumbnailImage, path.Join(*photoCachePath, thumbnail_name), 70)
|
||||
if !contentType.isWebCompatible() {
|
||||
highres_name := fmt.Sprintf("highres_%s_%s", path.Base(photo.Path), utils.GenerateToken())
|
||||
highres_name = strings.ReplaceAll(highres_name, ".", "_")
|
||||
highres_name = strings.ReplaceAll(highres_name, " ", "_")
|
||||
highres_name = highres_name + ".jpg"
|
||||
|
||||
baseImagePath = path.Join(*photoCachePath, highres_name)
|
||||
|
||||
err = imageData.EncodeHighRes(tx, baseImagePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating high-res cached image")
|
||||
}
|
||||
|
||||
photoDimensions, err = GetPhotoDimensions(baseImagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
photo.PhotoID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg")
|
||||
if err != nil {
|
||||
log.Printf("Could not insert highres photo url: %d, %s\n", photo.PhotoID, path.Base(photo.Path))
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Verify that highres photo still exists in cache
|
||||
baseImagePath = path.Join(*photoCachePath, highResURL.PhotoName)
|
||||
|
||||
if _, err := os.Stat(baseImagePath); os.IsNotExist(err) {
|
||||
fmt.Printf("High-res photo found in database but not in cache, re-encoding photo to cache: %s\n", highResURL.PhotoName)
|
||||
|
||||
err = imageData.EncodeHighRes(tx, baseImagePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating high-res cached image")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save original photo to database
|
||||
if origURL == nil {
|
||||
// Make sure photo dimensions is set
|
||||
if photoDimensions == nil {
|
||||
photoDimensions, err = GetPhotoDimensions(baseImagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = saveOriginalPhotoToDB(tx, photo, imageData, photoDimensions); err != nil {
|
||||
return errors.Wrap(err, "saving original photo to database")
|
||||
}
|
||||
}
|
||||
|
||||
// Save thumbnail to cache
|
||||
if thumbURL == nil {
|
||||
thumbnail_name := fmt.Sprintf("thumbnail_%s_%s", path.Base(photo.Path), utils.GenerateToken())
|
||||
thumbnail_name = strings.ReplaceAll(thumbnail_name, ".", "_")
|
||||
thumbnail_name = strings.ReplaceAll(thumbnail_name, " ", "_")
|
||||
thumbnail_name = thumbnail_name + ".jpg"
|
||||
|
||||
// thumbnailImage, err := imageData.ThumbnailImage(tx)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
thumbOutputPath := path.Join(*photoCachePath, thumbnail_name)
|
||||
|
||||
thumbSize, err := EncodeThumbnail(baseImagePath, thumbOutputPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create thumbnail cached image")
|
||||
}
|
||||
|
||||
thumbSize := thumbnailImage.Bounds().Max
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, thumbnail_name, thumbSize.X, thumbSize.Y, models.PhotoThumbnail, "image/jpeg")
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, thumbnail_name, thumbSize.Width, thumbSize.Height, models.PhotoThumbnail, "image/jpeg")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -137,73 +175,9 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
|||
if _, err := os.Stat(thumbPath); os.IsNotExist(err) {
|
||||
fmt.Printf("Thumbnail photo found in database but not in cache, re-encoding photo to cache: %s\n", thumbURL.PhotoName)
|
||||
|
||||
thumbnailImage, err := imageData.ThumbnailImage(tx)
|
||||
_, err := EncodeThumbnail(baseImagePath, thumbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = EncodeImageJPEG(thumbnailImage, thumbPath, 70)
|
||||
if err != nil {
|
||||
log.Println("ERROR: creating thumbnail cached image")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate high res jpeg
|
||||
if highResURL == nil {
|
||||
|
||||
contentType, err := imageData.ContentType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
original_web_safe := false
|
||||
for _, web_mime := range WebMimetypes {
|
||||
if *contentType == web_mime {
|
||||
original_web_safe = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !original_web_safe {
|
||||
highres_name := fmt.Sprintf("highres_%s_%s", photoName, utils.GenerateToken())
|
||||
highres_name = strings.ReplaceAll(highres_name, ".", "_")
|
||||
highres_name = strings.ReplaceAll(highres_name, " ", "_")
|
||||
highres_name = highres_name + ".jpg"
|
||||
|
||||
photoImage, err := imageData.PhotoImage(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = EncodeImageJPEG(photoImage, path.Join(*photoCachePath, highres_name), 70)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating high-res cached image")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
photo.PhotoID, highres_name, photoImage.Bounds().Max.X, photoImage.Bounds().Max.Y, models.PhotoHighRes, "image/jpeg")
|
||||
if err != nil {
|
||||
log.Printf("Could not insert highres photo url: %d, %s\n", photo.PhotoID, photoName)
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Verify that highres photo still exists in cache
|
||||
highResPath := path.Join(*photoCachePath, highResURL.PhotoName)
|
||||
|
||||
if _, err := os.Stat(highResPath); os.IsNotExist(err) {
|
||||
fmt.Printf("High-res photo found in database but not in cache, re-encoding photo to cache: %s\n", highResURL.PhotoName)
|
||||
|
||||
photoImage, err := imageData.PhotoImage(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = EncodeImageJPEG(photoImage, highResPath, 70)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could create high-res cached image")
|
||||
return errors.Wrap(err, "could not create thumbnail cached image")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -241,3 +215,25 @@ func makePhotoCacheDir(photo *models.Photo) (*string, error) {
|
|||
|
||||
return &photoCachePath, nil
|
||||
}
|
||||
|
||||
func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Photo, imageData EncodeImageData, photoDimensions *PhotoDimensions) error {
|
||||
photoName := path.Base(photo.Path)
|
||||
photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
|
||||
photoBaseExt := path.Ext(photoName)
|
||||
|
||||
original_image_name := fmt.Sprintf("%s_%s", photoBaseName, utils.GenerateToken())
|
||||
original_image_name = strings.ReplaceAll(original_image_name, " ", "_") + photoBaseExt
|
||||
|
||||
contentType, err := imageData.ContentType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, original_image_name, photoDimensions.Width, photoDimensions.Height, models.PhotoOriginal, contentType)
|
||||
if err != nil {
|
||||
log.Printf("Could not insert original photo url: %d, %s\n", photo.PhotoID, photoName)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue