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 (
"bytes"
"fmt"
"time"
"github.com/dsoprea/go-logging"
)
@ -68,3 +69,14 @@ func DumpBytesClauseToString(data []byte) 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 (
"bytes"
"reflect"
"time"
"encoding/binary"
@ -209,6 +210,15 @@ func (ve *ValueEncoder) Encode(value interface{}) (ed EncodedData, err error) {
case []SignedRational:
ed, err = ve.encodeSignedRationals(value.([]SignedRational))
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:
log.Panicf("value not encodable: [%s] [%v]", reflect.TypeOf(value), value)
}

View File

@ -1,8 +1,10 @@
package exifcommon
import (
"bytes"
"reflect"
"testing"
"time"
"github.com/dsoprea/go-logging"
)
@ -564,3 +566,27 @@ func TestValueEncoder_Encode__SignedRational(t *testing.T) {
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"
"strings"
"testing"
"time"
"github.com/dsoprea/go-exif/v2/common"
"github.com/dsoprea/go-exif/v2/undefined"
@ -1550,6 +1551,71 @@ func ExampleIfdBuilder_SetStandardWithName_updateGps() {
// 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) {
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()
if err != nil {
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
// occupy
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 {
log.Panicf("IndexedTag [%s] (%d) has no supported types.", it.IfdPath, it.Id)
} else if len(it.SupportedTypes) == 1 {

View File

@ -3,6 +3,7 @@ package exif
import (
"reflect"
"testing"
"time"
"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) {
it := &IndexedTag{
Id: 0xb,

View File

@ -3,6 +3,7 @@ package exif
import (
"fmt"
"math"
"reflect"
"strconv"
"strings"
"time"
@ -17,6 +18,10 @@ var (
utilityLogger = log.NewLogger("exif.utility")
)
var (
timeType = reflect.TypeOf(time.Time{})
)
// ParseExifFullTimestamp parses dates like "2018:11:30 13:01:49" into a UTC
// `time.Time` struct.
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
// `time.Time` struct. It will attempt to convert to UTC first.
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.
@ -201,6 +207,7 @@ func GetFlatExifData(exifData []byte) (exifTags []ExifTag, err error) {
return exifTags, nil
}
// GpsDegreesEquals returns true if the two `GpsDegrees` are identical.
func GpsDegreesEquals(gi1, gi2 GpsDegrees) bool {
if gi2.Orientation != gi1.Orientation {
return false
@ -220,3 +227,8 @@ func GpsDegreesEquals(gi1, gi2 GpsDegrees) bool {
return true
}
// IsTime returns true if the value is a `time.Time`.
func IsTime(v interface{}) bool {
return reflect.TypeOf(v) == timeType
}