Timestamps can now be set directly

dustin/master
Dustin Oprea 2020-06-07 01:33:12 -04:00
parent d69a43ee6a
commit 7edf52b885
8 changed files with 151 additions and 2 deletions

View File

@ -3,6 +3,7 @@ package exifcommon
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"time"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
) )
@ -68,3 +69,14 @@ func DumpBytesClauseToString(data []byte) string {
return b.String() return b.String()
} }
// ExifFullTimestampString produces a string like "2018:11:30 13:01:49" from a
// `time.Time` struct. It will attempt to convert to UTC first.
func ExifFullTimestampString(t time.Time) (fullTimestampPhrase string) {
// RELEASE(dustin): Dump this for the next release. It duplicates the same function now in exifcommon.
t = t.UTC()
return fmt.Sprintf("%04d:%02d:%02d %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
}

View File

@ -3,6 +3,7 @@ package exifcommon
import ( import (
"bytes" "bytes"
"reflect" "reflect"
"time"
"encoding/binary" "encoding/binary"
@ -209,6 +210,15 @@ func (ve *ValueEncoder) Encode(value interface{}) (ed EncodedData, err error) {
case []SignedRational: case []SignedRational:
ed, err = ve.encodeSignedRationals(value.([]SignedRational)) ed, err = ve.encodeSignedRationals(value.([]SignedRational))
log.PanicIf(err) log.PanicIf(err)
case time.Time:
// For convenience, if the user doesn't want to deal with translation
// semantics with timestamps.
t := value.(time.Time)
s := ExifFullTimestampString(t)
ed, err = ve.encodeAscii(s)
log.PanicIf(err)
default: default:
log.Panicf("value not encodable: [%s] [%v]", reflect.TypeOf(value), value) log.Panicf("value not encodable: [%s] [%v]", reflect.TypeOf(value), value)
} }

View File

@ -1,8 +1,10 @@
package exifcommon package exifcommon
import ( import (
"bytes"
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
) )
@ -564,3 +566,27 @@ func TestValueEncoder_Encode__SignedRational(t *testing.T) {
t.Fatalf("Unit-count not correct.") t.Fatalf("Unit-count not correct.")
} }
} }
func TestValueEncoder_Encode__Timestamp(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
now := time.Now()
ed, err := ve.Encode(now)
log.PanicIf(err)
if ed.Type != TypeAscii {
t.Fatalf("Timestamp not encoded as ASCII.")
}
expectedTimestampBytes := ExifFullTimestampString(now)
// Leave an extra byte for the NUL.
expected := make([]byte, len(expectedTimestampBytes)+1)
copy(expected, expectedTimestampBytes)
if bytes.Equal(ed.Encoded, expected) != true {
t.Fatalf("Timestamp not encoded correctly: [%s] != [%s]", string(ed.Encoded), string(expected))
}
}

View File

@ -7,6 +7,7 @@ import (
"sort" "sort"
"strings" "strings"
"testing" "testing"
"time"
"github.com/dsoprea/go-exif/v2/common" "github.com/dsoprea/go-exif/v2/common"
"github.com/dsoprea/go-exif/v2/undefined" "github.com/dsoprea/go-exif/v2/undefined"
@ -1550,6 +1551,71 @@ func ExampleIfdBuilder_SetStandardWithName_updateGps() {
// Degrees<O=[N] D=(11) M=(22) S=(33)> // Degrees<O=[N] D=(11) M=(22) S=(33)>
} }
func ExampleIfdBuilder_SetStandardWithName_timestamp() {
// Check initial value.
filepath := getTestGpsImageFilepath()
rawExif, err := SearchFileAndExtractExif(filepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
rootIfd := index.RootIfd
// Update the value.
rootIb := NewIfdBuilderFromExistingChain(rootIfd)
exifIb, err := rootIb.ChildWithTagId(exifcommon.IfdExifStandardIfdIdentity.TagId())
log.PanicIf(err)
t := time.Date(2020, 06, 7, 1, 30, 0, 0, time.UTC)
err = exifIb.SetStandardWithName("DateTimeDigitized", t)
log.PanicIf(err)
// Encode to bytes.
ibe := NewIfdByteEncoder()
updatedRawExif, err := ibe.EncodeToExif(rootIb)
log.PanicIf(err)
// Decode from bytes.
_, updatedIndex, err := Collect(im, ti, updatedRawExif)
log.PanicIf(err)
updatedRootIfd := updatedIndex.RootIfd
// Test.
updatedExifIfd, err := updatedRootIfd.ChildWithIfdPath(exifcommon.IfdExifStandardIfdIdentity)
log.PanicIf(err)
results, err := updatedExifIfd.FindTagWithName("DateTimeDigitized")
log.PanicIf(err)
ite := results[0]
phrase, err := ite.FormatFirst()
log.PanicIf(err)
fmt.Printf("%s\n", phrase)
// Output:
// 2020:06:07 01:30:00
}
func TestIfdBuilder_NewIfdBuilderFromExistingChain_RealData(t *testing.T) { func TestIfdBuilder_NewIfdBuilderFromExistingChain_RealData(t *testing.T) {
testImageFilepath := getTestImageFilepath() testImageFilepath := getTestImageFilepath()

View File

@ -234,6 +234,8 @@ func (ite *IfdTagEntry) FormatFirst() (phrase string, err error) {
} }
}() }()
// TODO(dustin): We should add a convenience type "timestamp", to simplify translating to and from the physical ASCII and provide validation.
value, err := ite.Value() value, err := ite.Value()
if err != nil { if err != nil {
if err == exifcommon.ErrUnhandledUndefinedTypedTag { if err == exifcommon.ErrUnhandledUndefinedTypedTag {

View File

@ -114,6 +114,12 @@ func (it *IndexedTag) Is(ifdPath string, id uint16) bool {
// WidestSupportedType returns the largest type that this tag's value can // WidestSupportedType returns the largest type that this tag's value can
// occupy // occupy
func (it *IndexedTag) GetEncodingType(value interface{}) exifcommon.TagTypePrimitive { func (it *IndexedTag) GetEncodingType(value interface{}) exifcommon.TagTypePrimitive {
// For convenience, we handle encoding a `time.Time` directly.
if IsTime(value) == true {
// Timestamps are encoded as ASCII.
value = ""
}
if len(it.SupportedTypes) == 0 { if len(it.SupportedTypes) == 0 {
log.Panicf("IndexedTag [%s] (%d) has no supported types.", it.IfdPath, it.Id) log.Panicf("IndexedTag [%s] (%d) has no supported types.", it.IfdPath, it.Id)
} else if len(it.SupportedTypes) == 1 { } else if len(it.SupportedTypes) == 1 {

View File

@ -3,6 +3,7 @@ package exif
import ( import (
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
@ -181,6 +182,20 @@ func TestIndexedTag_GetEncodingType_BothRationalTypes(t *testing.T) {
} }
} }
func TestIndexedTag_GetEncodingType_Timestamp(t *testing.T) {
it := &IndexedTag{
SupportedTypes: []exifcommon.TagTypePrimitive{
exifcommon.TypeAscii,
},
}
zeroTime := time.Time{}
if it.GetEncodingType(zeroTime) != exifcommon.TypeAscii {
t.Fatalf("Expected the timestamp to to be encoded as ASCII.")
}
}
func TestIndexedTag_DoesSupportType(t *testing.T) { func TestIndexedTag_DoesSupportType(t *testing.T) {
it := &IndexedTag{ it := &IndexedTag{
Id: 0xb, Id: 0xb,

View File

@ -3,6 +3,7 @@ package exif
import ( import (
"fmt" "fmt"
"math" "math"
"reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -17,6 +18,10 @@ var (
utilityLogger = log.NewLogger("exif.utility") utilityLogger = log.NewLogger("exif.utility")
) )
var (
timeType = reflect.TypeOf(time.Time{})
)
// ParseExifFullTimestamp parses dates like "2018:11:30 13:01:49" into a UTC // ParseExifFullTimestamp parses dates like "2018:11:30 13:01:49" into a UTC
// `time.Time` struct. // `time.Time` struct.
func ParseExifFullTimestamp(fullTimestampPhrase string) (timestamp time.Time, err error) { func ParseExifFullTimestamp(fullTimestampPhrase string) (timestamp time.Time, err error) {
@ -70,9 +75,10 @@ func ParseExifFullTimestamp(fullTimestampPhrase string) (timestamp time.Time, er
// ExifFullTimestampString produces a string like "2018:11:30 13:01:49" from a // ExifFullTimestampString produces a string like "2018:11:30 13:01:49" from a
// `time.Time` struct. It will attempt to convert to UTC first. // `time.Time` struct. It will attempt to convert to UTC first.
func ExifFullTimestampString(t time.Time) (fullTimestampPhrase string) { func ExifFullTimestampString(t time.Time) (fullTimestampPhrase string) {
t = t.UTC()
return fmt.Sprintf("%04d:%02d:%02d %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) // RELEASE(dustin): Dump this for the next release. It duplicates the same function now in exifcommon.
return exifcommon.ExifFullTimestampString(t)
} }
// ExifTag is one simple representation of a tag in a flat list of all of them. // ExifTag is one simple representation of a tag in a flat list of all of them.
@ -201,6 +207,7 @@ func GetFlatExifData(exifData []byte) (exifTags []ExifTag, err error) {
return exifTags, nil return exifTags, nil
} }
// GpsDegreesEquals returns true if the two `GpsDegrees` are identical.
func GpsDegreesEquals(gi1, gi2 GpsDegrees) bool { func GpsDegreesEquals(gi1, gi2 GpsDegrees) bool {
if gi2.Orientation != gi1.Orientation { if gi2.Orientation != gi1.Orientation {
return false return false
@ -220,3 +227,8 @@ func GpsDegreesEquals(gi1, gi2 GpsDegrees) bool {
return true return true
} }
// IsTime returns true if the value is a `time.Time`.
func IsTime(v interface{}) bool {
return reflect.TypeOf(v) == timeType
}