diff --git a/exif.go b/exif.go index 2fd95ee..f73a5cb 100644 --- a/exif.go +++ b/exif.go @@ -38,7 +38,7 @@ func (e *Exif) Parse(data []byte, visitor TagVisitor) (err error) { }() if e.IsExif(data) == false { - return ErrNotExif + log.Panic(ErrNotExif) } // Good reference: diff --git a/exif_test.go b/exif_test.go index acb5363..cd12551 100644 --- a/exif_test.go +++ b/exif_test.go @@ -5,6 +5,7 @@ import ( "os" "path" "fmt" + "reflect" "io/ioutil" @@ -64,8 +65,16 @@ func TestParse(t *testing.T) { // Run the parse. ti := NewTagIndex() + tags := make([]string, 0) + + visitor := func(tagId uint16, tagType TagType, valueContext ValueContext) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + log.PrintErrorf(err, "The visitor encountered an error.") + } + }() - visitor := func(tagId, tagType uint16, tagCount, valueOffset uint32) (err error) { it, err := ti.GetWithTagId(tagId) if err != nil { if err == ErrTagNotFound { @@ -75,21 +84,42 @@ func TestParse(t *testing.T) { } } - fmt.Printf("Tag: ID=(0x%04x) NAME=[%s] IFD=[%s] TYPE=(%d) COUNT=(%d) VALUE-OFFSET=(%d)\n", tagId, it.Name, it.Ifd, tagType, tagCount, valueOffset) + valueString, err := tagType.ValueString(valueContext, true) + log.PanicIf(err) -// 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 + description := fmt.Sprintf("ID=(0x%04x) NAME=[%s] IFD=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]", tagId, it.Name, it.Ifd, valueContext.UnitCount, tagType.Name(), valueString) + tags = append(tags, description) return nil } err = e.Parse(data[foundAt:], visitor) log.PanicIf(err) + + expected := []string { + "ID=(0x010f) NAME=[Make] IFD=[Image] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]", + "ID=(0x0110) NAME=[Model] IFD=[Image] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]", + "ID=(0x0112) NAME=[Orientation] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[1]", + "ID=(0x011a) NAME=[XResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]", + "ID=(0x011b) NAME=[YResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]", + "ID=(0x0128) NAME=[ResolutionUnit] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[2]", + "ID=(0x0132) NAME=[DateTime] IFD=[Image] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]", + "ID=(0x013b) NAME=[Artist] IFD=[Image] COUNT=(1) TYPE=[ASCII] VALUE=[]", + "ID=(0x0213) NAME=[YCbCrPositioning] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[2]", + "ID=(0x8298) NAME=[Copyright] IFD=[Image] COUNT=(1) TYPE=[ASCII] VALUE=[]", + "ID=(0x8769) NAME=[ExifTag] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[360]", + "ID=(0x8825) NAME=[GPSTag] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[9554]", + "ID=(0x0103) NAME=[Compression] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[6]", + "ID=(0x011a) NAME=[XResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]", + "ID=(0x011b) NAME=[YResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]", + "ID=(0x0128) NAME=[ResolutionUnit] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[2]", + "ID=(0x0201) NAME=[JPEGInterchangeFormat] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[11444]", + "ID=(0x0202) NAME=[JPEGInterchangeFormatLength] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[21491]", + } + + if reflect.DeepEqual(tags, expected) == false { + t.Fatalf("tags not correct:\n%v", tags) + } } func init() { diff --git a/ifd.go b/ifd.go index 9eba654..11a5dfb 100644 --- a/ifd.go +++ b/ifd.go @@ -9,24 +9,10 @@ import ( "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 @@ -86,23 +72,43 @@ func (ifd *Ifd) getUint16() (value uint16, err error) { // 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) { +func (ifd *Ifd) getUint32() (value uint32, raw []byte, err error) { defer func() { if state := recover(); state != nil { err = log.Wrap(state.(error)) } }() - err = ifd.read(ifd.buffer, &value) + raw = make([]byte, 4) + + _, err = ifd.buffer.Read(raw) log.PanicIf(err) ifd.currentOffset += 4 - return value, nil + if ifd.byteOrder.IsBigEndian() { + value = binary.BigEndian.Uint32(raw) + } else { + value = binary.LittleEndian.Uint32(raw) + } + + return value, raw, nil +} + +// ValueContext describes all of the parameters required to find and extract +// the actual tag value. +type ValueContext struct { + UnitCount uint32 + ValueOffset uint32 + RawValueOffset []byte + RawExif []byte } -type TagVisitor func(tagId, tagType uint16, tagCount, valueOffset uint32) (err error) +// TagVisitor is an optional callback that can get hit for every tag we parse +// through. `rawExif` is the byte array startign after the EXIF header (where +// the offsets of all IFDs and values are calculated from). +type TagVisitor func(tagId uint16, tagType TagType, valueContext ValueContext) (err error) // parseCurrentIfd decodes the IFD block that we're currently sitting on the // first byte of. @@ -129,19 +135,28 @@ func (ifd *Ifd) parseCurrentIfd(visitor TagVisitor) (nextIfdOffset uint32, err e tagType, err := ifd.getUint16() log.PanicIf(err) - tagCount, err := ifd.getUint32() + unitCount, _, err := ifd.getUint32() log.PanicIf(err) - valueOffset, err := ifd.getUint32() + valueOffset, rawValueOffset, err := ifd.getUint32() log.PanicIf(err) if visitor != nil { - err := visitor(tagId, tagType, tagCount, valueOffset) + tt := NewTagType(tagType, ifd.byteOrder) + + vc := ValueContext{ + UnitCount: unitCount, + ValueOffset: valueOffset, + RawValueOffset: rawValueOffset, + RawExif: ifd.data[ifd.ifdTopOffset:], + } + + err := visitor(tagId, tt, vc) log.PanicIf(err) } } - nextIfdOffset, err = ifd.getUint32() + nextIfdOffset, _, err = ifd.getUint32() log.PanicIf(err) ifdLogger.Debugf(nil, "Next IFD at offset: (%08x)", nextIfdOffset) diff --git a/tags.go b/tags.go index ef5ed09..813aa23 100644 --- a/tags.go +++ b/tags.go @@ -140,7 +140,7 @@ func (ti *TagIndex) GetWithTagId(id uint16) (it *IndexedTag, err error) { it, found := ti.tagsById[id] if found == false { - return nil, ErrTagNotFound + log.Panic(ErrTagNotFound) } return it, nil diff --git a/type.go b/type.go new file mode 100644 index 0000000..340308b --- /dev/null +++ b/type.go @@ -0,0 +1,611 @@ +package exif + +import ( + "bytes" + "errors" + "fmt" + + "encoding/binary" + + "github.com/dsoprea/go-logging" +) + +const ( + TypeByte = uint16(1) + TypeAscii = uint16(2) + TypeShort = uint16(3) + TypeLong = uint16(4) + TypeRational = uint16(5) + TypeUndefined = uint16(6) + TypeSignedLong = uint16(9) + TypeSignedRational = uint16(10) +) + +var ( + TypeNames = map[uint16]string { + TypeByte: "BYTE", + TypeAscii: "ASCII", + TypeShort: "SHORT", + TypeLong: "LONG", + TypeRational: "RATIONAL", + TypeUndefined: "UNDEFINED", + TypeSignedLong: "SLONG", + TypeSignedRational: "SRATIONAL", + } +) + +var ( + typeLogger = log.NewLogger("exif.type") +) + +var ( + // ErrCantDetermineTagValueSize is used when we're trying to determine a + //size for a non-standard/undefined type. + ErrCantDetermineTagValueSize = errors.New("can not determine tag-value size") + + // ErrNotEnoughData is used when there isn't enough data to accomodate what + // we're trying to parse (sizeof(type) * unit_count). + ErrNotEnoughData = errors.New("not enough data for type") + + // ErrWrongType is used when we try to parse anything other than the current type. + ErrWrongType = errors.New("wrong type, can not parse") +) + + +const ( + BigEndianByteOrder = iota + LittleEndianByteOrder = iota +) + +type IfdByteOrder int + +func (ibo IfdByteOrder) IsBigEndian() bool { + return ibo == BigEndianByteOrder +} + +func (ibo IfdByteOrder) IsLittleEndian() bool { + return ibo == LittleEndianByteOrder +} + + +type Rational struct { + Numerator uint32 + Denominator uint32 +} + +type SignedRational struct { + Numerator int32 + Denominator int32 +} + + +type TagType struct { + tagType uint16 + name string + byteOrder IfdByteOrder +} + +func NewTagType(tagType uint16, byteOrder IfdByteOrder) TagType { + name, found := TypeNames[tagType] + if found == false { + log.Panicf("tag-type not valid: 0x%04x", tagType) + } + + return TagType{ + tagType: tagType, + name: name, + byteOrder: byteOrder, + } +} + +func (tt TagType) String() string { + return fmt.Sprintf("TagType", tt.name) +} + +func (tt TagType) Name() string { + return tt.name +} + +func (tt TagType) Type() uint16 { + return tt.tagType +} + + +func (tt TagType) Size() int { + if tt.tagType == TypeByte { + return 1 + } else if tt.tagType == TypeAscii { + return 1 + } else if tt.tagType == TypeShort { + return 2 + } else if tt.tagType == TypeLong { + return 4 + } else if tt.tagType == TypeRational { + return 8 + } else if tt.tagType == TypeSignedLong { + return 4 + } else if tt.tagType == TypeSignedRational { + return 8 + } else { + log.Panic(ErrCantDetermineTagValueSize) + + // Never called. + return 0 + } +} + +// ValueIsEmbedded will return a boolean indicating whether the value should be +// found directly within the IFD entry or an offset to somewhere else. +func (tt TagType) ValueIsEmbedded(unitCount uint32) bool { + return (tt.Size() * int(unitCount)) <= 4 +} + +func (tt TagType) ParseBytes(data []byte, rawCount uint32) (value []uint8, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.tagType != TypeByte { + log.Panic(ErrWrongType) + } + + count := int(rawCount) + + if len(data) < (tt.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + value = make([]uint8, count) + for i := 0; i < count; i++ { + value[i] = uint8(data[i]) + } + + return value, nil +} + +func (tt TagType) ParseAscii(data []byte, rawCount uint32) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.tagType != TypeAscii { + log.Panic(ErrWrongType) + } + + count := int(rawCount) + + if len(data) < (tt.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + return string(data[:count]), nil +} + +func (tt TagType) ParseShorts(data []byte, rawCount uint32) (value []uint16, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.tagType != TypeShort { + log.Panic(ErrWrongType) + } + + count := int(rawCount) + + if len(data) < (tt.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + value = make([]uint16, count) + for i := 0; i < count; i++ { + if tt.byteOrder.IsBigEndian() { + value[i] = binary.BigEndian.Uint16(data[i*2:]) + } else { + value[i] = binary.LittleEndian.Uint16(data[i*2:]) + } + } + + return value, nil +} + +func (tt TagType) ParseLongs(data []byte, rawCount uint32) (value []uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.tagType != TypeLong { + log.Panic(ErrWrongType) + } + + count := int(rawCount) + + if len(data) < (tt.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + value = make([]uint32, count) + for i := 0; i < count; i++ { + if tt.byteOrder.IsBigEndian() { + value[i] = binary.BigEndian.Uint32(data[i*4:]) + } else { + value[i] = binary.LittleEndian.Uint32(data[i*4:]) + } + } + + return value, nil +} + +func (tt TagType) ParseRationals(data []byte, rawCount uint32) (value []Rational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.tagType != TypeRational { + log.Panic(ErrWrongType) + } + + count := int(rawCount) + + if len(data) < (tt.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + value = make([]Rational, count) + for i := 0; i < count; i++ { + if tt.byteOrder.IsBigEndian() { + value[i].Numerator = binary.BigEndian.Uint32(data[i*8:]) + value[i].Denominator = binary.BigEndian.Uint32(data[i*8 + 4:]) + } else { + value[i].Numerator = binary.LittleEndian.Uint32(data[i*8:]) + value[i].Denominator = binary.LittleEndian.Uint32(data[i*8 + 4:]) + } + } + + return value, nil +} + +func (tt TagType) ParseSignedLongs(data []byte, rawCount uint32) (value []int32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.tagType != TypeSignedLong { + log.Panic(ErrWrongType) + } + + count := int(rawCount) + + if len(data) < (tt.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + b := bytes.NewBuffer(data) + + value = make([]int32, count) + for i := 0; i < count; i++ { + if tt.byteOrder.IsBigEndian() { + err := binary.Read(b, binary.BigEndian, &value[i]) + log.PanicIf(err) + } else { + err := binary.Read(b, binary.LittleEndian, &value[i]) + log.PanicIf(err) + } + } + + return value, nil +} + +func (tt TagType) ParseSignedRationals(data []byte, rawCount uint32) (value []SignedRational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.tagType != TypeSignedRational { + log.Panic(ErrWrongType) + } + + count := int(rawCount) + + if len(data) < (tt.Size() * count) { + log.Panic(ErrNotEnoughData) + } + + b := bytes.NewBuffer(data) + + value = make([]SignedRational, count) + for i := 0; i < count; i++ { + if tt.byteOrder.IsBigEndian() { + err = binary.Read(b, binary.BigEndian, &value[i].Numerator) + log.PanicIf(err) + + err = binary.Read(b, binary.BigEndian, &value[i].Denominator) + log.PanicIf(err) + } else { + err = binary.Read(b, binary.LittleEndian, &value[i].Numerator) + log.PanicIf(err) + + err = binary.Read(b, binary.LittleEndian, &value[i].Denominator) + log.PanicIf(err) + } + } + + return value, nil +} + +func (tt TagType) ReadByteValues(valueContext ValueContext) (value []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.ValueIsEmbedded(valueContext.UnitCount) == true { + typeLogger.Debugf(nil, "Reading BYTE value (embedded).") + + value, err = tt.ParseBytes(valueContext.RawValueOffset, valueContext.UnitCount) + log.PanicIf(err) + } else { + typeLogger.Debugf(nil, "Reading BYTE value (at offset).") + + value, err = tt.ParseBytes(valueContext.RawExif[valueContext.ValueOffset:], valueContext.UnitCount) + log.PanicIf(err) + } + + return value, nil +} + +func (tt TagType) ReadAsciiValue(valueContext ValueContext) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.ValueIsEmbedded(valueContext.UnitCount) == true { + typeLogger.Debugf(nil, "Reading ASCII value (embedded).") + + value, err = tt.ParseAscii(valueContext.RawValueOffset, valueContext.UnitCount) + log.PanicIf(err) + } else { + typeLogger.Debugf(nil, "Reading ASCII value (at offset).") + + value, err = tt.ParseAscii(valueContext.RawExif[valueContext.ValueOffset:], valueContext.UnitCount) + log.PanicIf(err) + } + + len_ := len(value) + if value[len_ - 1] != 0 { + typeLogger.Warningf(nil, "ascii value not terminated with nul: [%s]", value) + +// TODO(dustin): !! Debugging + fmt.Printf("ascii value not terminated with nul: [%s]", value) + + return value, nil + } else { + return value[:len_ - 1], nil + } +} + +func (tt TagType) ReadShortValues(valueContext ValueContext) (value []uint16, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.ValueIsEmbedded(valueContext.UnitCount) == true { + typeLogger.Debugf(nil, "Reading SHORT value (embedded).") + + value, err = tt.ParseShorts(valueContext.RawValueOffset, valueContext.UnitCount) + log.PanicIf(err) + } else { + typeLogger.Debugf(nil, "Reading SHORT value (at offset).") + + value, err = tt.ParseShorts(valueContext.RawExif[valueContext.ValueOffset:], valueContext.UnitCount) + log.PanicIf(err) + } + + return value, nil +} + +func (tt TagType) ReadLongValues(valueContext ValueContext) (value []uint32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.ValueIsEmbedded(valueContext.UnitCount) == true { + typeLogger.Debugf(nil, "Reading LONG value (embedded).") + + value, err = tt.ParseLongs(valueContext.RawValueOffset, valueContext.UnitCount) + log.PanicIf(err) + } else { + typeLogger.Debugf(nil, "Reading LONG value (at offset).") + + value, err = tt.ParseLongs(valueContext.RawExif[valueContext.ValueOffset:], valueContext.UnitCount) + log.PanicIf(err) + } + + return value, nil +} + +func (tt TagType) ReadRationalValues(valueContext ValueContext) (value []Rational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.ValueIsEmbedded(valueContext.UnitCount) == true { + typeLogger.Debugf(nil, "Reading RATIONAL value (embedded).") + + value, err = tt.ParseRationals(valueContext.RawValueOffset, valueContext.UnitCount) + log.PanicIf(err) + } else { + typeLogger.Debugf(nil, "Reading RATIONAL value (at offset).") + + value, err = tt.ParseRationals(valueContext.RawExif[valueContext.ValueOffset:], valueContext.UnitCount) + log.PanicIf(err) + } + + return value, nil +} + +func (tt TagType) ReadSignedLongValues(valueContext ValueContext) (value []int32, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.ValueIsEmbedded(valueContext.UnitCount) == true { + typeLogger.Debugf(nil, "Reading SLONG value (embedded).") + + value, err = tt.ParseSignedLongs(valueContext.RawValueOffset, valueContext.UnitCount) + log.PanicIf(err) + } else { + typeLogger.Debugf(nil, "Reading SLONG value (at offset).") + + value, err = tt.ParseSignedLongs(valueContext.RawExif[valueContext.ValueOffset:], valueContext.UnitCount) + log.PanicIf(err) + } + + return value, nil +} + +func (tt TagType) ReadSignedRationalValues(valueContext ValueContext) (value []SignedRational, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.ValueIsEmbedded(valueContext.UnitCount) == true { + typeLogger.Debugf(nil, "Reading SRATIONAL value (embedded).") + + value, err = tt.ParseSignedRationals(valueContext.RawValueOffset, valueContext.UnitCount) + log.PanicIf(err) + } else { + typeLogger.Debugf(nil, "Reading SRATIONAL value (at offset).") + + value, err = tt.ParseSignedRationals(valueContext.RawExif[valueContext.ValueOffset:], valueContext.UnitCount) + log.PanicIf(err) + } + + return value, nil +} + +// ValueString extracts and parses the given value, and returns a flat string. +// Where the type is not ASCII, `justFirst` indicates whether to just stringify +// the first item in the slice (or return an empty string if the slice is +// empty). +func (tt TagType) ValueString(valueContext ValueContext, justFirst bool) (value string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if tt.Type() == TypeByte { + raw, err := tt.ReadByteValues(valueContext) + log.PanicIf(err) + + if justFirst == false { + return fmt.Sprintf("%v", raw), nil + } else if valueContext.UnitCount > 0 { + return fmt.Sprintf("%v", raw[0]), nil + } else { + return "", nil + } + } else if tt.Type() == TypeAscii { + raw, err := tt.ReadAsciiValue(valueContext) + log.PanicIf(err) + + return fmt.Sprintf("%s", raw), nil + } else if tt.Type() == TypeShort { + raw, err := tt.ReadShortValues(valueContext) + log.PanicIf(err) + + if justFirst == false { + return fmt.Sprintf("%v", raw), nil + } else if valueContext.UnitCount > 0 { + return fmt.Sprintf("%v", raw[0]), nil + } else { + return "", nil + } + } else if tt.Type() == TypeLong { + raw, err := tt.ReadLongValues(valueContext) + log.PanicIf(err) + + if justFirst == false { + return fmt.Sprintf("%v", raw), nil + } else if valueContext.UnitCount > 0 { + return fmt.Sprintf("%v", raw[0]), nil + } else { + return "", nil + } + } else if tt.Type() == TypeRational { + raw, err := tt.ReadRationalValues(valueContext) + log.PanicIf(err) + + parts := make([]string, len(raw)) + for i, r := range raw { + parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator) + } + + if justFirst == false { + return fmt.Sprintf("%v", parts), nil + } else if valueContext.UnitCount > 0 { + return parts[0], nil + } else { + return "", nil + } + } else if tt.Type() == TypeSignedLong { + raw, err := tt.ReadSignedLongValues(valueContext) + log.PanicIf(err) + + if justFirst == false { + return fmt.Sprintf("%v", raw), nil + } else if valueContext.UnitCount > 0 { + return fmt.Sprintf("%v", raw[0]), nil + } else { + return "", nil + } + } else if tt.Type() == TypeRational { + raw, err := tt.ReadSignedRationalValues(valueContext) + log.PanicIf(err) + + parts := make([]string, len(raw)) + for i, r := range raw { + parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator) + } + + if justFirst == false { + return fmt.Sprintf("%v", raw), nil + } else if valueContext.UnitCount > 0 { + return parts[0], nil + } else { + return "", nil + } + } else { + log.Panicf("value of type [%s] is unparseable", tt) + + // Never called. + return "", nil + } +}