vendor exif
This commit is contained in:
parent
1abff34b3d
commit
b4ee62151d
24
vendor/github.com/rwcarlsen/goexif/LICENSE
generated
vendored
Normal file
24
vendor/github.com/rwcarlsen/goexif/LICENSE
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
Copyright (c) 2012, Robert Carlsen & Contributors
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
4
vendor/github.com/rwcarlsen/goexif/exif/README.md
generated
vendored
Normal file
4
vendor/github.com/rwcarlsen/goexif/exif/README.md
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
To regenerate the regression test data, run `go generate` inside the exif
|
||||||
|
package directory and commit the changes to *regress_expected_test.go*.
|
||||||
|
|
619
vendor/github.com/rwcarlsen/goexif/exif/exif.go
generated
vendored
Normal file
619
vendor/github.com/rwcarlsen/goexif/exif/exif.go
generated
vendored
Normal file
@ -0,0 +1,619 @@
|
|||||||
|
// Package exif implements decoding of EXIF data as defined in the EXIF 2.2
|
||||||
|
// specification (http://www.exif.org/Exif2-2.PDF).
|
||||||
|
package exif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rwcarlsen/goexif/tiff"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
jpeg_APP1 = 0xE1
|
||||||
|
|
||||||
|
exifPointer = 0x8769
|
||||||
|
gpsPointer = 0x8825
|
||||||
|
interopPointer = 0xA005
|
||||||
|
)
|
||||||
|
|
||||||
|
// A decodeError is returned when the image cannot be decoded as a tiff image.
|
||||||
|
type decodeError struct {
|
||||||
|
cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (de decodeError) Error() string {
|
||||||
|
return fmt.Sprintf("exif: decode failed (%v) ", de.cause.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsShortReadTagValueError identifies a ErrShortReadTagValue error.
|
||||||
|
func IsShortReadTagValueError(err error) bool {
|
||||||
|
de, ok := err.(decodeError)
|
||||||
|
if ok {
|
||||||
|
return de.cause == tiff.ErrShortReadTagValue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TagNotPresentError is returned when the requested field is not
|
||||||
|
// present in the EXIF.
|
||||||
|
type TagNotPresentError FieldName
|
||||||
|
|
||||||
|
func (tag TagNotPresentError) Error() string {
|
||||||
|
return fmt.Sprintf("exif: tag %q is not present", string(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsTagNotPresentError(err error) bool {
|
||||||
|
_, ok := err.(TagNotPresentError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser allows the registration of custom parsing and field loading
|
||||||
|
// in the Decode function.
|
||||||
|
type Parser interface {
|
||||||
|
// 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{}
|
||||||
|
|
||||||
|
type tiffErrors map[tiffError]string
|
||||||
|
|
||||||
|
func (te tiffErrors) Error() string {
|
||||||
|
var allErrors []string
|
||||||
|
for k, v := range te {
|
||||||
|
allErrors = append(allErrors, fmt.Sprintf("%s: %v\n", stagePrefix[k], v))
|
||||||
|
}
|
||||||
|
return strings.Join(allErrors, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCriticalError, given the error returned by Decode, reports whether the
|
||||||
|
// returned *Exif may contain usable information.
|
||||||
|
func IsCriticalError(err error) bool {
|
||||||
|
_, ok := err.(tiffErrors)
|
||||||
|
return !ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExifError reports whether the error happened while decoding the EXIF
|
||||||
|
// sub-IFD.
|
||||||
|
func IsExifError(err error) bool {
|
||||||
|
if te, ok := err.(tiffErrors); ok {
|
||||||
|
_, isExif := te[loadExif]
|
||||||
|
return isExif
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGPSError reports whether the error happened while decoding the GPS sub-IFD.
|
||||||
|
func IsGPSError(err error) bool {
|
||||||
|
if te, ok := err.(tiffErrors); ok {
|
||||||
|
_, isGPS := te[loadExif]
|
||||||
|
return isGPS
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInteroperabilityError reports whether the error happened while decoding the
|
||||||
|
// Interoperability sub-IFD.
|
||||||
|
func IsInteroperabilityError(err error) bool {
|
||||||
|
if te, ok := err.(tiffErrors); ok {
|
||||||
|
_, isInterop := te[loadInteroperability]
|
||||||
|
return isInterop
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type tiffError int
|
||||||
|
|
||||||
|
const (
|
||||||
|
loadExif tiffError = iota
|
||||||
|
loadGPS
|
||||||
|
loadInteroperability
|
||||||
|
)
|
||||||
|
|
||||||
|
var stagePrefix = map[tiffError]string{
|
||||||
|
loadExif: "loading EXIF sub-IFD",
|
||||||
|
loadGPS: "loading GPS sub-IFD",
|
||||||
|
loadInteroperability: "loading Interoperability sub-IFD",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse reads data from the tiff data in x and populates the tags
|
||||||
|
// in x. If parsing a sub-IFD fails, the error is recorded and
|
||||||
|
// parsing continues with the remaining sub-IFDs.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
te := make(tiffErrors)
|
||||||
|
|
||||||
|
// recurse into exif, gps, and interop sub-IFDs
|
||||||
|
if err := loadSubDir(x, ExifIFDPointer, exifFields); err != nil {
|
||||||
|
te[loadExif] = err.Error()
|
||||||
|
}
|
||||||
|
if err := loadSubDir(x, GPSInfoIFDPointer, gpsFields); err != nil {
|
||||||
|
te[loadGPS] = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := loadSubDir(x, InteroperabilityIFDPointer, interopFields); err != nil {
|
||||||
|
te[loadInteroperability] = err.Error()
|
||||||
|
}
|
||||||
|
if len(te) > 0 {
|
||||||
|
return te
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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, err := tag.Int64(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.Seek(offset, 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("exif: seek to sub-IFD %s failed: %v", ptr, err)
|
||||||
|
}
|
||||||
|
subDir, _, err := tiff.DecodeDir(r, x.Tiff.Order)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("exif: sub-IFD %s decode failed: %v", ptr, err)
|
||||||
|
}
|
||||||
|
x.LoadTags(subDir, fieldMap, false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exif provides access to decoded EXIF metadata fields and values.
|
||||||
|
type Exif struct {
|
||||||
|
Tiff *tiff.Tiff
|
||||||
|
main map[FieldName]*tiff.Tag
|
||||||
|
Raw []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// The error can be inspected with functions such as IsCriticalError to
|
||||||
|
// determine whether the returned object might still be usable.
|
||||||
|
func Decode(r io.Reader) (*Exif, error) {
|
||||||
|
// EXIF data in JPEG is stored in the APP1 marker. EXIF data uses the TIFF
|
||||||
|
// format to store data.
|
||||||
|
// If we're parsing a TIFF image, we don't need to strip away any data.
|
||||||
|
// If we're parsing a JPEG image, we need to strip away the JPEG APP1
|
||||||
|
// marker and also the EXIF header.
|
||||||
|
|
||||||
|
header := make([]byte, 4)
|
||||||
|
n, err := r.Read(header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if n < len(header) {
|
||||||
|
return nil, errors.New("exif: short read on header")
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTiff bool
|
||||||
|
switch string(header) {
|
||||||
|
case "II*\x00":
|
||||||
|
// TIFF - Little endian (Intel)
|
||||||
|
isTiff = true
|
||||||
|
case "MM\x00*":
|
||||||
|
// TIFF - Big endian (Motorola)
|
||||||
|
isTiff = true
|
||||||
|
default:
|
||||||
|
// Not TIFF, assume JPEG
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the header bytes back into the reader.
|
||||||
|
r = io.MultiReader(bytes.NewReader(header), r)
|
||||||
|
var (
|
||||||
|
er *bytes.Reader
|
||||||
|
tif *tiff.Tiff
|
||||||
|
)
|
||||||
|
|
||||||
|
if isTiff {
|
||||||
|
// Functions below need the IFDs from the TIFF data to be stored in a
|
||||||
|
// *bytes.Reader. We use TeeReader to get a copy of the bytes as a
|
||||||
|
// side-effect of tiff.Decode() doing its work.
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
tr := io.TeeReader(r, b)
|
||||||
|
tif, err = tiff.Decode(tr)
|
||||||
|
er = bytes.NewReader(b.Bytes())
|
||||||
|
} else {
|
||||||
|
// Locate the JPEG APP1 header.
|
||||||
|
var sec *appSec
|
||||||
|
sec, err = newAppSec(jpeg_APP1, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Strip away EXIF header.
|
||||||
|
er, err = sec.exifReader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tif, err = tiff.Decode(er)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, decodeError{cause: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
er.Seek(0, 0)
|
||||||
|
raw, err := ioutil.ReadAll(er)
|
||||||
|
if err != nil {
|
||||||
|
return nil, decodeError{cause: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build an exif structure from the tiff
|
||||||
|
x := &Exif{
|
||||||
|
main: map[FieldName]*tiff.Tag{},
|
||||||
|
Tiff: tif,
|
||||||
|
Raw: raw,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range parsers {
|
||||||
|
if err := p.Parse(x); err != nil {
|
||||||
|
if _, ok := err.(tiffErrors); ok {
|
||||||
|
return x, err
|
||||||
|
}
|
||||||
|
// This should never happen, as Parse always returns a tiffError
|
||||||
|
// for now, but that could change.
|
||||||
|
return x, fmt.Errorf("exif: parser %v failed (%v)", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadTags loads tags into the available fields from the tiff Directory
|
||||||
|
// using the given tagid-fieldname mapping. Used to load makernote and
|
||||||
|
// other meta-data. If showMissing is true, tags in d that are not in the
|
||||||
|
// fieldMap will be loaded with the FieldName UnknownPrefix followed by the
|
||||||
|
// tag ID (in hex format).
|
||||||
|
func (x *Exif) LoadTags(d *tiff.Dir, fieldMap map[uint16]FieldName, showMissing bool) {
|
||||||
|
for _, tag := range d.Tags {
|
||||||
|
name := fieldMap[tag.Id]
|
||||||
|
if name == "" {
|
||||||
|
if !showMissing {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name = FieldName(fmt.Sprintf("%v%x", UnknownPrefix, tag.Id))
|
||||||
|
}
|
||||||
|
x.main[name] = tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves the EXIF tag for the given field name.
|
||||||
|
//
|
||||||
|
// If the tag is not known or not present, an error is returned. If the
|
||||||
|
// tag name is known, the error will be a TagNotPresentError.
|
||||||
|
func (x *Exif) Get(name FieldName) (*tiff.Tag, error) {
|
||||||
|
if tg, ok := x.main[name]; ok {
|
||||||
|
return tg, nil
|
||||||
|
}
|
||||||
|
return nil, TagNotPresentError(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walker is the interface used to traverse all fields of an Exif object.
|
||||||
|
type Walker interface {
|
||||||
|
// Walk is called for each non-nil EXIF field. Returning a non-nil
|
||||||
|
// error aborts the walk/traversal.
|
||||||
|
Walk(name FieldName, tag *tiff.Tag) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk calls the Walk method of w with the name and tag for every non-nil
|
||||||
|
// EXIF field. If w aborts the walk with an error, that error is returned.
|
||||||
|
func (x *Exif) Walk(w Walker) error {
|
||||||
|
for name, tag := range x.main {
|
||||||
|
if err := w.Walk(name, tag); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DateTime returns the EXIF's "DateTimeOriginal" field, which
|
||||||
|
// is the creation time of the photo. If not found, it tries
|
||||||
|
// the "DateTime" (which is meant as the modtime) instead.
|
||||||
|
// The error will be TagNotPresentErr if none of those tags
|
||||||
|
// were found, or a generic error if the tag value was
|
||||||
|
// not a string, or the error returned by time.Parse.
|
||||||
|
//
|
||||||
|
// If the EXIF lacks timezone information or GPS time, the returned
|
||||||
|
// time's Location will be time.Local.
|
||||||
|
func (x *Exif) DateTime() (time.Time, error) {
|
||||||
|
var dt time.Time
|
||||||
|
tag, err := x.Get(DateTimeOriginal)
|
||||||
|
if err != nil {
|
||||||
|
tag, err = x.Get(DateTime)
|
||||||
|
if err != nil {
|
||||||
|
return dt, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tag.Format() != tiff.StringVal {
|
||||||
|
return dt, errors.New("DateTime[Original] not in string format")
|
||||||
|
}
|
||||||
|
exifTimeLayout := "2006:01:02 15:04:05"
|
||||||
|
dateStr := strings.TrimRight(string(tag.Val), "\x00")
|
||||||
|
// TODO(bradfitz,mpl): look for timezone offset, GPS time, etc.
|
||||||
|
// For now, just always return the time.Local timezone.
|
||||||
|
return time.ParseInLocation(exifTimeLayout, dateStr, time.Local)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ratFloat(num, dem int64) float64 {
|
||||||
|
return float64(num) / float64(dem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to parse a Geo degrees value from a string as it was found in some
|
||||||
|
// EXIF data.
|
||||||
|
// Supported formats so far:
|
||||||
|
// - "52,00000,50,00000,34,01180" ==> 52 deg 50'34.0118"
|
||||||
|
// Probably due to locale the comma is used as decimal mark as well as the
|
||||||
|
// separator of three floats (degrees, minutes, seconds)
|
||||||
|
// http://en.wikipedia.org/wiki/Decimal_mark#Hindu.E2.80.93Arabic_numeral_system
|
||||||
|
// - "52.0,50.0,34.01180" ==> 52deg50'34.0118"
|
||||||
|
// - "52,50,34.01180" ==> 52deg50'34.0118"
|
||||||
|
func parseTagDegreesString(s string) (float64, error) {
|
||||||
|
const unparsableErrorFmt = "Unknown coordinate format: %s"
|
||||||
|
isSplitRune := func(c rune) bool {
|
||||||
|
return c == ',' || c == ';'
|
||||||
|
}
|
||||||
|
parts := strings.FieldsFunc(s, isSplitRune)
|
||||||
|
var degrees, minutes, seconds float64
|
||||||
|
var err error
|
||||||
|
switch len(parts) {
|
||||||
|
case 6:
|
||||||
|
degrees, err = strconv.ParseFloat(parts[0]+"."+parts[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||||
|
}
|
||||||
|
minutes, err = strconv.ParseFloat(parts[2]+"."+parts[3], 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||||
|
}
|
||||||
|
minutes = math.Copysign(minutes, degrees)
|
||||||
|
seconds, err = strconv.ParseFloat(parts[4]+"."+parts[5], 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||||
|
}
|
||||||
|
seconds = math.Copysign(seconds, degrees)
|
||||||
|
case 3:
|
||||||
|
degrees, err = strconv.ParseFloat(parts[0], 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||||
|
}
|
||||||
|
minutes, err = strconv.ParseFloat(parts[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||||
|
}
|
||||||
|
minutes = math.Copysign(minutes, degrees)
|
||||||
|
seconds, err = strconv.ParseFloat(parts[2], 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||||
|
}
|
||||||
|
seconds = math.Copysign(seconds, degrees)
|
||||||
|
default:
|
||||||
|
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
|
||||||
|
}
|
||||||
|
return degrees + minutes/60.0 + seconds/3600.0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse3Rat2(tag *tiff.Tag) ([3]float64, error) {
|
||||||
|
v := [3]float64{}
|
||||||
|
for i := range v {
|
||||||
|
num, den, err := tag.Rat2(i)
|
||||||
|
if err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
v[i] = ratFloat(num, den)
|
||||||
|
if tag.Count < uint32(i+2) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tagDegrees(tag *tiff.Tag) (float64, error) {
|
||||||
|
switch tag.Format() {
|
||||||
|
case tiff.RatVal:
|
||||||
|
// The usual case, according to the Exif spec
|
||||||
|
// (http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf,
|
||||||
|
// sec 4.6.6, p. 52 et seq.)
|
||||||
|
v, err := parse3Rat2(tag)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, err
|
||||||
|
}
|
||||||
|
return v[0] + v[1]/60 + v[2]/3600.0, nil
|
||||||
|
case tiff.StringVal:
|
||||||
|
// Encountered this weird case with a panorama picture taken with a HTC phone
|
||||||
|
s, err := tag.StringVal()
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, err
|
||||||
|
}
|
||||||
|
return parseTagDegreesString(s)
|
||||||
|
default:
|
||||||
|
// don't know how to parse value, give up
|
||||||
|
return 0.0, fmt.Errorf("Malformed EXIF Tag Degrees")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LatLong returns the latitude and longitude of the photo and
|
||||||
|
// whether it was present.
|
||||||
|
func (x *Exif) LatLong() (lat, long float64, err error) {
|
||||||
|
// All calls of x.Get might return an TagNotPresentError
|
||||||
|
longTag, err := x.Get(FieldName("GPSLongitude"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ewTag, err := x.Get(FieldName("GPSLongitudeRef"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
latTag, err := x.Get(FieldName("GPSLatitude"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nsTag, err := x.Get(FieldName("GPSLatitudeRef"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if long, err = tagDegrees(longTag); err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
|
||||||
|
}
|
||||||
|
if lat, err = tagDegrees(latTag); err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("Cannot parse latitude: %v", err)
|
||||||
|
}
|
||||||
|
ew, err := ewTag.StringVal()
|
||||||
|
if err == nil && ew == "W" {
|
||||||
|
long *= -1.0
|
||||||
|
} else if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
|
||||||
|
}
|
||||||
|
ns, err := nsTag.StringVal()
|
||||||
|
if err == nil && ns == "S" {
|
||||||
|
lat *= -1.0
|
||||||
|
} else if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
|
||||||
|
}
|
||||||
|
return lat, long, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a pretty text representation of the decoded exif data.
|
||||||
|
func (x *Exif) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for name, tag := range x.main {
|
||||||
|
fmt.Fprintf(&buf, "%s: %s\n", name, tag)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
start, err := offset.Int(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
length, err := x.Get(ThumbJPEGInterchangeFormatLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
l, err := length.Int(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Raw[start : start+l], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJson implements the encoding/json.Marshaler interface providing output of
|
||||||
|
// all EXIF fields present (names and values).
|
||||||
|
func (x Exif) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(x.main)
|
||||||
|
}
|
||||||
|
|
||||||
|
type appSec struct {
|
||||||
|
marker byte
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// newAppSec finds marker in r and returns the corresponding application data
|
||||||
|
// section.
|
||||||
|
func newAppSec(marker byte, r io.Reader) (*appSec, error) {
|
||||||
|
br := bufio.NewReader(r)
|
||||||
|
app := &appSec{marker: marker}
|
||||||
|
var dataLen int
|
||||||
|
|
||||||
|
// seek to marker
|
||||||
|
for dataLen == 0 {
|
||||||
|
if _, err := br.ReadBytes(0xFF); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c, err := br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if c != marker {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dataLenBytes := make([]byte, 2)
|
||||||
|
for k,_ := range dataLenBytes {
|
||||||
|
c, err := br.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dataLenBytes[k] = c
|
||||||
|
}
|
||||||
|
dataLen = int(binary.BigEndian.Uint16(dataLenBytes)) - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// read section data
|
||||||
|
nread := 0
|
||||||
|
for nread < dataLen {
|
||||||
|
s := make([]byte, dataLen-nread)
|
||||||
|
n, err := br.Read(s)
|
||||||
|
nread += n
|
||||||
|
if err != nil && nread < dataLen {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
app.data = append(app.data, s[:n]...)
|
||||||
|
}
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reader returns a reader on this appSec.
|
||||||
|
func (app *appSec) reader() *bytes.Reader {
|
||||||
|
return bytes.NewReader(app.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// exifReader returns a reader on this appSec with the read cursor advanced to
|
||||||
|
// the start of the exif's tiff encoded portion.
|
||||||
|
func (app *appSec) exifReader() (*bytes.Reader, error) {
|
||||||
|
if len(app.data) < 6 {
|
||||||
|
return nil, errors.New("exif: failed to find exif intro marker")
|
||||||
|
}
|
||||||
|
|
||||||
|
// read/check for exif special mark
|
||||||
|
exif := app.data[:6]
|
||||||
|
if !bytes.Equal(exif, append([]byte("Exif"), 0x00, 0x00)) {
|
||||||
|
return nil, errors.New("exif: failed to find exif intro marker")
|
||||||
|
}
|
||||||
|
return bytes.NewReader(app.data[6:]), nil
|
||||||
|
}
|
293
vendor/github.com/rwcarlsen/goexif/exif/fields.go
generated
vendored
Normal file
293
vendor/github.com/rwcarlsen/goexif/exif/fields.go
generated
vendored
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
package exif
|
||||||
|
|
||||||
|
type FieldName string
|
||||||
|
|
||||||
|
// UnknownPrefix is used as the first part of field names for decoded tags for
|
||||||
|
// which there is no known/supported EXIF field.
|
||||||
|
const UnknownPrefix = "UnknownTag_"
|
||||||
|
|
||||||
|
// Primary EXIF fields
|
||||||
|
const (
|
||||||
|
ImageWidth FieldName = "ImageWidth"
|
||||||
|
ImageLength = "ImageLength" // Image height called Length by EXIF spec
|
||||||
|
BitsPerSample = "BitsPerSample"
|
||||||
|
Compression = "Compression"
|
||||||
|
PhotometricInterpretation = "PhotometricInterpretation"
|
||||||
|
Orientation = "Orientation"
|
||||||
|
SamplesPerPixel = "SamplesPerPixel"
|
||||||
|
PlanarConfiguration = "PlanarConfiguration"
|
||||||
|
YCbCrSubSampling = "YCbCrSubSampling"
|
||||||
|
YCbCrPositioning = "YCbCrPositioning"
|
||||||
|
XResolution = "XResolution"
|
||||||
|
YResolution = "YResolution"
|
||||||
|
ResolutionUnit = "ResolutionUnit"
|
||||||
|
DateTime = "DateTime"
|
||||||
|
ImageDescription = "ImageDescription"
|
||||||
|
Make = "Make"
|
||||||
|
Model = "Model"
|
||||||
|
Software = "Software"
|
||||||
|
Artist = "Artist"
|
||||||
|
Copyright = "Copyright"
|
||||||
|
ExifIFDPointer = "ExifIFDPointer"
|
||||||
|
GPSInfoIFDPointer = "GPSInfoIFDPointer"
|
||||||
|
InteroperabilityIFDPointer = "InteroperabilityIFDPointer"
|
||||||
|
ExifVersion = "ExifVersion"
|
||||||
|
FlashpixVersion = "FlashpixVersion"
|
||||||
|
ColorSpace = "ColorSpace"
|
||||||
|
ComponentsConfiguration = "ComponentsConfiguration"
|
||||||
|
CompressedBitsPerPixel = "CompressedBitsPerPixel"
|
||||||
|
PixelXDimension = "PixelXDimension"
|
||||||
|
PixelYDimension = "PixelYDimension"
|
||||||
|
MakerNote = "MakerNote"
|
||||||
|
UserComment = "UserComment"
|
||||||
|
RelatedSoundFile = "RelatedSoundFile"
|
||||||
|
DateTimeOriginal = "DateTimeOriginal"
|
||||||
|
DateTimeDigitized = "DateTimeDigitized"
|
||||||
|
SubSecTime = "SubSecTime"
|
||||||
|
SubSecTimeOriginal = "SubSecTimeOriginal"
|
||||||
|
SubSecTimeDigitized = "SubSecTimeDigitized"
|
||||||
|
ImageUniqueID = "ImageUniqueID"
|
||||||
|
ExposureTime = "ExposureTime"
|
||||||
|
FNumber = "FNumber"
|
||||||
|
ExposureProgram = "ExposureProgram"
|
||||||
|
SpectralSensitivity = "SpectralSensitivity"
|
||||||
|
ISOSpeedRatings = "ISOSpeedRatings"
|
||||||
|
OECF = "OECF"
|
||||||
|
ShutterSpeedValue = "ShutterSpeedValue"
|
||||||
|
ApertureValue = "ApertureValue"
|
||||||
|
BrightnessValue = "BrightnessValue"
|
||||||
|
ExposureBiasValue = "ExposureBiasValue"
|
||||||
|
MaxApertureValue = "MaxApertureValue"
|
||||||
|
SubjectDistance = "SubjectDistance"
|
||||||
|
MeteringMode = "MeteringMode"
|
||||||
|
LightSource = "LightSource"
|
||||||
|
Flash = "Flash"
|
||||||
|
FocalLength = "FocalLength"
|
||||||
|
SubjectArea = "SubjectArea"
|
||||||
|
FlashEnergy = "FlashEnergy"
|
||||||
|
SpatialFrequencyResponse = "SpatialFrequencyResponse"
|
||||||
|
FocalPlaneXResolution = "FocalPlaneXResolution"
|
||||||
|
FocalPlaneYResolution = "FocalPlaneYResolution"
|
||||||
|
FocalPlaneResolutionUnit = "FocalPlaneResolutionUnit"
|
||||||
|
SubjectLocation = "SubjectLocation"
|
||||||
|
ExposureIndex = "ExposureIndex"
|
||||||
|
SensingMethod = "SensingMethod"
|
||||||
|
FileSource = "FileSource"
|
||||||
|
SceneType = "SceneType"
|
||||||
|
CFAPattern = "CFAPattern"
|
||||||
|
CustomRendered = "CustomRendered"
|
||||||
|
ExposureMode = "ExposureMode"
|
||||||
|
WhiteBalance = "WhiteBalance"
|
||||||
|
DigitalZoomRatio = "DigitalZoomRatio"
|
||||||
|
FocalLengthIn35mmFilm = "FocalLengthIn35mmFilm"
|
||||||
|
SceneCaptureType = "SceneCaptureType"
|
||||||
|
GainControl = "GainControl"
|
||||||
|
Contrast = "Contrast"
|
||||||
|
Saturation = "Saturation"
|
||||||
|
Sharpness = "Sharpness"
|
||||||
|
DeviceSettingDescription = "DeviceSettingDescription"
|
||||||
|
SubjectDistanceRange = "SubjectDistanceRange"
|
||||||
|
LensMake = "LensMake"
|
||||||
|
LensModel = "LensModel"
|
||||||
|
)
|
||||||
|
|
||||||
|
// thumbnail fields
|
||||||
|
const (
|
||||||
|
ThumbJPEGInterchangeFormat = "ThumbJPEGInterchangeFormat" // offset to thumb jpeg SOI
|
||||||
|
ThumbJPEGInterchangeFormatLength = "ThumbJPEGInterchangeFormatLength" // byte length of thumb
|
||||||
|
)
|
||||||
|
|
||||||
|
// GPS fields
|
||||||
|
const (
|
||||||
|
GPSVersionID FieldName = "GPSVersionID"
|
||||||
|
GPSLatitudeRef = "GPSLatitudeRef"
|
||||||
|
GPSLatitude = "GPSLatitude"
|
||||||
|
GPSLongitudeRef = "GPSLongitudeRef"
|
||||||
|
GPSLongitude = "GPSLongitude"
|
||||||
|
GPSAltitudeRef = "GPSAltitudeRef"
|
||||||
|
GPSAltitude = "GPSAltitude"
|
||||||
|
GPSTimeStamp = "GPSTimeStamp"
|
||||||
|
GPSSatelites = "GPSSatelites"
|
||||||
|
GPSStatus = "GPSStatus"
|
||||||
|
GPSMeasureMode = "GPSMeasureMode"
|
||||||
|
GPSDOP = "GPSDOP"
|
||||||
|
GPSSpeedRef = "GPSSpeedRef"
|
||||||
|
GPSSpeed = "GPSSpeed"
|
||||||
|
GPSTrackRef = "GPSTrackRef"
|
||||||
|
GPSTrack = "GPSTrack"
|
||||||
|
GPSImgDirectionRef = "GPSImgDirectionRef"
|
||||||
|
GPSImgDirection = "GPSImgDirection"
|
||||||
|
GPSMapDatum = "GPSMapDatum"
|
||||||
|
GPSDestLatitudeRef = "GPSDestLatitudeRef"
|
||||||
|
GPSDestLatitude = "GPSDestLatitude"
|
||||||
|
GPSDestLongitudeRef = "GPSDestLongitudeRef"
|
||||||
|
GPSDestLongitude = "GPSDestLongitude"
|
||||||
|
GPSDestBearingRef = "GPSDestBearingRef"
|
||||||
|
GPSDestBearing = "GPSDestBearing"
|
||||||
|
GPSDestDistanceRef = "GPSDestDistanceRef"
|
||||||
|
GPSDestDistance = "GPSDestDistance"
|
||||||
|
GPSProcessingMethod = "GPSProcessingMethod"
|
||||||
|
GPSAreaInformation = "GPSAreaInformation"
|
||||||
|
GPSDateStamp = "GPSDateStamp"
|
||||||
|
GPSDifferential = "GPSDifferential"
|
||||||
|
)
|
||||||
|
|
||||||
|
// interoperability fields
|
||||||
|
const (
|
||||||
|
InteroperabilityIndex FieldName = "InteroperabilityIndex"
|
||||||
|
)
|
||||||
|
|
||||||
|
var exifFields = map[uint16]FieldName{
|
||||||
|
/////////////////////////////////////
|
||||||
|
////////// IFD 0 ////////////////////
|
||||||
|
/////////////////////////////////////
|
||||||
|
|
||||||
|
// image data structure for the thumbnail
|
||||||
|
0x0100: ImageWidth,
|
||||||
|
0x0101: ImageLength,
|
||||||
|
0x0102: BitsPerSample,
|
||||||
|
0x0103: Compression,
|
||||||
|
0x0106: PhotometricInterpretation,
|
||||||
|
0x0112: Orientation,
|
||||||
|
0x0115: SamplesPerPixel,
|
||||||
|
0x011C: PlanarConfiguration,
|
||||||
|
0x0212: YCbCrSubSampling,
|
||||||
|
0x0213: YCbCrPositioning,
|
||||||
|
0x011A: XResolution,
|
||||||
|
0x011B: YResolution,
|
||||||
|
0x0128: ResolutionUnit,
|
||||||
|
|
||||||
|
// Other tags
|
||||||
|
0x0132: DateTime,
|
||||||
|
0x010E: ImageDescription,
|
||||||
|
0x010F: Make,
|
||||||
|
0x0110: Model,
|
||||||
|
0x0131: Software,
|
||||||
|
0x013B: Artist,
|
||||||
|
0x8298: Copyright,
|
||||||
|
|
||||||
|
// private tags
|
||||||
|
exifPointer: ExifIFDPointer,
|
||||||
|
|
||||||
|
/////////////////////////////////////
|
||||||
|
////////// Exif sub IFD /////////////
|
||||||
|
/////////////////////////////////////
|
||||||
|
|
||||||
|
gpsPointer: GPSInfoIFDPointer,
|
||||||
|
interopPointer: InteroperabilityIFDPointer,
|
||||||
|
|
||||||
|
0x9000: ExifVersion,
|
||||||
|
0xA000: FlashpixVersion,
|
||||||
|
|
||||||
|
0xA001: ColorSpace,
|
||||||
|
|
||||||
|
0x9101: ComponentsConfiguration,
|
||||||
|
0x9102: CompressedBitsPerPixel,
|
||||||
|
0xA002: PixelXDimension,
|
||||||
|
0xA003: PixelYDimension,
|
||||||
|
|
||||||
|
0x927C: MakerNote,
|
||||||
|
0x9286: UserComment,
|
||||||
|
|
||||||
|
0xA004: RelatedSoundFile,
|
||||||
|
0x9003: DateTimeOriginal,
|
||||||
|
0x9004: DateTimeDigitized,
|
||||||
|
0x9290: SubSecTime,
|
||||||
|
0x9291: SubSecTimeOriginal,
|
||||||
|
0x9292: SubSecTimeDigitized,
|
||||||
|
|
||||||
|
0xA420: ImageUniqueID,
|
||||||
|
|
||||||
|
// picture conditions
|
||||||
|
0x829A: ExposureTime,
|
||||||
|
0x829D: FNumber,
|
||||||
|
0x8822: ExposureProgram,
|
||||||
|
0x8824: SpectralSensitivity,
|
||||||
|
0x8827: ISOSpeedRatings,
|
||||||
|
0x8828: OECF,
|
||||||
|
0x9201: ShutterSpeedValue,
|
||||||
|
0x9202: ApertureValue,
|
||||||
|
0x9203: BrightnessValue,
|
||||||
|
0x9204: ExposureBiasValue,
|
||||||
|
0x9205: MaxApertureValue,
|
||||||
|
0x9206: SubjectDistance,
|
||||||
|
0x9207: MeteringMode,
|
||||||
|
0x9208: LightSource,
|
||||||
|
0x9209: Flash,
|
||||||
|
0x920A: FocalLength,
|
||||||
|
0x9214: SubjectArea,
|
||||||
|
0xA20B: FlashEnergy,
|
||||||
|
0xA20C: SpatialFrequencyResponse,
|
||||||
|
0xA20E: FocalPlaneXResolution,
|
||||||
|
0xA20F: FocalPlaneYResolution,
|
||||||
|
0xA210: FocalPlaneResolutionUnit,
|
||||||
|
0xA214: SubjectLocation,
|
||||||
|
0xA215: ExposureIndex,
|
||||||
|
0xA217: SensingMethod,
|
||||||
|
0xA300: FileSource,
|
||||||
|
0xA301: SceneType,
|
||||||
|
0xA302: CFAPattern,
|
||||||
|
0xA401: CustomRendered,
|
||||||
|
0xA402: ExposureMode,
|
||||||
|
0xA403: WhiteBalance,
|
||||||
|
0xA404: DigitalZoomRatio,
|
||||||
|
0xA405: FocalLengthIn35mmFilm,
|
||||||
|
0xA406: SceneCaptureType,
|
||||||
|
0xA407: GainControl,
|
||||||
|
0xA408: Contrast,
|
||||||
|
0xA409: Saturation,
|
||||||
|
0xA40A: Sharpness,
|
||||||
|
0xA40B: DeviceSettingDescription,
|
||||||
|
0xA40C: SubjectDistanceRange,
|
||||||
|
0xA433: LensMake,
|
||||||
|
0xA434: LensModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
var gpsFields = map[uint16]FieldName{
|
||||||
|
/////////////////////////////////////
|
||||||
|
//// GPS sub-IFD ////////////////////
|
||||||
|
/////////////////////////////////////
|
||||||
|
0x0: GPSVersionID,
|
||||||
|
0x1: GPSLatitudeRef,
|
||||||
|
0x2: GPSLatitude,
|
||||||
|
0x3: GPSLongitudeRef,
|
||||||
|
0x4: GPSLongitude,
|
||||||
|
0x5: GPSAltitudeRef,
|
||||||
|
0x6: GPSAltitude,
|
||||||
|
0x7: GPSTimeStamp,
|
||||||
|
0x8: GPSSatelites,
|
||||||
|
0x9: GPSStatus,
|
||||||
|
0xA: GPSMeasureMode,
|
||||||
|
0xB: GPSDOP,
|
||||||
|
0xC: GPSSpeedRef,
|
||||||
|
0xD: GPSSpeed,
|
||||||
|
0xE: GPSTrackRef,
|
||||||
|
0xF: GPSTrack,
|
||||||
|
0x10: GPSImgDirectionRef,
|
||||||
|
0x11: GPSImgDirection,
|
||||||
|
0x12: GPSMapDatum,
|
||||||
|
0x13: GPSDestLatitudeRef,
|
||||||
|
0x14: GPSDestLatitude,
|
||||||
|
0x15: GPSDestLongitudeRef,
|
||||||
|
0x16: GPSDestLongitude,
|
||||||
|
0x17: GPSDestBearingRef,
|
||||||
|
0x18: GPSDestBearing,
|
||||||
|
0x19: GPSDestDistanceRef,
|
||||||
|
0x1A: GPSDestDistance,
|
||||||
|
0x1B: GPSProcessingMethod,
|
||||||
|
0x1C: GPSAreaInformation,
|
||||||
|
0x1D: GPSDateStamp,
|
||||||
|
0x1E: GPSDifferential,
|
||||||
|
}
|
||||||
|
|
||||||
|
var interopFields = map[uint16]FieldName{
|
||||||
|
/////////////////////////////////////
|
||||||
|
//// Interoperability sub-IFD ///////
|
||||||
|
/////////////////////////////////////
|
||||||
|
0x1: InteroperabilityIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
var thumbnailFields = map[uint16]FieldName{
|
||||||
|
0x0201: ThumbJPEGInterchangeFormat,
|
||||||
|
0x0202: ThumbJPEGInterchangeFormatLength,
|
||||||
|
}
|
79
vendor/github.com/rwcarlsen/goexif/exif/regen_regress.go
generated
vendored
Normal file
79
vendor/github.com/rwcarlsen/goexif/exif/regen_regress.go
generated
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// +build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rwcarlsen/goexif/exif"
|
||||||
|
"github.com/rwcarlsen/goexif/tiff"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
fname := flag.Arg(0)
|
||||||
|
|
||||||
|
dst, err := os.Create(fname)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
|
||||||
|
dir, err := os.Open("samples")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
|
||||||
|
names, err := dir.Readdirnames(0)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, name := range names {
|
||||||
|
names[i] = filepath.Join("samples", name)
|
||||||
|
}
|
||||||
|
makeExpected(names, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeExpected(files []string, w io.Writer) {
|
||||||
|
fmt.Fprintf(w, "package exif\n\n")
|
||||||
|
fmt.Fprintf(w, "var regressExpected = map[string]map[FieldName]string{\n")
|
||||||
|
|
||||||
|
for _, name := range files {
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
x, err := exif.Decode(f)
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "\"%v\": map[FieldName]string{\n", filepath.Base(name))
|
||||||
|
x.Walk(®resswalk{w})
|
||||||
|
fmt.Fprintf(w, "},\n")
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "}")
|
||||||
|
}
|
||||||
|
|
||||||
|
type regresswalk struct {
|
||||||
|
wr io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *regresswalk) Walk(name exif.FieldName, tag *tiff.Tag) error {
|
||||||
|
if strings.HasPrefix(string(name), exif.UnknownPrefix) {
|
||||||
|
fmt.Fprintf(w.wr, "\"%v\": `%v`,\n", name, tag.String())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w.wr, "%v: `%v`,\n", name, tag.String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
BIN
vendor/github.com/rwcarlsen/goexif/exif/sample1.jpg
generated
vendored
Normal file
BIN
vendor/github.com/rwcarlsen/goexif/exif/sample1.jpg
generated
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
BIN
vendor/github.com/rwcarlsen/goexif/tiff/sample1.tif
generated
vendored
Normal file
BIN
vendor/github.com/rwcarlsen/goexif/tiff/sample1.tif
generated
vendored
Normal file
Binary file not shown.
438
vendor/github.com/rwcarlsen/goexif/tiff/tag.go
generated
vendored
Normal file
438
vendor/github.com/rwcarlsen/goexif/tiff/tag.go
generated
vendored
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
package tiff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Format specifies the Go type equivalent used to represent the basic
|
||||||
|
// tiff data types.
|
||||||
|
type Format int
|
||||||
|
|
||||||
|
const (
|
||||||
|
IntVal Format = iota
|
||||||
|
FloatVal
|
||||||
|
RatVal
|
||||||
|
StringVal
|
||||||
|
UndefVal
|
||||||
|
OtherVal
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrShortReadTagValue = errors.New("tiff: short read of tag value")
|
||||||
|
|
||||||
|
var formatNames = map[Format]string{
|
||||||
|
IntVal: "int",
|
||||||
|
FloatVal: "float",
|
||||||
|
RatVal: "rational",
|
||||||
|
StringVal: "string",
|
||||||
|
UndefVal: "undefined",
|
||||||
|
OtherVal: "other",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataType represents the basic tiff tag data types.
|
||||||
|
type DataType uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
DTByte DataType = 1
|
||||||
|
DTAscii = 2
|
||||||
|
DTShort = 3
|
||||||
|
DTLong = 4
|
||||||
|
DTRational = 5
|
||||||
|
DTSByte = 6
|
||||||
|
DTUndefined = 7
|
||||||
|
DTSShort = 8
|
||||||
|
DTSLong = 9
|
||||||
|
DTSRational = 10
|
||||||
|
DTFloat = 11
|
||||||
|
DTDouble = 12
|
||||||
|
)
|
||||||
|
|
||||||
|
var typeNames = map[DataType]string{
|
||||||
|
DTByte: "byte",
|
||||||
|
DTAscii: "ascii",
|
||||||
|
DTShort: "short",
|
||||||
|
DTLong: "long",
|
||||||
|
DTRational: "rational",
|
||||||
|
DTSByte: "signed byte",
|
||||||
|
DTUndefined: "undefined",
|
||||||
|
DTSShort: "signed short",
|
||||||
|
DTSLong: "signed long",
|
||||||
|
DTSRational: "signed rational",
|
||||||
|
DTFloat: "float",
|
||||||
|
DTDouble: "double",
|
||||||
|
}
|
||||||
|
|
||||||
|
// typeSize specifies the size in bytes of each type.
|
||||||
|
var typeSize = map[DataType]uint32{
|
||||||
|
DTByte: 1,
|
||||||
|
DTAscii: 1,
|
||||||
|
DTShort: 2,
|
||||||
|
DTLong: 4,
|
||||||
|
DTRational: 8,
|
||||||
|
DTSByte: 1,
|
||||||
|
DTUndefined: 1,
|
||||||
|
DTSShort: 2,
|
||||||
|
DTSLong: 4,
|
||||||
|
DTSRational: 8,
|
||||||
|
DTFloat: 4,
|
||||||
|
DTDouble: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag reflects the parsed content of a tiff IFD tag.
|
||||||
|
type Tag struct {
|
||||||
|
// Id is the 2-byte tiff tag identifier.
|
||||||
|
Id uint16
|
||||||
|
// Type is an integer (1 through 12) indicating the tag value's data type.
|
||||||
|
Type DataType
|
||||||
|
// Count is the number of type Type stored in the tag's value (i.e. the
|
||||||
|
// tag's value is an array of type Type and length Count).
|
||||||
|
Count uint32
|
||||||
|
// Val holds the bytes that represent the tag's value.
|
||||||
|
Val []byte
|
||||||
|
// ValOffset holds byte offset of the tag value w.r.t. the beginning of the
|
||||||
|
// reader it was decoded from. Zero if the tag value fit inside the offset
|
||||||
|
// field.
|
||||||
|
ValOffset uint32
|
||||||
|
|
||||||
|
order binary.ByteOrder
|
||||||
|
intVals []int64
|
||||||
|
floatVals []float64
|
||||||
|
ratVals [][]int64
|
||||||
|
strVal string
|
||||||
|
format Format
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeTag parses a tiff-encoded IFD tag from r and returns a Tag object. The
|
||||||
|
// first read from r should be the first byte of the tag. ReadAt offsets should
|
||||||
|
// generally be relative to the beginning of the tiff structure (not relative
|
||||||
|
// to the beginning of the tag).
|
||||||
|
func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) {
|
||||||
|
t := new(Tag)
|
||||||
|
t.order = order
|
||||||
|
|
||||||
|
err := binary.Read(r, order, &t.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("tiff: tag id read failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = binary.Read(r, order, &t.Type)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("tiff: tag type read failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = binary.Read(r, order, &t.Count)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("tiff: tag component count read failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// There seems to be a relatively common corrupt tag which has a Count of
|
||||||
|
// MaxUint32. This is probably not a valid value, so return early.
|
||||||
|
if t.Count == 1<<32-1 {
|
||||||
|
return t, errors.New("invalid Count offset in tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
valLen := typeSize[t.Type] * t.Count
|
||||||
|
if valLen == 0 {
|
||||||
|
return t, errors.New("zero length tag value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if valLen > 4 {
|
||||||
|
binary.Read(r, order, &t.ValOffset)
|
||||||
|
|
||||||
|
// Use a bytes.Buffer so we don't allocate a huge slice if the tag
|
||||||
|
// is corrupt.
|
||||||
|
var buff bytes.Buffer
|
||||||
|
sr := io.NewSectionReader(r, int64(t.ValOffset), int64(valLen))
|
||||||
|
n, err := io.Copy(&buff, sr)
|
||||||
|
if err != nil {
|
||||||
|
return t, errors.New("tiff: tag value read failed: " + err.Error())
|
||||||
|
} else if n != int64(valLen) {
|
||||||
|
return t, ErrShortReadTagValue
|
||||||
|
}
|
||||||
|
t.Val = buff.Bytes()
|
||||||
|
|
||||||
|
} else {
|
||||||
|
val := make([]byte, valLen)
|
||||||
|
if _, err = io.ReadFull(r, val); err != nil {
|
||||||
|
return t, errors.New("tiff: tag offset read failed: " + err.Error())
|
||||||
|
}
|
||||||
|
// ignore padding.
|
||||||
|
if _, err = io.ReadFull(r, make([]byte, 4-valLen)); err != nil {
|
||||||
|
return t, errors.New("tiff: tag offset read failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Val = val
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, t.convertVals()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tag) convertVals() error {
|
||||||
|
r := bytes.NewReader(t.Val)
|
||||||
|
|
||||||
|
switch t.Type {
|
||||||
|
case DTAscii:
|
||||||
|
if len(t.Val) > 0 {
|
||||||
|
t.strVal = string(t.Val[:len(t.Val)-1]) // ignore the last byte (NULL).
|
||||||
|
}
|
||||||
|
case DTByte:
|
||||||
|
var v uint8
|
||||||
|
t.intVals = make([]int64, int(t.Count))
|
||||||
|
for i := range t.intVals {
|
||||||
|
err := binary.Read(r, t.order, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.intVals[i] = int64(v)
|
||||||
|
}
|
||||||
|
case DTShort:
|
||||||
|
var v uint16
|
||||||
|
t.intVals = make([]int64, int(t.Count))
|
||||||
|
for i := range t.intVals {
|
||||||
|
err := binary.Read(r, t.order, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.intVals[i] = int64(v)
|
||||||
|
}
|
||||||
|
case DTLong:
|
||||||
|
var v uint32
|
||||||
|
t.intVals = make([]int64, int(t.Count))
|
||||||
|
for i := range t.intVals {
|
||||||
|
err := binary.Read(r, t.order, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.intVals[i] = int64(v)
|
||||||
|
}
|
||||||
|
case DTSByte:
|
||||||
|
var v int8
|
||||||
|
t.intVals = make([]int64, int(t.Count))
|
||||||
|
for i := range t.intVals {
|
||||||
|
err := binary.Read(r, t.order, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.intVals[i] = int64(v)
|
||||||
|
}
|
||||||
|
case DTSShort:
|
||||||
|
var v int16
|
||||||
|
t.intVals = make([]int64, int(t.Count))
|
||||||
|
for i := range t.intVals {
|
||||||
|
err := binary.Read(r, t.order, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.intVals[i] = int64(v)
|
||||||
|
}
|
||||||
|
case DTSLong:
|
||||||
|
var v int32
|
||||||
|
t.intVals = make([]int64, int(t.Count))
|
||||||
|
for i := range t.intVals {
|
||||||
|
err := binary.Read(r, t.order, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.intVals[i] = int64(v)
|
||||||
|
}
|
||||||
|
case DTRational:
|
||||||
|
t.ratVals = make([][]int64, int(t.Count))
|
||||||
|
for i := range t.ratVals {
|
||||||
|
var n, d uint32
|
||||||
|
err := binary.Read(r, t.order, &n)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Read(r, t.order, &d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.ratVals[i] = []int64{int64(n), int64(d)}
|
||||||
|
}
|
||||||
|
case DTSRational:
|
||||||
|
t.ratVals = make([][]int64, int(t.Count))
|
||||||
|
for i := range t.ratVals {
|
||||||
|
var n, d int32
|
||||||
|
err := binary.Read(r, t.order, &n)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Read(r, t.order, &d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.ratVals[i] = []int64{int64(n), int64(d)}
|
||||||
|
}
|
||||||
|
case DTFloat: // float32
|
||||||
|
t.floatVals = make([]float64, int(t.Count))
|
||||||
|
for i := range t.floatVals {
|
||||||
|
var v float32
|
||||||
|
err := binary.Read(r, t.order, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.floatVals[i] = float64(v)
|
||||||
|
}
|
||||||
|
case DTDouble:
|
||||||
|
t.floatVals = make([]float64, int(t.Count))
|
||||||
|
for i := range t.floatVals {
|
||||||
|
var u float64
|
||||||
|
err := binary.Read(r, t.order, &u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.floatVals[i] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t.Type {
|
||||||
|
case DTByte, DTShort, DTLong, DTSByte, DTSShort, DTSLong:
|
||||||
|
t.format = IntVal
|
||||||
|
case DTRational, DTSRational:
|
||||||
|
t.format = RatVal
|
||||||
|
case DTFloat, DTDouble:
|
||||||
|
t.format = FloatVal
|
||||||
|
case DTAscii:
|
||||||
|
t.format = StringVal
|
||||||
|
case DTUndefined:
|
||||||
|
t.format = UndefVal
|
||||||
|
default:
|
||||||
|
t.format = OtherVal
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format returns a value indicating which method can be called to retrieve the
|
||||||
|
// tag's value properly typed (e.g. integer, rational, etc.).
|
||||||
|
func (t *Tag) Format() Format { return t.format }
|
||||||
|
|
||||||
|
func (t *Tag) typeErr(to Format) error {
|
||||||
|
return &wrongFmtErr{typeNames[t.Type], formatNames[to]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rat returns the tag's i'th value as a rational number. It returns a nil and
|
||||||
|
// an error if this tag's Format is not RatVal. It panics for zero deminators
|
||||||
|
// or if i is out of range.
|
||||||
|
func (t *Tag) Rat(i int) (*big.Rat, error) {
|
||||||
|
n, d, err := t.Rat2(i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return big.NewRat(n, d), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rat2 returns the tag's i'th value as a rational number represented by a
|
||||||
|
// numerator-denominator pair. It returns an error if the tag's Format is not
|
||||||
|
// RatVal. It panics if i is out of range.
|
||||||
|
func (t *Tag) Rat2(i int) (num, den int64, err error) {
|
||||||
|
if t.format != RatVal {
|
||||||
|
return 0, 0, t.typeErr(RatVal)
|
||||||
|
}
|
||||||
|
return t.ratVals[i][0], t.ratVals[i][1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 returns the tag's i'th value as an integer. It returns an error if the
|
||||||
|
// tag's Format is not IntVal. It panics if i is out of range.
|
||||||
|
func (t *Tag) Int64(i int) (int64, error) {
|
||||||
|
if t.format != IntVal {
|
||||||
|
return 0, t.typeErr(IntVal)
|
||||||
|
}
|
||||||
|
return t.intVals[i], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int returns the tag's i'th value as an integer. It returns an error if the
|
||||||
|
// tag's Format is not IntVal. It panics if i is out of range.
|
||||||
|
func (t *Tag) Int(i int) (int, error) {
|
||||||
|
if t.format != IntVal {
|
||||||
|
return 0, t.typeErr(IntVal)
|
||||||
|
}
|
||||||
|
return int(t.intVals[i]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float returns the tag's i'th value as a float. It returns an error if the
|
||||||
|
// tag's Format is not IntVal. It panics if i is out of range.
|
||||||
|
func (t *Tag) Float(i int) (float64, error) {
|
||||||
|
if t.format != FloatVal {
|
||||||
|
return 0, t.typeErr(FloatVal)
|
||||||
|
}
|
||||||
|
return t.floatVals[i], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringVal returns the tag's value as a string. It returns an error if the
|
||||||
|
// tag's Format is not StringVal. It panics if i is out of range.
|
||||||
|
func (t *Tag) StringVal() (string, error) {
|
||||||
|
if t.format != StringVal {
|
||||||
|
return "", t.typeErr(StringVal)
|
||||||
|
}
|
||||||
|
return t.strVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a nicely formatted version of the tag.
|
||||||
|
func (t *Tag) String() string {
|
||||||
|
data, err := t.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return "ERROR: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Count == 1 {
|
||||||
|
return strings.Trim(fmt.Sprintf("%s", data), "[]")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tag) MarshalJSON() ([]byte, error) {
|
||||||
|
switch t.format {
|
||||||
|
case StringVal, UndefVal:
|
||||||
|
return nullString(t.Val), nil
|
||||||
|
case OtherVal:
|
||||||
|
return []byte(fmt.Sprintf("unknown tag type '%v'", t.Type)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := []string{}
|
||||||
|
for i := 0; i < int(t.Count); i++ {
|
||||||
|
switch t.format {
|
||||||
|
case RatVal:
|
||||||
|
n, d, _ := t.Rat2(i)
|
||||||
|
rv = append(rv, fmt.Sprintf(`"%v/%v"`, n, d))
|
||||||
|
case FloatVal:
|
||||||
|
v, _ := t.Float(i)
|
||||||
|
rv = append(rv, fmt.Sprintf("%v", v))
|
||||||
|
case IntVal:
|
||||||
|
v, _ := t.Int(i)
|
||||||
|
rv = append(rv, fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprintf(`[%s]`, strings.Join(rv, ","))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullString(in []byte) []byte {
|
||||||
|
rv := bytes.Buffer{}
|
||||||
|
rv.WriteByte('"')
|
||||||
|
for _, b := range in {
|
||||||
|
if unicode.IsPrint(rune(b)) {
|
||||||
|
rv.WriteByte(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rv.WriteByte('"')
|
||||||
|
rvb := rv.Bytes()
|
||||||
|
if utf8.Valid(rvb) {
|
||||||
|
return rvb
|
||||||
|
}
|
||||||
|
return []byte(`""`)
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrongFmtErr struct {
|
||||||
|
From, To string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *wrongFmtErr) Error() string {
|
||||||
|
return fmt.Sprintf("cannot convert tag type '%v' into '%v'", e.From, e.To)
|
||||||
|
}
|
153
vendor/github.com/rwcarlsen/goexif/tiff/tiff.go
generated
vendored
Normal file
153
vendor/github.com/rwcarlsen/goexif/tiff/tiff.go
generated
vendored
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
// Package tiff implements TIFF decoding as defined in TIFF 6.0 specification at
|
||||||
|
// http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf
|
||||||
|
package tiff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadAtReader is used when decoding Tiff tags and directories
|
||||||
|
type ReadAtReader interface {
|
||||||
|
io.Reader
|
||||||
|
io.ReaderAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tiff provides access to a decoded tiff data structure.
|
||||||
|
type Tiff struct {
|
||||||
|
// Dirs is an ordered slice of the tiff's Image File Directories (IFDs).
|
||||||
|
// The IFD at index 0 is IFD0.
|
||||||
|
Dirs []*Dir
|
||||||
|
// The tiff's byte-encoding (i.e. big/little endian).
|
||||||
|
Order binary.ByteOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode parses tiff-encoded data from r and returns a Tiff struct that
|
||||||
|
// reflects the structure and content of the tiff data. The first read from r
|
||||||
|
// should be the first byte of the tiff-encoded data and not necessarily the
|
||||||
|
// first byte of an os.File object.
|
||||||
|
func Decode(r io.Reader) (*Tiff, error) {
|
||||||
|
data, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("tiff: could not read data")
|
||||||
|
}
|
||||||
|
buf := bytes.NewReader(data)
|
||||||
|
|
||||||
|
t := new(Tiff)
|
||||||
|
|
||||||
|
// read byte order
|
||||||
|
bo := make([]byte, 2)
|
||||||
|
if _, err = io.ReadFull(buf, bo); err != nil {
|
||||||
|
return nil, errors.New("tiff: could not read tiff byte order")
|
||||||
|
}
|
||||||
|
if string(bo) == "II" {
|
||||||
|
t.Order = binary.LittleEndian
|
||||||
|
} else if string(bo) == "MM" {
|
||||||
|
t.Order = binary.BigEndian
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("tiff: could not read tiff byte order")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for special tiff marker
|
||||||
|
var sp int16
|
||||||
|
err = binary.Read(buf, t.Order, &sp)
|
||||||
|
if err != nil || 42 != sp {
|
||||||
|
return nil, errors.New("tiff: could not find special tiff marker")
|
||||||
|
}
|
||||||
|
|
||||||
|
// load offset to first IFD
|
||||||
|
var offset int32
|
||||||
|
err = binary.Read(buf, t.Order, &offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("tiff: could not read offset to first IFD")
|
||||||
|
}
|
||||||
|
|
||||||
|
// load IFD's
|
||||||
|
var d *Dir
|
||||||
|
prev := offset
|
||||||
|
for offset != 0 {
|
||||||
|
// seek to offset
|
||||||
|
_, err := buf.Seek(int64(offset), 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("tiff: seek to IFD failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.Len() == 0 {
|
||||||
|
return nil, errors.New("tiff: seek offset after EOF")
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the dir
|
||||||
|
d, offset, err = DecodeDir(buf, t.Order)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset == prev {
|
||||||
|
return nil, errors.New("tiff: recursive IFD")
|
||||||
|
}
|
||||||
|
prev = offset
|
||||||
|
|
||||||
|
t.Dirs = append(t.Dirs, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tf *Tiff) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprint(&buf, "Tiff{")
|
||||||
|
for _, d := range tf.Dirs {
|
||||||
|
fmt.Fprintf(&buf, "%s, ", d.String())
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&buf, "}")
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dir provides access to the parsed content of a tiff Image File Directory (IFD).
|
||||||
|
type Dir struct {
|
||||||
|
Tags []*Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeDir parses a tiff-encoded IFD from r and returns a Dir object. offset
|
||||||
|
// is the offset to the next IFD. The first read from r should be at the first
|
||||||
|
// byte of the IFD. ReadAt offsets should generally be relative to the
|
||||||
|
// beginning of the tiff structure (not relative to the beginning of the IFD).
|
||||||
|
func DecodeDir(r ReadAtReader, order binary.ByteOrder) (d *Dir, offset int32, err error) {
|
||||||
|
d = new(Dir)
|
||||||
|
|
||||||
|
// get num of tags in ifd
|
||||||
|
var nTags int16
|
||||||
|
err = binary.Read(r, order, &nTags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.New("tiff: failed to read IFD tag count: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// load tags
|
||||||
|
for n := 0; n < int(nTags); n++ {
|
||||||
|
t, err := DecodeTag(r, order)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
d.Tags = append(d.Tags, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get offset to next ifd
|
||||||
|
err = binary.Read(r, order, &offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.New("tiff: falied to read offset to next IFD: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dir) String() string {
|
||||||
|
s := "Dir{"
|
||||||
|
for _, t := range d.Tags {
|
||||||
|
s += t.String() + ", "
|
||||||
|
}
|
||||||
|
return s + "}"
|
||||||
|
}
|
19
vendor/vendor.json
vendored
Normal file
19
vendor/vendor.json
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"comment": "",
|
||||||
|
"ignore": "test",
|
||||||
|
"package": [
|
||||||
|
{
|
||||||
|
"checksumSHA1": "Q68FxXX04PtPDIG7C1RlSDhx/ko=",
|
||||||
|
"path": "github.com/rwcarlsen/goexif/exif",
|
||||||
|
"revision": "709fab3d192d7c62f86043caff1e7e3fb0f42bd8",
|
||||||
|
"revisionTime": "2015-05-20T14:06:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "0+tTLlssYWyGuq+vQs4IiPIXJW4=",
|
||||||
|
"path": "github.com/rwcarlsen/goexif/tiff",
|
||||||
|
"revision": "709fab3d192d7c62f86043caff1e7e3fb0f42bd8",
|
||||||
|
"revisionTime": "2015-05-20T14:06:47Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rootPath": "mcquay.me/arrange"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user