init
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
// This tool dumps EXIF information from images.
|
||||
//
|
||||
// Example command-line:
|
||||
//
|
||||
// exif-read-tool -filepath <file-path>
|
||||
//
|
||||
// Example Output:
|
||||
//
|
||||
// IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]
|
||||
// IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]
|
||||
// IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[1]
|
||||
// IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
|
||||
// ...
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"b612.me/exif"
|
||||
"b612.me/exif/common"
|
||||
)
|
||||
|
||||
const (
|
||||
thumbnailFilenameIndexPlaceholder = "<index>"
|
||||
)
|
||||
|
||||
var (
|
||||
mainLogger = log.NewLogger("main.main")
|
||||
)
|
||||
|
||||
// IfdEntry is a JSON model for representing a single tag.
|
||||
type IfdEntry struct {
|
||||
IfdPath string `json:"ifd_path"`
|
||||
FqIfdPath string `json:"fq_ifd_path"`
|
||||
IfdIndex int `json:"ifd_index"`
|
||||
TagId uint16 `json:"tag_id"`
|
||||
TagName string `json:"tag_name"`
|
||||
TagTypeId exifcommon.TagTypePrimitive `json:"tag_type_id"`
|
||||
TagTypeName string `json:"tag_type_name"`
|
||||
UnitCount uint32 `json:"unit_count"`
|
||||
Value interface{} `json:"value"`
|
||||
ValueString string `json:"value_string"`
|
||||
}
|
||||
|
||||
type parameters struct {
|
||||
Filepath string `short:"f" long:"filepath" required:"true" description:"File-path of image"`
|
||||
PrintAsJson bool `short:"j" long:"json" description:"Print out as JSON"`
|
||||
IsVerbose bool `short:"v" long:"verbose" description:"Print logging"`
|
||||
ThumbnailOutputFilepath string `short:"t" long:"thumbnail-output-filepath" description:"File-path to write thumbnail to (if present)"`
|
||||
DoNotPrintTags bool `short:"n" long:"no-tags" description:"Do not actually print tags. Good for auditing the logs or merely checking the EXIF structure for errors."`
|
||||
SkipBlocks int `short:"s" long:"skip" description:"Skip this many EXIF blocks before returning"`
|
||||
DoUniversalTagSearch bool `short:"u" long:"universal-tags" description:"If tags not found in known mapped IFDs, fallback to trying all IFDs."`
|
||||
}
|
||||
|
||||
var (
|
||||
arguments = new(parameters)
|
||||
)
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
if errRaw := recover(); errRaw != nil {
|
||||
err := errRaw.(error)
|
||||
log.PrintError(err)
|
||||
|
||||
os.Exit(-2)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err := flags.Parse(arguments)
|
||||
if err != nil {
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
if arguments.IsVerbose == true {
|
||||
cla := log.NewConsoleLogAdapter()
|
||||
log.AddAdapter("console", cla)
|
||||
|
||||
scp := log.NewStaticConfigurationProvider()
|
||||
scp.SetLevelName(log.LevelNameDebug)
|
||||
|
||||
log.LoadConfiguration(scp)
|
||||
}
|
||||
|
||||
f, err := os.Open(arguments.Filepath)
|
||||
log.PanicIf(err)
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
log.PanicIf(err)
|
||||
|
||||
rawExif, err := exif.SearchAndExtractExifN(data, arguments.SkipBlocks)
|
||||
if err != nil {
|
||||
if err == exif.ErrNoExif {
|
||||
fmt.Printf("No EXIF data.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
mainLogger.Debugf(nil, "EXIF blob is (%d) bytes.", len(rawExif))
|
||||
|
||||
// Run the parse.
|
||||
|
||||
entries, _, err := exif.GetFlatExifDataUniversalSearch(rawExif, nil, arguments.DoUniversalTagSearch)
|
||||
if err != nil {
|
||||
if arguments.SkipBlocks > 0 {
|
||||
mainLogger.Warningf(nil, "Encountered an error. This might be related to the request to skip EXIF blocks.")
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
// Write the thumbnail is requested and present.
|
||||
|
||||
thumbnailOutputFilepath := arguments.ThumbnailOutputFilepath
|
||||
if thumbnailOutputFilepath != "" {
|
||||
im, err := exifcommon.NewIfdMappingWithStandard()
|
||||
log.PanicIf(err)
|
||||
|
||||
ti := exif.NewTagIndex()
|
||||
|
||||
_, index, err := exif.Collect(im, ti, rawExif)
|
||||
log.PanicIf(err)
|
||||
|
||||
var thumbnail []byte
|
||||
if ifd, found := index.Lookup[exif.ThumbnailFqIfdPath]; found == true {
|
||||
thumbnail, err = ifd.Thumbnail()
|
||||
if err != nil && err != exif.ErrNoThumbnail {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if thumbnail == nil {
|
||||
mainLogger.Debugf(nil, "No thumbnails found.")
|
||||
} else {
|
||||
if arguments.PrintAsJson == false {
|
||||
fmt.Printf("Writing (%d) bytes for thumbnail: [%s]\n", len(thumbnail), thumbnailOutputFilepath)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
err := ioutil.WriteFile(thumbnailOutputFilepath, thumbnail, 0644)
|
||||
log.PanicIf(err)
|
||||
}
|
||||
}
|
||||
|
||||
if arguments.DoNotPrintTags == false {
|
||||
if arguments.PrintAsJson == true {
|
||||
data, err := json.MarshalIndent(entries, "", " ")
|
||||
log.PanicIf(err)
|
||||
|
||||
fmt.Println(string(data))
|
||||
} else {
|
||||
thumbnailTags := 0
|
||||
for _, entry := range entries {
|
||||
fmt.Printf("IFD-PATH=[%s] ID=(0x%04x) NAME=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]\n", entry.IfdPath, entry.TagId, entry.TagName, entry.UnitCount, entry.TagTypeName, entry.Formatted)
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
|
||||
if thumbnailTags == 2 {
|
||||
fmt.Printf("There is a thumbnail.\n")
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"b612.me/exif/common"
|
||||
)
|
||||
|
||||
func TestMainProc(t *testing.T) {
|
||||
appFilepath := getAppFilepath()
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
cmd := exec.Command(
|
||||
"go", "run", appFilepath,
|
||||
"--filepath", testImageFilepath)
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
cmd.Stdout = b
|
||||
cmd.Stderr = b
|
||||
|
||||
err := cmd.Run()
|
||||
actual := b.String()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf(actual)
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
expected := `IFD-PATH=[IFD] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]
|
||||
IFD-PATH=[IFD] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]
|
||||
IFD-PATH=[IFD] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[[1]]
|
||||
IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[[72/1]]
|
||||
IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[[72/1]]
|
||||
IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[[2]]
|
||||
IFD-PATH=[IFD] ID=(0x0132) NAME=[DateTime] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]
|
||||
IFD-PATH=[IFD] ID=(0x013b) NAME=[Artist] COUNT=(1) TYPE=[ASCII] VALUE=[]
|
||||
IFD-PATH=[IFD] ID=(0x0213) NAME=[YCbCrPositioning] COUNT=(1) TYPE=[SHORT] VALUE=[[2]]
|
||||
IFD-PATH=[IFD] ID=(0x8298) NAME=[Copyright] COUNT=(1) TYPE=[ASCII] VALUE=[]
|
||||
IFD-PATH=[IFD] ID=(0x8769) NAME=[ExifTag] COUNT=(1) TYPE=[LONG] VALUE=[[360]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x829a) NAME=[ExposureTime] COUNT=(1) TYPE=[RATIONAL] VALUE=[[1/640]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x829d) NAME=[FNumber] COUNT=(1) TYPE=[RATIONAL] VALUE=[[4/1]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x8822) NAME=[ExposureProgram] COUNT=(1) TYPE=[SHORT] VALUE=[[4]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x8827) NAME=[ISOSpeedRatings] COUNT=(1) TYPE=[SHORT] VALUE=[[1600]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x8830) NAME=[SensitivityType] COUNT=(1) TYPE=[SHORT] VALUE=[[2]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] COUNT=(1) TYPE=[LONG] VALUE=[[1600]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9000) NAME=[ExifVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0230]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9003) NAME=[DateTimeOriginal] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9004) NAME=[DateTimeDigitized] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9101) NAME=[ComponentsConfiguration] COUNT=(4) TYPE=[UNDEFINED] VALUE=[Exif9101ComponentsConfiguration<ID=[YCBCR] BYTES=[1 2 3 0]>]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9201) NAME=[ShutterSpeedValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[[614400/65536]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9202) NAME=[ApertureValue] COUNT=(1) TYPE=[RATIONAL] VALUE=[[262144/65536]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9204) NAME=[ExposureBiasValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[[0/1]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9207) NAME=[MeteringMode] COUNT=(1) TYPE=[SHORT] VALUE=[[5]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9209) NAME=[Flash] COUNT=(1) TYPE=[SHORT] VALUE=[[16]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x920a) NAME=[FocalLength] COUNT=(1) TYPE=[RATIONAL] VALUE=[[16/1]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x927c) NAME=[MakerNote] COUNT=(8152) TYPE=[UNDEFINED] VALUE=[MakerNote<TYPE-ID=[28 00 01 00 03 00 31 00 00 00 74 05 00 00 02 00 03 00 04 00] LEN=(8152) SHA1=[d4154aa7df5474efe7ab38de2595919b9b4cc29f]>]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] COUNT=(264) TYPE=[UNDEFINED] VALUE=[UserComment<SIZE=(256) ENCODING=[UNDEFINED] V=[0 0 0 0 0 0 0 0]... LEN=(256)>]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9290) NAME=[SubSecTime] COUNT=(3) TYPE=[ASCII] VALUE=[00]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9291) NAME=[SubSecTimeOriginal] COUNT=(3) TYPE=[ASCII] VALUE=[00]
|
||||
IFD-PATH=[IFD/Exif] ID=(0x9292) NAME=[SubSecTimeDigitized] COUNT=(3) TYPE=[ASCII] VALUE=[00]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa000) NAME=[FlashpixVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0100]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa001) NAME=[ColorSpace] COUNT=(1) TYPE=[SHORT] VALUE=[[1]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa002) NAME=[PixelXDimension] COUNT=(1) TYPE=[SHORT] VALUE=[[3840]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa003) NAME=[PixelYDimension] COUNT=(1) TYPE=[SHORT] VALUE=[[2560]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa005) NAME=[InteroperabilityTag] COUNT=(1) TYPE=[LONG] VALUE=[[9326]]
|
||||
IFD-PATH=[IFD/Exif/Iop] ID=(0x0001) NAME=[InteroperabilityIndex] COUNT=(4) TYPE=[ASCII] VALUE=[R98]
|
||||
IFD-PATH=[IFD/Exif/Iop] ID=(0x0002) NAME=[InteroperabilityVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0100]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa20e) NAME=[FocalPlaneXResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[[3840000/1461]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa20f) NAME=[FocalPlaneYResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[[2560000/972]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa210) NAME=[FocalPlaneResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[[2]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa401) NAME=[CustomRendered] COUNT=(1) TYPE=[SHORT] VALUE=[[0]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa402) NAME=[ExposureMode] COUNT=(1) TYPE=[SHORT] VALUE=[[0]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa403) NAME=[WhiteBalance] COUNT=(1) TYPE=[SHORT] VALUE=[[0]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa406) NAME=[SceneCaptureType] COUNT=(1) TYPE=[SHORT] VALUE=[[0]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa430) NAME=[CameraOwnerName] COUNT=(1) TYPE=[ASCII] VALUE=[]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa431) NAME=[BodySerialNumber] COUNT=(13) TYPE=[ASCII] VALUE=[063024020097]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa432) NAME=[LensSpecification] COUNT=(4) TYPE=[RATIONAL] VALUE=[[16/1 35/1 0/1 0/1]]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa434) NAME=[LensModel] COUNT=(22) TYPE=[ASCII] VALUE=[EF16-35mm f/4L IS USM]
|
||||
IFD-PATH=[IFD/Exif] ID=(0xa435) NAME=[LensSerialNumber] COUNT=(11) TYPE=[ASCII] VALUE=[2400001068]
|
||||
IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] COUNT=(1) TYPE=[LONG] VALUE=[[9554]]
|
||||
IFD-PATH=[IFD/GPSInfo] ID=(0x0000) NAME=[GPSVersionID] COUNT=(4) TYPE=[BYTE] VALUE=[02 03 00 00]
|
||||
IFD-PATH=[IFD1] ID=(0x0103) NAME=[Compression] COUNT=(1) TYPE=[SHORT] VALUE=[[6]]
|
||||
IFD-PATH=[IFD1] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[[72/1]]
|
||||
IFD-PATH=[IFD1] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[[72/1]]
|
||||
IFD-PATH=[IFD1] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[[2]]
|
||||
IFD-PATH=[IFD1] ID=(0x0201) NAME=[JPEGInterchangeFormat] COUNT=(1) TYPE=[LONG] VALUE=[[11444]]
|
||||
IFD-PATH=[IFD1] ID=(0x0202) NAME=[JPEGInterchangeFormatLength] COUNT=(1) TYPE=[LONG] VALUE=[[21491]]
|
||||
|
||||
`
|
||||
|
||||
if actual != expected {
|
||||
t.Fatalf("Output not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainJson(t *testing.T) {
|
||||
appFilepath := getAppFilepath()
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
cmd := exec.Command(
|
||||
"go", "run", appFilepath,
|
||||
"--filepath", testImageFilepath,
|
||||
"--json")
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
cmd.Stdout = b
|
||||
cmd.Stderr = b
|
||||
|
||||
err := cmd.Run()
|
||||
actualRaw := b.Bytes()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf(string(actualRaw))
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
// Parse actual data.
|
||||
|
||||
actual := make([]map[string]interface{}, 0)
|
||||
|
||||
err = json.Unmarshal(actualRaw, &actual)
|
||||
log.PanicIf(err)
|
||||
|
||||
// Read and parse expected data.
|
||||
|
||||
assetsPath := exifcommon.GetTestAssetsPath()
|
||||
jsonFilepath := path.Join(assetsPath, "main_test_exif.json")
|
||||
|
||||
expectedRaw, err := ioutil.ReadFile(jsonFilepath)
|
||||
log.PanicIf(err)
|
||||
|
||||
expected := make([]map[string]interface{}, 0)
|
||||
|
||||
err = json.Unmarshal(expectedRaw, &expected)
|
||||
log.PanicIf(err)
|
||||
|
||||
// if reflect.DeepEqual(actual, expected) == false {
|
||||
// t.Fatalf("Output not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", actualRaw, expectedRaw)
|
||||
// }
|
||||
|
||||
for i, tagInfo := range actual {
|
||||
if reflect.DeepEqual(tagInfo, expected[i]) == false {
|
||||
actualBytes, err := json.MarshalIndent(tagInfo, "", " ")
|
||||
log.PanicIf(err)
|
||||
|
||||
expectedBytes, err := json.MarshalIndent(expected[i], "", " ")
|
||||
log.PanicIf(err)
|
||||
|
||||
t.Fatalf("Tag (%d) not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", i, string(actualBytes), string(expectedBytes))
|
||||
}
|
||||
}
|
||||
|
||||
if len(actual) != len(expected) {
|
||||
t.Fatalf("Actual tags not same length as expected tags.")
|
||||
}
|
||||
}
|
||||
|
||||
func getAppFilepath() string {
|
||||
moduleRootPath := exifcommon.GetModuleRootPath()
|
||||
appFilepath := path.Join(moduleRootPath, "command", "exif-read-tool", "main.go")
|
||||
|
||||
return appFilepath
|
||||
}
|
||||
|
||||
func getTestImageFilepath() string {
|
||||
assetsPath := exifcommon.GetTestAssetsPath()
|
||||
testImageFilepath := path.Join(assetsPath, "NDM_8901.jpg")
|
||||
|
||||
return testImageFilepath
|
||||
}
|
||||
Reference in New Issue
Block a user