mirror of https://github.com/perkeep/perkeep.git
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
This commit is contained in:
parent
108e16510f
commit
f0d9c04bc2
|
@ -434,7 +434,7 @@ func (ix *Index) populateFile(fetcher blob.Fetcher, b *schema.Blob, mm *mutation
|
||||||
}
|
}
|
||||||
|
|
||||||
func tagFormatString(tag *tiff.Tag) string {
|
func tagFormatString(tag *tiff.Tag) string {
|
||||||
switch tag.Format() {
|
switch tag.TypeCategory() {
|
||||||
case tiff.IntVal:
|
case tiff.IntVal:
|
||||||
return "int"
|
return "int"
|
||||||
case tiff.RatVal:
|
case tiff.RatVal:
|
||||||
|
@ -472,13 +472,13 @@ func indexEXIF(wholeRef blob.Ref, header []byte, mm *mutationMap) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
key := keyEXIFTag.Key(wholeRef, fmt.Sprintf("%04x", tag.Id))
|
key := keyEXIFTag.Key(wholeRef, fmt.Sprintf("%04x", tag.Id))
|
||||||
numComp := int(tag.Ncomp)
|
numComp := int(tag.Count)
|
||||||
if tag.Format() == tiff.StringVal {
|
if tag.TypeCategory() == tiff.StringVal {
|
||||||
numComp = 1
|
numComp = 1
|
||||||
}
|
}
|
||||||
var val bytes.Buffer
|
var val bytes.Buffer
|
||||||
val.WriteString(keyEXIFTag.Val(tagFmt, numComp, ""))
|
val.WriteString(keyEXIFTag.Val(tagFmt, numComp, ""))
|
||||||
if tag.Format() == tiff.StringVal {
|
if tag.TypeCategory() == tiff.StringVal {
|
||||||
str := tag.StringVal()
|
str := tag.StringVal()
|
||||||
if containsUnsafeRawStrByte(str) {
|
if containsUnsafeRawStrByte(str) {
|
||||||
val.WriteString(urle(str))
|
val.WriteString(urle(str))
|
||||||
|
@ -486,7 +486,7 @@ func indexEXIF(wholeRef blob.Ref, header []byte, mm *mutationMap) {
|
||||||
val.WriteString(str)
|
val.WriteString(str)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for i := 0; i < int(tag.Ncomp); i++ {
|
for i := 0; i < int(tag.Count); i++ {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
val.WriteByte('|')
|
val.WriteByte('|')
|
||||||
}
|
}
|
||||||
|
|
|
@ -977,7 +977,7 @@ func exifDateTimeInLocation(x *exif.Exif, loc *time.Location) (time.Time, error)
|
||||||
return time.Time{}, err
|
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")
|
return time.Time{}, errors.New("DateTime[Original] not in string format")
|
||||||
}
|
}
|
||||||
const exifTimeLayout = "2006:01:02 15:04:05"
|
const exifTimeLayout = "2006:01:02 15:04:05"
|
||||||
|
|
|
@ -1,82 +1,78 @@
|
||||||
Changing code under third_party/github.com/camlistore/goexif
|
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
|
These instructions assume you have a github.com account. Further it is assumed
|
||||||
----------------------------------------------------------------
|
that your camlistore copy is at $GOPATH/src/camlistore.org
|
||||||
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.
|
|
||||||
|
|
||||||
Sync github.com/camlistore/goexif -> camlistore.org/third_party/github.com/camlistore/goexif
|
1. Checkout the github.com/rwcarlsen/goexif by calling
|
||||||
--------------------------------------------------------------------------------------------
|
$ go get -u github.com/rwcarlsen/goexif
|
||||||
1. Once someone on the camlistore team merges in the latest from upstream,
|
$ cd $GOPATH/src/github.com/rwcarlsen/goexif
|
||||||
checkout a local copy:
|
|
||||||
|
|
||||||
$ git clone https://github.com/camlistore/goexif
|
2. Use git show to determine the revision.
|
||||||
$ cd goexif
|
$ git show commit cf045e9d6ba052fd348f82394d364cca4937589a
|
||||||
|
|
||||||
2. Make a patch to apply to the camlistore.org copy. You'll need to know the
|
3. Start merging using your favorite tools (vimdiff, meld, kdiff3, etc.)
|
||||||
git rev of github.com/camlistore/goexif that was last merged to
|
|
||||||
camlistore.org/third_party, for this example we'll use 030a4566:
|
|
||||||
|
|
||||||
# Create individual patches that have been applied upstream.
|
4. For the moment make sure to rewrite any new goexif import paths. For
|
||||||
$ git format-patch -o /tmp/patches 030a4566
|
example,
|
||||||
$ 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/*
|
|
||||||
|
|
||||||
# If something fails to apply try:
|
import "github.com/rwcarlsen/goexif/tiff"
|
||||||
$ git apply <PATCH THAT FAILED> --reject
|
|
||||||
$ edit edit edit
|
|
||||||
$ git add <ANY FIXED FILES>
|
|
||||||
$ git am --resolved
|
|
||||||
|
|
||||||
# If it is a patch camlistore already had, because we created it and
|
needs to be rewritten to
|
||||||
# pushed it upstream, you can skip it with:
|
|
||||||
$ git am --skip
|
|
||||||
|
|
||||||
# Now create a new branch to squash all the changes into one. Keeping a
|
import "camlistore.org/third_party/github.com/camlistore/goexif/tiff"
|
||||||
# record of upstream commits in the default commit message of a single
|
|
||||||
# commit.
|
|
||||||
$ git checkout -b patches_squashed master
|
|
||||||
$ git merge --squash patches_individual
|
|
||||||
|
|
||||||
# Verify no new files have been added that require import path updating:
|
NOTE: Currently, you cannot use rewrite-imports.sh. This is going to change
|
||||||
$ cd /path/to/camlistore.org/third_party/
|
in the future, as "third_party/github.com/camlistore/goexif/" is going to
|
||||||
$ ./rewrite-imports.sh -l
|
be renamed to "third_party/github.com/rwcarlsen/goexif/".
|
||||||
# If any rewrites are required, run:
|
|
||||||
$ ./rewrite-imports.sh -w
|
|
||||||
|
|
||||||
# Now create a commit that will be sent for review.
|
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
|
$ git commit -v
|
||||||
|
|
||||||
# Enter your commit message on the first line per usual.
|
# Enter your commit message on the first line per usual.
|
||||||
# You should see summaries of all the changes merged in the commit
|
# Add the goexif revision from step 1. to the commit message as well
|
||||||
# message. Leave these, they will be useful next sync as we'll know what
|
# as the commit messages of any noteworthy commits from upstream.
|
||||||
# commits were sync'd. Send the change for review.
|
$ devcam review
|
||||||
$ ./misc/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
|
Syncing Camlistore contributions back to upstream 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
|
1. Use a merge tool like meld, kdiff3, or similar to manually merge the code.
|
||||||
to rwcarlsen/goexif go1 branch, and the head fork as camlistore/goexif go1
|
$ meld $GOPATH/src/camlistore.org/third_party/github.com/camlistore/goexif \
|
||||||
branch.
|
$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.
|
|
||||||
|
|
|
@ -4,18 +4,20 @@ goexif
|
||||||
Provides decoding of basic exif and tiff encoded data. Still in alpha - no guarantees.
|
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"
|
Suggestions and pull requests are welcome. Functionality is split into two packages - "exif" and "tiff"
|
||||||
The exif package depends on the tiff package.
|
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:
|
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:
|
Or if you just want the tiff package:
|
||||||
|
|
||||||
```
|
```
|
||||||
go get github.com/camlistore/goexif/tiff
|
go get github.com/rwcarlsen/goexif/tiff
|
||||||
```
|
```
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
@ -28,7 +30,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/camlistore/goexif/exif"
|
"github.com/rwcarlsen/goexif/exif"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -40,17 +42,17 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
x, err := exif.Decode(f)
|
x, err := exif.Decode(f)
|
||||||
f.Close()
|
defer f.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
camModel, _ := x.Get("Model")
|
camModel, _ := x.Get(exif.Model)
|
||||||
date, _ := x.Get("DateTimeOriginal")
|
date, _ := x.Get(exif.DateTimeOriginal)
|
||||||
fmt.Println(camModel.StringVal())
|
fmt.Println(camModel.StringVal())
|
||||||
fmt.Println(date.StringVal())
|
fmt.Println(date.StringVal())
|
||||||
|
|
||||||
focal, _ := x.Get("FocalLength")
|
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)
|
fmt.Printf("%v/%v", numer, denom)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -21,12 +21,12 @@ func ExampleDecode() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
camModel, _ := x.Get("Model")
|
camModel, _ := x.Get(exif.Model)
|
||||||
date, _ := x.Get("DateTimeOriginal")
|
date, _ := x.Get(exif.DateTimeOriginal)
|
||||||
fmt.Println(camModel.StringVal())
|
fmt.Println(camModel.StringVal())
|
||||||
fmt.Println(date.StringVal())
|
fmt.Println(date.StringVal())
|
||||||
|
|
||||||
focal, _ := x.Get("FocalLength")
|
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)
|
fmt.Printf("%v/%v", numer, denom)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Package exif implements decoding of EXIF data as defined in the EXIF 2.2
|
// 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
|
package exif
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -10,28 +10,16 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"camlistore.org/third_party/github.com/camlistore/goexif/tiff"
|
"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 (
|
const (
|
||||||
|
jpeg_APP1 = 0xE1
|
||||||
|
|
||||||
exifPointer = 0x8769
|
exifPointer = 0x8769
|
||||||
gpsPointer = 0x8825
|
gpsPointer = 0x8825
|
||||||
interopPointer = 0xA005
|
interopPointer = 0xA005
|
||||||
|
@ -45,32 +33,95 @@ func (tag TagNotPresentError) Error() string {
|
||||||
return fmt.Sprintf("exif: tag %q is not present", string(tag))
|
return fmt.Sprintf("exif: tag %q is not present", string(tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
func isTagNotPresentErr(err error) bool {
|
// Parser allows the registration of custom parsing and field loading
|
||||||
_, ok := err.(TagNotPresentError)
|
// in the Decode function.
|
||||||
return ok
|
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 {
|
type Exif struct {
|
||||||
tif *tiff.Tiff
|
Tiff *tiff.Tiff
|
||||||
|
|
||||||
main map[FieldName]*tiff.Tag
|
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) {
|
func Decode(r io.Reader) (*Exif, error) {
|
||||||
// EXIF data in JPEG is stored in the APP1 marker. EXIF data uses the TIFF
|
// EXIF data in JPEG is stored in the APP1 marker. EXIF data uses the TIFF
|
||||||
// format to store data.
|
// 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 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
|
// If we're parsing a JPEG image, we need to strip away the JPEG APP1
|
||||||
// marker and also the EXIF header.
|
// marker and also the EXIF header.
|
||||||
|
|
||||||
header := make([]byte, 4)
|
header := make([]byte, 4)
|
||||||
n, err := r.Read(header)
|
n, err := r.Read(header)
|
||||||
if n < len(header) {
|
|
||||||
return nil, errors.New("exif: short read on header")
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if n < len(header) {
|
||||||
|
return nil, errors.New("exif: short read on header")
|
||||||
|
}
|
||||||
|
|
||||||
var isTiff bool
|
var isTiff bool
|
||||||
switch string(header) {
|
switch string(header) {
|
||||||
|
@ -100,9 +151,9 @@ func Decode(r io.Reader) (*Exif, error) {
|
||||||
tif, err = tiff.Decode(tr)
|
tif, err = tiff.Decode(tr)
|
||||||
er = bytes.NewReader(b.Bytes())
|
er = bytes.NewReader(b.Bytes())
|
||||||
} else {
|
} else {
|
||||||
// Strip away JPEG APP1 header.
|
// Locate the JPEG APP1 header.
|
||||||
var sec *appSec
|
var sec *appSec
|
||||||
sec, err = newAppSec(0xE1, r)
|
sec, err = newAppSec(jpeg_APP1, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -115,55 +166,47 @@ func Decode(r io.Reader) (*Exif, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
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
|
// build an exif structure from the tiff
|
||||||
x := &Exif{
|
x := &Exif{
|
||||||
main: map[FieldName]*tiff.Tag{},
|
main: map[FieldName]*tiff.Tag{},
|
||||||
tif: tif,
|
Tiff: tif,
|
||||||
|
Raw: raw,
|
||||||
}
|
}
|
||||||
|
|
||||||
ifd0 := tif.Dirs[0]
|
for i, p := range parsers {
|
||||||
for _, tag := range ifd0.Tags {
|
if err := p.Parse(x); err != nil {
|
||||||
name := exifFields[tag.Id]
|
return x, fmt.Errorf("exif: parser %v failed (%v)", i, err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return x, nil
|
return x, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Exif) loadSubDir(r *bytes.Reader, ptrName FieldName, fieldMap map[uint16]FieldName) error {
|
// LoadTags loads tags into the available fields from the tiff Directory
|
||||||
tag, ok := x.main[ptrName]
|
// using the given tagid-fieldname mapping. Used to load makernote and
|
||||||
if !ok {
|
// other meta-data. If showMissing is true, tags in d that are not in the
|
||||||
return nil
|
// fieldMap will be loaded with the FieldName UnknownPrefix followed by the
|
||||||
}
|
// tag ID (in hex format).
|
||||||
offset := tag.Int(0)
|
func (x *Exif) LoadTags(d *tiff.Dir, fieldMap map[uint16]FieldName, showMissing bool) {
|
||||||
|
for _, tag := range d.Tags {
|
||||||
_, 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 {
|
|
||||||
name := fieldMap[tag.Id]
|
name := fieldMap[tag.Id]
|
||||||
|
if name == "" {
|
||||||
|
if !showMissing {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name = FieldName(fmt.Sprintf("%v%x", UnknownPrefix, tag.Id))
|
||||||
|
}
|
||||||
x.main[name] = tag
|
x.main[name] = tag
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves the EXIF tag for the given field name.
|
// 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
|
// 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.
|
// tag name is known, the error will be a TagNotPresentError.
|
||||||
func (x *Exif) Get(name FieldName) (*tiff.Tag, error) {
|
func (x *Exif) Get(name FieldName) (*tiff.Tag, error) {
|
||||||
if !validField[name] {
|
if tg, ok := x.main[name]; ok {
|
||||||
return nil, fmt.Errorf("exif: invalid tag name %q", name)
|
|
||||||
} else if tg, ok := x.main[name]; ok {
|
|
||||||
return tg, nil
|
return tg, nil
|
||||||
}
|
}
|
||||||
return nil, TagNotPresentError(name)
|
return nil, TagNotPresentError(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walker is the interface used to traverse all exif fields of an Exif object.
|
// Walker is the interface used to traverse all fields of an Exif object.
|
||||||
// Returning a non-nil error aborts the walk/traversal.
|
|
||||||
type Walker interface {
|
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(name FieldName, tag *tiff.Tag) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk calls the Walk method of w with the name and tag for every non-nil exif
|
// Walk calls the Walk method of w with the name and tag for every non-nil
|
||||||
// field.
|
// EXIF field. If w aborts the walk with an error, that error is returned.
|
||||||
func (x *Exif) Walk(w Walker) error {
|
func (x *Exif) Walk(w Walker) error {
|
||||||
for name, tag := range x.main {
|
for name, tag := range x.main {
|
||||||
if err := w.Walk(name, tag); err != nil {
|
if err := w.Walk(name, tag); err != nil {
|
||||||
|
@ -214,7 +256,7 @@ func (x *Exif) DateTime() (time.Time, error) {
|
||||||
return dt, err
|
return dt, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tag.Format() != tiff.StringVal {
|
if tag.TypeCategory() != tiff.StringVal {
|
||||||
return dt, errors.New("DateTime[Original] not in string format")
|
return dt, errors.New("DateTime[Original] not in string format")
|
||||||
}
|
}
|
||||||
exifTimeLayout := "2006:01:02 15:04:05"
|
exifTimeLayout := "2006:01:02 15:04:05"
|
||||||
|
@ -271,6 +313,22 @@ func (x *Exif) String() string {
|
||||||
return buf.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) {
|
func (x Exif) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(x.main)
|
return json.Marshal(x.main)
|
||||||
}
|
}
|
||||||
|
@ -285,7 +343,7 @@ type appSec struct {
|
||||||
func newAppSec(marker byte, r io.Reader) (*appSec, error) {
|
func newAppSec(marker byte, r io.Reader) (*appSec, error) {
|
||||||
br := bufio.NewReader(r)
|
br := bufio.NewReader(r)
|
||||||
app := &appSec{marker: marker}
|
app := &appSec{marker: marker}
|
||||||
var dataLen uint16
|
var dataLen int
|
||||||
|
|
||||||
// seek to marker
|
// seek to marker
|
||||||
for dataLen == 0 {
|
for dataLen == 0 {
|
||||||
|
@ -303,16 +361,16 @@ func newAppSec(marker byte, r io.Reader) (*appSec, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dataLen = binary.BigEndian.Uint16(dataLenBytes)
|
dataLen = int(binary.BigEndian.Uint16(dataLenBytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// read section data
|
// read section data
|
||||||
nread := 0
|
nread := 0
|
||||||
for nread < int(dataLen) {
|
for nread < dataLen {
|
||||||
s := make([]byte, int(dataLen)-nread)
|
s := make([]byte, dataLen-nread)
|
||||||
n, err := br.Read(s)
|
n, err := br.Read(s)
|
||||||
nread += n
|
nread += n
|
||||||
if err != nil && nread < int(dataLen) {
|
if err != nil && nread < dataLen {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
app.data = append(app.data, s[:n]...)
|
app.data = append(app.data, s[:n]...)
|
||||||
|
|
|
@ -1,62 +1,142 @@
|
||||||
package exif
|
package exif
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"camlistore.org/third_party/github.com/camlistore/goexif/tiff"
|
"camlistore.org/third_party/github.com/camlistore/goexif/tiff"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecode(t *testing.T) {
|
// switch to true to regenerate regression expected values
|
||||||
name := "sample1.jpg"
|
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)
|
f, err := os.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%v\n", err)
|
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)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not open sample directory '%s': %v", fpath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
names, err := f.Readdirnames(0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not read sample directory '%s': %v", fpath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
x, err := Decode(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
} else if x == nil {
|
||||||
if x == nil {
|
t.Fatalf("No error and yet %v was not decoded", name)
|
||||||
t.Fatalf("No error and yet %v was not decoded\n", name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err := x.Get("Model")
|
x.Walk(&walker{name, t})
|
||||||
t.Logf("Model: %v", val)
|
cnt++
|
||||||
t.Log(x)
|
}
|
||||||
|
if cnt != len(regressExpected) {
|
||||||
|
t.Errorf("Did not process enough samples, got %d, want %d", cnt, len(regressExpected))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type walker struct {
|
type walker struct {
|
||||||
|
picName string
|
||||||
t *testing.T
|
t *testing.T
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *walker) Walk(name FieldName, tag *tiff.Tag) error {
|
func (w *walker) Walk(field FieldName, tag *tiff.Tag) error {
|
||||||
w.t.Logf("%v: %v", name, tag)
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWalk(t *testing.T) {
|
|
||||||
name := "sample1.jpg"
|
|
||||||
f, err := os.Open(name)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
x, err := Decode(f)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if x == nil {
|
|
||||||
t.Fatal("bad err")
|
|
||||||
}
|
|
||||||
|
|
||||||
x.Walk(&walker{t})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarshal(t *testing.T) {
|
func TestMarshal(t *testing.T) {
|
||||||
name := "sample1.jpg"
|
name := filepath.Join(*dataDir, "sample1.jpg")
|
||||||
f, err := os.Open(name)
|
f, err := os.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%v\n", err)
|
t.Fatalf("%v\n", err)
|
||||||
|
|
|
@ -2,18 +2,137 @@ package exif
|
||||||
|
|
||||||
type FieldName string
|
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 (
|
const (
|
||||||
ImageWidth FieldName = "ImageWidth"
|
ImageWidth FieldName = "ImageWidth"
|
||||||
ImageLength FieldName = "ImageLength" // height
|
ImageLength = "ImageLength" // Image height called Length by EXIF spec
|
||||||
Orientation FieldName = "Orientation"
|
BitsPerSample = "BitsPerSample"
|
||||||
DateTime FieldName = "DateTime"
|
Compression = "Compression"
|
||||||
DateTimeOriginal FieldName = "DateTimeOriginal"
|
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 (
|
const (
|
||||||
exifIFDPointer FieldName = "ExifIFDPointer"
|
ThumbJPEGInterchangeFormat = "ThumbJPEGInterchangeFormat" // offset to thumb jpeg SOI
|
||||||
gpsInfoIFDPointer = "GPSInfoIFDPointer"
|
ThumbJPEGInterchangeFormatLength = "ThumbJPEGInterchangeFormatLength" // byte length of thumb
|
||||||
interoperabilityIFDPointer = "InteroperabilityIFDPointer"
|
)
|
||||||
|
|
||||||
|
// 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{
|
var exifFields = map[uint16]FieldName{
|
||||||
|
@ -21,145 +140,150 @@ var exifFields = map[uint16]FieldName{
|
||||||
////////// IFD 0 ////////////////////
|
////////// IFD 0 ////////////////////
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
|
||||||
// image data structure
|
// image data structure for the thumbnail
|
||||||
0x0100: "ImageWidth",
|
0x0100: ImageWidth,
|
||||||
0x0101: "ImageLength",
|
0x0101: ImageLength,
|
||||||
0x0102: "BitsPerSample",
|
0x0102: BitsPerSample,
|
||||||
0x0103: "Compression",
|
0x0103: Compression,
|
||||||
0x0106: "PhotometricInterpretation",
|
0x0106: PhotometricInterpretation,
|
||||||
0x0112: "Orientation",
|
0x0112: Orientation,
|
||||||
0x0115: "SamplesPerPixel",
|
0x0115: SamplesPerPixel,
|
||||||
0x011C: "PlanarConfiguration",
|
0x011C: PlanarConfiguration,
|
||||||
0x0212: "YCbCrSubSampling",
|
0x0212: YCbCrSubSampling,
|
||||||
0x0213: "YCbCrPositioning",
|
0x0213: YCbCrPositioning,
|
||||||
0x011A: "XResolution",
|
0x011A: XResolution,
|
||||||
0x011B: "YResolution",
|
0x011B: YResolution,
|
||||||
0x0128: "ResolutionUnit",
|
0x0128: ResolutionUnit,
|
||||||
|
|
||||||
// Other tags
|
// Other tags
|
||||||
0x0132: "DateTime",
|
0x0132: DateTime,
|
||||||
0x010E: "ImageDescription",
|
0x010E: ImageDescription,
|
||||||
0x010F: "Make",
|
0x010F: Make,
|
||||||
0x0110: "Model",
|
0x0110: Model,
|
||||||
0x0131: "Software",
|
0x0131: Software,
|
||||||
0x013B: "Artist",
|
0x013B: Artist,
|
||||||
0x8298: "Copyright",
|
0x8298: Copyright,
|
||||||
|
|
||||||
// private tags
|
// private tags
|
||||||
exifPointer: "ExifIFDPointer",
|
exifPointer: ExifIFDPointer,
|
||||||
|
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
////////// Exif sub IFD /////////////
|
////////// Exif sub IFD /////////////
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
|
|
||||||
gpsPointer: "GPSInfoIFDPointer",
|
gpsPointer: GPSInfoIFDPointer,
|
||||||
interopPointer: "InteroperabilityIFDPointer",
|
interopPointer: InteroperabilityIFDPointer,
|
||||||
|
|
||||||
0x9000: "ExifVersion",
|
0x9000: ExifVersion,
|
||||||
0xA000: "FlashpixVersion",
|
0xA000: FlashpixVersion,
|
||||||
|
|
||||||
0xA001: "ColorSpace",
|
0xA001: ColorSpace,
|
||||||
|
|
||||||
0x9101: "ComponentsConfiguration",
|
0x9101: ComponentsConfiguration,
|
||||||
0x9102: "CompressedBitsPerPixel",
|
0x9102: CompressedBitsPerPixel,
|
||||||
0xA002: "PixelXDimension",
|
0xA002: PixelXDimension,
|
||||||
0xA003: "PixelYDimension",
|
0xA003: PixelYDimension,
|
||||||
|
|
||||||
0x927C: "MakerNote",
|
0x927C: MakerNote,
|
||||||
0x9286: "UserComment",
|
0x9286: UserComment,
|
||||||
|
|
||||||
0xA004: "RelatedSoundFile",
|
0xA004: RelatedSoundFile,
|
||||||
0x9003: "DateTimeOriginal",
|
0x9003: DateTimeOriginal,
|
||||||
0x9004: "DateTimeDigitized",
|
0x9004: DateTimeDigitized,
|
||||||
0x9290: "SubSecTime",
|
0x9290: SubSecTime,
|
||||||
0x9291: "SubSecTimeOriginal",
|
0x9291: SubSecTimeOriginal,
|
||||||
0x9292: "SubSecTimeDigitized",
|
0x9292: SubSecTimeDigitized,
|
||||||
|
|
||||||
0xA420: "ImageUniqueID",
|
0xA420: ImageUniqueID,
|
||||||
|
|
||||||
// picture conditions
|
// picture conditions
|
||||||
0x829A: "ExposureTime",
|
0x829A: ExposureTime,
|
||||||
0x829D: "FNumber",
|
0x829D: FNumber,
|
||||||
0x8822: "ExposureProgram",
|
0x8822: ExposureProgram,
|
||||||
0x8824: "SpectralSensitivity",
|
0x8824: SpectralSensitivity,
|
||||||
0x8827: "ISOSpeedRatings",
|
0x8827: ISOSpeedRatings,
|
||||||
0x8828: "OECF",
|
0x8828: OECF,
|
||||||
0x9201: "ShutterSpeedValue",
|
0x9201: ShutterSpeedValue,
|
||||||
0x9202: "ApertureValue",
|
0x9202: ApertureValue,
|
||||||
0x9203: "BrightnessValue",
|
0x9203: BrightnessValue,
|
||||||
0x9204: "ExposureBiasValue",
|
0x9204: ExposureBiasValue,
|
||||||
0x9205: "MaxApertureValue",
|
0x9205: MaxApertureValue,
|
||||||
0x9206: "SubjectDistance",
|
0x9206: SubjectDistance,
|
||||||
0x9207: "MeteringMode",
|
0x9207: MeteringMode,
|
||||||
0x9208: "LightSource",
|
0x9208: LightSource,
|
||||||
0x9209: "Flash",
|
0x9209: Flash,
|
||||||
0x920A: "FocalLength",
|
0x920A: FocalLength,
|
||||||
0x9214: "SubjectArea",
|
0x9214: SubjectArea,
|
||||||
0xA20B: "FlashEnergy",
|
0xA20B: FlashEnergy,
|
||||||
0xA20C: "SpatialFrequencyResponse",
|
0xA20C: SpatialFrequencyResponse,
|
||||||
0xA20E: "FocalPlaneXResolution",
|
0xA20E: FocalPlaneXResolution,
|
||||||
0xA20F: "FocalPlaneYResolution",
|
0xA20F: FocalPlaneYResolution,
|
||||||
0xA210: "FocalPlaneResolutionUnit",
|
0xA210: FocalPlaneResolutionUnit,
|
||||||
0xA214: "SubjectLocation",
|
0xA214: SubjectLocation,
|
||||||
0xA215: "ExposureIndex",
|
0xA215: ExposureIndex,
|
||||||
0xA217: "SensingMethod",
|
0xA217: SensingMethod,
|
||||||
0xA300: "FileSource",
|
0xA300: FileSource,
|
||||||
0xA301: "SceneType",
|
0xA301: SceneType,
|
||||||
0xA302: "CFAPattern",
|
0xA302: CFAPattern,
|
||||||
0xA401: "CustomRendered",
|
0xA401: CustomRendered,
|
||||||
0xA402: "ExposureMode",
|
0xA402: ExposureMode,
|
||||||
0xA403: "WhiteBalance",
|
0xA403: WhiteBalance,
|
||||||
0xA404: "DigitalZoomRatio",
|
0xA404: DigitalZoomRatio,
|
||||||
0xA405: "FocalLengthIn35mmFilm",
|
0xA405: FocalLengthIn35mmFilm,
|
||||||
0xA406: "SceneCaptureType",
|
0xA406: SceneCaptureType,
|
||||||
0xA407: "GainControl",
|
0xA407: GainControl,
|
||||||
0xA408: "Contrast",
|
0xA408: Contrast,
|
||||||
0xA409: "Saturation",
|
0xA409: Saturation,
|
||||||
0xA40A: "Sharpness",
|
0xA40A: Sharpness,
|
||||||
0xA40B: "DeviceSettingDescription",
|
0xA40B: DeviceSettingDescription,
|
||||||
0xA40C: "SubjectDistanceRange",
|
0xA40C: SubjectDistanceRange,
|
||||||
}
|
}
|
||||||
|
|
||||||
var gpsFields = map[uint16]FieldName{
|
var gpsFields = map[uint16]FieldName{
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
//// GPS sub-IFD ////////////////////
|
//// GPS sub-IFD ////////////////////
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
0x0: "GPSVersionID",
|
0x0: GPSVersionID,
|
||||||
0x1: "GPSLatitudeRef",
|
0x1: GPSLatitudeRef,
|
||||||
0x2: "GPSLatitude",
|
0x2: GPSLatitude,
|
||||||
0x3: "GPSLongitudeRef",
|
0x3: GPSLongitudeRef,
|
||||||
0x4: "GPSLongitude",
|
0x4: GPSLongitude,
|
||||||
0x5: "GPSAltitudeRef",
|
0x5: GPSAltitudeRef,
|
||||||
0x6: "GPSAltitude",
|
0x6: GPSAltitude,
|
||||||
0x7: "GPSTimeStamp",
|
0x7: GPSTimeStamp,
|
||||||
0x8: "GPSSatelites",
|
0x8: GPSSatelites,
|
||||||
0x9: "GPSStatus",
|
0x9: GPSStatus,
|
||||||
0xA: "GPSMeasureMode",
|
0xA: GPSMeasureMode,
|
||||||
0xB: "GPSDOP",
|
0xB: GPSDOP,
|
||||||
0xC: "GPSSpeedRef",
|
0xC: GPSSpeedRef,
|
||||||
0xD: "GPSSpeed",
|
0xD: GPSSpeed,
|
||||||
0xE: "GPSTrackRef",
|
0xE: GPSTrackRef,
|
||||||
0xF: "GPSTrack",
|
0xF: GPSTrack,
|
||||||
0x10: "GPSImgDirectionRef",
|
0x10: GPSImgDirectionRef,
|
||||||
0x11: "GPSImgDirection",
|
0x11: GPSImgDirection,
|
||||||
0x12: "GPSMapDatum",
|
0x12: GPSMapDatum,
|
||||||
0x13: "GPSDestLatitudeRef",
|
0x13: GPSDestLatitudeRef,
|
||||||
0x14: "GPSDestLatitude",
|
0x14: GPSDestLatitude,
|
||||||
0x15: "GPSDestLongitudeRef",
|
0x15: GPSDestLongitudeRef,
|
||||||
0x16: "GPSDestLongitude",
|
0x16: GPSDestLongitude,
|
||||||
0x17: "GPSDestBearingRef",
|
0x17: GPSDestBearingRef,
|
||||||
0x18: "GPSDestBearing",
|
0x18: GPSDestBearing,
|
||||||
0x19: "GPSDestDistanceRef",
|
0x19: GPSDestDistanceRef,
|
||||||
0x1A: "GPSDestDistance",
|
0x1A: GPSDestDistance,
|
||||||
0x1B: "GPSProcessingMethod",
|
0x1B: GPSProcessingMethod,
|
||||||
0x1C: "GPSAreaInformation",
|
0x1C: GPSAreaInformation,
|
||||||
0x1D: "GPSDateStamp",
|
0x1D: GPSDateStamp,
|
||||||
0x1E: "GPSDifferential",
|
0x1E: GPSDifferential,
|
||||||
}
|
}
|
||||||
|
|
||||||
var interopFields = map[uint16]FieldName{
|
var interopFields = map[uint16]FieldName{
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
//// Interoperability sub-IFD ///////
|
//// Interoperability sub-IFD ///////
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
0x1: "InteroperabilityIndex",
|
0x1: InteroperabilityIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
var thumbnailFields = map[uint16]FieldName{
|
||||||
|
0x0201: ThumbJPEGInterchangeFormat,
|
||||||
|
0x0202: ThumbJPEGInterchangeFormatLength,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]}`,
|
||||||
|
},
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -5,16 +5,19 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FormatType int
|
// TypeCategory specifies the Go type equivalent used to represent the basic
|
||||||
|
// tiff data types.
|
||||||
|
type TypeCategory int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
IntVal FormatType = iota
|
IntVal TypeCategory = iota
|
||||||
FloatVal
|
FloatVal
|
||||||
RatVal
|
RatVal
|
||||||
StringVal
|
StringVal
|
||||||
|
@ -22,32 +25,55 @@ const (
|
||||||
OtherVal
|
OtherVal
|
||||||
)
|
)
|
||||||
|
|
||||||
var fmtSize = map[uint16]uint32{
|
// DataType represents the basic tiff tag data types.
|
||||||
1: 1,
|
type DataType uint16
|
||||||
2: 1,
|
|
||||||
3: 2,
|
const (
|
||||||
4: 4,
|
DTByte DataType = 1
|
||||||
5: 8,
|
DTAscii = 2
|
||||||
6: 1,
|
DTShort = 3
|
||||||
7: 1,
|
DTLong = 4
|
||||||
8: 2,
|
DTRational = 5
|
||||||
9: 4,
|
DTSByte = 6
|
||||||
10: 8,
|
DTUndefined = 7
|
||||||
11: 4,
|
DTSShort = 8
|
||||||
12: 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.
|
// Tag reflects the parsed content of a tiff IFD tag.
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
// Id is the 2-byte tiff tag identifier
|
// Id is the 2-byte tiff tag identifier.
|
||||||
Id uint16
|
Id uint16
|
||||||
// Fmt is an integer (1 through 12) indicating the tag value's format.
|
// Type is an integer (1 through 12) indicating the tag value's data type.
|
||||||
Fmt uint16
|
Type DataType
|
||||||
// Ncomp is the number of type Fmt stored in the tag's value (i.e. the tag's
|
// Count is the number of type Type stored in the tag's value (i.e. the
|
||||||
// value is an array of type Fmt and length Ncomp).
|
// tag's value is an array of type Type and length Count).
|
||||||
Ncomp uint32
|
Count uint32
|
||||||
// Val holds the bytes that represent the tag's value.
|
// Val holds the bytes that represent the tag's value.
|
||||||
Val []byte
|
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
|
order binary.ByteOrder
|
||||||
|
|
||||||
|
@ -57,10 +83,10 @@ type Tag struct {
|
||||||
strVal string
|
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
|
// 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
|
// generally be relative to the beginning of the tiff structure (not relative
|
||||||
// beginning of the tag).
|
// to the beginning of the tag).
|
||||||
func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) {
|
func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) {
|
||||||
t := new(Tag)
|
t := new(Tag)
|
||||||
t.order = order
|
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())
|
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 {
|
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 {
|
if err != nil {
|
||||||
return nil, errors.New("tiff: tag component count read failed: " + err.Error())
|
return nil, errors.New("tiff: tag component count read failed: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
valLen := fmtSize[t.Fmt] * t.Ncomp
|
valLen := typeSize[t.Type] * t.Count
|
||||||
var offset uint32
|
|
||||||
if valLen > 4 {
|
if valLen > 4 {
|
||||||
binary.Read(r, order, &offset)
|
binary.Read(r, order, &t.ValOffset)
|
||||||
t.Val = make([]byte, valLen)
|
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 {
|
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 {
|
} else {
|
||||||
val := make([]byte, valLen)
|
val := make([]byte, valLen)
|
||||||
n, err := r.Read(val)
|
if _, err = io.ReadFull(r, val); err != nil {
|
||||||
if err != nil || n != int(valLen) {
|
return t, errors.New("tiff: tag offset read failed: " + err.Error())
|
||||||
return nil, errors.New("tiff: tag offset read failed: " + err.Error())
|
|
||||||
}
|
}
|
||||||
|
// ignore padding.
|
||||||
n, err = r.Read(make([]byte, 4-valLen))
|
if _, err = io.ReadFull(r, make([]byte, 4-valLen)); err != nil {
|
||||||
if err != nil || n != 4-int(valLen) {
|
return t, errors.New("tiff: tag offset read failed: " + err.Error())
|
||||||
return nil, errors.New("tiff: tag offset read failed: " + err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Val = val
|
t.Val = val
|
||||||
|
@ -112,62 +135,62 @@ func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) {
|
||||||
func (t *Tag) convertVals() {
|
func (t *Tag) convertVals() {
|
||||||
r := bytes.NewReader(t.Val)
|
r := bytes.NewReader(t.Val)
|
||||||
|
|
||||||
switch t.Fmt {
|
switch t.Type {
|
||||||
case 2: // ascii string
|
case DTAscii:
|
||||||
if len(t.Val) > 0 {
|
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
|
var v uint8
|
||||||
t.intVals = make([]int64, int(t.Ncomp))
|
t.intVals = make([]int64, int(t.Count))
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := range t.intVals {
|
||||||
err := binary.Read(r, t.order, &v)
|
err := binary.Read(r, t.order, &v)
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
t.intVals[i] = int64(v)
|
t.intVals[i] = int64(v)
|
||||||
}
|
}
|
||||||
case 3:
|
case DTShort:
|
||||||
var v uint16
|
var v uint16
|
||||||
t.intVals = make([]int64, int(t.Ncomp))
|
t.intVals = make([]int64, int(t.Count))
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := range t.intVals {
|
||||||
err := binary.Read(r, t.order, &v)
|
err := binary.Read(r, t.order, &v)
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
t.intVals[i] = int64(v)
|
t.intVals[i] = int64(v)
|
||||||
}
|
}
|
||||||
case 4:
|
case DTLong:
|
||||||
var v uint32
|
var v uint32
|
||||||
t.intVals = make([]int64, int(t.Ncomp))
|
t.intVals = make([]int64, int(t.Count))
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := range t.intVals {
|
||||||
err := binary.Read(r, t.order, &v)
|
err := binary.Read(r, t.order, &v)
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
t.intVals[i] = int64(v)
|
t.intVals[i] = int64(v)
|
||||||
}
|
}
|
||||||
case 6:
|
case DTSByte:
|
||||||
var v int8
|
var v int8
|
||||||
t.intVals = make([]int64, int(t.Ncomp))
|
t.intVals = make([]int64, int(t.Count))
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := range t.intVals {
|
||||||
err := binary.Read(r, t.order, &v)
|
err := binary.Read(r, t.order, &v)
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
t.intVals[i] = int64(v)
|
t.intVals[i] = int64(v)
|
||||||
}
|
}
|
||||||
case 8:
|
case DTSShort:
|
||||||
var v int16
|
var v int16
|
||||||
t.intVals = make([]int64, int(t.Ncomp))
|
t.intVals = make([]int64, int(t.Count))
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := range t.intVals {
|
||||||
err := binary.Read(r, t.order, &v)
|
err := binary.Read(r, t.order, &v)
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
t.intVals[i] = int64(v)
|
t.intVals[i] = int64(v)
|
||||||
}
|
}
|
||||||
case 9:
|
case DTSLong:
|
||||||
var v int32
|
var v int32
|
||||||
t.intVals = make([]int64, int(t.Ncomp))
|
t.intVals = make([]int64, int(t.Count))
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := range t.intVals {
|
||||||
err := binary.Read(r, t.order, &v)
|
err := binary.Read(r, t.order, &v)
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
t.intVals[i] = int64(v)
|
t.intVals[i] = int64(v)
|
||||||
}
|
}
|
||||||
case 5: // unsigned rational
|
case DTRational:
|
||||||
t.ratVals = make([][]int64, int(t.Ncomp))
|
t.ratVals = make([][]int64, int(t.Count))
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := range t.ratVals {
|
||||||
var n, d uint32
|
var n, d uint32
|
||||||
err := binary.Read(r, t.order, &n)
|
err := binary.Read(r, t.order, &n)
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
|
@ -175,9 +198,9 @@ func (t *Tag) convertVals() {
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
t.ratVals[i] = []int64{int64(n), int64(d)}
|
t.ratVals[i] = []int64{int64(n), int64(d)}
|
||||||
}
|
}
|
||||||
case 10: // signed rational
|
case DTSRational:
|
||||||
t.ratVals = make([][]int64, int(t.Ncomp))
|
t.ratVals = make([][]int64, int(t.Count))
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := range t.ratVals {
|
||||||
var n, d int32
|
var n, d int32
|
||||||
err := binary.Read(r, t.order, &n)
|
err := binary.Read(r, t.order, &n)
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
|
@ -185,17 +208,17 @@ func (t *Tag) convertVals() {
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
t.ratVals[i] = []int64{int64(n), int64(d)}
|
t.ratVals[i] = []int64{int64(n), int64(d)}
|
||||||
}
|
}
|
||||||
case 11: // float32
|
case DTFloat: // float32
|
||||||
t.floatVals = make([]float64, int(t.Ncomp))
|
t.floatVals = make([]float64, int(t.Count))
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := range t.floatVals {
|
||||||
var v float32
|
var v float32
|
||||||
err := binary.Read(r, t.order, &v)
|
err := binary.Read(r, t.order, &v)
|
||||||
panicOn(err)
|
panicOn(err)
|
||||||
t.floatVals[i] = float64(v)
|
t.floatVals[i] = float64(v)
|
||||||
}
|
}
|
||||||
case 12: // float64 (double)
|
case DTDouble:
|
||||||
t.floatVals = make([]float64, int(t.Ncomp))
|
t.floatVals = make([]float64, int(t.Count))
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := range t.floatVals {
|
||||||
var u float64
|
var u float64
|
||||||
err := binary.Read(r, t.order, &u)
|
err := binary.Read(r, t.order, &u)
|
||||||
panicOn(err)
|
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.).
|
// tag's value properly typed (e.g. integer, rational, etc.).
|
||||||
func (t *Tag) Format() FormatType {
|
func (t *Tag) TypeCategory() TypeCategory {
|
||||||
switch t.Fmt {
|
switch t.Type {
|
||||||
case 1, 3, 4, 6, 8, 9:
|
case DTByte, DTShort, DTLong, DTSByte, DTSShort, DTSLong:
|
||||||
return IntVal
|
return IntVal
|
||||||
case 5, 10:
|
case DTRational, DTSRational:
|
||||||
return RatVal
|
return RatVal
|
||||||
case 11, 12:
|
case DTFloat, DTDouble:
|
||||||
return FloatVal
|
return FloatVal
|
||||||
case 2:
|
case DTAscii:
|
||||||
return StringVal
|
return StringVal
|
||||||
case 7:
|
case DTUndefined:
|
||||||
return UndefVal
|
return UndefVal
|
||||||
}
|
}
|
||||||
return OtherVal
|
return OtherVal
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rat returns the tag's i'th value as a rational number. It panics if the tag format
|
// Rat returns the tag's i'th value as a rational number. It panics if the tag
|
||||||
// is not RatVal, if the denominator is zero, or if the tag has no i'th
|
// TypeCategory is not RatVal, if the denominator is zero, or if the tag has no
|
||||||
// component. If a denominator could be zero, use Rat2.
|
// i'th component. If a denominator could be zero, use Rat2.
|
||||||
func (t *Tag) Rat(i int) *big.Rat {
|
func (t *Tag) Rat(i int) *big.Rat {
|
||||||
n, d := t.Rat2(i)
|
n, d := t.Rat2(i)
|
||||||
return big.NewRat(n, d)
|
return big.NewRat(n, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rat2 returns the tag's i'th value as a rational number represented by a
|
// 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.
|
// or if the tag value has no i'th component.
|
||||||
func (t *Tag) Rat2(i int) (num, den int64) {
|
func (t *Tag) Rat2(i int) (num, den int64) {
|
||||||
if t.Format() != RatVal {
|
if t.TypeCategory() != RatVal {
|
||||||
panic("Tag format is not 'rational'")
|
panic("Tag type category is not 'rational'")
|
||||||
}
|
}
|
||||||
return t.ratVals[i][0], t.ratVals[i][1]
|
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
|
// Int returns the tag's i'th value as an integer. It panics if the tag
|
||||||
// IntVal or if the tag value has no i'th component.
|
// TypeCategory is not IntVal or if the tag value has no i'th component.
|
||||||
func (t *Tag) Int(i int) int64 {
|
func (t *Tag) Int(i int) int64 {
|
||||||
if t.Format() != IntVal {
|
if t.TypeCategory() != IntVal {
|
||||||
panic("Tag format is not 'int'")
|
panic("Tag type category is not 'int'")
|
||||||
}
|
}
|
||||||
return t.intVals[i]
|
return t.intVals[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Float returns the tag's i'th value as a float. It panics if the tag format is not
|
// Float returns the tag's i'th value as a float. It panics if the tag
|
||||||
// FloatVal or if the tag value has no i'th component.
|
// TypeCategory is not FloatVal or if the tag value has no i'th component.
|
||||||
func (t *Tag) Float(i int) float64 {
|
func (t *Tag) Float(i int) float64 {
|
||||||
if t.Format() != FloatVal {
|
if t.TypeCategory() != FloatVal {
|
||||||
panic("Tag format is not 'float'")
|
panic("Tag type category is not 'float'")
|
||||||
}
|
}
|
||||||
return t.floatVals[i]
|
return t.floatVals[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringVal returns the tag's value as a string. It panics if the tag
|
// 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 {
|
func (t *Tag) StringVal() string {
|
||||||
if t.Format() != StringVal {
|
if t.TypeCategory() != StringVal {
|
||||||
panic("Tag format is not 'ascii string'")
|
panic("Tag type category is not 'ascii string'")
|
||||||
}
|
}
|
||||||
return t.strVal
|
return t.strVal
|
||||||
}
|
}
|
||||||
|
@ -276,17 +299,17 @@ func (t *Tag) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tag) MarshalJSON() ([]byte, error) {
|
func (t *Tag) MarshalJSON() ([]byte, error) {
|
||||||
f := t.Format()
|
f := t.TypeCategory()
|
||||||
|
|
||||||
switch f {
|
switch f {
|
||||||
case StringVal, UndefVal:
|
case StringVal, UndefVal:
|
||||||
return nullString(t.Val), nil
|
return nullString(t.Val), nil
|
||||||
case OtherVal:
|
case OtherVal:
|
||||||
panic(fmt.Sprintf("Unhandled type Fmt=%v", t.Fmt))
|
panic(fmt.Sprintf("Unhandled tag type=%v", t.Type))
|
||||||
}
|
}
|
||||||
|
|
||||||
rv := []string{}
|
rv := []string{}
|
||||||
for i := 0; i < int(t.Ncomp); i++ {
|
for i := 0; i < int(t.Count); i++ {
|
||||||
switch f {
|
switch f {
|
||||||
case RatVal:
|
case RatVal:
|
||||||
n, d := t.Rat2(i)
|
n, d := t.Rat2(i)
|
||||||
|
|
|
@ -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
|
package tiff
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
@ -15,7 +17,7 @@ type ReadAtReader interface {
|
||||||
io.ReaderAt
|
io.ReaderAt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tiff provides access to decoded tiff data.
|
// Tiff provides access to a decoded tiff data structure.
|
||||||
type Tiff struct {
|
type Tiff struct {
|
||||||
// Dirs is an ordered slice of the tiff's Image File Directories (IFDs).
|
// Dirs is an ordered slice of the tiff's Image File Directories (IFDs).
|
||||||
// The IFD at index 0 is IFD0.
|
// The IFD at index 0 is IFD0.
|
||||||
|
@ -24,10 +26,10 @@ type Tiff struct {
|
||||||
Order binary.ByteOrder
|
Order binary.ByteOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode parses tiff-encoded data from r and returns a Tiff that reflects the
|
// Decode parses tiff-encoded data from r and returns a Tiff struct that
|
||||||
// structure and content of the tiff data. The first read from r should be the
|
// reflects the structure and content of the tiff data. The first read from r
|
||||||
// first byte of the tiff-encoded data (not necessarily the first byte of an
|
// should be the first byte of the tiff-encoded data and not necessarily the
|
||||||
// os.File object).
|
// first byte of an os.File object.
|
||||||
func Decode(r io.Reader) (*Tiff, error) {
|
func Decode(r io.Reader) (*Tiff, error) {
|
||||||
data, err := ioutil.ReadAll(r)
|
data, err := ioutil.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -39,10 +41,9 @@ func Decode(r io.Reader) (*Tiff, error) {
|
||||||
|
|
||||||
// read byte order
|
// read byte order
|
||||||
bo := make([]byte, 2)
|
bo := make([]byte, 2)
|
||||||
n, err := buf.Read(bo)
|
if _, err = io.ReadFull(buf, bo); err != nil {
|
||||||
if n < len(bo) || err != nil {
|
|
||||||
return nil, errors.New("tiff: could not read tiff byte order")
|
return nil, errors.New("tiff: could not read tiff byte order")
|
||||||
} else {
|
}
|
||||||
if string(bo) == "II" {
|
if string(bo) == "II" {
|
||||||
t.Order = binary.LittleEndian
|
t.Order = binary.LittleEndian
|
||||||
} else if string(bo) == "MM" {
|
} else if string(bo) == "MM" {
|
||||||
|
@ -50,12 +51,11 @@ func Decode(r io.Reader) (*Tiff, error) {
|
||||||
} else {
|
} 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
|
// check for special tiff marker
|
||||||
var sp int16
|
var sp int16
|
||||||
err = binary.Read(buf, t.Order, &sp)
|
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")
|
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 {
|
func (tf *Tiff) String() string {
|
||||||
s := "Tiff{"
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprint(&buf, "Tiff{")
|
||||||
for _, d := range tf.Dirs {
|
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 {
|
type Dir struct {
|
||||||
Tags []*Tag
|
Tags []*Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeDir parses a tiff-encoded IFD from r and returns a Dir object. offset
|
// 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
|
// 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
|
// byte of the IFD. ReadAt offsets should generally be relative to the
|
||||||
// tiff structure (not relative to the beginning of the IFD).
|
// 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) {
|
func DecodeDir(r ReadAtReader, order binary.ByteOrder) (d *Dir, offset int32, err error) {
|
||||||
d = new(Dir)
|
d = new(Dir)
|
||||||
|
|
||||||
|
@ -114,7 +116,7 @@ func DecodeDir(r ReadAtReader, order binary.ByteOrder) (d *Dir, offset int32, er
|
||||||
var nTags int16
|
var nTags int16
|
||||||
err = binary.Read(r, order, &nTags)
|
err = binary.Read(r, order, &nTags)
|
||||||
if err != nil {
|
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
|
// load tags
|
||||||
|
|
|
@ -4,10 +4,14 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var dataDir = flag.String("test_data_dir", ".", "Directory where the data files for testing are located")
|
||||||
|
|
||||||
type input struct {
|
type input struct {
|
||||||
tgId string
|
tgId string
|
||||||
tpe string
|
tpe string
|
||||||
|
@ -18,7 +22,7 @@ type input struct {
|
||||||
|
|
||||||
type output struct {
|
type output struct {
|
||||||
id uint16
|
id uint16
|
||||||
format uint16
|
typ DataType
|
||||||
count uint32
|
count uint32
|
||||||
val []byte
|
val []byte
|
||||||
}
|
}
|
||||||
|
@ -38,97 +42,97 @@ var set1 = []tagTest{
|
||||||
// {"TgId", "TYPE", "N-VALUES", "OFFSET--", "VAL..."},
|
// {"TgId", "TYPE", "N-VALUES", "OFFSET--", "VAL..."},
|
||||||
input{"0003", "0002", "00000002", "11000000", ""},
|
input{"0003", "0002", "00000002", "11000000", ""},
|
||||||
input{"0300", "0200", "02000000", "11000000", ""},
|
input{"0300", "0200", "02000000", "11000000", ""},
|
||||||
output{0x0003, 0x0002, 0x0002, []byte{0x11, 0x00}},
|
output{0x0003, DataType(0x0002), 0x0002, []byte{0x11, 0x00}},
|
||||||
},
|
},
|
||||||
tagTest{
|
tagTest{
|
||||||
input{"0001", "0002", "00000006", "00000012", "111213141516"},
|
input{"0001", "0002", "00000006", "00000012", "111213141516"},
|
||||||
input{"0100", "0200", "06000000", "12000000", "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 ////////////////
|
//////////// int (1-byte) type ////////////////
|
||||||
tagTest{
|
tagTest{
|
||||||
input{"0001", "0001", "00000001", "11000000", ""},
|
input{"0001", "0001", "00000001", "11000000", ""},
|
||||||
input{"0100", "0100", "01000000", "11000000", ""},
|
input{"0100", "0100", "01000000", "11000000", ""},
|
||||||
output{0x0001, 0x0001, 0x0001, []byte{0x11}},
|
output{0x0001, DataType(0x0001), 0x0001, []byte{0x11}},
|
||||||
},
|
},
|
||||||
tagTest{
|
tagTest{
|
||||||
input{"0001", "0001", "00000005", "00000010", "1112131415"},
|
input{"0001", "0001", "00000005", "00000010", "1112131415"},
|
||||||
input{"0100", "0100", "05000000", "10000000", "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{
|
tagTest{
|
||||||
input{"0001", "0006", "00000001", "11000000", ""},
|
input{"0001", "0006", "00000001", "11000000", ""},
|
||||||
input{"0100", "0600", "01000000", "11000000", ""},
|
input{"0100", "0600", "01000000", "11000000", ""},
|
||||||
output{0x0001, 0x0006, 0x0001, []byte{0x11}},
|
output{0x0001, DataType(0x0006), 0x0001, []byte{0x11}},
|
||||||
},
|
},
|
||||||
tagTest{
|
tagTest{
|
||||||
input{"0001", "0006", "00000005", "00000010", "1112131415"},
|
input{"0001", "0006", "00000005", "00000010", "1112131415"},
|
||||||
input{"0100", "0600", "05000000", "10000000", "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 ////////////////
|
//////////// int (2-byte) types ////////////////
|
||||||
tagTest{
|
tagTest{
|
||||||
input{"0001", "0003", "00000002", "11111212", ""},
|
input{"0001", "0003", "00000002", "11111212", ""},
|
||||||
input{"0100", "0300", "02000000", "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{
|
tagTest{
|
||||||
input{"0001", "0003", "00000003", "00000010", "111213141516"},
|
input{"0001", "0003", "00000003", "00000010", "111213141516"},
|
||||||
input{"0100", "0300", "03000000", "10000000", "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{
|
tagTest{
|
||||||
input{"0001", "0008", "00000001", "11120000", ""},
|
input{"0001", "0008", "00000001", "11120000", ""},
|
||||||
input{"0100", "0800", "01000000", "11120000", ""},
|
input{"0100", "0800", "01000000", "11120000", ""},
|
||||||
output{0x0001, 0x0008, 0x0001, []byte{0x11, 0x12}},
|
output{0x0001, DataType(0x0008), 0x0001, []byte{0x11, 0x12}},
|
||||||
},
|
},
|
||||||
tagTest{
|
tagTest{
|
||||||
input{"0001", "0008", "00000003", "00000100", "111213141516"},
|
input{"0001", "0008", "00000003", "00000100", "111213141516"},
|
||||||
input{"0100", "0800", "03000000", "00100000", "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 ////////////////
|
//////////// int (4-byte) types ////////////////
|
||||||
tagTest{
|
tagTest{
|
||||||
input{"0001", "0004", "00000001", "11121314", ""},
|
input{"0001", "0004", "00000001", "11121314", ""},
|
||||||
input{"0100", "0400", "01000000", "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{
|
tagTest{
|
||||||
input{"0001", "0004", "00000002", "00000010", "1112131415161718"},
|
input{"0001", "0004", "00000002", "00000010", "1112131415161718"},
|
||||||
input{"0100", "0400", "02000000", "10000000", "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{
|
tagTest{
|
||||||
input{"0001", "0009", "00000001", "11121314", ""},
|
input{"0001", "0009", "00000001", "11121314", ""},
|
||||||
input{"0100", "0900", "01000000", "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{
|
tagTest{
|
||||||
input{"0001", "0009", "00000002", "00000011", "1112131415161819"},
|
input{"0001", "0009", "00000002", "00000011", "1112131415161819"},
|
||||||
input{"0100", "0900", "02000000", "11000000", "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 ////////////////////
|
//////////// rational types ////////////////////
|
||||||
tagTest{
|
tagTest{
|
||||||
input{"0001", "0005", "00000001", "00000010", "1112131415161718"},
|
input{"0001", "0005", "00000001", "00000010", "1112131415161718"},
|
||||||
input{"0100", "0500", "01000000", "10000000", "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{
|
tagTest{
|
||||||
input{"0001", "000A", "00000001", "00000011", "1112131415161819"},
|
input{"0001", "000A", "00000001", "00000011", "1112131415161819"},
|
||||||
input{"0100", "0A00", "01000000", "11000000", "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 ///////////////////////
|
//////////// float types ///////////////////////
|
||||||
tagTest{
|
tagTest{
|
||||||
input{"0001", "0005", "00000001", "00000010", "1112131415161718"},
|
input{"0001", "0005", "00000001", "00000010", "1112131415161718"},
|
||||||
input{"0100", "0500", "01000000", "10000000", "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{
|
tagTest{
|
||||||
input{"0101", "000A", "00000001", "00000011", "1112131415161819"},
|
input{"0101", "000A", "00000001", "00000011", "1112131415161819"},
|
||||||
input{"0101", "0A00", "01000000", "11000000", "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 {
|
if tg.Id != out.id {
|
||||||
t.Errorf("(%v) tag %v id decode: expected %v, got %v", order, i, out.id, tg.Id)
|
t.Errorf("(%v) tag %v id decode: expected %v, got %v", order, i, out.id, tg.Id)
|
||||||
}
|
}
|
||||||
if tg.Fmt != out.format {
|
if tg.Type != out.typ {
|
||||||
t.Errorf("(%v) tag %v format decode: expected %v, got %v", order, i, out.format, tg.Fmt)
|
t.Errorf("(%v) tag %v type decode: expected %v, got %v", order, i, out.typ, tg.Type)
|
||||||
}
|
}
|
||||||
if tg.Ncomp != out.count {
|
if tg.Count != out.count {
|
||||||
t.Errorf("(%v) tag %v N-components decode: expected %v, got %v", order, i, out.count, tg.Ncomp)
|
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) {
|
if !bytes.Equal(tg.Val, out.val) {
|
||||||
t.Errorf("(%v) tag %v value decode: expected %v, got %v", order, i, out.val, tg.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) {
|
func TestDecode(t *testing.T) {
|
||||||
name := "sample1.tif"
|
name := filepath.Join(*dataDir, "sample1.tif")
|
||||||
f, err := os.Open(name)
|
f, err := os.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%v\n", err)
|
t.Fatalf("%v\n", err)
|
||||||
|
@ -212,7 +216,7 @@ func TestDecodeTag_blob(t *testing.T) {
|
||||||
|
|
||||||
t.Logf("tag: %v+\n", tg)
|
t.Logf("tag: %v+\n", tg)
|
||||||
n, d := tg.Rat2(0)
|
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 {
|
func data() []byte {
|
||||||
|
|
Loading…
Reference in New Issue