diff --git a/assets/webp_container_specification.pdf b/assets/webp_container_specification.pdf new file mode 100644 index 0000000..e2cca5d Binary files /dev/null and b/assets/webp_container_specification.pdf differ diff --git a/go.mod b/go.mod index a3e69ec..065a998 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.16 require ( github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15 github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd + github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e ) diff --git a/media_parser.go b/media_parser.go new file mode 100644 index 0000000..0142365 --- /dev/null +++ b/media_parser.go @@ -0,0 +1,144 @@ +package webp + +import ( + "bytes" + "io" + "os" + + "github.com/dsoprea/go-exif/v3" + "github.com/dsoprea/go-exif/v3/common" + "github.com/dsoprea/go-logging/v2" + "github.com/dsoprea/go-utility/v2/image" +) + +// WebpMediaParser is a `riimage.MediaParser` that knows how to parse JPEG +// images. +type WebpMediaParser struct { +} + +// NewWebpMediaParser returns a new WebpMediaParser. +func NewWebpMediaParser() *WebpMediaParser { + + // TODO(dustin): Add test + + return new(WebpMediaParser) +} + +type RawExifData []byte + +// Exif returns the EXIF's root IFD. +func (ref RawExifData) Exif() (rootIfd *exif.Ifd, data []byte, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + im, err := exifcommon.NewIfdMappingWithStandard() + log.PanicIf(err) + + ti := exif.NewTagIndex() + + _, index, err := exif.Collect(im, ti, ref) + log.PanicIf(err) + + return index.RootIfd, ref, nil +} + +// 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 (wmp *WebpMediaParser) Parse(rs io.ReadSeeker, size int) (mc riimage.MediaContext, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + wp := NewWebpParser(rs) + + visitorCb := func(fourCc [4]byte, dataGetter DataGetterFunc) (err error) { + if fourCc != exifFourCc { + return nil + } + + exifData, err := dataGetter() + log.PanicIf(err) + + mc = RawExifData(exifData) + + return nil + } + + err = wp.enumerateChunks(visitorCb) + log.PanicIf(err) + + if mc != nil { + return mc, nil + } + + return nil, exif.ErrNoExif +} + +// ParseFile parses a JPEG file. Even if it fails, it will return the list of +// segments encountered prior to the failure. +func (wmp *WebpMediaParser) ParseFile(filepath string) (mc 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() + + mc, err = wmp.Parse(f, int(size)) + log.PanicIf(err) + + return mc, nil +} + +// ParseBytes parses a JPEG byte-slice. Even if it fails, it will return the +// list of segments encountered prior to the failure. +func (wmp *WebpMediaParser) ParseBytes(data []byte) (mc riimage.MediaContext, err error) { + defer func() { + if state := recover(); state != nil { + err = log.Wrap(state.(error)) + } + }() + + br := bytes.NewReader(data) + + mc, err = wmp.Parse(br, len(data)) + log.PanicIf(err) + + return mc, nil +} + +// LooksLikeFormat indicates whether the data looks like a JPEG image. +func (wmp *WebpMediaParser) LooksLikeFormat(data []byte) bool { + defer func() { + if errRaw := recover(); errRaw != nil { + log.Panic(errRaw.(error)) + } + }() + + br := bytes.NewReader(data) + wp := NewWebpParser(br) + + _, err := wp.readHeader() + return err == nil +} + +var ( + // Enforce interface conformance. + _ riimage.MediaParser = new(WebpMediaParser) +) diff --git a/media_parser_test.go b/media_parser_test.go new file mode 100644 index 0000000..7efbba6 --- /dev/null +++ b/media_parser_test.go @@ -0,0 +1,80 @@ +package webp + +import ( + "os" + "testing" + + "io/ioutil" + + "github.com/dsoprea/go-exif/v3/common" + "github.com/dsoprea/go-logging/v2" +) + +func TestWebpMediaParser_ParseBytes(t *testing.T) { + wmp := NewWebpMediaParser() + + filepath := GetTestImageFilepath() + + // f, err := os.Open(filepath) + // log.PanicIf(err) + + // defer f.Close() + + data, err := ioutil.ReadFile(filepath) + log.PanicIf(err) + + // f, err := os.Open(filepath) + // log.PanicIf(err) + + // defer f.Close() + + mc, err := wmp.ParseBytes(data) + log.PanicIf(err) + + rootIfd, _, err := mc.Exif() + log.PanicIf(err) + + ii := rootIfd.IfdIdentity() + if ii != exifcommon.IfdStandardIfdIdentity { + t.Fatalf("Root IFD is not the correct type: [%s]", ii) + } +} + +func TestWebpMediaParser_ParseFile(t *testing.T) { + wmp := NewWebpMediaParser() + + filepath := GetTestImageFilepath() + + mc, err := wmp.ParseFile(filepath) + log.PanicIf(err) + + rootIfd, _, err := mc.Exif() + log.PanicIf(err) + + ii := rootIfd.IfdIdentity() + if ii != exifcommon.IfdStandardIfdIdentity { + t.Fatalf("Root IFD is not the correct type: [%s]", ii) + } +} + +func TestWebpMediaParser_Parse(t *testing.T) { + wmp := NewWebpMediaParser() + + filepath := GetTestImageFilepath() + + f, err := os.Open(filepath) + log.PanicIf(err) + + defer f.Close() + + mc, err := wmp.Parse(f, 0) + log.PanicIf(err) + + rootIfd, _, err := mc.Exif() + log.PanicIf(err) + + ii := rootIfd.IfdIdentity() + if ii != exifcommon.IfdStandardIfdIdentity { + t.Fatalf("Root IFD is not the correct type: [%s]", ii) + } +} diff --git a/webp.go b/webp.go index ef57fa4..343f2b9 100644 --- a/webp.go +++ b/webp.go @@ -23,6 +23,12 @@ var ( DefaultEndianness = binary.LittleEndian ) +var ( + riffFileHeaderBytes = [4]byte{'R', 'I', 'F', 'F'} + webpBytes = [4]byte{'W', 'E', 'B', 'P'} + exifFourCc = [4]byte{'E', 'X', 'I', 'F'} +) + // WebpParser parses WEBP RIFF streams. type WebpParser struct { r io.Reader @@ -35,11 +41,6 @@ func NewWebpParser(r io.Reader) *WebpParser { } } -var ( - riffFileHeaderBytes = [4]byte{'R', 'I', 'F', 'F'} - webpBytes = [4]byte{'W', 'E', 'B', 'P'} -) - // readHeader returns the file-header if we're sitting on the first byte. func (wp *WebpParser) readHeader() (fileSize int64, err error) { defer func() { diff --git a/webp_test.go b/webp_test.go index 9326121..4ee9a38 100644 --- a/webp_test.go +++ b/webp_test.go @@ -9,10 +9,6 @@ import ( "github.com/dsoprea/go-logging/v2" ) -var ( - exifFourCc = [4]byte{'E', 'X', 'I', 'F'} -) - func TestWebpParser_readHeader(t *testing.T) { filepath := GetTestImageFilepath()