mirror of https://github.com/perkeep/perkeep.git
Merge upstream goexif
This pulls the changes from the current HEAD of https://github.com/rwcarlsen/goexif (eb2943811adc24a1a40d6dc0525995d4f8563d08) Notable changes: - Removed explicit panics in favor of error returns - renamed TypeCategory to Format and made format calculated upon decoding rather than repeatedly for every format call - Merged contributions from Camlistore (exif.LatLong(), exif.DateTime() etc.) - Change String method to just return the string value - and don't have square brackets if only a single value - add separate Int and Int64 retrieval methods - Doc updates Minor changes in camlistore.org/pkg/* were neccessary to reflect changes in the API (handling of returned errors) and in names of exported fields and methods. Change-Id: I50412b5e68d2c9ca766ff2ad1a4ac26926baccab
This commit is contained in:
parent
2aed1b8241
commit
59a451c2dc
|
@ -357,12 +357,16 @@ func DecodeConfig(r io.Reader) (Config, error) {
|
|||
if err != nil {
|
||||
imageDebug(`No "Orientation" tag in EXIF.`)
|
||||
} else {
|
||||
orient := tag.Int(0)
|
||||
switch orient {
|
||||
// those are the orientations that require
|
||||
// a rotation of ±90
|
||||
case leftSideTop, rightSideTop, rightSideBottom, leftSideBottom:
|
||||
swapDimensions = true
|
||||
orient, err := tag.Int(0)
|
||||
if err == nil {
|
||||
switch orient {
|
||||
// those are the orientations that require
|
||||
// a rotation of ±90
|
||||
case leftSideTop, rightSideTop, rightSideBottom, leftSideBottom:
|
||||
swapDimensions = true
|
||||
}
|
||||
} else {
|
||||
imageDebug(fmt.Sprintf("EXIF Error: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -464,7 +468,11 @@ func exifOrientation(r io.Reader) (int, FlipDirection) {
|
|||
imageDebug(`No "Orientation" tag in EXIF; will not rotate or flip.`)
|
||||
return 0, 0
|
||||
}
|
||||
orient := tag.Int(0)
|
||||
orient, err := tag.Int(0)
|
||||
if err != nil {
|
||||
imageDebug(fmt.Sprintf("EXIF error: %v", err))
|
||||
return 0, 0
|
||||
}
|
||||
switch orient {
|
||||
case topLeftSide:
|
||||
// do nothing
|
||||
|
|
|
@ -434,7 +434,7 @@ func (ix *Index) populateFile(fetcher blob.Fetcher, b *schema.Blob, mm *mutation
|
|||
}
|
||||
|
||||
func tagFormatString(tag *tiff.Tag) string {
|
||||
switch tag.TypeCategory() {
|
||||
switch tag.Format() {
|
||||
case tiff.IntVal:
|
||||
return "int"
|
||||
case tiff.RatVal:
|
||||
|
@ -473,13 +473,17 @@ func indexEXIF(wholeRef blob.Ref, header []byte, mm *mutationMap) {
|
|||
}
|
||||
key := keyEXIFTag.Key(wholeRef, fmt.Sprintf("%04x", tag.Id))
|
||||
numComp := int(tag.Count)
|
||||
if tag.TypeCategory() == tiff.StringVal {
|
||||
if tag.Format() == tiff.StringVal {
|
||||
numComp = 1
|
||||
}
|
||||
var val bytes.Buffer
|
||||
val.WriteString(keyEXIFTag.Val(tagFmt, numComp, ""))
|
||||
if tag.TypeCategory() == tiff.StringVal {
|
||||
str := tag.StringVal()
|
||||
if tag.Format() == tiff.StringVal {
|
||||
str, err := tag.StringVal()
|
||||
if err != nil {
|
||||
log.Printf("Invalid EXIF string data: %v", err)
|
||||
return nil
|
||||
}
|
||||
if containsUnsafeRawStrByte(str) {
|
||||
val.WriteString(urle(str))
|
||||
} else {
|
||||
|
@ -492,12 +496,26 @@ func indexEXIF(wholeRef blob.Ref, header []byte, mm *mutationMap) {
|
|||
}
|
||||
switch tagFmt {
|
||||
case "int":
|
||||
fmt.Fprintf(&val, "%d", tag.Int(i))
|
||||
v, err := tag.Int(i)
|
||||
if err != nil {
|
||||
log.Printf("Invalid EXIF int data: %v", err)
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(&val, "%d", v)
|
||||
case "rat":
|
||||
n, d := tag.Rat2(i)
|
||||
n, d, err := tag.Rat2(i)
|
||||
if err != nil {
|
||||
log.Printf("Invalid EXIF rat data: %v", err)
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(&val, "%d/%d", n, d)
|
||||
case "float":
|
||||
fmt.Fprintf(&val, "%v", tag.Float(i))
|
||||
v, err := tag.Float(i)
|
||||
if err != nil {
|
||||
log.Printf("Invalid EXIF float data: %v", err)
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(&val, "%v", v)
|
||||
default:
|
||||
panic("shouldn't get here")
|
||||
}
|
||||
|
@ -508,8 +526,10 @@ func indexEXIF(wholeRef blob.Ref, header []byte, mm *mutationMap) {
|
|||
return nil
|
||||
}))
|
||||
|
||||
if lat, long, ok := ex.LatLong(); ok {
|
||||
if lat, long, err := ex.LatLong(); err == nil {
|
||||
mm.Set(keyEXIFGPS.Key(wholeRef), keyEXIFGPS.Val(fmt.Sprint(lat), fmt.Sprint(long)))
|
||||
} else if !exif.IsTagNotPresentError(err) {
|
||||
log.Printf("Invalid EXIF GPS data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -954,12 +954,14 @@ func FileTime(f io.ReaderAt) (time.Time, error) {
|
|||
// If the EXIF file only had local timezone, but it did have
|
||||
// GPS, then lookup the timezone and correct the time.
|
||||
if ct.Location() == time.Local {
|
||||
if lat, long, ok := ex.LatLong(); ok {
|
||||
if lat, long, err := ex.LatLong(); err == nil {
|
||||
if loc := lookupLocation(latlong.LookupZoneName(lat, long)); loc != nil {
|
||||
if t, err := exifDateTimeInLocation(ex, loc); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
} else if !exif.IsTagNotPresentError(err) {
|
||||
log.Printf("Invalid EXIF GPS data: %v", err)
|
||||
}
|
||||
}
|
||||
return ct, nil
|
||||
|
@ -977,7 +979,7 @@ func exifDateTimeInLocation(x *exif.Exif, loc *time.Location) (time.Time, error)
|
|||
return time.Time{}, err
|
||||
}
|
||||
}
|
||||
if tag.TypeCategory() != tiff.StringVal {
|
||||
if tag.Format() != tiff.StringVal {
|
||||
return time.Time{}, errors.New("DateTime[Original] not in string format")
|
||||
}
|
||||
const exifTimeLayout = "2006:01:02 15:04:05"
|
||||
|
|
|
@ -26,14 +26,15 @@ Example usage:
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"log"
|
||||
"fmt"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
"github.com/rwcarlsen/goexif/mknote"
|
||||
)
|
||||
|
||||
func main() {
|
||||
func ExampleDecode() {
|
||||
fname := "sample1.jpg"
|
||||
|
||||
f, err := os.Open(fname)
|
||||
|
@ -41,20 +42,28 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Optionally register camera makenote data parsing - currently Nikon and
|
||||
// Canon are supported.
|
||||
exif.RegisterParsers(mknote.All...)
|
||||
|
||||
x, err := exif.Decode(f)
|
||||
defer f.Close()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
camModel, _ := x.Get(exif.Model)
|
||||
date, _ := x.Get(exif.DateTimeOriginal)
|
||||
camModel, _ := x.Get(exif.Model) // normally, don't ignore errors!
|
||||
fmt.Println(camModel.StringVal())
|
||||
fmt.Println(date.StringVal())
|
||||
|
||||
focal, _ := x.Get(exif.FocalLength)
|
||||
numer, denom := focal.Rat2(0) // retrieve first (only) rat. value
|
||||
numer, denom, _ := focal.Rat2(0) // retrieve first (only) rat. value
|
||||
fmt.Printf("%v/%v", numer, denom)
|
||||
|
||||
// Two convenience functions exist for date/time taken and GPS coords:
|
||||
tm, _ := x.DateTime()
|
||||
fmt.Println("Taken: ", tm)
|
||||
|
||||
lat, long, _ := x.LatLong()
|
||||
fmt.Println("lat, long: ", lat, ", ", long)
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
To regenerate the regression test data, run `go generate` inside the exif
|
||||
package directory and commit the changes to *regress_expected_test.go*.
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
|
||||
"camlistore.org/third_party/github.com/rwcarlsen/goexif/exif"
|
||||
"camlistore.org/third_party/github.com/rwcarlsen/goexif/mknote"
|
||||
)
|
||||
|
||||
func ExampleDecode() {
|
||||
|
@ -16,17 +17,26 @@ func ExampleDecode() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Optionally register camera makenote data parsing - currently Nikon and
|
||||
// Canon are supported.
|
||||
exif.RegisterParsers(mknote.All...)
|
||||
|
||||
x, err := exif.Decode(f)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
camModel, _ := x.Get(exif.Model)
|
||||
date, _ := x.Get(exif.DateTimeOriginal)
|
||||
camModel, _ := x.Get(exif.Model) // normally, don't ignore errors!
|
||||
fmt.Println(camModel.StringVal())
|
||||
fmt.Println(date.StringVal())
|
||||
|
||||
focal, _ := x.Get(exif.FocalLength)
|
||||
numer, denom := focal.Rat2(0) // retrieve first (only) rat. value
|
||||
numer, denom, _ := focal.Rat2(0) // retrieve first (only) rat. value
|
||||
fmt.Printf("%v/%v", numer, denom)
|
||||
|
||||
// Two convenience functions exist for date/time taken and GPS coords:
|
||||
tm, _ := x.DateTime()
|
||||
fmt.Println("Taken: ", tm)
|
||||
|
||||
lat, long, _ := x.LatLong()
|
||||
fmt.Println("lat, long: ", lat, ", ", long)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -33,6 +35,11 @@ func (tag TagNotPresentError) Error() string {
|
|||
return fmt.Sprintf("exif: tag %q is not present", string(tag))
|
||||
}
|
||||
|
||||
func IsTagNotPresentError(err error) bool {
|
||||
_, ok := err.(TagNotPresentError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Parser allows the registration of custom parsing and field loading
|
||||
// in the Decode function.
|
||||
type Parser interface {
|
||||
|
@ -81,7 +88,10 @@ func loadSubDir(x *Exif, ptr FieldName, fieldMap map[uint16]FieldName) error {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
offset := tag.Int(0)
|
||||
offset, err := tag.Int64(0)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = r.Seek(offset, 0)
|
||||
if err != nil {
|
||||
|
@ -256,7 +266,7 @@ func (x *Exif) DateTime() (time.Time, error) {
|
|||
return dt, err
|
||||
}
|
||||
}
|
||||
if tag.TypeCategory() != tiff.StringVal {
|
||||
if tag.Format() != tiff.StringVal {
|
||||
return dt, errors.New("DateTime[Original] not in string format")
|
||||
}
|
||||
exifTimeLayout := "2006:01:02 15:04:05"
|
||||
|
@ -270,13 +280,103 @@ func ratFloat(num, dem int64) float64 {
|
|||
return float64(num) / float64(dem)
|
||||
}
|
||||
|
||||
func tagDegrees(tag *tiff.Tag) float64 {
|
||||
return ratFloat(tag.Rat2(0)) + ratFloat(tag.Rat2(1))/60 + ratFloat(tag.Rat2(2))/3600
|
||||
// Tries to parse a Geo degrees value from a string as it was found in some
|
||||
// EXIF data.
|
||||
// Supported formats so far:
|
||||
// - "52,00000,50,00000,34,01180" ==> 52 deg 50'34.0118"
|
||||
// Probably due to locale the comma is used as decimal mark as well as the
|
||||
// separator of three floats (degrees, minutes, seconds)
|
||||
// http://en.wikipedia.org/wiki/Decimal_mark#Hindu.E2.80.93Arabic_numeral_system
|
||||
// - "52.0,50.0,34.01180" ==> 52deg50'34.0118"
|
||||
// - "52,50,34.01180" ==> 52deg50'34.0118"
|
||||
func parseTagDegreesString(s string) (float64, error) {
|
||||
const unparsableErrorFmt = "Unknown coordinate format: %s"
|
||||
isSplitRune := func(c rune) bool {
|
||||
return c == ',' || c == ';'
|
||||
}
|
||||
parts := strings.FieldsFunc(s, isSplitRune)
|
||||
var degrees, minutes, seconds float64
|
||||
var err error
|
||||
switch len(parts) {
|
||||
case 6:
|
||||
degrees, err = strconv.ParseFloat(parts[0]+"."+parts[1], 64)
|
||||
if err != nil {
|
||||
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||
}
|
||||
minutes, err = strconv.ParseFloat(parts[2]+"."+parts[3], 64)
|
||||
if err != nil {
|
||||
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||
}
|
||||
minutes = math.Copysign(minutes, degrees)
|
||||
seconds, err = strconv.ParseFloat(parts[4]+"."+parts[5], 64)
|
||||
if err != nil {
|
||||
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||
}
|
||||
seconds = math.Copysign(seconds, degrees)
|
||||
case 3:
|
||||
degrees, err = strconv.ParseFloat(parts[0], 64)
|
||||
if err != nil {
|
||||
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||
}
|
||||
minutes, err = strconv.ParseFloat(parts[1], 64)
|
||||
if err != nil {
|
||||
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||
}
|
||||
minutes = math.Copysign(minutes, degrees)
|
||||
seconds, err = strconv.ParseFloat(parts[2], 64)
|
||||
if err != nil {
|
||||
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||
}
|
||||
seconds = math.Copysign(seconds, degrees)
|
||||
default:
|
||||
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||
}
|
||||
return degrees + minutes/60.0 + seconds/3600.0, nil
|
||||
}
|
||||
|
||||
func parse3Rat2(tag *tiff.Tag) ([3]float64, error) {
|
||||
v := [3]float64{}
|
||||
for i := range v {
|
||||
num, den, err := tag.Rat2(i)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
v[i] = ratFloat(num, den)
|
||||
if tag.Count < uint32(i+2) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func tagDegrees(tag *tiff.Tag) (float64, error) {
|
||||
switch tag.Format() {
|
||||
case tiff.RatVal:
|
||||
// The usual case, according to the Exif spec
|
||||
// (http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf,
|
||||
// sec 4.6.6, p. 52 et seq.)
|
||||
v, err := parse3Rat2(tag)
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
return v[0] + v[1]/60 + v[2]/3600.0, nil
|
||||
case tiff.StringVal:
|
||||
// Encountered this weird case with a panorama picture taken with a HTC phone
|
||||
s, err := tag.StringVal()
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
return parseTagDegreesString(s)
|
||||
default:
|
||||
// don't know how to parse value, give up
|
||||
return 0.0, fmt.Errorf("Malformed EXIF Tag Degrees")
|
||||
}
|
||||
}
|
||||
|
||||
// LatLong returns the latitude and longitude of the photo and
|
||||
// whether it was present.
|
||||
func (x *Exif) LatLong() (lat, long float64, ok bool) {
|
||||
func (x *Exif) LatLong() (lat, long float64, err error) {
|
||||
// All calls of x.Get might return an TagNotPresentError
|
||||
longTag, err := x.Get(FieldName("GPSLongitude"))
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -293,15 +393,25 @@ func (x *Exif) LatLong() (lat, long float64, ok bool) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
long = tagDegrees(longTag)
|
||||
lat = tagDegrees(latTag)
|
||||
if ewTag.StringVal() == "W" {
|
||||
if long, err = tagDegrees(longTag); err != nil {
|
||||
return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
|
||||
}
|
||||
if lat, err = tagDegrees(latTag); err != nil {
|
||||
return 0, 0, fmt.Errorf("Cannot parse latitude: %v", err)
|
||||
}
|
||||
ew, err := ewTag.StringVal()
|
||||
if err == nil && ew == "W" {
|
||||
long *= -1.0
|
||||
} else if err != nil {
|
||||
return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
|
||||
}
|
||||
if nsTag.StringVal() == "S" {
|
||||
ns, err := nsTag.StringVal()
|
||||
if err == nil && ns == "S" {
|
||||
lat *= -1.0
|
||||
} else if err != nil {
|
||||
return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
|
||||
}
|
||||
return lat, long, true
|
||||
return lat, long, nil
|
||||
}
|
||||
|
||||
// String returns a pretty text representation of the decoded exif data.
|
||||
|
@ -320,11 +430,21 @@ func (x *Exif) JpegThumbnail() ([]byte, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start, err := offset.Int(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
length, err := x.Get(ThumbJPEGInterchangeFormatLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x.Raw[offset.Int(0) : offset.Int(0)+length.Int(0)], nil
|
||||
l, err := length.Int(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return x.Raw[start : start+l], nil
|
||||
}
|
||||
|
||||
// MarshalJson implements the encoding/json.Marshaler interface providing output of
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package exif
|
||||
|
||||
//go:generate go run regen_regress.go -- regress_expected_test.go
|
||||
//go:generate go fmt regress_expected_test.go
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -12,77 +15,8 @@ import (
|
|||
"camlistore.org/third_party/github.com/rwcarlsen/goexif/tiff"
|
||||
)
|
||||
|
||||
// switch to true to regenerate regression expected values
|
||||
var regenRegress = false
|
||||
|
||||
var dataDir = flag.String("test_data_dir", ".", "Directory where the data files for testing are located")
|
||||
|
||||
// TestRegenRegress regenerates the expected image exif fields/values for
|
||||
// sample images.
|
||||
func TestRegenRegress(t *testing.T) {
|
||||
if !regenRegress {
|
||||
return
|
||||
}
|
||||
|
||||
dst, err := os.Create("regress_expected_test.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
dir, err := os.Open(".")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
names, err := dir.Readdirnames(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, name := range names {
|
||||
names[i] = filepath.Join(".", name)
|
||||
}
|
||||
makeExpected(names, dst)
|
||||
}
|
||||
|
||||
func makeExpected(files []string, w io.Writer) {
|
||||
fmt.Fprintf(w, "package exif\n\n")
|
||||
fmt.Fprintf(w, "var regressExpected = map[string]map[FieldName]string{\n")
|
||||
|
||||
for _, name := range files {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
x, err := Decode(f)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\t\"%v\": map[FieldName]string{\n", filepath.Base(name))
|
||||
x.Walk(®resswalk{w})
|
||||
fmt.Fprintf(w, "\t},\n")
|
||||
f.Close()
|
||||
}
|
||||
fmt.Fprintf(w, "}\n")
|
||||
}
|
||||
|
||||
type regresswalk struct {
|
||||
wr io.Writer
|
||||
}
|
||||
|
||||
func (w *regresswalk) Walk(name FieldName, tag *tiff.Tag) error {
|
||||
if strings.HasPrefix(string(name), UnknownPrefix) {
|
||||
fmt.Fprintf(w.wr, "\t\t\"%v\": `%v`,\n", name, tag.String())
|
||||
} else {
|
||||
fmt.Fprintf(w.wr, "\t\t%v: `%v`,\n", name, tag.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
fpath := filepath.Join(*dataDir, "")
|
||||
f, err := os.Open(fpath)
|
||||
|
@ -114,6 +48,7 @@ func TestDecode(t *testing.T) {
|
|||
t.Fatalf("No error and yet %v was not decoded", name)
|
||||
}
|
||||
|
||||
t.Logf("checking pic %v", name)
|
||||
x.Walk(&walker{name, t})
|
||||
cnt++
|
||||
}
|
||||
|
@ -129,8 +64,28 @@ type walker struct {
|
|||
|
||||
func (w *walker) Walk(field FieldName, tag *tiff.Tag) error {
|
||||
// this needs to be commented out when regenerating regress expected vals
|
||||
if v := regressExpected[w.picName][field]; v != tag.String() {
|
||||
w.t.Errorf("pic %v: expected '%v' got '%v'", w.picName, v, tag.String())
|
||||
pic := regressExpected[w.picName]
|
||||
if pic == nil {
|
||||
w.t.Errorf(" regression data not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
exp, ok := pic[field]
|
||||
if !ok {
|
||||
w.t.Errorf(" regression data does not have field %v", field)
|
||||
return nil
|
||||
}
|
||||
|
||||
s := tag.String()
|
||||
if tag.Count == 1 && s != "\"\"" {
|
||||
s = fmt.Sprintf("[%s]", s)
|
||||
}
|
||||
got := tag.String()
|
||||
|
||||
if exp != got {
|
||||
fmt.Println("s: ", s)
|
||||
fmt.Printf("len(s)=%v\n", len(s))
|
||||
w.t.Errorf(" field %v bad tag: expected '%s', got '%s'", field, exp, got)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -158,3 +113,36 @@ func TestMarshal(t *testing.T) {
|
|||
|
||||
t.Logf("%s", b)
|
||||
}
|
||||
|
||||
func testSingleParseDegreesString(t *testing.T, s string, w float64) {
|
||||
g, err := parseTagDegreesString(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if math.Abs(w-g) > 1e-10 {
|
||||
t.Errorf("Wrong parsing result %s: Want %.12f, got %.12f", s, w, g)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTagDegreesString(t *testing.T) {
|
||||
// semicolon as decimal mark
|
||||
testSingleParseDegreesString(t, "52,00000,50,00000,34,01180", 52.842781055556) // comma as separator
|
||||
testSingleParseDegreesString(t, "52,00000;50,00000;34,01180", 52.842781055556) // semicolon as separator
|
||||
|
||||
// point as decimal mark
|
||||
testSingleParseDegreesString(t, "14.00000,44.00000,34.01180", 14.742781055556) // comma as separator
|
||||
testSingleParseDegreesString(t, "14.00000;44.00000;34.01180", 14.742781055556) // semicolon as separator
|
||||
testSingleParseDegreesString(t, "14.00000;44.00000,34.01180", 14.742781055556) // mixed separators
|
||||
|
||||
testSingleParseDegreesString(t, "-008.0,30.0,03.6", -8.501) // leading zeros
|
||||
|
||||
// no decimal places
|
||||
testSingleParseDegreesString(t, "-10,15,54", -10.265)
|
||||
testSingleParseDegreesString(t, "-10;15;54", -10.265)
|
||||
|
||||
// incorrect mix of comma and point as decimal mark
|
||||
s := "-17,00000,15.00000,04.80000"
|
||||
if _, err := parseTagDegreesString(s); err == nil {
|
||||
t.Error("parseTagDegreesString: false positive for " + s)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
"github.com/rwcarlsen/goexif/tiff"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
fname := flag.Arg(0)
|
||||
|
||||
dst, err := os.Create(fname)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
dir, err := os.Open("")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
names, err := dir.Readdirnames(0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for i, name := range names {
|
||||
names[i] = filepath.Join(".", name)
|
||||
}
|
||||
makeExpected(names, dst)
|
||||
}
|
||||
|
||||
func makeExpected(files []string, w io.Writer) {
|
||||
fmt.Fprintf(w, "package exif\n\n")
|
||||
fmt.Fprintf(w, "var regressExpected = map[string]map[FieldName]string{\n")
|
||||
|
||||
for _, name := range files {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
x, err := exif.Decode(f)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\"%v\": map[FieldName]string{\n", filepath.Base(name))
|
||||
x.Walk(®resswalk{w})
|
||||
fmt.Fprintf(w, "},\n")
|
||||
f.Close()
|
||||
}
|
||||
fmt.Fprintf(w, "}")
|
||||
}
|
||||
|
||||
type regresswalk struct {
|
||||
wr io.Writer
|
||||
}
|
||||
|
||||
func (w *regresswalk) Walk(name exif.FieldName, tag *tiff.Tag) error {
|
||||
if strings.HasPrefix(string(name), exif.UnknownPrefix) {
|
||||
fmt.Fprintf(w.wr, "\"%v\": `%v`,\n", name, tag.String())
|
||||
} else {
|
||||
fmt.Fprintf(w.wr, "%v: `%v`,\n", name, tag.String())
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -2,61 +2,61 @@ package exif
|
|||
|
||||
var regressExpected = map[string]map[FieldName]string{
|
||||
"sample1.jpg": map[FieldName]string{
|
||||
ExifIFDPointer: `{Id: 8769, Val: [216]}`,
|
||||
YResolution: `{Id: 11B, Val: ["256/1"]}`,
|
||||
PixelXDimension: `{Id: A002, Val: [500]}`,
|
||||
FocalLengthIn35mmFilm: `{Id: A405, Val: [35]}`,
|
||||
GPSLatitudeRef: `{Id: 1, Val: "N"}`,
|
||||
FNumber: `{Id: 829D, Val: ["45/10"]}`,
|
||||
GPSTimeStamp: `{Id: 7, Val: ["18/1","7/1","37/1"]}`,
|
||||
SubSecTime: `{Id: 9290, Val: "63"}`,
|
||||
ExifVersion: `{Id: 9000, Val: "0220"}`,
|
||||
PixelYDimension: `{Id: A003, Val: [375]}`,
|
||||
ExposureMode: `{Id: A402, Val: [0]}`,
|
||||
Saturation: `{Id: A409, Val: [0]}`,
|
||||
GPSInfoIFDPointer: `{Id: 8825, Val: [820]}`,
|
||||
ExposureBiasValue: `{Id: 9204, Val: ["0/6"]}`,
|
||||
MeteringMode: `{Id: 9207, Val: [3]}`,
|
||||
Flash: `{Id: 9209, Val: [0]}`,
|
||||
SubSecTimeOriginal: `{Id: 9291, Val: "63"}`,
|
||||
FileSource: `{Id: A300, Val: ""}`,
|
||||
GainControl: `{Id: A407, Val: [1]}`,
|
||||
SubjectDistanceRange: `{Id: A40C, Val: [0]}`,
|
||||
ThumbJPEGInterchangeFormatLength: `{Id: 202, Val: [4034]}`,
|
||||
FlashpixVersion: `{Id: A000, Val: "0100"}`,
|
||||
UserComment: `{Id: 9286, Val: "taken at basilica of chinese"}`,
|
||||
CustomRendered: `{Id: A401, Val: [0]}`,
|
||||
GPSVersionID: `{Id: 0, Val: [2,2,0,0]}`,
|
||||
Orientation: `{Id: 112, Val: [1]}`,
|
||||
DateTimeDigitized: `{Id: 9004, Val: "2003:11:23 18:07:37"}`,
|
||||
RelatedSoundFile: `{Id: A004, Val: " "}`,
|
||||
DigitalZoomRatio: `{Id: A404, Val: ["1/1"]}`,
|
||||
Sharpness: `{Id: A40A, Val: [0]}`,
|
||||
Model: `{Id: 110, Val: "NIKON D2H"}`,
|
||||
CompressedBitsPerPixel: `{Id: 9102, Val: ["4/1"]}`,
|
||||
FocalLength: `{Id: 920A, Val: ["2333/100"]}`,
|
||||
SceneType: `{Id: A301, Val: ""}`,
|
||||
DateTime: `{Id: 132, Val: "2005:07:02 10:38:28"}`,
|
||||
ThumbJPEGInterchangeFormat: `{Id: 201, Val: [1088]}`,
|
||||
Contrast: `{Id: A408, Val: [1]}`,
|
||||
GPSLongitude: `{Id: 4, Val: ["116/1","23/1","27/1"]}`,
|
||||
ExposureProgram: `{Id: 8822, Val: [3]}`,
|
||||
XResolution: `{Id: 11A, Val: ["256/1"]}`,
|
||||
SensingMethod: `{Id: A217, Val: [2]}`,
|
||||
GPSLatitude: `{Id: 2, Val: ["39/1","54/1","56/1"]}`,
|
||||
Make: `{Id: 10F, Val: "NIKON CORPORATION"}`,
|
||||
ColorSpace: `{Id: A001, Val: [65535]}`,
|
||||
Software: `{Id: 131, Val: "Opanda PowerExif"}`,
|
||||
DateTimeOriginal: `{Id: 9003, Val: "2003:11:23 18:07:37"}`,
|
||||
MaxApertureValue: `{Id: 9205, Val: ["3/1"]}`,
|
||||
LightSource: `{Id: 9208, Val: [0]}`,
|
||||
SceneCaptureType: `{Id: A406, Val: [0]}`,
|
||||
GPSLongitudeRef: `{Id: 3, Val: "E"}`,
|
||||
ResolutionUnit: `{Id: 128, Val: [2]}`,
|
||||
SubSecTimeDigitized: `{Id: 9292, Val: "63"}`,
|
||||
CFAPattern: `{Id: A302, Val: ""}`,
|
||||
WhiteBalance: `{Id: A403, Val: [0]}`,
|
||||
GPSDateStamp: `{Id: 1D, Val: "2003:11:23"}`,
|
||||
ExposureTime: `{Id: 829A, Val: ["1/125"]}`,
|
||||
Saturation: `0`,
|
||||
MaxApertureValue: `"3/1"`,
|
||||
LightSource: `0`,
|
||||
FocalLength: `"2333/100"`,
|
||||
SubSecTimeDigitized: `"63"`,
|
||||
ColorSpace: `65535`,
|
||||
DateTimeOriginal: `"2003:11:23 18:07:37"`,
|
||||
SceneType: `""`,
|
||||
CFAPattern: `""`,
|
||||
DigitalZoomRatio: `"1/1"`,
|
||||
GainControl: `1`,
|
||||
Contrast: `1`,
|
||||
FNumber: `"45/10"`,
|
||||
GPSVersionID: `[2,2,0,0]`,
|
||||
GPSLatitudeRef: `"N"`,
|
||||
Orientation: `1`,
|
||||
ResolutionUnit: `2`,
|
||||
ExposureTime: `"1/125"`,
|
||||
MeteringMode: `3`,
|
||||
GPSLongitudeRef: `"E"`,
|
||||
GPSTimeStamp: `["18/1","7/1","37/1"]`,
|
||||
YResolution: `"256/1"`,
|
||||
ThumbJPEGInterchangeFormatLength: `4034`,
|
||||
SubSecTime: `"63"`,
|
||||
CustomRendered: `0`,
|
||||
ExposureMode: `0`,
|
||||
DateTime: `"2005:07:02 10:38:28"`,
|
||||
ExposureProgram: `3`,
|
||||
SubSecTimeOriginal: `"63"`,
|
||||
GPSLongitude: `["116/1","23/1","27/1"]`,
|
||||
XResolution: `"256/1"`,
|
||||
ExifVersion: `"0220"`,
|
||||
CompressedBitsPerPixel: `"4/1"`,
|
||||
Flash: `0`,
|
||||
FocalLengthIn35mmFilm: `35`,
|
||||
Model: `"NIKON D2H"`,
|
||||
ThumbJPEGInterchangeFormat: `1088`,
|
||||
SensingMethod: `2`,
|
||||
SceneCaptureType: `0`,
|
||||
Software: `"Opanda PowerExif"`,
|
||||
Sharpness: `0`,
|
||||
ExifIFDPointer: `216`,
|
||||
GPSInfoIFDPointer: `820`,
|
||||
RelatedSoundFile: `" "`,
|
||||
SubjectDistanceRange: `0`,
|
||||
ExposureBiasValue: `"0/6"`,
|
||||
FlashpixVersion: `"0100"`,
|
||||
PixelYDimension: `375`,
|
||||
FileSource: `""`,
|
||||
WhiteBalance: `0`,
|
||||
GPSDateStamp: `"2003:11:23"`,
|
||||
Make: `"NIKON CORPORATION"`,
|
||||
DateTimeDigitized: `"2003:11:23 18:07:37"`,
|
||||
UserComment: `"taken at basilica of chinese"`,
|
||||
PixelXDimension: `500`,
|
||||
GPSLatitude: `["39/1","54/1","56/1"]`,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ func (_ *canon) Parse(x *exif.Exif) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if mk.StringVal() != "Canon" {
|
||||
if val, err := mk.StringVal(); err != nil || val != "Canon" {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -12,12 +12,12 @@ import (
|
|||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// TypeCategory specifies the Go type equivalent used to represent the basic
|
||||
// Format specifies the Go type equivalent used to represent the basic
|
||||
// tiff data types.
|
||||
type TypeCategory int
|
||||
type Format int
|
||||
|
||||
const (
|
||||
IntVal TypeCategory = iota
|
||||
IntVal Format = iota
|
||||
FloatVal
|
||||
RatVal
|
||||
StringVal
|
||||
|
@ -25,6 +25,15 @@ const (
|
|||
OtherVal
|
||||
)
|
||||
|
||||
var formatNames = map[Format]string{
|
||||
IntVal: "int",
|
||||
FloatVal: "float",
|
||||
RatVal: "rational",
|
||||
StringVal: "string",
|
||||
UndefVal: "undefined",
|
||||
OtherVal: "other",
|
||||
}
|
||||
|
||||
// DataType represents the basic tiff tag data types.
|
||||
type DataType uint16
|
||||
|
||||
|
@ -43,6 +52,21 @@ const (
|
|||
DTDouble = 12
|
||||
)
|
||||
|
||||
var typeNames = map[DataType]string{
|
||||
DTByte: "byte",
|
||||
DTAscii: "ascii",
|
||||
DTShort: "short",
|
||||
DTLong: "long",
|
||||
DTRational: "rational",
|
||||
DTSByte: "signed byte",
|
||||
DTUndefined: "undefined",
|
||||
DTSShort: "signed short",
|
||||
DTSLong: "signed long",
|
||||
DTSRational: "signed rational",
|
||||
DTFloat: "float",
|
||||
DTDouble: "double",
|
||||
}
|
||||
|
||||
// typeSize specifies the size in bytes of each type.
|
||||
var typeSize = map[DataType]uint32{
|
||||
DTByte: 1,
|
||||
|
@ -75,12 +99,12 @@ type Tag struct {
|
|||
// field.
|
||||
ValOffset uint32
|
||||
|
||||
order binary.ByteOrder
|
||||
|
||||
order binary.ByteOrder
|
||||
intVals []int64
|
||||
floatVals []float64
|
||||
ratVals [][]int64
|
||||
strVal string
|
||||
format Format
|
||||
}
|
||||
|
||||
// DecodeTag parses a tiff-encoded IFD tag from r and returns a Tag object. The
|
||||
|
@ -127,12 +151,10 @@ func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) {
|
|||
t.Val = val
|
||||
}
|
||||
|
||||
t.convertVals()
|
||||
|
||||
return t, nil
|
||||
return t, t.convertVals()
|
||||
}
|
||||
|
||||
func (t *Tag) convertVals() {
|
||||
func (t *Tag) convertVals() error {
|
||||
r := bytes.NewReader(t.Val)
|
||||
|
||||
switch t.Type {
|
||||
|
@ -145,7 +167,9 @@ func (t *Tag) convertVals() {
|
|||
t.intVals = make([]int64, int(t.Count))
|
||||
for i := range t.intVals {
|
||||
err := binary.Read(r, t.order, &v)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.intVals[i] = int64(v)
|
||||
}
|
||||
case DTShort:
|
||||
|
@ -153,7 +177,9 @@ func (t *Tag) convertVals() {
|
|||
t.intVals = make([]int64, int(t.Count))
|
||||
for i := range t.intVals {
|
||||
err := binary.Read(r, t.order, &v)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.intVals[i] = int64(v)
|
||||
}
|
||||
case DTLong:
|
||||
|
@ -161,7 +187,9 @@ func (t *Tag) convertVals() {
|
|||
t.intVals = make([]int64, int(t.Count))
|
||||
for i := range t.intVals {
|
||||
err := binary.Read(r, t.order, &v)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.intVals[i] = int64(v)
|
||||
}
|
||||
case DTSByte:
|
||||
|
@ -169,7 +197,9 @@ func (t *Tag) convertVals() {
|
|||
t.intVals = make([]int64, int(t.Count))
|
||||
for i := range t.intVals {
|
||||
err := binary.Read(r, t.order, &v)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.intVals[i] = int64(v)
|
||||
}
|
||||
case DTSShort:
|
||||
|
@ -177,7 +207,9 @@ func (t *Tag) convertVals() {
|
|||
t.intVals = make([]int64, int(t.Count))
|
||||
for i := range t.intVals {
|
||||
err := binary.Read(r, t.order, &v)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.intVals[i] = int64(v)
|
||||
}
|
||||
case DTSLong:
|
||||
|
@ -185,7 +217,9 @@ func (t *Tag) convertVals() {
|
|||
t.intVals = make([]int64, int(t.Count))
|
||||
for i := range t.intVals {
|
||||
err := binary.Read(r, t.order, &v)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.intVals[i] = int64(v)
|
||||
}
|
||||
case DTRational:
|
||||
|
@ -193,9 +227,13 @@ func (t *Tag) convertVals() {
|
|||
for i := range t.ratVals {
|
||||
var n, d uint32
|
||||
err := binary.Read(r, t.order, &n)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, t.order, &d)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.ratVals[i] = []int64{int64(n), int64(d)}
|
||||
}
|
||||
case DTSRational:
|
||||
|
@ -203,9 +241,13 @@ func (t *Tag) convertVals() {
|
|||
for i := range t.ratVals {
|
||||
var n, d int32
|
||||
err := binary.Read(r, t.order, &n)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(r, t.order, &d)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.ratVals[i] = []int64{int64(n), int64(d)}
|
||||
}
|
||||
case DTFloat: // float32
|
||||
|
@ -213,7 +255,9 @@ func (t *Tag) convertVals() {
|
|||
for i := range t.floatVals {
|
||||
var v float32
|
||||
err := binary.Read(r, t.order, &v)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.floatVals[i] = float64(v)
|
||||
}
|
||||
case DTDouble:
|
||||
|
@ -221,103 +265,129 @@ func (t *Tag) convertVals() {
|
|||
for i := range t.floatVals {
|
||||
var u float64
|
||||
err := binary.Read(r, t.order, &u)
|
||||
panicOn(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.floatVals[i] = u
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TypeCategory returns a value indicating which method can be called to retrieve the
|
||||
// tag's value properly typed (e.g. integer, rational, etc.).
|
||||
func (t *Tag) TypeCategory() TypeCategory {
|
||||
switch t.Type {
|
||||
case DTByte, DTShort, DTLong, DTSByte, DTSShort, DTSLong:
|
||||
return IntVal
|
||||
t.format = IntVal
|
||||
case DTRational, DTSRational:
|
||||
return RatVal
|
||||
t.format = RatVal
|
||||
case DTFloat, DTDouble:
|
||||
return FloatVal
|
||||
t.format = FloatVal
|
||||
case DTAscii:
|
||||
return StringVal
|
||||
t.format = StringVal
|
||||
case DTUndefined:
|
||||
return UndefVal
|
||||
t.format = UndefVal
|
||||
default:
|
||||
t.format = OtherVal
|
||||
}
|
||||
return OtherVal
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rat returns the tag's i'th value as a rational number. It panics if the tag
|
||||
// TypeCategory is not RatVal, if the denominator is zero, or if the tag has no
|
||||
// i'th component. If a denominator could be zero, use Rat2.
|
||||
func (t *Tag) Rat(i int) *big.Rat {
|
||||
n, d := t.Rat2(i)
|
||||
return big.NewRat(n, d)
|
||||
// Format returns a value indicating which method can be called to retrieve the
|
||||
// tag's value properly typed (e.g. integer, rational, etc.).
|
||||
func (t *Tag) Format() Format { return t.format }
|
||||
|
||||
func (t *Tag) typeErr(to Format) error {
|
||||
return &wrongFmtErr{typeNames[t.Type], formatNames[to]}
|
||||
}
|
||||
|
||||
// Rat returns the tag's i'th value as a rational number. It returns a nil and
|
||||
// an error if this tag's Format is not RatVal. It panics for zero deminators
|
||||
// or if i is out of range.
|
||||
func (t *Tag) Rat(i int) (*big.Rat, error) {
|
||||
n, d, err := t.Rat2(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return big.NewRat(n, d), nil
|
||||
}
|
||||
|
||||
// Rat2 returns the tag's i'th value as a rational number represented by a
|
||||
// numerator-denominator pair. It panics if the tag TypeCategory is not RatVal
|
||||
// or if the tag value has no i'th component.
|
||||
func (t *Tag) Rat2(i int) (num, den int64) {
|
||||
if t.TypeCategory() != RatVal {
|
||||
panic("Tag type category is not 'rational'")
|
||||
// numerator-denominator pair. It returns an error if the tag's Format is not
|
||||
// RatVal. It panics if i is out of range.
|
||||
func (t *Tag) Rat2(i int) (num, den int64, err error) {
|
||||
if t.format != RatVal {
|
||||
return 0, 0, t.typeErr(RatVal)
|
||||
}
|
||||
return t.ratVals[i][0], t.ratVals[i][1]
|
||||
return t.ratVals[i][0], t.ratVals[i][1], nil
|
||||
}
|
||||
|
||||
// Int returns the tag's i'th value as an integer. It panics if the tag
|
||||
// TypeCategory is not IntVal or if the tag value has no i'th component.
|
||||
func (t *Tag) Int(i int) int64 {
|
||||
if t.TypeCategory() != IntVal {
|
||||
panic("Tag type category is not 'int'")
|
||||
// Int64 returns the tag's i'th value as an integer. It returns an error if the
|
||||
// tag's Format is not IntVal. It panics if i is out of range.
|
||||
func (t *Tag) Int64(i int) (int64, error) {
|
||||
if t.format != IntVal {
|
||||
return 0, t.typeErr(IntVal)
|
||||
}
|
||||
return t.intVals[i]
|
||||
return t.intVals[i], nil
|
||||
}
|
||||
|
||||
// Float returns the tag's i'th value as a float. It panics if the tag
|
||||
// TypeCategory is not FloatVal or if the tag value has no i'th component.
|
||||
func (t *Tag) Float(i int) float64 {
|
||||
if t.TypeCategory() != FloatVal {
|
||||
panic("Tag type category is not 'float'")
|
||||
// Int returns the tag's i'th value as an integer. It returns an error if the
|
||||
// tag's Format is not IntVal. It panics if i is out of range.
|
||||
func (t *Tag) Int(i int) (int, error) {
|
||||
if t.format != IntVal {
|
||||
return 0, t.typeErr(IntVal)
|
||||
}
|
||||
return t.floatVals[i]
|
||||
return int(t.intVals[i]), nil
|
||||
}
|
||||
|
||||
// StringVal returns the tag's value as a string. It panics if the tag
|
||||
// TypeCategory is not StringVal.
|
||||
func (t *Tag) StringVal() string {
|
||||
if t.TypeCategory() != StringVal {
|
||||
panic("Tag type category is not 'ascii string'")
|
||||
// Float returns the tag's i'th value as a float. It returns an error if the
|
||||
// tag's Format is not IntVal. It panics if i is out of range.
|
||||
func (t *Tag) Float(i int) (float64, error) {
|
||||
if t.format != FloatVal {
|
||||
return 0, t.typeErr(FloatVal)
|
||||
}
|
||||
return t.strVal
|
||||
return t.floatVals[i], nil
|
||||
}
|
||||
|
||||
// StringVal returns the tag's value as a string. It returns an error if the
|
||||
// tag's Format is not StringVal. It panics if i is out of range.
|
||||
func (t *Tag) StringVal() (string, error) {
|
||||
if t.format != StringVal {
|
||||
return "", t.typeErr(StringVal)
|
||||
}
|
||||
return t.strVal, nil
|
||||
}
|
||||
|
||||
// String returns a nicely formatted version of the tag.
|
||||
func (t *Tag) String() string {
|
||||
data, err := t.MarshalJSON()
|
||||
panicOn(err)
|
||||
val := string(data)
|
||||
return fmt.Sprintf("{Id: %X, Val: %v}", t.Id, val)
|
||||
if err != nil {
|
||||
return "ERROR: " + err.Error()
|
||||
}
|
||||
|
||||
if t.Count == 1 {
|
||||
return strings.Trim(fmt.Sprintf("%s", data), "[]")
|
||||
}
|
||||
return fmt.Sprintf("%s", data)
|
||||
}
|
||||
|
||||
func (t *Tag) MarshalJSON() ([]byte, error) {
|
||||
f := t.TypeCategory()
|
||||
|
||||
switch f {
|
||||
switch t.format {
|
||||
case StringVal, UndefVal:
|
||||
return nullString(t.Val), nil
|
||||
case OtherVal:
|
||||
panic(fmt.Sprintf("Unhandled tag type=%v", t.Type))
|
||||
return []byte(fmt.Sprintf("unknown tag type '%v'", t.Type)), nil
|
||||
}
|
||||
|
||||
rv := []string{}
|
||||
for i := 0; i < int(t.Count); i++ {
|
||||
switch f {
|
||||
switch t.format {
|
||||
case RatVal:
|
||||
n, d := t.Rat2(i)
|
||||
n, d, _ := t.Rat2(i)
|
||||
rv = append(rv, fmt.Sprintf(`"%v/%v"`, n, d))
|
||||
case FloatVal:
|
||||
rv = append(rv, fmt.Sprintf("%v", t.Float(i)))
|
||||
v, _ := t.Float(i)
|
||||
rv = append(rv, fmt.Sprintf("%v", v))
|
||||
case IntVal:
|
||||
rv = append(rv, fmt.Sprintf("%v", t.Int(i)))
|
||||
v, _ := t.Int(i)
|
||||
rv = append(rv, fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
return []byte(fmt.Sprintf(`[%s]`, strings.Join(rv, ","))), nil
|
||||
|
@ -339,8 +409,10 @@ func nullString(in []byte) []byte {
|
|||
return []byte(`""`)
|
||||
}
|
||||
|
||||
func panicOn(err error) {
|
||||
if err != nil {
|
||||
panic("unexpected error: " + err.Error())
|
||||
}
|
||||
type wrongFmtErr struct {
|
||||
From, To string
|
||||
}
|
||||
|
||||
func (e *wrongFmtErr) Error() string {
|
||||
return fmt.Sprintf("cannot convert tag type '%v' into '%v'", e.From, e.To)
|
||||
}
|
||||
|
|
|
@ -215,7 +215,10 @@ func TestDecodeTag_blob(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Logf("tag: %v+\n", tg)
|
||||
n, d := tg.Rat2(0)
|
||||
n, d, err := tg.Rat2(0)
|
||||
if err != nil {
|
||||
t.Fatalf("tag decoded wrong type: %v", err)
|
||||
}
|
||||
t.Logf("tag rat val: %v/%v\n", n, d)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue