diff --git a/api/go.mod b/api/go.mod index 695f711e..f8e5032e 100644 --- a/api/go.mod +++ b/api/go.mod @@ -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 diff --git a/api/go.sum b/api/go.sum index a7678391..81cfb4b3 100644 --- a/api/go.sum +++ b/api/go.sum @@ -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= diff --git a/api/scanner/album_scanner.go b/api/scanner/album_scanner.go index 88a8349d..2ca14a08 100644 --- a/api/scanner/album_scanner.go +++ b/api/scanner/album_scanner.go @@ -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 } diff --git a/api/scanner/process_photo.go b/api/scanner/process_photo.go index 1726879e..b59f814d 100644 --- a/api/scanner/process_photo.go +++ b/api/scanner/process_photo.go @@ -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 diff --git a/api/utils/utils.go b/api/utils/utils.go index 7af2512b..672b0cbe 100644 --- a/api/utils/utils.go +++ b/api/utils/utils.go @@ -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, + } +} diff --git a/ui/src/components/photoGallery/Photo.js b/ui/src/components/photoGallery/Photo.js index 01620922..14e14e7c 100644 --- a/ui/src/components/photoGallery/Photo.js +++ b/ui/src/components/photoGallery/Photo.js @@ -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; diff --git a/ui/src/components/photoGallery/PhotoGallery.js b/ui/src/components/photoGallery/PhotoGallery.js index b583081f..add7dca8 100644 --- a/ui/src/components/photoGallery/PhotoGallery.js +++ b/ui/src/components/photoGallery/PhotoGallery.js @@ -78,6 +78,8 @@ const PhotoGallery = ({ ) } + console.log(minWidth) + return (