| /* GStreamer |
| * Copyright (C) 2010 Stefan Kost <stefan.kost@nokia.com> |
| * Copyright (C) 2010 Thiago Santos <thiago.sousa.santos@collabora.co.uk> |
| * |
| * 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., 51 Franklin St, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| /** |
| * SECTION:gsttagxmp |
| * @title: GstXmptag |
| * @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 #GstTagList<!-- -->s. |
| * |
| * Please note that the xmp parser is very lightweight and not strict at all. |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include "config.h" |
| #endif |
| #include "tag.h" |
| #include <gst/gsttagsetter.h> |
| #include "gsttageditingprivate.h" |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <time.h> |
| #include <ctype.h> |
| |
| #define GST_CAT_DEFAULT gst_tag_ensure_debug_category() |
| |
| static GstDebugCategory * |
| gst_tag_ensure_debug_category (void) |
| { |
| static gsize cat_gonce = 0; |
| |
| if (g_once_init_enter (&cat_gonce)) { |
| GstDebugCategory *cat = NULL; |
| |
| GST_DEBUG_CATEGORY_INIT (cat, "xmp-tags", 0, "XMP GstTag helper functions"); |
| |
| g_once_init_leave (&cat_gonce, (gsize) cat); |
| } |
| |
| return (GstDebugCategory *) cat_gonce; |
| } |
| |
| static const gchar *schema_list[] = { |
| "dc", |
| "xap", |
| "tiff", |
| "exif", |
| "photoshop", |
| "Iptc4xmpCore", |
| "Iptc4xmpExt", |
| NULL |
| }; |
| |
| /** |
| * gst_tag_xmp_list_schemas: |
| * |
| * Gets the list of supported schemas in the xmp lib |
| * |
| * Returns: (transfer none): a %NULL terminated array of strings with the |
| * schema names |
| */ |
| const gchar ** |
| gst_tag_xmp_list_schemas (void) |
| { |
| return schema_list; |
| } |
| |
| typedef struct _XmpSerializationData XmpSerializationData; |
| typedef struct _XmpTag XmpTag; |
| |
| /* |
| * Serializes a GValue into a string. |
| */ |
| typedef gchar *(*XmpSerializationFunc) (const GValue * value); |
| |
| /* |
| * Deserializes @str that is the gstreamer tag @gst_tag represented in |
| * XMP as the @xmp_tag_value and adds the result to the @taglist. |
| * |
| * @pending_tags is passed so that compound xmp tags can search for its |
| * complements on the list and use them. Note that used complements should |
| * be freed and removed from the list. |
| * The list is of PendingXmpTag |
| */ |
| typedef void (*XmpDeserializationFunc) (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * xmp_tag_value, |
| const gchar * str, GSList ** pending_tags); |
| |
| struct _XmpSerializationData |
| { |
| GString *data; |
| const gchar **schemas; |
| }; |
| |
| static gboolean |
| xmp_serialization_data_use_schema (XmpSerializationData * serdata, |
| const gchar * schemaname) |
| { |
| gint i = 0; |
| if (serdata->schemas == NULL) |
| return TRUE; |
| |
| while (serdata->schemas[i] != NULL) { |
| if (strcmp (serdata->schemas[i], schemaname) == 0) |
| return TRUE; |
| i++; |
| } |
| return FALSE; |
| } |
| |
| typedef enum |
| { |
| GstXmpTagTypeNone = 0, |
| GstXmpTagTypeSimple, |
| GstXmpTagTypeBag, |
| GstXmpTagTypeSeq, |
| GstXmpTagTypeStruct, |
| |
| /* Not really a xmp type, this is a tag that in gst is represented with |
| * a single value and on xmp it needs 2 (or more) simple values |
| * |
| * e.g. GST_TAG_GEO_LOCATION_ELEVATION needs to be mapped into 2 complementary |
| * tags in the exif's schema. One of them stores the absolute elevation, |
| * and the other one stores if it is above of below sea level. |
| */ |
| GstXmpTagTypeCompound |
| } GstXmpTagType; |
| |
| struct _XmpTag |
| { |
| const gchar *gst_tag; |
| const gchar *tag_name; |
| GstXmpTagType type; |
| |
| /* some tags must be inside a Bag even |
| * if they are a single entry. Set it here so we know */ |
| GstXmpTagType supertype; |
| |
| /* For tags that need a rdf:parseType attribute */ |
| const gchar *parse_type; |
| |
| /* Used for struct and compound types */ |
| GSList *children; |
| |
| XmpSerializationFunc serialize; |
| XmpDeserializationFunc deserialize; |
| }; |
| |
| static GstTagMergeMode |
| xmp_tag_get_merge_mode (XmpTag * xmptag) |
| { |
| switch (xmptag->type) { |
| case GstXmpTagTypeBag: |
| case GstXmpTagTypeSeq: |
| return GST_TAG_MERGE_APPEND; |
| case GstXmpTagTypeSimple: |
| default: |
| return GST_TAG_MERGE_KEEP; |
| } |
| } |
| |
| static const gchar * |
| xmp_tag_type_get_name (GstXmpTagType tagtype) |
| { |
| switch (tagtype) { |
| case GstXmpTagTypeSeq: |
| return "rdf:Seq"; |
| case GstXmpTagTypeBag: |
| return "rdf:Bag"; |
| default: |
| break; |
| } |
| |
| /* Make compiler happy */ |
| g_return_val_if_reached (""); |
| } |
| |
| struct _PendingXmpTag |
| { |
| XmpTag *xmp_tag; |
| gchar *str; |
| }; |
| typedef struct _PendingXmpTag PendingXmpTag; |
| |
| /* |
| * A schema is a mapping of strings (the tag name in gstreamer) to a list of |
| * tags in xmp (XmpTag). |
| */ |
| typedef GHashTable GstXmpSchema; |
| #define gst_xmp_schema_lookup g_hash_table_lookup |
| #define gst_xmp_schema_insert g_hash_table_insert |
| static GstXmpSchema * |
| gst_xmp_schema_new () |
| { |
| return g_hash_table_new (g_direct_hash, g_direct_equal); |
| } |
| |
| /* |
| * Mappings from schema names into the schema group of tags (GstXmpSchema) |
| */ |
| static GHashTable *__xmp_schemas; |
| |
| static GstXmpSchema * |
| _gst_xmp_get_schema (const gchar * name) |
| { |
| GQuark key; |
| GstXmpSchema *schema; |
| |
| key = g_quark_from_string (name); |
| |
| schema = g_hash_table_lookup (__xmp_schemas, GUINT_TO_POINTER (key)); |
| if (!schema) { |
| GST_WARNING ("Schema %s doesn't exist", name); |
| } |
| return schema; |
| } |
| |
| static void |
| _gst_xmp_add_schema (const gchar * name, GstXmpSchema * schema) |
| { |
| GQuark key; |
| |
| key = g_quark_from_string (name); |
| |
| if (g_hash_table_lookup (__xmp_schemas, GUINT_TO_POINTER (key))) { |
| GST_WARNING ("Schema %s already exists, ignoring", name); |
| g_assert_not_reached (); |
| return; |
| } |
| |
| g_hash_table_insert (__xmp_schemas, GUINT_TO_POINTER (key), schema); |
| } |
| |
| static void |
| _gst_xmp_schema_add_mapping (GstXmpSchema * schema, XmpTag * tag) |
| { |
| GQuark key; |
| |
| key = g_quark_from_string (tag->gst_tag); |
| |
| if (gst_xmp_schema_lookup (schema, GUINT_TO_POINTER (key))) { |
| GST_WARNING ("Tag %s already present for the schema", tag->gst_tag); |
| g_assert_not_reached (); |
| return; |
| } |
| gst_xmp_schema_insert (schema, GUINT_TO_POINTER (key), tag); |
| } |
| |
| static XmpTag * |
| gst_xmp_tag_create (const gchar * gst_tag, const gchar * xmp_tag, |
| gint xmp_type, XmpSerializationFunc serialization_func, |
| XmpDeserializationFunc deserialization_func) |
| { |
| XmpTag *xmpinfo; |
| |
| xmpinfo = g_slice_new (XmpTag); |
| xmpinfo->gst_tag = gst_tag; |
| xmpinfo->tag_name = xmp_tag; |
| xmpinfo->type = xmp_type; |
| xmpinfo->supertype = GstXmpTagTypeNone; |
| xmpinfo->parse_type = NULL; |
| xmpinfo->serialize = serialization_func; |
| xmpinfo->deserialize = deserialization_func; |
| xmpinfo->children = NULL; |
| |
| return xmpinfo; |
| } |
| |
| static XmpTag * |
| gst_xmp_tag_create_compound (const gchar * gst_tag, const gchar * xmp_tag_a, |
| const gchar * xmp_tag_b, XmpSerializationFunc serialization_func_a, |
| XmpSerializationFunc serialization_func_b, |
| XmpDeserializationFunc deserialization_func) |
| { |
| XmpTag *xmptag; |
| XmpTag *xmptag_a = |
| gst_xmp_tag_create (gst_tag, xmp_tag_a, GstXmpTagTypeSimple, |
| serialization_func_a, deserialization_func); |
| XmpTag *xmptag_b = |
| gst_xmp_tag_create (gst_tag, xmp_tag_b, GstXmpTagTypeSimple, |
| serialization_func_b, deserialization_func); |
| |
| xmptag = |
| gst_xmp_tag_create (gst_tag, NULL, GstXmpTagTypeCompound, NULL, NULL); |
| |
| xmptag->children = g_slist_prepend (xmptag->children, xmptag_b); |
| xmptag->children = g_slist_prepend (xmptag->children, xmptag_a); |
| |
| return xmptag; |
| } |
| |
| static void |
| _gst_xmp_schema_add_simple_mapping (GstXmpSchema * schema, |
| const gchar * gst_tag, const gchar * xmp_tag, gint xmp_type, |
| XmpSerializationFunc serialization_func, |
| XmpDeserializationFunc deserialization_func) |
| { |
| _gst_xmp_schema_add_mapping (schema, |
| gst_xmp_tag_create (gst_tag, xmp_tag, xmp_type, serialization_func, |
| deserialization_func)); |
| } |
| |
| /* |
| * We do not return a copy here because elements are |
| * appended, and the API is not public, so we shouldn't |
| * have our lists modified during usage |
| */ |
| #if 0 |
| static GPtrArray * |
| _xmp_tag_get_mapping (const gchar * gst_tag, XmpSerializationData * serdata) |
| { |
| GPtrArray *ret = NULL; |
| GHashTableIter iter; |
| GQuark key = g_quark_from_string (gst_tag); |
| gpointer iterkey, value; |
| const gchar *schemaname; |
| |
| g_hash_table_iter_init (&iter, __xmp_schemas); |
| while (!ret && g_hash_table_iter_next (&iter, &iterkey, &value)) { |
| GstXmpSchema *schema = (GstXmpSchema *) value; |
| |
| schemaname = g_quark_to_string (GPOINTER_TO_UINT (iterkey)); |
| if (xmp_serialization_data_use_schema (serdata, schemaname)) |
| ret = |
| (GPtrArray *) gst_xmp_schema_lookup (schema, GUINT_TO_POINTER (key)); |
| } |
| return ret; |
| } |
| #endif |
| |
| /* finds the gst tag that maps to this xmp tag in this schema */ |
| static const gchar * |
| _gst_xmp_schema_get_mapping_reverse (GstXmpSchema * schema, |
| const gchar * xmp_tag, XmpTag ** _xmp_tag) |
| { |
| GHashTableIter iter; |
| gpointer key, value; |
| const gchar *ret = NULL; |
| |
| /* Iterate over the hashtable */ |
| g_hash_table_iter_init (&iter, schema); |
| while (!ret && g_hash_table_iter_next (&iter, &key, &value)) { |
| XmpTag *xmpinfo = (XmpTag *) value; |
| |
| if (xmpinfo->tag_name) { |
| if (strcmp (xmpinfo->tag_name, xmp_tag) == 0) { |
| *_xmp_tag = xmpinfo; |
| ret = g_quark_to_string (GPOINTER_TO_UINT (key)); |
| goto out; |
| } |
| } else if (xmpinfo->children) { |
| GSList *iter; |
| for (iter = xmpinfo->children; iter; iter = g_slist_next (iter)) { |
| XmpTag *child = iter->data; |
| if (strcmp (child->tag_name, xmp_tag) == 0) { |
| *_xmp_tag = child; |
| ret = g_quark_to_string (GPOINTER_TO_UINT (key)); |
| goto out; |
| } |
| } |
| } else { |
| g_assert_not_reached (); |
| } |
| } |
| |
| out: |
| return ret; |
| } |
| |
| /* finds the gst tag that maps to this xmp tag (searches on all schemas) */ |
| static const gchar * |
| _gst_xmp_tag_get_mapping_reverse (const gchar * xmp_tag, XmpTag ** _xmp_tag) |
| { |
| GHashTableIter iter; |
| gpointer key, value; |
| const gchar *ret = NULL; |
| |
| /* Iterate over the hashtable */ |
| g_hash_table_iter_init (&iter, __xmp_schemas); |
| while (!ret && g_hash_table_iter_next (&iter, &key, &value)) { |
| ret = _gst_xmp_schema_get_mapping_reverse ((GstXmpSchema *) value, xmp_tag, |
| _xmp_tag); |
| } |
| return ret; |
| } |
| |
| /* utility functions/macros */ |
| |
| #define METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR (3.6) |
| #define KILOMETERS_PER_HOUR_TO_METERS_PER_SECOND (1/3.6) |
| #define MILES_PER_HOUR_TO_METERS_PER_SECOND (0.44704) |
| #define KNOTS_TO_METERS_PER_SECOND (0.514444) |
| |
| static gchar * |
| double_to_fraction_string (gdouble num) |
| { |
| gint frac_n; |
| gint frac_d; |
| |
| gst_util_double_to_fraction (num, &frac_n, &frac_d); |
| return g_strdup_printf ("%d/%d", frac_n, frac_d); |
| } |
| |
| /* (de)serialize functions */ |
| static gchar * |
| serialize_exif_gps_coordinate (const GValue * value, gchar pos, gchar neg) |
| { |
| gdouble num; |
| gchar c; |
| gint integer; |
| gchar fraction[G_ASCII_DTOSTR_BUF_SIZE]; |
| |
| g_return_val_if_fail (G_VALUE_TYPE (value) == G_TYPE_DOUBLE, NULL); |
| |
| num = g_value_get_double (value); |
| if (num < 0) { |
| c = neg; |
| num *= -1; |
| } else { |
| c = pos; |
| } |
| integer = (gint) num; |
| |
| g_ascii_dtostr (fraction, sizeof (fraction), (num - integer) * 60); |
| |
| /* FIXME review GPSCoordinate serialization spec for the .mm or ,ss |
| * decision. Couldn't understand it clearly */ |
| return g_strdup_printf ("%d,%s%c", integer, fraction, c); |
| } |
| |
| static gchar * |
| serialize_exif_latitude (const GValue * value) |
| { |
| return serialize_exif_gps_coordinate (value, 'N', 'S'); |
| } |
| |
| static gchar * |
| serialize_exif_longitude (const GValue * value) |
| { |
| return serialize_exif_gps_coordinate (value, 'E', 'W'); |
| } |
| |
| static void |
| deserialize_exif_gps_coordinate (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * str, gchar pos, gchar neg) |
| { |
| gdouble value = 0; |
| gint d = 0, m = 0, s = 0; |
| gdouble m2 = 0; |
| gchar c = 0; |
| const gchar *current; |
| |
| /* get the degrees */ |
| if (sscanf (str, "%d", &d) != 1) |
| goto error; |
| |
| /* find the beginning of the minutes */ |
| current = strchr (str, ','); |
| if (current == NULL) |
| goto end; |
| current += 1; |
| |
| /* check if it uses ,SS or .mm */ |
| if (strchr (current, ',') != NULL) { |
| if (!sscanf (current, "%d,%d%c", &m, &s, &c)) |
| goto error; |
| } else { |
| gchar *copy = g_strdup (current); |
| gint len = strlen (copy); |
| gint i; |
| |
| /* check the last letter */ |
| for (i = len - 1; len >= 0; len--) { |
| if (g_ascii_isspace (copy[i])) |
| continue; |
| |
| if (g_ascii_isalpha (copy[i])) { |
| /* found it */ |
| c = copy[i]; |
| copy[i] = '\0'; |
| break; |
| |
| } else { |
| /* something is wrong */ |
| g_free (copy); |
| goto error; |
| } |
| } |
| |
| /* use a copy so we can change the last letter as E can cause |
| * problems here */ |
| m2 = g_ascii_strtod (copy, NULL); |
| g_free (copy); |
| } |
| |
| end: |
| /* we can add them all as those that aren't parsed are 0 */ |
| value = d + (m / 60.0) + (s / (60.0 * 60.0)) + (m2 / 60.0); |
| |
| if (c == pos) { |
| //NOP |
| } else if (c == neg) { |
| value *= -1; |
| } else { |
| goto error; |
| } |
| |
| gst_tag_list_add (taglist, xmp_tag_get_merge_mode (xmptag), gst_tag, value, |
| NULL); |
| return; |
| |
| error: |
| GST_WARNING ("Failed to deserialize gps coordinate: %s", str); |
| } |
| |
| static void |
| deserialize_exif_latitude (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * xmp_tag, const gchar * str, |
| GSList ** pending_tags) |
| { |
| deserialize_exif_gps_coordinate (xmptag, taglist, gst_tag, str, 'N', 'S'); |
| } |
| |
| static void |
| deserialize_exif_longitude (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * xmp_tag, const gchar * str, |
| GSList ** pending_tags) |
| { |
| deserialize_exif_gps_coordinate (xmptag, taglist, gst_tag, str, 'E', 'W'); |
| } |
| |
| static gchar * |
| serialize_exif_altitude (const GValue * value) |
| { |
| gdouble num; |
| |
| num = g_value_get_double (value); |
| |
| if (num < 0) |
| num *= -1; |
| |
| return double_to_fraction_string (num); |
| } |
| |
| static gchar * |
| serialize_exif_altituderef (const GValue * value) |
| { |
| gdouble num; |
| |
| num = g_value_get_double (value); |
| |
| /* 0 means above sea level, 1 means below */ |
| if (num >= 0) |
| return g_strdup ("0"); |
| return g_strdup ("1"); |
| } |
| |
| static void |
| deserialize_exif_altitude (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * xmp_tag, const gchar * str, |
| GSList ** pending_tags) |
| { |
| const gchar *altitude_str = NULL; |
| const gchar *altituderef_str = NULL; |
| gint frac_n; |
| gint frac_d; |
| gdouble value; |
| |
| GSList *entry; |
| PendingXmpTag *ptag = NULL; |
| |
| /* find the other missing part */ |
| if (strcmp (xmp_tag, "exif:GPSAltitude") == 0) { |
| altitude_str = str; |
| |
| for (entry = *pending_tags; entry; entry = g_slist_next (entry)) { |
| ptag = (PendingXmpTag *) entry->data; |
| |
| if (strcmp (ptag->xmp_tag->tag_name, "exif:GPSAltitudeRef") == 0) { |
| altituderef_str = ptag->str; |
| break; |
| } |
| } |
| |
| } else if (strcmp (xmp_tag, "exif:GPSAltitudeRef") == 0) { |
| altituderef_str = str; |
| |
| for (entry = *pending_tags; entry; entry = g_slist_next (entry)) { |
| ptag = (PendingXmpTag *) entry->data; |
| |
| if (strcmp (ptag->xmp_tag->tag_name, "exif:GPSAltitude") == 0) { |
| altitude_str = ptag->str; |
| break; |
| } |
| } |
| |
| } else { |
| GST_WARNING ("Unexpected xmp tag %s", xmp_tag); |
| return; |
| } |
| |
| if (!altitude_str) { |
| GST_WARNING ("Missing exif:GPSAltitude tag"); |
| return; |
| } |
| if (!altituderef_str) { |
| GST_WARNING ("Missing exif:GPSAltitudeRef tag"); |
| return; |
| } |
| |
| if (sscanf (altitude_str, "%d/%d", &frac_n, &frac_d) != 2) { |
| GST_WARNING ("Failed to parse fraction: %s", altitude_str); |
| return; |
| } |
| |
| gst_util_fraction_to_double (frac_n, frac_d, &value); |
| |
| if (altituderef_str[0] == '0') { |
| /* nop */ |
| } else if (altituderef_str[0] == '1') { |
| value *= -1; |
| } else { |
| GST_WARNING ("Unexpected exif:AltitudeRef value: %s", altituderef_str); |
| return; |
| } |
| |
| /* add to the taglist */ |
| gst_tag_list_add (taglist, xmp_tag_get_merge_mode (xmptag), |
| GST_TAG_GEO_LOCATION_ELEVATION, value, NULL); |
| |
| /* clean up entry */ |
| g_free (ptag->str); |
| g_slice_free (PendingXmpTag, ptag); |
| *pending_tags = g_slist_delete_link (*pending_tags, entry); |
| } |
| |
| static gchar * |
| serialize_exif_gps_speed (const GValue * value) |
| { |
| return double_to_fraction_string (g_value_get_double (value) * |
| METERS_PER_SECOND_TO_KILOMETERS_PER_HOUR); |
| } |
| |
| static gchar * |
| serialize_exif_gps_speedref (const GValue * value) |
| { |
| /* we always use km/h */ |
| return g_strdup ("K"); |
| } |
| |
| static void |
| deserialize_exif_gps_speed (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * xmp_tag, const gchar * str, |
| GSList ** pending_tags) |
| { |
| const gchar *speed_str = NULL; |
| const gchar *speedref_str = NULL; |
| gint frac_n; |
| gint frac_d; |
| gdouble value; |
| |
| GSList *entry; |
| PendingXmpTag *ptag = NULL; |
| |
| /* find the other missing part */ |
| if (strcmp (xmp_tag, "exif:GPSSpeed") == 0) { |
| speed_str = str; |
| |
| for (entry = *pending_tags; entry; entry = g_slist_next (entry)) { |
| ptag = (PendingXmpTag *) entry->data; |
| |
| if (strcmp (ptag->xmp_tag->tag_name, "exif:GPSSpeedRef") == 0) { |
| speedref_str = ptag->str; |
| break; |
| } |
| } |
| |
| } else if (strcmp (xmp_tag, "exif:GPSSpeedRef") == 0) { |
| speedref_str = str; |
| |
| for (entry = *pending_tags; entry; entry = g_slist_next (entry)) { |
| ptag = (PendingXmpTag *) entry->data; |
| |
| if (strcmp (ptag->xmp_tag->tag_name, "exif:GPSSpeed") == 0) { |
| speed_str = ptag->str; |
| break; |
| } |
| } |
| |
| } else { |
| GST_WARNING ("Unexpected xmp tag %s", xmp_tag); |
| return; |
| } |
| |
| if (!speed_str) { |
| GST_WARNING ("Missing exif:GPSSpeed tag"); |
| return; |
| } |
| if (!speedref_str) { |
| GST_WARNING ("Missing exif:GPSSpeedRef tag"); |
| return; |
| } |
| |
| if (sscanf (speed_str, "%d/%d", &frac_n, &frac_d) != 2) { |
| GST_WARNING ("Failed to parse fraction: %s", speed_str); |
| return; |
| } |
| |
| gst_util_fraction_to_double (frac_n, frac_d, &value); |
| |
| if (speedref_str[0] == 'K') { |
| value *= KILOMETERS_PER_HOUR_TO_METERS_PER_SECOND; |
| } else if (speedref_str[0] == 'M') { |
| value *= MILES_PER_HOUR_TO_METERS_PER_SECOND; |
| } else if (speedref_str[0] == 'N') { |
| value *= KNOTS_TO_METERS_PER_SECOND; |
| } else { |
| GST_WARNING ("Unexpected exif:SpeedRef value: %s", speedref_str); |
| return; |
| } |
| |
| /* add to the taglist */ |
| gst_tag_list_add (taglist, xmp_tag_get_merge_mode (xmptag), |
| GST_TAG_GEO_LOCATION_MOVEMENT_SPEED, value, NULL); |
| |
| /* clean up entry */ |
| g_free (ptag->str); |
| g_slice_free (PendingXmpTag, ptag); |
| *pending_tags = g_slist_delete_link (*pending_tags, entry); |
| } |
| |
| static gchar * |
| serialize_exif_gps_direction (const GValue * value) |
| { |
| return double_to_fraction_string (g_value_get_double (value)); |
| } |
| |
| static gchar * |
| serialize_exif_gps_directionref (const GValue * value) |
| { |
| /* T for true geographic direction (M would mean magnetic) */ |
| return g_strdup ("T"); |
| } |
| |
| static void |
| deserialize_exif_gps_direction (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * xmp_tag, const gchar * str, |
| GSList ** pending_tags, const gchar * direction_tag, |
| const gchar * directionref_tag) |
| { |
| const gchar *dir_str = NULL; |
| const gchar *dirref_str = NULL; |
| gint frac_n; |
| gint frac_d; |
| gdouble value; |
| |
| GSList *entry; |
| PendingXmpTag *ptag = NULL; |
| |
| /* find the other missing part */ |
| if (strcmp (xmp_tag, direction_tag) == 0) { |
| dir_str = str; |
| |
| for (entry = *pending_tags; entry; entry = g_slist_next (entry)) { |
| ptag = (PendingXmpTag *) entry->data; |
| |
| if (strcmp (ptag->xmp_tag->tag_name, directionref_tag) == 0) { |
| dirref_str = ptag->str; |
| break; |
| } |
| } |
| |
| } else if (strcmp (xmp_tag, directionref_tag) == 0) { |
| dirref_str = str; |
| |
| for (entry = *pending_tags; entry; entry = g_slist_next (entry)) { |
| ptag = (PendingXmpTag *) entry->data; |
| |
| if (strcmp (ptag->xmp_tag->tag_name, direction_tag) == 0) { |
| dir_str = ptag->str; |
| break; |
| } |
| } |
| |
| } else { |
| GST_WARNING ("Unexpected xmp tag %s", xmp_tag); |
| return; |
| } |
| |
| if (!dir_str) { |
| GST_WARNING ("Missing %s tag", dir_str); |
| return; |
| } |
| if (!dirref_str) { |
| GST_WARNING ("Missing %s tag", dirref_str); |
| return; |
| } |
| |
| if (sscanf (dir_str, "%d/%d", &frac_n, &frac_d) != 2) { |
| GST_WARNING ("Failed to parse fraction: %s", dir_str); |
| return; |
| } |
| |
| gst_util_fraction_to_double (frac_n, frac_d, &value); |
| |
| if (dirref_str[0] == 'T') { |
| /* nop */ |
| } else if (dirref_str[0] == 'M') { |
| GST_WARNING ("Magnetic direction tags aren't supported yet"); |
| return; |
| } else { |
| GST_WARNING ("Unexpected %s value: %s", directionref_tag, dirref_str); |
| return; |
| } |
| |
| /* add to the taglist */ |
| gst_tag_list_add (taglist, xmp_tag_get_merge_mode (xmptag), gst_tag, value, |
| NULL); |
| |
| /* clean up entry */ |
| g_free (ptag->str); |
| g_slice_free (PendingXmpTag, ptag); |
| *pending_tags = g_slist_delete_link (*pending_tags, entry); |
| } |
| |
| static void |
| deserialize_exif_gps_track (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * xmp_tag, const gchar * str, |
| GSList ** pending_tags) |
| { |
| deserialize_exif_gps_direction (xmptag, taglist, gst_tag, xmp_tag, str, |
| pending_tags, "exif:GPSTrack", "exif:GPSTrackRef"); |
| } |
| |
| static void |
| deserialize_exif_gps_img_direction (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * xmp_tag, const gchar * str, |
| GSList ** pending_tags) |
| { |
| deserialize_exif_gps_direction (xmptag, taglist, gst_tag, xmp_tag, str, |
| pending_tags, "exif:GPSImgDirection", "exif:GPSImgDirectionRef"); |
| } |
| |
| static void |
| deserialize_xmp_rating (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * xmp_tag, const gchar * str, |
| GSList ** pending_tags) |
| { |
| guint value; |
| |
| if (sscanf (str, "%u", &value) != 1) { |
| GST_WARNING ("Failed to parse xmp:Rating %s", str); |
| return; |
| } |
| |
| if (value > 100) { |
| GST_WARNING ("Unsupported Rating tag %u (should be from 0 to 100), " |
| "ignoring", value); |
| return; |
| } |
| |
| gst_tag_list_add (taglist, xmp_tag_get_merge_mode (xmptag), gst_tag, value, |
| NULL); |
| } |
| |
| static gchar * |
| serialize_tiff_orientation (const GValue * value) |
| { |
| const gchar *str; |
| gint num; |
| |
| str = g_value_get_string (value); |
| if (str == NULL) { |
| GST_WARNING ("Failed to get image orientation tag value"); |
| return NULL; |
| } |
| |
| num = __exif_tag_image_orientation_to_exif_value (str); |
| if (num == -1) |
| return NULL; |
| |
| return g_strdup_printf ("%d", num); |
| } |
| |
| static void |
| deserialize_tiff_orientation (XmpTag * xmptag, GstTagList * taglist, |
| const gchar * gst_tag, const gchar * xmp_tag, const gchar * str, |
| GSList ** pending_tags) |
| { |
| guint value; |
| const gchar *orientation = NULL; |
| |
| if (sscanf (str, "%u", &value) != 1) { |
| GST_WARNING ("Failed to parse tiff:Orientation %s", str); |
| return; |
| } |
| |
| if (value < 1 || value > 8) { |
| GST_WARNING ("Invalid tiff:Orientation tag %u (should be from 1 to 8), " |
| "ignoring", value); |
| return; |
| } |
| |
| orientation = __exif_tag_image_orientation_from_exif_value (value); |
| if (orientation == NULL) |
| return; |
| gst_tag_list_add (taglist, xmp_tag_get_merge_mode (xmptag), gst_tag, |
| orientation, NULL); |
| } |
| |
| |
| /* look at this page for addtional schemas |
| * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/XMP.html |
| */ |
| static gpointer |
| _init_xmp_tag_map (gpointer user_data) |
| { |
| XmpTag *xmpinfo; |
| GstXmpSchema *schema; |
| |
| __xmp_schemas = g_hash_table_new (g_direct_hash, g_direct_equal); |
| |
| /* add the maps */ |
| /* dublic code metadata |
| * http://dublincore.org/documents/dces/ |
| */ |
| schema = gst_xmp_schema_new (); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_ARTIST, |
| "dc:creator", GstXmpTagTypeSeq, NULL, NULL); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_COPYRIGHT, |
| "dc:rights", GstXmpTagTypeSimple, NULL, NULL); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_DATE_TIME, "dc:date", |
| GstXmpTagTypeSeq, NULL, NULL); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_DESCRIPTION, |
| "dc:description", GstXmpTagTypeSimple, NULL, NULL); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_KEYWORDS, |
| "dc:subject", GstXmpTagTypeBag, NULL, NULL); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_TITLE, "dc:title", |
| GstXmpTagTypeSimple, NULL, NULL); |
| /* FIXME: we probably want GST_TAG_{,AUDIO_,VIDEO_}MIME_TYPE */ |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_VIDEO_CODEC, |
| "dc:format", GstXmpTagTypeSimple, NULL, NULL); |
| _gst_xmp_add_schema ("dc", schema); |
| |
| /* xap (xmp) schema */ |
| schema = gst_xmp_schema_new (); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_USER_RATING, |
| "xmp:Rating", GstXmpTagTypeSimple, NULL, deserialize_xmp_rating); |
| _gst_xmp_add_schema ("xap", schema); |
| |
| /* tiff */ |
| schema = gst_xmp_schema_new (); |
| _gst_xmp_schema_add_simple_mapping (schema, |
| GST_TAG_DEVICE_MANUFACTURER, "tiff:Make", GstXmpTagTypeSimple, NULL, |
| NULL); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_DEVICE_MODEL, |
| "tiff:Model", GstXmpTagTypeSimple, NULL, NULL); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_APPLICATION_NAME, |
| "tiff:Software", GstXmpTagTypeSimple, NULL, NULL); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_IMAGE_ORIENTATION, |
| "tiff:Orientation", GstXmpTagTypeSimple, serialize_tiff_orientation, |
| deserialize_tiff_orientation); |
| _gst_xmp_add_schema ("tiff", schema); |
| |
| /* exif schema */ |
| schema = gst_xmp_schema_new (); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_DATE_TIME, |
| "exif:DateTimeOriginal", GstXmpTagTypeSimple, NULL, NULL); |
| _gst_xmp_schema_add_simple_mapping (schema, |
| GST_TAG_GEO_LOCATION_LATITUDE, "exif:GPSLatitude", |
| GstXmpTagTypeSimple, serialize_exif_latitude, deserialize_exif_latitude); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_GEO_LOCATION_LONGITUDE, |
| "exif:GPSLongitude", GstXmpTagTypeSimple, serialize_exif_longitude, |
| deserialize_exif_longitude); |
| _gst_xmp_schema_add_simple_mapping (schema, |
| GST_TAG_CAPTURING_EXPOSURE_COMPENSATION, "exif:ExposureBiasValue", |
| GstXmpTagTypeSimple, NULL, NULL); |
| |
| /* compound exif tags */ |
| xmpinfo = gst_xmp_tag_create_compound (GST_TAG_GEO_LOCATION_ELEVATION, |
| "exif:GPSAltitude", "exif:GPSAltitudeRef", serialize_exif_altitude, |
| serialize_exif_altituderef, deserialize_exif_altitude); |
| _gst_xmp_schema_add_mapping (schema, xmpinfo); |
| |
| xmpinfo = gst_xmp_tag_create_compound (GST_TAG_GEO_LOCATION_MOVEMENT_SPEED, |
| "exif:GPSSpeed", "exif:GPSSpeedRef", serialize_exif_gps_speed, |
| serialize_exif_gps_speedref, deserialize_exif_gps_speed); |
| _gst_xmp_schema_add_mapping (schema, xmpinfo); |
| |
| xmpinfo = |
| gst_xmp_tag_create_compound (GST_TAG_GEO_LOCATION_MOVEMENT_DIRECTION, |
| "exif:GPSTrack", "exif:GPSTrackRef", serialize_exif_gps_direction, |
| serialize_exif_gps_directionref, deserialize_exif_gps_track); |
| _gst_xmp_schema_add_mapping (schema, xmpinfo); |
| |
| xmpinfo = gst_xmp_tag_create_compound (GST_TAG_GEO_LOCATION_CAPTURE_DIRECTION, |
| "exif:GPSImgDirection", "exif:GPSImgDirectionRef", |
| serialize_exif_gps_direction, serialize_exif_gps_directionref, |
| deserialize_exif_gps_img_direction); |
| _gst_xmp_schema_add_mapping (schema, xmpinfo); |
| |
| _gst_xmp_add_schema ("exif", schema); |
| |
| /* photoshop schema */ |
| schema = gst_xmp_schema_new (); |
| _gst_xmp_schema_add_simple_mapping (schema, |
| GST_TAG_GEO_LOCATION_COUNTRY, "photoshop:Country", |
| GstXmpTagTypeSimple, NULL, NULL); |
| _gst_xmp_schema_add_simple_mapping (schema, GST_TAG_GEO_LOCATION_CITY, |
| "photoshop:City", GstXmpTagTypeSimple, NULL, NULL); |
| _gst_xmp_add_schema ("photoshop", schema); |
| |
| /* iptc4xmpcore schema */ |
| schema = gst_xmp_schema_new (); |
| _gst_xmp_schema_add_simple_mapping (schema, |
| GST_TAG_GEO_LOCATION_SUBLOCATION, "Iptc4xmpCore:Location", |
| GstXmpTagTypeSimple, NULL, NULL); |
| _gst_xmp_add_schema ("Iptc4xmpCore", schema); |
| |
| /* iptc4xmpext schema */ |
| schema = gst_xmp_schema_new (); |
| xmpinfo = gst_xmp_tag_create (NULL, "Iptc4xmpExt:LocationShown", |
| GstXmpTagTypeStruct, NULL, NULL); |
| xmpinfo->supertype = GstXmpTagTypeBag; |
| xmpinfo->parse_type = "Resource"; |
| xmpinfo->children = g_slist_prepend (xmpinfo->children, |
| gst_xmp_tag_create (GST_TAG_GEO_LOCATION_SUBLOCATION, |
| "LocationDetails:Sublocation", GstXmpTagTypeSimple, NULL, NULL)); |
| xmpinfo->children = |
| g_slist_prepend (xmpinfo->children, |
| gst_xmp_tag_create (GST_TAG_GEO_LOCATION_CITY, |
| "LocationDetails:City", GstXmpTagTypeSimple, NULL, NULL)); |
| xmpinfo->children = |
| g_slist_prepend (xmpinfo->children, |
| gst_xmp_tag_create (GST_TAG_GEO_LOCATION_COUNTRY, |
| "LocationDetails:Country", GstXmpTagTypeSimple, NULL, NULL)); |
| _gst_xmp_schema_add_mapping (schema, xmpinfo); |
| _gst_xmp_add_schema ("Iptc4xmpExt", schema); |
| |
| return NULL; |
| } |
| |
| static void |
| xmp_tags_initialize () |
| { |
| static GOnce my_once = G_ONCE_INIT; |
| g_once (&my_once, (GThreadFunc) _init_xmp_tag_map, NULL); |
| } |
| |
| typedef struct _GstXmpNamespaceMatch GstXmpNamespaceMatch; |
| struct _GstXmpNamespaceMatch |
| { |
| const gchar *ns_prefix; |
| const gchar *ns_uri; |
| |
| /* |
| * Stores extra namespaces for array tags |
| * The namespaces should be writen in the form: |
| * |
| * xmlns:XpTo="http://some.org/your/ns/name/ (next ones)" |
| */ |
| const gchar *extra_ns; |
| }; |
| |
| static const GstXmpNamespaceMatch ns_match[] = { |
| {"dc", "http://purl.org/dc/elements/1.1/", NULL}, |
| {"exif", "http://ns.adobe.com/exif/1.0/", NULL}, |
| {"tiff", "http://ns.adobe.com/tiff/1.0/", NULL}, |
| {"xap", "http://ns.adobe.com/xap/1.0/", NULL}, |
| {"photoshop", "http://ns.adobe.com/photoshop/1.0/", NULL}, |
| {"Iptc4xmpCore", "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/", NULL}, |
| {"Iptc4xmpExt", "http://iptc.org/std/Iptc4xmpExt/2008-02-29/", |
| "xmlns:LocationDetails=\"http://iptc.org/std/Iptc4xmpExt/2008-02-29/LocationDetails/\""}, |
| {NULL, NULL, NULL} |
| }; |
| |
| typedef struct _GstXmpNamespaceMap GstXmpNamespaceMap; |
| struct _GstXmpNamespaceMap |
| { |
| const gchar *original_ns; |
| gchar *gstreamer_ns; |
| }; |
| |
| /* parsing */ |
| |
| static void |
| read_one_tag (GstTagList * list, XmpTag * xmptag, |
| const gchar * v, GSList ** pending_tags) |
| { |
| GType tag_type; |
| GstTagMergeMode merge_mode; |
| const gchar *tag = xmptag->gst_tag; |
| |
| g_return_if_fail (tag != NULL); |
| |
| if (xmptag->deserialize) { |
| xmptag->deserialize (xmptag, list, tag, xmptag->tag_name, v, pending_tags); |
| return; |
| } |
| |
| merge_mode = xmp_tag_get_merge_mode (xmptag); |
| 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, merge_mode, tag, v, NULL); |
| break; |
| } |
| case G_TYPE_DOUBLE:{ |
| gdouble value = 0; |
| gint frac_n, frac_d; |
| |
| if (sscanf (v, "%d/%d", &frac_n, &frac_d) == 2) { |
| gst_util_fraction_to_double (frac_n, frac_d, &value); |
| gst_tag_list_add (list, merge_mode, tag, value, NULL); |
| } else { |
| GST_WARNING ("Failed to parse fraction: %s", v); |
| } |
| break; |
| } |
| default: |
| if (tag_type == GST_TYPE_DATE_TIME) { |
| GstDateTime *datetime; |
| |
| if (v == NULL || *v == '\0') { |
| GST_WARNING ("Empty string for datetime parsing"); |
| return; |
| } |
| |
| GST_DEBUG ("Parsing %s into a datetime", v); |
| datetime = gst_date_time_new_from_iso8601_string (v); |
| if (datetime) { |
| gst_tag_list_add (list, merge_mode, tag, datetime, NULL); |
| gst_date_time_unref (datetime); |
| } |
| |
| } else if (tag_type == G_TYPE_DATE) { |
| GST_ERROR ("Use GST_TYPE_DATE_TIME in tags instead of G_TYPE_DATE"); |
| } else { |
| GST_WARNING ("unhandled type for %s from xmp", tag); |
| } |
| break; |
| } |
| } |
| |
| /** |
| * gst_tag_list_from_xmp_buffer: |
| * @buffer: buffer |
| * |
| * Parse a xmp packet into a taglist. |
| * |
| * Returns: new taglist or %NULL, free the list when done |
| */ |
| GstTagList * |
| gst_tag_list_from_xmp_buffer (GstBuffer * buffer) |
| { |
| GstTagList *list = NULL; |
| GstMapInfo info; |
| gchar *xps, *xp1, *xp2, *xpe, *ns, *ne; |
| gsize len, max_ft_len; |
| gboolean in_tag; |
| gchar *part = NULL, *pp; |
| guint i; |
| XmpTag *last_xmp_tag = NULL; |
| GSList *pending_tags = NULL; |
| |
| /* Used for strucuture xmp tags */ |
| XmpTag *context_tag = NULL; |
| |
| GstXmpNamespaceMap ns_map[] = { |
| {"dc", NULL} |
| , |
| {"exif", NULL} |
| , |
| {"tiff", NULL} |
| , |
| {"xap", NULL} |
| , |
| {"photoshop", NULL} |
| , |
| {"Iptc4xmpCore", NULL} |
| , |
| {"Iptc4xmpExt", NULL} |
| , |
| {NULL, NULL} |
| }; |
| |
| xmp_tags_initialize (); |
| |
| g_return_val_if_fail (GST_IS_BUFFER (buffer), NULL); |
| |
| GST_LOG ("Starting xmp parsing"); |
| |
| gst_buffer_map (buffer, &info, GST_MAP_READ); |
| xps = (gchar *) info.data; |
| len = info.size; |
| g_return_val_if_fail (len > 0, NULL); |
| |
| xpe = &xps[len + 1]; |
| |
| /* check header and footer */ |
| xp1 = g_strstr_len (xps, len, "<?xpacket begin"); |
| if (!xp1) |
| goto missing_header; |
| xp1 = &xp1[strlen ("<?xpacket begin")]; |
| while (*xp1 != '>' && *xp1 != '<' && xp1 < xpe) |
| xp1++; |
| if (*xp1 != '>') |
| goto missing_header; |
| |
| /* Use 2 here to count for an extra trailing \n that was added |
| * in old versions, this makes it able to parse xmp packets with |
| * and without this trailing char */ |
| max_ft_len = 2 + strlen ("<?xpacket end=\".\"?>"); |
| if (len < max_ft_len) |
| goto missing_footer; |
| |
| xp2 = g_strstr_len (&xps[len - max_ft_len], max_ft_len, "<?xpacket "); |
| if (!xp2) |
| goto missing_footer; |
| |
| GST_INFO ("xmp header okay"); |
| |
| /* skip > and text until first xml-node */ |
| xp1++; |
| while (*xp1 != '<' && xp1 < xpe) |
| xp1++; |
| |
| /* no tag can be longer than the whole buffer */ |
| part = g_malloc (xp2 - xp1); |
| list = gst_tag_list_new_empty (); |
| |
| /* 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])) { |
| g_free (ns_map[i].gstreamer_ns); |
| ns_map[i].gstreamer_ns = g_strdup (&as[6]); |
| } |
| } |
| } else { |
| XmpTag *xmp_tag = NULL; |
| /* FIXME: eventually 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 |
| */ |
| if (context_tag) { |
| GSList *iter; |
| |
| for (iter = context_tag->children; iter; |
| iter = g_slist_next (iter)) { |
| XmpTag *child = iter->data; |
| |
| GST_DEBUG ("Looking at child tag %s : %s", child->tag_name, |
| as); |
| if (strcmp (child->tag_name, as) == 0) { |
| xmp_tag = child; |
| break; |
| } |
| } |
| |
| } else { |
| GST_LOG ("Looking for tag: %s", as); |
| _gst_xmp_tag_get_mapping_reverse (as, &xmp_tag); |
| } |
| if (xmp_tag) { |
| PendingXmpTag *ptag; |
| |
| GST_DEBUG ("Found xmp tag: %s -> %s", xmp_tag->tag_name, |
| xmp_tag->gst_tag); |
| |
| /* we shouldn't find a xmp structure here */ |
| g_assert (xmp_tag->gst_tag != NULL); |
| |
| ptag = g_slice_new (PendingXmpTag); |
| ptag->xmp_tag = xmp_tag; |
| ptag->str = g_strdup (v); |
| |
| pending_tags = g_slist_prepend (pending_tags, ptag); |
| } |
| } |
| /* 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 { |
| /* |
| <dc:type><rdf:Bag><rdf:li>Image</rdf:li></rdf:Bag></dc:type> |
| <dc:creator><rdf:Seq><rdf:li/></rdf:Seq></dc:creator> |
| */ |
| /* FIXME: eventually rewrite ns */ |
| |
| /* skip rdf tags for now */ |
| if (strncmp (part, "rdf:", 4)) { |
| /* if we're inside some struct, we look only on its children */ |
| if (context_tag) { |
| GSList *iter; |
| |
| /* check if this is the closing of the context */ |
| if (part[0] == '/' |
| && strcmp (part + 1, context_tag->tag_name) == 0) { |
| GST_DEBUG ("Closing context tag %s", part); |
| context_tag = NULL; |
| } else { |
| |
| for (iter = context_tag->children; iter; |
| iter = g_slist_next (iter)) { |
| XmpTag *child = iter->data; |
| |
| GST_DEBUG ("Looking at child tag %s : %s", child->tag_name, |
| part); |
| if (strcmp (child->tag_name, part) == 0) { |
| last_xmp_tag = child; |
| break; |
| } |
| } |
| } |
| |
| } else { |
| GST_LOG ("Looking for tag: %s", part); |
| _gst_xmp_tag_get_mapping_reverse (part, &last_xmp_tag); |
| if (last_xmp_tag && last_xmp_tag->type == GstXmpTagTypeStruct) { |
| context_tag = last_xmp_tag; |
| last_xmp_tag = NULL; |
| } |
| } |
| } |
| } |
| } |
| GST_LOG ("Next cycle"); |
| /* 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_xmp_tag) { |
| PendingXmpTag *ptag; |
| |
| GST_DEBUG ("Found tag %s -> %s", last_xmp_tag->tag_name, |
| last_xmp_tag->gst_tag); |
| |
| if (last_xmp_tag->type == GstXmpTagTypeStruct) { |
| g_assert (context_tag == NULL); /* we can't handle struct nesting currently */ |
| |
| context_tag = last_xmp_tag; |
| } else { |
| ptag = g_slice_new (PendingXmpTag); |
| ptag->xmp_tag = last_xmp_tag; |
| ptag->str = g_strdup (part); |
| |
| pending_tags = g_slist_prepend (pending_tags, ptag); |
| } |
| } |
| } |
| /* next cycle */ |
| in_tag = TRUE; |
| ns = ne; |
| pp = part; |
| } |
| } |
| |
| pending_tags = g_slist_reverse (pending_tags); |
| |
| GST_DEBUG ("Done accumulating tags, now handling them"); |
| |
| while (pending_tags) { |
| PendingXmpTag *ptag = (PendingXmpTag *) pending_tags->data; |
| |
| pending_tags = g_slist_delete_link (pending_tags, pending_tags); |
| |
| read_one_tag (list, ptag->xmp_tag, ptag->str, &pending_tags); |
| |
| g_free (ptag->str); |
| g_slice_free (PendingXmpTag, ptag); |
| } |
| |
| GST_INFO ("xmp packet parsed, %d entries", gst_tag_list_n_tags (list)); |
| |
| out: |
| |
| /* free resources */ |
| i = 0; |
| while (ns_map[i].original_ns) { |
| g_free (ns_map[i].gstreamer_ns); |
| i++; |
| } |
| |
| g_free (part); |
| |
| gst_buffer_unmap (buffer, &info); |
| |
| return list; |
| |
| /* Errors */ |
| missing_header: |
| GST_WARNING ("malformed xmp packet header"); |
| goto out; |
| missing_footer: |
| GST_WARNING ("malformed xmp packet footer"); |
| goto out; |
| broken_xml: |
| GST_WARNING ("malformed xml tag: %s", part); |
| gst_tag_list_unref (list); |
| list = NULL; |
| goto out; |
| } |
| |
| |
| /* formatting */ |
| |
| static void |
| string_open_tag (GString * string, const char *tag) |
| { |
| g_string_append_c (string, '<'); |
| g_string_append (string, tag); |
| g_string_append_c (string, '>'); |
| } |
| |
| static void |
| string_close_tag (GString * string, const char *tag) |
| { |
| g_string_append (string, "</"); |
| g_string_append (string, tag); |
| g_string_append (string, ">\n"); |
| } |
| |
| static char * |
| gst_value_serialize_xmp (const GValue * value) |
| { |
| switch (G_VALUE_TYPE (value)) { |
| case G_TYPE_STRING: |
| return g_markup_escape_text (g_value_get_string (value), -1); |
| case G_TYPE_INT: |
| return g_strdup_printf ("%d", g_value_get_int (value)); |
| case G_TYPE_UINT: |
| return g_strdup_printf ("%u", g_value_get_uint (value)); |
| case G_TYPE_DOUBLE: |
| return double_to_fraction_string (g_value_get_double (value)); |
| default: |
| break; |
| } |
| /* put non-switchable types here */ |
| if (G_VALUE_TYPE (value) == G_TYPE_DATE) { |
| const GDate *date = g_value_get_boxed (value); |
| |
| return g_strdup_printf ("%04d-%02d-%02d", |
| (gint) g_date_get_year (date), (gint) g_date_get_month (date), |
| (gint) g_date_get_day (date)); |
| } else if (G_VALUE_TYPE (value) == GST_TYPE_DATE_TIME) { |
| gint year, month, day, hour, min, sec, microsec; |
| gfloat gmt_offset = 0; |
| gint gmt_offset_hour, gmt_offset_min; |
| GstDateTime *datetime = (GstDateTime *) g_value_get_boxed (value); |
| |
| if (!gst_date_time_has_time (datetime)) |
| return gst_date_time_to_iso8601_string (datetime); |
| |
| /* can't just use gst_date_time_to_iso8601_string() here because we need |
| * the timezone info with a colon, i.e. as +03:00 instead of +0300 */ |
| year = gst_date_time_get_year (datetime); |
| month = gst_date_time_get_month (datetime); |
| day = gst_date_time_get_day (datetime); |
| hour = gst_date_time_get_hour (datetime); |
| min = gst_date_time_get_minute (datetime); |
| sec = gst_date_time_get_second (datetime); |
| microsec = gst_date_time_get_microsecond (datetime); |
| gmt_offset = gst_date_time_get_time_zone_offset (datetime); |
| if (gmt_offset == 0) { |
| /* UTC */ |
| return g_strdup_printf ("%04d-%02d-%02dT%02d:%02d:%02d.%06dZ", |
| year, month, day, hour, min, sec, microsec); |
| } else { |
| gmt_offset_hour = ABS (gmt_offset); |
| gmt_offset_min = (ABS (gmt_offset) - gmt_offset_hour) * 60; |
| |
| return g_strdup_printf ("%04d-%02d-%02dT%02d:%02d:%02d.%06d%c%02d:%02d", |
| year, month, day, hour, min, sec, microsec, |
| gmt_offset >= 0 ? '+' : '-', gmt_offset_hour, gmt_offset_min); |
| } |
| } else { |
| return NULL; |
| } |
| } |
| |
| static void |
| write_one_tag (const GstTagList * list, XmpTag * xmp_tag, gpointer user_data) |
| { |
| guint i = 0, ct; |
| XmpSerializationData *serialization_data = user_data; |
| GString *data = serialization_data->data; |
| char *s; |
| |
| /* struct type handled differently */ |
| if (xmp_tag->type == GstXmpTagTypeStruct || |
| xmp_tag->type == GstXmpTagTypeCompound) { |
| GSList *iter; |
| gboolean use_it = FALSE; |
| |
| /* check if any of the inner tags are present on the taglist */ |
| for (iter = xmp_tag->children; iter && !use_it; iter = g_slist_next (iter)) { |
| XmpTag *child_tag = iter->data; |
| |
| if (gst_tag_list_get_value_index (list, child_tag->gst_tag, 0) != NULL) { |
| use_it = TRUE; |
| break; |
| } |
| } |
| |
| if (use_it) { |
| if (xmp_tag->tag_name) |
| string_open_tag (data, xmp_tag->tag_name); |
| |
| if (xmp_tag->supertype) { |
| string_open_tag (data, xmp_tag_type_get_name (xmp_tag->supertype)); |
| if (xmp_tag->parse_type) { |
| g_string_append (data, "<rdf:li rdf:parseType=\""); |
| g_string_append (data, xmp_tag->parse_type); |
| g_string_append_c (data, '"'); |
| g_string_append_c (data, '>'); |
| } else { |
| string_open_tag (data, "rdf:li"); |
| } |
| } |
| |
| /* now write it */ |
| for (iter = xmp_tag->children; iter; iter = g_slist_next (iter)) { |
| write_one_tag (list, iter->data, user_data); |
| } |
| |
| if (xmp_tag->supertype) { |
| string_close_tag (data, "rdf:li"); |
| string_close_tag (data, xmp_tag_type_get_name (xmp_tag->supertype)); |
| } |
| |
| if (xmp_tag->tag_name) |
| string_close_tag (data, xmp_tag->tag_name); |
| } |
| return; |
| } |
| |
| /* at this point we must have a gst_tag */ |
| g_assert (xmp_tag->gst_tag); |
| if (gst_tag_list_get_value_index (list, xmp_tag->gst_tag, 0) == NULL) |
| return; |
| |
| ct = gst_tag_list_get_tag_size (list, xmp_tag->gst_tag); |
| string_open_tag (data, xmp_tag->tag_name); |
| |
| /* fast path for single valued tag */ |
| if (ct == 1 || xmp_tag->type == GstXmpTagTypeSimple) { |
| if (xmp_tag->serialize) { |
| s = xmp_tag->serialize (gst_tag_list_get_value_index (list, |
| xmp_tag->gst_tag, 0)); |
| } else { |
| s = gst_value_serialize_xmp (gst_tag_list_get_value_index (list, |
| xmp_tag->gst_tag, 0)); |
| } |
| if (s) { |
| g_string_append (data, s); |
| g_free (s); |
| } else { |
| GST_WARNING ("unhandled type for %s to xmp", xmp_tag->gst_tag); |
| } |
| } else { |
| const gchar *typename; |
| |
| typename = xmp_tag_type_get_name (xmp_tag->type); |
| |
| string_open_tag (data, typename); |
| for (i = 0; i < ct; i++) { |
| GST_DEBUG ("mapping %s[%u/%u] to xmp", xmp_tag->gst_tag, i, ct); |
| if (xmp_tag->serialize) { |
| s = xmp_tag->serialize (gst_tag_list_get_value_index (list, |
| xmp_tag->gst_tag, i)); |
| } else { |
| s = gst_value_serialize_xmp (gst_tag_list_get_value_index (list, |
| xmp_tag->gst_tag, i)); |
| } |
| if (s) { |
| string_open_tag (data, "rdf:li"); |
| g_string_append (data, s); |
| string_close_tag (data, "rdf:li"); |
| g_free (s); |
| } else { |
| GST_WARNING ("unhandled type for %s to xmp", xmp_tag->gst_tag); |
| } |
| } |
| string_close_tag (data, typename); |
| } |
| |
| string_close_tag (data, xmp_tag->tag_name); |
| } |
| |
| /** |
| * gst_tag_list_to_xmp_buffer: |
| * @list: tags |
| * @read_only: does the container forbid inplace editing |
| * @schemas: (array zero-terminated): |
| * %NULL terminated array of schemas to be used on serialization |
| * |
| * Formats a taglist as a xmp packet using only the selected |
| * schemas. An empty list (%NULL) means that all schemas should |
| * be used |
| * |
| * Returns: new buffer or %NULL, unref the buffer when done |
| */ |
| GstBuffer * |
| gst_tag_list_to_xmp_buffer (const GstTagList * list, gboolean read_only, |
| const gchar ** schemas) |
| { |
| GstBuffer *buffer = NULL; |
| XmpSerializationData serialization_data; |
| GString *data; |
| guint i; |
| gsize bsize; |
| gpointer bdata; |
| |
| serialization_data.data = g_string_sized_new (4096); |
| serialization_data.schemas = schemas; |
| data = serialization_data.data; |
| |
| xmp_tags_initialize (); |
| |
| g_return_val_if_fail (GST_IS_TAG_LIST (list), NULL); |
| |
| /* xmp header */ |
| g_string_append (data, |
| "<?xpacket begin=\"\xEF\xBB\xBF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n"); |
| g_string_append (data, |
| "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"GStreamer\">\n"); |
| g_string_append (data, |
| "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\""); |
| i = 0; |
| while (ns_match[i].ns_prefix) { |
| if (xmp_serialization_data_use_schema (&serialization_data, |
| ns_match[i].ns_prefix)) { |
| g_string_append_printf (data, " xmlns:%s=\"%s\"", |
| ns_match[i].ns_prefix, ns_match[i].ns_uri); |
| if (ns_match[i].extra_ns) { |
| g_string_append_printf (data, " %s", ns_match[i].extra_ns); |
| } |
| } |
| i++; |
| } |
| g_string_append (data, ">\n"); |
| g_string_append (data, "<rdf:Description rdf:about=\"\">\n"); |
| |
| /* iterate the schemas */ |
| if (schemas == NULL) { |
| /* use all schemas */ |
| schemas = gst_tag_xmp_list_schemas (); |
| } |
| for (i = 0; schemas[i] != NULL; i++) { |
| GstXmpSchema *schema = _gst_xmp_get_schema (schemas[i]); |
| GHashTableIter iter; |
| gpointer key, value; |
| |
| if (schema == NULL) |
| continue; |
| |
| /* Iterate over the hashtable */ |
| g_hash_table_iter_init (&iter, schema); |
| while (g_hash_table_iter_next (&iter, &key, &value)) { |
| write_one_tag (list, value, (gpointer) & serialization_data); |
| } |
| } |
| |
| /* xmp footer */ |
| g_string_append (data, "</rdf:Description>\n"); |
| g_string_append (data, "</rdf:RDF>\n"); |
| g_string_append (data, "</x:xmpmeta>\n"); |
| |
| if (!read_only) { |
| /* the xmp spec recommends 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, "<?xpacket end=\"%c\"?>", |
| (read_only ? 'r' : 'w')); |
| |
| bsize = data->len; |
| bdata = g_string_free (data, FALSE); |
| |
| buffer = gst_buffer_new_wrapped (bdata, bsize); |
| |
| return buffer; |
| } |
| |
| #undef gst_xmp_schema_lookup |
| #undef gst_xmp_schema_insert |