2019-08-29 04:18:51 -04:00
|
|
|
// Copyright 2019 The Hugo Authors. All rights reserved.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
package exif
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"math/big"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
"unicode"
|
|
|
|
"unicode/utf8"
|
|
|
|
|
2019-09-01 11:27:21 -04:00
|
|
|
"github.com/bep/tmc"
|
|
|
|
|
2019-08-29 04:18:51 -04:00
|
|
|
_exif "github.com/rwcarlsen/goexif/exif"
|
|
|
|
"github.com/rwcarlsen/goexif/tiff"
|
|
|
|
)
|
|
|
|
|
|
|
|
const exifTimeLayout = "2006:01:02 15:04:05"
|
|
|
|
|
2022-04-21 04:59:13 -04:00
|
|
|
// ExifInfo holds the decoded Exif data for an Image.
|
|
|
|
type ExifInfo struct {
|
|
|
|
// GPS latitude in degrees.
|
|
|
|
Lat float64
|
|
|
|
|
|
|
|
// GPS longitude in degrees.
|
2019-09-01 11:27:21 -04:00
|
|
|
Long float64
|
2022-04-21 04:59:13 -04:00
|
|
|
|
|
|
|
// Image creation date/time.
|
2019-09-01 11:27:21 -04:00
|
|
|
Date time.Time
|
2022-04-21 04:59:13 -04:00
|
|
|
|
|
|
|
// A collection of the available Exif tags for this Image.
|
2019-09-01 11:27:21 -04:00
|
|
|
Tags Tags
|
2019-08-29 04:18:51 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
type Decoder struct {
|
|
|
|
includeFieldsRe *regexp.Regexp
|
|
|
|
excludeFieldsrRe *regexp.Regexp
|
|
|
|
noDate bool
|
|
|
|
noLatLong bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func IncludeFields(expression string) func(*Decoder) error {
|
|
|
|
return func(d *Decoder) error {
|
|
|
|
re, err := compileRegexp(expression)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
d.includeFieldsRe = re
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func ExcludeFields(expression string) func(*Decoder) error {
|
|
|
|
return func(d *Decoder) error {
|
|
|
|
re, err := compileRegexp(expression)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
d.excludeFieldsrRe = re
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithLatLongDisabled(disabled bool) func(*Decoder) error {
|
|
|
|
return func(d *Decoder) error {
|
|
|
|
d.noLatLong = disabled
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithDateDisabled(disabled bool) func(*Decoder) error {
|
|
|
|
return func(d *Decoder) error {
|
|
|
|
d.noDate = disabled
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func compileRegexp(expression string) (*regexp.Regexp, error) {
|
|
|
|
expression = strings.TrimSpace(expression)
|
|
|
|
if expression == "" {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
if !strings.HasPrefix(expression, "(") {
|
|
|
|
// Make it case insensitive
|
|
|
|
expression = "(?i)" + expression
|
|
|
|
}
|
|
|
|
|
|
|
|
return regexp.Compile(expression)
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewDecoder(options ...func(*Decoder) error) (*Decoder, error) {
|
|
|
|
d := &Decoder{}
|
|
|
|
for _, opt := range options {
|
|
|
|
if err := opt(d); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return d, nil
|
|
|
|
}
|
|
|
|
|
2022-04-21 04:59:13 -04:00
|
|
|
func (d *Decoder) Decode(r io.Reader) (ex *ExifInfo, err error) {
|
2019-08-29 04:18:51 -04:00
|
|
|
defer func() {
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
err = fmt.Errorf("Exif failed: %v", r)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
var x *_exif.Exif
|
|
|
|
x, err = _exif.Decode(r)
|
|
|
|
if err != nil {
|
|
|
|
if err.Error() == "EOF" {
|
|
|
|
// Found no Exif
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var tm time.Time
|
|
|
|
var lat, long float64
|
|
|
|
|
|
|
|
if !d.noDate {
|
|
|
|
tm, _ = x.DateTime()
|
|
|
|
}
|
|
|
|
|
|
|
|
if !d.noLatLong {
|
|
|
|
lat, long, _ = x.LatLong()
|
|
|
|
}
|
|
|
|
|
2022-03-17 17:03:27 -04:00
|
|
|
walker := &exifWalker{x: x, vals: make(map[string]any), includeMatcher: d.includeFieldsRe, excludeMatcher: d.excludeFieldsrRe}
|
2019-08-29 04:18:51 -04:00
|
|
|
if err = x.Walk(walker); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-21 04:59:13 -04:00
|
|
|
ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: walker.vals}
|
2019-08-29 04:18:51 -04:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-17 17:03:27 -04:00
|
|
|
func decodeTag(x *_exif.Exif, f _exif.FieldName, t *tiff.Tag) (any, error) {
|
2019-08-29 04:18:51 -04:00
|
|
|
switch t.Format() {
|
|
|
|
case tiff.StringVal, tiff.UndefVal:
|
|
|
|
s := nullString(t.Val)
|
|
|
|
if strings.Contains(string(f), "DateTime") {
|
|
|
|
if d, err := tryParseDate(x, s); err == nil {
|
|
|
|
return d, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return s, nil
|
|
|
|
case tiff.OtherVal:
|
|
|
|
return "unknown", nil
|
|
|
|
}
|
|
|
|
|
2022-03-17 17:03:27 -04:00
|
|
|
var rv []any
|
2019-08-29 04:18:51 -04:00
|
|
|
|
|
|
|
for i := 0; i < int(t.Count); i++ {
|
|
|
|
switch t.Format() {
|
|
|
|
case tiff.RatVal:
|
|
|
|
n, d, _ := t.Rat2(i)
|
|
|
|
rat := big.NewRat(n, d)
|
|
|
|
if n == 1 {
|
|
|
|
rv = append(rv, rat)
|
|
|
|
} else {
|
|
|
|
f, _ := rat.Float64()
|
|
|
|
rv = append(rv, f)
|
|
|
|
}
|
|
|
|
|
|
|
|
case tiff.FloatVal:
|
|
|
|
v, _ := t.Float(i)
|
|
|
|
rv = append(rv, v)
|
|
|
|
case tiff.IntVal:
|
|
|
|
v, _ := t.Int(i)
|
|
|
|
rv = append(rv, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if t.Count == 1 {
|
|
|
|
if len(rv) == 1 {
|
|
|
|
return rv[0], nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return rv, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Code borrowed from exif.DateTime and adjusted.
|
|
|
|
func tryParseDate(x *_exif.Exif, s string) (time.Time, error) {
|
|
|
|
dateStr := strings.TrimRight(s, "\x00")
|
|
|
|
// TODO(bep): look for timezone offset, GPS time, etc.
|
|
|
|
timeZone := time.Local
|
|
|
|
if tz, _ := x.TimeZone(); tz != nil {
|
|
|
|
timeZone = tz
|
|
|
|
}
|
|
|
|
return time.ParseInLocation(exifTimeLayout, dateStr, timeZone)
|
|
|
|
}
|
|
|
|
|
|
|
|
type exifWalker struct {
|
|
|
|
x *_exif.Exif
|
2022-03-17 17:03:27 -04:00
|
|
|
vals map[string]any
|
2019-08-29 04:18:51 -04:00
|
|
|
includeMatcher *regexp.Regexp
|
|
|
|
excludeMatcher *regexp.Regexp
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *exifWalker) Walk(f _exif.FieldName, tag *tiff.Tag) error {
|
|
|
|
name := string(f)
|
|
|
|
if e.excludeMatcher != nil && e.excludeMatcher.MatchString(name) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if e.includeMatcher != nil && !e.includeMatcher.MatchString(name) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
val, err := decodeTag(e.x, f, tag)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
e.vals[name] = val
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func nullString(in []byte) string {
|
|
|
|
var rv bytes.Buffer
|
2021-03-13 13:45:09 -05:00
|
|
|
for len(in) > 0 {
|
|
|
|
r, size := utf8.DecodeRune(in)
|
|
|
|
if unicode.IsGraphic(r) {
|
|
|
|
rv.WriteRune(r)
|
2019-08-29 04:18:51 -04:00
|
|
|
}
|
2021-03-13 13:45:09 -05:00
|
|
|
in = in[size:]
|
2019-08-29 04:18:51 -04:00
|
|
|
}
|
2021-03-13 13:45:09 -05:00
|
|
|
return rv.String()
|
2019-08-29 04:18:51 -04:00
|
|
|
}
|
2019-09-01 11:27:21 -04:00
|
|
|
|
|
|
|
var tcodec *tmc.Codec
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
var err error
|
|
|
|
tcodec, err = tmc.New()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-17 17:03:27 -04:00
|
|
|
type Tags map[string]any
|
2019-09-01 11:27:21 -04:00
|
|
|
|
|
|
|
func (v *Tags) UnmarshalJSON(b []byte) error {
|
2022-03-17 17:03:27 -04:00
|
|
|
vv := make(map[string]any)
|
2019-09-01 11:27:21 -04:00
|
|
|
if err := tcodec.Unmarshal(b, &vv); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
*v = vv
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v Tags) MarshalJSON() ([]byte, error) {
|
|
|
|
return tcodec.Marshal(v)
|
|
|
|
}
|