diff --git a/ifd_builder.go b/ifd_builder.go index 4d3986e..791f121 100644 --- a/ifd_builder.go +++ b/ifd_builder.go @@ -1,5 +1,10 @@ package exif +// NOTES: +// +// The thumbnail offset and length tags shouldn't be set directly. Use the +// (*IfdBuilder).SetThumbnail() method instead. + import ( "errors" "fmt" @@ -246,6 +251,10 @@ type IfdBuilder struct { // nextIb represents the next link if we're chaining to another. nextIb *IfdBuilder + + // thumbnailData is populated with thumbnail data if there was thumbnail + // data. Otherwise, it's nil. + thumbnailData []byte } func NewIfdBuilder(ii IfdIdentity, byteOrder binary.ByteOrder) (ib *IfdBuilder) { @@ -339,6 +348,66 @@ func (ib *IfdBuilder) Tags() (tags []builderTag) { return ib.tags } +// SetThumbnail sets thumbnail data. +// +// NOTES: +// +// - We don't manage any facet of the thumbnail data. This is the +// responsibility of the user/developer. +// - This method will fail unless the thumbnail is set on a the root IFD. +// However, in order to be valid, it must be set on the second one, linked to +// by the first, as per the EXIF/TIFF specification. +// - We set the offset to (0) now but will allocate the data and properly assign +// the offset when the IB is encoded (later). +func (ib *IfdBuilder) SetThumbnail(data []byte) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if ib.ii != RootIi { + log.Panicf("thumbnails can only go into a root Ifd (and only the second one)") + } + +// TODO(dustin): !! Add a test for this. + + if data == nil || len(data) == 0 { + +// TODO(dustin): !! Debugging. +fmt.Printf("Thumbnail empty.\n") + + log.Panic("thumbnail is empty") + } + + ib.thumbnailData = data + +fmt.Printf("SETTING THUMBNAIL.\n") + + ibtvfb := NewIfdBuilderTagValueFromBytes(ib.thumbnailData) + offsetBt := NewBuilderTag(ib.ii, ThumbnailOffsetTagId, TypeLong, ibtvfb) + // offsetBt := NewStandardBuilderTagFromConfig(ib.ii, ThumbnailOffsetTagId, ib.byteOrder, []uint32 { 0 }) + + err = ib.Set(offsetBt) + log.PanicIf(err) + + sizeBt := NewStandardBuilderTagFromConfig(ib.ii, ThumbnailSizeTagId, ib.byteOrder, []uint32 { uint32(len(ib.thumbnailData)) }) + + err = ib.Set(sizeBt) + log.PanicIf(err) + + +// TODO(dustin): !! Debugging. +fmt.Printf("Set thumbnail into IB.\n") + + + return nil +} + +func (ib *IfdBuilder) Thumbnail() []byte { + return ib.thumbnailData +} + func (ib *IfdBuilder) printTagTree(levels int) { indent := strings.Repeat(" ", levels * 2) @@ -583,6 +652,27 @@ func (ib *IfdBuilder) Replace(tagId uint16, bt builderTag) (err error) { return nil } +// Set will add a new entry or update an existing entry. +func (ib *IfdBuilder) Set(bt builderTag) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + position, err := ib.Find(bt.tagId) + if err == nil { + ib.tags[position] = bt + } else if log.Is(err, ErrTagEntryNotFound) == true { + err = ib.Add(bt) + log.PanicIf(err) + } else { + log.Panic(err) + } + + return nil +} + func (ib *IfdBuilder) FindN(tagId uint16, maxFound int) (found []int, err error) { defer func() { if state := recover(); state != nil { @@ -689,10 +779,32 @@ func (ib *IfdBuilder) AddTagsFromExisting(ifd *Ifd, itevr *IfdTagEntryValueResol } }() + thumbnailData, err := ifd.Thumbnail() + if err == nil { + +// TODO(dustin): !! Debugging. +fmt.Printf("Importing thumbnail: %s\n", ifd.Identity()) + + err = ib.SetThumbnail(thumbnailData) + log.PanicIf(err) + } else if log.Is(err, ErrNoThumbnail) == false { + log.Panic(err) + } else { + +// TODO(dustin): !! Debugging. +fmt.Printf("NO THUMBNAIL FOUND: %s\n", ifd.Identity()) + + } + for _, ite := range ifd.Entries { - // If we want to add an IFD tag, we'll have to build it first and *then* - // add it via a different method. if ite.ChildIfdName != "" { + // If we want to add an IFD tag, we'll have to build it first and + // *then* add it via a different method. + + continue + } else if ite.TagId == ThumbnailOffsetTagId || ite.TagId == ThumbnailSizeTagId { + // These will be added on-the-fly when we encode. + continue } diff --git a/ifd_builder_encode.go b/ifd_builder_encode.go index af20184..7872446 100644 --- a/ifd_builder_encode.go +++ b/ifd_builder_encode.go @@ -234,10 +234,13 @@ func (ibe *IfdByteEncoder) encodeTagToBytes(ib *IfdBuilder, bt *builderTag, bw * len_ := len(valueBytes) unitCount := uint32(len_) / typeSize - remainder := uint32(len_) % typeSize - if remainder > 0 { - log.Panicf("tag value of (%d) bytes not evenly divisible by type-size (%d)", len_, typeSize) + if _, found := tagsWithoutAlignment[bt.tagId]; found == false { + remainder := uint32(len_) % typeSize + + if remainder > 0 { + log.Panicf("tag (0x%02x) value of (%d) bytes not evenly divisible by type-size (%d)", bt.tagId, len_, typeSize) + } } err = bw.WriteUint32(unitCount) @@ -337,6 +340,8 @@ func (ibe *IfdByteEncoder) encodeIfdToBytes(ib *IfdBuilder, ifdAddressableOffset log.PanicIf(err) if childIfdBlock != nil { + // We aren't allowed to have non-nil child IFDs if we're just + // sizing things up. if nextIfdOffsetToWrite == 0 { log.Panicf("no IFD offset provided for child-IFDs; no new child-IFDs permitted") } @@ -384,6 +389,23 @@ func (ibe *IfdByteEncoder) encodeIfdToBytes(ib *IfdBuilder, ifdAddressableOffset _, err = b.Write(dataBytes) log.PanicIf(err) +// TODO(dustin): !! Debugging. + // if thumbnailOffset != uint32(0) { + // currentRelativeOffset := thumbnailOffset - ifdAddressableOffset + // currentBuffer := b.Bytes() + + // len_ := len(thumbnailData) + // extractedThumbnailData := currentBuffer[int(currentRelativeOffset):int(currentRelativeOffset) + len_] + + // // We didn't have enough data available, which would be queer. + // if len(extractedThumbnailData) != len_ { + // log.Panicf("extracted thumbnail data was truncated; not enough data") + // } + +// // fmt.Printf("Re-extracted (%d) bytes of thumbnail data.\n", len(extractedThumbnailData)) +// // DumpBytes(extractedThumbnailData[:50]) +// } + // Append any child IFD blocks after our table and data blocks. These IFDs // were equipped with the appropriate offset information so it's expected // that all offsets referred to by these will be correct. @@ -400,6 +422,13 @@ func (ibe *IfdByteEncoder) encodeIfdToBytes(ib *IfdBuilder, ifdAddressableOffset ibe.pushToJournal("encodeIfdToBytes", "<", "%s", ib) +// if nextIfdOffsetToWrite > uint32(0) { +// fmt.Printf("Encoded IB: %s ADDRESSABLE-OFFSET=(0x%08x; %d):\n", ib.ii, ifdAddressableOffset, ifdAddressableOffset) +// fmt.Printf("\n") +// DumpBytes(b.Bytes()) +// fmt.Printf("\n") +// } + return b.Bytes(), tableSize, dataSize, childIfdSizes, nil } diff --git a/ifd_enumerate.go b/ifd_enumerate.go index 05c9c25..cd8b431 100644 --- a/ifd_enumerate.go +++ b/ifd_enumerate.go @@ -176,7 +176,7 @@ type TagVisitor func(ii IfdIdentity, ifdIndex int, tagId uint16, tagType TagType // ParseIfd decodes the IFD block that we're currently sitting on the first // byte of. -func (ie *IfdEnumerate) ParseIfd(ii IfdIdentity, ifdIndex int, ite *IfdTagEnumerator, visitor TagVisitor, doDescend bool) (nextIfdOffset uint32, entries []*IfdTagEntry, err error) { +func (ie *IfdEnumerate) ParseIfd(ii IfdIdentity, ifdIndex int, ite *IfdTagEnumerator, visitor TagVisitor, doDescend bool) (nextIfdOffset uint32, entries []*IfdTagEntry, thumbnailData []byte, err error) { defer func() { if state := recover(); state != nil { err = log.Wrap(state.(error)) @@ -188,12 +188,24 @@ func (ie *IfdEnumerate) ParseIfd(ii IfdIdentity, ifdIndex int, ite *IfdTagEnumer ifdEnumerateLogger.Debugf(nil, "Current IFD tag-count: (%d)", tagCount) - entries = make([]*IfdTagEntry, tagCount) + entries = make([]*IfdTagEntry, 0) + + var iteThumbnailOffset *IfdTagEntry + var iteThumbnailSize *IfdTagEntry for i := 0; i < int(tagCount); i++ { tag, err := ie.parseTag(ii, i, ite) log.PanicIf(err) + if tag.TagId == ThumbnailOffsetTagId { + iteThumbnailOffset = tag + + continue + } else if tag.TagId == ThumbnailSizeTagId { + iteThumbnailSize = tag + continue + } + if visitor != nil { tt := NewTagType(tag.TagType, ie.byteOrder) @@ -220,7 +232,12 @@ func (ie *IfdEnumerate) ParseIfd(ii IfdIdentity, ifdIndex int, ite *IfdTagEnumer log.PanicIf(err) } - entries[i] = tag + entries = append(entries, tag) + } + + if iteThumbnailOffset != nil && iteThumbnailSize != nil { + thumbnailData, err = ie.parseThumbnail(iteThumbnailOffset, iteThumbnailSize) + log.PanicIf(err) } nextIfdOffset, _, err = ite.getUint32() @@ -228,7 +245,37 @@ func (ie *IfdEnumerate) ParseIfd(ii IfdIdentity, ifdIndex int, ite *IfdTagEnumer ifdEnumerateLogger.Debugf(nil, "Next IFD at offset: (%08x)", nextIfdOffset) - return nextIfdOffset, entries, nil + return nextIfdOffset, entries, thumbnailData, nil +} + +func (ie *IfdEnumerate) parseThumbnail(offsetIte, lengthIte *IfdTagEntry) (thumbnailData []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + addressableData := ie.exifData[ExifAddressableAreaStart:] + + + vRaw, err := lengthIte.Value(addressableData, ie.byteOrder) + log.PanicIf(err) + + vList := vRaw.([]uint32) + if len(vList) != 1 { + log.Panicf("not exactly one long: (%d)", len(vList)) + } + + length := vList[0] + + // The tag is official a LONG type, but it's actually an offset to a blob of bytes. + offsetIte.TagType = TypeByte + offsetIte.UnitCount = length + + thumbnailData, err = offsetIte.ValueBytes(addressableData, ie.byteOrder) + log.PanicIf(err) + + return thumbnailData, nil } // Scan enumerates the different EXIF's IFD blocks. @@ -243,7 +290,7 @@ func (ie *IfdEnumerate) scan(ii IfdIdentity, ifdOffset uint32, visitor TagVisito ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (%04x).", ii.IfdName, ifdIndex, ifdOffset) ite := ie.getTagEnumerator(ifdOffset) - nextIfdOffset, _, err := ie.ParseIfd(ii, ifdIndex, ite, visitor, true) + nextIfdOffset, _, _, err := ie.ParseIfd(ii, ifdIndex, ite, visitor, true) log.PanicIf(err) if nextIfdOffset == 0 { @@ -385,64 +432,6 @@ func (ifd Ifd) String() string { return fmt.Sprintf("Ifd (%d)", length, len(ifd.addressableData) - int(offset)) - } - - ifd.thumbnailData = ifd.addressableData[offset:offset + length] - - return nil -} - func (ifd *Ifd) Thumbnail() (data []byte, err error) { defer func() { if state := recover(); state != nil { @@ -737,7 +726,7 @@ func (ie *IfdEnumerate) Collect(rootIfdOffset uint32) (index IfdIndex, err error ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (%04x).", ii.IfdName, index, offset) ite := ie.getTagEnumerator(offset) - nextIfdOffset, entries, err := ie.ParseIfd(ii, index, ite, nil, false) + nextIfdOffset, entries, thumbnailData, err := ie.ParseIfd(ii, index, ite, nil, false) log.PanicIf(err) id := len(ifds) @@ -766,11 +755,9 @@ func (ie *IfdEnumerate) Collect(rootIfdOffset uint32) (index IfdIndex, err error EntriesByTagId: entriesByTagId, Children: make([]*Ifd, 0), NextIfdOffset: nextIfdOffset, + thumbnailData: thumbnailData, } - err = ifd.parseThumbnail() - log.PanicIf(err) - // Add ourselves to a big list of IFDs. ifds = append(ifds, &ifd) @@ -858,7 +845,7 @@ func ParseOneIfd(ii IfdIdentity, byteOrder binary.ByteOrder, ifdBlock []byte, vi ite := NewIfdTagEnumerator(ifdBlock, byteOrder, 0) - nextIfdOffset, entries, err = ie.ParseIfd(ii, 0, ite, visitor, true) + nextIfdOffset, entries, _, err = ie.ParseIfd(ii, 0, ite, visitor, true) log.PanicIf(err) return nextIfdOffset, entries, nil diff --git a/tags.go b/tags.go index 5e5e449..fed95fa 100644 --- a/tags.go +++ b/tags.go @@ -79,6 +79,14 @@ var ( GpsIi: 3, ExifIopIi: 4, } + + // tagsWithoutAlignment is a tag-lookup for tags whose value size won't + // necessarily be a multiple of its tag-type. + tagsWithoutAlignment = map[uint16]struct{} { + // The thumbnail offset is stored as a long, but its data is a binary + // blob (not a slice of longs). + ThumbnailOffsetTagId: struct{}{}, + } ) var (