mirror of https://github.com/gohugoio/hugo
367 lines
7.4 KiB
Go
367 lines
7.4 KiB
Go
// 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 (
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bep/imagemeta"
|
|
"github.com/bep/logg"
|
|
"github.com/bep/tmc"
|
|
)
|
|
|
|
// ExifInfo holds the decoded Exif data for an Image.
|
|
type ExifInfo struct {
|
|
// GPS latitude in degrees.
|
|
Lat float64
|
|
|
|
// GPS longitude in degrees.
|
|
Long float64
|
|
|
|
// Image creation date/time.
|
|
Date time.Time
|
|
|
|
// A collection of the available Exif tags for this Image.
|
|
Tags Tags
|
|
}
|
|
|
|
type Decoder struct {
|
|
includeFieldsRe *regexp.Regexp
|
|
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 {
|
|
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 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 == "" {
|
|
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
|
|
}
|
|
|
|
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 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, _ = tagInfos.GetDateTime()
|
|
}
|
|
|
|
if !d.noLatLong {
|
|
lat, long, _ = tagInfos.GetLatLong()
|
|
}
|
|
|
|
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: tags}
|
|
|
|
return
|
|
}
|
|
|
|
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(tmc.WithTypeAdapters(tmcAdapters))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Tags is a map of EXIF tags.
|
|
type Tags map[string]any
|
|
|
|
// UnmarshalJSON is for internal use only.
|
|
func (v *Tags) UnmarshalJSON(b []byte) error {
|
|
vv := make(map[string]any)
|
|
if err := tcodec.Unmarshal(b, &vv); err != nil {
|
|
return err
|
|
}
|
|
|
|
*v = vv
|
|
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON is for internal use only.
|
|
func (v Tags) MarshalJSON() ([]byte, error) {
|
|
return tcodec.Marshal(v)
|
|
}
|