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 (
|
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())
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue