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, "