From f0d9c04bc27677a6f72e6a634f380d26739a273b Mon Sep 17 00:00:00 2001 From: Fabian Wickborn Date: Wed, 3 Sep 2014 22:47:24 +0200 Subject: [PATCH] Merge goexif with upstream package This pulls the changes from the current HEAD of https://github.com/rwcarlsen/goexif (rev cf045e9d6ba052fd348f82394d364cca4937589a) Changes to goexif from upstream: - Add support for reading Nikon and Canon maker notes - Adds parser registration to exif package - Renamed cmd to exifstat - Renamed exported fields and methods in goexif/tiff - adds support for bare tiff images. bug fix and minor cosmetics - support pulling the thumbnail - adds thumbnail support to exif pkg - tiff defines DataType and constants for the datatype values - Update covnertVals and TypeCategory to use new constants for DataType - Renamed test data dir in exif tests - created type+constants for raw tiff tag data types Not merged from upstream: - ~1 MB of test JPGs in goexif/exif/samples Minor changes in camlistore.org/pkg/* were neccessary to reflect the name changes in the exported fields and methods. Change-Id: I0fdcad2d7b5e01e0d4160a5eb52b8ec750d353cf --- pkg/index/receive.go | 10 +- pkg/schema/schema.go | 2 +- .../camlistore/goexif/README.camlistore | 124 +++--- .../github.com/camlistore/goexif/README.md | 44 ++- .../github.com/camlistore/goexif/cmd/main.go | 36 -- .../camlistore/goexif/exif/example_test.go | 6 +- .../github.com/camlistore/goexif/exif/exif.go | 206 ++++++---- .../camlistore/goexif/exif/exif_test.go | 132 +++++-- .../camlistore/goexif/exif/fields.go | 364 ++++++++++++------ .../goexif/exif/regress_expected_test.go | 62 +++ .../camlistore/goexif/exifstat/main.go | 60 +++ .../camlistore/goexif/mknote/fields.go | 268 +++++++++++++ .../camlistore/goexif/mknote/mknote.go | 70 ++++ .../github.com/camlistore/goexif/tiff/tag.go | 223 ++++++----- .../github.com/camlistore/goexif/tiff/tiff.go | 48 +-- .../camlistore/goexif/tiff/tiff_test.go | 60 +-- 16 files changed, 1214 insertions(+), 501 deletions(-) delete mode 100644 third_party/github.com/camlistore/goexif/cmd/main.go create mode 100644 third_party/github.com/camlistore/goexif/exif/regress_expected_test.go create mode 100644 third_party/github.com/camlistore/goexif/exifstat/main.go create mode 100644 third_party/github.com/camlistore/goexif/mknote/fields.go create mode 100644 third_party/github.com/camlistore/goexif/mknote/mknote.go diff --git a/pkg/index/receive.go b/pkg/index/receive.go index cd9c75966..70e231e60 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.Format() { + switch tag.TypeCategory() { case tiff.IntVal: return "int" case tiff.RatVal: @@ -472,13 +472,13 @@ func indexEXIF(wholeRef blob.Ref, header []byte, mm *mutationMap) { return nil } key := keyEXIFTag.Key(wholeRef, fmt.Sprintf("%04x", tag.Id)) - numComp := int(tag.Ncomp) - if tag.Format() == tiff.StringVal { + numComp := int(tag.Count) + if tag.TypeCategory() == tiff.StringVal { numComp = 1 } var val bytes.Buffer val.WriteString(keyEXIFTag.Val(tagFmt, numComp, "")) - if tag.Format() == tiff.StringVal { + if tag.TypeCategory() == tiff.StringVal { str := tag.StringVal() if containsUnsafeRawStrByte(str) { val.WriteString(urle(str)) @@ -486,7 +486,7 @@ func indexEXIF(wholeRef blob.Ref, header []byte, mm *mutationMap) { val.WriteString(str) } } else { - for i := 0; i < int(tag.Ncomp); i++ { + for i := 0; i < int(tag.Count); i++ { if i > 0 { val.WriteByte('|') } diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 46ef97c32..675dbc24c 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -977,7 +977,7 @@ func exifDateTimeInLocation(x *exif.Exif, loc *time.Location) (time.Time, error) return time.Time{}, err } } - if tag.Format() != tiff.StringVal { + if tag.TypeCategory() != 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/camlistore/goexif/README.camlistore b/third_party/github.com/camlistore/goexif/README.camlistore index eb1efc3be..636b05a1d 100644 --- a/third_party/github.com/camlistore/goexif/README.camlistore +++ b/third_party/github.com/camlistore/goexif/README.camlistore @@ -1,82 +1,78 @@ Changing code under third_party/github.com/camlistore/goexif ============================================================ -These instructions assume you have a github.com account. +Syncing updates from upstream +----------------------------- -Sync github.com/rwcarlsen/goexif -> github.com/camlistore/goexif ----------------------------------------------------------------- -1. Issue a pull request at https://github.com/camlistore/goexif/pulls, set - the base fork to camlistore/goexif go1 branch, and the head fork as - rwcarlsen/goexif go1 branch. Follow the normal github workflow until - someone on the camlistore project merges in the changes. +These instructions assume you have a github.com account. Further it is assumed +that your camlistore copy is at $GOPATH/src/camlistore.org -Sync github.com/camlistore/goexif -> camlistore.org/third_party/github.com/camlistore/goexif --------------------------------------------------------------------------------------------- -1. Once someone on the camlistore team merges in the latest from upstream, - checkout a local copy: +1. Checkout the github.com/rwcarlsen/goexif by calling + $ go get -u github.com/rwcarlsen/goexif + $ cd $GOPATH/src/github.com/rwcarlsen/goexif - $ git clone https://github.com/camlistore/goexif - $ cd goexif +2. Use git show to determine the revision. + $ git show commit cf045e9d6ba052fd348f82394d364cca4937589a -2. Make a patch to apply to the camlistore.org copy. You'll need to know the - git rev of github.com/camlistore/goexif that was last merged to - camlistore.org/third_party, for this example we'll use 030a4566: +3. Start merging using your favorite tools (vimdiff, meld, kdiff3, etc.) - # Create individual patches that have been applied upstream. - $ git format-patch -o /tmp/patches 030a4566 - $ cd /path/to/camlistore.org/third_party/github.com/camlistore/goexif - # Create new branch to temporarily apply each upstream change. - $ git checkout -b patches_individual - # Apply patches. - $ git am --directory=third_party/github.com/camlistore/goexif /tmp/patches/* +4. For the moment make sure to rewrite any new goexif import paths. For + example, - # If something fails to apply try: - $ git apply --reject - $ edit edit edit - $ git add - $ git am --resolved + import "github.com/rwcarlsen/goexif/tiff" - # If it is a patch camlistore already had, because we created it and - # pushed it upstream, you can skip it with: - $ git am --skip + needs to be rewritten to - # Now create a new branch to squash all the changes into one. Keeping a - # record of upstream commits in the default commit message of a single - # commit. - $ git checkout -b patches_squashed master - $ git merge --squash patches_individual + import "camlistore.org/third_party/github.com/camlistore/goexif/tiff" - # Verify no new files have been added that require import path updating: - $ cd /path/to/camlistore.org/third_party/ - $ ./rewrite-imports.sh -l - # If any rewrites are required, run: - $ ./rewrite-imports.sh -w + NOTE: Currently, you cannot use rewrite-imports.sh. This is going to change + in the future, as "third_party/github.com/camlistore/goexif/" is going to + be renamed to "third_party/github.com/rwcarlsen/goexif/". - # Now create a commit that will be sent for review. - $ git commit -v +5. Camlistore does not need to have all test data of upstream goexif in its + repository. For that reason, do not merge the file + exif/regress_expected_test.go and the subfolder 'exif/samples' to + camlistore. When merging exif_test.go, make sure to keep the test path to + be "." instead of "samples". + + Rationale: regress_expected_test.go is generated anew by exif_test.go (func + TestRegenRegress()), when the non-exported boolean variable 'regenRegress' + is set to true. In upstream, this file is generated on the subfolder + called 'samples', which contains some JPGs. Since Camlistore omits the samples + folder, we change the exif_test.go to use the goexif/exif folder directly + (which contains the single sample1.jpg) instead of the samples subfolder. + Unless new test images are added, regress_expected_test.go does not need to + be regenerated. It is unlikely that Camlistore will add new test data. + +6. Ensure all Camlistore code still compiles and all tests run through: + $ devcam test + +7. # Now create a commit that will be sent for review. + $ git commit -v # Enter your commit message on the first line per usual. - # You should see summaries of all the changes merged in the commit - # message. Leave these, they will be useful next sync as we'll know what - # commits were sync'd. Send the change for review. - $ ./misc/review + # Add the goexif revision from step 1. to the commit message as well + # as the commit messages of any noteworthy commits from upstream. + $ devcam review -Sync camlistore.org/third_party/github.com/camlistore/goexif -> github.com/camlistore/goexif ----------------------------------------------------------------------------------------------- -1. TODO(wathiede): this should follow a similar process as 'Sync - github.com/camlistore/goexif -> - camlistore.org/third_party/github.com/camlistore/goexif' Basically use - format-patch to generate a patch for each change we've made in - camlistore.org's repo and apply to a fork of github.com/camlistore/goexif. - Maybe skip the 'merge --squash' step, and keep each change in the log of - github.com/camlistore/goexif. -Sync github.com/camlistore/goexif -> github.com/rwcarlsen/goexif --------------------------------------------------------------------- -1. This should follow the standard github pull-request workflow. Issue a - pull request at https://github.com/request/goexif/pulls, set the base fork - to rwcarlsen/goexif go1 branch, and the head fork as camlistore/goexif go1 - branch. +Syncing Camlistore contributions back to upstream goexif +------------------------------------------------------------ + +1. Use a merge tool like meld, kdiff3, or similar to manually merge the code. + $ meld $GOPATH/src/camlistore.org/third_party/github.com/camlistore/goexif \ + $GOPATH/src/github.com/rwcarlsen/goexif + +2. While merging, make sure to have the correct goexif import paths. For + example, + + import "camlistore.org/third_party/github.com/camlistore/goexif/tiff" + + needs to be + + import "github.com/rwcarlsen/goexif/tiff" + +3. Make sure the samples folder path in exif_test.go stays "sample". + +4. Follow the github.com procedures to commit and submit a Pull Request. -2. Address any feedback during review and rwcarlsen will merge changes to - github.com/rwcarlsen/goexif as appropriate. diff --git a/third_party/github.com/camlistore/goexif/README.md b/third_party/github.com/camlistore/goexif/README.md index 2694d76ca..9c0d42ec8 100644 --- a/third_party/github.com/camlistore/goexif/README.md +++ b/third_party/github.com/camlistore/goexif/README.md @@ -4,18 +4,20 @@ goexif Provides decoding of basic exif and tiff encoded data. Still in alpha - no guarantees. Suggestions and pull requests are welcome. Functionality is split into two packages - "exif" and "tiff" The exif package depends on the tiff package. -Documentation can be found at http://go.pkgdoc.org/github.com/camlistore/goexif +Documentation can be found at http://godoc.org/github.com/rwcarlsen/goexif + +Like goexif? - Bitcoin tips welcome: 17w65FVqx196Qp7tfCCSLqyvsHUhiEEa7P To install, in a terminal type: ``` -go get github.com/camlistore/goexif/exif +go get github.com/rwcarlsen/goexif/exif ``` Or if you just want the tiff package: ``` -go get github.com/camlistore/goexif/tiff +go get github.com/rwcarlsen/goexif/tiff ``` Example usage: @@ -28,31 +30,31 @@ import ( "log" "fmt" - "github.com/camlistore/goexif/exif" + "github.com/rwcarlsen/goexif/exif" ) func main() { - fname := "sample1.jpg" + fname := "sample1.jpg" - f, err := os.Open(fname) - if err != nil { - log.Fatal(err) - } + f, err := os.Open(fname) + if err != nil { + log.Fatal(err) + } - x, err := exif.Decode(f) - f.Close() - if err != nil { - log.Fatal(err) - } + x, err := exif.Decode(f) + defer f.Close() + if err != nil { + log.Fatal(err) + } - camModel, _ := x.Get("Model") - date, _ := x.Get("DateTimeOriginal") - fmt.Println(camModel.StringVal()) - fmt.Println(date.StringVal()) + camModel, _ := x.Get(exif.Model) + date, _ := x.Get(exif.DateTimeOriginal) + fmt.Println(camModel.StringVal()) + fmt.Println(date.StringVal()) - focal, _ := x.Get("FocalLength") - numer, denom := focal.Rat2(0) // retrieve first (only) rat. value - fmt.Printf("%v/%v", numer, denom) + focal, _ := x.Get(exif.FocalLength) + numer, denom := focal.Rat2(0) // retrieve first (only) rat. value + fmt.Printf("%v/%v", numer, denom) } ``` diff --git a/third_party/github.com/camlistore/goexif/cmd/main.go b/third_party/github.com/camlistore/goexif/cmd/main.go deleted file mode 100644 index ac1823237..000000000 --- a/third_party/github.com/camlistore/goexif/cmd/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "os" - - "camlistore.org/third_party/github.com/camlistore/goexif/exif" - "camlistore.org/third_party/github.com/camlistore/goexif/tiff" -) - -func main() { - flag.Parse() - fname := flag.Arg(0) - - f, err := os.Open(fname) - if err != nil { - log.Fatal(err) - } - - x, err := exif.Decode(f) - if err != nil { - log.Fatal(err) - } - - x.Walk(Walker{}) -} - -type Walker struct{} - -func (_ Walker) Walk(name exif.FieldName, tag *tiff.Tag) error { - data, _ := tag.MarshalJSON() - fmt.Printf("%v: %v\n", name, string(data)) - return nil -} diff --git a/third_party/github.com/camlistore/goexif/exif/example_test.go b/third_party/github.com/camlistore/goexif/exif/example_test.go index afef2f187..552407a56 100644 --- a/third_party/github.com/camlistore/goexif/exif/example_test.go +++ b/third_party/github.com/camlistore/goexif/exif/example_test.go @@ -21,12 +21,12 @@ func ExampleDecode() { log.Fatal(err) } - camModel, _ := x.Get("Model") - date, _ := x.Get("DateTimeOriginal") + camModel, _ := x.Get(exif.Model) + date, _ := x.Get(exif.DateTimeOriginal) fmt.Println(camModel.StringVal()) fmt.Println(date.StringVal()) - focal, _ := x.Get("FocalLength") + focal, _ := x.Get(exif.FocalLength) numer, denom := focal.Rat2(0) // retrieve first (only) rat. value fmt.Printf("%v/%v", numer, denom) } diff --git a/third_party/github.com/camlistore/goexif/exif/exif.go b/third_party/github.com/camlistore/goexif/exif/exif.go index e89249349..0c0795576 100644 --- a/third_party/github.com/camlistore/goexif/exif/exif.go +++ b/third_party/github.com/camlistore/goexif/exif/exif.go @@ -1,5 +1,5 @@ // Package exif implements decoding of EXIF data as defined in the EXIF 2.2 -// specification. +// specification (http://www.exif.org/Exif2-2.PDF). package exif import ( @@ -10,28 +10,16 @@ import ( "errors" "fmt" "io" + "io/ioutil" "strings" "time" "camlistore.org/third_party/github.com/camlistore/goexif/tiff" ) -var validField map[FieldName]bool - -func init() { - validField = make(map[FieldName]bool) - for _, name := range exifFields { - validField[name] = true - } - for _, name := range gpsFields { - validField[name] = true - } - for _, name := range interopFields { - validField[name] = true - } -} - const ( + jpeg_APP1 = 0xE1 + exifPointer = 0x8769 gpsPointer = 0x8825 interopPointer = 0xA005 @@ -45,32 +33,95 @@ func (tag TagNotPresentError) Error() string { return fmt.Sprintf("exif: tag %q is not present", string(tag)) } -func isTagNotPresentErr(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 { + // Parse should read data from x and insert parsed fields into x via + // LoadTags. + Parse(x *Exif) error } +var parsers []Parser + +func init() { + RegisterParsers(&parser{}) +} + +// RegisterParsers registers one or more parsers to be automatically called +// when decoding EXIF data via the Decode function. +func RegisterParsers(ps ...Parser) { + parsers = append(parsers, ps...) +} + +type parser struct{} + +func (p *parser) Parse(x *Exif) error { + x.LoadTags(x.Tiff.Dirs[0], exifFields, false) + + // thumbnails + if len(x.Tiff.Dirs) >= 2 { + x.LoadTags(x.Tiff.Dirs[1], thumbnailFields, false) + } + + // recurse into exif, gps, and interop sub-IFDs + if err := loadSubDir(x, ExifIFDPointer, exifFields); err != nil { + return err + } + if err := loadSubDir(x, GPSInfoIFDPointer, gpsFields); err != nil { + return err + } + + return loadSubDir(x, InteroperabilityIFDPointer, interopFields) +} + +func loadSubDir(x *Exif, ptr FieldName, fieldMap map[uint16]FieldName) error { + r := bytes.NewReader(x.Raw) + + tag, err := x.Get(ptr) + if err != nil { + return nil + } + offset := tag.Int(0) + + _, err = r.Seek(offset, 0) + if err != nil { + return errors.New("exif: seek to sub-IFD failed: " + err.Error()) + } + subDir, _, err := tiff.DecodeDir(r, x.Tiff.Order) + if err != nil { + return errors.New("exif: sub-IFD decode failed: " + err.Error()) + } + x.LoadTags(subDir, fieldMap, false) + return nil +} + +// Exif provides access to decoded EXIF metadata fields and values. type Exif struct { - tif *tiff.Tiff - + Tiff *tiff.Tiff main map[FieldName]*tiff.Tag + Raw []byte } -// Decode parses EXIF-encoded data from r and returns a queryable Exif object. +// Decode parses EXIF-encoded data from r and returns a queryable Exif +// object. After the exif data section is called and the tiff structure +// decoded, each registered parser is called (in order of registration). If +// one parser returns an error, decoding terminates and the remaining +// parsers are not called. func Decode(r io.Reader) (*Exif, error) { // EXIF data in JPEG is stored in the APP1 marker. EXIF data uses the TIFF // format to store data. // If we're parsing a TIFF image, we don't need to strip away any data. // If we're parsing a JPEG image, we need to strip away the JPEG APP1 // marker and also the EXIF header. + header := make([]byte, 4) n, err := r.Read(header) - if n < len(header) { - return nil, errors.New("exif: short read on header") - } if err != nil { return nil, err } + if n < len(header) { + return nil, errors.New("exif: short read on header") + } var isTiff bool switch string(header) { @@ -100,9 +151,9 @@ func Decode(r io.Reader) (*Exif, error) { tif, err = tiff.Decode(tr) er = bytes.NewReader(b.Bytes()) } else { - // Strip away JPEG APP1 header. + // Locate the JPEG APP1 header. var sec *appSec - sec, err = newAppSec(0xE1, r) + sec, err = newAppSec(jpeg_APP1, r) if err != nil { return nil, err } @@ -115,55 +166,47 @@ func Decode(r io.Reader) (*Exif, error) { } if err != nil { - return nil, errors.New("exif: decode failed: " + err.Error()) + return nil, fmt.Errorf("exif: decode failed (%v) ", err) + } + + er.Seek(0, 0) + raw, err := ioutil.ReadAll(er) + if err != nil { + return nil, fmt.Errorf("exif: decode failed (%v) ", err) } // build an exif structure from the tiff x := &Exif{ main: map[FieldName]*tiff.Tag{}, - tif: tif, + Tiff: tif, + Raw: raw, } - ifd0 := tif.Dirs[0] - for _, tag := range ifd0.Tags { - name := exifFields[tag.Id] - x.main[name] = tag - } - - // recurse into exif, gps, and interop sub-IFDs - if err = x.loadSubDir(er, exifIFDPointer, exifFields); err != nil { - return x, err - } - if err = x.loadSubDir(er, gpsInfoIFDPointer, gpsFields); err != nil { - return x, err - } - if err = x.loadSubDir(er, interoperabilityIFDPointer, interopFields); err != nil { - return x, err + for i, p := range parsers { + if err := p.Parse(x); err != nil { + return x, fmt.Errorf("exif: parser %v failed (%v)", i, err) + } } return x, nil } -func (x *Exif) loadSubDir(r *bytes.Reader, ptrName FieldName, fieldMap map[uint16]FieldName) error { - tag, ok := x.main[ptrName] - if !ok { - return nil - } - offset := tag.Int(0) - - _, err := r.Seek(offset, 0) - if err != nil { - return errors.New("exif: seek to sub-IFD failed: " + err.Error()) - } - subDir, _, err := tiff.DecodeDir(r, x.tif.Order) - if err != nil { - return errors.New("exif: sub-IFD decode failed: " + err.Error()) - } - for _, tag := range subDir.Tags { +// LoadTags loads tags into the available fields from the tiff Directory +// using the given tagid-fieldname mapping. Used to load makernote and +// other meta-data. If showMissing is true, tags in d that are not in the +// fieldMap will be loaded with the FieldName UnknownPrefix followed by the +// tag ID (in hex format). +func (x *Exif) LoadTags(d *tiff.Dir, fieldMap map[uint16]FieldName, showMissing bool) { + for _, tag := range d.Tags { name := fieldMap[tag.Id] + if name == "" { + if !showMissing { + continue + } + name = FieldName(fmt.Sprintf("%v%x", UnknownPrefix, tag.Id)) + } x.main[name] = tag } - return nil } // Get retrieves the EXIF tag for the given field name. @@ -171,22 +214,21 @@ func (x *Exif) loadSubDir(r *bytes.Reader, ptrName FieldName, fieldMap map[uint1 // If the tag is not known or not present, an error is returned. If the // tag name is known, the error will be a TagNotPresentError. func (x *Exif) Get(name FieldName) (*tiff.Tag, error) { - if !validField[name] { - return nil, fmt.Errorf("exif: invalid tag name %q", name) - } else if tg, ok := x.main[name]; ok { + if tg, ok := x.main[name]; ok { return tg, nil } return nil, TagNotPresentError(name) } -// Walker is the interface used to traverse all exif fields of an Exif object. -// Returning a non-nil error aborts the walk/traversal. +// Walker is the interface used to traverse all fields of an Exif object. type Walker interface { + // Walk is called for each non-nil EXIF field. Returning a non-nil + // error aborts the walk/traversal. Walk(name FieldName, tag *tiff.Tag) error } -// Walk calls the Walk method of w with the name and tag for every non-nil exif -// field. +// Walk calls the Walk method of w with the name and tag for every non-nil +// EXIF field. If w aborts the walk with an error, that error is returned. func (x *Exif) Walk(w Walker) error { for name, tag := range x.main { if err := w.Walk(name, tag); err != nil { @@ -214,7 +256,7 @@ func (x *Exif) DateTime() (time.Time, error) { return dt, err } } - if tag.Format() != tiff.StringVal { + if tag.TypeCategory() != tiff.StringVal { return dt, errors.New("DateTime[Original] not in string format") } exifTimeLayout := "2006:01:02 15:04:05" @@ -271,6 +313,22 @@ func (x *Exif) String() string { return buf.String() } +// JpegThumbnail returns the jpeg thumbnail if it exists. If it doesn't exist, +// TagNotPresentError will be returned +func (x *Exif) JpegThumbnail() ([]byte, error) { + offset, err := x.Get(ThumbJPEGInterchangeFormat) + 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 +} + +// MarshalJson implements the encoding/json.Marshaler interface providing output of +// all EXIF fields present (names and values). func (x Exif) MarshalJSON() ([]byte, error) { return json.Marshal(x.main) } @@ -285,7 +343,7 @@ type appSec struct { func newAppSec(marker byte, r io.Reader) (*appSec, error) { br := bufio.NewReader(r) app := &appSec{marker: marker} - var dataLen uint16 + var dataLen int // seek to marker for dataLen == 0 { @@ -303,16 +361,16 @@ func newAppSec(marker byte, r io.Reader) (*appSec, error) { if err != nil { return nil, err } - dataLen = binary.BigEndian.Uint16(dataLenBytes) + dataLen = int(binary.BigEndian.Uint16(dataLenBytes)) } // read section data nread := 0 - for nread < int(dataLen) { - s := make([]byte, int(dataLen)-nread) + for nread < dataLen { + s := make([]byte, dataLen-nread) n, err := br.Read(s) nread += n - if err != nil && nread < int(dataLen) { + if err != nil && nread < dataLen { return nil, err } app.data = append(app.data, s[:n]...) diff --git a/third_party/github.com/camlistore/goexif/exif/exif_test.go b/third_party/github.com/camlistore/goexif/exif/exif_test.go index 888020275..66c329161 100644 --- a/third_party/github.com/camlistore/goexif/exif/exif_test.go +++ b/third_party/github.com/camlistore/goexif/exif/exif_test.go @@ -1,62 +1,142 @@ package exif import ( + "flag" + "fmt" + "io" "os" + "path/filepath" + "strings" "testing" "camlistore.org/third_party/github.com/camlistore/goexif/tiff" ) -func TestDecode(t *testing.T) { - name := "sample1.jpg" - f, err := os.Open(name) - if err != nil { - t.Fatalf("%v\n", err) +// 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 } - x, err := Decode(f) + dst, err := os.Create("regress_expected_test.go") if err != nil { t.Fatal(err) } - if x == nil { - t.Fatalf("No error and yet %v was not decoded\n", name) + defer dst.Close() + + dir, err := os.Open(".") + if err != nil { + t.Fatal(err) } + defer dir.Close() - val, err := x.Get("Model") - t.Logf("Model: %v", val) - t.Log(x) + names, err := dir.Readdirnames(0) + if err != nil { + t.Fatal(err) + } + for i, name := range names { + names[i] = filepath.Join(".", name) + } + makeExpected(names, dst) } -type walker struct { - t *testing.T +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") } -func (w *walker) Walk(name FieldName, tag *tiff.Tag) error { - w.t.Logf("%v: %v", name, tag) +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 TestWalk(t *testing.T) { - name := "sample1.jpg" - f, err := os.Open(name) +func TestDecode(t *testing.T) { + fpath := filepath.Join(*dataDir, "") + f, err := os.Open(fpath) if err != nil { - t.Fatalf("%v\n", err) + t.Fatalf("Could not open sample directory '%s': %v", fpath, err) } - x, err := Decode(f) + names, err := f.Readdirnames(0) if err != nil { - t.Error(err) - } - if x == nil { - t.Fatal("bad err") + t.Fatalf("Could not read sample directory '%s': %v", fpath, err) } - x.Walk(&walker{t}) + cnt := 0 + for _, name := range names { + if !strings.HasSuffix(name, ".jpg") { + t.Logf("skipping non .jpg file %v", name) + continue + } + t.Logf("testing file %v", name) + f, err := os.Open(filepath.Join(fpath, name)) + if err != nil { + t.Fatal(err) + } + x, err := Decode(f) + if err != nil { + t.Fatal(err) + } else if x == nil { + t.Fatalf("No error and yet %v was not decoded", name) + } + + x.Walk(&walker{name, t}) + cnt++ + } + if cnt != len(regressExpected) { + t.Errorf("Did not process enough samples, got %d, want %d", cnt, len(regressExpected)) + } +} + +type walker struct { + picName string + t *testing.T +} + +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()) + } + return nil } func TestMarshal(t *testing.T) { - name := "sample1.jpg" + name := filepath.Join(*dataDir, "sample1.jpg") f, err := os.Open(name) if err != nil { t.Fatalf("%v\n", err) diff --git a/third_party/github.com/camlistore/goexif/exif/fields.go b/third_party/github.com/camlistore/goexif/exif/fields.go index 1ea55e886..9b574f0b4 100644 --- a/third_party/github.com/camlistore/goexif/exif/fields.go +++ b/third_party/github.com/camlistore/goexif/exif/fields.go @@ -2,18 +2,137 @@ package exif type FieldName string +// UnknownPrefix is used as the first part of field names for decoded tags for +// which there is no known/supported EXIF field. +const UnknownPrefix = "UnknownTag_" + +// Primary EXIF fields const ( - ImageWidth FieldName = "ImageWidth" - ImageLength FieldName = "ImageLength" // height - Orientation FieldName = "Orientation" - DateTime FieldName = "DateTime" - DateTimeOriginal FieldName = "DateTimeOriginal" + ImageWidth FieldName = "ImageWidth" + ImageLength = "ImageLength" // Image height called Length by EXIF spec + BitsPerSample = "BitsPerSample" + Compression = "Compression" + PhotometricInterpretation = "PhotometricInterpretation" + Orientation = "Orientation" + SamplesPerPixel = "SamplesPerPixel" + PlanarConfiguration = "PlanarConfiguration" + YCbCrSubSampling = "YCbCrSubSampling" + YCbCrPositioning = "YCbCrPositioning" + XResolution = "XResolution" + YResolution = "YResolution" + ResolutionUnit = "ResolutionUnit" + DateTime = "DateTime" + ImageDescription = "ImageDescription" + Make = "Make" + Model = "Model" + Software = "Software" + Artist = "Artist" + Copyright = "Copyright" + ExifIFDPointer = "ExifIFDPointer" + GPSInfoIFDPointer = "GPSInfoIFDPointer" + InteroperabilityIFDPointer = "InteroperabilityIFDPointer" + ExifVersion = "ExifVersion" + FlashpixVersion = "FlashpixVersion" + ColorSpace = "ColorSpace" + ComponentsConfiguration = "ComponentsConfiguration" + CompressedBitsPerPixel = "CompressedBitsPerPixel" + PixelXDimension = "PixelXDimension" + PixelYDimension = "PixelYDimension" + MakerNote = "MakerNote" + UserComment = "UserComment" + RelatedSoundFile = "RelatedSoundFile" + DateTimeOriginal = "DateTimeOriginal" + DateTimeDigitized = "DateTimeDigitized" + SubSecTime = "SubSecTime" + SubSecTimeOriginal = "SubSecTimeOriginal" + SubSecTimeDigitized = "SubSecTimeDigitized" + ImageUniqueID = "ImageUniqueID" + ExposureTime = "ExposureTime" + FNumber = "FNumber" + ExposureProgram = "ExposureProgram" + SpectralSensitivity = "SpectralSensitivity" + ISOSpeedRatings = "ISOSpeedRatings" + OECF = "OECF" + ShutterSpeedValue = "ShutterSpeedValue" + ApertureValue = "ApertureValue" + BrightnessValue = "BrightnessValue" + ExposureBiasValue = "ExposureBiasValue" + MaxApertureValue = "MaxApertureValue" + SubjectDistance = "SubjectDistance" + MeteringMode = "MeteringMode" + LightSource = "LightSource" + Flash = "Flash" + FocalLength = "FocalLength" + SubjectArea = "SubjectArea" + FlashEnergy = "FlashEnergy" + SpatialFrequencyResponse = "SpatialFrequencyResponse" + FocalPlaneXResolution = "FocalPlaneXResolution" + FocalPlaneYResolution = "FocalPlaneYResolution" + FocalPlaneResolutionUnit = "FocalPlaneResolutionUnit" + SubjectLocation = "SubjectLocation" + ExposureIndex = "ExposureIndex" + SensingMethod = "SensingMethod" + FileSource = "FileSource" + SceneType = "SceneType" + CFAPattern = "CFAPattern" + CustomRendered = "CustomRendered" + ExposureMode = "ExposureMode" + WhiteBalance = "WhiteBalance" + DigitalZoomRatio = "DigitalZoomRatio" + FocalLengthIn35mmFilm = "FocalLengthIn35mmFilm" + SceneCaptureType = "SceneCaptureType" + GainControl = "GainControl" + Contrast = "Contrast" + Saturation = "Saturation" + Sharpness = "Sharpness" + DeviceSettingDescription = "DeviceSettingDescription" + SubjectDistanceRange = "SubjectDistanceRange" ) +// thumbnail fields const ( - exifIFDPointer FieldName = "ExifIFDPointer" - gpsInfoIFDPointer = "GPSInfoIFDPointer" - interoperabilityIFDPointer = "InteroperabilityIFDPointer" + ThumbJPEGInterchangeFormat = "ThumbJPEGInterchangeFormat" // offset to thumb jpeg SOI + ThumbJPEGInterchangeFormatLength = "ThumbJPEGInterchangeFormatLength" // byte length of thumb +) + +// GPS fields +const ( + GPSVersionID FieldName = "GPSVersionID" + GPSLatitudeRef = "GPSLatitudeRef" + GPSLatitude = "GPSLatitude" + GPSLongitudeRef = "GPSLongitudeRef" + GPSLongitude = "GPSLongitude" + GPSAltitudeRef = "GPSAltitudeRef" + GPSAltitude = "GPSAltitude" + GPSTimeStamp = "GPSTimeStamp" + GPSSatelites = "GPSSatelites" + GPSStatus = "GPSStatus" + GPSMeasureMode = "GPSMeasureMode" + GPSDOP = "GPSDOP" + GPSSpeedRef = "GPSSpeedRef" + GPSSpeed = "GPSSpeed" + GPSTrackRef = "GPSTrackRef" + GPSTrack = "GPSTrack" + GPSImgDirectionRef = "GPSImgDirectionRef" + GPSImgDirection = "GPSImgDirection" + GPSMapDatum = "GPSMapDatum" + GPSDestLatitudeRef = "GPSDestLatitudeRef" + GPSDestLatitude = "GPSDestLatitude" + GPSDestLongitudeRef = "GPSDestLongitudeRef" + GPSDestLongitude = "GPSDestLongitude" + GPSDestBearingRef = "GPSDestBearingRef" + GPSDestBearing = "GPSDestBearing" + GPSDestDistanceRef = "GPSDestDistanceRef" + GPSDestDistance = "GPSDestDistance" + GPSProcessingMethod = "GPSProcessingMethod" + GPSAreaInformation = "GPSAreaInformation" + GPSDateStamp = "GPSDateStamp" + GPSDifferential = "GPSDifferential" +) + +// interoperability fields +const ( + InteroperabilityIndex FieldName = "InteroperabilityIndex" ) var exifFields = map[uint16]FieldName{ @@ -21,145 +140,150 @@ var exifFields = map[uint16]FieldName{ ////////// IFD 0 //////////////////// ///////////////////////////////////// - // image data structure - 0x0100: "ImageWidth", - 0x0101: "ImageLength", - 0x0102: "BitsPerSample", - 0x0103: "Compression", - 0x0106: "PhotometricInterpretation", - 0x0112: "Orientation", - 0x0115: "SamplesPerPixel", - 0x011C: "PlanarConfiguration", - 0x0212: "YCbCrSubSampling", - 0x0213: "YCbCrPositioning", - 0x011A: "XResolution", - 0x011B: "YResolution", - 0x0128: "ResolutionUnit", + // image data structure for the thumbnail + 0x0100: ImageWidth, + 0x0101: ImageLength, + 0x0102: BitsPerSample, + 0x0103: Compression, + 0x0106: PhotometricInterpretation, + 0x0112: Orientation, + 0x0115: SamplesPerPixel, + 0x011C: PlanarConfiguration, + 0x0212: YCbCrSubSampling, + 0x0213: YCbCrPositioning, + 0x011A: XResolution, + 0x011B: YResolution, + 0x0128: ResolutionUnit, // Other tags - 0x0132: "DateTime", - 0x010E: "ImageDescription", - 0x010F: "Make", - 0x0110: "Model", - 0x0131: "Software", - 0x013B: "Artist", - 0x8298: "Copyright", + 0x0132: DateTime, + 0x010E: ImageDescription, + 0x010F: Make, + 0x0110: Model, + 0x0131: Software, + 0x013B: Artist, + 0x8298: Copyright, // private tags - exifPointer: "ExifIFDPointer", + exifPointer: ExifIFDPointer, ///////////////////////////////////// ////////// Exif sub IFD ///////////// ///////////////////////////////////// - gpsPointer: "GPSInfoIFDPointer", - interopPointer: "InteroperabilityIFDPointer", + gpsPointer: GPSInfoIFDPointer, + interopPointer: InteroperabilityIFDPointer, - 0x9000: "ExifVersion", - 0xA000: "FlashpixVersion", + 0x9000: ExifVersion, + 0xA000: FlashpixVersion, - 0xA001: "ColorSpace", + 0xA001: ColorSpace, - 0x9101: "ComponentsConfiguration", - 0x9102: "CompressedBitsPerPixel", - 0xA002: "PixelXDimension", - 0xA003: "PixelYDimension", + 0x9101: ComponentsConfiguration, + 0x9102: CompressedBitsPerPixel, + 0xA002: PixelXDimension, + 0xA003: PixelYDimension, - 0x927C: "MakerNote", - 0x9286: "UserComment", + 0x927C: MakerNote, + 0x9286: UserComment, - 0xA004: "RelatedSoundFile", - 0x9003: "DateTimeOriginal", - 0x9004: "DateTimeDigitized", - 0x9290: "SubSecTime", - 0x9291: "SubSecTimeOriginal", - 0x9292: "SubSecTimeDigitized", + 0xA004: RelatedSoundFile, + 0x9003: DateTimeOriginal, + 0x9004: DateTimeDigitized, + 0x9290: SubSecTime, + 0x9291: SubSecTimeOriginal, + 0x9292: SubSecTimeDigitized, - 0xA420: "ImageUniqueID", + 0xA420: ImageUniqueID, // picture conditions - 0x829A: "ExposureTime", - 0x829D: "FNumber", - 0x8822: "ExposureProgram", - 0x8824: "SpectralSensitivity", - 0x8827: "ISOSpeedRatings", - 0x8828: "OECF", - 0x9201: "ShutterSpeedValue", - 0x9202: "ApertureValue", - 0x9203: "BrightnessValue", - 0x9204: "ExposureBiasValue", - 0x9205: "MaxApertureValue", - 0x9206: "SubjectDistance", - 0x9207: "MeteringMode", - 0x9208: "LightSource", - 0x9209: "Flash", - 0x920A: "FocalLength", - 0x9214: "SubjectArea", - 0xA20B: "FlashEnergy", - 0xA20C: "SpatialFrequencyResponse", - 0xA20E: "FocalPlaneXResolution", - 0xA20F: "FocalPlaneYResolution", - 0xA210: "FocalPlaneResolutionUnit", - 0xA214: "SubjectLocation", - 0xA215: "ExposureIndex", - 0xA217: "SensingMethod", - 0xA300: "FileSource", - 0xA301: "SceneType", - 0xA302: "CFAPattern", - 0xA401: "CustomRendered", - 0xA402: "ExposureMode", - 0xA403: "WhiteBalance", - 0xA404: "DigitalZoomRatio", - 0xA405: "FocalLengthIn35mmFilm", - 0xA406: "SceneCaptureType", - 0xA407: "GainControl", - 0xA408: "Contrast", - 0xA409: "Saturation", - 0xA40A: "Sharpness", - 0xA40B: "DeviceSettingDescription", - 0xA40C: "SubjectDistanceRange", + 0x829A: ExposureTime, + 0x829D: FNumber, + 0x8822: ExposureProgram, + 0x8824: SpectralSensitivity, + 0x8827: ISOSpeedRatings, + 0x8828: OECF, + 0x9201: ShutterSpeedValue, + 0x9202: ApertureValue, + 0x9203: BrightnessValue, + 0x9204: ExposureBiasValue, + 0x9205: MaxApertureValue, + 0x9206: SubjectDistance, + 0x9207: MeteringMode, + 0x9208: LightSource, + 0x9209: Flash, + 0x920A: FocalLength, + 0x9214: SubjectArea, + 0xA20B: FlashEnergy, + 0xA20C: SpatialFrequencyResponse, + 0xA20E: FocalPlaneXResolution, + 0xA20F: FocalPlaneYResolution, + 0xA210: FocalPlaneResolutionUnit, + 0xA214: SubjectLocation, + 0xA215: ExposureIndex, + 0xA217: SensingMethod, + 0xA300: FileSource, + 0xA301: SceneType, + 0xA302: CFAPattern, + 0xA401: CustomRendered, + 0xA402: ExposureMode, + 0xA403: WhiteBalance, + 0xA404: DigitalZoomRatio, + 0xA405: FocalLengthIn35mmFilm, + 0xA406: SceneCaptureType, + 0xA407: GainControl, + 0xA408: Contrast, + 0xA409: Saturation, + 0xA40A: Sharpness, + 0xA40B: DeviceSettingDescription, + 0xA40C: SubjectDistanceRange, } var gpsFields = map[uint16]FieldName{ ///////////////////////////////////// //// GPS sub-IFD //////////////////// ///////////////////////////////////// - 0x0: "GPSVersionID", - 0x1: "GPSLatitudeRef", - 0x2: "GPSLatitude", - 0x3: "GPSLongitudeRef", - 0x4: "GPSLongitude", - 0x5: "GPSAltitudeRef", - 0x6: "GPSAltitude", - 0x7: "GPSTimeStamp", - 0x8: "GPSSatelites", - 0x9: "GPSStatus", - 0xA: "GPSMeasureMode", - 0xB: "GPSDOP", - 0xC: "GPSSpeedRef", - 0xD: "GPSSpeed", - 0xE: "GPSTrackRef", - 0xF: "GPSTrack", - 0x10: "GPSImgDirectionRef", - 0x11: "GPSImgDirection", - 0x12: "GPSMapDatum", - 0x13: "GPSDestLatitudeRef", - 0x14: "GPSDestLatitude", - 0x15: "GPSDestLongitudeRef", - 0x16: "GPSDestLongitude", - 0x17: "GPSDestBearingRef", - 0x18: "GPSDestBearing", - 0x19: "GPSDestDistanceRef", - 0x1A: "GPSDestDistance", - 0x1B: "GPSProcessingMethod", - 0x1C: "GPSAreaInformation", - 0x1D: "GPSDateStamp", - 0x1E: "GPSDifferential", + 0x0: GPSVersionID, + 0x1: GPSLatitudeRef, + 0x2: GPSLatitude, + 0x3: GPSLongitudeRef, + 0x4: GPSLongitude, + 0x5: GPSAltitudeRef, + 0x6: GPSAltitude, + 0x7: GPSTimeStamp, + 0x8: GPSSatelites, + 0x9: GPSStatus, + 0xA: GPSMeasureMode, + 0xB: GPSDOP, + 0xC: GPSSpeedRef, + 0xD: GPSSpeed, + 0xE: GPSTrackRef, + 0xF: GPSTrack, + 0x10: GPSImgDirectionRef, + 0x11: GPSImgDirection, + 0x12: GPSMapDatum, + 0x13: GPSDestLatitudeRef, + 0x14: GPSDestLatitude, + 0x15: GPSDestLongitudeRef, + 0x16: GPSDestLongitude, + 0x17: GPSDestBearingRef, + 0x18: GPSDestBearing, + 0x19: GPSDestDistanceRef, + 0x1A: GPSDestDistance, + 0x1B: GPSProcessingMethod, + 0x1C: GPSAreaInformation, + 0x1D: GPSDateStamp, + 0x1E: GPSDifferential, } var interopFields = map[uint16]FieldName{ ///////////////////////////////////// //// Interoperability sub-IFD /////// ///////////////////////////////////// - 0x1: "InteroperabilityIndex", + 0x1: InteroperabilityIndex, +} + +var thumbnailFields = map[uint16]FieldName{ + 0x0201: ThumbJPEGInterchangeFormat, + 0x0202: ThumbJPEGInterchangeFormatLength, } diff --git a/third_party/github.com/camlistore/goexif/exif/regress_expected_test.go b/third_party/github.com/camlistore/goexif/exif/regress_expected_test.go new file mode 100644 index 000000000..98088c1f3 --- /dev/null +++ b/third_party/github.com/camlistore/goexif/exif/regress_expected_test.go @@ -0,0 +1,62 @@ +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"]}`, + }, +} diff --git a/third_party/github.com/camlistore/goexif/exifstat/main.go b/third_party/github.com/camlistore/goexif/exifstat/main.go new file mode 100644 index 000000000..0b383c9d5 --- /dev/null +++ b/third_party/github.com/camlistore/goexif/exifstat/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "github.com/rwcarlsen/goexif/exif" + "github.com/rwcarlsen/goexif/mknote" + "github.com/rwcarlsen/goexif/tiff" +) + +var mnote = flag.Bool("mknote", false, "try to parse makernote data") +var thumb = flag.Bool("thumb", false, "dump thumbail data to stdout (for first listed image file)") + +func main() { + flag.Parse() + fnames := flag.Args() + + if *mnote { + exif.RegisterParsers(mknote.All...) + } + + for _, name := range fnames { + f, err := os.Open(name) + if err != nil { + log.Printf("err on %v: %v", name, err) + continue + } + + x, err := exif.Decode(f) + if err != nil { + log.Printf("err on %v: %v", name, err) + continue + } + + if *thumb { + data, err := x.JpegThumbnail() + if err != nil { + log.Fatal("no thumbnail present") + } + if _, err := os.Stdout.Write(data); err != nil { + log.Fatal(err) + } + return + } + + fmt.Printf("\n---- Image '%v' ----\n", name) + x.Walk(Walker{}) + } +} + +type Walker struct{} + +func (_ Walker) Walk(name exif.FieldName, tag *tiff.Tag) error { + data, _ := tag.MarshalJSON() + fmt.Printf(" %v: %v\n", name, string(data)) + return nil +} diff --git a/third_party/github.com/camlistore/goexif/mknote/fields.go b/third_party/github.com/camlistore/goexif/mknote/fields.go new file mode 100644 index 000000000..e67d11c20 --- /dev/null +++ b/third_party/github.com/camlistore/goexif/mknote/fields.go @@ -0,0 +1,268 @@ +package mknote + +import "github.com/rwcarlsen/goexif/exif" + +// Useful resources used in creating these tables: +// http://www.exiv2.org/makernote.html +// http://www.exiv2.org/tags-canon.html +// http://www.exiv2.org/tags-nikon.html + +// Known Maker Note fields +const ( + // common fields + ISOSpeed exif.FieldName = "ISOSpeed" + ColorMode = "ColorMode" + Quality = "Quality" + Sharpening = "Sharpening" + Focus = "Focus" + FlashSetting = "FlashSetting" + FlashDevice = "FlashDevice" + WhiteBalanceBias = "WhiteBalanceBias" + WB_RBLevels = "WB_RBLevels" + ProgramShift = "ProgramShift" + ExposureDiff = "ExposureDiff" + ISOSelection = "ISOSelection" + DataDump = "DataDump" + Preview = "Preview" + FlashComp = "FlashComp" + ISOSettings = "ISOSettings" + ImageBoundary = "ImageBoundary" + FlashExposureComp = "FlashExposureComp" + FlashBracketComp = "FlashBracketComp" + ExposureBracketComp = "ExposureBracketComp" + ImageProcessing = "ImageProcessing" + CropHiSpeed = "CropHiSpeed" + ExposureTuning = "ExposureTuning" + SerialNumber = "SerialNumber" + ImageAuthentication = "ImageAuthentication" + ActiveDLighting = "ActiveDLighting" + VignetteControl = "VignetteControl" + ImageAdjustment = "ImageAdjustment" + ToneComp = "ToneComp" + AuxiliaryLens = "AuxiliaryLens" + LensType = "LensType" + Lens = "Lens" + FocusDistance = "FocusDistance" + DigitalZoom = "DigitalZoom" + FlashMode = "FlashMode" + ShootingMode = "ShootingMode" + AutoBracketRelease = "AutoBracketRelease" + LensFStops = "LensFStops" + ContrastCurve = "ContrastCurve" + ColorHue = "ColorHue" + SceneMode = "SceneMode" + HueAdjustment = "HueAdjustment" + NEFCompression = "NEFCompression" + NoiseReduction = "NoiseReduction" + LinearizationTable = "LinearizationTable" + RawImageCenter = "RawImageCenter" + SensorPixelSize = "SensorPixelSize" + SceneAssist = "SceneAssist" + RetouchHistory = "RetouchHistory" + ImageDataSize = "ImageDataSize" + ImageCount = "ImageCount" + DeletedImageCount = "DeletedImageCount" + ShutterCount = "ShutterCount" + ImageOptimization = "ImageOptimization" + SaturationText = "SaturationText" + VariProgram = "VariProgram" + ImageStabilization = "ImageStabilization" + AFResponse = "AFResponse" + HighISONoiseReduction = "HighISONoiseReduction" + ToningEffect = "ToningEffect" + PrintIM = "PrintIM" + CaptureData = "CaptureData" + CaptureVersion = "CaptureVersion" + CaptureOffsets = "CaptureOffsets" + ScanIFD = "ScanIFD" + ICCProfile = "ICCProfile" + CaptureOutput = "CaptureOutput" + Panorama = "Panorama" + ImageType = "ImageType" + FirmwareVersion = "FirmwareVersion" + FileNumber = "FileNumber" + OwnerName = "OwnerName" + CameraInfo = "CameraInfo" + CustomFunctions = "CustomFunctions" + ModelID = "ModelID" + PictureInfo = "PictureInfo" + ThumbnailImageValidArea = "ThumbnailImageValidArea" + SerialNumberFormat = "SerialNumberFormat" + SuperMacro = "SuperMacro" + OriginalDecisionDataOffset = "OriginalDecisionDataOffset" + WhiteBalanceTable = "WhiteBalanceTable" + LensModel = "LensModel" + InternalSerialNumber = "InternalSerialNumber" + DustRemovalData = "DustRemovalData" + ProcessingInfo = "ProcessingInfo" + MeasuredColor = "MeasuredColor" + VRDOffset = "VRDOffset" + SensorInfo = "SensorInfo" + ColorData = "ColorData" + + // Nikon-specific fields + Nikon_Version = "Nikon.Version" + Nikon_WhiteBalance = "Nikon.WhiteBalance" + Nikon_ColorSpace = "Nikon.ColorSpace" + Nikon_LightSource = "Nikon.LightSource" + Nikon_Saturation = "Nikon_Saturation" + Nikon_ShotInfo = "Nikon.ShotInfo" // A sub-IFD + Nikon_VRInfo = "Nikon.VRInfo" // A sub-IFD + Nikon_PictureControl = "Nikon.PictureControl" // A sub-IFD + Nikon_WorldTime = "Nikon.WorldTime" // A sub-IFD + Nikon_ISOInfo = "Nikon.ISOInfo" // A sub-IFD + Nikon_AFInfo = "Nikon.AFInfo" // A sub-IFD + Nikon_ColorBalance = "Nikon.ColorBalance" // A sub-IFD + Nikon_LensData = "Nikon.LensData" // A sub-IFD + Nikon_SerialNO = "Nikon.SerialNO" // usually starts with "NO=" + Nikon_FlashInfo = "Nikon.FlashInfo" // A sub-IFD + Nikon_MultiExposure = "Nikon.MultiExposure" // A sub-IFD + Nikon_AFInfo2 = "Nikon.AFInfo2" // A sub-IFD + Nikon_FileInfo = "Nikon.FileInfo" // A sub-IFD + Nikon_AFTune = "Nikon.AFTune" // A sub-IFD + Nikon3_0x000a = "Nikon3.0x000a" + Nikon3_0x009b = "Nikon3.0x009b" + Nikon3_0x009f = "Nikon3.0x009f" + Nikon3_0x00a3 = "Nikon3.0x00a3" + + // Canon-specific fiends + Canon_CameraSettings = "Canon.CameraSettings" // A sub-IFD + Canon_ShotInfo = "Canon.ShotInfo" // A sub-IFD + Canon_AFInfo = "Canon.AFInfo" + Canon_0x0000 = "Canon.0x0000" + Canon_0x0003 = "Canon.0x0003" + Canon_0x00b5 = "Canon.0x00b5" + Canon_0x00c0 = "Canon.0x00c0" + Canon_0x00c1 = "Canon.0x00c1" +) + +var makerNoteCanonFields = map[uint16]exif.FieldName{ + 0x0000: Canon_0x0000, + 0x0001: Canon_CameraSettings, + 0x0002: exif.FocalLength, + 0x0003: Canon_0x0003, + 0x0004: Canon_ShotInfo, + 0x0005: Panorama, + 0x0006: ImageType, + 0x0007: FirmwareVersion, + 0x0008: FileNumber, + 0x0009: OwnerName, + 0x000c: SerialNumber, + 0x000d: CameraInfo, + 0x000f: CustomFunctions, + 0x0010: ModelID, + 0x0012: PictureInfo, + 0x0013: ThumbnailImageValidArea, + 0x0015: SerialNumberFormat, + 0x001a: SuperMacro, + 0x0026: Canon_AFInfo, + 0x0083: OriginalDecisionDataOffset, + 0x00a4: WhiteBalanceTable, + 0x0095: LensModel, + 0x0096: InternalSerialNumber, + 0x0097: DustRemovalData, + 0x0099: CustomFunctions, + 0x00a0: ProcessingInfo, + 0x00aa: MeasuredColor, + 0x00b4: exif.ColorSpace, + 0x00b5: Canon_0x00b5, + 0x00c0: Canon_0x00c0, + 0x00c1: Canon_0x00c1, + 0x00d0: VRDOffset, + 0x00e0: SensorInfo, + 0x4001: ColorData, +} + +// Nikon version 3 Maker Notes fields (used by E5400, SQ, D2H, D70, and newer) +var makerNoteNikon3Fields = map[uint16]exif.FieldName{ + 0x0001: Nikon_Version, + 0x0002: ISOSpeed, + 0x0003: ColorMode, + 0x0004: Quality, + 0x0005: Nikon_WhiteBalance, + 0x0006: Sharpening, + 0x0007: Focus, + 0x0008: FlashSetting, + 0x0009: FlashDevice, + 0x000a: Nikon3_0x000a, + 0x000b: WhiteBalanceBias, + 0x000c: WB_RBLevels, + 0x000d: ProgramShift, + 0x000e: ExposureDiff, + 0x000f: ISOSelection, + 0x0010: DataDump, + 0x0011: Preview, + 0x0012: FlashComp, + 0x0013: ISOSettings, + 0x0016: ImageBoundary, + 0x0017: FlashExposureComp, + 0x0018: FlashBracketComp, + 0x0019: ExposureBracketComp, + 0x001a: ImageProcessing, + 0x001b: CropHiSpeed, + 0x001c: ExposureTuning, + 0x001d: SerialNumber, + 0x001e: Nikon_ColorSpace, + 0x001f: Nikon_VRInfo, + 0x0020: ImageAuthentication, + 0x0022: ActiveDLighting, + 0x0023: Nikon_PictureControl, + 0x0024: Nikon_WorldTime, + 0x0025: Nikon_ISOInfo, + 0x002a: VignetteControl, + 0x0080: ImageAdjustment, + 0x0081: ToneComp, + 0x0082: AuxiliaryLens, + 0x0083: LensType, + 0x0084: Lens, + 0x0085: FocusDistance, + 0x0086: DigitalZoom, + 0x0087: FlashMode, + 0x0088: Nikon_AFInfo, + 0x0089: ShootingMode, + 0x008a: AutoBracketRelease, + 0x008b: LensFStops, + 0x008c: ContrastCurve, + 0x008d: ColorHue, + 0x008f: SceneMode, + 0x0090: Nikon_LightSource, + 0x0091: Nikon_ShotInfo, + 0x0092: HueAdjustment, + 0x0093: NEFCompression, + 0x0094: Nikon_Saturation, + 0x0095: NoiseReduction, + 0x0096: LinearizationTable, + 0x0097: Nikon_ColorBalance, + 0x0098: Nikon_LensData, + 0x0099: RawImageCenter, + 0x009a: SensorPixelSize, + 0x009b: Nikon3_0x009b, + 0x009c: SceneAssist, + 0x009e: RetouchHistory, + 0x009f: Nikon3_0x009f, + 0x00a0: Nikon_SerialNO, + 0x00a2: ImageDataSize, + 0x00a3: Nikon3_0x00a3, + 0x00a5: ImageCount, + 0x00a6: DeletedImageCount, + 0x00a7: ShutterCount, + 0x00a8: Nikon_FlashInfo, + 0x00a9: ImageOptimization, + 0x00aa: SaturationText, + 0x00ab: VariProgram, + 0x00ac: ImageStabilization, + 0x00ad: AFResponse, + 0x00b0: Nikon_MultiExposure, + 0x00b1: HighISONoiseReduction, + 0x00b3: ToningEffect, + 0x00b7: Nikon_AFInfo2, + 0x00b8: Nikon_FileInfo, + 0x00b9: Nikon_AFTune, + 0x0e00: PrintIM, + 0x0e01: CaptureData, + 0x0e09: CaptureVersion, + 0x0e0e: CaptureOffsets, + 0x0e10: ScanIFD, + 0x0e1d: ICCProfile, + 0x0e1e: CaptureOutput, +} diff --git a/third_party/github.com/camlistore/goexif/mknote/mknote.go b/third_party/github.com/camlistore/goexif/mknote/mknote.go new file mode 100644 index 000000000..d0caf7ebd --- /dev/null +++ b/third_party/github.com/camlistore/goexif/mknote/mknote.go @@ -0,0 +1,70 @@ +// Package mknote provides makernote parsers that can be used with goexif/exif. +package mknote + +import ( + "bytes" + + "github.com/rwcarlsen/goexif/exif" + "github.com/rwcarlsen/goexif/tiff" +) + +var ( + // Canon is an exif.Parser for canon makernote data. + Canon = &canon{} + // NikonV3 is an exif.Parser for nikon makernote data. + NikonV3 = &nikonV3{} + // All is a list of all available makernote parsers + All = []exif.Parser{Canon, NikonV3} +) + +type canon struct{} + +// Parse decodes all Canon makernote data found in x and adds it to x. +func (_ *canon) Parse(x *exif.Exif) error { + m, err := x.Get(exif.MakerNote) + if err != nil { + return nil + } + + mk, err := x.Get(exif.Make) + if err != nil { + return nil + } + + if mk.StringVal() != "Canon" { + return nil + } + + // Canon notes are a single IFD directory with no header. + // Reader offsets need to be w.r.t. the original tiff structure. + buf := bytes.NewReader(append(make([]byte, m.ValOffset), m.Val...)) + buf.Seek(int64(m.ValOffset), 0) + + mkNotesDir, _, err := tiff.DecodeDir(buf, x.Tiff.Order) + if err != nil { + return err + } + x.LoadTags(mkNotesDir, makerNoteCanonFields, false) + return nil +} + +type nikonV3 struct{} + +// Parse decodes all Nikon makernote data found in x and adds it to x. +func (_ *nikonV3) Parse(x *exif.Exif) error { + m, err := x.Get(exif.MakerNote) + if err != nil { + return nil + } else if bytes.Compare(m.Val[:6], []byte("Nikon\000")) != 0 { + return nil + } + + // Nikon v3 maker note is a self-contained IFD (offsets are relative + // to the start of the maker note) + mkNotes, err := tiff.Decode(bytes.NewReader(m.Val[10:])) + if err != nil { + return err + } + x.LoadTags(mkNotes.Dirs[0], makerNoteNikon3Fields, false) + return nil +} diff --git a/third_party/github.com/camlistore/goexif/tiff/tag.go b/third_party/github.com/camlistore/goexif/tiff/tag.go index b2e87f2c6..0b6ddde6f 100644 --- a/third_party/github.com/camlistore/goexif/tiff/tag.go +++ b/third_party/github.com/camlistore/goexif/tiff/tag.go @@ -5,16 +5,19 @@ import ( "encoding/binary" "errors" "fmt" + "io" "math/big" "strings" "unicode" "unicode/utf8" ) -type FormatType int +// TypeCategory specifies the Go type equivalent used to represent the basic +// tiff data types. +type TypeCategory int const ( - IntVal FormatType = iota + IntVal TypeCategory = iota FloatVal RatVal StringVal @@ -22,32 +25,55 @@ const ( OtherVal ) -var fmtSize = map[uint16]uint32{ - 1: 1, - 2: 1, - 3: 2, - 4: 4, - 5: 8, - 6: 1, - 7: 1, - 8: 2, - 9: 4, - 10: 8, - 11: 4, - 12: 8, +// DataType represents the basic tiff tag data types. +type DataType uint16 + +const ( + DTByte DataType = 1 + DTAscii = 2 + DTShort = 3 + DTLong = 4 + DTRational = 5 + DTSByte = 6 + DTUndefined = 7 + DTSShort = 8 + DTSLong = 9 + DTSRational = 10 + DTFloat = 11 + DTDouble = 12 +) + +// typeSize specifies the size in bytes of each type. +var typeSize = map[DataType]uint32{ + DTByte: 1, + DTAscii: 1, + DTShort: 2, + DTLong: 4, + DTRational: 8, + DTSByte: 1, + DTUndefined: 1, + DTSShort: 2, + DTSLong: 4, + DTSRational: 8, + DTFloat: 4, + DTDouble: 8, } // Tag reflects the parsed content of a tiff IFD tag. type Tag struct { - // Id is the 2-byte tiff tag identifier + // Id is the 2-byte tiff tag identifier. Id uint16 - // Fmt is an integer (1 through 12) indicating the tag value's format. - Fmt uint16 - // Ncomp is the number of type Fmt stored in the tag's value (i.e. the tag's - // value is an array of type Fmt and length Ncomp). - Ncomp uint32 + // Type is an integer (1 through 12) indicating the tag value's data type. + Type DataType + // Count is the number of type Type stored in the tag's value (i.e. the + // tag's value is an array of type Type and length Count). + Count uint32 // Val holds the bytes that represent the tag's value. Val []byte + // ValOffset holds byte offset of the tag value w.r.t. the beginning of the + // reader it was decoded from. Zero if the tag value fit inside the offset + // field. + ValOffset uint32 order binary.ByteOrder @@ -57,10 +83,10 @@ type Tag struct { strVal string } -// DecodeTag parses a tiff-encoded IFD tag from r and returns Tag object. The +// DecodeTag parses a tiff-encoded IFD tag from r and returns a Tag object. The // first read from r should be the first byte of the tag. ReadAt offsets should -// be relative to the beginning of the tiff structure (not relative to the -// beginning of the tag). +// generally be relative to the beginning of the tiff structure (not relative +// to the beginning of the tag). func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) { t := new(Tag) t.order = order @@ -70,35 +96,32 @@ func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) { return nil, errors.New("tiff: tag id read failed: " + err.Error()) } - err = binary.Read(r, order, &t.Fmt) + err = binary.Read(r, order, &t.Type) if err != nil { - return nil, errors.New("tiff: tag format read failed: " + err.Error()) + return nil, errors.New("tiff: tag type read failed: " + err.Error()) } - err = binary.Read(r, order, &t.Ncomp) + err = binary.Read(r, order, &t.Count) if err != nil { return nil, errors.New("tiff: tag component count read failed: " + err.Error()) } - valLen := fmtSize[t.Fmt] * t.Ncomp - var offset uint32 + valLen := typeSize[t.Type] * t.Count if valLen > 4 { - binary.Read(r, order, &offset) + binary.Read(r, order, &t.ValOffset) t.Val = make([]byte, valLen) - n, err := r.ReadAt(t.Val, int64(offset)) + n, err := r.ReadAt(t.Val, int64(t.ValOffset)) if n != int(valLen) || err != nil { - return nil, errors.New("tiff: tag value read failed: " + err.Error()) + return t, errors.New("tiff: tag value read failed: " + err.Error()) } } else { val := make([]byte, valLen) - n, err := r.Read(val) - if err != nil || n != int(valLen) { - return nil, errors.New("tiff: tag offset read failed: " + err.Error()) + if _, err = io.ReadFull(r, val); err != nil { + return t, errors.New("tiff: tag offset read failed: " + err.Error()) } - - n, err = r.Read(make([]byte, 4-valLen)) - if err != nil || n != 4-int(valLen) { - return nil, errors.New("tiff: tag offset read failed: " + err.Error()) + // ignore padding. + if _, err = io.ReadFull(r, make([]byte, 4-valLen)); err != nil { + return t, errors.New("tiff: tag offset read failed: " + err.Error()) } t.Val = val @@ -112,62 +135,62 @@ func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) { func (t *Tag) convertVals() { r := bytes.NewReader(t.Val) - switch t.Fmt { - case 2: // ascii string + switch t.Type { + case DTAscii: if len(t.Val) > 0 { - t.strVal = string(t.Val[:len(t.Val)-1]) + t.strVal = string(t.Val[:len(t.Val)-1]) // ignore the last byte (NULL). } - case 1: + case DTByte: var v uint8 - t.intVals = make([]int64, int(t.Ncomp)) - for i := 0; i < int(t.Ncomp); i++ { + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { err := binary.Read(r, t.order, &v) panicOn(err) t.intVals[i] = int64(v) } - case 3: + case DTShort: var v uint16 - t.intVals = make([]int64, int(t.Ncomp)) - for i := 0; i < int(t.Ncomp); i++ { + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { err := binary.Read(r, t.order, &v) panicOn(err) t.intVals[i] = int64(v) } - case 4: + case DTLong: var v uint32 - t.intVals = make([]int64, int(t.Ncomp)) - for i := 0; i < int(t.Ncomp); i++ { + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { err := binary.Read(r, t.order, &v) panicOn(err) t.intVals[i] = int64(v) } - case 6: + case DTSByte: var v int8 - t.intVals = make([]int64, int(t.Ncomp)) - for i := 0; i < int(t.Ncomp); i++ { + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { err := binary.Read(r, t.order, &v) panicOn(err) t.intVals[i] = int64(v) } - case 8: + case DTSShort: var v int16 - t.intVals = make([]int64, int(t.Ncomp)) - for i := 0; i < int(t.Ncomp); i++ { + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { err := binary.Read(r, t.order, &v) panicOn(err) t.intVals[i] = int64(v) } - case 9: + case DTSLong: var v int32 - t.intVals = make([]int64, int(t.Ncomp)) - for i := 0; i < int(t.Ncomp); i++ { + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { err := binary.Read(r, t.order, &v) panicOn(err) t.intVals[i] = int64(v) } - case 5: // unsigned rational - t.ratVals = make([][]int64, int(t.Ncomp)) - for i := 0; i < int(t.Ncomp); i++ { + case DTRational: + t.ratVals = make([][]int64, int(t.Count)) + for i := range t.ratVals { var n, d uint32 err := binary.Read(r, t.order, &n) panicOn(err) @@ -175,9 +198,9 @@ func (t *Tag) convertVals() { panicOn(err) t.ratVals[i] = []int64{int64(n), int64(d)} } - case 10: // signed rational - t.ratVals = make([][]int64, int(t.Ncomp)) - for i := 0; i < int(t.Ncomp); i++ { + case DTSRational: + t.ratVals = make([][]int64, int(t.Count)) + for i := range t.ratVals { var n, d int32 err := binary.Read(r, t.order, &n) panicOn(err) @@ -185,17 +208,17 @@ func (t *Tag) convertVals() { panicOn(err) t.ratVals[i] = []int64{int64(n), int64(d)} } - case 11: // float32 - t.floatVals = make([]float64, int(t.Ncomp)) - for i := 0; i < int(t.Ncomp); i++ { + case DTFloat: // float32 + t.floatVals = make([]float64, int(t.Count)) + for i := range t.floatVals { var v float32 err := binary.Read(r, t.order, &v) panicOn(err) t.floatVals[i] = float64(v) } - case 12: // float64 (double) - t.floatVals = make([]float64, int(t.Ncomp)) - for i := 0; i < int(t.Ncomp); i++ { + case DTDouble: + t.floatVals = make([]float64, int(t.Count)) + for i := range t.floatVals { var u float64 err := binary.Read(r, t.order, &u) panicOn(err) @@ -204,65 +227,65 @@ func (t *Tag) convertVals() { } } -// Format returns a value indicating which method can be called to retrieve the +// 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) Format() FormatType { - switch t.Fmt { - case 1, 3, 4, 6, 8, 9: +func (t *Tag) TypeCategory() TypeCategory { + switch t.Type { + case DTByte, DTShort, DTLong, DTSByte, DTSShort, DTSLong: return IntVal - case 5, 10: + case DTRational, DTSRational: return RatVal - case 11, 12: + case DTFloat, DTDouble: return FloatVal - case 2: + case DTAscii: return StringVal - case 7: + case DTUndefined: return UndefVal } return OtherVal } -// Rat returns the tag's i'th value as a rational number. It panics if the tag format -// 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. +// 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) } // Rat2 returns the tag's i'th value as a rational number represented by a -// numerator-denominator pair. It panics if the tag format is not RatVal +// 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.Format() != RatVal { - panic("Tag format is not 'rational'") + if t.TypeCategory() != RatVal { + panic("Tag type category is not 'rational'") } return t.ratVals[i][0], t.ratVals[i][1] } -// Int returns the tag's i'th value as an integer. It panics if the tag format is not -// IntVal or if the tag value has no i'th component. +// 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.Format() != IntVal { - panic("Tag format is not 'int'") + if t.TypeCategory() != IntVal { + panic("Tag type category is not 'int'") } return t.intVals[i] } -// Float returns the tag's i'th value as a float. It panics if the tag format is not -// FloatVal or if the tag value has no i'th component. +// 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.Format() != FloatVal { - panic("Tag format is not 'float'") + if t.TypeCategory() != FloatVal { + panic("Tag type category is not 'float'") } return t.floatVals[i] } // StringVal returns the tag's value as a string. It panics if the tag -// format is not StringVal +// TypeCategory is not StringVal. func (t *Tag) StringVal() string { - if t.Format() != StringVal { - panic("Tag format is not 'ascii string'") + if t.TypeCategory() != StringVal { + panic("Tag type category is not 'ascii string'") } return t.strVal } @@ -276,17 +299,17 @@ func (t *Tag) String() string { } func (t *Tag) MarshalJSON() ([]byte, error) { - f := t.Format() + f := t.TypeCategory() switch f { case StringVal, UndefVal: return nullString(t.Val), nil case OtherVal: - panic(fmt.Sprintf("Unhandled type Fmt=%v", t.Fmt)) + panic(fmt.Sprintf("Unhandled tag type=%v", t.Type)) } rv := []string{} - for i := 0; i < int(t.Ncomp); i++ { + for i := 0; i < int(t.Count); i++ { switch f { case RatVal: n, d := t.Rat2(i) diff --git a/third_party/github.com/camlistore/goexif/tiff/tiff.go b/third_party/github.com/camlistore/goexif/tiff/tiff.go index 4affc687f..118aa39b6 100644 --- a/third_party/github.com/camlistore/goexif/tiff/tiff.go +++ b/third_party/github.com/camlistore/goexif/tiff/tiff.go @@ -1,10 +1,12 @@ -// Package tiff implements TIFF decoding as defined in TIFF 6.0 specification. +// Package tiff implements TIFF decoding as defined in TIFF 6.0 specification at +// http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf package tiff import ( "bytes" "encoding/binary" "errors" + "fmt" "io" "io/ioutil" ) @@ -15,7 +17,7 @@ type ReadAtReader interface { io.ReaderAt } -// Tiff provides access to decoded tiff data. +// Tiff provides access to a decoded tiff data structure. type Tiff struct { // Dirs is an ordered slice of the tiff's Image File Directories (IFDs). // The IFD at index 0 is IFD0. @@ -24,10 +26,10 @@ type Tiff struct { Order binary.ByteOrder } -// Decode parses tiff-encoded data from r and returns a Tiff that reflects the -// structure and content of the tiff data. The first read from r should be the -// first byte of the tiff-encoded data (not necessarily the first byte of an -// os.File object). +// Decode parses tiff-encoded data from r and returns a Tiff struct that +// reflects the structure and content of the tiff data. The first read from r +// should be the first byte of the tiff-encoded data and not necessarily the +// first byte of an os.File object. func Decode(r io.Reader) (*Tiff, error) { data, err := ioutil.ReadAll(r) if err != nil { @@ -39,23 +41,21 @@ func Decode(r io.Reader) (*Tiff, error) { // read byte order bo := make([]byte, 2) - n, err := buf.Read(bo) - if n < len(bo) || err != nil { + if _, err = io.ReadFull(buf, bo); err != nil { return nil, errors.New("tiff: could not read tiff byte order") + } + if string(bo) == "II" { + t.Order = binary.LittleEndian + } else if string(bo) == "MM" { + t.Order = binary.BigEndian } else { - if string(bo) == "II" { - t.Order = binary.LittleEndian - } else if string(bo) == "MM" { - t.Order = binary.BigEndian - } else { - return nil, errors.New("tiff: could not read tiff byte order") - } + return nil, errors.New("tiff: could not read tiff byte order") } // check for special tiff marker var sp int16 err = binary.Read(buf, t.Order, &sp) - if err != nil || 0x002A != sp { + if err != nil || 42 != sp { return nil, errors.New("tiff: could not find special tiff marker") } @@ -91,22 +91,24 @@ func Decode(r io.Reader) (*Tiff, error) { } func (tf *Tiff) String() string { - s := "Tiff{" + var buf bytes.Buffer + fmt.Fprint(&buf, "Tiff{") for _, d := range tf.Dirs { - s += d.String() + ", " + fmt.Fprintf(&buf, "%s, ", d.String()) } - return s + "}" + fmt.Fprintf(&buf, "}") + return buf.String() } -// Dir reflects the parsed content of a tiff Image File Directory (IFD). +// Dir provides access to the parsed content of a tiff Image File Directory (IFD). type Dir struct { Tags []*Tag } // DecodeDir parses a tiff-encoded IFD from r and returns a Dir object. offset // is the offset to the next IFD. The first read from r should be at the first -// byte of the IFD. ReadAt offsets should be relative to the beginning of the -// tiff structure (not relative to the beginning of the IFD). +// byte of the IFD. ReadAt offsets should generally be relative to the +// beginning of the tiff structure (not relative to the beginning of the IFD). func DecodeDir(r ReadAtReader, order binary.ByteOrder) (d *Dir, offset int32, err error) { d = new(Dir) @@ -114,7 +116,7 @@ func DecodeDir(r ReadAtReader, order binary.ByteOrder) (d *Dir, offset int32, er var nTags int16 err = binary.Read(r, order, &nTags) if err != nil { - return nil, 0, errors.New("tiff: falied to read IFD tag count: " + err.Error()) + return nil, 0, errors.New("tiff: failed to read IFD tag count: " + err.Error()) } // load tags diff --git a/third_party/github.com/camlistore/goexif/tiff/tiff_test.go b/third_party/github.com/camlistore/goexif/tiff/tiff_test.go index 2dd2982db..45c975ff1 100644 --- a/third_party/github.com/camlistore/goexif/tiff/tiff_test.go +++ b/third_party/github.com/camlistore/goexif/tiff/tiff_test.go @@ -4,10 +4,14 @@ import ( "bytes" "encoding/binary" "encoding/hex" + "flag" "os" + "path/filepath" "testing" ) +var dataDir = flag.String("test_data_dir", ".", "Directory where the data files for testing are located") + type input struct { tgId string tpe string @@ -17,10 +21,10 @@ type input struct { } type output struct { - id uint16 - format uint16 - count uint32 - val []byte + id uint16 + typ DataType + count uint32 + val []byte } type tagTest struct { @@ -38,97 +42,97 @@ var set1 = []tagTest{ // {"TgId", "TYPE", "N-VALUES", "OFFSET--", "VAL..."}, input{"0003", "0002", "00000002", "11000000", ""}, input{"0300", "0200", "02000000", "11000000", ""}, - output{0x0003, 0x0002, 0x0002, []byte{0x11, 0x00}}, + output{0x0003, DataType(0x0002), 0x0002, []byte{0x11, 0x00}}, }, tagTest{ input{"0001", "0002", "00000006", "00000012", "111213141516"}, input{"0100", "0200", "06000000", "12000000", "111213141516"}, - output{0x0001, 0x0002, 0x0006, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16}}, + output{0x0001, DataType(0x0002), 0x0006, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16}}, }, //////////// int (1-byte) type //////////////// tagTest{ input{"0001", "0001", "00000001", "11000000", ""}, input{"0100", "0100", "01000000", "11000000", ""}, - output{0x0001, 0x0001, 0x0001, []byte{0x11}}, + output{0x0001, DataType(0x0001), 0x0001, []byte{0x11}}, }, tagTest{ input{"0001", "0001", "00000005", "00000010", "1112131415"}, input{"0100", "0100", "05000000", "10000000", "1112131415"}, - output{0x0001, 0x0001, 0x0005, []byte{0x11, 0x12, 0x13, 0x14, 0x15}}, + output{0x0001, DataType(0x0001), 0x0005, []byte{0x11, 0x12, 0x13, 0x14, 0x15}}, }, tagTest{ input{"0001", "0006", "00000001", "11000000", ""}, input{"0100", "0600", "01000000", "11000000", ""}, - output{0x0001, 0x0006, 0x0001, []byte{0x11}}, + output{0x0001, DataType(0x0006), 0x0001, []byte{0x11}}, }, tagTest{ input{"0001", "0006", "00000005", "00000010", "1112131415"}, input{"0100", "0600", "05000000", "10000000", "1112131415"}, - output{0x0001, 0x0006, 0x0005, []byte{0x11, 0x12, 0x13, 0x14, 0x15}}, + output{0x0001, DataType(0x0006), 0x0005, []byte{0x11, 0x12, 0x13, 0x14, 0x15}}, }, //////////// int (2-byte) types //////////////// tagTest{ input{"0001", "0003", "00000002", "11111212", ""}, input{"0100", "0300", "02000000", "11111212", ""}, - output{0x0001, 0x0003, 0x0002, []byte{0x11, 0x11, 0x12, 0x12}}, + output{0x0001, DataType(0x0003), 0x0002, []byte{0x11, 0x11, 0x12, 0x12}}, }, tagTest{ input{"0001", "0003", "00000003", "00000010", "111213141516"}, input{"0100", "0300", "03000000", "10000000", "111213141516"}, - output{0x0001, 0x0003, 0x0003, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16}}, + output{0x0001, DataType(0x0003), 0x0003, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16}}, }, tagTest{ input{"0001", "0008", "00000001", "11120000", ""}, input{"0100", "0800", "01000000", "11120000", ""}, - output{0x0001, 0x0008, 0x0001, []byte{0x11, 0x12}}, + output{0x0001, DataType(0x0008), 0x0001, []byte{0x11, 0x12}}, }, tagTest{ input{"0001", "0008", "00000003", "00000100", "111213141516"}, input{"0100", "0800", "03000000", "00100000", "111213141516"}, - output{0x0001, 0x0008, 0x0003, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16}}, + output{0x0001, DataType(0x0008), 0x0003, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16}}, }, //////////// int (4-byte) types //////////////// tagTest{ input{"0001", "0004", "00000001", "11121314", ""}, input{"0100", "0400", "01000000", "11121314", ""}, - output{0x0001, 0x0004, 0x0001, []byte{0x11, 0x12, 0x13, 0x14}}, + output{0x0001, DataType(0x0004), 0x0001, []byte{0x11, 0x12, 0x13, 0x14}}, }, tagTest{ input{"0001", "0004", "00000002", "00000010", "1112131415161718"}, input{"0100", "0400", "02000000", "10000000", "1112131415161718"}, - output{0x0001, 0x0004, 0x0002, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}}, + output{0x0001, DataType(0x0004), 0x0002, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}}, }, tagTest{ input{"0001", "0009", "00000001", "11121314", ""}, input{"0100", "0900", "01000000", "11121314", ""}, - output{0x0001, 0x0009, 0x0001, []byte{0x11, 0x12, 0x13, 0x14}}, + output{0x0001, DataType(0x0009), 0x0001, []byte{0x11, 0x12, 0x13, 0x14}}, }, tagTest{ input{"0001", "0009", "00000002", "00000011", "1112131415161819"}, input{"0100", "0900", "02000000", "11000000", "1112131415161819"}, - output{0x0001, 0x0009, 0x0002, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19}}, + output{0x0001, DataType(0x0009), 0x0002, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19}}, }, //////////// rational types //////////////////// tagTest{ input{"0001", "0005", "00000001", "00000010", "1112131415161718"}, input{"0100", "0500", "01000000", "10000000", "1112131415161718"}, - output{0x0001, 0x0005, 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}}, + output{0x0001, DataType(0x0005), 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}}, }, tagTest{ input{"0001", "000A", "00000001", "00000011", "1112131415161819"}, input{"0100", "0A00", "01000000", "11000000", "1112131415161819"}, - output{0x0001, 0x000A, 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19}}, + output{0x0001, DataType(0x000A), 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19}}, }, //////////// float types /////////////////////// tagTest{ input{"0001", "0005", "00000001", "00000010", "1112131415161718"}, input{"0100", "0500", "01000000", "10000000", "1112131415161718"}, - output{0x0001, 0x0005, 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}}, + output{0x0001, DataType(0x0005), 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}}, }, tagTest{ input{"0101", "000A", "00000001", "00000011", "1112131415161819"}, input{"0101", "0A00", "01000000", "11000000", "1112131415161819"}, - output{0x0101, 0x000A, 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19}}, + output{0x0101, DataType(0x000A), 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19}}, }, } @@ -151,11 +155,11 @@ func testSingle(t *testing.T, order binary.ByteOrder, in input, out output, i in if tg.Id != out.id { t.Errorf("(%v) tag %v id decode: expected %v, got %v", order, i, out.id, tg.Id) } - if tg.Fmt != out.format { - t.Errorf("(%v) tag %v format decode: expected %v, got %v", order, i, out.format, tg.Fmt) + if tg.Type != out.typ { + t.Errorf("(%v) tag %v type decode: expected %v, got %v", order, i, out.typ, tg.Type) } - if tg.Ncomp != out.count { - t.Errorf("(%v) tag %v N-components decode: expected %v, got %v", order, i, out.count, tg.Ncomp) + if tg.Count != out.count { + t.Errorf("(%v) tag %v component count decode: expected %v, got %v", order, i, out.count, tg.Count) } if !bytes.Equal(tg.Val, out.val) { t.Errorf("(%v) tag %v value decode: expected %v, got %v", order, i, out.val, tg.Val) @@ -188,7 +192,7 @@ func buildInput(in input, order binary.ByteOrder) []byte { } func TestDecode(t *testing.T) { - name := "sample1.tif" + name := filepath.Join(*dataDir, "sample1.tif") f, err := os.Open(name) if err != nil { t.Fatalf("%v\n", err) @@ -212,7 +216,7 @@ func TestDecodeTag_blob(t *testing.T) { t.Logf("tag: %v+\n", tg) n, d := tg.Rat2(0) - t.Logf("tag rat val: %v\n", n, d) + t.Logf("tag rat val: %v/%v\n", n, d) } func data() []byte {