mirror of https://github.com/dsoprea/go-exif.git
Initial commit.
- Parsing works. - Not yet resolving values. - Not yet resolving the actual IDs. - Not yet able to make changes.pull/3/head
commit
685d801489
Binary file not shown.
After Width: | Height: | Size: 5.3 MiB |
|
@ -0,0 +1,95 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"bytes"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
exifLogger = log.NewLogger("exif.exif")
|
||||
ErrNotExif = errors.New("not exif data")
|
||||
)
|
||||
|
||||
type Exif struct {
|
||||
|
||||
}
|
||||
|
||||
func NewExif() *Exif {
|
||||
return new(Exif)
|
||||
}
|
||||
|
||||
func (e *Exif) IsExif(data []byte) (ok bool) {
|
||||
if bytes.Compare(data[:6], []byte("Exif\000\000")) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *Exif) Parse(data []byte) (err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
if e.IsExif(data) == false {
|
||||
|
||||
// TODO(dustin): !! Debugging.
|
||||
fmt.Printf("AppData doesn't look like EXIF. BYTES=(%d)\n", len(data))
|
||||
|
||||
return ErrNotExif
|
||||
}
|
||||
|
||||
// Good reference:
|
||||
//
|
||||
// CIPA DC-008-2016; JEITA CP-3451D
|
||||
// -> http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf
|
||||
|
||||
fmt.Printf("AppData DOES look like EXIF. BYTES=(%d)\n", len(data))
|
||||
byteOrderSignature := data[6:8]
|
||||
byteOrder := IfdByteOrder(BigEndianByteOrder)
|
||||
if string(byteOrderSignature) == "II" {
|
||||
byteOrder = IfdByteOrder(LittleEndianByteOrder)
|
||||
} else if string(byteOrderSignature) != "MM" {
|
||||
log.Panicf("byte-order not recognized: [%v]", byteOrderSignature)
|
||||
}
|
||||
|
||||
fmt.Printf("BYTE-ORDER: [%s]\n", byteOrderSignature)
|
||||
|
||||
fixedBytes := data[8:10]
|
||||
if fixedBytes[0] != 0x2a || fixedBytes[1] != 0x00 {
|
||||
exifLogger.Warningf(nil, "EXIF app-data header fixed-bytes should be 0x002a but are: [%v]", fixedBytes)
|
||||
|
||||
// TODO(dustin): Debugging.
|
||||
fmt.Printf("EXIF app-data header fixed-bytes should be 0x002a but are: [%v]\n", fixedBytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
firstIfdOffset := uint32(0)
|
||||
if byteOrder.IsLittleEndian() == true {
|
||||
firstIfdOffset = binary.LittleEndian.Uint32(data[10:14])
|
||||
} else {
|
||||
firstIfdOffset = binary.BigEndian.Uint32(data[10:14])
|
||||
}
|
||||
|
||||
ifd := NewIfd(data, byteOrder)
|
||||
|
||||
visitor := func() error {
|
||||
// TODO(dustin): !! Debugging.
|
||||
|
||||
fmt.Printf("IFD visitor.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
err = ifd.Scan(visitor, firstIfdOffset)
|
||||
log.PanicIf(err)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
assetsPath = ""
|
||||
)
|
||||
|
||||
|
||||
func TestIsExif_True(t *testing.T) {
|
||||
e := NewExif()
|
||||
|
||||
if ok := e.IsExif([]byte("Exif\000\000")); ok != true {
|
||||
t.Fatalf("expected true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsExif_False(t *testing.T) {
|
||||
e := NewExif()
|
||||
|
||||
if ok := e.IsExif([]byte("something unexpected")); ok != false {
|
||||
t.Fatalf("expected false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
// Open the file.
|
||||
|
||||
filepath := path.Join(assetsPath, "NDM_8901.jpg")
|
||||
f, err := os.Open(filepath)
|
||||
log.PanicIf(err)
|
||||
|
||||
defer f.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
log.PanicIf(err)
|
||||
|
||||
// Search for the beginning of the EXIF information. The EXIF is near the
|
||||
// very beginning of our/most JPEGs, so this has a very low cost.
|
||||
|
||||
e := NewExif()
|
||||
|
||||
foundAt := -1
|
||||
for i := 0; i < len(data); i++ {
|
||||
if e.IsExif(data[i:i + 6]) == true {
|
||||
foundAt = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundAt == -1 {
|
||||
log.Panicf("EXIF start not found")
|
||||
}
|
||||
|
||||
// Run the parse.
|
||||
|
||||
err = e.Parse(data[foundAt:])
|
||||
log.PanicIf(err)
|
||||
}
|
||||
|
||||
func init() {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
if goPath == "" {
|
||||
log.Panicf("GOPATH is empty")
|
||||
}
|
||||
|
||||
assetsPath = path.Join(goPath, "src", "github.com", "dsoprea", "go-exif", "assets")
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
const (
|
||||
BigEndianByteOrder = iota
|
||||
LittleEndianByteOrder = iota
|
||||
)
|
||||
|
||||
var (
|
||||
ifdLogger = log.NewLogger("exifjpeg.ifd")
|
||||
)
|
||||
|
||||
type IfdByteOrder int
|
||||
|
||||
func (ibo IfdByteOrder) IsBigEndian() bool {
|
||||
return ibo == BigEndianByteOrder
|
||||
}
|
||||
|
||||
func (ibo IfdByteOrder) IsLittleEndian() bool {
|
||||
return ibo == LittleEndianByteOrder
|
||||
}
|
||||
|
||||
type Ifd struct {
|
||||
data []byte
|
||||
buffer *bytes.Buffer
|
||||
byteOrder IfdByteOrder
|
||||
currentOffset uint32
|
||||
ifdTopOffset uint32
|
||||
}
|
||||
|
||||
func NewIfd(data []byte, byteOrder IfdByteOrder) *Ifd {
|
||||
return &Ifd{
|
||||
data: data,
|
||||
buffer: bytes.NewBuffer(data),
|
||||
byteOrder: byteOrder,
|
||||
ifdTopOffset: 6,
|
||||
}
|
||||
}
|
||||
|
||||
// read is a wrapper around the built-in reader that applies which endianness
|
||||
// we are.
|
||||
func (ifd *Ifd) read(r io.Reader, into interface{}) (err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
if ifd.byteOrder.IsLittleEndian() == true {
|
||||
err := binary.Read(r, binary.LittleEndian, into)
|
||||
log.PanicIf(err)
|
||||
} else {
|
||||
err := binary.Read(r, binary.BigEndian, into)
|
||||
log.PanicIf(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getUint16 reads a uint16 and advances both our current and our current
|
||||
// accumulator (which allows us to know how far to seek to the beginning of the
|
||||
// next IFD when it's time to jump).
|
||||
func (ifd *Ifd) getUint16() (value uint16, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
err = ifd.read(ifd.buffer, &value)
|
||||
log.PanicIf(err)
|
||||
|
||||
ifd.currentOffset += 2
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// getUint32 reads a uint32 and advances both our current and our current
|
||||
// accumulator (which allows us to know how far to seek to the beginning of the
|
||||
// next IFD when it's time to jump).
|
||||
func (ifd *Ifd) getUint32() (value uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
err = ifd.read(ifd.buffer, &value)
|
||||
log.PanicIf(err)
|
||||
|
||||
ifd.currentOffset += 4
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// parseCurrentIfd decodes the IFD block that we're currently sitting on the
|
||||
// first byte of.
|
||||
func (ifd *Ifd) parseCurrentIfd() (nextIfdOffset uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
tagCount, err := ifd.getUint16()
|
||||
log.PanicIf(err)
|
||||
|
||||
fmt.Printf("IFD: TOTAL TAG COUNT=(%02x)\n", tagCount)
|
||||
|
||||
for i := uint16(0); i < tagCount; i++ {
|
||||
// TODO(dustin): !! 0x8769 tag-IDs are child IFDs.
|
||||
tagId, err := ifd.getUint16()
|
||||
log.PanicIf(err)
|
||||
|
||||
fmt.Printf("IFD: Tag (%d) ID=(%02x)\n", i, tagId)
|
||||
|
||||
|
||||
tagType, err := ifd.getUint16()
|
||||
log.PanicIf(err)
|
||||
|
||||
fmt.Printf("IFD: Tag (%d) TYPE=(%d)\n", i, tagType)
|
||||
|
||||
|
||||
tagCount, err := ifd.getUint32()
|
||||
log.PanicIf(err)
|
||||
|
||||
fmt.Printf("IFD: Tag (%d) COUNT=(%02x)\n", i, tagCount)
|
||||
|
||||
|
||||
valueOffset, err := ifd.getUint32()
|
||||
log.PanicIf(err)
|
||||
|
||||
fmt.Printf("IFD: Tag (%d) VALUE-OFFSET=(%x)\n", i, valueOffset)
|
||||
|
||||
// Notes on the tag-value's value (we'll have to use this as a pointer if the type potentially requires more than four bytes):
|
||||
//
|
||||
// This tag records the offset from the start of the TIFF header to the position where the value itself is
|
||||
// recorded. In cases where the value fits in 4 Bytes, the value itself is recorded. If the value is smaller
|
||||
// than 4 Bytes, the value is stored in the 4-Byte area starting from the left, i.e., from the lower end of
|
||||
// the byte offset area. For example, in big endian format, if the type is SHORT and the value is 1, it is
|
||||
// recorded as 00010000.H
|
||||
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
|
||||
nextIfdOffset, err = ifd.getUint32()
|
||||
log.PanicIf(err)
|
||||
|
||||
fmt.Printf("IFD: NEXT-IFD-OFFSET=(%x)\n", nextIfdOffset)
|
||||
|
||||
fmt.Printf("\n")
|
||||
|
||||
return nextIfdOffset, nil
|
||||
}
|
||||
|
||||
// forwardToIfd jumps to the beginning of an IFD block that starts on or after
|
||||
// the current position.
|
||||
func (ifd *Ifd) forwardToIfd(ifdOffset uint32) (err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Printf("IFD: Forwarding to IFD. TOP-OFFSET=(%d) IFD-OFFSET=(%d)\n", ifd.ifdTopOffset, ifdOffset)
|
||||
|
||||
nextOffset := ifd.ifdTopOffset + ifdOffset
|
||||
|
||||
// We're assuming the guarantee that the next IFD will follow the
|
||||
// current one. So, figure out how far it is from our current position.
|
||||
delta := nextOffset - ifd.currentOffset
|
||||
ifd.buffer.Next(int(delta))
|
||||
|
||||
ifd.currentOffset = nextOffset
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type IfdVisitor func() error
|
||||
|
||||
// Scan enumerates the different EXIF blocks (called IFDs).
|
||||
func (ifd *Ifd) Scan(v IfdVisitor, firstIfdOffset uint32) (err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
err = ifd.forwardToIfd(firstIfdOffset)
|
||||
log.PanicIf(err)
|
||||
|
||||
for {
|
||||
nextIfdOffset, err := ifd.parseCurrentIfd()
|
||||
log.PanicIf(err)
|
||||
|
||||
if nextIfdOffset == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
err = ifd.forwardToIfd(nextIfdOffset)
|
||||
log.PanicIf(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue