Skip to content

Commit

Permalink
Retrieve metadata for an image using exiftool
Browse files Browse the repository at this point in the history
The exiftool can return json using the -json flag, so extract the EXIF
and ITPC data from an image file using it.
  • Loading branch information
akrabat committed May 31, 2020
1 parent 7799862 commit 35b97b4
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 48 deletions.
39 changes: 19 additions & 20 deletions commands/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ image to Flickr.
package commands

import (
"github.com/akrabat/Golem/exif"
"github.com/akrabat/rodeo/internal"
"fmt"
"github.com/spf13/cobra"
"log"
"github.com/spf13/viper"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -50,36 +50,35 @@ var infoCmd = &cobra.Command{
os.Exit(2)
}

exiftool := viper.GetString("cmd.exiftool")
if exiftool == "" {
fmt.Println("Error: cmd.exiftool needs to be configured.")
fmt.Println("Config file:", viper.ConfigFileUsed(), "\n")
os.Exit(2)
}

for _, filename := range args {
fileInfo(filename)
fileInfo(filename, exiftool)
fmt.Printf("\n")
}
},
}

func fileInfo(filename string) {
func fileInfo(filename string, exiftool string) {
fmt.Printf("%v:\n", filepath.Base(filename))

f, err := os.Open(filename)
info, err := internal.GetImageInfo(filename, exiftool)
if err != nil {
log.Fatal(err)
}

image, err := ImgMeta.ReadJpeg(f)
if err != nil {
fmt.Println(err.Error())
return
}

basicInfo := ImgMeta.GetBasicInfo(image)

fmt.Printf(" Title: %v\n", basicInfo.Title)
fmt.Printf(" Description: %v\n", basicInfo.Descr)
fmt.Printf(" Title: %v\n", info.Title)
fmt.Printf(" Description: %v\n", info.Description)

sort.Sort(sort.StringSlice(basicInfo.Keywords[:]))
fmt.Printf(" Keywords: %v\n", strings.Join(basicInfo.Keywords[:], ", "))
sort.Sort(sort.StringSlice(info.Keywords[:]))
fmt.Printf(" Keywords: %v\n", strings.Join(info.Keywords[:], ", "))

fmt.Printf(" Dimensions: width:%v, height:%v\n", basicInfo.Width, basicInfo.Height)
fmt.Printf(" Camera: %v %v\n", basicInfo.Make, basicInfo.Model)
fmt.Printf(" Exposure: %vs, f/%v, ISO%v\n", basicInfo.ShutterSpeed, basicInfo.Aperture, basicInfo.ISO)
fmt.Printf(" Dimensions: width:%v, height:%v\n", info.Width, info.Height)
fmt.Printf(" Camera: %v %v\n", info.Make, info.Model)
fmt.Printf(" Exposure: %vs, f/%v, ISO%v\n", info.ShutterSpeed, info.Aperture, info.ISO)
}
2 changes: 1 addition & 1 deletion commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Version: "0.0.1",
Version: "0.0.2",
Use: "rodeo",
Short: "Flickr command line tool",
Long: `A command line tool to work with Flickr and images.
Expand Down
43 changes: 19 additions & 24 deletions commands/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ image to Flickr.
package commands

import (
"github.com/akrabat/Golem/exif"
"github.com/akrabat/rodeo/internal"
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gopkg.in/masci/flickr.v2"
"gopkg.in/masci/flickr.v2/photosets"
"log"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -77,23 +76,6 @@ var uploadCmd = &cobra.Command{
func uploadFile(filename string) string {
fmt.Println("Processing " + filename)

f, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}

image, err := ImgMeta.ReadJpeg(f)
if err != nil {
fmt.Println(err.Error())
return ""
}

if err := f.Close(); err != nil {
log.Fatal(err)
}

basicInfo := ImgMeta.GetBasicInfo(image)

apiKey := viper.GetString("flickr.api_key")
apiSecret := viper.GetString("flickr.api_secret")
oauthToken := viper.GetString("flickr.oauth_token")
Expand All @@ -102,6 +84,18 @@ func uploadFile(filename string) string {
fmt.Println("Unable to continue. Please run the 'rodeo authenticate' command first")
}

exiftool := viper.GetString("cmd.exiftool")
if exiftool == "" {
fmt.Println("Error: cmd.exiftool needs to be configured.")
fmt.Println("Config file:", viper.ConfigFileUsed(), "\n")
os.Exit(2)
}

info, err := internal.GetImageInfo(filename, exiftool)
if err != nil {
return ""
}

// process keyword settings from config
var keywordsToRemove []string
var keywordsToAdd []string
Expand All @@ -115,7 +109,7 @@ func uploadFile(filename string) string {
// - delete: if `true`, then do not include this keyword as a Flickr tag
// - album_id: if set, then add this photo to that album
// - permissions: if set, then set permissions on this photo
for _, keyword := range basicInfo.Keywords {
for _, keyword := range info.Keywords {
addKeyword := true
settings, ok := keywordSettings[keyword].(map[string]interface{})
if ok {
Expand Down Expand Up @@ -155,7 +149,7 @@ func uploadFile(filename string) string {
}
}
} else {
keywordsToAdd = basicInfo.Keywords
keywordsToAdd = info.Keywords
}

// Upload file to Flickr
Expand All @@ -164,7 +158,7 @@ func uploadFile(filename string) string {
client.OAuthToken = oauthToken
client.OAuthTokenSecret = oauthTokenSecret

title := strings.Trim(basicInfo.Title, " ")
title := strings.Trim(info.Title, " ")
if title == "" {
// no title - use filename (without extension)
title = filepath.Base(filename)
Expand All @@ -182,8 +176,8 @@ func uploadFile(filename string) string {
Hidden: 1, // not hidden
SafetyLevel: 1, // safe
}
if basicInfo.Descr != "" {
params.Description = basicInfo.Descr
if info.Description != "" {
params.Description = info.Description
}

response, err := flickr.UploadFile(client, filename, &params)
Expand All @@ -199,6 +193,7 @@ func uploadFile(filename string) string {
for _, albumId := range albumsToAddTo {
respAdd, err := photosets.AddPhoto(client, albumId, photoId)
if err != nil {
//noinspection GoNilness
fmt.Println("Failed adding photo to the set:"+albumId, err, respAdd.ErrorMsg())
} else {
fmt.Println("Added photo", photoId, "to set", albumId)
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module github.com/akrabat/rodeo
go 1.14

require (
github.com/akrabat/Golem v0.0.0-20200530144310-560a738b360b
github.com/mitchellh/go-homedir v1.1.0
github.com/spf13/cobra v1.0.0
github.com/spf13/viper v1.7.0
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/akrabat/Golem v0.0.0-20200530144310-560a738b360b h1:CT+0W8ZUETIi1NwF7pVWHq0B7ry+2dyGV+4+DJ8/t3E=
github.com/akrabat/Golem v0.0.0-20200530144310-560a738b360b/go.mod h1:+aPpItGwX8uMzY9h0uUJNbNHNTWfFJPzAjiRTcRjsIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
Expand Down
70 changes: 70 additions & 0 deletions internal/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package internal

import (
"encoding/json"
"fmt"
"log"
"os/exec"
)

type ImageInfo struct {
Width uint `json:"ExifImageWidth"`
Height uint `json:"ExifImageHeight"`
Title string `json:"ObjectName"`
Description string `json:"Caption-Abstract"`
Keywords stringArray `json:"Keywords"`
DateCreated string `json:"DateTimeOriginal"`
Make string `json:"Make"`
Model string `json:"Model"`
ShutterSpeed string `json:"ShutterSpeedValue"`
Aperture json.Number `json:"ApertureValue"`
ISO json.Number `json:"ISO"`
}

// A stringArray is an array of strings that has been unmarshalled from a JSON
// property that could be either a string or an array of string
type stringArray []string

func (sa *stringArray) UnmarshalJSON(data []byte) error {
if len(data) > 0 {
switch data[0] {
case '"':
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
*sa = stringArray([]string{s})
case '[':
var s []string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
*sa = stringArray(s)
}
}
return nil
}

// Read metadata (Exif/IPTC) from image using exiftool
func GetImageInfo(filename string, exiftool string) (*ImageInfo, error) {

cmd := exec.Command(exiftool, "-j", "-exif:*", "-iptc:*", filename)

out, err := cmd.Output()
if err != nil {
fmt.Println("Error: ", err)
return nil, err
}

// Exiftool always returns an array but as we only passed in one filename we know that there's only one element.
// Remove the surrounding `[` and `]` from the JSON string to convert to a single object.
out = out[1 : len(out)-2]

info := ImageInfo{}
err = json.Unmarshal(out, &info)
if err != nil {
log.Println(err)
}

return &info, nil
}

0 comments on commit 35b97b4

Please sign in to comment.