diff --git a/imagedecoder_jpg.go b/imagedecoder_jpg.go index 5ff1227..0fdb0e0 100644 --- a/imagedecoder_jpg.go +++ b/imagedecoder_jpg.go @@ -77,7 +77,7 @@ func (e *imageDecoderJPEG) decode() error { e.skip(int64(xmpIDLen)) length -= xmpIDLen r := io.LimitReader(e.r, int64(length)) - if err := decodeXMP(r, e.opts.HandleTag); err != nil { + if err := decodeXMP(r, e.opts); err != nil { return err } continue diff --git a/imagedecoder_tif.go b/imagedecoder_tif.go index 00568e0..1108b79 100644 --- a/imagedecoder_tif.go +++ b/imagedecoder_tif.go @@ -59,7 +59,7 @@ func (e *imageDecoderTIF) decode() error { pos := e.pos() e.seek(int(valueOffset)) r := io.LimitReader(e.r, int64(count)) - if err := decodeXMP(r, e.opts.HandleTag); err != nil { + if err := decodeXMP(r, e.opts); err != nil { return err } sourceSet = sourceSet.Remove(TagSourceXMP) diff --git a/imagedecoder_webp.go b/imagedecoder_webp.go index 05446a7..fecfbb5 100644 --- a/imagedecoder_webp.go +++ b/imagedecoder_webp.go @@ -76,9 +76,8 @@ func (e *decoderWebP) decode() error { chunkLen := e.read4() - switch chunkID { - - case fccVP8X: + switch { + case chunkID == fccVP8X: if chunkLen != 10 { return ErrInvalidFormat } @@ -103,24 +102,17 @@ func (e *decoderWebP) decode() error { if !hasEXIF && !hasXMP { return nil } - case fccEXIF: - if !sourceSet.Has(TagSourceEXIF) { - continue - } + case chunkID == fccEXIF && sourceSet.Has(TagSourceEXIF): r := io.LimitReader(e.r, int64(chunkLen)) dec := newMetaDecoderEXIF(r, e.opts.HandleTag) - if err := dec.decode(); err != nil { return err } sourceSet = sourceSet.Remove(TagSourceEXIF) - case fccXMP: - if !sourceSet.Has(TagSourceXMP) { - continue - } + case chunkID == fccXMP && sourceSet.Has(TagSourceXMP): sourceSet = sourceSet.Remove(TagSourceXMP) r := io.LimitReader(e.r, int64(chunkLen)) - if err := decodeXMP(r, e.opts.HandleTag); err != nil { + if err := decodeXMP(r, e.opts); err != nil { return err } default: diff --git a/imagemeta.go b/imagemeta.go index 42626c0..f7626ce 100644 --- a/imagemeta.go +++ b/imagemeta.go @@ -45,12 +45,12 @@ func Decode(opts Options) (err error) { if opts.R == nil { return fmt.Errorf("need a reader") } - if opts.HandleTag == nil { - return fmt.Errorf("need a HandleTag function") - } if opts.ImageFormat == ImageFormatAuto { return fmt.Errorf("need an image format; format detection not implemented yet") } + if opts.HandleTag == nil { + opts.HandleTag = func(TagInfo) error { return nil } + } if opts.Sources == 0 { opts.Sources = TagSourceEXIF | TagSourceIPTC | TagSourceXMP } @@ -118,6 +118,12 @@ type Options struct { // The function to call for each tag. HandleTag HandleTagFunc + // The default XMP handler is currently very simple: + // It decodes the RDF.Description.Attrs using Go's xml package and passes each tag to HandleTag. + // If HandleXMP is set, the decoder will call this function for each XMP packet instead. + // Note that r must be read completely. + HandleXMP func(r io.Reader) error + // If set, the decoder will only read the given tag sources. // Note that this is a bitmask and you may send multiple sources at once. Sources TagSource diff --git a/imagemeta_test.go b/imagemeta_test.go index 306f288..b4950de 100644 --- a/imagemeta_test.go +++ b/imagemeta_test.go @@ -2,6 +2,7 @@ package imagemeta_test import ( "fmt" + "io" "math" "math/big" "os" @@ -117,6 +118,53 @@ func TestDecodeOrientationOnly(t *testing.T) { } +func TestDecodeCustomXMPHandler(t *testing.T) { + c := qt.New(t) + + img, close := getSunrise(c, imagemeta.ImageFormatWebP) + c.Cleanup(close) + + var xml string + err := imagemeta.Decode( + imagemeta.Options{ + R: img, + ImageFormat: imagemeta.ImageFormatWebP, + HandleXMP: func(r io.Reader) error { + b, err := io.ReadAll(r) + xml = string(b) + return err + }, + Sources: imagemeta.TagSourceXMP, + }, + ) + + c.Assert(err, qt.IsNil) + c.Assert(xml, qt.Contains, "Sunrise in Spain") + +} + +func TestDecodeCustomXMPHandlerShortRead(t *testing.T) { + c := qt.New(t) + + img, close := getSunrise(c, imagemeta.ImageFormatWebP) + c.Cleanup(close) + + err := imagemeta.Decode( + imagemeta.Options{ + R: img, + ImageFormat: imagemeta.ImageFormatWebP, + HandleXMP: func(r io.Reader) error { + return nil + }, + Sources: imagemeta.TagSourceXMP, + }, + ) + + c.Assert(err, qt.IsNotNil) + c.Assert(err.Error(), qt.Contains, "expected EOF after XMP") + +} + func TestSmoke(t *testing.T) { c := qt.New(t) diff --git a/metadecoder_xmp.go b/metadecoder_xmp.go index 9f871de..ad59cd9 100644 --- a/metadecoder_xmp.go +++ b/metadecoder_xmp.go @@ -2,6 +2,7 @@ package imagemeta import ( "encoding/xml" + "errors" "io" ) @@ -17,7 +18,19 @@ type xmpmeta struct { RDF rdf `xml:"RDF"` } -func decodeXMP(r io.Reader, handleTag HandleTagFunc) error { +func decodeXMP(r io.Reader, opts Options) error { + if opts.HandleXMP != nil { + if err := opts.HandleXMP(r); err != nil { + return err + } + // Read one more byte to make sure we're at EOF. + var b [1]byte + if _, err := r.Read(b[:]); err != io.EOF { + return errors.New("expected EOF after XMP") + } + return nil + } + var meta xmpmeta if err := xml.NewDecoder(r).Decode(&meta); err != nil { return err @@ -34,7 +47,7 @@ func decodeXMP(r io.Reader, handleTag HandleTagFunc) error { Value: attr.Value, } - if err := handleTag(tagInfo); err != nil { + if err := opts.HandleTag(tagInfo); err != nil { return err } }