diff --git a/exif.go b/exif.go index b83d60c..7b0d996 100644 --- a/exif.go +++ b/exif.go @@ -143,22 +143,34 @@ func makeExifDatumIterator(data *ExifData, cIter *C.Exiv2ExifDatumIterator) *Exi return datum } +// ExifStripKey removes the given key from the EXIF data. func (i *Image) ExifStripKey(key string) error { return i.StripKey(EXIF, key) } +// ExifStripMetadata removes all EXIF metadata except the keys in the unless array. func (i *Image) ExifStripMetadata(unless []string) error { - exifData := i.GetExifData() - for iter := exifData.Iterator(); iter.HasNext(); { - key := iter.Next().Key() - // Skip unless - if contains(key, unless) { - continue - } - err := i.StripKey(EXIF, key) - if err != nil { - return err + var cErr *C.Exiv2Error + + tagsToRemove := getKeysToRemove(i.GetExifData(), unless) + if len(tagsToRemove) == 0 { + return nil + } + + cTags := getCTags(tagsToRemove) + defer func() { + for _, cstr := range cTags { + C.free(unsafe.Pointer(cstr)) } + }() + + C.exiv2_exif_strip_data(i.img, &cTags[0], C.int(len(cTags)), &cErr) + + if cErr != nil { + err := makeError(cErr) + C.exiv2_error_free(cErr) + return err } + return nil } diff --git a/exiv.go b/exiv.go index 179e1c6..9d6e503 100644 --- a/exiv.go +++ b/exiv.go @@ -287,3 +287,31 @@ func contains(needle string, haystack []string) bool { } return false } + +// getCTags converts a map of tags to a C array of C strings +func getCTags(goTags []string) (input []*C.char) { + for _, value := range goTags { + input = append(input, C.CString(value)) + } + + return +} + +// This interface is used to get all the tags from a metadata format. +// Won't be available in the public API. +type dataFormat interface { + AllTags() map[string]string +} + +// getTagsToRemove returns a list of tags to remove from the metadata +// For now this method won't be added to the public API. We must see if it's +// useful or not. +func getKeysToRemove(m dataFormat, unless []string) (tagsToRemove []string) { + for key, _ := range m.AllTags() { + if !contains(key, unless) { + tagsToRemove = append(tagsToRemove, key) + } + } + + return +} diff --git a/exiv_test.go b/exiv_test.go index f62a8ad..c3e835a 100644 --- a/exiv_test.go +++ b/exiv_test.go @@ -149,7 +149,9 @@ func TestMetadata(t *testing.T) { } assert.Equal(t, map[string]string{ - "Exif.Image.ExifTag": "130", + "Exif.Image.Artist": "John Doe", + "Exif.Image.Copyright": "©2023 John Doe, all rights reserved", + "Exif.Image.ExifTag": "202", "Exif.Image.Make": "FakeMake", "Exif.Image.Model": "FakeModel", "Exif.Image.ResolutionUnit": "2", @@ -611,61 +613,38 @@ func TestXmpStrip(t *testing.T) { } func TestStripMetadata(t *testing.T) { + // The test for this function must need plenty of tags to ensure it won't generate an unexpected behavior + initializeImage("testdata/pixel.jpg", t) img, err := goexiv.Open("testdata/pixel.jpg") require.NoError(t, err) - // add two strings to the EXIF data - err = img.SetExifString("Exif.Photo.UserComment", "123") - require.NoError(t, err) - - err = img.SetExifString("Exif.Photo.DateTimeOriginal", "123") - require.NoError(t, err) - - // add two strings to the IPTC data - err = img.SetIptcString("Iptc.Application2.Caption", "123") - require.NoError(t, err) - - err = img.SetIptcString("Iptc.Application2.Keywords", "123") - require.NoError(t, err) - - // add two strings to the XMP data - err = img.SetXmpString("Xmp.dc.description", "123") - require.NoError(t, err) - - err = img.SetXmpString("Xmp.dc.subject", "123") - require.NoError(t, err) - - err = img.StripMetadata([]string{"Exif.Photo.UserComment", "Iptc.Application2.Caption", "Xmp.dc.description"}) - require.NoError(t, err) - err = img.ReadMetadata() require.NoError(t, err) - exifData := img.GetExifData() - iptcData := img.GetIptcData() - xmpData := img.GetXmpData() - - _, err = exifData.GetString("Exif.Photo.UserComment") - require.NoError(t, err) - - _, err = exifData.GetString("Exif.Photo.DateTimeOriginal") - require.Error(t, err) - - _, err = iptcData.GetString("Iptc.Application2.Caption") + err = img.StripMetadata([]string{"Exif.Image.Copyright", "Iptc.Application2.Copyright", "Xmp.iptc.CreditLine"}) require.NoError(t, err) - _, err = iptcData.GetString("Iptc.Application2.Keywords") - require.Error(t, err) + // Exif + exifData := img.GetExifData() + assert.Equal(t, map[string]string{ + "Exif.Image.Copyright": "©2023 John Doe, all rights reserved", + }, exifData.AllTags()) - _, err = xmpData.GetString("Xmp.dc.description") - require.NoError(t, err) + // IPTC + iptcData := img.GetIptcData() + assert.Equal(t, map[string]string{ + "Iptc.Application2.Copyright": "this is the copy, right?", + }, iptcData.AllTags()) - _, err = xmpData.GetString("Xmp.dc.subject") - require.Error(t, err) + // XMP + xmpData := img.GetXmpData() + assert.Equal(t, map[string]string{ + "Xmp.iptc.CreditLine": "John Doe", + }, xmpData.AllTags()) } func BenchmarkImage_GetBytes_KeepAlive(b *testing.B) { - bytes, err := ioutil.ReadFile("testdata/stripped_pixel.jpg") + bytes, err := os.ReadFile("testdata/stripped_pixel.jpg") require.NoError(b, err) var wg sync.WaitGroup @@ -721,6 +700,8 @@ func initializeImage(path string, t *testing.T) { img.SetIptcString("Iptc.Application2.TimeCreated", "124932:0100") exifTags := map[string]string{ + "Exif.Image.Artist": "John Doe", + "Exif.Image.Copyright": "©2023 John Doe, all rights reserved", "Exif.Image.Make": "FakeMake", "Exif.Image.Model": "FakeModel", "Exif.Image.ResolutionUnit": "2", diff --git a/helper.cpp b/helper.cpp index 11dcb9f..9ae4586 100644 --- a/helper.cpp +++ b/helper.cpp @@ -522,6 +522,72 @@ exiv2_xmp_strip_key(Exiv2Image *img, char *key, Exiv2Error **error) } } +void +exiv2_exif_strip_data(Exiv2Image *img, char **keysToRemove, int len, Exiv2Error **error) { + Exiv2::ExifData exifData = img->image->exifData(); + + for (int i = 0; i < len; i++) { + try { + Exiv2::ExifData::iterator pos = exifData.findKey(Exiv2::ExifKey(keysToRemove[i])); + if (pos == exifData.end()) { + continue; + } + exifData.erase(pos); + } catch (Exiv2::Error &e) { + if (error) { + *error = new Exiv2Error(e); + } + } + } + // Finally, write the remaining Exif data to the image file + img->image->setExifData(exifData); + img->image->writeMetadata(); +} + +void +exiv2_iptc_strip_data(Exiv2Image *img, char **keysToRemove, int len, Exiv2Error **error) { + Exiv2::IptcData iptcData = img->image->iptcData(); + + for (int i = 0; i < len; i++) { + try { + Exiv2::IptcData::iterator pos = iptcData.findKey(Exiv2::IptcKey(keysToRemove[i])); + if (pos == iptcData.end()) { + continue; + } + iptcData.erase(pos); + } catch (Exiv2::Error &e) { + if (error) { + *error = new Exiv2Error(e); + } + } + } + // Finally, write the remaining Iptc data to the image file + img->image->setIptcData(iptcData); + img->image->writeMetadata(); +} + +void +exiv2_xmp_strip_data(Exiv2Image *img, char **keysToRemove, int len, Exiv2Error **error) { + Exiv2::XmpData xmpData = img->image->xmpData(); + + for (int i = 0; i < len; i++) { + try { + Exiv2::XmpData::iterator pos = xmpData.findKey(Exiv2::XmpKey(keysToRemove[i])); + if (pos == xmpData.end()) { + continue; + } + xmpData.erase(pos); + } catch (Exiv2::Error &e) { + if (error) { + *error = new Exiv2Error(e); + } + } + } + // Finally, write the remaining Xmp data to the image file + img->image->setXmpData(xmpData); + img->image->writeMetadata(); +} + DEFINE_FREE_FUNCTION(exiv2_exif_data, Exiv2ExifData*); const char* exiv2_exif_datum_key(const Exiv2ExifDatum *datum) diff --git a/helper.h b/helper.h index b6b34b8..a1bcc0f 100644 --- a/helper.h +++ b/helper.h @@ -72,6 +72,10 @@ void exiv2_exif_strip_key(Exiv2Image *img, char *key, Exiv2Error **error); void exiv2_iptc_strip_key(Exiv2Image *img, char *key, Exiv2Error **error); void exiv2_xmp_strip_key(Exiv2Image *img, char *key, Exiv2Error **error); +void exiv2_exif_strip_data(Exiv2Image *img, char **keysToRemove, int len, Exiv2Error **error); +void exiv2_iptc_strip_data(Exiv2Image *img, char **keysToRemove, int len, Exiv2Error **error); +void exiv2_xmp_strip_data(Exiv2Image *img, char **keysToRemove, int len, Exiv2Error **error); + const unsigned char* exiv2_image_icc_profile(Exiv2Image *img); long exiv2_image_icc_profile_size(Exiv2Image *img); diff --git a/iptc.go b/iptc.go index eea8328..bb6d3a3 100644 --- a/iptc.go +++ b/iptc.go @@ -147,22 +147,34 @@ func makeIptcDatumIterator(data *IptcData, cIter *C.Exiv2IptcDatumIterator) *Ipt return datum } +// IptcStripKey removes the given key from the IPTC metadata. func (i *Image) IptcStripKey(key string) error { return i.StripKey(IPTC, key) } +// IptcStripMetadata removes all EXIF metadata except the keys in the unless array. func (i *Image) IptcStripMetadata(unless []string) error { - iptcData := i.GetIptcData() - for iter := iptcData.Iterator(); iter.HasNext(); { - key := iter.Next().Key() - // Skip unless - if contains(key, unless) { - continue - } - err := i.StripKey(IPTC, key) - if err != nil { - return err + var cErr *C.Exiv2Error + + tagsToRemove := getKeysToRemove(i.GetIptcData(), unless) + if len(tagsToRemove) == 0 { + return nil + } + + cTags := getCTags(tagsToRemove) + defer func() { + for _, cstr := range cTags { + C.free(unsafe.Pointer(cstr)) } + }() + + C.exiv2_iptc_strip_data(i.img, &cTags[0], C.int(len(cTags)), &cErr) + + if cErr != nil { + err := makeError(cErr) + C.exiv2_error_free(cErr) + return err } + return nil } diff --git a/xmp.go b/xmp.go index 623b659..e438b7b 100644 --- a/xmp.go +++ b/xmp.go @@ -112,19 +112,41 @@ func (d *XmpData) GetString(key string) (string, error) { return datum.String(), nil } +// AllTags returns all ZMP tags +func (d *XmpData) AllTags() map[string]string { + keyValues := map[string]string{} + for i := d.Iterator(); i.HasNext(); { + d := i.Next() + keyValues[d.Key()] = d.String() + } + + return keyValues +} + +// XmpStripMetadata removes all EXIF metadata except the keys in the unless array. func (i *Image) XmpStripMetadata(unless []string) error { - xmpData := i.GetXmpData() - for iter := xmpData.Iterator(); iter.HasNext(); { - key := iter.Next().Key() - // Skip unless - if contains(key, unless) { - continue - } - err := i.StripKey(XMP, key) - if err != nil { - return err + var cErr *C.Exiv2Error + + tagsToRemove := getKeysToRemove(i.GetXmpData(), unless) + if len(tagsToRemove) == 0 { + return nil + } + + cTags := getCTags(tagsToRemove) + defer func() { + for _, cstr := range cTags { + C.free(unsafe.Pointer(cstr)) } + }() + + C.exiv2_xmp_strip_data(i.img, &cTags[0], C.int(len(cTags)), &cErr) + + if cErr != nil { + err := makeError(cErr) + C.exiv2_error_free(cErr) + return err } + return nil }