mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 19:22:24 -06:00 
			
		
		
		
	
		
			
				
	
	
		
			352 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package jpegstructure
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
 | 
						|
	"crypto/sha1"
 | 
						|
	"encoding/hex"
 | 
						|
 | 
						|
	"github.com/dsoprea/go-exif/v3"
 | 
						|
	"github.com/dsoprea/go-exif/v3/common"
 | 
						|
	"github.com/dsoprea/go-iptc"
 | 
						|
	"github.com/dsoprea/go-logging"
 | 
						|
	"github.com/dsoprea/go-photoshop-info-format"
 | 
						|
	"github.com/dsoprea/go-utility/v2/image"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	pirIptcImageResourceId = uint16(0x0404)
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	// exifPrefix is the prefix found at the top of an EXIF slice. This is JPEG-
 | 
						|
	// specific.
 | 
						|
	exifPrefix = []byte{'E', 'x', 'i', 'f', 0, 0}
 | 
						|
 | 
						|
	xmpPrefix = []byte("http://ns.adobe.com/xap/1.0/\000")
 | 
						|
 | 
						|
	ps30Prefix = []byte("Photoshop 3.0\000")
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	// ErrNoXmp is returned if XMP data was requested but not found.
 | 
						|
	ErrNoXmp = errors.New("no XMP data")
 | 
						|
 | 
						|
	// ErrNoIptc is returned if IPTC data was requested but not found.
 | 
						|
	ErrNoIptc = errors.New("no IPTC data")
 | 
						|
 | 
						|
	// ErrNoPhotoshopData is returned if Photoshop info was requested but not
 | 
						|
	// found.
 | 
						|
	ErrNoPhotoshopData = errors.New("no photoshop data")
 | 
						|
)
 | 
						|
 | 
						|
// SofSegment has info read from a SOF segment.
 | 
						|
type SofSegment struct {
 | 
						|
	// BitsPerSample is the bits-per-sample.
 | 
						|
	BitsPerSample byte
 | 
						|
 | 
						|
	// Width is the image width.
 | 
						|
	Width uint16
 | 
						|
 | 
						|
	// Height is the image height.
 | 
						|
	Height uint16
 | 
						|
 | 
						|
	// ComponentCount is the number of color components.
 | 
						|
	ComponentCount byte
 | 
						|
}
 | 
						|
 | 
						|
// String returns a string representation of the SOF segment.
 | 
						|
func (ss SofSegment) String() string {
 | 
						|
 | 
						|
	// TODO(dustin): Add test
 | 
						|
 | 
						|
	return fmt.Sprintf("SOF<BitsPerSample=(%d) Width=(%d) Height=(%d) ComponentCount=(%d)>", ss.BitsPerSample, ss.Width, ss.Height, ss.ComponentCount)
 | 
						|
}
 | 
						|
 | 
						|
// SegmentVisitor describes a segment-visitor struct.
 | 
						|
type SegmentVisitor interface {
 | 
						|
	// HandleSegment is triggered for each segment encountered as well as the
 | 
						|
	// scan-data.
 | 
						|
	HandleSegment(markerId byte, markerName string, counter int, lastIsScanData bool) error
 | 
						|
}
 | 
						|
 | 
						|
// SofSegmentVisitor describes a visitor that is only called for each SOF
 | 
						|
// segment.
 | 
						|
type SofSegmentVisitor interface {
 | 
						|
	// HandleSof is called for each encountered SOF segment.
 | 
						|
	HandleSof(sof *SofSegment) error
 | 
						|
}
 | 
						|
 | 
						|
// Segment describes a single segment.
 | 
						|
type Segment struct {
 | 
						|
	MarkerId   byte
 | 
						|
	MarkerName string
 | 
						|
	Offset     int
 | 
						|
	Data       []byte
 | 
						|
 | 
						|
	photoshopInfo map[uint16]photoshopinfo.Photoshop30InfoRecord
 | 
						|
	iptcTags      map[iptc.StreamTagKey][]iptc.TagData
 | 
						|
}
 | 
						|
 | 
						|
// SetExif encodes and sets EXIF data into this segment.
 | 
						|
func (s *Segment) SetExif(ib *exif.IfdBuilder) (err error) {
 | 
						|
	defer func() {
 | 
						|
		if state := recover(); state != nil {
 | 
						|
			err = log.Wrap(state.(error))
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	ibe := exif.NewIfdByteEncoder()
 | 
						|
 | 
						|
	exifData, err := ibe.EncodeToExif(ib)
 | 
						|
	log.PanicIf(err)
 | 
						|
 | 
						|
	l := len(exifPrefix)
 | 
						|
 | 
						|
	s.Data = make([]byte, l+len(exifData))
 | 
						|
	copy(s.Data[0:], exifPrefix)
 | 
						|
	copy(s.Data[l:], exifData)
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Exif returns an `exif.Ifd` instance for the EXIF data we currently have.
 | 
						|
func (s *Segment) Exif() (rootIfd *exif.Ifd, data []byte, err error) {
 | 
						|
	defer func() {
 | 
						|
		if state := recover(); state != nil {
 | 
						|
			err = log.Wrap(state.(error))
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	l := len(exifPrefix)
 | 
						|
 | 
						|
	rawExif := s.Data[l:]
 | 
						|
 | 
						|
	jpegLogger.Debugf(nil, "Attempting to parse (%d) byte EXIF blob (Exif).", len(rawExif))
 | 
						|
 | 
						|
	im, err := exifcommon.NewIfdMappingWithStandard()
 | 
						|
	log.PanicIf(err)
 | 
						|
 | 
						|
	ti := exif.NewTagIndex()
 | 
						|
 | 
						|
	_, index, err := exif.Collect(im, ti, rawExif)
 | 
						|
	log.PanicIf(err)
 | 
						|
 | 
						|
	return index.RootIfd, rawExif, nil
 | 
						|
}
 | 
						|
 | 
						|
// FlatExif parses the EXIF data and just returns a list of tags.
 | 
						|
func (s *Segment) FlatExif() (exifTags []exif.ExifTag, err error) {
 | 
						|
	defer func() {
 | 
						|
		if state := recover(); state != nil {
 | 
						|
			err = log.Wrap(state.(error))
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	// TODO(dustin): Add test
 | 
						|
 | 
						|
	l := len(exifPrefix)
 | 
						|
 | 
						|
	rawExif := s.Data[l:]
 | 
						|
 | 
						|
	jpegLogger.Debugf(nil, "Attempting to parse (%d) byte EXIF blob (FlatExif).", len(rawExif))
 | 
						|
 | 
						|
	exifTags, _, err = exif.GetFlatExifData(rawExif, nil)
 | 
						|
	log.PanicIf(err)
 | 
						|
 | 
						|
	return exifTags, nil
 | 
						|
}
 | 
						|
 | 
						|
// EmbeddedString returns a string of properties that can be embedded into an
 | 
						|
// longer string of properties.
 | 
						|
func (s *Segment) EmbeddedString() string {
 | 
						|
	h := sha1.New()
 | 
						|
	h.Write(s.Data)
 | 
						|
 | 
						|
	// TODO(dustin): Add test
 | 
						|
 | 
						|
	digestString := hex.EncodeToString(h.Sum(nil))
 | 
						|
 | 
						|
	return fmt.Sprintf("OFFSET=(0x%08x %10d) ID=(0x%02x) NAME=[%-5s] SIZE=(%10d) SHA1=[%s]", s.Offset, s.Offset, s.MarkerId, markerNames[s.MarkerId], len(s.Data), digestString)
 | 
						|
}
 | 
						|
 | 
						|
// String returns a descriptive string.
 | 
						|
func (s *Segment) String() string {
 | 
						|
 | 
						|
	// TODO(dustin): Add test
 | 
						|
 | 
						|
	return fmt.Sprintf("Segment<%s>", s.EmbeddedString())
 | 
						|
}
 | 
						|
 | 
						|
// IsExif returns true if EXIF data.
 | 
						|
func (s *Segment) IsExif() bool {
 | 
						|
	if s.MarkerId != MARKER_APP1 {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	// TODO(dustin): Add test
 | 
						|
 | 
						|
	l := len(exifPrefix)
 | 
						|
 | 
						|
	if len(s.Data) < l {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	if bytes.Equal(s.Data[:l], exifPrefix) == false {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
// IsXmp returns true if XMP data.
 | 
						|
func (s *Segment) IsXmp() bool {
 | 
						|
	if s.MarkerId != MARKER_APP1 {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	// TODO(dustin): Add test
 | 
						|
 | 
						|
	l := len(xmpPrefix)
 | 
						|
 | 
						|
	if len(s.Data) < l {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	if bytes.Equal(s.Data[:l], xmpPrefix) == false {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
// FormattedXmp returns a formatted XML string. This only makes sense for a
 | 
						|
// segment comprised of XML data (like XMP).
 | 
						|
func (s *Segment) FormattedXmp() (formatted string, err error) {
 | 
						|
	defer func() {
 | 
						|
		if state := recover(); state != nil {
 | 
						|
			err = log.Wrap(state.(error))
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	// TODO(dustin): Add test
 | 
						|
 | 
						|
	if s.IsXmp() != true {
 | 
						|
		log.Panicf("not an XMP segment")
 | 
						|
	}
 | 
						|
 | 
						|
	l := len(xmpPrefix)
 | 
						|
 | 
						|
	raw := string(s.Data[l:])
 | 
						|
 | 
						|
	formatted, err = FormatXml(raw)
 | 
						|
	log.PanicIf(err)
 | 
						|
 | 
						|
	return formatted, nil
 | 
						|
}
 | 
						|
 | 
						|
func (s *Segment) parsePhotoshopInfo() (photoshopInfo map[uint16]photoshopinfo.Photoshop30InfoRecord, err error) {
 | 
						|
	defer func() {
 | 
						|
		if state := recover(); state != nil {
 | 
						|
			err = log.Wrap(state.(error))
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	if s.photoshopInfo != nil {
 | 
						|
		return s.photoshopInfo, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if s.MarkerId != MARKER_APP13 {
 | 
						|
		return nil, ErrNoPhotoshopData
 | 
						|
	}
 | 
						|
 | 
						|
	l := len(ps30Prefix)
 | 
						|
 | 
						|
	if len(s.Data) < l {
 | 
						|
		return nil, ErrNoPhotoshopData
 | 
						|
	}
 | 
						|
 | 
						|
	if bytes.Equal(s.Data[:l], ps30Prefix) == false {
 | 
						|
		return nil, ErrNoPhotoshopData
 | 
						|
	}
 | 
						|
 | 
						|
	data := s.Data[l:]
 | 
						|
	b := bytes.NewBuffer(data)
 | 
						|
 | 
						|
	// Parse it.
 | 
						|
 | 
						|
	pirIndex, err := photoshopinfo.ReadPhotoshop30Info(b)
 | 
						|
	log.PanicIf(err)
 | 
						|
 | 
						|
	s.photoshopInfo = pirIndex
 | 
						|
 | 
						|
	return s.photoshopInfo, nil
 | 
						|
}
 | 
						|
 | 
						|
// IsIptc returns true if XMP data.
 | 
						|
func (s *Segment) IsIptc() bool {
 | 
						|
	// TODO(dustin): Add test
 | 
						|
 | 
						|
	// There's a cost to determining if there's IPTC data, so we won't do it
 | 
						|
	// more than once.
 | 
						|
	if s.iptcTags != nil {
 | 
						|
		return true
 | 
						|
	}
 | 
						|
 | 
						|
	photoshopInfo, err := s.parsePhotoshopInfo()
 | 
						|
	if err != nil {
 | 
						|
		if err == ErrNoPhotoshopData {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
 | 
						|
		log.Panic(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Bail if the Photoshop info doesn't have IPTC data.
 | 
						|
 | 
						|
	_, found := photoshopInfo[pirIptcImageResourceId]
 | 
						|
	if found == false {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
// Iptc parses Photoshop info (if present) and then parses the IPTC info inside
 | 
						|
// it (if present).
 | 
						|
func (s *Segment) Iptc() (tags map[iptc.StreamTagKey][]iptc.TagData, err error) {
 | 
						|
	defer func() {
 | 
						|
		if state := recover(); state != nil {
 | 
						|
			err = log.Wrap(state.(error))
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	// Cache the parse.
 | 
						|
	if s.iptcTags != nil {
 | 
						|
		return s.iptcTags, nil
 | 
						|
	}
 | 
						|
 | 
						|
	photoshopInfo, err := s.parsePhotoshopInfo()
 | 
						|
	log.PanicIf(err)
 | 
						|
 | 
						|
	iptcPir, found := photoshopInfo[pirIptcImageResourceId]
 | 
						|
	if found == false {
 | 
						|
		return nil, ErrNoIptc
 | 
						|
	}
 | 
						|
 | 
						|
	b := bytes.NewBuffer(iptcPir.Data)
 | 
						|
 | 
						|
	tags, err = iptc.ParseStream(b)
 | 
						|
	log.PanicIf(err)
 | 
						|
 | 
						|
	s.iptcTags = tags
 | 
						|
 | 
						|
	return tags, nil
 | 
						|
}
 | 
						|
 | 
						|
var (
 | 
						|
	// Enforce interface conformance.
 | 
						|
	_ riimage.MediaContext = new(Segment)
 | 
						|
)
 |