1
Fork 0

Implement proper photo rotation based on exif data

This commit is contained in:
viktorstrate 2020-03-12 12:30:55 +01:00
parent a65a7bc289
commit 179ec5283b
7 changed files with 106 additions and 29 deletions

View File

@ -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

View File

@ -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=

View File

@ -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
}

View File

@ -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

View File

@ -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,
}
}

View File

@ -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;

View File

@ -78,6 +78,8 @@ const PhotoGallery = ({
)
}
console.log(minWidth)
return (
<Photo
key={photo.id}