commit 685d8014897d5e8d2cc8d407cc2d3d1b01cad59d Author: Dustin Oprea Date: Sat Apr 14 14:38:35 2018 -0400 Initial commit. - Parsing works. - Not yet resolving values. - Not yet resolving the actual IDs. - Not yet able to make changes. diff --git a/assets/NDM_8901.jpg b/assets/NDM_8901.jpg new file mode 100644 index 0000000..b892262 Binary files /dev/null and b/assets/NDM_8901.jpg differ diff --git a/exif.go b/exif.go new file mode 100644 index 0000000..14d4187 --- /dev/null +++ b/exif.go @@ -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 +} diff --git a/exif_test.go b/exif_test.go new file mode 100644 index 0000000..2ed915b --- /dev/null +++ b/exif_test.go @@ -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") +} + diff --git a/ifd.go b/ifd.go new file mode 100644 index 0000000..272c037 --- /dev/null +++ b/ifd.go @@ -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 +}