Switch EXIF library

Closes #10855
Closes #8586
Closes #8996
This commit is contained in:
Bjørn Erik Pedersen
2024-07-07 12:54:30 +02:00
parent a28bed0817
commit 72ff937e11
12 changed files with 297 additions and 194 deletions

View File

@@ -14,25 +14,18 @@
package exif
import (
"bytes"
"fmt"
"io"
"math"
"math/big"
"regexp"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/bep/imagemeta"
"github.com/bep/logg"
"github.com/bep/tmc"
_exif "github.com/rwcarlsen/goexif/exif"
"github.com/rwcarlsen/goexif/tiff"
)
const exifTimeLayout = "2006:01:02 15:04:05"
// ExifInfo holds the decoded Exif data for an Image.
type ExifInfo struct {
// GPS latitude in degrees.
@@ -53,6 +46,15 @@ type Decoder struct {
excludeFieldsrRe *regexp.Regexp
noDate bool
noLatLong bool
warnl logg.LevelLogger
}
func (d *Decoder) shouldInclude(s string) bool {
return (d.includeFieldsRe == nil || d.includeFieldsRe.MatchString(s))
}
func (d *Decoder) shouldExclude(s string) bool {
return d.excludeFieldsrRe != nil && d.excludeFieldsrRe.MatchString(s)
}
func IncludeFields(expression string) func(*Decoder) error {
@@ -91,6 +93,13 @@ func WithDateDisabled(disabled bool) func(*Decoder) error {
}
}
func WithWarnLogger(warnl logg.LevelLogger) func(*Decoder) error {
return func(d *Decoder) error {
d.warnl = warnl
return nil
}
}
func compileRegexp(expression string) (*regexp.Regexp, error) {
expression = strings.TrimSpace(expression)
if expression == "" {
@@ -115,148 +124,222 @@ func NewDecoder(options ...func(*Decoder) error) (*Decoder, error) {
return d, nil
}
func (d *Decoder) Decode(r io.Reader) (ex *ExifInfo, err error) {
var (
isTimeTag = func(s string) bool {
return strings.Contains(s, "Time")
}
isGPSTag = func(s string) bool {
return strings.HasPrefix(s, "GPS")
}
)
// Filename is only used for logging.
func (d *Decoder) Decode(filename string, format imagemeta.ImageFormat, r io.Reader) (ex *ExifInfo, err error) {
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 tagInfos imagemeta.Tags
handleTag := func(ti imagemeta.TagInfo) error {
tagInfos.Add(ti)
return nil
}
shouldInclude := func(ti imagemeta.TagInfo) bool {
if ti.Source == imagemeta.EXIF {
if !d.noDate {
// We need the time tags to calculate the date.
if isTimeTag(ti.Tag) {
return true
}
}
if !d.noLatLong {
// We need to GPS tags to calculate the lat/long.
if isGPSTag(ti.Tag) {
return true
}
}
if !strings.HasPrefix(ti.Namespace, "IFD0") {
// Drop thumbnail tags.
return false
}
}
if d.shouldExclude(ti.Tag) {
return false
}
return d.shouldInclude(ti.Tag)
}
var warnf func(string, ...any)
if d.warnl != nil {
// There should be very little warnings (fingers crossed!),
// but this will typically be unrecognized formats.
// To be able to possibly get rid of these warnings,
// we need to know what images are causing them.
warnf = func(format string, args ...any) {
format = fmt.Sprintf("%q: %s: ", filename, format)
d.warnl.Logf(format, args...)
}
}
err = imagemeta.Decode(
imagemeta.Options{
R: r.(io.ReadSeeker),
ImageFormat: format,
ShouldHandleTag: shouldInclude,
HandleTag: handleTag,
Sources: imagemeta.EXIF, // For now. TODO(bep)
Warnf: warnf,
},
)
var tm time.Time
var lat, long float64
if !d.noDate {
tm, _ = x.DateTime()
tm, _ = tagInfos.GetDateTime()
}
if !d.noLatLong {
lat, long, _ = x.LatLong()
if math.IsNaN(lat) {
lat = 0
}
if math.IsNaN(long) {
long = 0
}
lat, long, _ = tagInfos.GetLatLong()
}
walker := &exifWalker{x: x, vals: make(map[string]any), includeMatcher: d.includeFieldsRe, excludeMatcher: d.excludeFieldsrRe}
if err = x.Walk(walker); err != nil {
return
tags := make(map[string]any)
for k, v := range tagInfos.All() {
if d.shouldExclude(k) {
continue
}
if !d.shouldInclude(k) {
continue
}
tags[k] = v.Value
}
ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: walker.vals}
ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: tags}
return
}
func decodeTag(x *_exif.Exif, f _exif.FieldName, t *tiff.Tag) (any, error) {
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
}
var rv []any
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 t is int or t > 1, use float64
if rat.IsInt() || rat.Cmp(big.NewRat(1, 1)) == 1 {
f, _ := rat.Float64()
rv = append(rv, f)
} else {
rv = append(rv, rat)
}
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
vals map[string]any
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
for len(in) > 0 {
r, size := utf8.DecodeRune(in)
if unicode.IsGraphic(r) {
rv.WriteRune(r)
}
in = in[size:]
}
return rv.String()
}
var tcodec *tmc.Codec
func init() {
newIntadapter := func(target any) tmc.Adapter {
var bitSize int
var isSigned bool
switch target.(type) {
case int:
bitSize = 0
isSigned = true
case int8:
bitSize = 8
isSigned = true
case int16:
bitSize = 16
isSigned = true
case int32:
bitSize = 32
isSigned = true
case int64:
bitSize = 64
isSigned = true
case uint:
bitSize = 0
case uint8:
bitSize = 8
case uint16:
bitSize = 16
case uint32:
bitSize = 32
case uint64:
bitSize = 64
}
intFromString := func(s string) (any, error) {
if bitSize == 0 {
return strconv.Atoi(s)
}
var v any
var err error
if isSigned {
v, err = strconv.ParseInt(s, 10, bitSize)
} else {
v, err = strconv.ParseUint(s, 10, bitSize)
}
if err != nil {
return 0, err
}
if isSigned {
i := v.(int64)
switch target.(type) {
case int:
return int(i), nil
case int8:
return int8(i), nil
case int16:
return int16(i), nil
case int32:
return int32(i), nil
case int64:
return i, nil
}
}
i := v.(uint64)
switch target.(type) {
case uint:
return uint(i), nil
case uint8:
return uint8(i), nil
case uint16:
return uint16(i), nil
case uint32:
return uint32(i), nil
case uint64:
return i, nil
}
return 0, fmt.Errorf("unsupported target type %T", target)
}
intToString := func(v any) (string, error) {
return fmt.Sprintf("%d", v), nil
}
return tmc.NewAdapter(target, intFromString, intToString)
}
ru, _ := imagemeta.NewRat[uint32](1, 2)
ri, _ := imagemeta.NewRat[int32](1, 2)
tmcAdapters := []tmc.Adapter{
tmc.NewAdapter(ru, nil, nil),
tmc.NewAdapter(ri, nil, nil),
newIntadapter(int(1)),
newIntadapter(int8(1)),
newIntadapter(int16(1)),
newIntadapter(int32(1)),
newIntadapter(int64(1)),
newIntadapter(uint(1)),
newIntadapter(uint8(1)),
newIntadapter(uint16(1)),
newIntadapter(uint32(1)),
newIntadapter(uint64(1)),
}
tmcAdapters = append(tmc.DefaultTypeAdapters, tmcAdapters...)
var err error
tcodec, err = tmc.New()
tcodec, err = tmc.New(tmc.WithTypeAdapters(tmcAdapters))
if err != nil {
panic(err)
}