diff --git a/docs/libs/gst-plugins-base-libs-docs.sgml b/docs/libs/gst-plugins-base-libs-docs.sgml index 538236050f..f954c3f41e 100644 --- a/docs/libs/gst-plugins-base-libs-docs.sgml +++ b/docs/libs/gst-plugins-base-libs-docs.sgml @@ -186,6 +186,7 @@ + diff --git a/docs/libs/gst-plugins-base-libs-sections.txt b/docs/libs/gst-plugins-base-libs-sections.txt index 1a91601aac..c0237e6c3d 100644 --- a/docs/libs/gst-plugins-base-libs-sections.txt +++ b/docs/libs/gst-plugins-base-libs-sections.txt @@ -1663,6 +1663,14 @@ gst_tag_to_id3_tag gst_tag_list_add_id3_image +
+gsttagxmp +gst/tag/tag.h +gst_tag_list_from_xmp_buffer +gst_tag_list_to_xmp_buffer + +
+
gsttagdemux gst/tag/gsttagdemux.h diff --git a/gst-libs/gst/tag/Makefile.am b/gst-libs/gst/tag/Makefile.am index 6d978cc1d7..d7073e546c 100644 --- a/gst-libs/gst/tag/Makefile.am +++ b/gst-libs/gst/tag/Makefile.am @@ -6,7 +6,9 @@ libgsttaginclude_HEADERS = \ lib_LTLIBRARIES = libgsttag-@GST_MAJORMINOR@.la -libgsttag_@GST_MAJORMINOR@_la_SOURCES = gstvorbistag.c gstid3tag.c lang.c tags.c gsttagdemux.c +libgsttag_@GST_MAJORMINOR@_la_SOURCES = \ + gstvorbistag.c gstid3tag.c gstxmptag.c \ + lang.c tags.c gsttagdemux.c libgsttag_@GST_MAJORMINOR@_la_CFLAGS = $(GST_PLUGINS_BASE_CFLAGS) $(GST_BASE_CFLAGS) $(GST_CFLAGS) libgsttag_@GST_MAJORMINOR@_la_LIBADD = $(GST_BASE_LIBS) $(GST_LIBS) libgsttag_@GST_MAJORMINOR@_la_LDFLAGS = $(GST_LIB_LDFLAGS) $(GST_ALL_LDFLAGS) $(GST_LT_LDFLAGS) diff --git a/gst-libs/gst/tag/gstxmptag.c b/gst-libs/gst/tag/gstxmptag.c new file mode 100644 index 0000000000..d5e76b4898 --- /dev/null +++ b/gst-libs/gst/tag/gstxmptag.c @@ -0,0 +1,504 @@ +/* GStreamer + * Copyright (C) 2010 Stefan Kost + * + * gstxmptag.c: library for reading / modifying xmp tags + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/** + * SECTION:gsttagxmp + * @short_description: tag mappings and support functions for plugins + * dealing with xmp packets + * @see_also: #GstTagList + * + * Contains various utility functions for plugins to parse or create + * xmp packets and map them to and from #GstTagLists. + * + * Please note that the xmp parser is very lightweight and not strict at all. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include "gsttageditingprivate.h" +#include +#include +#include +#include + +/* look at this page for addtional schemas + * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/XMP.html + */ +static const GstTagEntryMatch tag_matches[] = { + /* dublic code metadata + * http://dublincore.org/documents/dces/ + */ + {GST_TAG_ARTIST, "dc:creator"}, + {GST_TAG_COPYRIGHT, "dc:rights"}, + {GST_TAG_DATE, "dc:date"}, + {GST_TAG_DATE, "exif:DateTimeOriginal"}, + {GST_TAG_DESCRIPTION, "dc:description"}, + {GST_TAG_KEYWORDS, "dc:subject"}, + {GST_TAG_TITLE, "dc:title"}, + /* FIXME: we probably want GST_TAG_{,AUDIO_,VIDEO_}MIME_TYPE */ + {GST_TAG_VIDEO_CODEC, "dc:format"}, + /* */ + {NULL, NULL} +}; + +typedef struct _GstXmpNamespaceMatch GstXmpNamespaceMatch; +struct _GstXmpNamespaceMatch +{ + const gchar *ns_prefix; + const gchar *ns_uri; +}; + +static const GstXmpNamespaceMatch ns_match[] = { + {"dc", "http://purl.org/dc/elements/1.1/"}, + {"exif", "http://ns.adobe.com/exif/1.0/"}, + {"tiff", "http://ns.adobe.com/tiff/1.0/"}, + {"xap", "http://ns.adobe.com/xap/1.0/"}, + {NULL, NULL} +}; + +typedef struct _GstXmpNamespaceMap GstXmpNamespaceMap; +struct _GstXmpNamespaceMap +{ + const gchar *original_ns; + gchar *gstreamer_ns; +}; +static GstXmpNamespaceMap ns_map[] = { + {"dc", NULL}, + {"exif", NULL}, + {"tiff", NULL}, + {"xap", NULL}, + {NULL, NULL} +}; + +/* parsing */ + +static void +read_one_tag (GstTagList * list, const gchar * tag, const gchar * v) +{ + GType tag_type = gst_tag_get_type (tag); + + /* add gstreamer tag depending on type */ + switch (tag_type) { + case G_TYPE_STRING:{ + gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, tag, v, NULL); + break; + } + default: + if (tag_type == GST_TYPE_DATE) { + GDate *date; + gint d, m, y; + + /* this is ISO 8601 Date and Time Format + * %F Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99) + * %T The time in 24-hour notation (%H:%M:%S). (SU) + * e.g. 2009-05-30T18:26:14+03:00 */ + + /* FIXME: this would be the proper way, but needs + #define _XOPEN_SOURCE before #include + + date = g_date_new (); + struct tm tm={0,}; + strptime (dts, "%FT%TZ", &tm); + g_date_set_time_t (date, mktime(&tm)); + */ + /* FIXME: this cannot parse the date + date = g_date_new (); + g_date_set_parse (date, v); + if (g_date_valid (date)) { + gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, tag, + date, NULL); + } else { + GST_WARNING ("unparsable date: '%s'", v); + } + */ + /* poor mans straw */ + sscanf (v, "%04d-%02d-%02dT", &y, &m, &d); + date = g_date_new_dmy (d, m, y); + gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, tag, date, NULL); + g_date_free (date); + } else { + GST_WARNING ("unhandled type for %s from xmp", tag); + } + break; + } +} + +/** + * gst_tag_list_from_xmp_buffer: + * @list: buffer + * + * Parse a xmp packet into a taglist. + * + * Returns: new taglist or %NULL, free the list when done + * + * Since: 0.10.29 + */ +GstTagList * +gst_tag_list_from_xmp_buffer (const GstBuffer * buffer) +{ + GstTagList *list = NULL; + const gchar *xps, *xp1, *xp2, *xpe, *ns, *ne; + guint len, max_ft_len; + gboolean in_tag; + gchar *part, *pp; + guint i; + const gchar *last_tag = NULL; + + g_return_val_if_fail (GST_IS_BUFFER (buffer), NULL); + g_return_val_if_fail (GST_BUFFER_SIZE (buffer) > 0, NULL); + + xps = (const gchar *) GST_BUFFER_DATA (buffer); + len = GST_BUFFER_SIZE (buffer); + xpe = &xps[len + 1]; + + /* check header and footer */ + xp1 = g_strstr_len (xps, len, "' && *xp1 != '<' && xp1 < xpe) + xp1++; + if (*xp1 != '>') + goto missing_header; + + max_ft_len = 1 + strlen ("\n"); + if (len < max_ft_len) + goto missing_footer; + + GST_DEBUG ("checking footer: [%s]", &xps[len - max_ft_len]); + xp2 = g_strstr_len (&xps[len - max_ft_len], max_ft_len, " and text until first xml-node */ + xp1++; + while (*xp1 != '<' && xp1 < xpe) + xp1++; + + /* no tag can be longer that the whole buffer */ + part = g_malloc (xp2 - xp1); + list = gst_tag_list_new (); + + /* parse data into a list of nodes */ + /* data is between xp1..xp2 */ + in_tag = TRUE; + ns = ne = xp1; + pp = part; + while (ne < xp2) { + if (in_tag) { + ne++; + while (ne < xp2 && *ne != '>' && *ne != '<') { + if (*ne == '\n' || *ne == '\t' || *ne == ' ') { + while (ne < xp2 && (*ne == '\n' || *ne == '\t' || *ne == ' ')) + ne++; + *pp++ = ' '; + } else { + *pp++ = *ne++; + } + } + *pp = '\0'; + if (*ne != '>') + goto broken_xml; + /* create node */ + /* {XML, ns, ne-ns} */ + if (ns[0] != '/') { + gchar *as = strchr (part, ' '); + /* only log start nodes */ + GST_INFO ("xml: %s", part); + + if (as) { + gchar *ae, *d; + + /* skip ' ' and scan the attributes */ + as++; + d = ae = as; + + /* split attr=value pairs */ + while (*ae != '\0') { + if (*ae == '=') { + /* attr/value delimmiter */ + d = ae; + } else if (*ae == '"') { + /* scan values */ + gchar *v; + + ae++; + while (*ae != '\0' && *ae != '"') + ae++; + + *d = *ae = '\0'; + v = &d[2]; + GST_INFO (" : [%s][%s]", as, v); + if (!strncmp (as, "xmlns:", 6)) { + i = 0; + /* we need to rewrite known namespaces to what we use in + * tag_matches */ + while (ns_match[i].ns_prefix) { + if (!strcmp (ns_match[i].ns_uri, v)) + break; + i++; + } + if (ns_match[i].ns_prefix) { + if (strcmp (ns_map[i].original_ns, &as[6])) { + ns_map[i].gstreamer_ns = g_strdup (&as[6]); + } + } + } else { + /* FIXME: eventualy rewrite ns + * find ':' + * check if ns before ':' is in ns_map and ns_map[i].gstreamer_ns!=NULL + * do 2 stage filter in tag_matches + */ + i = 0; + while (tag_matches[i].gstreamer_tag) { + if (!strcmp (tag_matches[i].original_tag, as)) + break; + i++; + } + if (tag_matches[i].gstreamer_tag) { + read_one_tag (list, tag_matches[i].gstreamer_tag, v); + } + } + /* restore chars overwritten by '\0' */ + *d = '='; + *ae = '"'; + } else if (*ae == '\0' || *ae == ' ') { + /* end of attr/value pair */ + as = &ae[1]; + } + /* to next char if not eos */ + if (*ae != '\0') + ae++; + } + } else { + /* + Image + + */ + /* FIXME: eventualy rewrite ns */ + + /* skip rdf tags for now */ + if (strncmp (part, "rdf:", 4)) { + i = 0; + while (tag_matches[i].gstreamer_tag) { + if (!strcmp (tag_matches[i].original_tag, part)) + break; + i++; + } + if (tag_matches[i].gstreamer_tag) { + last_tag = tag_matches[i].gstreamer_tag; + } + } + } + } + /* next cycle */ + ne++; + if (ne < xp2) { + if (*ne != '<') + in_tag = FALSE; + ns = ne; + pp = part; + } + } else { + while (ne < xp2 && *ne != '<') { + *pp++ = *ne; + ne++; + } + *pp = '\0'; + /* create node */ + /* {TXT, ns, (ne-ns)-1} */ + if (ns[0] != '\n' && &ns[1] < ne) { + /* only log non-newline nodes, we still have to parse them */ + GST_INFO ("txt: %s", part); + if (last_tag) { + read_one_tag (list, last_tag, part); + } + } + /* next cycle */ + in_tag = TRUE; + ns = ne; + pp = part; + } + } + GST_INFO ("xmp packet parsed, %d entries", + gst_structure_n_fields ((GstStructure *) list)); + + /* free resources */ + i = 0; + while (ns_map[i].original_ns) { + g_free (ns_map[i].gstreamer_ns); + i++; + } + g_free (part); + + return list; + + /* Errors */ +missing_header: + GST_WARNING ("malformed xmp packet header"); + return NULL; +missing_footer: + GST_WARNING ("malformed xmp packet footer"); + return NULL; +broken_xml: + GST_WARNING ("malformed xml tag: %s", part); + return NULL; +} + + +/* formatting */ + +static void +write_one_tag (const GstTagList * list, const gchar * tag, gpointer user_data) +{ + guint i = 0, ct = gst_tag_list_get_tag_size (list, tag); + GType tag_type = gst_tag_get_type (tag); + GString *data = user_data; + const gchar *xmp_tag = NULL, *fmt; + + /* map gst-tag to xmp tag */ + while (tag_matches[i].gstreamer_tag != NULL) { + if (strcmp (tag, tag_matches[i].gstreamer_tag) == 0) { + xmp_tag = tag_matches[i].original_tag; + break; + } + i++; + } + + if (!xmp_tag) { + GST_WARNING ("no mapping for %s to xmp", tag); + return; + } + + /* write single or multi valued field */ + if (ct > 1) { + g_string_append_printf (data, "<%s>", xmp_tag); + fmt = "%s"; + } else { + g_string_append_printf (data, "<%s>", xmp_tag); + fmt = "%s"; + } + for (i = 0; i < ct; i++) { + GST_DEBUG ("mapping %s[%u/%u] to xmp", tag, i, ct); + switch (tag_type) { + case G_TYPE_STRING:{ + gchar *str; + + if (!gst_tag_list_get_string_index (list, tag, i, &str)) + g_return_if_reached (); + g_string_append_printf (data, fmt, str); + g_free (str); + break; + } + default: + if (tag_type == GST_TYPE_DATE) { + GDate *date; + gchar *str; + + if (!gst_tag_list_get_date_index (list, tag, i, &date)) + g_return_if_reached (); + + str = g_strdup_printf ("%04d-%02d-%02d", + (gint) g_date_get_year (date), (gint) g_date_get_month (date), + (gint) g_date_get_day (date)); + g_string_append_printf (data, fmt, str); + g_free (str); + g_date_free (date); + } else { + GST_WARNING ("unhandled type for %s to xmp", tag); + } + break; + } + } + if (ct > 1) { + g_string_append_printf (data, "\n", xmp_tag); + } else { + g_string_append_printf (data, "\n", xmp_tag); + } +} + +/** + * gst_tag_list_to_xmp_buffer: + * @list: tags + * @read_only: does the container forbid inplace editing + * + * Formats a taglist as a xmp packet. + * + * Returns: new buffer or %NULL, unref the buffer when done + * + * Since: 0.10.29 + */ +GstBuffer * +gst_tag_list_to_xmp_buffer (const GstTagList * list, gboolean read_only) +{ + GstBuffer *buffer = NULL; + GString *data = g_string_sized_new (4096); + guint i; + + g_return_val_if_fail (GST_IS_TAG_LIST (list), NULL); + + /* xmp header */ + g_string_append (data, + "\n"); + g_string_append (data, + "\n"); + g_string_append (data, + "\n"); + g_string_append (data, "\n"); + + /* iterate the taglist */ + gst_tag_list_foreach (list, write_one_tag, data); + + /* xmp footer */ + g_string_append (data, "\n"); + g_string_append (data, "\n"); + g_string_append (data, "\n"); + + if (!read_only) { + /* the xmp spec recommand to add 2-4KB padding for in-place editable xmp */ + guint i; + + for (i = 0; i < 32; i++) { + g_string_append (data, " " " " + " " " " "\n"); + } + } + g_string_append_printf (data, "\n", + (read_only ? 'r' : 'w')); + + buffer = gst_buffer_new (); + GST_BUFFER_SIZE (buffer) = data->len + 1; + GST_BUFFER_DATA (buffer) = (guint8 *) g_string_free (data, FALSE); + GST_BUFFER_MALLOCDATA (buffer) = GST_BUFFER_DATA (buffer); + + return buffer; +} diff --git a/gst-libs/gst/tag/tag.h b/gst-libs/gst/tag/tag.h index 0a439c045d..28d5209337 100644 --- a/gst-libs/gst/tag/tag.h +++ b/gst-libs/gst/tag/tag.h @@ -219,6 +219,11 @@ gboolean gst_tag_list_add_id3_image (GstTagList * tag_list, guint image_data_len, guint id3_picture_type); +/* functions to convert GstBuffers with xmp packets contents to GstTagLists and back */ +GstTagList * gst_tag_list_from_xmp_buffer (const GstBuffer * buffer); +GstBuffer * gst_tag_list_to_xmp_buffer (const GstTagList * list, + gboolean read_only); + /* other tag-related functions */ gboolean gst_tag_parse_extended_comment (const gchar * ext_comment, diff --git a/tests/check/libs/tag.c b/tests/check/libs/tag.c index 292df92903..bfe738dc94 100644 --- a/tests/check/libs/tag.c +++ b/tests/check/libs/tag.c @@ -749,6 +749,110 @@ GST_START_TEST (test_language_utils) GST_END_TEST; +GST_START_TEST (test_xmp_formatting) +{ + GstTagList *list; + GstBuffer *buf; + const gchar *text; + guint len; + + /* test data */ + list = gst_tag_list_new_full (GST_TAG_TITLE, "test title", + GST_TAG_DESCRIPTION, "test decription", + GST_TAG_KEYWORDS, "keyword1", GST_TAG_KEYWORDS, "keyword2", NULL); + + buf = gst_tag_list_to_xmp_buffer (list, FALSE); + fail_unless (buf != NULL); + + text = (const gchar *) GST_BUFFER_DATA (buf); + len = GST_BUFFER_SIZE (buf); + + /* check the content */ + fail_unless (g_strrstr_len (text, len, "test title<") != NULL); + fail_unless (g_strrstr_len (text, len, ">test decription<") != NULL); + fail_unless (g_strrstr_len (text, len, ">keyword1<") != NULL); + fail_unless (g_strrstr_len (text, len, ">keyword2<") != NULL); + fail_unless (g_strrstr_len (text, len, "" + "" + ""; + const gchar *xmp_footer = + "" "" "\n"; + struct + { + const gchar *xmp_data; + gint result_size; + gint result_test; + } test_data[] = { + { + "", -1, -1}, { + "", 0, -1}, { + "", 0, -1}, { + "", 0, -1}, { + "test", + 1, 0}, { + "", + 1, 0}, { + NULL, -1, -1} + }; + + /* test data */ + buf = gst_buffer_new (); + + i = 0; + while (test_data[i].xmp_data) { + GST_DEBUG ("trying test-data %u", i); + + text = g_strconcat (xmp_header, test_data[i].xmp_data, xmp_footer, NULL); + GST_BUFFER_DATA (buf) = (guint8 *) text; + GST_BUFFER_SIZE (buf) = strlen (text) + 1; + + + list = gst_tag_list_from_xmp_buffer (buf); + if (test_data[i].result_size >= 0) { + fail_unless (list != NULL); + + result_size = gst_structure_n_fields ((GstStructure *) list); + fail_unless (result_size == test_data[i].result_size); + + /* check the taglist content */ + switch (test_data[i].result_test) { + case 0: + ASSERT_TAG_LIST_HAS_STRING (list, "description", "test"); + break; + default: + break; + } + } + if (list) + gst_tag_list_free (list); + + g_free (text); + i++; + } + + gst_buffer_unref (buf); +} + +GST_END_TEST; + + static Suite * tag_suite (void) { @@ -762,6 +866,8 @@ tag_suite (void) tcase_add_test (tc_chain, test_id3_tags); tcase_add_test (tc_chain, test_id3v1_utf8_tag); tcase_add_test (tc_chain, test_language_utils); + tcase_add_test (tc_chain, test_xmp_formatting); + tcase_add_test (tc_chain, test_xmp_parsing); return s; } diff --git a/win32/common/libgsttag.def b/win32/common/libgsttag.def index cbdc44990b..62dc12d032 100644 --- a/win32/common/libgsttag.def +++ b/win32/common/libgsttag.def @@ -16,8 +16,10 @@ EXPORTS gst_tag_image_type_get_type gst_tag_list_add_id3_image gst_tag_list_from_vorbiscomment_buffer + gst_tag_list_from_xmp_buffer gst_tag_list_new_from_id3v1 gst_tag_list_to_vorbiscomment_buffer + gst_tag_list_to_xmp_buffer gst_tag_parse_extended_comment gst_tag_register_musicbrainz_tags gst_tag_to_id3_tag