From 0d5803562703e215ed8b9df047fbbb75f852e3a3 Mon Sep 17 00:00:00 2001 From: Raphael Quinet Date: Fri, 4 Mar 2005 23:01:48 +0000 Subject: [PATCH] Added placeholder for file properties. 2005-03-04 Raphael Quinet * menus/image-menu.xml.in: Added placeholder for file properties. * plug-ins/metadata/.cvsignore * plug-ins/metadata/Makefile.am * plug-ins/metadata/README * plug-ins/metadata/interface.c * plug-ins/metadata/interface.h * plug-ins/metadata/metadata.c * plug-ins/metadata/xmp-model.c * plug-ins/metadata/xmp-model.h * plug-ins/metadata/xmp-parse.c * plug-ins/metadata/xmp-parse.h * plug-ins/metadata/xmp-gen.c * plug-ins/metadata/xmp-gen.h: First import of metadata editor. Currently, it cannot read metadata (except for XMP), it cannot edit metadata and it cannot save metadata (just export). But this might improve later... The README file contains some info. --- ChangeLog | 20 + menus/image-menu.xml.in | 2 + plug-ins/metadata/.cvsignore | 6 + plug-ins/metadata/Makefile.am | 56 ++ plug-ins/metadata/README | 76 +++ plug-ins/metadata/interface.c | 605 ++++++++++++++++++ plug-ins/metadata/interface.h | 34 + plug-ins/metadata/metadata.c | 481 ++++++++++++++ plug-ins/metadata/xmp-gen.c | 331 ++++++++++ plug-ins/metadata/xmp-gen.h | 36 ++ plug-ins/metadata/xmp-model.c | 1120 ++++++++++++++++++++++++++++++++ plug-ins/metadata/xmp-model.h | 128 ++++ plug-ins/metadata/xmp-parse.c | 1138 +++++++++++++++++++++++++++++++++ plug-ins/metadata/xmp-parse.h | 130 ++++ 14 files changed, 4163 insertions(+) create mode 100644 plug-ins/metadata/.cvsignore create mode 100644 plug-ins/metadata/Makefile.am create mode 100644 plug-ins/metadata/README create mode 100644 plug-ins/metadata/interface.c create mode 100644 plug-ins/metadata/interface.h create mode 100644 plug-ins/metadata/metadata.c create mode 100644 plug-ins/metadata/xmp-gen.c create mode 100644 plug-ins/metadata/xmp-gen.h create mode 100644 plug-ins/metadata/xmp-model.c create mode 100644 plug-ins/metadata/xmp-model.h create mode 100644 plug-ins/metadata/xmp-parse.c create mode 100644 plug-ins/metadata/xmp-parse.h diff --git a/ChangeLog b/ChangeLog index 2a01c64dd2..98be30e079 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,23 @@ +2005-03-04 Raphaël Quinet + + * menus/image-menu.xml.in: Added placeholder for file properties. + + * plug-ins/metadata/.cvsignore + * plug-ins/metadata/Makefile.am + * plug-ins/metadata/README + * plug-ins/metadata/interface.c + * plug-ins/metadata/interface.h + * plug-ins/metadata/metadata.c + * plug-ins/metadata/xmp-model.c + * plug-ins/metadata/xmp-model.h + * plug-ins/metadata/xmp-parse.c + * plug-ins/metadata/xmp-parse.h + * plug-ins/metadata/xmp-gen.c + * plug-ins/metadata/xmp-gen.h: First import of metadata editor. + Currently, it cannot read metadata (except for XMP), it cannot + edit metadata and it cannot save metadata (just export). But this + might improve later... The README file contains some info. + 2005-03-04 Sven Neumann * app/dialogs/user-install-dialog.c diff --git a/menus/image-menu.xml.in b/menus/image-menu.xml.in index e1c7fcbe10..dea5600dd1 100644 --- a/menus/image-menu.xml.in +++ b/menus/image-menu.xml.in @@ -30,6 +30,8 @@ + + diff --git a/plug-ins/metadata/.cvsignore b/plug-ins/metadata/.cvsignore new file mode 100644 index 0000000000..5a79fffc50 --- /dev/null +++ b/plug-ins/metadata/.cvsignore @@ -0,0 +1,6 @@ +Makefile +Makefile.in +.deps +.libs +metadata +xmpdump diff --git a/plug-ins/metadata/Makefile.am b/plug-ins/metadata/Makefile.am new file mode 100644 index 0000000000..96167372a2 --- /dev/null +++ b/plug-ins/metadata/Makefile.am @@ -0,0 +1,56 @@ +## Process this file with automake to produce Makefile.in + +libgimpui = $(top_builddir)/libgimp/libgimpui-$(GIMP_API_VERSION).la +libgimpwidgets = $(top_builddir)/libgimpwidgets/libgimpwidgets-$(GIMP_API_VERSION).la +libgimp = $(top_builddir)/libgimp/libgimp-$(GIMP_API_VERSION).la +libgimpcolor = $(top_builddir)/libgimpcolor/libgimpcolor-$(GIMP_API_VERSION).la +libgimpbase = $(top_builddir)/libgimpbase/libgimpbase-$(GIMP_API_VERSION).la + +if OS_WIN32 +mwindows = -mwindows +endif + +AM_LDFLAGS = $(mwindows) + +libexecdir = $(gimpplugindir)/plug-ins + +libexec_PROGRAMS = metadata + +metadata_SOURCES = \ + metadata.c \ + interface.h \ + interface.c \ + xmp-model.h \ + xmp-model.c \ + xmp-parse.h \ + xmp-parse.c \ + xmp-gen.h \ + xmp-gen.c +# exif-parse.h \ +# exif-parse.c \ +# exif-gen.h \ +# exif-gen.c \ +# iptc-parse.h \ +# iptc-parse.c + +noinst_PROGRAMS = xmpdump +xmpdump_SOURCES = \ + xmpdump.c \ + xmp-parse.h \ + xmp-parse.c + +INCLUDES = \ + -I$(top_srcdir) \ + $(GTK_CFLAGS) \ + $(EXIF_CFLAGS) \ + -I$(includedir) + +LDADD = \ + $(libgimpui) \ + $(libgimpwidgets) \ + $(libgimp) \ + $(libgimpcolor) \ + $(libgimpbase) \ + $(GTK_LIBS) \ + $(RT_LIBS) \ + $(INTLLIBS) diff --git a/plug-ins/metadata/README b/plug-ins/metadata/README new file mode 100644 index 0000000000..36a7e8a082 --- /dev/null +++ b/plug-ins/metadata/README @@ -0,0 +1,76 @@ +What is this? +------------- + +This is the beginning of an editor for metadata. It is far from +complete yet. + +Current status +-------------- +- It does not read metadata, except if there is an XMP block in the + image file. +- It does not write metadata, it can only export XMP into a separate + file. +- It does not edit metadata, except for some very limited editing of + basic scalar types (strings, integers, ...). It is currently not + possible to add new properties using the editor (but this can be + done via the PDB call plug_in_metadata_set_simple()). +- Basically, the current code is only useful for viewing some XMP + metadata if the image file contains an XMP block. The import/export + functions are also supposed to work. Import merges, so it is + possible for example to import a license file such as the XMP files + from Creative Commons in order to apply a license to an image. +- The metadata is saved in a persistent parasite that is overwritten + after each call. Caution: the editor should not be open while you + are trying to test some of the PDB calls, otherwise you would + discard all changes as soon as you press OK. This is a problem that + cannot be solved easily as long as the metadata editor is a plug-in. + +How to find test images? +------------------------ + +The easiest way is to try a Google search for "/File/Info") + plug-ins/Makefile.am (added "metadata" in SUBDIRS) + plug-ins/common/jpeg.c (changed to get/set metadata through PDB) +New files: + plug-ins/metadata/Makefile.am (generates Makefile.in to get a Makefile) + plug-ins/metadata/metadata.c (main part: registers the plug-in, etc.) + plug-ins/metadata/interface.h + plug-ins/metadata/interface.c (user interface: widgets and callbacks) + plug-ins/metadata/xmp-model.h + plug-ins/metadata/xmp-model.c (model for the treeview, list of XMP schemas) + plug-ins/metadata/xmp-parse.h + plug-ins/metadata/xmp-parse.c (simple parser for XMP metadata) + plug-ins/metadata/xmp-gen.h + plug-ins/metadata/xmp-gen.c (generates XMP metadata from the tree model) + plug-ins/metadata/exif-parse.h + plug-ins/metadata/exif-parse.c (converts EXIF into the XMP tree model) + plug-ins/metadata/exif-gen.h + plug-ins/metadata/exif-gen.c (generates EXIF metadata from the tree model) diff --git a/plug-ins/metadata/interface.c b/plug-ins/metadata/interface.c new file mode 100644 index 0000000000..98555485b3 --- /dev/null +++ b/plug-ins/metadata/interface.c @@ -0,0 +1,605 @@ +/* interface.c - user interface for the metadata editor + * + * Copyright (C) 2004-2005, Raphaël Quinet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include +#ifdef HAVE_UNISTD_H +#include +#endif + +#include +#include + +#ifdef G_OS_WIN32 +#include +#endif + +#ifndef _O_BINARY +#define _O_BINARY 0 +#endif + +#include +#include +#include + +#include "libgimp/stdplugins-intl.h" + +#include "interface.h" +#include "xmp-model.h" +#include "xmp-gen.h" + +#define RESPONSE_IMPORT 1 +#define RESPONSE_EXPORT 2 + +typedef struct +{ + GtkWidget *dlg; + XMPModel *xmp_model; + GdkPixbuf *edit_icon; + GdkPixbuf *auto_icon; + gboolean run_ok; +} MetadataGui; + +static void +value_edited (GtkCellRendererText *cell, + const gchar *path_string, + const gchar *new_text, + gpointer data) +{ + GtkTreeModel *model = (GtkTreeModel *)data; + GtkTreePath *path = gtk_tree_path_new_from_string (path_string); + GtkTreeIter iter; + gchar *old_text; + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, COL_XMP_VALUE, &old_text, -1); + g_free (old_text); + + /* FIXME: update value[] array */ + /* FIXME: check widget xref and update other widget if not NULL */ + gtk_tree_store_set (GTK_TREE_STORE (model), &iter, + COL_XMP_VALUE, new_text, + -1); + + gtk_tree_path_free (path); +} + +static void +add_view_columns (GtkTreeView *treeview) +{ + gint col_offset; + GtkCellRenderer *renderer; + GtkTreeViewColumn *column; + GtkTreeModel *model = gtk_tree_view_get_model (treeview); + + /* Property Name */ + renderer = gtk_cell_renderer_text_new (); + g_object_set (renderer, "xalign", 0.0, NULL); + col_offset = + gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (treeview), + -1, _("Property"), + renderer, + "text", + COL_XMP_NAME, + "weight", + COL_XMP_WEIGHT, + "weight-set", + COL_XMP_WEIGHT_SET, + NULL); + column = gtk_tree_view_get_column (GTK_TREE_VIEW (treeview), col_offset - 1); + gtk_tree_view_column_set_clickable (GTK_TREE_VIEW_COLUMN (column), TRUE); + + /* Icon */ + renderer = gtk_cell_renderer_pixbuf_new (); + col_offset = + gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (treeview), + -1, "", + renderer, + "pixbuf", + COL_XMP_EDIT_ICON, + "visible", + COL_XMP_VISIBLE, + NULL); + column = gtk_tree_view_get_column (GTK_TREE_VIEW (treeview), col_offset - 1); + gtk_tree_view_column_set_clickable (GTK_TREE_VIEW_COLUMN (column), TRUE); + + /* Value */ + renderer = gtk_cell_renderer_text_new (); + g_object_set (renderer, "xalign", 0.0, NULL); + + g_signal_connect (renderer, "edited", + G_CALLBACK (value_edited), model); + col_offset = + gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (treeview), + -1, _("Value"), + renderer, + "text", + COL_XMP_VALUE, + "editable", + COL_XMP_EDITABLE, + "visible", + COL_XMP_VISIBLE, + NULL); + column = gtk_tree_view_get_column (GTK_TREE_VIEW (treeview), col_offset - 1); + gtk_tree_view_column_set_clickable (GTK_TREE_VIEW_COLUMN (column), TRUE); +} + +static gboolean +icon_foreach_func (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + gboolean editable; + MetadataGui *mgui = user_data; + + gtk_tree_model_get (model, iter, + COL_XMP_EDITABLE, &editable, + -1); + if (editable == XMP_AUTO_UPDATE) + gtk_tree_store_set (GTK_TREE_STORE (model), iter, + COL_XMP_EDIT_ICON, mgui->auto_icon, + -1); + else if (editable == TRUE) + gtk_tree_store_set (GTK_TREE_STORE (model), iter, + COL_XMP_EDIT_ICON, mgui->edit_icon, + -1); + else + gtk_tree_store_set (GTK_TREE_STORE (model), iter, + COL_XMP_EDIT_ICON, NULL, + -1); + return FALSE; +} + +static void +update_icons (MetadataGui *mgui) +{ + GtkTreeModel *model; + + /* add the edit icon to the rows that are editable */ + model = xmp_model_get_tree_model (mgui->xmp_model); + gtk_tree_model_foreach (model, icon_foreach_func, mgui); +} + +static void +add_description_tab (GtkWidget *notebook) +{ + GtkWidget *frame; + GtkWidget *table; + GtkWidget *entry; + GtkWidget *scrolled_window; + GtkWidget *text_view; + GtkTextBuffer *text_buffer; + + frame = gimp_frame_new (_("Description")); + gtk_notebook_append_page (GTK_NOTEBOOK (notebook), frame, + gtk_label_new (_("Description"))); + gtk_container_set_border_width (GTK_CONTAINER (frame), 10); + /* gtk_widget_show (frame); */ + + table = gtk_table_new (5, 2, FALSE); + gtk_table_set_col_spacings (GTK_TABLE (table), 6); + gtk_table_set_row_spacings (GTK_TABLE (table), 6); + gtk_container_add (GTK_CONTAINER (frame), table); + /* gtk_widget_show (table); */ + + entry = gtk_entry_new (); + gimp_table_attach_aligned (GTK_TABLE (table), 0, 0, + _("Image _Title:"), 0.0, 0.5, + entry, 1, FALSE); + + entry = gtk_entry_new (); + gimp_table_attach_aligned (GTK_TABLE (table), 0, 1, + _("_Author:"), 0.0, 0.5, + entry, 1, FALSE); + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_SHADOW_IN); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + text_view = gtk_text_view_new (); + text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view)); + gtk_text_buffer_set_text (text_buffer, "FIXME:\nThese widgets are currently disconnected from the XMP model.\nPlease use the Advanced tab.", -1); /*FIXME*/ + gtk_container_add (GTK_CONTAINER (scrolled_window), text_view); + gimp_table_attach_aligned (GTK_TABLE (table), 0, 2, + _("_Description:"), 0.0, 0.5, + scrolled_window, 1, FALSE); + + entry = gtk_entry_new (); + gimp_table_attach_aligned (GTK_TABLE (table), 0, 3, + _("Description _Writer:"), 0.0, 0.5, + entry, 1, FALSE); + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_SHADOW_IN); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + text_view = gtk_text_view_new (); + text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view)); + gtk_container_add (GTK_CONTAINER (scrolled_window), text_view); + gimp_table_attach_aligned (GTK_TABLE (table), 0, 4, + _("_Keywords:"), 0.0, 0.5, + scrolled_window, 1, FALSE); + + gtk_widget_show_all (frame); +} + +static void +add_copyright_tab (GtkWidget *notebook) +{ + GtkWidget *label; + + label = gtk_label_new (_("Empty")); + gtk_notebook_append_page (GTK_NOTEBOOK (notebook), label, + gtk_label_new (_("Copyright"))); + gtk_widget_show (label); +} + +static void +add_origin_tab (GtkWidget *notebook) +{ + GtkWidget *label; + + label = gtk_label_new (_("Empty")); + gtk_notebook_append_page (GTK_NOTEBOOK (notebook), label, + gtk_label_new (_("Origin"))); + gtk_widget_show (label); +} + +static void +add_camera1_tab (GtkWidget *notebook) +{ + GtkWidget *label; + + label = gtk_label_new (_("Empty")); + gtk_notebook_append_page (GTK_NOTEBOOK (notebook), label, + gtk_label_new (_("Camera 1"))); + gtk_widget_show (label); +} + +static void +add_camera2_tab (GtkWidget *notebook) +{ + GtkWidget *label; + + label = gtk_label_new (_("Empty")); + gtk_notebook_append_page (GTK_NOTEBOOK (notebook), label, + gtk_label_new (_("Camera 2"))); + gtk_widget_show (label); +} + +static void +add_advanced_tab (GtkWidget *notebook, + GtkTreeModel *model) +{ + GtkWidget *sw; + GtkWidget *treeview; + + /* Advanced tab */ + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (sw), + GTK_SHADOW_ETCHED_IN); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + gtk_notebook_append_page (GTK_NOTEBOOK (notebook), sw, + gtk_label_new (_("Advanced"))); + + /* create tree view - model will be unref'ed in xmp_model_free() */ + treeview = gtk_tree_view_new_with_model (model); + gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (treeview), TRUE); + + add_view_columns (GTK_TREE_VIEW (treeview)); + + gtk_container_add (GTK_CONTAINER (sw), treeview); + + /* expand all rows after the treeview widget has been realized */ + g_signal_connect (treeview, "realize", + G_CALLBACK (gtk_tree_view_expand_all), NULL); + gtk_widget_show (treeview); + gtk_widget_show (sw); +} + +/* show a transient message dialog */ +static void +metadata_message_dialog (GtkMessageType type, + GtkWindow *parent, + const gchar *title, + const gchar *message) +{ + GtkWidget *dlg; + + dlg = gtk_message_dialog_new (parent, 0, type, GTK_BUTTONS_OK, message); + + if (title) + gtk_window_set_title (GTK_WINDOW (dlg), title); + + gtk_window_set_role (GTK_WINDOW (dlg), "metadata-message"); + gtk_dialog_run (GTK_DIALOG (dlg)); + gtk_widget_destroy (dlg); +} + +/* load XMP metadata from a file (the file may contain other data) */ +static void +import_dialog_response (GtkWidget *dlg, + gint response_id, + gpointer data) +{ + MetadataGui *mgui = data; + + g_return_if_fail (mgui != NULL); + if (response_id == GTK_RESPONSE_OK) + { + gchar *filename; + gchar *buffer; + guint buffer_length; + GError *error = NULL; + + filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dlg)); + + if (! g_file_get_contents (filename, &buffer, &buffer_length, &error)) + { + metadata_message_dialog (GTK_MESSAGE_ERROR, GTK_WINDOW (dlg), + _("Open failed"), error->message); + g_error_free (error); + g_free (filename); + return; + } + + if (! xmp_model_parse_buffer (mgui->xmp_model, buffer, buffer_length, + TRUE, &error)) + { + metadata_message_dialog (GTK_MESSAGE_ERROR, GTK_WINDOW (dlg), + _("Open failed"), error->message); + g_error_free (error); + g_free (filename); + return; + } + + update_icons (mgui); + + g_free (buffer); + g_free (filename); + } + + gtk_widget_destroy (dlg); /* FIXME: destroy or unmap? */ +} + +/* select file to import */ +static void +file_import_dialog (GtkWidget *parent, + MetadataGui *mgui) +{ + static GtkWidget *dlg = NULL; + + if (! dlg) + { + dlg = + gtk_file_chooser_dialog_new (_("Import XMP from file"), + GTK_WINDOW (parent), + GTK_FILE_CHOOSER_ACTION_OPEN, + + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OPEN, GTK_RESPONSE_OK, + + NULL); + + /* FIXME: gimp_help_connect? */ + gtk_dialog_set_default_response (GTK_DIALOG (dlg), GTK_RESPONSE_OK); + + g_signal_connect (dlg, "destroy", + G_CALLBACK (gtk_widget_destroyed), + &dlg); + g_signal_connect (dlg, "response", + G_CALLBACK (import_dialog_response), + mgui); + } + gtk_window_present (GTK_WINDOW (dlg)); +} + +/* save XMP metadata to a file (only XMP, nothing else) */ +static void +export_dialog_response (GtkWidget *dlg, + gint response_id, + gpointer data) +{ + MetadataGui *mgui = data; + + g_return_if_fail (mgui != NULL); + if (response_id == GTK_RESPONSE_OK) + { + gchar *filename; + gchar *buffer; + guint buffer_length; + int fd; + + filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dlg)); + + /* FIXME: improve this code and rewrite the error handling */ + buffer_length = xmp_estimate_size (mgui->xmp_model); + buffer = g_new (gchar, buffer_length); + xmp_generate_block (mgui->xmp_model, buffer, buffer_length); + fd = g_open (filename, O_CREAT | O_TRUNC | O_WRONLY | _O_BINARY, 0666); + if (fd < 0) + { + metadata_message_dialog (GTK_MESSAGE_ERROR, GTK_WINDOW (dlg), + _("Open failed"), + _("Cannot create file")); + g_free (buffer); + g_free (filename); + return; + } + fprintf (stderr, "\nwriting %d bytes to %s...\n", strlen(buffer), filename); + if (write (fd, buffer, strlen (buffer)) < 0) + { + metadata_message_dialog (GTK_MESSAGE_ERROR, GTK_WINDOW (dlg), + _("Save failed"), + _("Some error occured while saving")); + g_free (buffer); + g_free (filename); + return; + } + if (close (fd) < 0) + { + metadata_message_dialog (GTK_MESSAGE_ERROR, GTK_WINDOW (dlg), + _("Save failed"), + _("Could not close the file")); + g_free (buffer); + g_free (filename); + return; + } + g_free (buffer); + g_free (filename); + } + + gtk_widget_destroy (dlg); /* FIXME: destroy or unmap? */ +} + +/* select file to export */ +static void +file_export_dialog (GtkWidget *parent, + MetadataGui *mgui) +{ + static GtkWidget *dlg = NULL; + + if (! dlg) + { + dlg = + gtk_file_chooser_dialog_new (_("Export XMP to file"), + GTK_WINDOW (parent), + GTK_FILE_CHOOSER_ACTION_SAVE, + + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OPEN, GTK_RESPONSE_OK, + + NULL); + + /* FIXME: gimp_help_connect? */ + gtk_dialog_set_default_response (GTK_DIALOG (dlg), GTK_RESPONSE_OK); + + g_signal_connect (dlg, "destroy", + G_CALLBACK (gtk_widget_destroyed), + &dlg); + g_signal_connect (dlg, "response", + G_CALLBACK (export_dialog_response), + mgui); + } + gtk_window_present (GTK_WINDOW (dlg)); +} + +static void +metadata_dialog_response (GtkWidget *widget, + gint response_id, + gpointer data) +{ + MetadataGui *mgui = data; + + g_return_if_fail (mgui != NULL); + switch (response_id) + { + case RESPONSE_IMPORT: + file_import_dialog (widget, mgui); + break; + + case RESPONSE_EXPORT: + file_export_dialog (widget, mgui); + break; + + case GTK_RESPONSE_OK: + mgui->run_ok = TRUE; + /*fallthrough*/ + + default: + gtk_widget_destroy (widget); + break; + } +} + +gboolean +metadata_dialog (gint32 image_ID, + XMPModel *xmp_model) +{ + MetadataGui mgui; + GtkWidget *notebook; + + gimp_ui_init ("metadata", FALSE); + mgui.dlg = gimp_dialog_new (_("Image Properties"), "metadata", + NULL, 0, + gimp_standard_help_func, "plug-in-metadata", + + _("_Import XMP"), RESPONSE_IMPORT, + _("_Export XMP"), RESPONSE_EXPORT, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_OK, + + NULL); + + g_signal_connect (mgui.dlg, "response", + G_CALLBACK (metadata_dialog_response), + &mgui); + g_signal_connect (mgui.dlg, "destroy", + G_CALLBACK (gtk_main_quit), + NULL); + + notebook = gtk_notebook_new (); + gtk_notebook_set_tab_pos (GTK_NOTEBOOK (notebook), GTK_POS_TOP); + gtk_box_pack_start (GTK_BOX (GTK_DIALOG (mgui.dlg)->vbox), notebook, + TRUE, TRUE, 0); + gtk_widget_show (notebook); + + mgui.xmp_model = xmp_model; + mgui.edit_icon = gtk_widget_render_icon (mgui.dlg, GIMP_STOCK_EDIT, + GTK_ICON_SIZE_MENU, NULL); + mgui.auto_icon = gtk_widget_render_icon (mgui.dlg, GIMP_STOCK_WILBER, + GTK_ICON_SIZE_MENU, NULL); + update_icons (&mgui); + mgui.run_ok = FALSE; + + /* add the tabs to the notebook */ + add_description_tab (notebook); + add_copyright_tab (notebook); + add_origin_tab (notebook); + add_camera1_tab (notebook); + add_camera2_tab (notebook); + add_advanced_tab (notebook, xmp_model_get_tree_model (mgui.xmp_model)); + + gtk_window_set_default_size (GTK_WINDOW (mgui.dlg), 400, 500); + gtk_widget_show (mgui.dlg); + + /* run, baby, run! */ + gtk_main (); + + /* clean up and return */ + g_object_unref (mgui.auto_icon); + g_object_unref (mgui.edit_icon); + return mgui.run_ok; +} diff --git a/plug-ins/metadata/interface.h b/plug-ins/metadata/interface.h new file mode 100644 index 0000000000..6fbc61275c --- /dev/null +++ b/plug-ins/metadata/interface.h @@ -0,0 +1,34 @@ +/* interface.h - user interface for the metadata editor + * + * Copyright (C) 2004-2005, Raphaël Quinet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + */ + +#ifndef INTERFACE_H +#define INTERFACE_H + +#include +#include "xmp-model.h" + +G_BEGIN_DECLS + +gboolean metadata_dialog (gint32 image_ID, + XMPModel *xmp_model); + +G_END_DECLS + +#endif /* INTERFACE_H */ diff --git a/plug-ins/metadata/metadata.c b/plug-ins/metadata/metadata.c new file mode 100644 index 0000000000..293e0722f2 --- /dev/null +++ b/plug-ins/metadata/metadata.c @@ -0,0 +1,481 @@ +/* metadata.c - main() for the metadata editor + * + * Copyright (C) 2004-2005, Raphaël Quinet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + */ + +#include "config.h" + +#include +#include + +#include +#include +#include + +#include "libgimp/stdplugins-intl.h" + +#include "interface.h" +#include "xmp-gen.h" +/* FIXME: uncomment when these are working +#include "exif-parse.h" +#include "exif-gen.h" +#include "iptc-parse.h" +*/ + +#define METADATA_PARASITE "gimp-metadata" +#define METADATA_MARKER "GIMP_XMP_1" +#define METADATA_MARKER_LEN (sizeof (METADATA_MARKER) - 1) + +#define HELP_ID "plug-in-metadata" + + +/* prototypes of local functions */ +static void query (void); +static void run (const gchar *name, + gint nparams, + const GimpParam *param, + gint *nreturn_vals, + GimpParam **return_vals); + +/* local variables */ +GimpPlugInInfo PLUG_IN_INFO = +{ + NULL, /* init_proc */ + NULL, /* quit_proc */ + query, /* query_proc */ + run, /* run_proc */ +}; + +/* local functions */ +MAIN () + +static void +query (void) +{ + static GimpParamDef editor_args[] = + { + { GIMP_PDB_INT32, "run_mode", "Interactive, non-interactive" }, + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_DRAWABLE, "drawable", "Input drawable (unused)" } + }; + + static GimpParamDef decode_xmp_args[] = + { + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_STRING, "xmp", "XMP packet" }, + }; + + static GimpParamDef encode_xmp_args[] = + { + { GIMP_PDB_IMAGE, "image", "Input image" }, + }; + static GimpParamDef encode_xmp_return_vals[] = + { + { GIMP_PDB_STRING, "xmp", "XMP packet" }, + }; + +/* FIXME: uncomment when these are working + static GimpParamDef decode_exif_args[] = + { + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_INT32, "exif_size", "size of the EXIF block" }, + { GIMP_PDB_INT8ARRAY, "exif", "EXIF block" }, + }; + + static GimpParamDef encode_exif_args[] = + { + { GIMP_PDB_IMAGE, "image", "Input image" }, + }; + static GimpParamDef encode_exif_return_vals[] = + { + { GIMP_PDB_INT32, "exif_size", "size of the EXIF block" }, + { GIMP_PDB_INT8ARRAY, "exif", "EXIF block" }, + }; +*/ + + static GimpParamDef get_args[] = + { + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_STRING, "schema", "XMP schema prefix or URI" }, + { GIMP_PDB_STRING, "property", "XMP property name" }, + }; + static GimpParamDef get_return_vals[] = + { + { GIMP_PDB_INT32, "type", "XMP property type" }, + { GIMP_PDB_INT32, "num_vals", "number of values" }, + { GIMP_PDB_STRINGARRAY, "vals", "XMP property values" }, + }; + + static GimpParamDef set_args[] = + { + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_STRING, "schema", "XMP schema prefix or URI" }, + { GIMP_PDB_STRING, "property", "XMP property name" }, + { GIMP_PDB_INT32, "type", "XMP property type" }, + { GIMP_PDB_INT32, "num_vals", "number of values" }, + { GIMP_PDB_STRINGARRAY, "vals", "XMP property values" }, + }; + + static GimpParamDef get_simple_args[] = + { + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_STRING, "schema", "XMP schema prefix or URI" }, + { GIMP_PDB_STRING, "property", "XMP property name" }, + }; + static GimpParamDef get_simple_return_vals[] = + { + { GIMP_PDB_STRING, "value", "XMP property value" }, + }; + + static GimpParamDef set_simple_args[] = + { + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_STRING, "schema", "XMP schema prefix or URI" }, + { GIMP_PDB_STRING, "property", "XMP property name" }, + { GIMP_PDB_STRING, "value", "XMP property value" }, + }; + +/* FIXME: uncomment when these are working + static GimpParamDef delete_args[] = + { + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_STRING, "schema", "XMP schema prefix or URI" }, + { GIMP_PDB_STRING, "property", "XMP property name" }, + }; + + static GimpParamDef add_schema_args[] = + { + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_STRING, "prefix", "XMP schema prefix" }, + { GIMP_PDB_STRING, "uri", "XMP schema URI" }, + }; +*/ + + gimp_install_procedure ("plug_in_metadata_editor", + "View and edit metadata (EXIF, IPTC, XMP)", + "View and edit metadata information attached to the " + "current image. This can include EXIF, IPTC and/or " + "XMP information. Some or all of this metadata " + "will be saved in the file, depending on the output " + "file format.", + "Raphaël Quinet ", + "Raphaël Quinet ", + "2004-2005", + N_("Propert_ies"), + "RGB*, INDEXED*, GRAY*", + GIMP_PLUGIN, + G_N_ELEMENTS (editor_args), 0, + editor_args, NULL); + gimp_plugin_menu_register ("plug_in_metadata_editor", + N_("/File/Info")); + gimp_plugin_icon_register ("plug_in_metadata_editor", + GIMP_ICON_TYPE_STOCK_ID, GTK_STOCK_PROPERTIES); + /* FIXME: The GNOME HIG recommends using the accel Alt+Return for this */ + + gimp_install_procedure ("plug_in_metadata_decode_xmp", + "Decode an XMP packet", + "Parse an XMP packet and merge the results with " + "any metadata already attached to the image. This " + "should be used when an XMP packet is read from an " + "image file.", + "Raphaël Quinet ", + "Raphaël Quinet ", + "2005", + NULL, + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (decode_xmp_args), 0, + decode_xmp_args, NULL); + + gimp_install_procedure ("plug_in_metadata_encode_xmp", + "Encode metadata into an XMP packet", + "Generate an XMP packet from the metadata " + "information attached to the image. The new XMP " + "packet can then be saved into a file.", + "Raphaël Quinet ", + "Raphaël Quinet ", + "2005", + NULL, + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (encode_xmp_args), + G_N_ELEMENTS (encode_xmp_return_vals), + encode_xmp_args, encode_xmp_return_vals); + +/* FIXME: uncomment when these are working + gimp_install_procedure ("plug_in_metadata_decode_exif", + "Decode an EXIF block", + "Parse an EXIF block and merge the results with " + "any metadata already attached to the image. This " + "should be used when an EXIF block is read from an " + "image file.", + "Raphaël Quinet ", + "Raphaël Quinet ", + "2005", + NULL, + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (decode_exif_args), 0, + decode_exif_args, NULL); + + gimp_install_procedure ("plug_in_metadata_encode_exif", + "Encode metadata into an EXIF block", + "Generate an EXIF block from the metadata " + "information attached to the image. The new EXIF " + "block can then be saved into a file.", + "Raphaël Quinet ", + "Raphaël Quinet ", + "2005", + NULL, + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (encode_exif_args), + G_N_ELEMENTS (encode_exif_return_vals), + encode_exif_args, encode_exif_return_vals); +*/ + + gimp_install_procedure ("plug_in_metadata_get", + "Retrieve the values of an XMP property", + "Retrieve the list of values associated with " + "an XMP property.", + "Raphaël Quinet ", + "Raphaël Quinet ", + "2005", + NULL, + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (get_args), + G_N_ELEMENTS (get_return_vals), + get_args, get_return_vals); + + gimp_install_procedure ("plug_in_metadata_set", + "Set the values of an XMP property", + "Set the list of values associated with " + "an XMP property. If a property with the same " + "name already exists, it will be replaced.", + "Raphaël Quinet ", + "Raphaël Quinet ", + "2005", + NULL, + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (set_args), 0, + set_args, NULL); + + gimp_install_procedure ("plug_in_metadata_get_simple", + "Retrieve the value of an XMP property", + "Retrieve value associated with a scalar XMP " + "property. This can only be done for simple " + "property types such as text or integers. " + "Structured types must be retrieved with " + "plug_in_metadata_get().", + "Raphaël Quinet ", + "Raphaël Quinet ", + "2005", + NULL, + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (get_simple_args), + G_N_ELEMENTS (get_simple_return_vals), + get_simple_args, get_simple_return_vals); + + gimp_install_procedure ("plug_in_metadata_set_simple", + "Set the value of an XMP property", + "Set the value of a scalar XMP property. This " + "can only be done for simple property types such " + "as text or integers. Structured types need to " + "be set with plug_in_metadata_set().", + "Raphaël Quinet ", + "Raphaël Quinet ", + "2005", + NULL, + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (set_simple_args), 0, + set_simple_args, NULL); + + +} + +static void +run (const gchar *name, + gint nparams, + const GimpParam *param, + gint *nreturn_vals, + GimpParam **return_vals) +{ + static GimpParam values[4]; + gint32 image_ID; + XMPModel *xmp_model; + GimpPDBStatusType status = GIMP_PDB_SUCCESS; + GimpParasite *parasite = NULL; + + *nreturn_vals = 1; + *return_vals = values; + + values[0].type = GIMP_PDB_STATUS; + values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + + INIT_I18N(); + + if (! strcmp (name, "plug_in_metadata_editor")) + image_ID = param[1].data.d_image; + else + image_ID = param[0].data.d_image; + + xmp_model = xmp_model_new (); + + /* if there is already a metadata parasite, load it */ + parasite = gimp_image_parasite_find (image_ID, METADATA_PARASITE); + if (parasite) + { + GError *error = NULL; + + if (!! strncmp (gimp_parasite_data (parasite), + METADATA_MARKER, METADATA_MARKER_LEN) + || ! xmp_model_parse_buffer (xmp_model, + gimp_parasite_data (parasite) + + METADATA_MARKER_LEN, + gimp_parasite_data_size (parasite) + - METADATA_MARKER_LEN, + FALSE, &error)) + { + g_warning ("Metadata parasite seems to be corrupt"); + /* continue anyway, we will attach a clean parasite later */ + } + gimp_parasite_free (parasite); + } + + /* If we have no metadata yet, try to find some XMP in the file (but + * ignore errors if nothing is found). FIXME: This is a workaround + * until all file plug-ins do the right thing when loading their + * files. + */ + if (xmp_model_is_empty (xmp_model)) + { + const gchar *filename; + GError *error = NULL; + + filename = gimp_image_get_filename (image_ID); + if (filename != NULL) + if (xmp_model_parse_file (xmp_model, filename, &error)) + /* g_message ("XMP loaded from file '%s'\n", filename) */; + } + + /* Now check what we are supposed to do */ + if (! strcmp (name, "plug_in_metadata_editor")) + { + GimpRunMode run_mode; + + run_mode = param[0].data.d_int32; + if (run_mode == GIMP_RUN_INTERACTIVE) + { + /* Hello, user! */ + if (! metadata_dialog (image_ID, xmp_model)) + status = GIMP_PDB_CANCEL; + } + } + else if (! strcmp (name, "plug_in_metadata_decode_xmp")) + { + const gchar *buffer; + GError *error = NULL; + + buffer = param[1].data.d_string; + if (! xmp_model_parse_buffer (xmp_model, buffer, strlen (buffer), + FALSE, &error)) + status = GIMP_PDB_EXECUTION_ERROR; + } + else if (! strcmp (name, "plug_in_metadata_encode_xmp")) + { + /* done below together with the parasite */ + } + else if (! strcmp (name, "plug_in_metadata_get")) + { + g_warning ("Not implemented yet\n"); /* FIXME */ + } + else if (! strcmp (name, "plug_in_metadata_set")) + { + g_warning ("Not implemented yet\n"); /* FIXME */ + } + else if (! strcmp (name, "plug_in_metadata_get_simple")) + { + const gchar *schema_name; + const gchar *property_name; + const gchar *value; + + schema_name = param[1].data.d_string; + property_name = param[2].data.d_string; + value = xmp_model_get_scalar_property (xmp_model, schema_name, + property_name); + if (value) + { + *nreturn_vals = 2; + values[1].type = GIMP_PDB_STRING; + values[1].data.d_string = g_strdup (value); + } + else + status = GIMP_PDB_EXECUTION_ERROR; + } + else if (! strcmp (name, "plug_in_metadata_set_simple")) + { + const gchar *schema_name; + const gchar *property_name; + const gchar *property_value; + + schema_name = param[1].data.d_string; + property_name = param[2].data.d_string; + property_value = param[3].data.d_string; + if (! xmp_model_set_scalar_property (xmp_model, schema_name, + property_name, property_value)) + status = GIMP_PDB_EXECUTION_ERROR; + } + else + { + status = GIMP_PDB_CALLING_ERROR; + } + + if (status == GIMP_PDB_SUCCESS) + { + gchar *buffer; + gssize buffer_size; + gssize used_size; + + /* Generate the updated parasite and attach it to the image */ + buffer_size = xmp_estimate_size (xmp_model); + buffer = g_new (gchar, buffer_size + METADATA_MARKER_LEN); + strcpy (buffer, METADATA_MARKER); + used_size = xmp_generate_block (xmp_model, + buffer + METADATA_MARKER_LEN, + buffer_size); + parasite = gimp_parasite_new (METADATA_PARASITE, + GIMP_PARASITE_PERSISTENT, + used_size + METADATA_MARKER_LEN, + (gpointer) buffer); + gimp_image_parasite_attach (image_ID, parasite); + if (! strcmp (name, "plug_in_metadata_encode_xmp")) + { + *nreturn_vals = 2; + values[1].type = GIMP_PDB_STRING; + values[1].data.d_string = g_strdup (buffer + METADATA_MARKER_LEN); + } + g_free (buffer); + xmp_model_free (xmp_model); + } + + values[0].data.d_status = status; +} diff --git a/plug-ins/metadata/xmp-gen.c b/plug-ins/metadata/xmp-gen.c new file mode 100644 index 0000000000..595be7d2b7 --- /dev/null +++ b/plug-ins/metadata/xmp-gen.c @@ -0,0 +1,331 @@ +/* xmp-gen.c - generate XMP metadata from the tree model + * + * Copyright (C) 2005, Raphaël Quinet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + */ + +#include "config.h" + +#include +#include + +#include +#include +#include + +#include "libgimp/stdplugins-intl.h" + +#include "xmp-gen.h" +#include "xmp-model.h" + +static gssize +size_schema (GtkTreeModel *model, + GtkTreeIter *iter, + const XMPSchema **schema_r) +{ + gtk_tree_model_get (model, iter, + COL_XMP_TYPE_XREF, schema_r, + -1); + return (sizeof (" \n") - 5 + + strlen ((*schema_r)->prefix) + + strlen ((*schema_r)->uri) + + sizeof (" \n\n") - 1); +} + +static gssize +size_property (GtkTreeModel *model, + GtkTreeIter *iter, + const XMPSchema *schema) +{ + const XMPProperty *property; + const gchar **value_array; + gssize length; + gint i; + + gtk_tree_model_get (model, iter, + COL_XMP_TYPE_XREF, &property, + COL_XMP_VALUE_RAW, &value_array, + -1); + + switch (property->type) + { + case XMP_TYPE_BOOLEAN: + case XMP_TYPE_DATE: + case XMP_TYPE_INTEGER: + case XMP_TYPE_REAL: + case XMP_TYPE_MIME_TYPE: + case XMP_TYPE_TEXT: + case XMP_TYPE_RATIONAL: + return (sizeof (" <%s:%s>%s\n") - 11 + + 2 * strlen (schema->prefix) + + 2 * strlen (property->name) + + strlen (value_array[0])); + + case XMP_TYPE_LOCALE_BAG: + case XMP_TYPE_TEXT_BAG: + case XMP_TYPE_XPATH_BAG: + case XMP_TYPE_JOB_BAG: + case XMP_TYPE_INTEGER_SEQ: + case XMP_TYPE_TEXT_SEQ: + case XMP_TYPE_RESOURCE_EVENT_SEQ: + case XMP_TYPE_RATIONAL_SEQ: + length = (sizeof (" <%s:%s>\n \n") - 5 + + sizeof (" \n \n") - 5 + + 2 * strlen (schema->prefix) + + 2 * strlen (property->name)); + for (i = 0; value_array[i] != NULL; i++) + length += (sizeof (" %s\n") - 3 + + strlen (value_array[i])); + return length; + + case XMP_TYPE_LANG_ALT: + length = (sizeof (" <%s:%s>\n \n") - 5 + + sizeof (" \n \n") - 5 + + 2 * strlen (schema->prefix) + + 2 * strlen (property->name)); + for (i = 0; value_array[i] != NULL; i += 2) + length += (sizeof (" %s\n") - 5 + + strlen (value_array[i]) + + strlen (value_array[i + 1])); + return length; + + case XMP_TYPE_URI: + return (sizeof (" <%s:%s rdf:resource='%s' />\n") - 7 + + strlen (schema->prefix) + + strlen (property->name) + + strlen (value_array[0])); + + case XMP_TYPE_RESOURCE_REF: + case XMP_TYPE_DIMENSIONS: + case XMP_TYPE_THUMBNAIL_ALT: + case XMP_TYPE_GPS_COORDINATE: + case XMP_TYPE_FLASH: + case XMP_TYPE_OECF_SFR: + case XMP_TYPE_CFA_PATTERN: + case XMP_TYPE_DEVICE_SETTINGS: + return 100; /* FIXME */ + + case XMP_TYPE_UNKNOWN: + return 0; + } + return 0; +} + +/** + * xmp_estimate_size: + * @xmp_model: An #XMPModel + * + * Return value: estimated size (upper bound) of the XMP (RDF) encoding of + * the given model. + **/ +gssize +xmp_estimate_size (XMPModel *xmp_model) +{ + GtkTreeModel *model; + GtkTreeIter iter; + GtkTreeIter child; + gssize buffer_size; + const XMPSchema *schema; + + model = xmp_model_get_tree_model (xmp_model); + buffer_size = 158 + 44 + 1; + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (model), &iter)) + { + do + { + buffer_size += size_schema (model, &iter, &schema); + if (gtk_tree_model_iter_children (model, &child, &iter)) + { + do + { + buffer_size += size_property (model, &child, schema); + } + while (gtk_tree_model_iter_next (model, &child)); + } + } + while (gtk_tree_model_iter_next (model, &iter)); + } + return buffer_size; +} + +static gint +gen_schema_start (GtkTreeModel *model, + GtkTreeIter *iter, + gchar *buffer, + const XMPSchema **schema_r) +{ + gtk_tree_model_get (model, iter, + COL_XMP_TYPE_XREF, schema_r, + -1); + return sprintf (buffer, " \n", + (*schema_r)->prefix, (*schema_r)->uri); +} + +static gint +gen_schema_end (GtkTreeModel *model, + GtkTreeIter *iter, + gchar *buffer) +{ + return sprintf (buffer, " \n\n"); +} + +static gint +gen_property (GtkTreeModel *model, + GtkTreeIter *iter, + gchar *buffer, + const XMPSchema *schema) +{ + const XMPProperty *property; + const gchar **value_array; + gssize length; + gint i; + + gtk_tree_model_get (model, iter, + COL_XMP_TYPE_XREF, &property, + COL_XMP_VALUE_RAW, &value_array, + -1); + g_return_val_if_fail (property->name != NULL, 0); + switch (property->type) + { + case XMP_TYPE_BOOLEAN: + case XMP_TYPE_DATE: + case XMP_TYPE_INTEGER: + case XMP_TYPE_REAL: + case XMP_TYPE_MIME_TYPE: + case XMP_TYPE_TEXT: + case XMP_TYPE_RATIONAL: + return sprintf (buffer, " <%s:%s>%s\n", + schema->prefix, property->name, + value_array[0], + schema->prefix, property->name); + + case XMP_TYPE_LOCALE_BAG: + case XMP_TYPE_TEXT_BAG: + case XMP_TYPE_XPATH_BAG: + case XMP_TYPE_JOB_BAG: + length = sprintf (buffer, " <%s:%s>\n \n", + schema->prefix, property->name); + for (i = 0; value_array[i] != NULL; i++) + length += sprintf (buffer + length, " %s\n", + value_array[i]); + length += sprintf (buffer + length, " \n \n", + schema->prefix, property->name); + return length; + + case XMP_TYPE_INTEGER_SEQ: + case XMP_TYPE_TEXT_SEQ: + case XMP_TYPE_RESOURCE_EVENT_SEQ: + case XMP_TYPE_RATIONAL_SEQ: + length = sprintf (buffer, " <%s:%s>\n \n", + schema->prefix, property->name); + for (i = 0; value_array[i] != NULL; i++) + length += sprintf (buffer + length, " %s\n", + value_array[i]); + length += sprintf (buffer + length, " \n \n", + schema->prefix, property->name); + return length; + + case XMP_TYPE_LANG_ALT: + length = sprintf (buffer, " <%s:%s>\n \n", + schema->prefix, property->name); + for (i = 0; value_array[i] != NULL; i += 2) + length += sprintf (buffer + length, + " %s\n", + value_array[i], value_array[i + 1]); + length += sprintf (buffer + length, " \n \n", + schema->prefix, property->name); + return length; + + case XMP_TYPE_URI: + return sprintf (buffer, " <%s:%s rdf:resource='%s' />\n", + schema->prefix, property->name, value_array[0]); + + case XMP_TYPE_RESOURCE_REF: + case XMP_TYPE_DIMENSIONS: + case XMP_TYPE_THUMBNAIL_ALT: + case XMP_TYPE_GPS_COORDINATE: + case XMP_TYPE_FLASH: + case XMP_TYPE_OECF_SFR: + case XMP_TYPE_CFA_PATTERN: + case XMP_TYPE_DEVICE_SETTINGS: + g_warning ("FIXME: output not implemented yet (%s)", property->name); + break; + + case XMP_TYPE_UNKNOWN: + g_warning ("Unknown property type for %s", property->name); + break; + } + return 0; +} + +/** + * xmp_generate_block: + * @xmp_model: An #XMPModel + * @buffer: buffer in which the XMP block will be written + * @buffer_size: maximum size of the buffer + * + * Generate XMP block from xmp_model. + * + * Return value: number of characters stored in the buffer (not including the terminating NUL). + */ +gssize +xmp_generate_block (XMPModel *xmp_model, + gchar *buffer, + gssize buffer_size) +{ + GtkTreeModel *model; + GtkTreeIter iter; + GtkTreeIter child; + gssize n; + const XMPSchema *schema; + + model = xmp_model_get_tree_model (xmp_model); + + strcpy (buffer, + "\n" + "\n" + "\n" + "\n"); + n = sizeof ( + "\n" + "\n" + "\n" + "\n") - 1; + /* generate the contents of the XMP block */ + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (model), &iter)) + { + do + { + n += gen_schema_start (model, &iter, buffer + n, &schema); + if (gtk_tree_model_iter_children (model, &child, &iter)) + { + do + { + n += gen_property (model, &child, buffer + n, schema); + } + while (gtk_tree_model_iter_next (model, &child)); + } + n += gen_schema_end (model, &iter, buffer + n); + } + while (gtk_tree_model_iter_next (model, &iter)); + } + n = strlen (buffer); + strcpy (buffer + n, "\n\n\n"); + n += sizeof ("\n\n\n") - 1; + + return n; +} diff --git a/plug-ins/metadata/xmp-gen.h b/plug-ins/metadata/xmp-gen.h new file mode 100644 index 0000000000..42422de2cd --- /dev/null +++ b/plug-ins/metadata/xmp-gen.h @@ -0,0 +1,36 @@ +/* xmp-gen.h - generate XMP metadata from the tree model + * + * Copyright (C) 2005, Raphaël Quinet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + */ +#ifndef XMP_GEN_H +#define XMP_GEN_H + +#include +#include "xmp-model.h" + +G_BEGIN_DECLS + +gssize xmp_estimate_size (XMPModel *xmp_model); + +gssize xmp_generate_block (XMPModel *xmp_model, + gchar *buffer, + gssize buffer_size); + +G_END_DECLS + +#endif /* XMP_GEN_H */ diff --git a/plug-ins/metadata/xmp-model.c b/plug-ins/metadata/xmp-model.c new file mode 100644 index 0000000000..19dbad9012 --- /dev/null +++ b/plug-ins/metadata/xmp-model.c @@ -0,0 +1,1120 @@ +/* xmp-model.c - treeview model for XMP metadata + * + * Copyright (C) 2004-2005, Raphaël Quinet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + */ + +#include "config.h" + +#include +#ifdef DEBUG_XMP_PARSER +# include +#endif + +#include +#include +#include + +#include "libgimp/stdplugins-intl.h" + +#include "xmp-parse.h" +#include "xmp-model.h" + +/* The main part of the XMPModel structure is the GtkTreeStore in + * which all references to XMP properties are stored. In the tree, + * the elements at the root level are the schemas (namespaces) and the + * children of the schemas are the XMP properties. + * + * If the XMP file contains a schema that is not part of the XMP + * specification, it will be included in the custom_schemas list and + * the corresponding element in the tree will get a reference to that + * list element instead of a reference to one of the static schema + * definitions included below. Same for custom properties inside a + * known or custom schema. + */ +struct _XMPModel +{ + GtkTreeStore *treestore; + GSList *custom_schemas; + GSList *custom_properties; + + XMPSchema *current_schema; + GtkTreeIter current_schema_iter; +}; + +static XMPProperty dc_properties[] = +{ + { "contributor", XMP_TYPE_TEXT, TRUE }, + { "coverage", XMP_TYPE_TEXT, TRUE }, + { "creator", XMP_TYPE_TEXT_SEQ, TRUE }, + { "date", XMP_TYPE_DATE, TRUE }, + { "description", XMP_TYPE_LANG_ALT, TRUE }, + { "format", XMP_TYPE_MIME_TYPE, XMP_AUTO_UPDATE }, + { "identifier", XMP_TYPE_TEXT, TRUE }, /*xmp:Identifier*/ + { "language", XMP_TYPE_LOCALE_BAG, FALSE }, + { "publisher", XMP_TYPE_TEXT_BAG, TRUE }, + { "relation", XMP_TYPE_TEXT_BAG, TRUE }, + { "rights", XMP_TYPE_LANG_ALT, TRUE }, + { "source", XMP_TYPE_TEXT, TRUE }, + { "subject", XMP_TYPE_TEXT_BAG, TRUE }, + { "title", XMP_TYPE_LANG_ALT, TRUE }, + { "type", XMP_TYPE_TEXT_BAG, TRUE }, + { NULL, XMP_TYPE_UNKNOWN, FALSE } +}; + +static XMPProperty xmp_properties[] = +{ + { "Advisory", XMP_TYPE_XPATH_BAG, TRUE }, + { "BaseURL", XMP_TYPE_URI, FALSE }, + { "CreateDate", XMP_TYPE_DATE, TRUE }, + { "CreatorTool", XMP_TYPE_TEXT, FALSE }, + { "Identifier", XMP_TYPE_TEXT_BAG, TRUE }, + { "MetadataDate", XMP_TYPE_DATE, XMP_AUTO_UPDATE }, + { "ModifyDate", XMP_TYPE_DATE, XMP_AUTO_UPDATE }, + { "NickName", XMP_TYPE_TEXT, TRUE }, + { "Thumbnails", XMP_TYPE_THUMBNAIL_ALT, TRUE }, + { NULL, XMP_TYPE_UNKNOWN, FALSE } +}; + +static XMPProperty xmprights_properties[] = +{ + { "Certificate", XMP_TYPE_URI, TRUE }, + { "Marked", XMP_TYPE_BOOLEAN, TRUE }, + { "Owner", XMP_TYPE_TEXT_BAG, TRUE }, + { "UsageTerms", XMP_TYPE_LANG_ALT, TRUE }, + { "WebStatement", XMP_TYPE_URI, TRUE }, + { NULL, XMP_TYPE_UNKNOWN, FALSE } +}; + +static XMPProperty xmpmm_properties[] = +{ + { "DerivedFrom", XMP_TYPE_RESOURCE_REF, FALSE }, + { "DocumentID", XMP_TYPE_URI, FALSE }, + { "History", XMP_TYPE_RESOURCE_EVENT_SEQ, FALSE }, + { "ManagedFrom", XMP_TYPE_RESOURCE_REF, FALSE }, + { "Manager", XMP_TYPE_TEXT, FALSE }, + { "ManageTo", XMP_TYPE_URI, FALSE }, + { "ManageUI", XMP_TYPE_URI, FALSE }, + { "ManagerVariant", XMP_TYPE_TEXT, FALSE }, + { "RenditionClass", XMP_TYPE_TEXT, FALSE }, + { "RenditionParams", XMP_TYPE_TEXT, FALSE }, + { "VersionID", XMP_TYPE_TEXT, FALSE }, + { "Versions", XMP_TYPE_TEXT_SEQ, FALSE }, + { "LastURL", XMP_TYPE_URI, FALSE }, /*deprecated*/ + { "RenditionOf", XMP_TYPE_RESOURCE_REF, FALSE }, /*deprecated*/ + { "SaveID", XMP_TYPE_INTEGER, FALSE }, /*deprecated*/ + { NULL, XMP_TYPE_UNKNOWN, FALSE } +}; + +static XMPProperty xmpbj_properties[] = +{ + { "JobRef", XMP_TYPE_JOB_BAG, TRUE }, + { NULL, XMP_TYPE_UNKNOWN, FALSE } +}; + +static XMPProperty xmptpg_properties[] = +{ + { "MaxPageSize", XMP_TYPE_DIMENSIONS, FALSE }, + { "NPages", XMP_TYPE_INTEGER, FALSE }, + { NULL, XMP_TYPE_UNKNOWN, FALSE } +}; + +static XMPProperty pdf_properties[] = +{ + { "Keywords", XMP_TYPE_TEXT, TRUE }, + { "PDFVersion", XMP_TYPE_TEXT, FALSE }, + { "Producer", XMP_TYPE_TEXT, FALSE }, + { NULL, XMP_TYPE_UNKNOWN, FALSE } +}; + +static XMPProperty photoshop_properties[] = +{ + { "AuthorsPosition", XMP_TYPE_TEXT, TRUE }, + { "CaptionWriter", XMP_TYPE_TEXT, TRUE }, + { "Category", XMP_TYPE_TEXT, TRUE },/* 3 ascii chars */ + { "City", XMP_TYPE_TEXT, TRUE }, + { "Country", XMP_TYPE_TEXT, TRUE }, + { "Credit", XMP_TYPE_TEXT, TRUE }, + { "DateCreated", XMP_TYPE_DATE, TRUE }, + { "Headline", XMP_TYPE_TEXT, TRUE }, + { "Instructions", XMP_TYPE_TEXT, TRUE }, + { "Source", XMP_TYPE_TEXT, TRUE }, + { "State", XMP_TYPE_TEXT, TRUE }, + { "SupplementalCategories",XMP_TYPE_TEXT, TRUE }, + { "TransmissionReference",XMP_TYPE_TEXT, TRUE }, + { "Urgency", XMP_TYPE_INTEGER, TRUE },/* range: 1-8 */ + { NULL, XMP_TYPE_UNKNOWN, FALSE } +}; + +static XMPProperty tiff_properties[] = +{ + { "ImageWidth", XMP_TYPE_INTEGER, XMP_AUTO_UPDATE }, + { "ImageLength", XMP_TYPE_INTEGER, XMP_AUTO_UPDATE }, + { "BitsPerSample", XMP_TYPE_INTEGER_SEQ, FALSE }, + { "Compression", XMP_TYPE_INTEGER, FALSE },/* 1 or 6 */ + { "PhotometricInterpretation",XMP_TYPE_INTEGER, FALSE },/* 2 or 6 */ + { "Orientation", XMP_TYPE_INTEGER, FALSE },/* 1-8 */ + { "SamplesPerPixel", XMP_TYPE_INTEGER, FALSE }, + { "PlanarConfiguration",XMP_TYPE_INTEGER, FALSE },/* 1 or 2 */ + { "YCbCrSubSampling",XMP_TYPE_INTEGER_SEQ, FALSE },/* 2,1 or 2,2 */ + { "YCbCrPositioning",XMP_TYPE_INTEGER, FALSE },/* 1 or 2 */ + { "XResolution", XMP_TYPE_RATIONAL, XMP_AUTO_UPDATE }, + { "YResolution", XMP_TYPE_RATIONAL, XMP_AUTO_UPDATE }, + { "ResolutionUnit", XMP_TYPE_INTEGER, XMP_AUTO_UPDATE },/*2or3*/ + { "TransferFunction",XMP_TYPE_INTEGER_SEQ, FALSE },/* 3 * 256 ints */ + { "WhitePoint", XMP_TYPE_RATIONAL_SEQ, FALSE }, + { "PrimaryChromaticities",XMP_TYPE_RATIONAL_SEQ, FALSE }, + { "YCbCrCoefficients",XMP_TYPE_RATIONAL_SEQ, FALSE }, + { "ReferenceBlackWhite",XMP_TYPE_RATIONAL_SEQ, FALSE }, + { "DateTime", XMP_TYPE_DATE, FALSE },/*xmp:ModifyDate*/ + { "ImageDescription",XMP_TYPE_LANG_ALT, TRUE },/*dc:description*/ + { "Make", XMP_TYPE_TEXT, FALSE }, + { "Model", XMP_TYPE_TEXT, FALSE }, + { "Software", XMP_TYPE_TEXT, FALSE },/*xmp:CreatorTool*/ + { "Artist", XMP_TYPE_TEXT, TRUE },/*dc:creator*/ + { "Copyright", XMP_TYPE_TEXT, TRUE },/*dc:rights*/ + { NULL, XMP_TYPE_UNKNOWN, FALSE } +}; + +static XMPProperty exif_properties[] = +{ + { "ExifVersion", XMP_TYPE_TEXT, XMP_AUTO_UPDATE },/*"0210*/ + { "FlashpixVersion", XMP_TYPE_TEXT, FALSE },/* "0100" */ + { "ColorSpace", XMP_TYPE_INTEGER, FALSE },/* 1 or -32768 */ + { "ComponentsConfiguration",XMP_TYPE_INTEGER_SEQ, FALSE },/* 4 ints */ + { "CompressedBitsPerPixel",XMP_TYPE_RATIONAL, FALSE }, + { "PixelXDimension", XMP_TYPE_INTEGER, XMP_AUTO_UPDATE }, + { "PixelYDimension", XMP_TYPE_INTEGER, XMP_AUTO_UPDATE }, + { "MakerNote", XMP_TYPE_TEXT, FALSE },/* base64 enc.? */ + { "UserComment", XMP_TYPE_TEXT, TRUE }, + { "RelatedSoundFile",XMP_TYPE_TEXT, FALSE },/* DOS 8.3 fname */ + { "DateTimeOriginal",XMP_TYPE_DATE, FALSE }, + { "DateTimeDigitized",XMP_TYPE_DATE, FALSE }, + { "ExposureTime", XMP_TYPE_RATIONAL, FALSE }, + { "FNumber", XMP_TYPE_RATIONAL, FALSE }, + { "ExposureProgram", XMP_TYPE_INTEGER, FALSE },/* 0-8 */ + { "SpectralSensitivity",XMP_TYPE_TEXT, FALSE },/* ? */ + { "ISOSpeedRatings", XMP_TYPE_INTEGER_SEQ, FALSE }, + { "OECF", XMP_TYPE_OECF_SFR, FALSE }, + { "ShutterSpeedValue",XMP_TYPE_RATIONAL, FALSE }, + { "ApertureValue", XMP_TYPE_RATIONAL, FALSE }, + { "BrightnessValue", XMP_TYPE_RATIONAL, FALSE }, + { "ExposureBiasValue",XMP_TYPE_RATIONAL, FALSE }, + { "MaxApertureValue",XMP_TYPE_RATIONAL, FALSE }, + { "SubjectDistance", XMP_TYPE_RATIONAL, FALSE },/* in meters */ + { "MeteringMode", XMP_TYPE_INTEGER, FALSE },/* 0-6 or 255 */ + { "LightSource", XMP_TYPE_INTEGER, FALSE },/* 0-3,17-22,255*/ + { "Flash", XMP_TYPE_FLASH, FALSE }, + { "FocalLength", XMP_TYPE_RATIONAL, FALSE }, + { "SubjectArea", XMP_TYPE_INTEGER_SEQ, FALSE }, + { "FlashEnergy", XMP_TYPE_RATIONAL, FALSE }, + { "SpatialFrequencyResponse",XMP_TYPE_OECF_SFR, FALSE }, + { "FocalPlaneXResolution",XMP_TYPE_RATIONAL, FALSE }, + { "FocalPlaneYResolution",XMP_TYPE_RATIONAL, FALSE }, + { "FocalPlaneResolutionUnit",XMP_TYPE_INTEGER, FALSE },/* unit: 2 or 3 */ + { "SubjectLocation", XMP_TYPE_INTEGER_SEQ, FALSE },/* 2 ints: X, Y */ + { "ExposureIndex", XMP_TYPE_RATIONAL, FALSE }, + { "SensingMethod", XMP_TYPE_INTEGER, FALSE },/* 1-8 */ + { "FileSource", XMP_TYPE_INTEGER, FALSE },/* 3 */ + { "SceneType", XMP_TYPE_INTEGER, FALSE },/* 1 */ + { "CFAPattern", XMP_TYPE_CFA_PATTERN, FALSE }, + { "CustomRendered", XMP_TYPE_INTEGER, FALSE },/* 0-1 */ + { "ExposureMode", XMP_TYPE_INTEGER, FALSE },/* 0-2 */ + { "WhiteBalance", XMP_TYPE_INTEGER, FALSE },/* 0-1 */ + { "DigitalZoomRatio",XMP_TYPE_RATIONAL, FALSE }, + { "FocalLengthIn35mmFilm",XMP_TYPE_INTEGER, FALSE },/* in mm */ + { "SceneCaptureType",XMP_TYPE_INTEGER, FALSE },/* 0-3 */ + { "GainControl", XMP_TYPE_INTEGER, FALSE },/* 0-4 */ + { "Contrast", XMP_TYPE_INTEGER, FALSE },/* 0-2 */ + { "Saturation", XMP_TYPE_INTEGER, FALSE },/* 0-2 */ + { "Sharpness", XMP_TYPE_INTEGER, FALSE },/* 0-2 */ + { "DeviceSettingDescription",XMP_TYPE_DEVICE_SETTINGS, FALSE }, + { "SubjectDistanceRange",XMP_TYPE_INTEGER, FALSE },/* 0-3 */ + { "ImageUniqueID", XMP_TYPE_TEXT, FALSE },/* 32 chars */ + { "GPSVersionID", XMP_TYPE_TEXT, FALSE },/* "2.0.0.0" */ + { "GPSLatitude", XMP_TYPE_GPS_COORDINATE, FALSE }, + { "GPSLongitude", XMP_TYPE_GPS_COORDINATE, FALSE }, + { "GPSAltitudeRef", XMP_TYPE_INTEGER, FALSE },/* 0-1 */ + { "GPSAltitude", XMP_TYPE_RATIONAL, FALSE },/* in meters */ + { "GPSTimeStamp", XMP_TYPE_DATE, FALSE }, + { "GPSSatellites", XMP_TYPE_TEXT, FALSE },/* ? */ + { "GPSStatus", XMP_TYPE_TEXT, FALSE },/* "A" or "V" */ + { "GPSMeasureMode", XMP_TYPE_INTEGER, FALSE },/* 2-3 */ + { "GPSDOP", XMP_TYPE_RATIONAL, FALSE }, + { "GPSSpeedRef", XMP_TYPE_TEXT, FALSE },/* "K","M","N" */ + { "GPSSpeed", XMP_TYPE_RATIONAL, FALSE }, + { "GPSTrackRef", XMP_TYPE_TEXT, FALSE },/* "T" or "M"" */ + { "GPSTrack", XMP_TYPE_RATIONAL, FALSE }, + { "GPSImgDirectionRef",XMP_TYPE_TEXT, FALSE },/* "T" or "M"" */ + { "GPSImgDirection", XMP_TYPE_RATIONAL, FALSE }, + { "GPSMapDatum", XMP_TYPE_TEXT, FALSE }, + { "GPSDestLatitude", XMP_TYPE_GPS_COORDINATE, FALSE }, + { "GPSDestLongitude",XMP_TYPE_GPS_COORDINATE, FALSE }, + { "GPSDestBearingRef",XMP_TYPE_TEXT, FALSE },/* "T" or "M"" */ + { "GPSDestBearing", XMP_TYPE_RATIONAL, FALSE }, + { "GPSDestDistanceRef",XMP_TYPE_TEXT, FALSE },/* "K","M","N" */ + { "GPSDestDistance", XMP_TYPE_RATIONAL, FALSE }, + { "GPSProcessingMethod",XMP_TYPE_TEXT, FALSE }, + { "GPSAreaInformation",XMP_TYPE_TEXT, FALSE }, + { "GPSDifferential", XMP_TYPE_INTEGER, FALSE },/* 0-1 */ + { NULL, XMP_TYPE_UNKNOWN, FALSE } +}; + +static XMPSchema xmp_schemas[] = +{ + /* XMP schemas defined as of January 2004 */ + { "http://purl.org/dc/elements/1.1/", "dc", + "Dublin Core", dc_properties }, + { "http://ns.adobe.com/xap/1.0/", "xmp", + "XMP Basic", xmp_properties }, + { "http://ns.adobe.com/xap/1.0/rights/", "xmpRights", + "XMP Rights Management", xmprights_properties }, + { "http://ns.adobe.com/xap/1.0/mm/", "xmpMM", + "XMP Media Management", xmpmm_properties }, + { "http://ns.adobe.com/xap/1.0/bj/", "xmpBJ", + "XMP Basic Job Ticket", xmpbj_properties }, + { "http://ns.adobe.com/xap/1.0/t/pg/", "xmpTPg", + "XMP Paged-Text", xmptpg_properties }, + { "http://ns.adobe.com/pdf/1.3/", "pdf", + "Adobe PDF", pdf_properties }, + { "http://ns.adobe.com/photoshop/1.0/", "photoshop", + "Photoshop", photoshop_properties }, + { "http://ns.adobe.com/tiff/1.0/", "tiff", + "EXIF (TIFF Properties)", tiff_properties }, + { "http://ns.adobe.com/exif/1.0/", "exif", + "EXIF (EXIF-specific Properties)", exif_properties }, + /* XMP sub-types */ + { "http://ns.adobe.com/xmp/Identifier/qual/1.0/", "xmpidq", + NULL, NULL }, + { "http://ns.adobe.com/xap/1.0/g/img/", "xapGImg", + NULL, NULL }, + { "http://ns.adobe.com/xap/1.0/sType/Dimensions#", "stDim", + NULL, NULL }, + { "http://ns.adobe.com/xap/1.0/sType/ResourceEvent#", "stEvt", + NULL, NULL }, + { "http://ns.adobe.com/xap/1.0/sType/ResourceRef#", "stRef", + NULL, NULL }, + { "http://ns.adobe.com/xap/1.0/sType/Version#", "stVer", + NULL, NULL }, + { "http://ns.adobe.com/xap/1.0/sType/Job#", "stJob", + NULL, NULL }, + /* other useful namespaces */ + { "http://web.resource.org/cc/", "cc", + "Creative Commons", NULL }, + { "http://ns.adobe.com/iX/1.0/", "iX", + NULL, NULL }, + { NULL, NULL, NULL } +}; + +/** + * xmp_model_new: + * + * Return value: a new #XMPModel. + **/ +XMPModel * +xmp_model_new (void) +{ + XMPModel *xmp_model; + + xmp_model = g_new (XMPModel, 1); + /* columns defined by the XMPModelColumns enum */ + xmp_model->treestore = + gtk_tree_store_new (XMP_MODEL_NUM_COLUMNS, + G_TYPE_STRING, /* name */ + G_TYPE_STRING, /* value as string (for viewing) */ + G_TYPE_POINTER, /* value as array (from parser) */ + G_TYPE_POINTER, /* XMPProperty or XMPSchema */ + G_TYPE_POINTER, /* GtkWidget cross-reference */ + G_TYPE_INT, /* editable? */ + GDK_TYPE_PIXBUF, /* edit icon */ + G_TYPE_BOOLEAN, /* visible? */ + G_TYPE_INT, /* font weight */ + G_TYPE_BOOLEAN /* font weight set? */ + ); + xmp_model->custom_schemas = NULL; + xmp_model->custom_properties = NULL; + xmp_model->current_schema = NULL; + return xmp_model; +} + +/** + * xmp_model_free: + * @xmp_model: an #XMPModel + * + * Frees an #XMPModel. + **/ +void +xmp_model_free (XMPModel *xmp_model) +{ + GtkTreeModel *model; + GtkTreeIter iter; + GtkTreeIter child; + gchar **value_array; + gint i; + + g_return_if_fail (xmp_model != NULL); + /* we used XMP_FLAG_DEFER_VALUE_FREE for the parser, so now we must free + all value arrays */ + model = xmp_model_get_tree_model (xmp_model); + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (model), &iter)) + { + do + { + if (gtk_tree_model_iter_children (model, &child, &iter)) + { + do + { + gtk_tree_model_get (model, &child, + COL_XMP_VALUE_RAW, &value_array, + -1); + /* FIXME: this does not free everything */ + for (i = 0; value_array[i] != NULL; i++) + g_free (value_array[i]); + g_free (value_array); + } + while (gtk_tree_model_iter_next (model, &child)); + } + } + while (gtk_tree_model_iter_next (model, &iter)); + } + g_object_unref (xmp_model->treestore); + /* FIXME: free custom schemas */ + g_free (xmp_model); +} + +/** + * xmp_model_is_empty: + * @xmp_model: an #XMPModel + * + * Return value: %TRUE if @xmp_model is empty (no shemas, no properties) + **/ +gboolean +xmp_model_is_empty (XMPModel *xmp_model) +{ + GtkTreeIter iter; + + g_return_val_if_fail (xmp_model != NULL, TRUE); + if ((xmp_model->custom_schemas != NULL) + || (xmp_model->custom_properties != NULL)) + return FALSE; + return !gtk_tree_model_get_iter_first (GTK_TREE_MODEL (xmp_model->treestore), + &iter); +} + +/* check if the given schema_uri matches a known schema; else return NULL */ +static XMPSchema * +find_xmp_schema (XMPModel *xmp_model, + const gchar *schema_uri) +{ + int i; + GSList *list; + + /* check if we know about this schema (exact match for URI) */ + for (i = 0; xmp_schemas[i].uri != NULL; ++i) + { + if (! strcmp (xmp_schemas[i].uri, schema_uri)) + { +#ifdef DEBUG_XMP_PARSER + if (xmp_schemas[i].name != NULL) + printf ("%s \t[%s]\n", xmp_schemas[i].name, xmp_schemas[i].uri); + else + printf ("*** \t[%s]\n", xmp_schemas[i].uri); +#endif + return &(xmp_schemas[i]); + } + } + /* try again but accept "http:" without "//", or missing "http://" */ + for (i = 0; xmp_schemas[i].uri != NULL; ++i) + { + if (g_str_has_prefix (xmp_schemas[i].uri, "http://") + && ((! strcmp (xmp_schemas[i].uri + 7, schema_uri)) + || (g_str_has_prefix (schema_uri, "http:") + && ! strcmp (xmp_schemas[i].uri + 7, schema_uri + 5)) + )) + { +#ifdef DEBUG_XMP_PARSER + printf ("%s \t~~~[%s]\n", xmp_schemas[i].name, xmp_schemas[i].uri); +#endif + return &(xmp_schemas[i]); + } + } + /* this is not a standard shema; now check the custom schemas */ + for (list = xmp_model->custom_schemas; list != NULL; list = list->next) + { + if (! strcmp (((XMPSchema *)(list->data))->uri, schema_uri)) + { +#ifdef DEBUG_XMP_PARSER + printf ("CUSTOM %s \t[%s]\n", + ((XMPSchema *)(list->data))->name, + ((XMPSchema *)(list->data))->uri); +#endif + return (XMPSchema *)(list->data); + } + } +#ifdef DEBUG_XMP_PARSER + printf ("Unknown schema URI %s\n", schema_uri); +#endif + return NULL; +} + +/* check if the given prefix matches a known schema; else return NULL */ +static XMPSchema * +find_xmp_schema_prefix (XMPModel *xmp_model, + const gchar *prefix) +{ + int i; + GSList *list; + + for (i = 0; xmp_schemas[i].uri != NULL; ++i) + if (! strcmp (xmp_schemas[i].prefix, prefix)) + return &(xmp_schemas[i]); + for (list = xmp_model->custom_schemas; list != NULL; list = list->next) + if (! strcmp (((XMPSchema *)(list->data))->prefix, prefix)) + return (XMPSchema *)(list->data); + return NULL; +} + +/* make the next lookup a bit faster if the tree is not modified */ +static void +save_iter_for_schema (XMPModel *xmp_model, + XMPSchema *schema, + GtkTreeIter *iter) +{ + xmp_model->current_schema = schema; + if (iter != NULL) + memcpy (&(xmp_model->current_schema_iter), iter, sizeof (GtkTreeIter)); +} + +/* find the GtkTreeIter for the given schema and return TRUE if the schema was + found in the tree; else return FALSE */ +static gboolean +find_iter_for_schema (XMPModel *xmp_model, + XMPSchema *schema, + GtkTreeIter *iter) +{ + XMPSchema *schema_xref; + + if (schema == xmp_model->current_schema) + { + memcpy (iter, &(xmp_model->current_schema_iter), sizeof (GtkTreeIter)); + return TRUE; + } + /* check where this schema has been stored in the tree */ + if (! gtk_tree_model_get_iter_first (GTK_TREE_MODEL (xmp_model->treestore), + iter)) + return FALSE; + + do + { + gtk_tree_model_get (GTK_TREE_MODEL (xmp_model->treestore), iter, + COL_XMP_TYPE_XREF, &schema_xref, + -1); + if (schema_xref == schema) + { + save_iter_for_schema (xmp_model, schema, iter); + return TRUE; + } + } + while (gtk_tree_model_iter_next (GTK_TREE_MODEL (xmp_model->treestore), + iter)); + return FALSE; +} + +/* remove a property from the list of children of schema_iter */ +static void +find_and_remove_property (XMPModel *xmp_model, + XMPProperty *property, + GtkTreeIter *schema_iter) +{ + GtkTreeIter child_iter; + XMPProperty *property_xref; + + if (! gtk_tree_model_iter_children (GTK_TREE_MODEL (xmp_model->treestore), + &child_iter, schema_iter)) + return; + for (;;) + { + gtk_tree_model_get (GTK_TREE_MODEL (xmp_model->treestore), &child_iter, + COL_XMP_TYPE_XREF, &property_xref, + -1); + if (property_xref == property) + { + if (! gtk_tree_store_remove (GTK_TREE_STORE (xmp_model->treestore), + &child_iter)) + break; + } + else + { + if (! gtk_tree_model_iter_next (GTK_TREE_MODEL(xmp_model->treestore), + &child_iter)) + break; + } + } +} + +/* add a schema to the tree */ +static void +add_known_schema (XMPModel *xmp_model, + XMPSchema *schema, + GtkTreeIter *iter) +{ + gtk_tree_store_append (xmp_model->treestore, iter, NULL); + gtk_tree_store_set (xmp_model->treestore, iter, + COL_XMP_NAME, schema->name, + COL_XMP_VALUE, schema->uri, + COL_XMP_VALUE_RAW, NULL, + COL_XMP_TYPE_XREF, schema, + COL_XMP_WIDGET_XREF, NULL, + COL_XMP_EDITABLE, FALSE, + COL_XMP_EDIT_ICON, NULL, + COL_XMP_VISIBLE, FALSE, + COL_XMP_WEIGHT, PANGO_WEIGHT_BOLD, + COL_XMP_WEIGHT_SET, TRUE, + -1); + save_iter_for_schema (xmp_model, schema, iter); +} + +/* called by the XMP parser - new schema */ +static gpointer +parse_start_schema (XMPParseContext *context, + const gchar *ns_uri, + const gchar *ns_prefix, + gpointer user_data, + GError **error) +{ + XMPModel *xmp_model = user_data; + GtkTreeIter iter; + XMPSchema *schema; + + g_return_val_if_fail (xmp_model != NULL, NULL); + schema = find_xmp_schema (xmp_model, ns_uri); + if (schema == NULL) + { + /* add schema to custom_schemas */ + schema = g_new (XMPSchema, 1); + schema->uri = g_strdup (ns_uri); + schema->prefix = g_strdup (ns_prefix); + schema->name = schema->uri; + schema->properties = NULL; + xmp_model->custom_schemas = g_slist_prepend (xmp_model->custom_schemas, + schema); + } + else if (find_iter_for_schema (xmp_model, schema, &iter)) + { + /* already in the tree, so no need to add it again */ + return schema; + } + /* schemas with NULL names are special and should not go in the tree */ + if (schema->name == NULL) + { + save_iter_for_schema (xmp_model, NULL, NULL); + return schema; + } + /* if the schema is not in the tree yet, add it now */ + add_known_schema (xmp_model, schema, &iter); + return schema; +} + +/* called by the XMP parser - end of schema */ +static void +parse_end_schema (XMPParseContext *context, + gpointer ns_user_data, + gpointer user_data, + GError **error) +{ + XMPModel *xmp_model = user_data; + XMPSchema *schema = ns_user_data; + + g_return_if_fail (xmp_model != NULL); + g_return_if_fail (schema != NULL); + xmp_model->current_schema = NULL; + /* printf ("End of %s\n", schema->name); */ +} + +/* called by the XMP parser - new property */ +static void +parse_set_property (XMPParseContext *context, + const gchar *name, + XMPParseType type, + const gchar **value, + gpointer ns_user_data, + gpointer user_data, + GError **error) +{ + XMPModel *xmp_model = user_data; + XMPSchema *schema = ns_user_data; + int i; + const gchar *ns_prefix; + XMPProperty *property; + GtkTreeIter iter; + GtkTreeIter child_iter; + gchar *tmp_name; + gchar *tmp_value; + + g_return_if_fail (xmp_model != NULL); + g_return_if_fail (schema != NULL); + if (! find_iter_for_schema (xmp_model, schema, &iter)) + { + g_warning ("Unable to set XMP property '%s' because its schema is bad", + name); + return; + } + ns_prefix = schema->prefix; + property = NULL; + if (schema->properties != NULL) + for (i = 0; schema->properties[i].name != NULL; ++i) + if (! strcmp (schema->properties[i].name, name)) + { + property = &(schema->properties[i]); + break; + } + /* if the same property was already present, remove it (replace it) */ + if (property != NULL) + find_and_remove_property (xmp_model, property, &iter); + + switch (type) + { + case XMP_PTYPE_TEXT: +#ifdef DEBUG_XMP_PARSER + printf ("\t%s:%s = \"%s\"\n", ns_prefix, name, value[0]); +#endif + if (property != NULL) + /* FIXME */; + else + { + property = g_new (XMPProperty, 1); + property->name = g_strdup (name); + property->type = XMP_TYPE_TEXT; + property->editable = TRUE; + xmp_model->custom_properties = + g_slist_prepend (xmp_model->custom_properties, property); + } + gtk_tree_store_append (xmp_model->treestore, &child_iter, &iter); + gtk_tree_store_set (xmp_model->treestore, &child_iter, + COL_XMP_NAME, name, + COL_XMP_VALUE, value[0], + COL_XMP_VALUE_RAW, value, + COL_XMP_TYPE_XREF, property, + COL_XMP_WIDGET_XREF, NULL, + COL_XMP_EDITABLE, property->editable, + COL_XMP_EDIT_ICON, NULL, + COL_XMP_VISIBLE, TRUE, + COL_XMP_WEIGHT, PANGO_WEIGHT_NORMAL, + COL_XMP_WEIGHT_SET, FALSE, + -1); + break; + + case XMP_PTYPE_RESOURCE: +#ifdef DEBUG_XMP_PARSER + printf ("\t%s:%s @ = \"%s\"\n", ns_prefix, name, + value[0]); +#endif + if (property != NULL) + /* FIXME */; + else + { + property = g_new (XMPProperty, 1); + property->name = g_strdup (name); + property->type = XMP_TYPE_URI; + property->editable = TRUE; + xmp_model->custom_properties = + g_slist_prepend (xmp_model->custom_properties, property); + } + tmp_name = g_strconcat (name, " @", NULL); + gtk_tree_store_append (xmp_model->treestore, &child_iter, &iter); + gtk_tree_store_set (xmp_model->treestore, &child_iter, + COL_XMP_NAME, tmp_name, + COL_XMP_VALUE, value[0], + COL_XMP_VALUE_RAW, value, + COL_XMP_TYPE_XREF, property, + COL_XMP_WIDGET_XREF, NULL, + COL_XMP_EDITABLE, property->editable, + COL_XMP_EDIT_ICON, NULL, + COL_XMP_VISIBLE, TRUE, + COL_XMP_WEIGHT, PANGO_WEIGHT_NORMAL, + COL_XMP_WEIGHT_SET, FALSE, + -1); + g_free (tmp_name); + break; + + case XMP_PTYPE_ORDERED_LIST: + case XMP_PTYPE_UNORDERED_LIST: +#ifdef DEBUG_XMP_PARSER + printf ("\t%s:%s [] =", ns_prefix, name); + for (i = 0; value[i] != NULL; i++) + if (i == 0) + printf (" \"%s\"", value[i]); + else + printf (", \"%s\"", value[i]); + printf ("\n"); +#endif + if (property != NULL) + /* FIXME */; + else + { + property = g_new (XMPProperty, 1); + property->name = g_strdup (name); + property->type = ((type == XMP_PTYPE_ORDERED_LIST) + ? XMP_TYPE_TEXT_BAG + : XMP_TYPE_TEXT_SEQ); + property->editable = TRUE; + xmp_model->custom_properties = + g_slist_prepend (xmp_model->custom_properties, property); + } + + tmp_name = g_strconcat (name, " []", NULL); + tmp_value = g_strjoinv ("; ", (gchar **) value); + gtk_tree_store_append (xmp_model->treestore, &child_iter, &iter); + gtk_tree_store_set (xmp_model->treestore, &child_iter, + COL_XMP_NAME, tmp_name, + COL_XMP_VALUE, tmp_value, + COL_XMP_VALUE_RAW, value, + COL_XMP_TYPE_XREF, property, + COL_XMP_WIDGET_XREF, NULL, + COL_XMP_EDITABLE, property->editable, + COL_XMP_EDIT_ICON, NULL, + COL_XMP_VISIBLE, TRUE, + COL_XMP_WEIGHT, PANGO_WEIGHT_NORMAL, + COL_XMP_WEIGHT_SET, FALSE, + -1); + g_free (tmp_value); + g_free (tmp_name); + break; + + case XMP_PTYPE_ALT_LANG: +#ifdef DEBUG_XMP_PARSER + for (i = 0; value[i] != NULL; i += 2) + printf ("\t%s:%s [lang:%s] = \"%s\"\n", ns_prefix, name, + value[i], value[i + 1]); +#endif + if (property != NULL) + /* FIXME */; + else + { + property = g_new (XMPProperty, 1); + property->name = g_strdup (name); + property->type = XMP_TYPE_LANG_ALT; + property->editable = TRUE; + xmp_model->custom_properties = + g_slist_prepend (xmp_model->custom_properties, property); + } + for (i = 0; value[i] != NULL; i += 2) + { + tmp_name = g_strconcat (name, " [", value[i], "]", NULL); + gtk_tree_store_append (xmp_model->treestore, &child_iter, &iter); + gtk_tree_store_set (xmp_model->treestore, &child_iter, + COL_XMP_NAME, tmp_name, + COL_XMP_VALUE, value[i + 1], + COL_XMP_VALUE_RAW, value, + COL_XMP_TYPE_XREF, property, + COL_XMP_WIDGET_XREF, NULL, + COL_XMP_EDITABLE, property->editable, + COL_XMP_EDIT_ICON, NULL, + COL_XMP_VISIBLE, TRUE, + COL_XMP_WEIGHT, PANGO_WEIGHT_NORMAL, + COL_XMP_WEIGHT_SET, FALSE, + -1); + g_free (tmp_name); + } + break; + + case XMP_PTYPE_STRUCTURE: +#ifdef DEBUG_XMP_PARSER + for (i = 2; value[i] != NULL; i += 2) + printf ("\t%s:%s [%s] = \"%s\"\n", ns_prefix, name, + value[i], value[i + 1]); +#endif + if (property != NULL) + /* FIXME */; + else + { + property = g_new (XMPProperty, 1); + property->name = g_strdup (name); + property->type = XMP_TYPE_UNKNOWN; + property->editable = TRUE; + xmp_model->custom_properties = + g_slist_prepend (xmp_model->custom_properties, property); + } + for (i = 2; value[i] != NULL; i += 2) + { + tmp_name = g_strconcat (name, " [", value[i], "]", NULL); + gtk_tree_store_append (xmp_model->treestore, &child_iter, &iter); + gtk_tree_store_set (xmp_model->treestore, &child_iter, + COL_XMP_NAME, tmp_name, + COL_XMP_VALUE, value[i + 1], + COL_XMP_VALUE_RAW, value, + COL_XMP_TYPE_XREF, property, + COL_XMP_WIDGET_XREF, NULL, + COL_XMP_EDITABLE, property->editable, + COL_XMP_EDIT_ICON, NULL, + COL_XMP_VISIBLE, TRUE, + COL_XMP_WEIGHT, PANGO_WEIGHT_NORMAL, + COL_XMP_WEIGHT_SET, FALSE, + -1); + g_free (tmp_name); + } + break; + + default: +#ifdef DEBUG_XMP_PARSER + printf ("\t%s:%s = ?\n", ns_prefix, name); +#endif + break; + } +} + +/* called by the XMP parser - parse error */ +static void +parse_error (XMPParseContext *context, + GError *error, + gpointer user_data) +{ + g_warning ("While parsing XMP metadata:\n%s\n", error->message); +} + +static XMPParser xmp_parser = { + parse_start_schema, + parse_end_schema, + parse_set_property, + parse_error +}; + +/** + * xmp_model_parse_buffer: + * @xmp_model: pointer to the #XMPModel in which the results will be stored + * @buffer: buffer to be parsed + * @buffer_length: length of the @buffer + * @skip_other_data: if %TRUE, allow arbitrary data before XMP packet marker + * @error: return location for a #GError + * + * Parse a buffer containing XMP metadata and merge the parsed contents into + * the supplied @xmp_model. If @skip_other_data is %TRUE, then the parser + * will try to find the marker in the buffer, skipping any + * unknown data found before it. + * + * Return value: %TRUE on success, %FALSE if an error was set + * + * (Note: this calls the functions from xmp_parse.c, which will call the + * functions in this file through the xmp_parser structure defined above.) + **/ +gboolean +xmp_model_parse_buffer (XMPModel *xmp_model, + const gchar *buffer, + gssize buffer_length, + gboolean skip_other_data, + GError **error) +{ + XMPParseFlags flags; + XMPParseContext *context; + + flags = XMP_FLAG_DEFER_VALUE_FREE; /* we will free the array ourselves */ + if (skip_other_data) + flags |= XMP_FLAG_FIND_XPACKET; + + context = xmp_parse_context_new (&xmp_parser, flags, xmp_model, NULL); + + if (! xmp_parse_context_parse (context, buffer, buffer_length, error)) + { + xmp_parse_context_free (context); + return FALSE; + } + + if (! xmp_parse_context_end_parse (context, error)) + { + xmp_parse_context_free (context); + return FALSE; + } + + xmp_parse_context_free (context); + return TRUE; +} + +/** + * xmp_model_parse_file: + * @xmp_model: pointer to the #XMPModel in which the results will be stored + * @filename: name of the file containing XMP metadata to parse + * @error: return location for a #GError + * + * Try to find XMP metadata in a file and merge its contents into the supplied + * @xmp_model. + * + * Return value: %TRUE on success, %FALSE if an error was set + **/ +gboolean +xmp_model_parse_file (XMPModel *xmp_model, + const gchar *filename, + GError **error) +{ + gchar *buffer; + guint buffer_length; + + g_return_val_if_fail (filename != NULL, FALSE); + if (! g_file_get_contents (filename, &buffer, &buffer_length, error)) + return FALSE; + if (! xmp_model_parse_buffer (xmp_model, buffer, buffer_length, + TRUE, error)) + return FALSE; + g_free (buffer); + return TRUE; +} + +/** + * xmp_model_get_tree_model: + * @xmp_model: pointer to an #XMPModel + * + * Return a pointer to the #GtkTreeModel contained in the #XMPModel. + **/ +GtkTreeModel * +xmp_model_get_tree_model (XMPModel *xmp_model) +{ + g_return_val_if_fail (xmp_model != NULL, NULL); + return GTK_TREE_MODEL (xmp_model->treestore); +} + +/** + * xmp_model_get_scalar_property: + * @xmp_model: pointer to an #XMPModel + * @schema_name: full URI or usual prefix of the schema + * @property_name: name of the property to store + * + * Store a new value for the specified XMP property. + * + * Return value: string representation of the value of that property, or %NULL if the property does not exist + **/ +const gchar * +xmp_model_get_scalar_property (XMPModel *xmp_model, + const gchar *schema_name, + const gchar *property_name) +{ + XMPSchema *schema; + GtkTreeIter iter; + XMPProperty *property = NULL; + GtkTreeIter child_iter; + int i; + XMPProperty *property_xref; + const gchar *value; + + g_return_val_if_fail (xmp_model != NULL, NULL); + g_return_val_if_fail (schema_name != NULL, NULL); + g_return_val_if_fail (property_name != NULL, NULL); + schema = find_xmp_schema (xmp_model, schema_name); + if (! schema) + schema = find_xmp_schema_prefix (xmp_model, schema_name); + if (! schema) + return NULL; + if (! find_iter_for_schema (xmp_model, schema, &iter)) + return NULL; + if (schema->properties != NULL) + for (i = 0; schema->properties[i].name != NULL; ++i) + if (! strcmp (schema->properties[i].name, property_name)) + { + property = &(schema->properties[i]); + break; + } + if (property == NULL) + return NULL; + if (! gtk_tree_model_iter_children (GTK_TREE_MODEL (xmp_model->treestore), + &child_iter, &iter)) + return NULL; + do + { + gtk_tree_model_get (GTK_TREE_MODEL (xmp_model->treestore), &child_iter, + COL_XMP_TYPE_XREF, &property_xref, + COL_XMP_VALUE, &value, + -1); + if (property_xref == property) + return value; + } + while (gtk_tree_model_iter_next (GTK_TREE_MODEL(xmp_model->treestore), + &child_iter)); + return NULL; +} + +/** + * xmp_model_set_scalar_property: + * @xmp_model: pointer to an #XMPModel + * @schema_name: full URI or usual prefix of the schema + * @property_name: name of the property to store + * @property_value: value to store + * + * Store a new value for the specified XMP property. + * + * Return value: %TRUE if the property was set, %FALSE if an error occured (for example, the @schema_name is invalid) + **/ +gboolean +xmp_model_set_scalar_property (XMPModel *xmp_model, + const gchar *schema_name, + const gchar *property_name, + const gchar *property_value) +{ + XMPSchema *schema; + GtkTreeIter iter; + XMPProperty *property = NULL; + GtkTreeIter child_iter; + int i; + gchar **value; + + g_return_val_if_fail (xmp_model != NULL, FALSE); + g_return_val_if_fail (schema_name != NULL, FALSE); + g_return_val_if_fail (property_name != NULL, FALSE); + g_return_val_if_fail (property_value != NULL, FALSE); + schema = find_xmp_schema (xmp_model, schema_name); + if (! schema) + schema = find_xmp_schema_prefix (xmp_model, schema_name); + if (! schema) + return FALSE; + + if (! find_iter_for_schema (xmp_model, schema, &iter)) + add_known_schema (xmp_model, schema, &iter); + + if (schema->properties != NULL) + for (i = 0; schema->properties[i].name != NULL; ++i) + if (! strcmp (schema->properties[i].name, property_name)) + { + property = &(schema->properties[i]); + break; + } + if (property != NULL) + find_and_remove_property (xmp_model, property, &iter); + else + { + property = g_new (XMPProperty, 1); + property->name = g_strdup (property_name); + property->type = XMP_TYPE_TEXT; + property->editable = TRUE; + xmp_model->custom_properties = + g_slist_prepend (xmp_model->custom_properties, property); + } + + value = g_new (gchar *, 2); + value[0] = g_strdup (property_value); + value[1] = NULL; + gtk_tree_store_append (xmp_model->treestore, &child_iter, &iter); + gtk_tree_store_set (xmp_model->treestore, &child_iter, + COL_XMP_NAME, g_strdup (property_name), + COL_XMP_VALUE, value[0], + COL_XMP_VALUE_RAW, value, + COL_XMP_TYPE_XREF, property, + COL_XMP_WIDGET_XREF, NULL, + COL_XMP_EDITABLE, property->editable, + COL_XMP_EDIT_ICON, NULL, + COL_XMP_VISIBLE, TRUE, + COL_XMP_WEIGHT, PANGO_WEIGHT_NORMAL, + COL_XMP_WEIGHT_SET, FALSE, + -1); + return TRUE; +} diff --git a/plug-ins/metadata/xmp-model.h b/plug-ins/metadata/xmp-model.h new file mode 100644 index 0000000000..5cf4cfdba4 --- /dev/null +++ b/plug-ins/metadata/xmp-model.h @@ -0,0 +1,128 @@ +/* xmp-model.h - treeview model for XMP metadata + * + * Copyright (C) 2004, Raphaël Quinet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + */ + +#ifndef XMP_MODEL_H +#define XMP_MODEL_H + +#include +#include + +G_BEGIN_DECLS + +typedef struct _XMPModel XMPModel; + +/* known data types for XMP properties, as found in the XMP specification */ +typedef enum +{ + XMP_TYPE_BOOLEAN, /* TEXT */ + XMP_TYPE_DATE, /* TEXT */ + XMP_TYPE_DIMENSIONS, /* STRUCTURE */ + XMP_TYPE_INTEGER, /* TEXT */ + XMP_TYPE_INTEGER_SEQ, /* ORDERED_LIST */ + XMP_TYPE_LANG_ALT, /* ALT_LANG */ + XMP_TYPE_LOCALE_BAG, /* UNORDERED_LIST */ + XMP_TYPE_REAL, /* TEXT */ + XMP_TYPE_MIME_TYPE, /* TEXT */ + XMP_TYPE_TEXT, /* TEXT */ + XMP_TYPE_TEXT_BAG, /* UNORDERED_LIST */ + XMP_TYPE_TEXT_SEQ, /* ORDERED_LIST */ + XMP_TYPE_THUMBNAIL_ALT, /* (?) */ + XMP_TYPE_URI, /* TEXT or RESOURCE (?) */ + XMP_TYPE_XPATH_BAG, /* UNORDERED_LIST */ + XMP_TYPE_RESOURCE_EVENT_SEQ, /* ORDERED_LIST */ + XMP_TYPE_RESOURCE_REF, /* TEXT */ + XMP_TYPE_JOB_BAG, /* UNORDERED_LIST */ + XMP_TYPE_RATIONAL, /* TEXT */ + XMP_TYPE_RATIONAL_SEQ, /* ORDERED_LIST */ + XMP_TYPE_GPS_COORDINATE, /* (?) */ + XMP_TYPE_FLASH, /* STRUCTURE */ + XMP_TYPE_OECF_SFR, /* (?) */ + XMP_TYPE_CFA_PATTERN, /* (?) */ + XMP_TYPE_DEVICE_SETTINGS, /* (?) */ + XMP_TYPE_UNKNOWN +} XMPType; + +/* columns used in the GtkTreeStore model holding the XMP metadata */ +typedef enum +{ + COL_XMP_NAME = 0, /* G_TYPE_STRING */ + COL_XMP_VALUE, /* G_TYPE_STRING */ + COL_XMP_VALUE_RAW, /* G_TYPE_POINTER */ + COL_XMP_TYPE_XREF, /* G_TYPE_POINTER */ + COL_XMP_WIDGET_XREF, /* G_TYPE_POINTER */ + COL_XMP_EDITABLE, /* G_TYPE_INT */ + COL_XMP_EDIT_ICON, /* GDK_TYPE_PIXBUF */ + COL_XMP_VISIBLE, /* G_TYPE_BOOLEAN */ + COL_XMP_WEIGHT, /* G_TYPE_INT */ + COL_XMP_WEIGHT_SET, /* G_TYPE_BOOLEAN */ + XMP_MODEL_NUM_COLUMNS +} XMPModelColumns; + +/* special value for the COL_XMP_EDITABLE column. not strictly boolean... */ +#define XMP_AUTO_UPDATE 2 + +/* XMP properties referenced in the tree via COL_XMP_TYPE_XREF (depth 2) */ +typedef struct +{ + const gchar *name; + XMPType type; + gboolean editable; +} XMPProperty; + +/* XMP schemas referenced in the tree via COL_XMP_TYPE_XREF (depth 1) */ +typedef struct +{ + const gchar *uri; + const gchar *prefix; + const gchar *name; + XMPProperty *properties; +} XMPSchema; + +XMPModel *xmp_model_new (void); + +void xmp_model_free (XMPModel *xmp_model); + +gboolean xmp_model_is_empty (XMPModel *xmp_model); + +gboolean xmp_model_parse_buffer (XMPModel *xmp_model, + const gchar *buffer, + gssize buffer_length, + gboolean skip_other_data, + GError **error); + +gboolean xmp_model_parse_file (XMPModel *xmp_model, + const gchar *filename, + GError **error); + +GtkTreeModel *xmp_model_get_tree_model (XMPModel *xmp_model); + +const gchar *xmp_model_get_scalar_property (XMPModel *xmp_model, + const gchar *schema_name, + const gchar *property_name); + +gboolean xmp_model_set_scalar_property (XMPModel *xmp_model, + const gchar *schema_name, + const gchar *property_name, + const gchar *property_value); + + +G_END_DECLS + +#endif /* XMP_MODEL_H */ diff --git a/plug-ins/metadata/xmp-parse.c b/plug-ins/metadata/xmp-parse.c new file mode 100644 index 0000000000..5ff0f21d5e --- /dev/null +++ b/plug-ins/metadata/xmp-parse.c @@ -0,0 +1,1138 @@ +/* xmp-parse.c - simple parser for XMP metadata + * + * Copyright (C) 2004, Raphaël Quinet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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. + */ + +/* This code implements a simple parser for XMP metadata. Its API is + * based on the one provided by GMarkupParser (part of Glib). + * + * This is not a full RDF parser: it shares some of the limitations + * inherited from glib (UTF-8 only, no special entities) and supports + * RDF only to the extent needed for XMP. XMP defines several + * "schemas" containing a list of "properties". Each property in a + * schema has one value, which can be a simple type (e.g., integer or + * text) or a structured type (rdf:Alt, rdf:Bag, rdf:Seq). As there + * is no need to support a much deeper nesting of elements, this + * parser does not try to maintain an arbitrarily large stack of + * elements. Also, it does not support RDF features that are + * forbidden by the XMP specs, such as rdf:parseType="Litteral". + * + * The design goals for this parser are: support all RDF features + * needed for XMP (at least the features explicitely described in the + * XMP spec), be tolerant in case unknown elements or attributes are + * found, be as simple as possible, avoid building a DOM tree. + * + * TODO: + * - support UCS-2 and UCS-4 besides UTF-8 (copy and convert the data) + * - write a decent scanner for finding as recommended + * in the XMP specification (including support for UCS-2 and UCS-4) + * - provide an API for passing unknown elements or tags to the caller + */ + +#ifndef WITHOUT_GIMP +# include "config.h" +# include +# include "xmp-parse.h" +# include "libgimp/stdplugins-intl.h" +#else +# include +# include "xmp-parse.h" +# define _(String) (String) +# define N_(String) (String) +#endif + +GQuark +xmp_parse_error_quark (void) +{ + static GQuark error_quark = 0; + + if (error_quark == 0) + error_quark = g_quark_from_static_string ("xmp-parse-error-quark"); + + return error_quark; +} + +/* The current version of XMP (January 2004) is relatively simple in + * that only a few elements (, , , + * ) may include other elements and no deep nesting is + * allowed. As a result, it is possible to include all allowed + * combinations directly into the state instead of having to maintain + * a separate stack besides the simple state enum. There is only a + * 1-element stack (saved_state) used for some special cases such as + * when skipping unknown tags or parsing a property with qualifiers. + * + * Here is a quick overview of the structure of an XMP document and + * the corresponding state after reading each element. Depending on + * the contents of each property, we can have the following cases + * that are summarized as STATE_INSIDE... below: + * - structured types can use any valid combination of the states + * between STATE_INSIDE_QDESC and STATE_INSIDE_SEQ_LI_RSC; + * - simple property types contain some text and no other element so + * the only state will be STATE_INSIDE_PROPERTY while reading that + * text; + * - if the shorthand notation is used for some simple properties, + * then they will be written as attributes of a top level + * rdf:Description instead of being a separate element, so the + * state will not go deeper than STATE_INSIDE_TOPLEVEL_DESC. + * + * (init) STATE_START + * STATE_INSIDE_XPACKET + * STATE_INSIDE_XMPMETA + * STATE_INSIDE_RDF + * STATE_INSIDE_TOPLEVEL_DESC + * STATE_INSIDE_PROPERTY + * ... (simple or structured property) STATE_INSIDE... + * STATE_INSIDE_TOPLEVEL_DESC + * STATE_INSIDE_PROPERTY + * ... (simple or structured property) STATE_INSIDE... + * STATE_INSIDE_TOPLEVEL_DESC + * STATE_INSIDE_RDF + * STATE_INSIDE_TOPLEVEL_DESC + * ... (some properties) STATE_INSIDE_PROPERTY + * STATE_INSIDE_RDF + * ... + * STATE_AFTER_RDF + * STATE_AFTER_XMPMETA + * STATE_AFTER_XPACKET + * + * Note: The abbreviation QDESC is used for the properties with + * qualifiers (when is used deeper than at the top + * level inside ). In that case, QDESC_VALUE contains the + * value of the property and QDESC_QUAL is used for each of the + * optional qualifiers. + */ +typedef enum +{ + STATE_START, + STATE_INSIDE_XPACKET, + STATE_INSIDE_XMPMETA, + STATE_INSIDE_RDF, + STATE_INSIDE_TOPLEVEL_DESC, + STATE_INSIDE_PROPERTY, + STATE_INSIDE_QDESC, + STATE_INSIDE_QDESC_VALUE, + STATE_INSIDE_QDESC_QUAL, + STATE_INSIDE_STRUCT_ADD_NS, + STATE_INSIDE_STRUCT, + STATE_INSIDE_STRUCT_ELEMENT, + STATE_INSIDE_ALT, + STATE_INSIDE_ALT_LI, + STATE_INSIDE_BAG, + STATE_INSIDE_BAG_LI, + STATE_INSIDE_BAG_LI_RSC, + STATE_INSIDE_SEQ, + STATE_INSIDE_SEQ_LI, + STATE_INSIDE_SEQ_LI_RSC, + STATE_AFTER_RDF, + STATE_AFTER_XMPMETA, + STATE_AFTER_XPACKET, + STATE_SKIPPING_UNKNOWN_ELEMENTS, + STATE_ERROR +} XMPParseState; + +typedef struct +{ + gint depth; + gchar *uri; + gchar *prefix; + gint prefix_len; + gpointer ns_user_data; +} XMLNameSpace; + +struct _XMPParseContext +{ + const XMPParser *parser; + XMPParseFlags flags; + + gpointer user_data; + GDestroyNotify user_data_dnotify; + + XMPParseState state; + gint depth; + + GSList *namespaces; + + gchar *property; + XMLNameSpace *property_ns; + XMPParseType property_type; + gchar **prop_value; + gint prop_cur_value; + gint prop_max_value; + gboolean prop_missing_value; + /* used when skipping tags, or inside a struct or property qualifier */ + XMPParseState saved_state; + gint saved_depth; + + GMarkupParseContext *markup_context; +}; + +static void +parse_error (XMPParseContext *context, + GError **error, + XMPParseError code, + const gchar *format, + ...) +{ + GError *tmp_error; + gchar *s; + va_list args; + + va_start (args, format); + s = g_strdup_vprintf (format, args); + va_end (args); + + if (code == XMP_ERROR_NO_XPACKET) + tmp_error = g_error_new (XMP_PARSE_ERROR, code, _("Error: %s"), s); + else + { + gint line_number; + gint char_number; + + g_markup_parse_context_get_position (context->markup_context, + &line_number, + &char_number); + tmp_error = g_error_new (XMP_PARSE_ERROR, code, + _("Error on line %d char %d: %s"), + line_number, char_number, s); + } + g_free (s); + + context->state = STATE_ERROR; + if (context->parser->error) + (*context->parser->error) (context, tmp_error, + context->user_data); + + g_propagate_error (error, tmp_error); +} + +static void +parse_error_element (XMPParseContext *context, + GError **error, + const gchar *expected_element, + gboolean optional, + const gchar *found_element) +{ + if (optional == TRUE) + parse_error (context, error, XMP_ERROR_UNEXPECTED_ELEMENT, + _("Expected text or optional element <%s>, found <%s> instead"), + expected_element, found_element); + else + parse_error (context, error, XMP_ERROR_UNEXPECTED_ELEMENT, + _("Expected element <%s>, found <%s> instead"), + expected_element, found_element); +} + +static void +unknown_element (XMPParseContext *context, + GError **error, + const gchar *element_name) +{ + if (context->flags & XMP_FLAG_NO_UNKNOWN_ELEMENTS) + parse_error (context, error, XMP_ERROR_UNKNOWN_ELEMENT, + _("Unknown element <%s>"), + element_name); + else + { + context->saved_state = context->state; + context->state = STATE_SKIPPING_UNKNOWN_ELEMENTS; + context->saved_depth = context->depth; + } +} + +static void +unknown_attribute (XMPParseContext *context, + GError **error, + const gchar *element_name, + const gchar *attribute_name, + const gchar *attribute_value) +{ + if (context->flags & XMP_FLAG_NO_UNKNOWN_ATTRIBUTES) + parse_error (context, error, XMP_ERROR_UNKNOWN_ATTRIBUTE, + _("Unknown attribute \"%s\"=\"%s\" in element <%s>"), + attribute_name, attribute_value, element_name); +} + +static gboolean +is_whitespace_string (const gchar *string) +{ + const gchar *p; + + if (string == NULL) + return TRUE; + /* XML accepts only 4 ASCII chars as whitespace and no other UNICODE chars */ + for (p = string; *p; ++p) + if (*p != ' ' && *p != '\t' && *p != '\r' && *p != '\n') + return FALSE; + return TRUE; +} + +static void +push_namespace (XMPParseContext *context, + const gchar *uri, + const gchar *prefix, + GError **error) +{ + XMLNameSpace *ns; + + if (! strcmp (uri, "http://www.w3.org/1999/02/22-rdf-syntax-ns#" /* RDF */) + || ! strcmp (uri, "adobe:ns:meta/" /* xmpmeta */)) + return; + ns = g_new (XMLNameSpace, 1); + ns->depth = context->depth; + ns->uri = g_strdup (uri); + ns->prefix = g_strdup (prefix); + ns->prefix_len = strlen (prefix); + context->namespaces = g_slist_prepend (context->namespaces, ns); + if (context->parser->start_schema) + ns->ns_user_data = (*context->parser->start_schema) (context, + ns->uri, + ns->prefix, + context->user_data, + error); + else + ns->ns_user_data = NULL; +} + +static void +pop_namespaces (XMPParseContext *context, + GError **error) +{ + XMLNameSpace *ns; + + if (context->namespaces == NULL) + return; + /* free all namespaces that are deeper than the current element depth */ + ns = context->namespaces->data; + while (ns->depth >= context->depth) + { + if (context->parser->end_schema) + (*context->parser->end_schema) (context, + ns->ns_user_data, + context->user_data, + error); + g_free (ns->uri); + g_free (ns->prefix); + context->namespaces = g_slist_delete_link (context->namespaces, + context->namespaces); + if (context->namespaces == NULL) + break; + ns = context->namespaces->data; + } +} + +static gboolean +has_ns_prefix (const gchar *name, + XMLNameSpace *ns) +{ + if (ns == NULL) + return FALSE; + return ((strncmp (name, ns->prefix, ns->prefix_len) == 0) + && (name[ns->prefix_len] == ':')); +} + +static XMLNameSpace * +new_property_in_ns (XMPParseContext *context, + const gchar *element_name) +{ + GSList *list; + XMLNameSpace *ns; + + g_return_val_if_fail (context->property == NULL, NULL); + g_return_val_if_fail (context->prop_cur_value == -1, NULL); + /* element_name is a new property if it starts with a known prefix */ + for (list = context->namespaces; + list != NULL; + list = g_slist_next (list)) + { + ns = list->data; + if (has_ns_prefix (element_name, ns)) + { + context->property = g_strdup (element_name + ns->prefix_len + 1); + context->property_type = XMP_PTYPE_UNKNOWN; + context->property_ns = ns; + context->prop_missing_value = FALSE; + return ns; + } + } + return NULL; +} + +static void +add_property_value (XMPParseContext *context, + XMPParseType type, + gchar *name, + gchar *value) +{ + g_return_if_fail (context->property != NULL); + if (type == XMP_PTYPE_TEXT || type == XMP_PTYPE_RESOURCE) + g_return_if_fail (context->prop_cur_value < 0); + if (context->property_type != type) + { + g_return_if_fail (context->property_type == XMP_PTYPE_UNKNOWN); + context->property_type = type; + } + if (context->prop_cur_value + 3 >= context->prop_max_value) + { + context->prop_max_value += 10; + context->prop_value = g_realloc (context->prop_value, + sizeof (gchar *) + * context->prop_max_value); + } + if (type == XMP_PTYPE_ALT_LANG || type == XMP_PTYPE_STRUCTURE) + { + context->prop_cur_value++; + context->prop_value[context->prop_cur_value] = name; + } + context->prop_cur_value++; + context->prop_value[context->prop_cur_value] = value; + context->prop_value[context->prop_cur_value + 1] = NULL; + context->prop_missing_value = (value == NULL); +} + +static void +update_property_value (XMPParseContext *context, + gchar *value) +{ + g_return_if_fail (context->property != NULL); + g_return_if_fail (context->prop_cur_value >= 0); + g_return_if_fail (context->prop_missing_value == TRUE); + context->prop_value[context->prop_cur_value] = value; + context->prop_missing_value = FALSE; +} + +static void +propagate_property (XMPParseContext *context, + GError **error) +{ + g_return_if_fail (context->property != NULL); + g_return_if_fail (context->prop_cur_value >= 0); + if (context->parser->set_property) + (*context->parser->set_property) (context, + context->property, + context->property_type, + (const gchar **)(context->prop_value), + context->property_ns->ns_user_data, + context->user_data, + error); + if (! (context->flags & XMP_FLAG_DEFER_VALUE_FREE)) + { + while (context->prop_cur_value >= 0) + { + g_free (context->prop_value[context->prop_cur_value]); + context->prop_cur_value--; + } + g_free (context->prop_value); + } + context->prop_value = NULL; + context->prop_cur_value = -1; + context->prop_max_value = 0; + g_free (context->property); + context->property = NULL; + context->property_ns = NULL; +} + +/* called from the GMarkupParser */ +static void +start_element_handler (GMarkupParseContext *markup_context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + XMPParseContext *context = user_data; + gint attr; + + /* + printf ("[%02d/%02d] %d <%s>\n", context->state, context->saved_state, + context->depth, element_name); + */ + context->depth++; + for (attr = 0; attribute_names[attr] != NULL; ++attr) + if (g_str_has_prefix (attribute_names[attr], "xmlns:")) + push_namespace (context, attribute_values[attr], + attribute_names[attr] + sizeof ("xmlns:") - 1, error); + switch (context->state) + { + case STATE_INSIDE_XPACKET: + if (! strcmp (element_name, "x:xmpmeta") + || ! strcmp (element_name, "x:xapmeta")) + context->state = STATE_INSIDE_XMPMETA; + else if (! strcmp (element_name, "rdf:RDF")) + { + /* the x:xmpmeta element is missing, but this is allowed */ + context->depth++; + context->state = STATE_INSIDE_RDF; + } + else + parse_error_element (context, error, "x:xmpmeta", + FALSE, element_name); + break; + + case STATE_INSIDE_XMPMETA: + if (! strcmp (element_name, "rdf:RDF")) + context->state = STATE_INSIDE_RDF; + else + parse_error_element (context, error, "rdf:RDF", + FALSE, element_name); + break; + + case STATE_INSIDE_RDF: + if (! strcmp (element_name, "rdf:Description")) + { + XMLNameSpace *ns; + gboolean about_seen = FALSE; + + context->state = STATE_INSIDE_TOPLEVEL_DESC; + for (attr = 0; attribute_names[attr] != NULL; ++attr) + { + if (! strcmp (attribute_names[attr], "rdf:about")/* new style */ + || ! strcmp (attribute_names[attr], "about") /* old style */) + about_seen = TRUE; + else if (g_str_has_prefix (attribute_names[attr], "xmlns")) + ; /* the namespace has already been pushed on the stack */ + else + { + ns = new_property_in_ns (context, attribute_names[attr]); + if (ns != NULL) + { + /* RDF shorthand notation */ + add_property_value (context, XMP_PTYPE_TEXT, NULL, + g_strdup (attribute_values[attr])); + propagate_property (context, error); + } + else + unknown_attribute (context, error, element_name, + attribute_names[attr], + attribute_values[attr]); + } + } + if ((about_seen == FALSE) + && (context->flags & XMP_FLAG_NO_MISSING_ABOUT)) + parse_error (context, error, XMP_ERROR_MISSING_ABOUT, + _("Required attribute rdf:about missing in <%s>"), + element_name); + } + else + parse_error_element (context, error, "rdf:Description", + FALSE, element_name); + break; + + case STATE_INSIDE_TOPLEVEL_DESC: + { + XMLNameSpace *ns; + + ns = new_property_in_ns (context, element_name); + if (ns != NULL) + { + context->state = STATE_INSIDE_PROPERTY; + for (attr = 0; attribute_names[attr] != NULL; ++attr) + { + if (! strcmp (attribute_names[attr], "rdf:resource")) + add_property_value (context, XMP_PTYPE_RESOURCE, NULL, + g_strdup (attribute_values[attr])); + else if (! strcmp (attribute_names[attr], "rdf:parseType") + && ! strcmp (attribute_values[attr], "Resource")) + { + context->saved_state = STATE_INSIDE_TOPLEVEL_DESC; + context->state = STATE_INSIDE_STRUCT_ADD_NS; + } + else + unknown_attribute (context, error, element_name, + attribute_names[attr], + attribute_values[attr]); + } + } + else + unknown_element (context, error, element_name); + } + break; + + case STATE_INSIDE_PROPERTY: + if (! strcmp (element_name, "rdf:Description")) + { + context->saved_state = context->state; + context->state = STATE_INSIDE_QDESC; + for (attr = 0; attribute_names[attr] != NULL; ++attr) + { + if (g_str_has_prefix (attribute_names[attr], "xmlns")) + { + /* this desc. is a structure, not a property qualifier */ + context->state = STATE_INSIDE_STRUCT_ADD_NS; + } + else + unknown_attribute (context, error, element_name, + attribute_names[attr], + attribute_values[attr]); + } + } + else if (! strcmp (element_name, "rdf:Alt")) + context->state = STATE_INSIDE_ALT; + else if (! strcmp (element_name, "rdf:Bag")) + context->state = STATE_INSIDE_BAG; + else if (! strcmp (element_name, "rdf:Seq")) + context->state = STATE_INSIDE_SEQ; + else + unknown_element (context, error, element_name); + break; + + case STATE_INSIDE_QDESC: + if (! strcmp (element_name, "rdf:value")) + context->state = STATE_INSIDE_QDESC_VALUE; + else + context->state = STATE_INSIDE_QDESC_QUAL; + break; + + case STATE_INSIDE_STRUCT_ADD_NS: + case STATE_INSIDE_STRUCT: + { + GSList *ns_list; + XMLNameSpace *ns; + gboolean found; + + /* compare with namespaces in scope of current rdf:Description */ + found = FALSE; + ns_list = context->namespaces; + while (ns_list != NULL) + { + ns = ns_list->data; + if (ns->depth < context->depth - 2) + break; + if (has_ns_prefix (element_name, ns)) + { + if (context->state == STATE_INSIDE_STRUCT_ADD_NS) + { + /* first element - save the namespace prefix and uri */ + add_property_value (context, XMP_PTYPE_STRUCTURE, + g_strdup (ns->prefix), + g_strdup (ns->uri)); + } + context->state = STATE_INSIDE_STRUCT_ELEMENT; + add_property_value (context, XMP_PTYPE_STRUCTURE, + g_strdup (element_name + + ns->prefix_len + 1), + NULL); + found = TRUE; + break; + } + ns_list = ns_list->next; + } + if (found == FALSE) + unknown_element (context, error, element_name); + } + break; + + case STATE_INSIDE_ALT: + if (! strcmp (element_name, "rdf:li")) + { + context->state = STATE_INSIDE_ALT_LI; + for (attr = 0; attribute_names[attr] != NULL; ++attr) + { + if (! strcmp (attribute_names[attr], "xml:lang")) + add_property_value (context, XMP_PTYPE_ALT_LANG, + g_strdup (attribute_values[attr]), + NULL); + else + unknown_attribute (context, error, element_name, + attribute_names[attr], + attribute_values[attr]); + } + } + else + parse_error_element (context, error, "rdf:li", + FALSE, element_name); + break; + + case STATE_INSIDE_BAG: + if (! strcmp (element_name, "rdf:li")) + { + context->state = STATE_INSIDE_BAG_LI; + for (attr = 0; attribute_names[attr] != NULL; ++attr) + { + if (! strcmp (attribute_names[attr], "rdf:parseType") + && ! strcmp (attribute_values[attr], "Resource")) + context->state = STATE_INSIDE_BAG_LI_RSC; + else + unknown_attribute (context, error, element_name, + attribute_names[attr], + attribute_values[attr]); + } + if (context->state != STATE_INSIDE_BAG_LI_RSC) + add_property_value (context, XMP_PTYPE_UNORDERED_LIST, + NULL, NULL); + } + else + parse_error_element (context, error, "rdf:li", + FALSE, element_name); + break; + + case STATE_INSIDE_SEQ: + if (! strcmp (element_name, "rdf:li")) + { + context->state = STATE_INSIDE_SEQ_LI; + for (attr = 0; attribute_names[attr] != NULL; ++attr) + { + if (! strcmp (attribute_names[attr], "rdf:parseType") + && ! strcmp (attribute_values[attr], "Resource")) + context->state = STATE_INSIDE_SEQ_LI_RSC; + else + unknown_attribute (context, error, element_name, + attribute_names[attr], + attribute_values[attr]); + } + if (context->state != STATE_INSIDE_SEQ_LI_RSC) + add_property_value (context, XMP_PTYPE_ORDERED_LIST, + NULL, NULL); + } + else + parse_error_element (context, error, "rdf:li", + FALSE, element_name); + break; + + case STATE_INSIDE_BAG_LI: + case STATE_INSIDE_SEQ_LI: + if (! strcmp (element_name, "rdf:Description")) + { + context->saved_state = context->state; + context->state = STATE_INSIDE_QDESC; + } + else + parse_error_element (context, error, "rdf:Description", + TRUE, element_name); + break; + + case STATE_INSIDE_BAG_LI_RSC: + case STATE_INSIDE_SEQ_LI_RSC: + unknown_element (context, error, element_name); + break; + + case STATE_SKIPPING_UNKNOWN_ELEMENTS: + break; + + default: + parse_error (context, error, XMP_ERROR_PARSE, + _("Nested elements (<%s>) are not allowed in this context"), + element_name); + break; + } +} + +/* called from the GMarkupParser */ +static void +end_element_handler (GMarkupParseContext *markup_context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + XMPParseContext *context = user_data; + + /* + printf ("[%02d/%02d] %d \n", context->state, context->saved_state, + context->depth, element_name); + */ + switch (context->state) + { + case STATE_INSIDE_PROPERTY: + context->state = STATE_INSIDE_TOPLEVEL_DESC; + if (context->property != NULL) + { + if (context->prop_cur_value < 0) + { + /* if not set yet, then property was empty */ + add_property_value (context, XMP_PTYPE_TEXT, + NULL, g_strdup ("")); + } + propagate_property (context, error); + } + break; + + case STATE_INSIDE_STRUCT: + context->state = context->saved_state; + if (context->property != NULL) + propagate_property (context, error); + break; + + case STATE_INSIDE_ALT: + case STATE_INSIDE_BAG: + case STATE_INSIDE_SEQ: + context->state = STATE_INSIDE_PROPERTY; + break; + + case STATE_INSIDE_QDESC: + context->state = context->saved_state; + break; + + case STATE_INSIDE_QDESC_VALUE: + case STATE_INSIDE_QDESC_QUAL: + context->state = STATE_INSIDE_QDESC; + break; + + case STATE_INSIDE_STRUCT_ELEMENT: + context->state = STATE_INSIDE_STRUCT; + if (context->prop_value[context->prop_cur_value] == NULL) + update_property_value (context, g_strdup ("")); + break; + + case STATE_INSIDE_ALT_LI: + context->state = STATE_INSIDE_ALT; + if (context->prop_value[context->prop_cur_value] == NULL) + update_property_value (context, g_strdup ("")); + break; + + case STATE_INSIDE_BAG_LI: + case STATE_INSIDE_BAG_LI_RSC: + context->state = STATE_INSIDE_BAG; + break; + + case STATE_INSIDE_SEQ_LI: + case STATE_INSIDE_SEQ_LI_RSC: + context->state = STATE_INSIDE_SEQ; + break; + + case STATE_INSIDE_TOPLEVEL_DESC: + g_return_if_fail (! strcmp (element_name, "rdf:Description")); + context->state = STATE_INSIDE_RDF; + break; + + case STATE_INSIDE_RDF: + g_return_if_fail (! strcmp (element_name, "rdf:RDF")); + context->state = STATE_AFTER_RDF; + break; + + case STATE_AFTER_RDF: + g_return_if_fail (! strcmp (element_name, "x:xmpmeta") + || ! strcmp (element_name, "x:xapmeta")); + context->state = STATE_AFTER_XMPMETA; + break; + + case STATE_SKIPPING_UNKNOWN_ELEMENTS: + if (context->depth == context->saved_depth) + { + /* resume normal processing */ + context->state = context->saved_state; + context->saved_state = STATE_ERROR; + } + break; + + default: + parse_error (context, error, XMP_ERROR_PARSE, + _("End of element <%s> not expected in this context"), + element_name); + break; + } + pop_namespaces (context, error); + context->depth--; +} + +/* called from the GMarkupParser */ +static void +text_handler (GMarkupParseContext *markup_context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + XMPParseContext *context = user_data; + + switch (context->state) + { + case STATE_INSIDE_PROPERTY: + if (! is_whitespace_string (text)) + add_property_value (context, XMP_PTYPE_TEXT, NULL, + g_strndup (text, text_len)); + break; + + case STATE_INSIDE_STRUCT_ELEMENT: + case STATE_INSIDE_ALT_LI: + case STATE_INSIDE_BAG_LI: + case STATE_INSIDE_SEQ_LI: + if (! is_whitespace_string (text)) + update_property_value (context, g_strndup (text, text_len)); + break; + + case STATE_INSIDE_QDESC_VALUE: + if (! is_whitespace_string (text)) + { + if (context->saved_state == STATE_INSIDE_PROPERTY) + add_property_value (context, XMP_PTYPE_TEXT, NULL, + g_strndup (text, text_len)); + else + update_property_value (context, g_strndup (text, text_len)); + } + break; + + case STATE_INSIDE_QDESC_QUAL: + /* + printf ("ignoring qualifier for part of \"%s\"[]: \"%.*s\"\n", + context->property, + (int)text_len, text); + */ + /* FIXME: notify the user? add a way to collect qualifiers? */ + break; + + case STATE_SKIPPING_UNKNOWN_ELEMENTS: + break; + + default: + if (! is_whitespace_string (text)) + parse_error (context, error, XMP_ERROR_INVALID_CONTENT, + _("The current element (<%s>) cannot contain text"), + g_markup_parse_context_get_element (markup_context)); + break; + } +} + +/* called from the GMarkupParser */ +static void +passthrough_handler (GMarkupParseContext *markup_context, + const gchar *passthrough_text, + gsize text_len, + gpointer user_data, + GError **error) +{ + XMPParseContext *context = user_data; + + switch (context->state) + { + case STATE_START: + case STATE_AFTER_XPACKET: + if ((text_len >= 21) + && (! strncmp (passthrough_text, "state = STATE_INSIDE_XPACKET; + else + parse_error (context, error, XMP_ERROR_PARSE, + _("XMP packets must start with ")); + break; + + case STATE_AFTER_RDF: + /* the x:xmpmeta element is missing */ + context->depth--; + pop_namespaces (context, error); + /*fallthrough*/ + case STATE_AFTER_XMPMETA: + if ((text_len >= 19) + && (! strncmp (passthrough_text, "state = STATE_AFTER_XPACKET; + else + parse_error (context, error, XMP_ERROR_PARSE, + _("XMP packets must end with ")); + break; + + default: + if ((text_len >= 18) + && (! strncmp (passthrough_text, " */ + else if (! (context->flags & XMP_FLAG_NO_COMMENTS) + && (text_len > 7) + && (! strncmp (passthrough_text, "