diff options
Diffstat (limited to 'vendor/github.com/dsoprea/go-jpeg-image-structure/v2')
10 files changed, 1758 insertions, 0 deletions
diff --git a/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/.MODULE_ROOT b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/.MODULE_ROOT new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/.MODULE_ROOT diff --git a/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/LICENSE b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/LICENSE new file mode 100644 index 000000000..163291ed6 --- /dev/null +++ b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/LICENSE @@ -0,0 +1,9 @@ +MIT LICENSE + +Copyright 2020 Dustin Oprea + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/README.md b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/README.md new file mode 100644 index 000000000..bf60ef504 --- /dev/null +++ b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/README.md @@ -0,0 +1,10 @@ +[](https://travis-ci.org/dsoprea/go-jpeg-image-structure/v2) +[](https://codecov.io/gh/dsoprea/go-jpeg-image-structure) +[](https://goreportcard.com/report/github.com/dsoprea/go-jpeg-image-structure/v2) +[](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure/v2) + +## Overview + +Parse raw JPEG data into individual segments of data. You can print or export this data, including hash digests for each. You can also parse/modify the EXIF data and write an updated image. + +EXIF, XMP, and IPTC data can also be extracted. The provided CLI tool can print this data as well. diff --git a/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/markers.go b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/markers.go new file mode 100644 index 000000000..a12171bd8 --- /dev/null +++ b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/markers.go @@ -0,0 +1,212 @@ +package jpegstructure + +import ( + "github.com/dsoprea/go-logging" +) + +const ( + // MARKER_SOI marker + MARKER_SOI = 0xd8 + + // MARKER_EOI marker + MARKER_EOI = 0xd9 + + // MARKER_SOS marker + MARKER_SOS = 0xda + + // MARKER_SOD marker + MARKER_SOD = 0x93 + + // MARKER_DQT marker + MARKER_DQT = 0xdb + + // MARKER_APP0 marker + MARKER_APP0 = 0xe0 + + // MARKER_APP1 marker + MARKER_APP1 = 0xe1 + + // MARKER_APP2 marker + MARKER_APP2 = 0xe2 + + // MARKER_APP3 marker + MARKER_APP3 = 0xe3 + + // MARKER_APP4 marker + MARKER_APP4 = 0xe4 + + // MARKER_APP5 marker + MARKER_APP5 = 0xe5 + + // MARKER_APP6 marker + MARKER_APP6 = 0xe6 + + // MARKER_APP7 marker + MARKER_APP7 = 0xe7 + + // MARKER_APP8 marker + MARKER_APP8 = 0xe8 + + // MARKER_APP10 marker + MARKER_APP10 = 0xea + + // MARKER_APP12 marker + MARKER_APP12 = 0xec + + // MARKER_APP13 marker + MARKER_APP13 = 0xed + + // MARKER_APP14 marker + MARKER_APP14 = 0xee + + // MARKER_APP15 marker + MARKER_APP15 = 0xef + + // MARKER_COM marker + MARKER_COM = 0xfe + + // MARKER_CME marker + MARKER_CME = 0x64 + + // MARKER_SIZ marker + MARKER_SIZ = 0x51 + + // MARKER_DHT marker + MARKER_DHT = 0xc4 + + // MARKER_JPG marker + MARKER_JPG = 0xc8 + + // MARKER_DAC marker + MARKER_DAC = 0xcc + + // MARKER_SOF0 marker + MARKER_SOF0 = 0xc0 + + // MARKER_SOF1 marker + MARKER_SOF1 = 0xc1 + + // MARKER_SOF2 marker + MARKER_SOF2 = 0xc2 + + // MARKER_SOF3 marker + MARKER_SOF3 = 0xc3 + + // MARKER_SOF5 marker + MARKER_SOF5 = 0xc5 + + // MARKER_SOF6 marker + MARKER_SOF6 = 0xc6 + + // MARKER_SOF7 marker + MARKER_SOF7 = 0xc7 + + // MARKER_SOF9 marker + MARKER_SOF9 = 0xc9 + + // MARKER_SOF10 marker + MARKER_SOF10 = 0xca + + // MARKER_SOF11 marker + MARKER_SOF11 = 0xcb + + // MARKER_SOF13 marker + MARKER_SOF13 = 0xcd + + // MARKER_SOF14 marker + MARKER_SOF14 = 0xce + + // MARKER_SOF15 marker + MARKER_SOF15 = 0xcf +) + +var ( + jpegLogger = log.NewLogger("jpegstructure.jpeg") + jpegMagicStandard = []byte{0xff, MARKER_SOI, 0xff} + jpegMagic2000 = []byte{0xff, 0x4f, 0xff} + + markerLen = map[byte]int{ + 0x00: 0, + 0x01: 0, + 0xd0: 0, + 0xd1: 0, + 0xd2: 0, + 0xd3: 0, + 0xd4: 0, + 0xd5: 0, + 0xd6: 0, + 0xd7: 0, + 0xd8: 0, + 0xd9: 0, + 0xda: 0, + + // J2C + 0x30: 0, + 0x31: 0, + 0x32: 0, + 0x33: 0, + 0x34: 0, + 0x35: 0, + 0x36: 0, + 0x37: 0, + 0x38: 0, + 0x39: 0, + 0x3a: 0, + 0x3b: 0, + 0x3c: 0, + 0x3d: 0, + 0x3e: 0, + 0x3f: 0, + 0x4f: 0, + 0x92: 0, + 0x93: 0, + + // J2C extensions + 0x74: 4, + 0x75: 4, + 0x77: 4, + } + + markerNames = map[byte]string{ + MARKER_SOI: "SOI", + MARKER_EOI: "EOI", + MARKER_SOS: "SOS", + MARKER_SOD: "SOD", + MARKER_DQT: "DQT", + MARKER_APP0: "APP0", + MARKER_APP1: "APP1", + MARKER_APP2: "APP2", + MARKER_APP3: "APP3", + MARKER_APP4: "APP4", + MARKER_APP5: "APP5", + MARKER_APP6: "APP6", + MARKER_APP7: "APP7", + MARKER_APP8: "APP8", + MARKER_APP10: "APP10", + MARKER_APP12: "APP12", + MARKER_APP13: "APP13", + MARKER_APP14: "APP14", + MARKER_APP15: "APP15", + MARKER_COM: "COM", + MARKER_CME: "CME", + MARKER_SIZ: "SIZ", + + MARKER_DHT: "DHT", + MARKER_JPG: "JPG", + MARKER_DAC: "DAC", + + MARKER_SOF0: "SOF0", + MARKER_SOF1: "SOF1", + MARKER_SOF2: "SOF2", + MARKER_SOF3: "SOF3", + MARKER_SOF5: "SOF5", + MARKER_SOF6: "SOF6", + MARKER_SOF7: "SOF7", + MARKER_SOF9: "SOF9", + MARKER_SOF10: "SOF10", + MARKER_SOF11: "SOF11", + MARKER_SOF13: "SOF13", + MARKER_SOF14: "SOF14", + MARKER_SOF15: "SOF15", + } +) diff --git a/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/media_parser.go b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/media_parser.go new file mode 100644 index 000000000..e6fc60bc4 --- /dev/null +++ b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/media_parser.go @@ -0,0 +1,139 @@ +package jpegstructure + +import ( + "bufio" + "bytes" + "image" + "io" + "os" + + "image/jpeg" + + "github.com/dsoprea/go-logging" + "github.com/dsoprea/go-utility/v2/image" +) + +// JpegMediaParser is a `riimage.MediaParser` that knows how to parse JPEG +// images. +type JpegMediaParser struct { +} + +// NewJpegMediaParser returns a new JpegMediaParser. +func NewJpegMediaParser() *JpegMediaParser { + + // TODO(dustin): Add test + + return new(JpegMediaParser) +} + +// Parse parses a JPEG uses an `io.ReadSeeker`. Even if it fails, it will return +// the list of segments encountered prior to the failure. +func (jmp *JpegMediaParser) Parse(rs io.ReadSeeker, size int) (ec riimage.MediaContext, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + s := bufio.NewScanner(rs) + + // Since each segment can be any size, our buffer must allowed to grow as + // large as the file. + buffer := []byte{} + s.Buffer(buffer, size) + + js := NewJpegSplitter(nil) + s.Split(js.Split) + + for s.Scan() != false { + } + + // Always return the segments that were parsed, at least until there was an + // error. + ec = js.Segments() + + log.PanicIf(s.Err()) + + return ec, nil +} + +// ParseFile parses a JPEG file. Even if it fails, it will return the list of +// segments encountered prior to the failure. +func (jmp *JpegMediaParser) ParseFile(filepath string) (ec riimage.MediaContext, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + f, err := os.Open(filepath) + log.PanicIf(err) + + defer f.Close() + + stat, err := f.Stat() + log.PanicIf(err) + + size := stat.Size() + + sl, err := jmp.Parse(f, int(size)) + + // Always return the segments that were parsed, at least until there was an + // error. + ec = sl + + log.PanicIf(err) + + return ec, nil +} + +// ParseBytes parses a JPEG byte-slice. Even if it fails, it will return the +// list of segments encountered prior to the failure. +func (jmp *JpegMediaParser) ParseBytes(data []byte) (ec riimage.MediaContext, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + br := bytes.NewReader(data) + + sl, err := jmp.Parse(br, len(data)) + + // Always return the segments that were parsed, at least until there was an + // error. + ec = sl + + log.PanicIf(err) + + return ec, nil +} + +// LooksLikeFormat indicates whether the data looks like a JPEG image. +func (jmp *JpegMediaParser) LooksLikeFormat(data []byte) bool { + if len(data) < 4 { + return false + } + + l := len(data) + if data[0] != 0xff || data[1] != MARKER_SOI || data[l-2] != 0xff || data[l-1] != MARKER_EOI { + return false + } + + return true +} + +// GetImage returns an image.Image-compatible struct. +func (jmp *JpegMediaParser) GetImage(r io.Reader) (img image.Image, err error) { + img, err = jpeg.Decode(r) + log.PanicIf(err) + + return img, nil +} + +var ( + // Enforce interface conformance. + _ riimage.MediaParser = new(JpegMediaParser) +) diff --git a/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/segment.go b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/segment.go new file mode 100644 index 000000000..6b433bf1f --- /dev/null +++ b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/segment.go @@ -0,0 +1,352 @@ +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) +) diff --git a/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/segment_list.go b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/segment_list.go new file mode 100644 index 000000000..b4f4d5810 --- /dev/null +++ b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/segment_list.go @@ -0,0 +1,416 @@ +package jpegstructure + +import ( + "bytes" + "fmt" + "io" + + "crypto/sha1" + "encoding/binary" + + "github.com/dsoprea/go-exif/v3" + "github.com/dsoprea/go-exif/v3/common" + "github.com/dsoprea/go-iptc" + "github.com/dsoprea/go-logging" +) + +// SegmentList contains a slice of segments. +type SegmentList struct { + segments []*Segment +} + +// NewSegmentList returns a new SegmentList struct. +func NewSegmentList(segments []*Segment) (sl *SegmentList) { + if segments == nil { + segments = make([]*Segment, 0) + } + + return &SegmentList{ + segments: segments, + } +} + +// OffsetsEqual returns true is all segments have the same marker-IDs and were +// found at the same offsets. +func (sl *SegmentList) OffsetsEqual(o *SegmentList) bool { + if len(o.segments) != len(sl.segments) { + return false + } + + for i, s := range o.segments { + if s.MarkerId != sl.segments[i].MarkerId || s.Offset != sl.segments[i].Offset { + return false + } + } + + return true +} + +// Segments returns the underlying slice of segments. +func (sl *SegmentList) Segments() []*Segment { + return sl.segments +} + +// Add adds another segment. +func (sl *SegmentList) Add(s *Segment) { + sl.segments = append(sl.segments, s) +} + +// Print prints segment info. +func (sl *SegmentList) Print() { + if len(sl.segments) == 0 { + fmt.Printf("No segments.\n") + } else { + exifIndex, _, err := sl.FindExif() + if err != nil { + if err == exif.ErrNoExif { + exifIndex = -1 + } else { + log.Panic(err) + } + } + + xmpIndex, _, err := sl.FindXmp() + if err != nil { + if err == ErrNoXmp { + xmpIndex = -1 + } else { + log.Panic(err) + } + } + + iptcIndex, _, err := sl.FindIptc() + if err != nil { + if err == ErrNoIptc { + iptcIndex = -1 + } else { + log.Panic(err) + } + } + + for i, s := range sl.segments { + fmt.Printf("%2d: %s", i, s.EmbeddedString()) + + if i == exifIndex { + fmt.Printf(" [EXIF]") + } else if i == xmpIndex { + fmt.Printf(" [XMP]") + } else if i == iptcIndex { + fmt.Printf(" [IPTC]") + } + + fmt.Printf("\n") + } + } +} + +// Validate checks that all of the markers are actually located at all of the +// recorded offsets. +func (sl *SegmentList) Validate(data []byte) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if len(sl.segments) < 2 { + log.Panicf("minimum segments not found") + } + + if sl.segments[0].MarkerId != MARKER_SOI { + log.Panicf("first segment not SOI") + } else if sl.segments[len(sl.segments)-1].MarkerId != MARKER_EOI { + log.Panicf("last segment not EOI") + } + + lastOffset := 0 + for i, s := range sl.segments { + if lastOffset != 0 && s.Offset <= lastOffset { + log.Panicf("segment offset not greater than the last: SEGMENT=(%d) (0x%08x) <= (0x%08x)", i, s.Offset, lastOffset) + } + + // The scan-data doesn't start with a marker. + if s.MarkerId == 0x0 { + continue + } + + o := s.Offset + if bytes.Compare(data[o:o+2], []byte{0xff, s.MarkerId}) != 0 { + log.Panicf("segment offset does not point to the start of a segment: SEGMENT=(%d) (0x%08x)", i, s.Offset) + } + + lastOffset = o + } + + return nil +} + +// FindExif returns the the segment that hosts the EXIF data (if present). +func (sl *SegmentList) FindExif() (index int, segment *Segment, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + for i, s := range sl.segments { + if s.IsExif() == true { + return i, s, nil + } + } + + return -1, nil, exif.ErrNoExif +} + +// FindXmp returns the the segment that hosts the XMP data (if present). +func (sl *SegmentList) FindXmp() (index int, segment *Segment, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + for i, s := range sl.segments { + if s.IsXmp() == true { + return i, s, nil + } + } + + return -1, nil, ErrNoXmp +} + +// FindIptc returns the the segment that hosts the IPTC data (if present). +func (sl *SegmentList) FindIptc() (index int, segment *Segment, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + for i, s := range sl.segments { + if s.IsIptc() == true { + return i, s, nil + } + } + + return -1, nil, ErrNoIptc +} + +// Exif returns an `exif.Ifd` instance for the EXIF data we currently have. +func (sl *SegmentList) Exif() (rootIfd *exif.Ifd, rawExif []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + _, s, err := sl.FindExif() + log.PanicIf(err) + + rootIfd, rawExif, err = s.Exif() + log.PanicIf(err) + + return rootIfd, rawExif, nil +} + +// Iptc returns embedded IPTC data if present. +func (sl *SegmentList) Iptc() (tags map[iptc.StreamTagKey][]iptc.TagData, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add comment and return data. + + _, s, err := sl.FindIptc() + log.PanicIf(err) + + tags, err = s.Iptc() + log.PanicIf(err) + + return tags, nil +} + +// ConstructExifBuilder returns an `exif.IfdBuilder` instance (needed for +// modifying) preloaded with all existing tags. +func (sl *SegmentList) ConstructExifBuilder() (rootIb *exif.IfdBuilder, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + rootIfd, _, err := sl.Exif() + if log.Is(err, exif.ErrNoExif) == true { + // No EXIF. Just create a boilerplate builder. + + im := exifcommon.NewIfdMapping() + + err := exifcommon.LoadStandardIfds(im) + log.PanicIf(err) + + ti := exif.NewTagIndex() + + rootIb := + exif.NewIfdBuilder( + im, + ti, + exifcommon.IfdStandardIfdIdentity, + exifcommon.EncodeDefaultByteOrder) + + return rootIb, nil + } else if err != nil { + log.Panic(err) + } + + rootIb = exif.NewIfdBuilderFromExistingChain(rootIfd) + + return rootIb, nil +} + +// DumpExif returns an unstructured list of tags (useful when just reviewing). +func (sl *SegmentList) DumpExif() (segmentIndex int, segment *Segment, exifTags []exif.ExifTag, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + segmentIndex, s, err := sl.FindExif() + if err != nil { + if err == exif.ErrNoExif { + return 0, nil, nil, err + } + + log.Panic(err) + } + + exifTags, err = s.FlatExif() + log.PanicIf(err) + + return segmentIndex, s, exifTags, nil +} + +func makeEmptyExifSegment() (s *Segment) { + + // TODO(dustin): Add test + + return &Segment{ + MarkerId: MARKER_APP1, + } +} + +// SetExif encodes and sets EXIF data into the given segment. If `index` is -1, +// append a new segment. +func (sl *SegmentList) SetExif(ib *exif.IfdBuilder) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + _, s, err := sl.FindExif() + if err != nil { + if log.Is(err, exif.ErrNoExif) == false { + log.Panic(err) + } + + s = makeEmptyExifSegment() + + prefix := sl.segments[:1] + + // Install it near the beginning where we know it's safe. We can't + // insert it after the EOI segment, and there might be more than one + // depending on implementation and/or lax adherence to the standard. + tail := append([]*Segment{s}, sl.segments[1:]...) + + sl.segments = append(prefix, tail...) + } + + err = s.SetExif(ib) + log.PanicIf(err) + + return nil +} + +// DropExif will drop the EXIF data if present. +func (sl *SegmentList) DropExif() (wasDropped bool, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // TODO(dustin): Add test + + i, _, err := sl.FindExif() + if err == nil { + // Found. + sl.segments = append(sl.segments[:i], sl.segments[i+1:]...) + + return true, nil + } else if log.Is(err, exif.ErrNoExif) == false { + log.Panic(err) + } + + // Not found. + return false, nil +} + +// Write writes the segment data to the given `io.Writer`. +func (sl *SegmentList) Write(w io.Writer) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + offset := 0 + + for i, s := range sl.segments { + h := sha1.New() + h.Write(s.Data) + + // The scan-data will have a marker-ID of (0) because it doesn't have a + // marker-ID or length. + if s.MarkerId != 0 { + _, err := w.Write([]byte{0xff}) + log.PanicIf(err) + + offset++ + + _, err = w.Write([]byte{s.MarkerId}) + log.PanicIf(err) + + offset++ + + sizeLen, found := markerLen[s.MarkerId] + if found == false || sizeLen == 2 { + sizeLen = 2 + l := uint16(len(s.Data) + sizeLen) + + err = binary.Write(w, binary.BigEndian, &l) + log.PanicIf(err) + + offset += 2 + } else if sizeLen == 4 { + l := uint32(len(s.Data) + sizeLen) + + err = binary.Write(w, binary.BigEndian, &l) + log.PanicIf(err) + + offset += 4 + } else if sizeLen != 0 { + log.Panicf("not a supported marker-size: SEGMENT-INDEX=(%d) MARKER-ID=(0x%02x) MARKER-SIZE-LEN=(%d)", i, s.MarkerId, sizeLen) + } + } + + _, err := w.Write(s.Data) + log.PanicIf(err) + + offset += len(s.Data) + } + + return nil +} diff --git a/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/splitter.go b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/splitter.go new file mode 100644 index 000000000..8e9c7c020 --- /dev/null +++ b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/splitter.go @@ -0,0 +1,437 @@ +package jpegstructure + +import ( + "bufio" + "bytes" + "io" + + "encoding/binary" + + "github.com/dsoprea/go-logging" +) + +// JpegSplitter uses the Go stream splitter to divide the JPEG stream into +// segments. +type JpegSplitter struct { + lastMarkerId byte + lastMarkerName string + counter int + lastIsScanData bool + visitor interface{} + + currentOffset int + segments *SegmentList + + scandataOffset int +} + +// NewJpegSplitter returns a new JpegSplitter. +func NewJpegSplitter(visitor interface{}) *JpegSplitter { + return &JpegSplitter{ + segments: NewSegmentList(nil), + visitor: visitor, + } +} + +// Segments returns all found segments. +func (js *JpegSplitter) Segments() *SegmentList { + return js.segments +} + +// MarkerId returns the ID of the last processed marker. +func (js *JpegSplitter) MarkerId() byte { + return js.lastMarkerId +} + +// MarkerName returns the name of the last-processed marker. +func (js *JpegSplitter) MarkerName() string { + return js.lastMarkerName +} + +// Counter returns the number of processed segments. +func (js *JpegSplitter) Counter() int { + return js.counter +} + +// IsScanData returns whether the last processed segment was scan-data. +func (js *JpegSplitter) IsScanData() bool { + return js.lastIsScanData +} + +func (js *JpegSplitter) processScanData(data []byte) (advanceBytes int, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + // Search through the segment, past all 0xff's therein, until we encounter + // the EOI segment. + + dataLength := -1 + for i := js.scandataOffset; i < len(data); i++ { + thisByte := data[i] + + if i == 0 { + continue + } + + lastByte := data[i-1] + if lastByte != 0xff { + continue + } + + if thisByte == 0x00 || thisByte >= 0xd0 && thisByte <= 0xd8 { + continue + } + + // After all of the other checks, this means that we're on the EOF + // segment. + if thisByte != MARKER_EOI { + continue + } + + dataLength = i - 1 + break + } + + if dataLength == -1 { + // On the next pass, start on the last byte of this pass, just in case + // the first byte of the two-byte sequence is here. + js.scandataOffset = len(data) - 1 + + jpegLogger.Debugf(nil, "Scan-data not fully available (%d).", len(data)) + return 0, nil + } + + js.lastIsScanData = true + js.lastMarkerId = 0 + js.lastMarkerName = "" + + // Note that we don't increment the counter since this isn't an actual + // segment. + + jpegLogger.Debugf(nil, "End of scan-data.") + + err = js.handleSegment(0x0, "!SCANDATA", 0x0, data[:dataLength]) + log.PanicIf(err) + + return dataLength, nil +} + +func (js *JpegSplitter) readSegment(data []byte) (count int, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + if js.counter == 0 { + // Verify magic bytes. + + if len(data) < 3 { + jpegLogger.Debugf(nil, "Not enough (1)") + return 0, nil + } + + if data[0] == jpegMagic2000[0] && data[1] == jpegMagic2000[1] && data[2] == jpegMagic2000[2] { + // TODO(dustin): Revisit JPEG2000 support. + log.Panicf("JPEG2000 not supported") + } + + if data[0] != jpegMagicStandard[0] || data[1] != jpegMagicStandard[1] || data[2] != jpegMagicStandard[2] { + log.Panicf("file does not look like a JPEG: (%02x) (%02x) (%02x)", data[0], data[1], data[2]) + } + } + + chunkLength := len(data) + + jpegLogger.Debugf(nil, "SPLIT: LEN=(%d) COUNTER=(%d)", chunkLength, js.counter) + + if js.scanDataIsNext() == true { + // If the last segment was the SOS, we're currently sitting on scan data. + // Search for the EOI marker afterward in order to know how much data + // there is. Return this as its own token. + // + // REF: https://stackoverflow.com/questions/26715684/parsing-jpeg-sos-marker + + advanceBytes, err := js.processScanData(data) + log.PanicIf(err) + + // This will either return 0 and implicitly request that we need more + // data and then need to run again or will return an actual byte count + // to progress by. + + return advanceBytes, nil + } else if js.lastMarkerId == MARKER_EOI { + // We have more data following the EOI, which is unexpected. There + // might be non-standard cruft at the end of the file. Terminate the + // parse because the file-structure is, technically, complete at this + // point. + + return 0, io.EOF + } else { + js.lastIsScanData = false + } + + // If we're here, we're supposed to be sitting on the 0xff bytes at the + // beginning of a segment (just before the marker). + + if data[0] != 0xff { + log.Panicf("not on new segment marker @ (%d): (%02X)", js.currentOffset, data[0]) + } + + i := 0 + found := false + for ; i < chunkLength; i++ { + jpegLogger.Debugf(nil, "Prefix check: (%d) %02X", i, data[i]) + + if data[i] != 0xff { + found = true + break + } + } + + jpegLogger.Debugf(nil, "Skipped over leading 0xFF bytes: (%d)", i) + + if found == false || i >= chunkLength { + jpegLogger.Debugf(nil, "Not enough (3)") + return 0, nil + } + + markerId := data[i] + + js.lastMarkerName = markerNames[markerId] + + sizeLen, found := markerLen[markerId] + jpegLogger.Debugf(nil, "MARKER-ID=%x SIZELEN=%v FOUND=%v", markerId, sizeLen, found) + + i++ + + b := bytes.NewBuffer(data[i:]) + payloadLength := 0 + + // marker-ID + size => 2 + <dynamic> + headerSize := 2 + sizeLen + + if found == false { + + // It's not one of the static-length markers. Read the length. + // + // The length is an unsigned 16-bit network/big-endian. + + // marker-ID + size => 2 + 2 + headerSize = 2 + 2 + + if i+2 >= chunkLength { + jpegLogger.Debugf(nil, "Not enough (4)") + return 0, nil + } + + l := uint16(0) + err = binary.Read(b, binary.BigEndian, &l) + log.PanicIf(err) + + if l <= 2 { + log.Panicf("length of size read for non-special marker (%02x) is unexpectedly not more than two.", markerId) + } + + // (l includes the bytes of the length itself.) + payloadLength = int(l) - 2 + jpegLogger.Debugf(nil, "DataLength (dynamically-sized segment): (%d)", payloadLength) + + i += 2 + } else if sizeLen > 0 { + + // Accommodates the non-zero markers in our marker index, which only + // represent J2C extensions. + // + // The length is an unsigned 32-bit network/big-endian. + + // TODO(dustin): !! This needs to be tested, but we need an image. + + if sizeLen != 4 { + log.Panicf("known non-zero marker is not four bytes, which is not currently handled: M=(%x)", markerId) + } + + if i+4 >= chunkLength { + jpegLogger.Debugf(nil, "Not enough (5)") + return 0, nil + } + + l := uint32(0) + err = binary.Read(b, binary.BigEndian, &l) + log.PanicIf(err) + + payloadLength = int(l) - 4 + jpegLogger.Debugf(nil, "DataLength (four-byte-length segment): (%u)", l) + + i += 4 + } + + jpegLogger.Debugf(nil, "PAYLOAD-LENGTH: %d", payloadLength) + + payload := data[i:] + + if payloadLength < 0 { + log.Panicf("payload length less than zero: (%d)", payloadLength) + } + + i += int(payloadLength) + + if i > chunkLength { + jpegLogger.Debugf(nil, "Not enough (6)") + return 0, nil + } + + jpegLogger.Debugf(nil, "Found whole segment.") + + js.lastMarkerId = markerId + + payloadWindow := payload[:payloadLength] + err = js.handleSegment(markerId, js.lastMarkerName, headerSize, payloadWindow) + log.PanicIf(err) + + js.counter++ + + jpegLogger.Debugf(nil, "Returning advance of (%d)", i) + + return i, nil +} + +func (js *JpegSplitter) scanDataIsNext() bool { + return js.lastMarkerId == MARKER_SOS +} + +// Split is the base splitting function that satisfies `bufio.SplitFunc`. +func (js *JpegSplitter) Split(data []byte, atEOF bool) (advance int, token []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + for len(data) > 0 { + currentAdvance, err := js.readSegment(data) + if err != nil { + if err == io.EOF { + // We've encountered an EOI marker. + return 0, nil, err + } + + log.Panic(err) + } + + if currentAdvance == 0 { + if len(data) > 0 && atEOF == true { + // Provide a little context in the error message. + + if js.scanDataIsNext() == true { + // Yes, we've ran into this. + + log.Panicf("scan-data is unbounded; EOI not encountered before EOF") + } else { + log.Panicf("partial segment data encountered before scan-data") + } + } + + // We don't have enough data for another segment. + break + } + + data = data[currentAdvance:] + advance += currentAdvance + } + + return advance, nil, nil +} + +func (js *JpegSplitter) parseSof(data []byte) (sof *SofSegment, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + stream := bytes.NewBuffer(data) + buffer := bufio.NewReader(stream) + + bitsPerSample, err := buffer.ReadByte() + log.PanicIf(err) + + height := uint16(0) + err = binary.Read(buffer, binary.BigEndian, &height) + log.PanicIf(err) + + width := uint16(0) + err = binary.Read(buffer, binary.BigEndian, &width) + log.PanicIf(err) + + componentCount, err := buffer.ReadByte() + log.PanicIf(err) + + sof = &SofSegment{ + BitsPerSample: bitsPerSample, + Width: width, + Height: height, + ComponentCount: componentCount, + } + + return sof, nil +} + +func (js *JpegSplitter) parseAppData(markerId byte, data []byte) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + return nil +} + +func (js *JpegSplitter) handleSegment(markerId byte, markerName string, headerSize int, payload []byte) (err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + cloned := make([]byte, len(payload)) + copy(cloned, payload) + + s := &Segment{ + MarkerId: markerId, + MarkerName: markerName, + Offset: js.currentOffset, + Data: cloned, + } + + jpegLogger.Debugf(nil, "Encountered marker (0x%02x) [%s] at offset (%d)", markerId, markerName, js.currentOffset) + + js.currentOffset += headerSize + len(payload) + + js.segments.Add(s) + + sv, ok := js.visitor.(SegmentVisitor) + if ok == true { + err = sv.HandleSegment(js.lastMarkerId, js.lastMarkerName, js.counter, js.lastIsScanData) + log.PanicIf(err) + } + + if markerId >= MARKER_SOF0 && markerId <= MARKER_SOF15 { + ssv, ok := js.visitor.(SofSegmentVisitor) + if ok == true { + sof, err := js.parseSof(payload) + log.PanicIf(err) + + err = ssv.HandleSof(sof) + log.PanicIf(err) + } + } else if markerId >= MARKER_APP0 && markerId <= MARKER_APP15 { + err := js.parseAppData(markerId, payload) + log.PanicIf(err) + } + + return nil +} diff --git a/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/testing_common.go b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/testing_common.go new file mode 100644 index 000000000..e7169c2f0 --- /dev/null +++ b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/testing_common.go @@ -0,0 +1,73 @@ +package jpegstructure + +import ( + "os" + "path" + + "github.com/dsoprea/go-logging" +) + +var ( + testImageRelFilepath = "NDM_8901.jpg" +) + +var ( + moduleRootPath = "" + assetsPath = "" +) + +// GetModuleRootPath returns the root-path of the module. +func GetModuleRootPath() string { + if moduleRootPath == "" { + moduleRootPath = os.Getenv("JPEG_MODULE_ROOT_PATH") + if moduleRootPath != "" { + return moduleRootPath + } + + currentWd, err := os.Getwd() + log.PanicIf(err) + + currentPath := currentWd + visited := make([]string, 0) + + for { + tryStampFilepath := path.Join(currentPath, ".MODULE_ROOT") + + _, err := os.Stat(tryStampFilepath) + if err != nil && os.IsNotExist(err) != true { + log.Panic(err) + } else if err == nil { + break + } + + visited = append(visited, tryStampFilepath) + + currentPath = path.Dir(currentPath) + if currentPath == "/" { + log.Panicf("could not find module-root: %v", visited) + } + } + + moduleRootPath = currentPath + } + + return moduleRootPath +} + +// GetTestAssetsPath returns the path of the test-assets. +func GetTestAssetsPath() string { + if assetsPath == "" { + moduleRootPath := GetModuleRootPath() + assetsPath = path.Join(moduleRootPath, "assets") + } + + return assetsPath +} + +// GetTestImageFilepath returns the file-path of the common test-image. +func GetTestImageFilepath() string { + assetsPath := GetTestAssetsPath() + filepath := path.Join(assetsPath, testImageRelFilepath) + + return filepath +} diff --git a/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/utility.go b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/utility.go new file mode 100644 index 000000000..1c618ba6d --- /dev/null +++ b/vendor/github.com/dsoprea/go-jpeg-image-structure/v2/utility.go @@ -0,0 +1,110 @@ +package jpegstructure + +import ( + "bytes" + "fmt" + "sort" + "strings" + + "github.com/dsoprea/go-logging" + "github.com/go-xmlfmt/xmlfmt" +) + +// DumpBytes prints the hex for a given byte-slice. +func DumpBytes(data []byte) { + fmt.Printf("DUMP: ") + for _, x := range data { + fmt.Printf("%02x ", x) + } + + fmt.Printf("\n") +} + +// DumpBytesClause prints a Go-formatted byte-slice expression. +func DumpBytesClause(data []byte) { + fmt.Printf("DUMP: ") + + fmt.Printf("[]byte { ") + + for i, x := range data { + fmt.Printf("0x%02x", x) + + if i < len(data)-1 { + fmt.Printf(", ") + } + } + + fmt.Printf(" }\n") +} + +// DumpBytesToString returns a string of hex-encoded bytes. +func DumpBytesToString(data []byte) string { + b := new(bytes.Buffer) + + for i, x := range data { + _, err := b.WriteString(fmt.Sprintf("%02x", x)) + log.PanicIf(err) + + if i < len(data)-1 { + _, err := b.WriteRune(' ') + log.PanicIf(err) + } + } + + return b.String() +} + +// DumpBytesClauseToString returns a string of Go-formatted byte values. +func DumpBytesClauseToString(data []byte) string { + b := new(bytes.Buffer) + + for i, x := range data { + _, err := b.WriteString(fmt.Sprintf("0x%02x", x)) + log.PanicIf(err) + + if i < len(data)-1 { + _, err := b.WriteString(", ") + log.PanicIf(err) + } + } + + return b.String() +} + +// FormatXml prettifies XML data. +func FormatXml(raw string) (formatted string, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + formatted = xmlfmt.FormatXML(raw, " ", " ") + formatted = strings.TrimSpace(formatted) + + return formatted, nil +} + +// SortStringStringMap sorts a string-string dictionary and returns it as a list +// of 2-tuples. +func SortStringStringMap(data map[string]string) (sorted [][2]string) { + // Sort keys. + + sortedKeys := make([]string, len(data)) + i := 0 + for key := range data { + sortedKeys[i] = key + i++ + } + + sort.Strings(sortedKeys) + + // Build result. + + sorted = make([][2]string, len(sortedKeys)) + for i, key := range sortedKeys { + sorted[i] = [2]string{key, data[key]} + } + + return sorted +} |