mirror of https://github.com/dsoprea/go-exif.git
Timestamps can now be set directly
parent
d69a43ee6a
commit
7edf52b885
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue