mirror of https://github.com/dsoprea/go-exif.git
387 lines
9.6 KiB
Go
387 lines
9.6 KiB
Go
package exif
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"encoding/binary"
|
|
|
|
"github.com/dsoprea/go-logging"
|
|
)
|
|
|
|
var (
|
|
ifdBuilderLogger = log.NewLogger("exif.ifd_builder")
|
|
)
|
|
|
|
var (
|
|
ErrTagEntryNotFound = errors.New("tag entry not found")
|
|
)
|
|
|
|
|
|
// TODO(dustin): !! Make sure we either replace existing IFDs or validate that the IFD doesn't already exist.
|
|
|
|
|
|
type builderTag struct {
|
|
ifdName string
|
|
tagId uint16
|
|
|
|
// value is either a value that can be encoded, an IfdBuilder instance (for
|
|
// child IFDs), or an IfdTagEntry instance representing an existing,
|
|
// previously-stored tag.
|
|
value interface{}
|
|
}
|
|
|
|
func (bt builderTag) String() string {
|
|
return fmt.Sprintf("BuilderTag<TAG-ID=(0x%02x) IFD=[%s] VALUE=[%v]>", bt.tagId, bt.ifdName, bt.value)
|
|
}
|
|
|
|
|
|
type IfdBuilder struct {
|
|
ifdName string
|
|
|
|
// ifdTagId will be non-zero if we're a child IFD.
|
|
ifdTagId uint16
|
|
|
|
byteOrder binary.ByteOrder
|
|
|
|
// Includes both normal tags and IFD tags (which point to child IFDs).
|
|
tags []builderTag
|
|
|
|
// existingOffset will be the offset that this IFD is currently found at if
|
|
// it represents an IFD that has previously been stored (or 0 if not).
|
|
existingOffset uint32
|
|
|
|
// nextIfd represents the next link if we're chaining to another.
|
|
nextIfd *IfdBuilder
|
|
}
|
|
|
|
func (ib *IfdBuilder) String() string {
|
|
nextIfdPhrase := ""
|
|
if ib.nextIfd != nil {
|
|
nextIfdPhrase = ib.nextIfd.ifdName
|
|
}
|
|
|
|
return fmt.Sprintf("IfdBuilder<NAME=[%s] TAG-ID=(0x%02x) BO=[%s] COUNT=(%d) OFFSET=(0x%04x) NEXT-IFD=(0x%04x)>", ib.ifdName, ib.ifdTagId, ib.byteOrder, len(ib.tags), ib.existingOffset, nextIfdPhrase)
|
|
}
|
|
|
|
func (ib *IfdBuilder) Tags() (tags []builderTag) {
|
|
return ib.tags
|
|
}
|
|
|
|
func (ib *IfdBuilder) Dump() {
|
|
fmt.Printf("IFD: %s\n", ib)
|
|
|
|
if len(ib.tags) > 0 {
|
|
fmt.Printf("\n")
|
|
|
|
for i, tag := range ib.tags {
|
|
fmt.Printf(" (%d): %s\n", i, tag)
|
|
}
|
|
|
|
fmt.Printf("\n")
|
|
}
|
|
}
|
|
|
|
func NewIfdBuilder(ifdName string, byteOrder binary.ByteOrder) (ib *IfdBuilder) {
|
|
found := false
|
|
for _, validName := range validIfds {
|
|
if validName == ifdName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if found == false {
|
|
log.Panicf("ifd not found: [%s]", ifdName)
|
|
}
|
|
|
|
ib = &IfdBuilder{
|
|
ifdName: ifdName,
|
|
ifdTagId: IfdTagIds[ifdName],
|
|
byteOrder: byteOrder,
|
|
tags: make([]builderTag, 0),
|
|
}
|
|
|
|
return ib
|
|
}
|
|
|
|
func NewIfdBuilderWithExistingIfd(ifd *Ifd, byteOrder binary.ByteOrder) (ib *IfdBuilder) {
|
|
ib = &IfdBuilder{
|
|
ifdName: ifd.Name,
|
|
byteOrder: byteOrder,
|
|
existingOffset: ifd.Offset,
|
|
}
|
|
|
|
return ib
|
|
}
|
|
|
|
func (ib *IfdBuilder) SetNextIfd(nextIfd *IfdBuilder) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
ib.nextIfd = nextIfd
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ib *IfdBuilder) DeleteN(tagId uint16, n int) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
if n < 1 {
|
|
log.Panicf("N must be at least 1: (%d)", n)
|
|
}
|
|
|
|
for ; n > 0; {
|
|
j := -1
|
|
for i, bt := range ib.tags {
|
|
if bt.tagId == tagId {
|
|
j = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if j == -1 {
|
|
log.Panic(ErrTagEntryNotFound)
|
|
}
|
|
|
|
ib.tags = append(ib.tags[:j], ib.tags[j + 1:]...)
|
|
n--
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ib *IfdBuilder) DeleteFirst(tagId uint16) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
err = ib.DeleteN(tagId, 1)
|
|
log.PanicIf(err)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ib *IfdBuilder) DeleteAll(tagId uint16) (n int, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
for {
|
|
err = ib.DeleteN(tagId, 1)
|
|
if log.Is(err, ErrTagEntryNotFound) == true {
|
|
break
|
|
} else if err != nil {
|
|
log.Panic(err)
|
|
}
|
|
|
|
n++
|
|
}
|
|
|
|
return n, nil
|
|
}
|
|
|
|
func (ib *IfdBuilder) ReplaceAt(position int, bt builderTag) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
if position < 0 {
|
|
log.Panicf("replacement position must be 0 or greater")
|
|
} else if position >= len(ib.tags) {
|
|
log.Panicf("replacement position does not exist")
|
|
}
|
|
|
|
ib.tags[position] = bt
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ib *IfdBuilder) Replace(tagId uint16, bt builderTag) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
position, err := ib.Find(tagId)
|
|
log.PanicIf(err)
|
|
|
|
ib.tags[position] = bt
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ib *IfdBuilder) FindN(tagId uint16, maxFound int) (found []int, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
found = make([]int, 0)
|
|
|
|
for i, bt := range ib.tags {
|
|
if bt.tagId == tagId {
|
|
found = append(found, i)
|
|
if maxFound == 0 || len(found) >= maxFound {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return found, nil
|
|
}
|
|
|
|
func (ib *IfdBuilder) Find(tagId uint16) (position int, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
found, err := ib.FindN(tagId, 1)
|
|
log.PanicIf(err)
|
|
|
|
if len(found) == 0 {
|
|
log.Panic(ErrTagEntryNotFound)
|
|
}
|
|
|
|
return found[0], nil
|
|
}
|
|
|
|
// TODO(dustin): !! Switch to producing bytes immediately so that they're validated.
|
|
|
|
func (ib *IfdBuilder) Add(bt builderTag) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
ib.tags = append(ib.tags, bt)
|
|
return nil
|
|
}
|
|
|
|
func (ib *IfdBuilder) AddChildIfd(childIfd *IfdBuilder) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
// TODO(dustin): !! We might not want to take an actual IfdBuilder instance, as
|
|
// these are mutable in nature (unless we definitely want to
|
|
// allow them to tbe chnaged right up until they're actually
|
|
// written). We might be better with a final, immutable tag
|
|
// container insted.
|
|
|
|
if childIfd.ifdTagId == 0 {
|
|
log.Panicf("IFD [%s] can not be used as a child IFD (not associated with a tag-ID)")
|
|
} else if childIfd.byteOrder != ib.byteOrder {
|
|
log.Panicf("Child IFD does not have the same byte-order: [%s] != [%s]", childIfd.byteOrder, ib.byteOrder)
|
|
}
|
|
|
|
bt := builderTag{
|
|
ifdName: childIfd.ifdName,
|
|
tagId: childIfd.ifdTagId,
|
|
value: childIfd,
|
|
}
|
|
|
|
ib.Add(bt)
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddTagsFromExisting does a verbatim copy of the entries in `ifd` to this
|
|
// builder. It excludes child IFDs. This must be added explicitly via
|
|
// `AddChildIfd()`.
|
|
func (ib *IfdBuilder) AddTagsFromExisting(ifd *Ifd, includeTagIds []uint16, excludeTagIds []uint16) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
|
|
// Notes: This is used to update existing IFDs (by constructing a new IFD with existing information).
|
|
// - How to handle the existing allocation? Obviously this will be an update
|
|
// operation, we should try and re-use the current space.
|
|
// - Inevitably, there will be fragmentation as IFDs are changed. We might not
|
|
// be able to avoid reallocation.
|
|
// - !! We'll potentially have to update every recorded tag and IFD offset.
|
|
// - We might just have to refuse to allow updates if we encountered any
|
|
// unmanageable tags (we'll definitely have to finish adding support for
|
|
// the well-known ones).
|
|
//
|
|
// - An IfdEnumerator might not be the right type of argument, here. It actively
|
|
// reads from a file and is not just a static container.
|
|
// - We might want to create a static-container type that can populate from
|
|
// an IfdEnumerator and then be read and re-read (like an IEnumerable vs IList).
|
|
|
|
|
|
|
|
for _, tag := 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 tag.IfdName != "" {
|
|
continue
|
|
}
|
|
|
|
if excludeTagIds != nil && len(excludeTagIds) > 0 {
|
|
found := false
|
|
for _, excludedTagId := range excludeTagIds {
|
|
if excludedTagId == tag.TagId {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if found == true {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if includeTagIds != nil && len(includeTagIds) > 0 {
|
|
// Whether or not there was a list of excludes, if there is a list
|
|
// of includes than the current tag has to be in it.
|
|
|
|
found := false
|
|
for _, includedTagId := range includeTagIds {
|
|
if includedTagId == tag.TagId {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if found == false {
|
|
continue
|
|
}
|
|
}
|
|
|
|
bt := builderTag{
|
|
tagId: tag.TagId,
|
|
|
|
// TODO(dustin): !! For right now, a IfdTagEntry instance will mean that the value will have to be inherited/copied from an existing offset.
|
|
value: tag,
|
|
}
|
|
|
|
err := ib.Add(bt)
|
|
log.PanicIf(err)
|
|
}
|
|
|
|
return nil
|
|
}
|