Implement proper photo rotation based on exif data
This commit is contained in:
parent
a65a7bc289
commit
179ec5283b
|
@ -4,6 +4,7 @@ go 1.13
|
|||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.10.2
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/golang-migrate/migrate v3.5.4+incompatible
|
||||
|
|
|
@ -6,6 +6,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dsoprea/go-exif v0.0.0-20200126052615-bd04addaf40f h1:yIPu74TXwq1dxn9edEn4p1si4DW/c/h7sgEJ/zMZNcg=
|
||||
github.com/dsoprea/go-exif v0.0.0-20200126052615-bd04addaf40f/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200126052615-bd04addaf40f h1:LJ4lH4r8MgKEC5HeSNRw81lfVOlAM0xELOOvf712H0o=
|
||||
|
@ -90,6 +92,7 @@ github.com/xor-gate/goexif2 v1.1.0 h1:OvTZ5iEvsDhRWFjV5xY3wT7uHFna28nSSP7ucau+cX
|
|||
github.com/xor-gate/goexif2 v1.1.0/go.mod h1:eRjn3VSkAwpNpxEx/CGmd0zg0JFGL3akrSMxnJ581AY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
|
|
@ -168,14 +168,14 @@ func scan(database *sql.DB, user *models.User) {
|
|||
|
||||
photo_paths_scanned = append(photo_paths_scanned, photoPath)
|
||||
|
||||
photo, newPhoto, err := ScanPhoto(tx, photoPath, albumId, processKey)
|
||||
photo, isNewPhoto, err := ScanPhoto(tx, photoPath, albumId, processKey)
|
||||
if err != nil {
|
||||
ScannerError("Scanning image %s: %s", photoPath, err)
|
||||
tx.Rollback()
|
||||
continue
|
||||
}
|
||||
|
||||
if newPhoto {
|
||||
if isNewPhoto {
|
||||
newPhotos.PushBack(photo)
|
||||
|
||||
if newPhotos.Len()%25 == 0 {
|
||||
|
@ -375,7 +375,7 @@ func processUnprocessedPhotos(database *sql.DB, user *models.User, notifyKey str
|
|||
err = ProcessPhoto(tx, photo)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
ScannerError("Could not process photo: %s", err)
|
||||
ScannerError("Could not process photo (%s): %s", photo.Path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/h2non/filetype"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||
"github.com/viktorstrate/photoview/api/utils"
|
||||
|
||||
|
@ -53,7 +53,7 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
|||
log.Printf("Processing photo: %s\n", photo.Path)
|
||||
|
||||
imageData := ProcessImageData{
|
||||
photoPath: photo.Path,
|
||||
photo: photo,
|
||||
}
|
||||
|
||||
photoName := path.Base(photo.Path)
|
||||
|
@ -76,7 +76,7 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
|||
original_image_name := fmt.Sprintf("%s_%s", photoBaseName, utils.GenerateToken())
|
||||
original_image_name = strings.ReplaceAll(original_image_name, " ", "_") + photoBaseExt
|
||||
|
||||
photoImage, err := imageData.PhotoImage()
|
||||
photoImage, err := imageData.PhotoImage(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -86,7 +86,9 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
|||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, original_image_name, photoImage.Bounds().Max.X, photoImage.Bounds().Max.Y, models.PhotoOriginal, contentType)
|
||||
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
|
||||
|
@ -118,7 +120,7 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
|||
thumbnail_name = strings.ReplaceAll(thumbnail_name, " ", "_")
|
||||
thumbnail_name = thumbnail_name + ".jpg"
|
||||
|
||||
thumbnailImage, err := imageData.ThumbnailImage()
|
||||
thumbnailImage, err := imageData.ThumbnailImage(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -140,7 +142,7 @@ 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()
|
||||
thumbnailImage, err := imageData.ThumbnailImage(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -175,7 +177,7 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
|||
highres_name = strings.ReplaceAll(highres_name, " ", "_")
|
||||
highres_name = highres_name + ".jpg"
|
||||
|
||||
photoImage, err := imageData.PhotoImage()
|
||||
photoImage, err := imageData.PhotoImage(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -199,7 +201,7 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
|||
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()
|
||||
photoImage, err := imageData.PhotoImage(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -264,7 +266,7 @@ func encodeImageJPEG(photoPath string, photoImage image.Image, jpegOptions *jpeg
|
|||
|
||||
// ProcessImageData is used to easily decode image data, with a cache so expensive operations are not repeated
|
||||
type ProcessImageData struct {
|
||||
photoPath string
|
||||
photo *models.Photo
|
||||
_photoImage image.Image
|
||||
_thumbnailImage image.Image
|
||||
_contentType *string
|
||||
|
@ -276,16 +278,16 @@ func (img *ProcessImageData) ContentType() (*string, error) {
|
|||
return img._contentType, nil
|
||||
}
|
||||
|
||||
file, err := os.Open(img.photoPath)
|
||||
file, err := os.Open(img.photo.Path)
|
||||
if err != nil {
|
||||
ScannerError("Could not open file %s: %s\n", img.photoPath, err)
|
||||
ScannerError("Could not open file %s: %s\n", img.photo.Path, err)
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
head := make([]byte, 261)
|
||||
if _, err := file.Read(head); err != nil {
|
||||
ScannerError("Could not read file %s: %s\n", img.photoPath, err)
|
||||
ScannerError("Could not read file %s: %s\n", img.photo.Path, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -299,12 +301,12 @@ func (img *ProcessImageData) ContentType() (*string, error) {
|
|||
}
|
||||
|
||||
// PhotoImage reads and decodes the image file and saves it in a cache so the photo in only decoded once
|
||||
func (img *ProcessImageData) PhotoImage() (image.Image, error) {
|
||||
func (img *ProcessImageData) PhotoImage(tx *sql.Tx) (image.Image, error) {
|
||||
if img._photoImage != nil {
|
||||
return img._photoImage, nil
|
||||
}
|
||||
|
||||
photoFile, err := os.Open(img.photoPath)
|
||||
photoFile, err := os.Open(img.photo.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -319,29 +321,80 @@ func (img *ProcessImageData) PhotoImage() (image.Image, error) {
|
|||
if contentType != nil && *contentType == "image/x-canon-cr2" {
|
||||
photoImg, err = cr2Decoder.Decode(photoFile)
|
||||
if err != nil {
|
||||
log.Println("ERROR: decoding cr2 raw image")
|
||||
return nil, err
|
||||
return nil, utils.HandleError("cr2 raw image decoding", err)
|
||||
}
|
||||
} else {
|
||||
photoImg, _, err = image.Decode(photoFile)
|
||||
if err != nil {
|
||||
log.Println("ERROR: decoding image")
|
||||
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)
|
||||
var orientation int
|
||||
if err = row.Scan(&orientation); err != nil {
|
||||
// If not found use default orientation (not rotate)
|
||||
if err == sql.ErrNoRows {
|
||||
orientation = 0
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("ORIENTATION: %d\n", orientation)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ThumbnailImage downsizes the image and returns it
|
||||
func (img *ProcessImageData) ThumbnailImage() (image.Image, error) {
|
||||
photoImage, err := img.PhotoImage()
|
||||
func (img *ProcessImageData) ThumbnailImage(tx *sql.Tx) (image.Image, error) {
|
||||
photoImage, err := img.PhotoImage(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thumbImage := resize.Thumbnail(1024, 1024, photoImage, resize.Bilinear)
|
||||
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
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package utils
|
||||
|
||||
import "crypto/rand"
|
||||
|
||||
import "math/big"
|
||||
|
||||
import "log"
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func GenerateToken() string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
@ -23,3 +24,20 @@ func GenerateToken() string {
|
|||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
type PhotoviewError struct {
|
||||
message string
|
||||
original error
|
||||
}
|
||||
|
||||
func (e PhotoviewError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.message, e.original)
|
||||
}
|
||||
|
||||
func HandleError(message string, err error) PhotoviewError {
|
||||
log.Printf("ERROR: %s: %s", message, err)
|
||||
return PhotoviewError{
|
||||
message: message,
|
||||
original: err,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import ProtectedImage from './ProtectedImage'
|
|||
|
||||
const PhotoContainer = styled.div`
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
/* flex-basis: 1; */
|
||||
height: 200px;
|
||||
margin: 4px;
|
||||
background-color: #eee;
|
||||
|
|
|
@ -78,6 +78,8 @@ const PhotoGallery = ({
|
|||
)
|
||||
}
|
||||
|
||||
console.log(minWidth)
|
||||
|
||||
return (
|
||||
<Photo
|
||||
key={photo.id}
|
||||
|
|
Loading…
Reference in New Issue