mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 08:02:27 -05:00 
			
		
		
		
	
		
			
	
	
		
			353 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			353 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) | ||
|  | ) |