mirror of
https://github.com/dsoprea/go-exif.git
synced 2025-05-31 11:41:57 +00:00
They'll also now embed all of the information they need to know since it is all known where the ITEs are created. This prevents the user from having to be involved in it. This makes it much more straightforward and enjoyable to use. - ifd_tag_entry.go - newIfdTagEntry now takes and embeds `addressableBytes` and `byteOrder`. - We've dumped the `value` member that let the caller preload a parsed value (or bytes?). It's no longer necessary since this led to inconsistencies and the ITE can produce these values directly, now. - `Value()` obviously no longer takes `addressableValue` and `byteOrder` since now embedded. - `Value()` will now see and return ErrUnhandledUnknownTypedTag directly (not wrapping it, since this is a handled case). - common/type.go: FormatFromType now uses Stringer as a fallback if possible. All undefined-tag wrapper types implement it, so the same function can handle both undefined and non-undefined values, and the individual types can control the strings presented in simple listing. - Dropped "resolveValue" parameters from all of the collect, visit, and parsing functions. Resolution is now a later step performed by the caller on the ITEs, directly. - This parameter was protection against undefined-type values disrupting simple enumeration, but now the user can simply produce the list of tags and can either choose to decode their value or not, directly. If they do, they, as of earlier, recent commits, also have the ability to properly manage unhandled undefined-values so they don't crash. - The ITEs can now create ValueContext structs directly (GetValueContext()), though it might not be necessary now that the ITEs can produce the values and encodings directly. - This also allowed us to dump several other GetValueContext() implementations elsewhere since it is now self-reliant on this type and those methods were essentially kludges for the lack of this. - Dump a bunch of "Value" methods from ITEs which just weren't useful or simple enough. Replaced by the above. - Fixed `(Ifd).String()` to have a pointer receiver.
242 lines
6.2 KiB
Go
242 lines
6.2 KiB
Go
package exif
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"encoding/binary"
|
|
"io/ioutil"
|
|
|
|
"github.com/dsoprea/go-logging"
|
|
)
|
|
|
|
const (
|
|
// ExifAddressableAreaStart is the absolute offset in the file that all
|
|
// offsets are relative to.
|
|
ExifAddressableAreaStart = uint32(0x0)
|
|
|
|
// ExifDefaultFirstIfdOffset is essentially the number of bytes in addition
|
|
// to `ExifAddressableAreaStart` that you have to move in order to escape
|
|
// the rest of the header and get to the earliest point where we can put
|
|
// stuff (which has to be the first IFD). This is the size of the header
|
|
// sequence containing the two-character byte-order, two-character fixed-
|
|
// bytes, and the four bytes describing the first-IFD offset.
|
|
ExifDefaultFirstIfdOffset = uint32(2 + 2 + 4)
|
|
)
|
|
|
|
var (
|
|
exifLogger = log.NewLogger("exif.exif")
|
|
|
|
BigEndianBoBytes = [2]byte{'M', 'M'}
|
|
LittleEndianBoBytes = [2]byte{'I', 'I'}
|
|
|
|
ByteOrderLookup = map[[2]byte]binary.ByteOrder{
|
|
BigEndianBoBytes: binary.BigEndian,
|
|
LittleEndianBoBytes: binary.LittleEndian,
|
|
}
|
|
|
|
ByteOrderLookupR = map[binary.ByteOrder][2]byte{
|
|
binary.BigEndian: BigEndianBoBytes,
|
|
binary.LittleEndian: LittleEndianBoBytes,
|
|
}
|
|
|
|
ExifFixedBytesLookup = map[binary.ByteOrder][2]byte{
|
|
binary.LittleEndian: [2]byte{0x2a, 0x00},
|
|
binary.BigEndian: [2]byte{0x00, 0x2a},
|
|
}
|
|
)
|
|
|
|
var (
|
|
ErrNoExif = errors.New("no exif data")
|
|
ErrExifHeaderError = errors.New("exif header error")
|
|
)
|
|
|
|
// SearchAndExtractExif returns a slice from the beginning of the EXIF data to
|
|
// end of the file (it's not practical to try and calculate where the data
|
|
// actually ends; it needs to be formally parsed).
|
|
func SearchAndExtractExif(data []byte) (rawExif []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err := log.Wrap(state.(error))
|
|
log.Panic(err)
|
|
}
|
|
}()
|
|
|
|
// Search for the beginning of the EXIF information. The EXIF is near the
|
|
// beginning of our/most JPEGs, so this has a very low cost.
|
|
|
|
foundAt := -1
|
|
for i := 0; i < len(data); i++ {
|
|
if _, err := ParseExifHeader(data[i:]); err == nil {
|
|
foundAt = i
|
|
break
|
|
} else if log.Is(err, ErrNoExif) == false {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if foundAt == -1 {
|
|
return nil, ErrNoExif
|
|
}
|
|
|
|
return data[foundAt:], nil
|
|
}
|
|
|
|
// SearchFileAndExtractExif returns a slice from the beginning of the EXIF data
|
|
// to the end of the file (it's not practical to try and calculate where the
|
|
// data actually ends).
|
|
func SearchFileAndExtractExif(filepath string) (rawExif []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err := log.Wrap(state.(error))
|
|
log.Panic(err)
|
|
}
|
|
}()
|
|
|
|
// Open the file.
|
|
|
|
f, err := os.Open(filepath)
|
|
log.PanicIf(err)
|
|
|
|
defer f.Close()
|
|
|
|
data, err := ioutil.ReadAll(f)
|
|
log.PanicIf(err)
|
|
|
|
rawExif, err = SearchAndExtractExif(data)
|
|
log.PanicIf(err)
|
|
|
|
return rawExif, nil
|
|
}
|
|
|
|
type ExifHeader struct {
|
|
ByteOrder binary.ByteOrder
|
|
FirstIfdOffset uint32
|
|
}
|
|
|
|
func (eh ExifHeader) String() string {
|
|
return fmt.Sprintf("ExifHeader<BYTE-ORDER=[%v] FIRST-IFD-OFFSET=(0x%02x)>", eh.ByteOrder, eh.FirstIfdOffset)
|
|
}
|
|
|
|
// ParseExifHeader parses the bytes at the very top of the header.
|
|
//
|
|
// This will panic with ErrNoExif on any data errors so that we can double as
|
|
// an EXIF-detection routine.
|
|
func ParseExifHeader(data []byte) (eh ExifHeader, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
// Good reference:
|
|
//
|
|
// CIPA DC-008-2016; JEITA CP-3451D
|
|
// -> http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf
|
|
|
|
if len(data) < 2 {
|
|
exifLogger.Warningf(nil, "Not enough data for EXIF header (1): (%d)", len(data))
|
|
return eh, ErrNoExif
|
|
}
|
|
|
|
byteOrderBytes := [2]byte{data[0], data[1]}
|
|
|
|
byteOrder, found := ByteOrderLookup[byteOrderBytes]
|
|
if found == false {
|
|
// exifLogger.Warningf(nil, "EXIF byte-order not recognized: [%v]", byteOrderBytes)
|
|
return eh, ErrNoExif
|
|
}
|
|
|
|
if len(data) < 4 {
|
|
exifLogger.Warningf(nil, "Not enough data for EXIF header (2): (%d)", len(data))
|
|
return eh, ErrNoExif
|
|
}
|
|
|
|
fixedBytes := [2]byte{data[2], data[3]}
|
|
expectedFixedBytes := ExifFixedBytesLookup[byteOrder]
|
|
if fixedBytes != expectedFixedBytes {
|
|
// exifLogger.Warningf(nil, "EXIF header fixed-bytes should be [%v] but are: [%v]", expectedFixedBytes, fixedBytes)
|
|
return eh, ErrNoExif
|
|
}
|
|
|
|
if len(data) < 2 {
|
|
exifLogger.Warningf(nil, "Not enough data for EXIF header (3): (%d)", len(data))
|
|
return eh, ErrNoExif
|
|
}
|
|
|
|
firstIfdOffset := byteOrder.Uint32(data[4:8])
|
|
|
|
eh = ExifHeader{
|
|
ByteOrder: byteOrder,
|
|
FirstIfdOffset: firstIfdOffset,
|
|
}
|
|
|
|
return eh, nil
|
|
}
|
|
|
|
// Visit recursively invokes a callback for every tag.
|
|
func Visit(rootIfdName string, ifdMapping *IfdMapping, tagIndex *TagIndex, exifData []byte, visitor RawTagWalk) (eh ExifHeader, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
eh, err = ParseExifHeader(exifData)
|
|
log.PanicIf(err)
|
|
|
|
ie := NewIfdEnumerate(ifdMapping, tagIndex, exifData, eh.ByteOrder)
|
|
|
|
err = ie.Scan(rootIfdName, eh.FirstIfdOffset, visitor)
|
|
log.PanicIf(err)
|
|
|
|
return eh, nil
|
|
}
|
|
|
|
// Collect recursively builds a static structure of all IFDs and tags.
|
|
func Collect(ifdMapping *IfdMapping, tagIndex *TagIndex, exifData []byte) (eh ExifHeader, index IfdIndex, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
eh, err = ParseExifHeader(exifData)
|
|
log.PanicIf(err)
|
|
|
|
ie := NewIfdEnumerate(ifdMapping, tagIndex, exifData, eh.ByteOrder)
|
|
|
|
index, err = ie.Collect(eh.FirstIfdOffset)
|
|
log.PanicIf(err)
|
|
|
|
return eh, index, nil
|
|
}
|
|
|
|
// BuildExifHeader constructs the bytes that go in the very beginning.
|
|
func BuildExifHeader(byteOrder binary.ByteOrder, firstIfdOffset uint32) (headerBytes []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
b := new(bytes.Buffer)
|
|
|
|
// This is the point in the data that all offsets are relative to.
|
|
boBytes := ByteOrderLookupR[byteOrder]
|
|
_, err = b.WriteString(string(boBytes[:]))
|
|
log.PanicIf(err)
|
|
|
|
fixedBytes := ExifFixedBytesLookup[byteOrder]
|
|
|
|
_, err = b.Write(fixedBytes[:])
|
|
log.PanicIf(err)
|
|
|
|
err = binary.Write(b, byteOrder, firstIfdOffset)
|
|
log.PanicIf(err)
|
|
|
|
return b.Bytes(), nil
|
|
}
|