From 59a451c2dc39a0309b04415683a3d8e8be7fe17b Mon Sep 17 00:00:00 2001 From: Fabian Wickborn Date: Thu, 28 Aug 2014 15:29:30 +0200 Subject: [PATCH] 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 --- pkg/images/images.go | 22 +- pkg/index/receive.go | 36 ++- pkg/schema/schema.go | 6 +- .../github.com/rwcarlsen/goexif/README.md | 29 ++- .../rwcarlsen/goexif/exif/README.md | 4 + .../rwcarlsen/goexif/exif/example_test.go | 18 +- .../github.com/rwcarlsen/goexif/exif/exif.go | 142 ++++++++++- .../rwcarlsen/goexif/exif/exif_test.go | 132 +++++------ .../rwcarlsen/goexif/exif/regen_regress.go | 79 ++++++ .../goexif/exif/regress_expected_test.go | 112 ++++----- .../rwcarlsen/goexif/mknote/mknote.go | 2 +- .../github.com/rwcarlsen/goexif/tiff/tag.go | 224 ++++++++++++------ .../rwcarlsen/goexif/tiff/tiff_test.go | 5 +- 13 files changed, 563 insertions(+), 248 deletions(-) create mode 100644 third_party/github.com/rwcarlsen/goexif/exif/README.md create mode 100644 third_party/github.com/rwcarlsen/goexif/exif/regen_regress.go diff --git a/pkg/images/images.go b/pkg/images/images.go index 99e66060b..c4a1fb413 100644 --- a/pkg/images/images.go +++ b/pkg/images/images.go @@ -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 diff --git a/pkg/index/receive.go b/pkg/index/receive.go index ab7b5aa80..a2abfa943 100644 --- a/pkg/index/receive.go +++ b/pkg/index/receive.go @@ -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) } } diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 020893e49..ab4a3ec0b 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -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" diff --git a/third_party/github.com/rwcarlsen/goexif/README.md b/third_party/github.com/rwcarlsen/goexif/README.md index 9c0d42ec8..c4451517f 100644 --- a/third_party/github.com/rwcarlsen/goexif/README.md +++ b/third_party/github.com/rwcarlsen/goexif/README.md @@ -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) } ``` diff --git a/third_party/github.com/rwcarlsen/goexif/exif/README.md b/third_party/github.com/rwcarlsen/goexif/exif/README.md new file mode 100644 index 000000000..b3bf5fa0e --- /dev/null +++ b/third_party/github.com/rwcarlsen/goexif/exif/README.md @@ -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*. + diff --git a/third_party/github.com/rwcarlsen/goexif/exif/example_test.go b/third_party/github.com/rwcarlsen/goexif/exif/example_test.go index ffd709f8f..61521fd40 100644 --- a/third_party/github.com/rwcarlsen/goexif/exif/example_test.go +++ b/third_party/github.com/rwcarlsen/goexif/exif/example_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) } diff --git a/third_party/github.com/rwcarlsen/goexif/exif/exif.go b/third_party/github.com/rwcarlsen/goexif/exif/exif.go index c7134c484..5df619393 100644 --- a/third_party/github.com/rwcarlsen/goexif/exif/exif.go +++ b/third_party/github.com/rwcarlsen/goexif/exif/exif.go @@ -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 diff --git a/third_party/github.com/rwcarlsen/goexif/exif/exif_test.go b/third_party/github.com/rwcarlsen/goexif/exif/exif_test.go index 4fce07f9d..058f046cf 100644 --- a/third_party/github.com/rwcarlsen/goexif/exif/exif_test.go +++ b/third_party/github.com/rwcarlsen/goexif/exif/exif_test.go @@ -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) + } +} diff --git a/third_party/github.com/rwcarlsen/goexif/exif/regen_regress.go b/third_party/github.com/rwcarlsen/goexif/exif/regen_regress.go new file mode 100644 index 000000000..34f5ea637 --- /dev/null +++ b/third_party/github.com/rwcarlsen/goexif/exif/regen_regress.go @@ -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 +} diff --git a/third_party/github.com/rwcarlsen/goexif/exif/regress_expected_test.go b/third_party/github.com/rwcarlsen/goexif/exif/regress_expected_test.go index 98088c1f3..07f46971b 100644 --- a/third_party/github.com/rwcarlsen/goexif/exif/regress_expected_test.go +++ b/third_party/github.com/rwcarlsen/goexif/exif/regress_expected_test.go @@ -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"]`, }, } diff --git a/third_party/github.com/rwcarlsen/goexif/mknote/mknote.go b/third_party/github.com/rwcarlsen/goexif/mknote/mknote.go index de5ecd053..ab7d69653 100644 --- a/third_party/github.com/rwcarlsen/goexif/mknote/mknote.go +++ b/third_party/github.com/rwcarlsen/goexif/mknote/mknote.go @@ -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 } diff --git a/third_party/github.com/rwcarlsen/goexif/tiff/tag.go b/third_party/github.com/rwcarlsen/goexif/tiff/tag.go index 0b6ddde6f..3ac620ec4 100644 --- a/third_party/github.com/rwcarlsen/goexif/tiff/tag.go +++ b/third_party/github.com/rwcarlsen/goexif/tiff/tag.go @@ -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) } diff --git a/third_party/github.com/rwcarlsen/goexif/tiff/tiff_test.go b/third_party/github.com/rwcarlsen/goexif/tiff/tiff_test.go index 45c975ff1..5db348dc8 100644 --- a/third_party/github.com/rwcarlsen/goexif/tiff/tiff_test.go +++ b/third_party/github.com/rwcarlsen/goexif/tiff/tiff_test.go @@ -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) }