[gegl] gegl, operations: add meta-data API



commit 72a9161e20f4b0d2b728e81e39c8ba36f6be79fe
Author: Brian Stafford <@briancs>
Date:   Thu May 7 01:57:12 2020 +0200

    gegl, operations: add meta-data API
    
    This adds GeglResolutionUnit, GeglMetaDataStore interface and a
    hashtable implementation, as suggested and refined in issue #222.
    
    Image loading and saving operations can now implement the "metadata"
    property. For loading ops this means extracting known metadata keys when
    processing, and for saving ops storing the applicable and set key.  The
    file operations can register maps of names of keys between its own
    naming scheme and the GeglMetaDataStore naming scheme, as well as
    provide conversion functions for rewriting formats, for instance for
    timestamps.

 docs/gegl-docs.xml              |    7 +
 gegl/gegl-metadata.c            |  140 +++++
 gegl/gegl-metadata.h            |  322 +++++++++++
 gegl/gegl-metadatahash.c        |  179 +++++++
 gegl/gegl-metadatahash.h        |   56 ++
 gegl/gegl-metadatastore.c       | 1119 +++++++++++++++++++++++++++++++++++++++
 gegl/gegl-metadatastore.h       |  724 +++++++++++++++++++++++++
 gegl/meson.build                |    6 +
 meson.build                     |    1 +
 operations/external/jpg-save.c  |  107 +++-
 operations/external/png-load.c  |   90 +++-
 operations/external/png-save.c  |  112 +++-
 operations/external/tiff-load.c |  119 +++++
 operations/external/tiff-save.c |   99 +++-
 14 files changed, 3075 insertions(+), 6 deletions(-)
---
diff --git a/docs/gegl-docs.xml b/docs/gegl-docs.xml
index 9ad656f3c..f451cf303 100644
--- a/docs/gegl-docs.xml
+++ b/docs/gegl-docs.xml
@@ -59,6 +59,13 @@
     <xi:include href="xml/gegl-op.xml" />
   </chapter>
 
+  <chapter id="Metadata">
+    <title>Image File Metadata</title>
+    <xi:include href="xml/gegl-metadata.xml" />
+    <xi:include href="xml/gegl-metadatastore.xml" />
+    <xi:include href="xml/gegl-metadatahash.xml" />
+  </chapter>
+
   <chapter id="Misc">
     <title>Misc</title>
     <xi:include href="xml/gegl-types.xml" />
diff --git a/gegl/gegl-metadata.c b/gegl/gegl-metadata.c
new file mode 100644
index 000000000..6da07e0ca
--- /dev/null
+++ b/gegl/gegl-metadata.c
@@ -0,0 +1,140 @@
+/* This file is part of GEGL
+ *
+ * GEGL 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 3 of the License, or (at your option) any later version.
+ *
+ * GEGL 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 GEGL; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * Copyright 2020 Brian Stafford
+ */
+
+#include "gegl-metadata.h"
+
+G_DEFINE_INTERFACE (GeglMetadata, gegl_metadata, G_TYPE_OBJECT)
+
+static void
+gegl_metadata_default_init (G_GNUC_UNUSED GeglMetadataInterface *iface)
+{
+}
+
+void
+gegl_metadata_register_map (GeglMetadata *metadata, const gchar *file_module,
+                            guint flags, const GeglMetadataMap *map, gsize n_map)
+{
+  GeglMetadataInterface *iface;
+
+  g_return_if_fail (GEGL_IS_METADATA (metadata));
+
+  iface = GEGL_METADATA_GET_IFACE (metadata);
+  g_return_if_fail (iface->register_map != NULL);
+  return (*iface->register_map) (metadata, file_module, flags, map, n_map);
+}
+
+void
+gegl_metadata_unregister_map (GeglMetadata *metadata)
+{
+  GeglMetadataInterface *iface;
+
+  g_return_if_fail (GEGL_IS_METADATA (metadata));
+
+  iface = GEGL_METADATA_GET_IFACE (metadata);
+  g_return_if_fail (iface->register_map != NULL);
+  return (*iface->register_map) (metadata, NULL, 0, NULL, 0);
+}
+
+gboolean
+gegl_metadata_set_resolution (GeglMetadata *metadata, GeglResolutionUnit unit,
+                              gfloat x, gfloat y)
+{
+  GeglMetadataInterface *iface;
+
+  g_return_val_if_fail (GEGL_IS_METADATA (metadata), FALSE);
+
+  iface = GEGL_METADATA_GET_IFACE (metadata);
+  g_return_val_if_fail (iface->set_resolution != NULL, FALSE);
+  return (*iface->set_resolution) (metadata, unit, x, y);
+}
+
+gboolean
+gegl_metadata_get_resolution (GeglMetadata *metadata, GeglResolutionUnit *unit,
+                              gfloat *x, gfloat *y)
+{
+  GeglMetadataInterface *iface;
+
+  g_return_val_if_fail (GEGL_IS_METADATA (metadata), FALSE);
+
+  iface = GEGL_METADATA_GET_IFACE (metadata);
+  g_return_val_if_fail (iface->get_resolution != NULL, FALSE);
+  return (*iface->get_resolution) (metadata, unit, x, y);
+}
+
+gboolean
+gegl_metadata_iter_lookup (GeglMetadata *metadata, GeglMetadataIter *iter,
+                           const gchar *key)
+{
+  GeglMetadataInterface *iface;
+
+  g_return_val_if_fail (GEGL_IS_METADATA (metadata), FALSE);
+
+  iface = GEGL_METADATA_GET_IFACE (metadata);
+  g_return_val_if_fail (iface->iter_lookup != NULL, FALSE);
+  return (*iface->iter_lookup) (metadata, iter, key);
+}
+
+void
+gegl_metadata_iter_init (GeglMetadata *metadata, GeglMetadataIter *iter)
+{
+  GeglMetadataInterface *iface;
+
+  g_return_if_fail (GEGL_IS_METADATA (metadata));
+
+  iface = GEGL_METADATA_GET_IFACE (metadata);
+  g_return_if_fail (iface->iter_init != NULL);
+  (*iface->iter_init) (metadata, iter);
+}
+
+const gchar *
+gegl_metadata_iter_next (GeglMetadata *metadata, GeglMetadataIter *iter)
+{
+  GeglMetadataInterface *iface;
+
+  g_return_val_if_fail (GEGL_IS_METADATA (metadata), FALSE);
+
+  iface = GEGL_METADATA_GET_IFACE (metadata);
+  g_return_val_if_fail (iface->iter_next != NULL, FALSE);
+  return (*iface->iter_next) (metadata, iter);
+}
+
+gboolean
+gegl_metadata_iter_set_value (GeglMetadata *metadata, GeglMetadataIter *iter,
+                              const GValue *value)
+{
+  GeglMetadataInterface *iface;
+
+  g_return_val_if_fail (GEGL_IS_METADATA (metadata), FALSE);
+
+  iface = GEGL_METADATA_GET_IFACE (metadata);
+  g_return_val_if_fail (iface->iter_set_value != NULL, FALSE);
+  return (*iface->iter_set_value) (metadata, iter, value);
+}
+
+gboolean
+gegl_metadata_iter_get_value (GeglMetadata *metadata, GeglMetadataIter *iter,
+                              GValue *value)
+{
+  GeglMetadataInterface *iface;
+
+  g_return_val_if_fail (GEGL_IS_METADATA (metadata), FALSE);
+
+  iface = GEGL_METADATA_GET_IFACE (metadata);
+  g_return_val_if_fail (iface->iter_get_value != NULL, FALSE);
+  return (*iface->iter_get_value) (metadata, iter, value);
+}
diff --git a/gegl/gegl-metadata.h b/gegl/gegl-metadata.h
new file mode 100644
index 000000000..f116e2a4d
--- /dev/null
+++ b/gegl/gegl-metadata.h
@@ -0,0 +1,322 @@
+/* This file is part of GEGL
+ *
+ * GEGL 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 3 of the License, or (at your option) any later version.
+ *
+ * GEGL 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 GEGL; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * Copyright 2020 Brian Stafford
+ */
+
+#ifndef __GEGL_METADATA_H__
+#define __GEGL_METADATA_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/**
+ * SECTION:gegl-metadata
+ * @title: GeglMetadata
+ * @short_description: A metadata interface for use with file modules.
+ * @see_also: #GeglMetadataStore #GeglMetadataHash
+ *
+ * Objects which need to store or retrieve image metadata when saving and
+ * loading image files should implement GeglMetadata. The object should be cast
+ * with GEGL_METADATA() and passed to the file load or save module via the
+ * `metadata` property. Image file modules should not implement the metadata
+ * property if either the module or file format does not support metadata.
+ *
+ * Gegl understands (but is not limited to) the following well-known metadata
+ * variables:
+ *
+ * - artist: Name of image creator.
+ * - comment: Miscellaneous comment; conversion from GIF comment.
+ * - copyright: Copyright notice.
+ * - description: Description of image (possibly long).
+ * - disclaimer: Legal disclaimer.
+ * - software: Software used to create the image.
+ * - source: Device used to create the image.
+ * - timestamp: Time of original image creation.
+ * - title: Short (one line) title or caption for image.
+ * - warning: Warning of nature of content.
+ *
+ * The Gegl Metadata subsystem can be used in one of three ways described
+ * below in order of increasing complexity:
+ *
+ * 1. Recommended: Create a #GeglMetadataHash and pass it to a file loader
+ *    or saver via its `metadata` property. #GeglMetadataHash is a subclass of
+ *    #GeglMetadataStore which saves metadata in a hash table but which adds no
+ *    new properties or methods.  Image file metadata to be retrieved or saved
+ *    is accessed via #GeglMetadataStore properties or methods. Metadata values
+ *    not directly supported by Gegl may be declared using a #GParamSpec.
+ * 2. Subclass #GeglMetadataStore. This may be useful if an application stores
+ *    metadata in internal structures which may be accessed via the subclass.
+ *    The subclass is used identically to #GeglMetadataHash.
+ *    #GeglMetadataStore aims to be sufficiently flexible to cover the majority
+ *    of application requirements.
+ * 3. Implement the #GeglMetadata interface. This option should only be used if
+ *    #GeglMetadataStore cannot adequately satisfy application requirements.
+ *    Particular attention should be paid to semantics of the interface methods
+ *    as the file modules interact directly with these.
+ *
+ * For more complex requirements than provided by the metadata subsystem it is
+ * probably better to use a library such as `exiv2` or similar.
+ */
+
+/**
+ * GeglResolutionUnit:
+ * @GEGL_RESOLUTION_UNIT_NONE: Unknown or resolution not applicable.
+ * @GEGL_RESOLUTION_UNIT_DPI: Dots or pixels per inch.
+ * @GEGL_RESOLUTION_UNIT_DPM: Dots or pixels per metre.
+ *
+ * An enumerated type specifying resolution (density) units.  If resolution
+ * units are unknown, X and Y resolution specify the pixel aspect ratio.
+ */
+typedef enum
+  {
+    GEGL_RESOLUTION_UNIT_NONE,
+    GEGL_RESOLUTION_UNIT_DPI,
+    GEGL_RESOLUTION_UNIT_DPM
+  }
+GeglResolutionUnit;
+
+/**
+ * GeglMapFlags:
+ * @GEGL_MAP_EXCLUDE_UNMAPPED: Prevent further mapping from being registered.
+ *
+ * Flags controlling the mapping strategy.
+ */
+typedef enum _GeglMapFlags
+  {
+    GEGL_MAP_EXCLUDE_UNMAPPED = 1
+  }
+GeglMapFlags;
+
+
+#define GEGL_TYPE_METADATA          (gegl_metadata_get_type ())
+G_DECLARE_INTERFACE (GeglMetadata, gegl_metadata, GEGL, METADATA, GObject)
+
+/**
+ * GeglMetadataMap:
+ * @local_name: Name of metadata variable used in the file module.
+ * @name: Standard metadata variable name used by Gegl.
+ * @transform: Optional #GValue transform function.
+ *
+ * Struct to describe how a metadata variable is mapped from the name used by
+ * the image file module to the name used by Gegl.  An optional transform
+ * function may be specified, e.g. to transform from a #GDatetime to a string.
+ */
+typedef struct _GeglMetadataMap {
+  const gchar *local_name;
+  const gchar *name;
+  GValueTransform transform;
+} GeglMetadataMap;
+
+/**
+ * GeglMetadataIter:
+ *
+ * An opaque type representing a metadata iterator.
+ */
+typedef struct {
+  /*< private >*/
+  guint       stamp;
+  gpointer    user_data;
+  gpointer    user_data2;
+  gpointer    user_data3;
+} GeglMetadataIter;
+
+/**
+ * GeglMetadataInterface:
+ * @register_map: See gegl_metadata_register_map().  If called with a NULL map,
+ * the registration is deleted.
+ * @set_resolution: See gegl_metadata_set_resolution().
+ * @get_resolution: See gegl_metadata_get_resolution().
+ * @iter_lookup: See gegl_metadata_iter_lookup().
+ * @iter_init: See gegl_metadata_iter_init().
+ * @iter_next: See gegl_metadata_iter_next().
+ * @iter_set_value: See gegl_metadata_iter_set_value().
+ * @iter_get_value: See gegl_metadata_iter_get_value().
+ *
+ * The #GeglMetadata interface structure.
+ */
+struct _GeglMetadataInterface
+{
+  /*< private >*/
+  GTypeInterface base_iface;
+
+  void        (*register_map)         (GeglMetadata *metadata,
+                                       const gchar *file_module,
+                                       guint flags,
+                                       const GeglMetadataMap *map,
+                                       gsize n_map);
+
+  /*< public >*/
+  gboolean    (*set_resolution)       (GeglMetadata *metadata,
+                                       GeglResolutionUnit unit,
+                                       gfloat x, gfloat y);
+  gboolean    (*get_resolution)       (GeglMetadata *metadata,
+                                       GeglResolutionUnit *unit,
+                                       gfloat *x, gfloat *y);
+
+  gboolean    (*iter_lookup)          (GeglMetadata *metadata,
+                                       GeglMetadataIter *iter,
+                                       const gchar *key);
+  void        (*iter_init)            (GeglMetadata *metadata,
+                                       GeglMetadataIter *iter);
+  const gchar *(*iter_next)           (GeglMetadata *metadata,
+                                       GeglMetadataIter *iter);
+  gboolean    (*iter_set_value)       (GeglMetadata *metadata,
+                                       GeglMetadataIter *iter,
+                                       const GValue *value);
+  gboolean    (*iter_get_value)       (GeglMetadata *metadata,
+                                       GeglMetadataIter *iter,
+                                       GValue *value);
+};
+
+/**
+ * gegl_metadata_register_map:
+ * @metadata:   The #GeglMetadata interface
+ * @file_module: String identifying the file module, e.g, `"gegl:png-save"`
+ * @flags:      Flags specifying capabilities of underlying file format
+ * @map: (array length=n_map): Array of mappings from file module metadata
+ *              names to Gegl well-known names.
+ * @n_map:      Number of entries in @map
+ *
+ * Set the name of the file module and pass an array of mappings from
+ * file-format specific metadata names to those used by Gegl. A GValue
+ * transformation function may be supplied, e.g. to parse or format timestamps.
+ */
+void            gegl_metadata_register_map      (GeglMetadata *metadata,
+                                                 const gchar *file_module,
+                                                 guint flags,
+                                                 const GeglMetadataMap *map,
+                                                 gsize n_map);
+
+/**
+ * gegl_metadata_unregister_map:
+ * @metadata:   The #GeglMetadata interface
+ *
+ * Unregister the file module mappings and any further mappings added or
+ * modified by the application.  This should be called after the file module
+ * completes operations.
+ */
+void            gegl_metadata_unregister_map    (GeglMetadata *metadata);
+
+/**
+ * gegl_metadata_set_resolution:
+ * @metadata:   The #GeglMetadata interface
+ * @unit:       Specify #GeglResolutionUnit
+ * @x:          X resolution
+ * @y:          Y resolution
+ *
+ * Set resolution retrieved from image file's metadata.  Intended for use by
+ * the image file reader.  If resolution is not supported by the application or
+ * if the operation fails %FALSE is returned and the values are ignored.
+ *
+ * Returns:     %TRUE if successful.
+ */
+gboolean        gegl_metadata_set_resolution    (GeglMetadata *metadata,
+                                                 GeglResolutionUnit unit,
+                                                 gfloat x, gfloat y);
+
+/**
+ * gegl_metadata_get_resolution:
+ * @metadata:   The #GeglMetadata interface
+ * @unit:       #GeglResolutionUnit return location
+ * @x:          X resolution return location
+ * @y:          Y resolution return location
+ *
+ * Retrieve resolution from the application image metadata.  Intended for use
+ * by the image file writer.  If resolution is not supported by the application
+ * or if the operation fails %FALSE is returned and the resolution values are
+ * not updated.
+ *
+ * Returns:     %TRUE if successful.
+ */
+gboolean        gegl_metadata_get_resolution    (GeglMetadata *metadata,
+                                                 GeglResolutionUnit *unit,
+                                                 gfloat *x, gfloat *y);
+
+/**
+ * gegl_metadata_iter_lookup:
+ * @metadata:   The #GeglMetadata interface
+ * @iter:       #GeglMetadataIter to be initialised
+ * @key:        Name of the value look up
+ *
+ * Look up the specified key and initialise an iterator to reference the
+ * associated metadata. The iterator is used in conjunction with
+ * gegl_metadata_set_value() and gegl_metadata_get_value(). Note that this
+ * iterator is not valid for gegl_metadata_iter_next().
+ *
+ * Returns:     %TRUE if key is found.
+ */
+gboolean        gegl_metadata_iter_lookup       (GeglMetadata *metadata,
+                                                 GeglMetadataIter *iter,
+                                                 const gchar *key);
+
+/**
+ * gegl_metadata_iter_init:
+ * @metadata:   The #GeglMetadata interface
+ * @iter:       #GeglMetadataIter to be initialised
+ *
+ * Initialise an iterator to find all supported metadata keys.
+ */
+void            gegl_metadata_iter_init         (GeglMetadata *metadata,
+                                                 GeglMetadataIter *iter);
+
+/**
+ * gegl_metadata_iter_next:
+ * @metadata:   The #GeglMetadata interface
+ * @iter:       #GeglMetadataIter to be updated
+ *
+ * Move the iterator to the next metadata item
+ *
+ * Returns:     key name if found, else %NULL
+ */
+const gchar    *gegl_metadata_iter_next         (GeglMetadata *metadata,
+                                                 GeglMetadataIter *iter);
+
+/**
+ * gegl_metadata_iter_set_value:
+ * @metadata:   The #GeglMetadata interface
+ * @iter:       #GeglMetadataIter referencing the value to set
+ * @value:      Value to set in the interface
+ *
+ * Set application data retrieved from image file's metadata.  Intended for use
+ * by the image file reader.  If the operation fails it returns %FALSE and
+ * @value is ignored.
+ *
+ * Returns:     %TRUE if successful.
+ */
+gboolean        gegl_metadata_iter_set_value    (GeglMetadata *metadata,
+                                                 GeglMetadataIter *iter,
+                                                 const GValue *value);
+
+/**
+ * gegl_metadata_iter_get_value:
+ * @metadata:   The #GeglMetadata interface
+ * @iter:       #GeglMetadataIter referencing the value to get
+ * @value:      Value to set in the interface
+ *
+ * Retrieve image file metadata from the application.  Intended for use by the
+ * image file writer. If the operation fails it returns %FALSE and @value is
+ * not updated.
+ *
+ * Returns:     %TRUE if successful.
+ */
+gboolean        gegl_metadata_iter_get_value    (GeglMetadata *metadata,
+                                                 GeglMetadataIter *iter,
+                                                 GValue *value);
+
+G_END_DECLS
+
+#endif
diff --git a/gegl/gegl-metadatahash.c b/gegl/gegl-metadatahash.c
new file mode 100644
index 000000000..0fe6c9aa9
--- /dev/null
+++ b/gegl/gegl-metadatahash.c
@@ -0,0 +1,179 @@
+/* This file is part of GEGL
+ *
+ * GEGL 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 3 of the License, or (at your option) any later version.
+ *
+ * GEGL 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 GEGL; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * Copyright 2020 Brian Stafford
+ */
+
+#include "gegl-metadatahash.h"
+
+struct _GeglMetadataHash
+  {
+    GeglMetadataStore parent_instance;
+
+    GHashTable *store;
+  };
+
+G_DEFINE_TYPE (GeglMetadataHash, gegl_metadata_hash, GEGL_TYPE_METADATA_STORE)
+
+typedef struct _GeglMetadataValue GeglMetadataValue;
+struct _GeglMetadataValue
+  {
+    GValue value;
+    GParamSpec *pspec;
+    gboolean shadow;
+  };
+
+/* GObject {{{1 */
+
+GeglMetadataStore *
+gegl_metadata_hash_new (void)
+{
+  return g_object_new (GEGL_TYPE_METADATA_HASH, NULL);
+}
+
+static void gegl_metadata_hash_finalize (GObject *object);
+
+/* GObject {{{1 */
+
+static void     gegl_metadata_hash_declare (GeglMetadataStore *store, GParamSpec *pspec, gboolean shadow);
+static gboolean gegl_metadata_hash_has_value (GeglMetadataStore *self, const gchar *name);
+static GParamSpec *gegl_metadata_hash_pspec (GeglMetadataStore *self, const gchar *name);
+static void     gegl_metadata_hash_set_value (GeglMetadataStore *self, const gchar *name, const GValue 
*value);
+static const GValue *gegl_metadata_hash_get_value (GeglMetadataStore *self, const gchar *name);
+
+static void
+gegl_metadata_hash_class_init (GeglMetadataHashClass *class)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (class);
+  GeglMetadataStoreClass *store_class = GEGL_METADATA_STORE_CLASS (class);
+
+  gobject_class->finalize = gegl_metadata_hash_finalize;
+
+  store_class->_declare = gegl_metadata_hash_declare;
+  store_class->has_value = gegl_metadata_hash_has_value;
+  store_class->pspec = gegl_metadata_hash_pspec;
+  store_class->set_value = gegl_metadata_hash_set_value;
+  store_class->_get_value = gegl_metadata_hash_get_value;
+}
+
+static void
+metadata_value_free (gpointer data)
+{
+  GeglMetadataValue *meta = data;
+
+  g_param_spec_unref (meta->pspec);
+  g_value_unset (&meta->value);
+}
+
+static void
+gegl_metadata_hash_init (GeglMetadataHash *self)
+{
+  self->store = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                       g_free, metadata_value_free);
+}
+
+static void
+gegl_metadata_hash_finalize (GObject *object)
+{
+  GeglMetadataHash *self = (GeglMetadataHash *) object;
+
+  g_hash_table_unref (self->store);
+  G_OBJECT_CLASS (gegl_metadata_hash_parent_class)->finalize (object);
+}
+
+/* Declare metadata {{{1 */
+
+static void
+gegl_metadata_hash_declare (GeglMetadataStore *store,
+                                      GParamSpec *pspec, gboolean shadow)
+{
+  GeglMetadataHash *self = (GeglMetadataHash *) store;
+  GeglMetadataValue *meta;
+  const gchar *name;
+
+  meta = g_slice_new0 (GeglMetadataValue);
+  meta->shadow = shadow;
+  meta->pspec = pspec;
+
+  name = g_param_spec_get_name (meta->pspec);
+  g_hash_table_replace (self->store, g_strdup (name), meta);
+}
+
+/* Metadata accessors {{{1 */
+
+static gboolean
+gegl_metadata_hash_has_value (GeglMetadataStore *store, const gchar *name)
+{
+  GeglMetadataHash *self = (GeglMetadataHash *) store;
+  GeglMetadataValue *meta;
+
+  g_return_val_if_fail (GEGL_IS_METADATA_HASH (self), FALSE);
+
+  meta = g_hash_table_lookup (self->store, name);
+  return meta != NULL && G_IS_VALUE (&meta->value);
+}
+
+static GParamSpec *
+gegl_metadata_hash_pspec (GeglMetadataStore *store, const gchar *name)
+{
+  GeglMetadataHash *self = (GeglMetadataHash *) store;
+  GeglMetadataValue *meta;
+
+  g_return_val_if_fail (GEGL_IS_METADATA_HASH (self), FALSE);
+
+  meta = g_hash_table_lookup (self->store, name);
+  return meta != NULL ? meta->pspec : NULL;
+}
+
+/* set/get by GValue */
+
+static void
+gegl_metadata_hash_set_value (GeglMetadataStore *store, const gchar *name,
+                              const GValue *value)
+{
+  GeglMetadataHash *self = (GeglMetadataHash *) store;
+  GeglMetadataValue *meta;
+  gboolean success;
+
+  g_return_if_fail (GEGL_IS_METADATA_HASH (self));
+
+  meta = g_hash_table_lookup (self->store, name);
+  g_return_if_fail (meta != NULL);
+
+  if (!G_IS_VALUE (&meta->value))
+    g_value_init (&meta->value, G_PARAM_SPEC_VALUE_TYPE (meta->pspec));
+
+  if (value != NULL)
+    success = g_param_value_convert (meta->pspec, value, &meta->value, FALSE);
+  else
+    {
+      g_param_value_set_default (meta->pspec, &meta->value);
+      success = TRUE;
+    }
+  if (success)
+    gegl_metadata_store_notify (store, meta->pspec, meta->shadow);
+}
+
+static const GValue *
+gegl_metadata_hash_get_value (GeglMetadataStore *store, const gchar *name)
+{
+  GeglMetadataHash *self = (GeglMetadataHash *) store;
+  GeglMetadataValue *meta;
+
+  g_return_val_if_fail (GEGL_IS_METADATA_HASH (self), NULL);
+
+  meta = g_hash_table_lookup (self->store, name);
+  return meta != NULL && G_IS_VALUE (&meta->value) ? &meta->value : NULL;
+}
diff --git a/gegl/gegl-metadatahash.h b/gegl/gegl-metadatahash.h
new file mode 100644
index 000000000..ba127a8b4
--- /dev/null
+++ b/gegl/gegl-metadatahash.h
@@ -0,0 +1,56 @@
+/* This file is part of GEGL
+ *
+ * GEGL 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 3 of the License, or (at your option) any later version.
+ *
+ * GEGL 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 GEGL; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * Copyright 2020 Brian Stafford
+ */
+
+#ifndef _geglmetadatahash_h
+#define _geglmetadatahash_h
+
+#include <glib.h>
+#include "gegl-metadatastore.h"
+
+G_BEGIN_DECLS
+
+#define GEGL_TYPE_METADATA_HASH   gegl_metadata_hash_get_type ()
+G_DECLARE_FINAL_TYPE (
+        GeglMetadataHash,
+        gegl_metadata_hash,
+        GEGL, METADATA_HASH,
+        GeglMetadataStore
+)
+
+/**
+ * SECTION:gegl-metadatahash
+ * @title: GeglMetadataHash
+ * @short_description: A metadata store object for use with file modules.
+ * @see_also: #GeglMetadata #GeglMetadataStore
+ *
+ * #GeglMetadataHash is a #GeglMetadataStore implementing the data store using
+ * a hash table. It adds no new methods or properties to #GeglMetadataStore.
+ */
+
+/**
+ * gegl_metadata_hash_new:
+ *
+ * Create a new #GeglMetadataHash
+ *
+ * Returns: (transfer full): New #GeglMetadataHash cast to #GeglMetadataStore
+ */
+GeglMetadataStore *       gegl_metadata_hash_new        (void);
+
+G_END_DECLS
+
+#endif
diff --git a/gegl/gegl-metadatastore.c b/gegl/gegl-metadatastore.c
new file mode 100644
index 000000000..098148811
--- /dev/null
+++ b/gegl/gegl-metadatastore.c
@@ -0,0 +1,1119 @@
+/* This file is part of GEGL
+ *
+ * GEGL 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 3 of the License, or (at your option) any later version.
+ *
+ * GEGL 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 GEGL; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * Copyright 2020 Brian Stafford
+ */
+
+#include "gegl-metadatastore.h"
+
+typedef struct _GeglMetadataStorePrivate GeglMetadataStorePrivate;
+struct _GeglMetadataStorePrivate
+  {
+    gchar *file_module_name;
+
+    /* Resolution */
+    GeglResolutionUnit resolution_unit;
+    gdouble resolution_x;
+    gdouble resolution_y;
+
+    /* Temporary name map used by file module */
+    GPtrArray *map;
+    gboolean exclude_unmapped;
+  };
+
+/* Register GeglResolutionUnit with GType {{{1 */
+
+GType
+gegl_resolution_unit_get_type (void)
+{
+  GType type;
+  const gchar *name;
+  static gsize gegl_resolution_unit_type;
+  static const GEnumValue values[] =
+    {
+      { GEGL_RESOLUTION_UNIT_NONE, "GEGL_RESOLUTION_UNIT_NONE", "none" },
+      { GEGL_RESOLUTION_UNIT_DPI,  "GEGL_RESOLUTION_UNIT_DPI",  "dpi" },
+      { GEGL_RESOLUTION_UNIT_DPM,  "GEGL_RESOLUTION_UNIT_DPM",  "dpm" },
+      { 0, NULL, NULL }
+    };
+
+  if (g_once_init_enter (&gegl_resolution_unit_type))
+    {
+      name = g_intern_static_string ("GeglResolutionUnit");
+      type = g_enum_register_static (name, values);
+      g_once_init_leave (&gegl_resolution_unit_type, type);
+    }
+  return gegl_resolution_unit_type;
+}
+
+/* GObject {{{1 */
+
+static void gegl_metadata_store_interface_init (GeglMetadataInterface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (
+  GeglMetadataStore, gegl_metadata_store, G_TYPE_OBJECT,
+  G_ADD_PRIVATE (GeglMetadataStore)
+  G_IMPLEMENT_INTERFACE (GEGL_TYPE_METADATA, gegl_metadata_store_interface_init)
+)
+
+enum
+  {
+    CHANGED,
+    MAPPED,
+    UNMAPPED,
+    GENERATE,
+    PARSE,
+    LAST_SIGNAL
+  };
+static guint gegl_metadata_store_signals[LAST_SIGNAL];
+
+enum
+  {
+    PROP_0,
+    PROP_RESOLUTION_UNIT,
+    PROP_RESOLUTION_X,
+    PROP_RESOLUTION_Y,
+    PROP_FILE_MODULE_NAME,
+
+    PROP_METADATA_SHADOW,
+    PROP_TITLE = PROP_METADATA_SHADOW,
+    PROP_ARTIST,
+    PROP_DESCRIPTION,
+    PROP_COPYRIGHT,
+    PROP_DISCLAIMER,
+    PROP_WARNING,
+    PROP_COMMENT,
+    PROP_SOFTWARE,
+    PROP_SOURCE,
+    PROP_TIMESTAMP,
+    N_PROPERTIES
+  };
+static GParamSpec *gegl_metadata_store_prop[N_PROPERTIES];
+
+static void gegl_metadata_store_constructed (GObject *object);
+static void gegl_metadata_store_finalize (GObject *object);
+static void gegl_metadata_store_get_property (GObject *object, guint param_id,
+                                              GValue *value, GParamSpec *pspec);
+static void gegl_metadata_store_set_property (GObject *object, guint param_id,
+                                              const GValue *value, GParamSpec *pspec);
+static GParamSpec *gegl_metadata_store_value_pspec (GeglMetadataStore *self,
+                                                    const gchar *name);
+
+/* inherited virtual methods */
+static void gegl_metadata_store_register_hook (GeglMetadataStore *self,
+                                               const gchar *file_module_name,
+                                               guint flags);
+static gboolean gegl_metadata_store_parse_value (GeglMetadataStore *self,
+                                                 GParamSpec *pspec,
+                                                 GValueTransform transform,
+                                                 const GValue *value);
+static gboolean gegl_metadata_store_generate_value (GeglMetadataStore *self,
+                                                    GParamSpec *pspec,
+                                                    GValueTransform transform,
+                                                    GValue *value);
+
+/* GObject {{{1 */
+
+static void
+gegl_metadata_store_class_init (GeglMetadataStoreClass *class)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (class);
+
+  gobject_class->constructed = gegl_metadata_store_constructed;
+  gobject_class->finalize = gegl_metadata_store_finalize;
+  gobject_class->set_property = gegl_metadata_store_set_property;
+  gobject_class->get_property = gegl_metadata_store_get_property;
+
+  class->register_hook = gegl_metadata_store_register_hook;
+  class->parse_value = gegl_metadata_store_parse_value;
+  class->generate_value = gegl_metadata_store_generate_value;
+
+  gegl_metadata_store_prop[PROP_RESOLUTION_UNIT] = g_param_spec_enum (
+        "resolution-unit", "Resolution Unit",
+        "Units for image resolution",
+        GEGL_TYPE_RESOLUTION_UNIT, GEGL_RESOLUTION_UNIT_DPI,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_RESOLUTION_X] = g_param_spec_double (
+        "resolution-x", "Resolution X",
+        "X Resolution",
+        -G_MAXDOUBLE, G_MAXDOUBLE, 300.0,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_RESOLUTION_Y] = g_param_spec_double (
+        "resolution-y", "Resolution Y",
+        "X Resolution",
+        -G_MAXDOUBLE, G_MAXDOUBLE, 300.0,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_FILE_MODULE_NAME] = g_param_spec_string (
+        "file-module-name", "File Module Name",
+        "Name of currently active file module or NULL",
+        NULL,
+        G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  gegl_metadata_store_prop[PROP_TITLE] = g_param_spec_string (
+        "title", "Title",
+        "Short title or caption",
+        NULL,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_ARTIST] = g_param_spec_string (
+        "artist", "Artist",
+        "Name of image creator",
+        NULL,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_DESCRIPTION] = g_param_spec_string (
+        "description", "Description",
+        "Description of image (possibly long)",
+        NULL,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_COPYRIGHT] = g_param_spec_string (
+        "copyright", "Copyright",
+        "Copyright notice",
+        NULL,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_DISCLAIMER] = g_param_spec_string (
+        "disclaimer", "Disclaimer",
+        "Legal disclaimer",
+        NULL,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_WARNING] = g_param_spec_string (
+        "warning", "Warning",
+        "Warning of nature of content",
+        NULL,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_COMMENT] = g_param_spec_string (
+        "comment", "Comment",
+        "Miscellaneous comment",
+        NULL,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_SOFTWARE] = g_param_spec_string (
+        "software", "Software",
+        "Software used to create the image",
+        NULL,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_SOURCE] = g_param_spec_string (
+        "source", "Source",
+        "Device used to create the image",
+        NULL,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+  gegl_metadata_store_prop[PROP_TIMESTAMP] = g_param_spec_boxed (
+        "timestamp", "Timestamp",
+        "Image creation time",
+        G_TYPE_DATE_TIME,
+        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, N_PROPERTIES, gegl_metadata_store_prop);
+
+  gegl_metadata_store_signals[CHANGED] = g_signal_new (
+        "changed",
+        G_TYPE_FROM_CLASS (class), G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST, 0,
+        NULL, NULL,
+        NULL, G_TYPE_NONE, 1, G_TYPE_PARAM);
+  gegl_metadata_store_signals[MAPPED] = g_signal_new (
+        "mapped",
+        G_TYPE_FROM_CLASS (class), G_SIGNAL_RUN_LAST, 0,
+        NULL, NULL,
+        NULL, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_BOOLEAN);
+  gegl_metadata_store_signals[UNMAPPED] = g_signal_new (
+        "unmapped",
+        G_TYPE_FROM_CLASS (class), G_SIGNAL_RUN_LAST, 0,
+        NULL, NULL,
+        NULL, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING);
+  gegl_metadata_store_signals[GENERATE] = g_signal_new (
+        "generate-value",
+        G_TYPE_FROM_CLASS (class), G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST, 0,
+        g_signal_accumulator_first_wins, NULL,
+        NULL, G_TYPE_BOOLEAN, 2, G_TYPE_PARAM, G_TYPE_VALUE);
+  gegl_metadata_store_signals[PARSE] = g_signal_new (
+        "parse-value",
+        G_TYPE_FROM_CLASS (class), G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST, 0,
+        g_signal_accumulator_first_wins, NULL,
+        NULL, G_TYPE_BOOLEAN, 2, G_TYPE_PARAM, G_TYPE_VALUE);
+}
+
+/* Make vmethods easier to read and type */
+#define VMETHOD(self,name) (*GEGL_METADATA_STORE_GET_CLASS ((self))->name)
+
+static void
+gegl_metadata_store_init (GeglMetadataStore *self)
+{
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+
+  priv->resolution_unit = GEGL_RESOLUTION_UNIT_DPI;
+  priv->resolution_x = 300.0;
+  priv->resolution_y = 300.0;
+}
+
+static void
+gegl_metadata_store_constructed (GObject *object)
+{
+  GeglMetadataStore *self = (GeglMetadataStore *) object;
+  guint i;
+  GParamSpec *pspec;
+
+  /* Shadow well-known metadata values with properties */
+  for (i = PROP_METADATA_SHADOW; i < N_PROPERTIES; i++)
+    {
+      pspec = g_param_spec_ref (gegl_metadata_store_prop[i]);
+      VMETHOD(self, _declare) (self, pspec, TRUE);
+    }
+
+  G_OBJECT_CLASS (gegl_metadata_store_parent_class)->constructed (object);
+}
+
+static void
+gegl_metadata_store_finalize (GObject *object)
+{
+  //GeglMetadataStore *self = (GeglMetadataStore *) object;
+
+  G_OBJECT_CLASS (gegl_metadata_store_parent_class)->finalize (object);
+}
+
+/* Properties {{{1 */
+
+static void
+gegl_metadata_store_set_property (GObject *object, guint prop_id,
+                                  const GValue *value, GParamSpec *pspec)
+{
+  GeglMetadataStore *self = GEGL_METADATA_STORE (object);
+  const gchar *name;
+
+  switch (prop_id)
+    {
+    case PROP_RESOLUTION_UNIT:
+      gegl_metadata_store_set_resolution_unit (self, g_value_get_enum (value));
+      break;
+    case PROP_RESOLUTION_X:
+      gegl_metadata_store_set_resolution_x (self, g_value_get_double (value));
+      break;
+    case PROP_RESOLUTION_Y:
+      gegl_metadata_store_set_resolution_y (self, g_value_get_double (value));
+      break;
+    default:
+      name = g_param_spec_get_name (pspec);
+      gegl_metadata_store_set_value (self, name, value);
+      return;
+    }
+}
+
+
+static void
+gegl_metadata_store_get_property (GObject *object, guint prop_id,
+                                  GValue *value, GParamSpec *pspec)
+{
+  GeglMetadataStore *self = GEGL_METADATA_STORE (object);
+  const gchar *name;
+
+  switch (prop_id)
+    {
+    case PROP_RESOLUTION_UNIT:
+      g_value_set_enum (value, gegl_metadata_store_get_resolution_unit (self));
+      break;
+    case PROP_RESOLUTION_X:
+      g_value_set_double (value, gegl_metadata_store_get_resolution_x (self));
+      break;
+    case PROP_RESOLUTION_Y:
+      g_value_set_double (value, gegl_metadata_store_get_resolution_y (self));
+      break;
+    case PROP_FILE_MODULE_NAME:
+      g_value_set_string (value, gegl_metadata_store_get_file_module_name (self));
+      break;
+    default:
+      name = g_param_spec_get_name (pspec);
+      gegl_metadata_store_get_value (self, name, value);
+      break;
+    }
+}
+
+/* "resolution-unit" {{{2 */
+
+void
+gegl_metadata_store_set_resolution_unit (GeglMetadataStore *self,
+                                         GeglResolutionUnit unit)
+{
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+
+  g_return_if_fail (GEGL_IS_METADATA_STORE (self));
+
+  if (priv->resolution_unit != unit)
+    {
+      priv->resolution_unit = unit;
+      g_object_notify_by_pspec (G_OBJECT (self), gegl_metadata_store_prop[PROP_RESOLUTION_UNIT]);
+    }
+}
+
+GeglResolutionUnit
+gegl_metadata_store_get_resolution_unit (GeglMetadataStore *self)
+{
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+
+  g_return_val_if_fail (GEGL_IS_METADATA_STORE (self), GEGL_RESOLUTION_UNIT_DPI);
+
+  return priv->resolution_unit;
+}
+
+/* "resolution-x" {{{2 */
+
+void
+gegl_metadata_store_set_resolution_x (GeglMetadataStore *self, gdouble resolution_x)
+{
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+
+  g_return_if_fail (GEGL_IS_METADATA_STORE (self));
+
+  if (priv->resolution_x != resolution_x)
+    {
+      priv->resolution_x = resolution_x;
+      g_object_notify_by_pspec (G_OBJECT (self), gegl_metadata_store_prop[PROP_RESOLUTION_X]);
+    }
+}
+
+gdouble
+gegl_metadata_store_get_resolution_x (GeglMetadataStore *self)
+{
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+
+  g_return_val_if_fail (GEGL_IS_METADATA_STORE (self), 0);
+
+  return priv->resolution_x;
+}
+
+/* "resolution-y" {{{2 */
+
+void
+gegl_metadata_store_set_resolution_y (GeglMetadataStore *self, gdouble resolution_y)
+{
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+
+  g_return_if_fail (GEGL_IS_METADATA_STORE (self));
+
+  if (priv->resolution_y != resolution_y)
+    {
+      priv->resolution_y = resolution_y;
+      g_object_notify_by_pspec (G_OBJECT (self), gegl_metadata_store_prop[PROP_RESOLUTION_Y]);
+    }
+}
+
+gdouble
+gegl_metadata_store_get_resolution_y (GeglMetadataStore *self)
+{
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+
+  g_return_val_if_fail (GEGL_IS_METADATA_STORE (self), 0);
+
+  return priv->resolution_y;
+}
+
+/* "file-module-name" {{{2 */
+
+const gchar *
+gegl_metadata_store_get_file_module_name (GeglMetadataStore *self)
+{
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+
+  g_return_val_if_fail (GEGL_IS_METADATA_STORE (self), 0);
+
+  return priv->file_module_name;
+}
+
+/* "title" {{{2 */
+
+void
+gegl_metadata_store_set_title (GeglMetadataStore *self, const gchar *title)
+{
+  gegl_metadata_store_set_string (self, "title", title);
+}
+
+const gchar *
+gegl_metadata_store_get_title (GeglMetadataStore *self)
+{
+  return gegl_metadata_store_get_string (self, "title");
+}
+
+/* "artist" {{{2 */
+
+void
+gegl_metadata_store_set_artist (GeglMetadataStore *self, const gchar *artist)
+{
+  gegl_metadata_store_set_string (self, "artist", artist);
+}
+
+const gchar *
+gegl_metadata_store_get_artist (GeglMetadataStore *self)
+{
+  return gegl_metadata_store_get_string (self, "artist");
+}
+
+/* "description" {{{2 */
+
+void
+gegl_metadata_store_set_description (GeglMetadataStore *self, const gchar *description)
+{
+  gegl_metadata_store_set_string (self, "description", description);
+}
+
+const gchar *
+gegl_metadata_store_get_description (GeglMetadataStore *self)
+{
+  return gegl_metadata_store_get_string (self, "description");
+}
+
+/* "copyright" {{{2 */
+
+void
+gegl_metadata_store_set_copyright (GeglMetadataStore *self, const gchar *copyright)
+{
+  gegl_metadata_store_set_string (self, "copyright", copyright);
+}
+
+const gchar *
+gegl_metadata_store_get_copyright (GeglMetadataStore *self)
+{
+  return gegl_metadata_store_get_string (self, "copyright");
+}
+
+/* "disclaimer" {{{2 */
+
+void
+gegl_metadata_store_set_disclaimer (GeglMetadataStore *self, const gchar *disclaimer)
+{
+  gegl_metadata_store_set_string (self, "disclaimer", disclaimer);
+}
+
+const gchar *
+gegl_metadata_store_get_disclaimer (GeglMetadataStore *self)
+{
+  return gegl_metadata_store_get_string (self, "disclaimer");
+}
+
+/* "warning" {{{2 */
+
+void
+gegl_metadata_store_set_warning (GeglMetadataStore *self, const gchar *warning)
+{
+  gegl_metadata_store_set_string (self, "warning", warning);
+}
+
+const gchar *
+gegl_metadata_store_get_warning (GeglMetadataStore *self)
+{
+  return gegl_metadata_store_get_string (self, "warning");
+}
+
+/* "comment" {{{2 */
+
+void
+gegl_metadata_store_set_comment (GeglMetadataStore *self, const gchar *comment)
+{
+  gegl_metadata_store_set_string (self, "comment", comment);
+}
+
+const gchar *
+gegl_metadata_store_get_comment (GeglMetadataStore *self)
+{
+  return gegl_metadata_store_get_string (self, "comment");
+}
+
+/* "software" {{{2 */
+
+void
+gegl_metadata_store_set_software (GeglMetadataStore *self, const gchar *software)
+{
+  gegl_metadata_store_set_string (self, "software", software);
+}
+
+const gchar *
+gegl_metadata_store_get_software (GeglMetadataStore *self)
+{
+  return gegl_metadata_store_get_string (self, "software");
+}
+
+/* "source" {{{2 */
+
+void
+gegl_metadata_store_set_source (GeglMetadataStore *self, const gchar *source)
+{
+  gegl_metadata_store_set_string (self, "source", source);
+}
+
+const gchar *
+gegl_metadata_store_get_source (GeglMetadataStore *self)
+{
+  return gegl_metadata_store_get_string (self, "source");
+}
+
+/* "timestamp" {{{2 */
+
+void
+gegl_metadata_store_set_timestamp (GeglMetadataStore *self,
+                                   const GDateTime *timestamp)
+{
+  GValue value = G_VALUE_INIT;
+
+  g_return_if_fail (GEGL_IS_METADATA_STORE (self));
+
+  g_value_init (&value, G_TYPE_DATE_TIME);
+  g_value_set_boxed (&value, timestamp);
+  gegl_metadata_store_set_value (self, "timestamp", &value);
+  g_value_unset (&value);
+}
+
+GDateTime *
+gegl_metadata_store_get_timestamp (GeglMetadataStore *self)
+{
+  GValue value = G_VALUE_INIT;
+  GDateTime *timestamp = NULL;
+
+  g_return_val_if_fail (GEGL_IS_METADATA_STORE (self), NULL);
+
+  g_value_init (&value, G_TYPE_DATE_TIME);
+  if (gegl_metadata_store_has_value (self, "timestamp"))
+    {
+      gegl_metadata_store_get_value (self, "timestamp", &value);
+      timestamp = g_date_time_ref (g_value_get_boxed (&value));
+    }
+  g_value_unset (&value);
+  return timestamp;
+}
+
+/* Declare metadata {{{1 */
+
+void
+gegl_metadata_store_declare (GeglMetadataStore *self, GParamSpec *pspec)
+{
+  g_return_if_fail (GEGL_IS_METADATA_STORE (self));
+
+  VMETHOD(self, _declare) (self, pspec, FALSE);
+}
+
+/* Metadata accessors {{{1 */
+
+gboolean
+gegl_metadata_store_has_value (GeglMetadataStore *self, const gchar *name)
+{
+  g_return_val_if_fail (GEGL_IS_METADATA_STORE (self), FALSE);
+
+  return VMETHOD(self, has_value) (self, name);
+}
+
+static GParamSpec *
+gegl_metadata_store_value_pspec (GeglMetadataStore *self, const gchar *name)
+{
+  g_return_val_if_fail (GEGL_IS_METADATA_STORE (self), G_TYPE_INVALID);
+
+  return VMETHOD(self, pspec) (self, name);
+}
+
+GType
+gegl_metadata_store_typeof_value (GeglMetadataStore *self, const gchar *name)
+{
+  GParamSpec *pspec;
+
+  pspec = gegl_metadata_store_value_pspec (self, name);
+  return pspec != NULL ? G_PARAM_SPEC_VALUE_TYPE (pspec) : G_TYPE_INVALID;
+}
+
+/* set/get by GValue */
+
+void
+gegl_metadata_store_notify (GeglMetadataStore *self, GParamSpec *pspec,
+                            gboolean notify)
+{
+  GQuark quark;
+
+  if (notify)
+    g_object_notify_by_pspec (G_OBJECT (self), pspec);
+  quark = g_param_spec_get_name_quark (pspec);
+  g_signal_emit (self, gegl_metadata_store_signals[CHANGED], quark, pspec);
+}
+
+void
+gegl_metadata_store_set_value (GeglMetadataStore *self, const gchar *name,
+                               const GValue *value)
+{
+  g_return_if_fail (GEGL_IS_METADATA_STORE (self));
+
+  VMETHOD(self, set_value) (self, name, value);
+}
+
+void
+gegl_metadata_store_get_value (GeglMetadataStore *self, const gchar *name,
+                               GValue *value)
+{
+  const GValue *internal;
+
+  g_return_if_fail (GEGL_IS_METADATA_STORE (self));
+
+  internal = VMETHOD(self, _get_value) (self, name);
+  g_return_if_fail (internal != NULL && G_IS_VALUE (internal));
+  g_value_transform (internal, value);
+}
+
+/* convenience methods for common case of string, these avoid having to
+   manipulate GValues and their associated lifetimes */
+
+void
+gegl_metadata_store_set_string (GeglMetadataStore *self, const gchar *name,
+                                const gchar *string)
+{
+  GValue internal = G_VALUE_INIT;
+
+  g_return_if_fail (GEGL_IS_METADATA_STORE (self));
+
+  g_value_init (&internal, G_TYPE_STRING);
+  g_value_set_static_string (&internal, string);
+  VMETHOD(self, set_value) (self, name, &internal);
+  g_value_unset (&internal);
+}
+
+const gchar *
+gegl_metadata_store_get_string (GeglMetadataStore *self, const gchar *name)
+{
+  const GValue *internal;
+
+  g_return_val_if_fail (GEGL_IS_METADATA_STORE (self), NULL);
+
+  internal = VMETHOD(self, _get_value) (self, name);
+  g_return_val_if_fail (internal != NULL && G_IS_VALUE (internal), NULL);
+  g_return_val_if_fail (G_VALUE_HOLDS (internal, G_TYPE_STRING), NULL);
+
+  return g_value_get_string (internal);
+}
+
+/* GeglMetadataMapValue {{{1 */
+
+typedef struct
+  {
+    gchar *local_name;
+    gchar *name;
+    GValueTransform transform;
+  } GeglMetadataMapValue;
+
+static GeglMetadataMapValue *
+metadata_map_new (const gchar *local_name, const gchar *name,
+                  GValueTransform transform)
+{
+  GeglMetadataMapValue *map_value;
+
+  map_value = g_slice_new (GeglMetadataMapValue);
+  map_value->local_name = g_strdup (local_name);
+  map_value->name = g_strdup (name);
+  map_value->transform = transform;
+  return map_value;
+}
+
+static void
+metadata_map_free (gpointer data)
+{
+  GeglMetadataMapValue *map_value = data;
+
+  g_free (map_value->local_name);
+  g_free (map_value->name);
+  g_slice_free (GeglMetadataMapValue, map_value);
+}
+
+
+static gboolean
+metadata_map_equal (gconstpointer a, gconstpointer b)
+{
+  const GeglMetadataMapValue *map_value = a;
+  const gchar *local_name = b;
+
+  return g_strcmp0 (map_value->local_name, local_name) == 0;
+}
+
+static GeglMetadataMapValue *
+metadata_map_lookup (GeglMetadataStore *self, const gchar *local_name)
+{
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+  guint indx;
+
+  g_return_val_if_fail (priv->map != NULL, NULL);
+
+  if (g_ptr_array_find_with_equal_func (priv->map, local_name,
+                                        metadata_map_equal, &indx))
+    return g_ptr_array_index (priv->map, indx);
+  return NULL;
+}
+
+/* Register metadata name and type mappings {{{1 */
+
+void
+gegl_metadata_store_register (GeglMetadataStore *self,
+                              const gchar *local_name, const gchar *name,
+                              GValueTransform transform)
+{
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+  GeglMetadataMapValue *map_value;
+  guint indx;
+
+  map_value = metadata_map_new (local_name, name, transform);
+
+  if (g_ptr_array_find_with_equal_func (priv->map, local_name,
+                                        metadata_map_equal, &indx))
+    {
+      metadata_map_free (g_ptr_array_index (priv->map, indx));
+      g_ptr_array_index (priv->map, indx) = map_value;
+    }
+  else
+    g_ptr_array_add (priv->map, map_value);
+}
+
+/* Metadata Interface {{{1 */
+
+/* register_map {{{2 */
+
+static void
+gegl_metadata_store_register_hook (GeglMetadataStore *self,
+                                   const gchar *file_module_name, guint flags)
+{
+  g_signal_emit (self, gegl_metadata_store_signals[MAPPED], 0,
+                 file_module_name, !!(flags & GEGL_MAP_EXCLUDE_UNMAPPED));
+}
+
+/* map == NULL clears map */
+static void
+gegl_metadata_store_register_map (GeglMetadata *metadata,
+                                  const gchar *file_module_name,
+                                  guint flags,
+                                  const GeglMetadataMap *map, gsize n_map)
+{
+  GeglMetadataStore *self = GEGL_METADATA_STORE (metadata);
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+  GeglMetadataMapValue *map_value;
+  gsize i;
+
+  if (priv->map != NULL)
+    g_ptr_array_unref (priv->map);
+
+  if (map != NULL)
+    {
+      priv->file_module_name = g_strdup (file_module_name);
+      priv->exclude_unmapped = !!(flags & GEGL_MAP_EXCLUDE_UNMAPPED);
+
+      priv->map = g_ptr_array_new_full (n_map, metadata_map_free);
+      for (i = 0; i < n_map; i++)
+        {
+          map_value = metadata_map_new (map[i].local_name,
+                                        map[i].name, map[i].transform);
+          g_ptr_array_add (priv->map, map_value);
+        }
+
+      VMETHOD(self, register_hook) (self, file_module_name, flags);
+    }
+  else
+    {
+      g_free (priv->file_module_name);
+
+      priv->map = NULL;
+      priv->file_module_name = NULL;
+      priv->exclude_unmapped = FALSE;
+    }
+  g_object_notify_by_pspec (G_OBJECT (self), gegl_metadata_store_prop[PROP_FILE_MODULE_NAME]);
+}
+
+/* resolution {{{2 */
+
+static gboolean
+gegl_metadata_store_set_resolution (GeglMetadata *metadata,
+                                    GeglResolutionUnit unit,
+                                    gfloat x, gfloat y)
+{
+  GeglMetadataStore *self = GEGL_METADATA_STORE (metadata);
+
+  /* sanity check */
+  if (x == 0.0f && y == 0.0f)
+    return FALSE;
+  if (x == 0.0f)
+    x = y;
+  else if (y == 0.0f)
+    y = x;
+  gegl_metadata_store_set_resolution_unit (self, unit);
+  gegl_metadata_store_set_resolution_x (self, x);
+  gegl_metadata_store_set_resolution_y (self, y);
+  return TRUE;
+}
+
+static gboolean
+gegl_metadata_store_get_resolution (GeglMetadata *metadata,
+                                    GeglResolutionUnit *unit,
+                                    gfloat *x, gfloat *y)
+{
+  GeglMetadataStore *self = GEGL_METADATA_STORE (metadata);
+
+  *unit = gegl_metadata_store_get_resolution_unit (self);
+  *x = gegl_metadata_store_get_resolution_x (self);
+  *y = gegl_metadata_store_get_resolution_y (self);
+  return TRUE;
+}
+
+/* iterators {{{2 */
+
+#define STAMP           0xa5caf30e
+#define INVALID_STAMP   0
+
+static gboolean
+gegl_metadata_store_iter_lookup (GeglMetadata *metadata, GeglMetadataIter *iter,
+                                 const gchar *local_name)
+{
+  GeglMetadataStore *self = GEGL_METADATA_STORE (metadata);
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+  GeglMetadataMapValue *map_value;
+
+  map_value = metadata_map_lookup (self, local_name);
+  if (map_value == NULL)
+    {
+      if (priv->exclude_unmapped)
+        return FALSE;
+      /* emit unmapped signal and try again */
+      g_signal_emit (self, gegl_metadata_store_signals[UNMAPPED], 0,
+                     priv->file_module_name, local_name);
+      map_value = metadata_map_lookup (self, local_name);
+      if (map_value == NULL)
+        return FALSE;
+    }
+  iter->stamp = STAMP;
+  iter->user_data = self;
+  iter->user_data2 = NULL;
+  iter->user_data3 = map_value;
+  return TRUE;
+}
+
+static void
+gegl_metadata_store_iter_init (GeglMetadata *metadata, GeglMetadataIter *iter)
+{
+  GeglMetadataStore *self = GEGL_METADATA_STORE (metadata);
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+
+  g_return_if_fail (priv->map != NULL);
+
+  iter->stamp = STAMP;
+  iter->user_data = self;
+  iter->user_data2 = &g_ptr_array_index (priv->map, 0);
+  iter->user_data3 = NULL;
+}
+
+static const gchar *
+gegl_metadata_store_iter_next (GeglMetadata *metadata, GeglMetadataIter *iter)
+{
+  GeglMetadataStore *self = GEGL_METADATA_STORE (metadata);
+  GeglMetadataStorePrivate *priv = gegl_metadata_store_get_instance_private (self);
+  GeglMetadataMapValue *map_value;
+  gpointer *pdata;
+
+  g_return_val_if_fail (iter->stamp == STAMP, NULL);
+  g_return_val_if_fail (iter->user_data == self, NULL);
+  g_return_val_if_fail (iter->user_data2 != NULL, NULL);
+
+  pdata = iter->user_data2;
+  if (pdata < &g_ptr_array_index (priv->map, priv->map->len))
+    {
+      map_value = *pdata++;
+      iter->user_data2 = pdata;
+      iter->user_data3 = map_value;
+      return map_value->local_name;
+    }
+
+  iter->stamp = INVALID_STAMP;
+  return NULL;
+}
+
+/* get/set mapped values {{{2 */
+
+/* If a "parse-value::name" signal is registered emit the signal to parse the
+ * value and return TRUE. If no  handler is registered, return FALSE.  The
+ * handler parses the supplied GValue and may set any number of metadata values
+ * using gegl_metadata_store_set_value().
+ */
+static gboolean
+gegl_metadata_store_parse_value (GeglMetadataStore *self,
+                                 GParamSpec *pspec,
+                                 GValueTransform transform,
+                                 const GValue *value)
+{
+  GQuark quark;
+  guint signal_id;
+  gboolean success;
+
+  quark = g_param_spec_get_name_quark (pspec);
+  signal_id = gegl_metadata_store_signals[PARSE];
+  if (g_signal_has_handler_pending (self, signal_id, quark, FALSE))
+    {
+      /* If the GValue types are compatible pass value directly to the signal
+         handler.  Otherwise initialise a GValue, attempt to transform the
+         value and, if successful, call the signal handler. */
+      success = FALSE;
+      if (g_value_type_compatible (G_PARAM_SPEC_VALUE_TYPE (pspec),
+                                   G_VALUE_TYPE (value)))
+        g_signal_emit (self, signal_id, quark, pspec, value, &success);
+      else
+        {
+          GValue temp = G_VALUE_INIT;
+
+          g_value_init (&temp, G_PARAM_SPEC_VALUE_TYPE (pspec));
+          if (transform != NULL)
+            {
+              (*transform) (value, &temp);
+              success = TRUE;
+            }
+          else
+            success = g_value_transform (value, &temp);
+          if (success)
+            g_signal_emit (self, signal_id, quark, pspec, &temp, &success);
+          g_value_unset (&temp);
+        }
+      return success;
+    }
+  return FALSE;
+}
+
+/* Note that the underlying value is not set if a parse_value() returns TRUE
+ * and that this processing is performed only when the metadata is accessed via
+ * the GeglMetadata interface.
+ */
+static gboolean
+gegl_metadata_store_iter_set_value (GeglMetadata *metadata,
+                                    GeglMetadataIter *iter,
+                                    const GValue *value)
+{
+  GeglMetadataStore *self = GEGL_METADATA_STORE (metadata);
+  GeglMetadataMapValue *map_value;
+  GParamSpec *pspec;
+
+  g_return_val_if_fail (iter->stamp == STAMP, FALSE);
+  g_return_val_if_fail (iter->user_data == self, FALSE);
+  g_return_val_if_fail (iter->user_data3 != NULL, FALSE);
+
+  map_value = iter->user_data3;
+
+  pspec = VMETHOD(self, pspec) (self, map_value->name);
+  g_return_val_if_fail (pspec != NULL, FALSE);
+
+  /* Try calling parse_value() */
+  if (VMETHOD(self, parse_value) (self, pspec, map_value->transform, value))
+    return TRUE;
+
+  if (map_value->transform == NULL)
+    VMETHOD(self, set_value) (self, map_value->name, value);
+  else
+    {
+      GValue xfrm = G_VALUE_INIT;
+
+      g_value_init (&xfrm, G_PARAM_SPEC_VALUE_TYPE (pspec));
+      (*map_value->transform) (value, &xfrm);
+      VMETHOD(self, set_value) (self, map_value->name, &xfrm);
+      g_value_unset (&xfrm);
+    }
+  return TRUE;
+}
+
+/* If a "generate-value::name" signal is registered emit the signal to parse
+ * the value and return TRUE. If no  handler is registered, return FALSE.  The
+ * signal handler must set a value of the type specified in the pspec argument
+ * and return TRUE if successful.
+ */
+static gboolean
+gegl_metadata_store_generate_value (GeglMetadataStore *self,
+                                    GParamSpec *pspec,
+                                    GValueTransform transform,
+                                    GValue *value)
+{
+  GQuark quark;
+  guint signal_id;
+  gboolean success;
+
+  quark = g_param_spec_get_name_quark (pspec);
+  signal_id = gegl_metadata_store_signals[GENERATE];
+  if (g_signal_has_handler_pending (self, signal_id, quark, FALSE))
+    {
+      /* if the GValue types are compatible pass the return value directly to
+         the signal handler.  Otherwise initialise a GValue, call the signal
+         handler and transform the return value as for the regular case below. */
+      success = FALSE;
+      if (g_value_type_compatible (G_PARAM_SPEC_VALUE_TYPE (pspec),
+                                   G_VALUE_TYPE (value)))
+        g_signal_emit (self, signal_id, quark, pspec, value, &success);
+      else
+        {
+          GValue temp = G_VALUE_INIT;
+
+          g_value_init (&temp, G_PARAM_SPEC_VALUE_TYPE (pspec));
+          g_signal_emit (self, signal_id, quark, pspec, &temp, &success);
+          if (success)
+            {
+              if (transform != NULL)
+                (*transform) (&temp, value);
+              else
+                g_value_transform (&temp, value);
+            }
+          g_value_unset (&temp);
+        }
+      return TRUE;
+    }
+  return FALSE;
+}
+
+/* Note that the underlying value is not accessed if a generate_value() returns
+ * TRUE that this processing is only performed when accessed via the
+ * GeglMetadata interface.  The signal handler can, however, access the actual
+ * stored value using gegl_metadata_store_get_value().
+ */
+static gboolean
+gegl_metadata_store_iter_get_value (GeglMetadata *metadata,
+                                    GeglMetadataIter *iter,
+                                    GValue *value)
+{
+  GeglMetadataStore *self = GEGL_METADATA_STORE (metadata);
+  GeglMetadataMapValue *map_value;
+  const GValue *meta;
+  GParamSpec *pspec;
+
+  g_return_val_if_fail (iter->stamp == STAMP, FALSE);
+  g_return_val_if_fail (iter->user_data == self, FALSE);
+  g_return_val_if_fail (iter->user_data3 != NULL, FALSE);
+
+  map_value = iter->user_data3;
+
+  pspec = VMETHOD(self, pspec) (self, map_value->name);
+  g_return_val_if_fail (pspec != NULL, FALSE);
+
+  /* Try calling generate_value() */
+  if (VMETHOD(self, generate_value) (self, pspec, map_value->transform, value))
+    return TRUE;
+
+  /* If a transform function is set, use that to convert the stored gvalue to
+     the requested type, otherwise use g_value_transform().  */
+  meta = VMETHOD(self, _get_value) (self, map_value->name);
+  if (meta == NULL)
+    return FALSE;
+
+  if (map_value->transform != NULL)
+    {
+      (*map_value->transform) (meta, value);
+      return TRUE;
+    }
+  return g_value_transform (meta, value);
+}
+
+static void
+gegl_metadata_store_interface_init (GeglMetadataInterface *iface)
+{
+  iface->register_map = gegl_metadata_store_register_map;
+  iface->set_resolution = gegl_metadata_store_set_resolution;
+  iface->get_resolution = gegl_metadata_store_get_resolution;
+  iface->iter_lookup = gegl_metadata_store_iter_lookup;
+  iface->iter_init = gegl_metadata_store_iter_init;
+  iface->iter_next = gegl_metadata_store_iter_next;
+  iface->iter_set_value = gegl_metadata_store_iter_set_value;
+  iface->iter_get_value = gegl_metadata_store_iter_get_value;
+}
diff --git a/gegl/gegl-metadatastore.h b/gegl/gegl-metadatastore.h
new file mode 100644
index 000000000..4889b7130
--- /dev/null
+++ b/gegl/gegl-metadatastore.h
@@ -0,0 +1,724 @@
+/* This file is part of GEGL
+ *
+ * GEGL 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 3 of the License, or (at your option) any later version.
+ *
+ * GEGL 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 GEGL; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * Copyright 2020 Brian Stafford
+ */
+
+#ifndef __GEGL_METADATA_STORE_H__
+#define __GEGL_METADATA_STORE_H__
+
+#include <glib.h>
+#include "gegl-metadata.h"
+
+G_BEGIN_DECLS
+
+#define GEGL_TYPE_METADATA_STORE   gegl_metadata_store_get_type ()
+G_DECLARE_DERIVABLE_TYPE (
+        GeglMetadataStore,
+        gegl_metadata_store,
+        GEGL, METADATA_STORE,
+        GObject
+)
+
+/**
+ * SECTION:gegl-metadatastore
+ * @title: GeglMetadataStore
+ * @short_description: A metadata store base class for use with file modules.
+ * @see_also: #GeglMetadata #GeglMetadataHash
+ *
+ * #GeglMetadataStore is a non-instantiable base class implementing the
+ * #GeglMetadata interface and provides methods for metadata access using
+ * well-known names.  For consistency with other #GObject features, the naming
+ * convention for metadata variables is the same as for GObject properties.
+ *
+ * Methods are provided allowing the application to test whether a particular
+ * metadata item has a value and to set or get the values. If a metadata value
+ * does not exist, a GLib warning is printed. The
+ * gegl_metadata_store_has_value() method can be used to test silently for
+ * unset variables.
+ *
+ * Signals are provided to allow an application to intercept metadata values
+ * from file modules, for example a Jpeg comment block might be parsed to set
+ * multiple metadata values, or multiple values may be formatted into the
+ * comment block.
+ *
+ * Image resolution and resolution units are accessible only as properties.
+ * Well-known metatdata values are shadowed by properties to allow applications
+ * to take advantage of features such as introspection and property binding.
+ *
+ * #GeglMetadataStore does not itself implement the storage mechanism, it must
+ * be subclassed to provide this. #GeglMetadataHash implements a store using a
+ * hash table.  For convenience gegl_metadata_hash_new() casts its return value
+ * to #GeglMetadataStore as it does not add any new methods or properties.
+ */
+
+/**
+ * GeglMetadataStoreClass:
+ * @_declare: The _declare virtual method creates a metadata variable in the
+ * underlying data store. It implements gegl_metadata_store_declare(). A
+ * #GParamSpec is used to describe the variable.  If the metadata shadows an
+ * object property, shadow should be %TRUE, otherwise %FALSE.  It is acceptable
+ * for a subclass to provide additional variables which are implicitly
+ * declared, that is, they need not be declared using
+ * gegl_metadata_store_declare(), however the @pspec method must still retrieve
+ * a #GParamSpec describing such variables.  This method MUST be provided by
+ * the subclass.
+ * @pspec: The pspec virtual method returns the #GParamSpec used to declare a
+ * metadata variable. It is used to implement
+ * gegl_metadata_store_typeof_value(). This method MUST be provided by the
+ * subclass.
+ * @has_value: The has_value virtual method implements
+ * gegl_metadata_store_has_value() It should return %TRUE if the variable is
+ * declared and contains a valid value of the correct type, otherwise %FALSE.
+ * This method MUST be provided by the subclass.
+ * @set_value: Set a metadata variable using a #GValue. Implements
+ * gegl_metadata_store_set_value().  The metadata variable should be declared
+ * and the #GValue must be of the correct type.  Note that failure to set a
+ * variable may be dependent of properties of the underlying storage mechanism.
+ * This method MUST be provided by the subclass.
+ * @_get_value: Return a pointer to a #GValue with the value of the metadata
+ * variable or %NULL if not declared or the variable does not contain a valid
+ * value.  Implements gegl_metadata_store_get_value().  This method MUST be
+ * provided by the subclass.
+ * @register_hook: This method is called after a file loader or saver registers
+ * a #GeglMetadataMap and before any further processing takes place.  It is
+ * intended to allow an application to create further application-specific
+ * mappings using gegl_metadata_store_register().  #GeglMetadataStore provides
+ * a default method which emits the `::mapped` signal.
+ * @parse_value: This method is called to optionally parse image file metadata
+ * prior to setting metadata variables in the #GeglMetadataStore. If no parser
+ * is available it returns %FALSE and the registered mapping is used.  If a
+ * parser available it should set one or more metadata variables using
+ * gegl_metadata_store_set_value() and return %TRUE. Note that the parser MUST
+ * return %TRUE even if setting individual values fails.  The default method
+ * checks if a signal handler is registered for the parse-value signal with
+ * the variable name as the detail parameter. If a handler is registered it
+ * emits the signal with the file metadata provided as a #GValue and returns
+ * %TRUE otherwise %FALSE.
+ * @generate_value: This method is called to optionally generate a value to be
+ * written to and image file. If no generator is available it returns %FALSE
+ * and the registered mapping is used. If a generator is available it should
+ * create a suitable value to be written to the image file and return %TRUE.
+ * The default method checks if a signal handler is registered for the
+ * generate-value signal with the variable name as the detail parameter. If a
+ * handler is registered it emits the signal with an initialised #GValue to
+ * receive the file metadata and returns %TRUE otherwise %FALSE.  @parse_value
+ * and @generate_value are provided to handle the case where some file formats
+ * overload, for example, image comments. A typical case is formatting many
+ * values into a TIFF file's ImageDescription field.
+ *
+ * The class structure for the #GeglMetadataStore
+ */
+struct _GeglMetadataStoreClass
+{
+  /*< private >*/
+  GObjectClass  parent_class;
+
+  /*< public >*/
+  /* Subclass MUST provide the following */
+  void          (*_declare)       (GeglMetadataStore *self,
+                                   GParamSpec *pspec,
+                                   gboolean shadow);
+  GParamSpec   *(*pspec)          (GeglMetadataStore *self,
+                                   const gchar *name);
+  void          (*set_value)      (GeglMetadataStore *self,
+                                   const gchar *name,
+                                   const GValue *value);
+  const GValue *(*_get_value)     (GeglMetadataStore *self,
+                                   const gchar *name);
+  gboolean      (*has_value)      (GeglMetadataStore *self,
+                                   const gchar *name);
+
+  /* Subclass MAY provide the following */
+  void          (*register_hook)  (GeglMetadataStore *self,
+                                   const gchar *file_module_name,
+                                   guint flags);
+  gboolean      (*parse_value)    (GeglMetadataStore *self,
+                                   GParamSpec *pspec,
+                                   GValueTransform transform,
+                                   const GValue *value);
+  gboolean      (*generate_value) (GeglMetadataStore *self,
+                                   GParamSpec *pspec,
+                                   GValueTransform transform,
+                                   GValue *value);
+
+  /*< private >*/
+  gpointer      padding[4];
+};
+
+/**
+ * GeglMetadataStore::changed:
+ * @self: The #GeglMetadataStore emitting the signal
+ * @pspec: A #GParamSpec declaring the metadata value
+ *
+ * `::changed` is emitted when a metadata value is changed. This is analogous
+ * to the `GObject::notify` signal.
+ */
+
+/**
+ * GeglMetadataStore::mapped:
+ * @self: The #GeglMetadataStore emitting the signal
+ * @file_module: The file module name
+ * @exclude_unmapped: %TRUE if the file module cannot handle unmapped values
+ *
+ * `::mapped` is emitted after a file module registers a mapping and before
+ * other processing takes place.  An application may respond to the signal by
+ * registering additional mappings or overriding existing values, for example
+ * it might override the TIFF ImageDescription tag to format multiple metadata
+ * values into the description.
+ */
+
+/**
+ * GeglMetadataStore::unmapped:
+ * @self: The #GeglMetadataStore emitting the signal
+ * @file_module: The file module name
+ * @local_name: The unmapped metadata name as used by the file module
+ *
+ * `::unmapped` is emitted when a file module tries to look up an unmapped
+ * metadata name. When the handler returns a second attempt is made to look
+ * up the metadata.
+ */
+
+/**
+ * GeglMetadataStore::generate-value:
+ * @self: The #GeglMetadataStore emitting the signal
+ * @pspec: A #GParamSpec declaring the metadata value
+ * @value: (inout): An initialised #GValue.
+ *
+ * If a signal handler is connected to `::generate-value` a signal is emitted
+ * when the file module accesses a value using gegl_metadata_get_value().
+ * The signal handler must generate a value of the type specified in the pspec
+ * argument. The signal handler's return value indicates the success of the
+ * operation.
+ *
+ * If no handler is connected the mapped metadata value is accessed normally,
+ *
+ * Returns: %TRUE if a value is generated successfully.
+ */
+
+/**
+ * GeglMetadataStore::parse-value:
+ * @self: The #GeglMetadataStore emitting the signal
+ * @pspec: A #GParamSpec declaring the metadata value
+ * @value: (inout): A #GValue containing the value to parse.
+ *
+ * If a signal handler is connected to `::parse-value` a signal is emitted when
+ * the file module accesses a value using gegl_metadata_set_value().  The
+ * signal handler should parse the value supplied in the #GValue and may set
+ * any number of metadata values using gegl_metadata_store_set_value().
+ *
+ * If no handler is connected the mapped metadata value is set normally,
+ *
+ * Returns: %TRUE if parsing is successful.
+ */
+
+/**
+ * GeglMetadataStore:resolution-unit:
+ *
+ * A #GeglResolutionUnit specifying units for the image resolution (density).
+ */
+
+/**
+ * gegl_metadata_store_set_resolution_unit:
+ * @self: A #GeglMetadataStore
+ * @unit: Units as a #GeglResolutionUnit
+ *
+ * Set the units used for the resolution (density) values.
+ */
+void          gegl_metadata_store_set_resolution_unit
+                                                (GeglMetadataStore *self,
+                                                 GeglResolutionUnit unit);
+
+/**
+ * gegl_metadata_store_get_resolution_unit:
+ * @self: A #GeglMetadataStore
+ *
+ * Get the units used for resolution.
+ *
+ * Returns: a #GeglResolutionUnit.
+ */
+GeglResolutionUnit
+              gegl_metadata_store_get_resolution_unit
+                                                (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:resolution-x:
+ *
+ * X resolution or density in dots per unit.
+ */
+
+/**
+ * gegl_metadata_store_set_resolution_x:
+ * @self: A #GeglMetadataStore
+ * @resolution_x: X resolution or density
+ *
+ * Set the X resolution or density in dots per unit.
+ */
+void          gegl_metadata_store_set_resolution_x
+                                                (GeglMetadataStore *self,
+                                                 gdouble resolution_x);
+
+/**
+ * gegl_metadata_store_get_resolution_x:
+ * @self: A #GeglMetadataStore
+ *
+ * Get the X resolution or density in dots per unit.
+ *
+ * Returns: X resolution
+ */
+gdouble       gegl_metadata_store_get_resolution_x
+                                                (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:resolution-y:
+ *
+ * Y resolution or density in dots per unit.
+ */
+
+/**
+ * gegl_metadata_store_set_resolution_y:
+ * @self: A #GeglMetadataStore
+ * @resolution_y: Y resolution or density
+ *
+ * Set the Y resolution or density in dots per unit.
+ */
+void          gegl_metadata_store_set_resolution_y
+                                                (GeglMetadataStore *self,
+                                                 gdouble resolution_y);
+
+/**
+ * gegl_metadata_store_get_resolution_y:
+ * @self: A #GeglMetadataStore
+ *
+ * Get the Y resolution or density in dots per unit.
+ *
+ * Returns: Y resolution
+ */
+gdouble       gegl_metadata_store_get_resolution_y
+                                                (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:file-module-name:
+ *
+ * Current file loader/saver module name. Valid only while a #GeglMetadata
+ * mapping is registered. This property is mainly provided for use in signal
+ * handlers.
+ */
+
+/**
+ * gegl_metadata_store_get_file_module_name:
+ * @self: A #GeglMetadataStore
+ *
+ * Return the name registered by the current file module.
+ *
+ * Returns: (transfer none): Current file module name or %NULL.
+ */
+const gchar * gegl_metadata_store_get_file_module_name
+                                                (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:title:
+ *
+ * Short (one line) title or caption for image.
+ */
+
+/**
+ * gegl_metadata_store_set_title:
+ * @self: A #GeglMetadataStore
+ * @title: Title string
+ *
+ * Set title or caption for image.
+ */
+void          gegl_metadata_store_set_title    (GeglMetadataStore *self,
+                                                const gchar *title);
+
+/**
+ * gegl_metadata_store_get_title:
+ * @self: A #GeglMetadataStore
+ *
+ * Get title or caption for image.
+ *
+ * Returns: (transfer none): Title or %NULL if not set
+ */
+const gchar * gegl_metadata_store_get_title    (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:artist:
+ *
+ * Name of image creator.
+ */
+
+/**
+ * gegl_metadata_store_set_artist:
+ * @self: A #GeglMetadataStore
+ * @artist: Artist string
+ *
+ * Set name of image creator.
+ */
+void          gegl_metadata_store_set_artist    (GeglMetadataStore *self,
+                                                 const gchar *artist);
+
+/**
+ * gegl_metadata_store_get_artist:
+ * @self: A #GeglMetadataStore
+ *
+ * Get name of image creator.
+ *
+ * Returns: (transfer none): Artist or %NULL if not set
+ */
+const gchar * gegl_metadata_store_get_artist    (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:description:
+ *
+ * Description of image (possibly long).
+ */
+
+/**
+ * gegl_metadata_store_set_description:
+ * @self: A #GeglMetadataStore
+ * @description: Description string
+ *
+ * Set description of image.
+ */
+void           gegl_metadata_store_set_description
+                                                (GeglMetadataStore *self,
+                                                 const gchar *description);
+
+/**
+ * gegl_metadata_store_get_description:
+ * @self: A #GeglMetadataStore
+ *
+ * Get description of image.
+ *
+ * Returns: (transfer none): Description or %NULL if not set
+ */
+const gchar * gegl_metadata_store_get_description
+                                                (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:copyright:
+ *
+ * Copyright notice.
+ */
+
+/**
+ * gegl_metadata_store_set_copyright:
+ * @self: A #GeglMetadataStore
+ * @copyright: Copyright string
+ *
+ * Set the copyright notice.
+ */
+void           gegl_metadata_store_set_copyright
+                                                (GeglMetadataStore *self,
+                                                 const gchar *copyright);
+
+/**
+ * gegl_metadata_store_get_copyright:
+ * @self: A #GeglMetadataStore
+ *
+ * Get the copyright notice.
+ *
+ * Returns: (transfer none): Copyright or %NULL if not set
+ */
+const gchar * gegl_metadata_store_get_copyright
+                                                (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:disclaimer:
+ *
+ * Legal disclaimer.
+ */
+
+/**
+ * gegl_metadata_store_set_disclaimer:
+ * @self: A #GeglMetadataStore
+ * @disclaimer: Disclaimer string
+ *
+ * Set the legal disclaimer.
+ */
+void            gegl_metadata_store_set_disclaimer
+                                                (GeglMetadataStore *self,
+                                                 const gchar *disclaimer);
+
+/**
+ * gegl_metadata_store_get_disclaimer:
+ * @self: A #GeglMetadataStore
+ *
+ * Get the legal disclaimer.
+ *
+ * Returns: (transfer none): Disclaimer or %NULL if not set
+ */
+const gchar * gegl_metadata_store_get_disclaimer
+                                                (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:warning:
+ *
+ * Warning of nature of content.
+ */
+
+/**
+ * gegl_metadata_store_set_warning:
+ * @self: A #GeglMetadataStore
+ * @warning: Warning string
+ *
+ * Set the warning of nature of content.
+ */
+void          gegl_metadata_store_set_warning   (GeglMetadataStore *self,
+                                                 const gchar *warning);
+
+/**
+ * gegl_metadata_store_get_warning:
+ * @self: A #GeglMetadataStore
+ *
+ * Get warning.
+ *
+ * Returns: (transfer none): Warning or %NULL if not set
+ */
+const gchar * gegl_metadata_store_get_warning   (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:comment:
+ *
+ * Miscellaneous comment; conversion from GIF comment.
+ */
+
+/**
+ * gegl_metadata_store_set_comment:
+ * @self: A #GeglMetadataStore
+ * @comment: Comment string
+ *
+ * Set the miscellaneous comment; conversion from GIF comment.
+ */
+void          gegl_metadata_store_set_comment   (GeglMetadataStore *self,
+                                                 const gchar *comment);
+
+/**
+ * gegl_metadata_store_get_comment:
+ * @self: A #GeglMetadataStore
+ *
+ * Get the comment.
+ *
+ * Returns: (transfer none): Comment or %NULL if not set
+ */
+const gchar * gegl_metadata_store_get_comment   (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:software:
+ *
+ * Software used to create the image.
+ */
+
+/**
+ * gegl_metadata_store_set_software:
+ * @self: A #GeglMetadataStore
+ * @software: Software string
+ *
+ * Set software used to create the image.
+ */
+void          gegl_metadata_store_set_software  (GeglMetadataStore *self,
+                                                 const gchar *software);
+
+/**
+ * gegl_metadata_store_get_software:
+ * @self: A #GeglMetadataStore
+ *
+ * Get software used to create the image.
+ *
+ * Returns: (transfer none): Software or %NULL if not set
+ */
+const gchar * gegl_metadata_store_get_software  (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:source:
+ *
+ * Device used to create the image.
+ */
+
+/**
+ * gegl_metadata_store_set_source:
+ * @self: A #GeglMetadataStore
+ * @source: Source string
+ *
+ * Set device used to create the image.
+ */
+void            gegl_metadata_store_set_source  (GeglMetadataStore *self,
+                                                 const gchar *source);
+
+/**
+ * gegl_metadata_store_get_source:
+ * @self: A #GeglMetadataStore
+ *
+ * Get device used to create the image.
+ *
+ * Returns: (transfer none): source or %NULL if not set
+ */
+const gchar * gegl_metadata_store_get_source    (GeglMetadataStore *self);
+
+/**
+ * GeglMetadataStore:timestamp:
+ *
+ * Time of original image creation.
+ */
+
+/**
+ * gegl_metadata_store_set_timestamp:
+ * @self: A #GeglMetadataStore
+ * @timestamp: A #GDateTime
+ *
+ * Set time of original image creation.
+ */
+void          gegl_metadata_store_set_timestamp (GeglMetadataStore *self,
+                                                 const GDateTime *timestamp);
+
+/**
+ * gegl_metadata_store_get_timestamp:
+ * @self: A #GeglMetadataStore
+ *
+ * Get time of original image creation.
+ *
+ * Returns: (transfer full): #GDateTime or %NULL if not set. Free with
+ *                           g_date_time_unref() when done.
+ */
+GDateTime *   gegl_metadata_store_get_timestamp (GeglMetadataStore *self);
+
+/**
+ * gegl_metadata_store_declare:
+ * @self: A #GeglMetadataStore
+ * @pspec: (transfer none): A #GParamSpec
+ *
+ * Declare a metadata value using a #GParamSpec.
+ */
+void          gegl_metadata_store_declare       (GeglMetadataStore *self,
+                                                 GParamSpec *pspec);
+
+/**
+ * gegl_metadata_store_has_value:
+ * @self: A #GeglMetadataStore
+ * @name: Metadata name
+ *
+ * Test whether the #GeglMetadataStore contains a value for the specified name.
+ *
+ * Returns: %TRUE if metadata is declared and contains a valid value.
+ */
+gboolean      gegl_metadata_store_has_value     (GeglMetadataStore *self,
+                                                 const gchar *name);
+
+/**
+ * gegl_metadata_store_typeof_value:
+ * @self: A #GeglMetadataStore
+ * @name: Metadata name
+ *
+ * Get the declared type of the value in the #GeglMetadataStore.
+ *
+ * Returns: Declared #GType of metadata value or %G_TYPE_INVALID.
+ */
+GType         gegl_metadata_store_typeof_value  (GeglMetadataStore *self,
+                                                 const gchar *name);
+
+/**
+ * gegl_metadata_store_set_value:
+ * @self: A #GeglMetadataStore
+ * @name: Metadata name
+ * @value: (in): (nullable): A valid #GValue or %NULL
+ *
+ * Set the specified metadata value. If @value is %NULL the default value from
+ * the associated #GParamSpec is used. This operation will fail if the value
+ * has not been previously declared.  A `changed::name` signal is emitted when
+ * the value is set. If the value is shadowed by a property a `notify::name`
+ * signal is also emitted.
+ */
+void          gegl_metadata_store_set_value     (GeglMetadataStore *self,
+                                                 const gchar *name,
+                                                 const GValue *value);
+
+/**
+ * gegl_metadata_store_get_value:
+ * @self: A #GeglMetadataStore
+ * @name: Metadata name
+ * @value: (inout): An initialised #GValue.
+ *
+ * Retrieve the metadata value. @value must be initialised with a compatible
+ * type. If the value is unset or has not been previously declared @value is
+ * unchanged and an error message is logged.
+ */
+void          gegl_metadata_store_get_value     (GeglMetadataStore *self,
+                                                 const gchar *name,
+                                                 GValue *value);
+
+/**
+ * gegl_metadata_store_set_string:
+ * @self: A #GeglMetadataStore
+ * @name: Metadata name
+ * @string: String value to set
+ *
+ * A slightly more efficient version of gegl_metadata_store_set_value()
+ * for string values avoiding a duplication. Otherwise it behaves the same
+ * gegl_metadata_store_set_value().
+ */
+void          gegl_metadata_store_set_string    (GeglMetadataStore *self,
+                                                 const gchar *name,
+                                                 const gchar *string);
+
+/**
+ * gegl_metadata_store_get_string:
+ * @self: A #GeglMetadataStore
+ * @name: Metadata name
+ *
+ * A slightly more efficient version of gegl_metadata_store_get_value()
+ * for string values avoiding a duplication. Otherwise it behaves the same
+ * gegl_metadata_store_get_value().
+
+ * Returns: (transfer none): String or %NULL.
+ */
+const gchar * gegl_metadata_store_get_string    (GeglMetadataStore *self,
+                                                 const gchar *name);
+
+/**
+ * gegl_metadata_store_register:
+ * @self: A #GeglMetadataStore
+ * @local_name: Metadata name known to file module
+ * @name: Metadata name
+ * @transform: (scope async): A #GValueTransform function or %NULL
+ *
+ */
+void          gegl_metadata_store_register      (GeglMetadataStore *self,
+                                                 const gchar *local_name,
+                                                 const gchar *name,
+                                                 GValueTransform transform);
+
+/**
+ * gegl_metadata_store_notify:
+ * @self: A #GeglMetadataStore
+ * @pspec: The #GParamSpec used to declare the variable.
+ * @shadow: The metadata variable shadows a property.
+ *
+ * gegl_metadata_store_notify() is called by subclasses when the value of a
+ * metadata variable changes. It emits the `::changed` signal with the variable
+ * name as the detail parameter.  Set @shadow = %TRUE if variable is shadowed
+ * by a property so that a notify signal is emitted with the property name as
+ * the detail parameter.
+ */
+void          gegl_metadata_store_notify        (GeglMetadataStore *self,
+                                                 GParamSpec *pspec,
+                                                 gboolean shadow);
+
+#define GEGL_TYPE_RESOLUTION_UNIT       gegl_resolution_unit_get_type ()
+GType         gegl_resolution_unit_get_type     (void) G_GNUC_CONST;
+
+G_END_DECLS
+
+#endif
diff --git a/gegl/meson.build b/gegl/meson.build
index 12c753cab..2bcaf8f53 100644
--- a/gegl/meson.build
+++ b/gegl/meson.build
@@ -29,6 +29,9 @@ gegl_introspectable_headers = files(
   'gegl-init.h',
   'gegl-lookup.h',
   'gegl-matrix.h',
+  'gegl-metadata.h',
+  'gegl-metadatastore.h',
+  'gegl-metadatahash.h',
   'gegl-operations-util.h',
   'gegl-parallel.h',
   'gegl-random.h',
@@ -58,6 +61,9 @@ gegl_sources = files(
   'gegl-introspection-support.c',
   'gegl-lookup.c',
   'gegl-matrix.c',
+  'gegl-metadata.c',
+  'gegl-metadatastore.c',
+  'gegl-metadatahash.c',
   'gegl-parallel.c',
   'gegl-random.c',
   'gegl-serialize.c',
diff --git a/meson.build b/meson.build
index ee932ee4a..1b3246deb 100644
--- a/meson.build
+++ b/meson.build
@@ -150,6 +150,7 @@ config.set('HAVE_UNISTD_H',    cc.has_header('unistd.h'))
 config.set('HAVE_EXECINFO_H',  cc.has_header('execinfo.h'))
 config.set('HAVE_FSYNC',       cc.has_function('fsync'))
 config.set('HAVE_MALLOC_TRIM', cc.has_function('malloc_trim'))
+config.set('HAVE_STRPTIME',    cc.has_function('strptime'))
 
 math    = cc.find_library('m', required: false)
 libdl   = cc.find_library('dl', required : false)
diff --git a/operations/external/jpg-save.c b/operations/external/jpg-save.c
index dcf3cdecb..a38601e47 100644
--- a/operations/external/jpg-save.c
+++ b/operations/external/jpg-save.c
@@ -18,6 +18,8 @@
 
 #include "config.h"
 #include <glib/gi18n-lib.h>
+#include <gegl-metadata.h>
+#include <math.h>
 
 
 #ifdef GEGL_PROPERTIES
@@ -41,6 +43,9 @@ property_boolean (progressive, _("Progressive"), TRUE)
 property_boolean (grayscale, _("Grayscale"), FALSE)
   description (_("Create a grayscale (monochrome) image"))
 
+property_object(metadata, _("Metadata"), GEGL_TYPE_METADATA)
+  description (_("Object to supply image metadata"))
+
 #else
 
 #define GEGL_OP_SINK
@@ -54,6 +59,42 @@ property_boolean (grayscale, _("Grayscale"), FALSE)
 
 static const gsize buffer_size = 4096;
 
+static void
+iso8601_format_timestamp (const GValue *src_value, GValue *dest_value)
+{
+  GDateTime *datetime;
+  gchar *datestr;
+
+  g_return_if_fail (G_TYPE_CHECK_VALUE_TYPE (src_value, G_TYPE_DATE_TIME));
+  g_return_if_fail (G_VALUE_HOLDS_STRING (dest_value));
+
+  datetime = g_value_get_boxed (src_value);
+  g_return_if_fail (datetime != NULL);
+
+#if GLIB_CHECK_VERSION(2,62,0)
+  datestr = g_date_time_format_iso8601 (datetime);
+#else
+  datestr = g_date_time_format (datetime, "%FT%TZ");
+#endif
+  g_return_if_fail (datestr != NULL);
+
+  g_value_take_string (dest_value, datestr);
+}
+
+static const GeglMetadataMap jpeg_save_metadata[] =
+{
+  { "Artist",                 "artist",       NULL },
+  { "Comment",                "comment",      NULL },
+  { "Copyright",              "copyright",    NULL },
+  { "Description",            "description",  NULL },
+  { "Disclaimer",             "disclaimer",   NULL },
+  { "Software",               "software",     NULL },
+  { "Timestamp",              "timestamp",    iso8601_format_timestamp },
+  { "Title",                  "title",        NULL },
+  { "Warning",                "warning",      NULL },
+};
+
+
 static void
 init_buffer (j_compress_ptr cinfo)
 {
@@ -232,7 +273,8 @@ export_jpg (GeglOperation               *operation,
             gint                         smoothing,
             gboolean                     optimize,
             gboolean                     progressive,
-            gboolean                     grayscale)
+            gboolean                     grayscale,
+            GeglMetadata                *metadata)
 {
   gint     src_x, src_y;
   gint     width, height;
@@ -296,8 +338,68 @@ export_jpg (GeglOperation               *operation,
   cinfo.restart_interval = 0;
   cinfo.restart_in_rows = 0;
 
+  /* Resolution */
+  if (metadata != NULL)
+    {
+      GeglResolutionUnit unit;
+      gfloat resx, resy;
+
+      gegl_metadata_register_map (metadata, "gegl:jpg-save", 0,
+                                  jpeg_save_metadata,
+                                  G_N_ELEMENTS (jpeg_save_metadata));
+
+      if (gegl_metadata_get_resolution (metadata, &unit, &resx, &resy))
+        switch (unit)
+          {
+          case GEGL_RESOLUTION_UNIT_DPI:
+            cinfo.density_unit = 1;               /* dots/inch */
+            cinfo.X_density = lroundf (resx);
+            cinfo.Y_density = lroundf (resy);
+            break;
+          case GEGL_RESOLUTION_UNIT_DPM:
+            cinfo.density_unit = 2;               /* dots/cm */
+            cinfo.X_density = lroundf (resx / 100.0f);
+            cinfo.Y_density = lroundf (resy / 100.0f);
+            break;
+          case GEGL_RESOLUTION_UNIT_NONE:
+          default:
+            cinfo.density_unit = 0;               /* unknown */
+            cinfo.X_density = lroundf (resx);
+            cinfo.Y_density = lroundf (resy);
+            break;
+          }
+    }
+
   jpeg_start_compress (&cinfo, TRUE);
 
+  if (metadata != NULL)
+    {
+      GValue value = G_VALUE_INIT;
+      GeglMetadataIter iter;
+      const gchar *keyword, *text;
+      GString *string;
+
+      string = g_string_new (NULL);
+
+      g_value_init (&value, G_TYPE_STRING);
+      gegl_metadata_iter_init (metadata, &iter);
+      while ((keyword = gegl_metadata_iter_next (metadata, &iter)) != NULL)
+        {
+          if (gegl_metadata_iter_get_value (metadata, &iter, &value))
+            {
+              text = g_value_get_string (&value);
+              g_string_append_printf (string, "## %s\n", keyword);
+              g_string_append (string, text);
+              g_string_append (string, "\n\n");
+            }
+        }
+      jpeg_write_marker (&cinfo, JPEG_COM, (guchar *) string->str, string->len);
+      g_value_unset (&value);
+      g_string_free (string, TRUE);
+
+      gegl_metadata_unregister_map (metadata);
+    }
+
   {
     int icc_len;
     const char *icc_profile;
@@ -384,7 +486,8 @@ process (GeglOperation       *operation,
   cinfo.dest = &dest;
 
   if (export_jpg (operation, input, result, cinfo,
-                  o->quality, o->smoothing, o->optimize, o->progressive, o->grayscale))
+                  o->quality, o->smoothing, o->optimize, o->progressive, o->grayscale,
+                  GEGL_METADATA (o->metadata)))
     {
       status = FALSE;
       g_warning("could not export JPEG file");
diff --git a/operations/external/png-load.c b/operations/external/png-load.c
index 2ef82bc22..28528bb33 100644
--- a/operations/external/png-load.c
+++ b/operations/external/png-load.c
@@ -19,8 +19,13 @@
  */
 
 #include "config.h"
+#ifdef HAVE_STRPTIME
+#define _XOPEN_SOURCE
+#include <time.h>
+#endif
 #include <glib/gi18n-lib.h>
 #include <gegl-gio-private.h>
+#include <gegl-metadata.h>
 
 #ifdef GEGL_PROPERTIES
 
@@ -28,6 +33,8 @@ property_file_path (path, _("File"), "")
   description (_("Path of file to load."))
 property_uri (uri, _("URI"), "")
   description (_("URI for file to load."))
+property_object (metadata, _("Metadata"), GEGL_TYPE_METADATA)
+  description (_("Object to supply image metadata"))
 
 #else
 
@@ -56,6 +63,54 @@ static GQuark error_quark(void)
   return g_quark_from_static_string ("gegl:load-png-error-quark");
 }
 
+#ifdef HAVE_STRPTIME
+static void
+png_parse_timestamp (const GValue *src_value, GValue *dest_value)
+{
+  GDateTime *datetime;
+  struct tm tm;
+  GTimeZone *tz;
+  const gchar *datestr;
+  gchar *ret;
+
+  g_return_if_fail (G_VALUE_HOLDS_STRING (src_value));
+  g_return_if_fail (G_TYPE_CHECK_VALUE_TYPE (dest_value, G_TYPE_DATE_TIME));
+
+  datestr = g_value_get_string (src_value);
+  g_return_if_fail (datestr != NULL);
+
+  /* PNG reccommends RFC 1123 but is loose in this respect. If parsing
+     fails, try again for an ISO 8601 date-time. */
+  tz = g_time_zone_new_utc ();
+  ret = strptime (datestr, "%a, %d %b %Y %H:%M:%S %z", &tm);
+  if (ret == NULL)
+    datetime = g_date_time_new_from_iso8601 (datestr, tz);
+  else
+    datetime = g_date_time_new (tz, tm.tm_year + 1900, tm.tm_mon + 1,
+                                tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
+  g_time_zone_unref (tz);
+
+  g_return_if_fail (datetime != NULL);
+  g_value_take_boxed (dest_value, datetime);
+}
+#endif
+
+static const GeglMetadataMap png_load_metadata[] =
+{
+  { "Title",                "title",        NULL },
+  { "Author",               "artist",       NULL },
+  { "Description",          "description",  NULL },
+  { "Copyright",            "copyright",    NULL },
+#ifdef HAVE_STRPTIME
+  { "Creation Time",        "timestamp",    png_parse_timestamp },
+#endif
+  { "Software",             "software",     NULL },
+  { "Disclaimer",           "disclaimer",   NULL },
+  { "Warning",              "warning",      NULL },
+  { "Source",               "source",       NULL },
+  { "Comment",              "comment",      NULL },
+};
+
 static void
 read_fn(png_structp png_ptr, png_bytep buffer, png_size_t length)
 {
@@ -220,6 +275,7 @@ gegl_buffer_import_png (GeglBuffer  *gegl_buffer,
                         gint        *ret_width,
                         gint        *ret_height,
                         const Babl  *format, // can be NULL
+                        GeglMetadata *metadata, // can be NULL
                         GError **err)
 {
   gint           width;
@@ -361,6 +417,38 @@ gegl_buffer_import_png (GeglBuffer  *gegl_buffer,
     }
 
     png_read_update_info (load_png_ptr, load_info_ptr);
+
+    if (metadata != NULL)
+      {
+        GValue value = G_VALUE_INIT;
+        png_textp text;
+        int i, size, unit;
+        png_uint_32 xres, yres;
+        GeglResolutionUnit resunit;
+        GeglMetadataIter iter;
+
+        gegl_metadata_register_map (metadata, "gegl:png-load", 0,
+                                    png_load_metadata,
+                                    G_N_ELEMENTS (png_load_metadata));
+
+        png_get_text (load_png_ptr, load_info_ptr, &text, &size);
+        g_value_init (&value, G_TYPE_STRING);
+        for (i = 0; i < size; i++)
+          {
+            g_value_set_static_string (&value, text[i].text);
+            if (gegl_metadata_iter_lookup (metadata, &iter, text[i].key))
+              gegl_metadata_iter_set_value (metadata, &iter, &value);
+          }
+        g_value_unset (&value);
+
+        if (png_get_pHYs (load_png_ptr, load_info_ptr, &xres, &yres, &unit))
+          {
+            resunit = unit == 1 ? GEGL_RESOLUTION_UNIT_DPM : GEGL_RESOLUTION_UNIT_NONE;
+            gegl_metadata_set_resolution (metadata, resunit, xres, yres);
+          }
+
+        gegl_metadata_unregister_map (metadata);
+      }
   }
 
   pixels = g_malloc0 (width*bpp);
@@ -522,7 +610,7 @@ process (GeglOperation       *operation,
   GInputStream *stream = gegl_gio_open_input_stream(o->uri, o->path, &infile, &err);
   WARN_IF_ERROR(err);
   problem = gegl_buffer_import_png (output, stream, 0, 0,
-                                    &width, &height, format, &err);
+                                    &width, &height, format, GEGL_METADATA (o->metadata), &err);
   WARN_IF_ERROR(err);
   g_input_stream_close(stream, NULL, NULL);
 
diff --git a/operations/external/png-save.c b/operations/external/png-save.c
index b57d933d4..8b0fa94f8 100644
--- a/operations/external/png-save.c
+++ b/operations/external/png-save.c
@@ -19,6 +19,8 @@
 
 #include "config.h"
 #include <glib/gi18n-lib.h>
+#include <gegl-metadata.h>
+#include <math.h>
 
 
 #ifdef GEGL_PROPERTIES
@@ -31,6 +33,8 @@ property_int (compression, _("Compression"), 3)
 property_int (bitdepth, _("Bitdepth"), 16)
   description (_("8 and 16 are the currently accepted values."))
   value_range (8, 16)
+property_object(metadata, _("Metadata"), GEGL_TYPE_METADATA)
+  description (_("Object to supply image metadata"))
 
 #else
 
@@ -42,6 +46,38 @@ property_int (bitdepth, _("Bitdepth"), 16)
 #include <gegl-gio-private.h>
 #include <png.h>
 
+static void
+png_format_timestamp (const GValue *src_value, GValue *dest_value)
+{
+  GDateTime *datetime;
+  gchar *datestr;
+
+  g_return_if_fail (G_TYPE_CHECK_VALUE_TYPE (src_value, G_TYPE_DATE_TIME));
+  g_return_if_fail (G_VALUE_HOLDS_STRING (dest_value));
+
+  datetime = g_value_get_boxed (src_value);
+  g_return_if_fail (datetime != NULL);
+
+  datestr = g_date_time_format (datetime, "%a, %d %b %Y %H:%M:%S %z");
+  g_return_if_fail (datestr != NULL);
+
+  g_value_take_string (dest_value, datestr);
+}
+
+static const GeglMetadataMap png_save_metadata[] =
+{
+  { "Title",                "title",        NULL },
+  { "Author",               "artist",       NULL },
+  { "Description",          "description",  NULL },
+  { "Copyright",            "copyright",    NULL },
+  { "Creation Time",        "timestamp",    png_format_timestamp },
+  { "Software",             "software",     NULL },
+  { "Disclaimer",           "disclaimer",   NULL },
+  { "Warning",              "warning",      NULL },
+  { "Source",               "source",       NULL },
+  { "Comment",              "comment",      NULL },
+};
+
 static void
 write_fn(png_structp png_ptr, png_bytep buffer, png_size_t length)
 {
@@ -75,6 +111,15 @@ error_fn(png_structp png_ptr, png_const_charp msg)
   g_printerr("LIBPNG ERROR: %s", msg);
 }
 
+static void
+clear_png_text (gpointer data)
+{
+  png_text *text = data;
+
+  g_free (text->key);
+  g_free (text->text);
+}
+
 static gint
 export_png (GeglOperation       *operation,
             GeglBuffer          *input,
@@ -82,7 +127,8 @@ export_png (GeglOperation       *operation,
             png_structp          png,
             png_infop            info,
             gint                 compression,
-            gint                 bit_depth)
+            gint                 bit_depth,
+            GeglMetadata        *metadata)
 {
   gint           i, src_x, src_y;
   png_uint_32    width, height;
@@ -93,6 +139,7 @@ export_png (GeglOperation       *operation,
   const Babl    *babl = gegl_buffer_get_format (input);
   const Babl    *space = babl_format_get_space (babl);
   const Babl    *format;
+  GArray        *itxt = NULL;
 
   src_x = result->x;
   src_y = result->y;
@@ -210,6 +257,64 @@ export_png (GeglOperation       *operation,
   {
   }
 
+  if (metadata != NULL)
+    {
+      GValue value = G_VALUE_INIT;
+      GeglMetadataIter iter;
+      GeglResolutionUnit unit;
+      gfloat resx, resy;
+      png_text text;
+      const gchar *keyword;
+
+      itxt = g_array_new (FALSE, FALSE, sizeof (png_text));
+      g_array_set_clear_func (itxt, clear_png_text);
+
+      gegl_metadata_register_map (metadata, "gegl:png-save", 0,
+                                  png_save_metadata,
+                                  G_N_ELEMENTS (png_save_metadata));
+
+      g_value_init (&value, G_TYPE_STRING);
+      gegl_metadata_iter_init (metadata, &iter);
+      while ((keyword = gegl_metadata_iter_next (metadata, &iter)) != NULL)
+        {
+          if (gegl_metadata_iter_get_value (metadata, &iter, &value))
+            {
+              memset (&text, 0, sizeof text);
+              text.compression = PNG_ITXT_COMPRESSION_NONE;
+              text.key = g_strdup (keyword);
+              text.text = g_value_dup_string (&value);
+              text.itxt_length = strlen (text.text);
+              text.lang = "en";
+              g_array_append_vals (itxt, &text, 1);
+            }
+        }
+      g_value_unset (&value);
+
+      if (itxt->len > 0)
+        png_set_text (png, info, (png_textp) itxt->data, itxt->len);
+
+      if (gegl_metadata_get_resolution (metadata, &unit, &resx, &resy))
+        switch (unit)
+        {
+        case GEGL_RESOLUTION_UNIT_DPI:
+          png_set_pHYs (png, info, lroundf (resx / 25.4f * 1000.0f),
+                                   lroundf (resy / 25.4f * 1000.0f),
+                                   PNG_RESOLUTION_METER);
+          break;
+        case GEGL_RESOLUTION_UNIT_DPM:
+          png_set_pHYs (png, info, lroundf (resx), lroundf (resy),
+                                   PNG_RESOLUTION_METER);
+          break;
+        case GEGL_RESOLUTION_UNIT_NONE:
+        default:
+          png_set_pHYs (png, info, lroundf (resx), lroundf (resy),
+                                    PNG_RESOLUTION_UNKNOWN);
+          break;
+        }
+
+      gegl_metadata_unregister_map (metadata);
+    }
+
   png_write_info (png, info);
 
 #if BYTE_ORDER == LITTLE_ENDIAN
@@ -236,6 +341,8 @@ export_png (GeglOperation       *operation,
 
   g_free (pixels);
 
+  if (itxt != NULL)
+    g_array_unref (itxt);
   return 0;
 }
 
@@ -273,7 +380,8 @@ process (GeglOperation       *operation,
 
   png_set_write_fn (png, stream, write_fn, flush_fn);
 
-  if (export_png (operation, input, result, png, info, o->compression, o->bitdepth))
+  if (export_png (operation, input, result, png, info, o->compression, o->bitdepth,
+                  GEGL_METADATA (o->metadata)))
     {
       status = FALSE;
       g_warning("could not export PNG file");
diff --git a/operations/external/tiff-load.c b/operations/external/tiff-load.c
index ccbc3a202..fc5ab8728 100644
--- a/operations/external/tiff-load.c
+++ b/operations/external/tiff-load.c
@@ -17,7 +17,12 @@
  */
 
 #include "config.h"
+#ifdef HAVE_STRPTIME
+#define _XOPEN_SOURCE
+#include <time.h>
+#endif
 #include <glib/gi18n-lib.h>
+#include <gegl-metadata.h>
 
 #ifdef GEGL_PROPERTIES
 
@@ -31,6 +36,9 @@ property_int(directory, _("Directory"), 1)
   value_range (1, G_MAXINT)
   ui_range (1, 16)
 
+property_object(metadata, _("Metadata"), GEGL_TYPE_METADATA)
+  description (_("Object to receive image metadata"))
+
 #else
 
 #define GEGL_OP_SOURCE
@@ -71,6 +79,48 @@ typedef struct
   gint height;
 } Priv;
 
+#ifdef HAVE_STRPTIME
+/* Parse the TIFF timestamp format - requires strptime() */
+static void
+tiff_parse_timestamp (const GValue *src_value, GValue *dest_value)
+{
+  GDateTime *datetime;
+  struct tm tm;
+  GTimeZone *tz;
+  const gchar *datestr;
+  gchar *ret;
+
+  g_return_if_fail (G_VALUE_HOLDS_STRING (src_value));
+  g_return_if_fail (G_TYPE_CHECK_VALUE_TYPE (dest_value, G_TYPE_DATE_TIME));
+
+  datestr = g_value_get_string (src_value);
+  g_return_if_fail (datestr != NULL);
+
+  ret = strptime (datestr, "%Y:%m:%d %T", &tm);
+  g_return_if_fail (ret != NULL);
+
+  tz = g_time_zone_new_local ();
+  datetime = g_date_time_new (tz, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
+                              tm.tm_hour, tm.tm_min, tm.tm_sec);
+  g_time_zone_unref (tz);
+
+  g_return_if_fail (datetime != NULL);
+  g_value_take_boxed (dest_value, datetime);
+}
+#endif
+
+static const GeglMetadataMap tiff_load_metadata[] =
+{
+  { "Artist",               "artist",       NULL },
+  { "Copyright",            "copyright",    NULL },
+#ifdef HAVE_STRPTIME
+  { "DateTime",             "timestamp",    tiff_parse_timestamp },
+#endif
+  { "ImageDescription",     "description",  NULL },
+  { "PageName",             "title",        NULL },
+  { "Software",             "software",     NULL },
+};
+
 static void
 cleanup(GeglOperation *operation)
 {
@@ -340,6 +390,19 @@ get_file_size(thandle_t handle)
   return (toff_t) size;
 }
 
+static void
+set_meta_string (GObject *metadata, const gchar *name, const gchar *value)
+{
+  GValue gvalue = G_VALUE_INIT;
+  GeglMetadataIter iter;
+
+  g_value_init (&gvalue, G_TYPE_STRING);
+  g_value_set_string (&gvalue, value);
+  if (gegl_metadata_iter_lookup (GEGL_METADATA (metadata), &iter, name))
+    gegl_metadata_iter_set_value (GEGL_METADATA (metadata), &iter, &gvalue);
+  g_value_unset (&gvalue);
+}
+
 static gint
 query_tiff(GeglOperation *operation)
 {
@@ -549,6 +612,62 @@ query_tiff(GeglOperation *operation)
   p->height = (gint) height;
   p->width = (gint) width;
 
+  if (o->metadata != NULL)
+    {
+      gfloat resx = 300.0f, resy = 300.0f;
+      gboolean have_x, have_y;
+      guint16 unit;
+      gchar *str;
+      GeglResolutionUnit resunit;
+
+      gegl_metadata_register_map (GEGL_METADATA (o->metadata),
+                                  "gegl:tiff-load",
+                                  GEGL_MAP_EXCLUDE_UNMAPPED,
+                                  tiff_load_metadata,
+                                  G_N_ELEMENTS (tiff_load_metadata));
+
+      TIFFGetFieldDefaulted (p->tiff, TIFFTAG_RESOLUTIONUNIT, &unit);
+      have_x = TIFFGetField (p->tiff, TIFFTAG_XRESOLUTION, &resx);
+      have_y = TIFFGetField (p->tiff, TIFFTAG_YRESOLUTION, &resy);
+      if (!have_x && have_y)
+        resx = resy;
+      else if (have_x && !have_y)
+        resy = resx;
+
+      switch (unit)
+        {
+        case RESUNIT_INCH:
+          resunit = GEGL_RESOLUTION_UNIT_DPI;
+          break;
+        case RESUNIT_CENTIMETER:
+          resunit = GEGL_RESOLUTION_UNIT_DPM;
+          resx *= 100.0f;
+          resy *= 100.0f;
+          break;
+        default:
+          resunit = GEGL_RESOLUTION_UNIT_NONE;
+          break;
+        }
+      gegl_metadata_set_resolution (GEGL_METADATA (o->metadata), resunit, resx, resy);
+
+      //XXX make and model for scanner
+
+      if (TIFFGetField (p->tiff, TIFFTAG_ARTIST, &str))
+        set_meta_string (o->metadata, "Artist", str);
+      if (TIFFGetField (p->tiff, TIFFTAG_COPYRIGHT, &str))
+        set_meta_string (o->metadata, "Copyright", str);
+      if (TIFFGetField (p->tiff, TIFFTAG_PAGENAME, &str))
+        set_meta_string (o->metadata, "PageName", str);
+      if (TIFFGetField (p->tiff, TIFFTAG_SOFTWARE, &str))
+        set_meta_string (o->metadata, "Software", str);
+      if (TIFFGetField (p->tiff, TIFFTAG_IMAGEDESCRIPTION, &str))
+        set_meta_string (o->metadata, "ImageDescription", str);
+      if (TIFFGetField (p->tiff, TIFFTAG_DATETIME, &str))
+        set_meta_string (o->metadata, "DateTime", str);
+
+      gegl_metadata_unregister_map (GEGL_METADATA (o->metadata));
+    }
+
   return 0;
 }
 
diff --git a/operations/external/tiff-save.c b/operations/external/tiff-save.c
index c63239204..06bc62aca 100644
--- a/operations/external/tiff-save.c
+++ b/operations/external/tiff-save.c
@@ -18,7 +18,7 @@
 
 #include "config.h"
 #include <glib/gi18n-lib.h>
-
+#include <gegl-metadata.h>
 
 #ifdef GEGL_PROPERTIES
 
@@ -31,6 +31,9 @@ property_int (fp, _("use floating point"), -1)
   description (_("floating point -1 means auto, 0 means integer 1 meant float."))
   value_range (-1, 1)
 
+property_object(metadata, _("Metadata"), GEGL_TYPE_METADATA)
+  description (_("Object to receive image metadata"))
+
 #else
 
 #define GEGL_OP_SINK
@@ -55,6 +58,34 @@ typedef struct
   TIFF *tiff;
 } Priv;
 
+static void
+tiff_format_timestamp (const GValue *src_value, GValue *dest_value)
+{
+  GDateTime *datetime;
+  gchar *datestr;
+
+  g_return_if_fail (G_TYPE_CHECK_VALUE_TYPE (src_value, G_TYPE_DATE_TIME));
+  g_return_if_fail (G_VALUE_HOLDS_STRING (dest_value));
+
+  datetime = g_value_get_boxed (src_value);
+  g_return_if_fail (datetime != NULL);
+
+  datestr = g_date_time_format (datetime, "%Y:%m:%d %T");
+  g_return_if_fail (datestr != NULL);
+
+  g_value_take_string (dest_value, datestr);
+}
+
+static const GeglMetadataMap tiff_save_metadata[] =
+{
+  { "Artist",               "artist",       NULL },
+  { "Copyright",            "copyright",    NULL },
+  { "DateTime",             "timestamp",    tiff_format_timestamp },
+  { "ImageDescription",     "description",  NULL },
+  { "PageName",             "title",        NULL },
+  { "Software",             "software",     NULL },
+};
+
 static void
 cleanup(GeglOperation *operation)
 {
@@ -366,6 +397,21 @@ save_contiguous(GeglOperation *operation,
   return 0;
 }
 
+static void
+SetFieldString (TIFF *tiff, guint tag, GeglMetadata *metadata, const gchar *name)
+{
+  GValue gvalue = G_VALUE_INIT;
+  GeglMetadataIter iter;
+
+  g_value_init (&gvalue, G_TYPE_STRING);
+  if (gegl_metadata_iter_lookup (metadata, &iter, name)
+      && gegl_metadata_iter_get_value (metadata, &iter, &gvalue))
+    {
+      TIFFSetField (tiff, tag, g_value_get_string (&gvalue));
+    }
+  g_value_unset (&gvalue);
+}
+
 static int
 export_tiff (GeglOperation *operation,
              GeglBuffer *input,
@@ -655,6 +701,57 @@ export_tiff (GeglOperation *operation,
 
   TIFFSetField(p->tiff, TIFFTAG_ROWSPERSTRIP, rows_per_stripe);
 
+  if (o->metadata != NULL)
+    {
+      GeglResolutionUnit unit;
+      gfloat xres, yres;
+
+      gegl_metadata_register_map (GEGL_METADATA (o->metadata),
+                                  "gegl:tiff-save",
+                                  GEGL_MAP_EXCLUDE_UNMAPPED,
+                                  tiff_save_metadata,
+                                  G_N_ELEMENTS (tiff_save_metadata));
+
+      if (gegl_metadata_get_resolution (GEGL_METADATA (o->metadata),
+                                        &unit, &xres, &yres))
+        switch (unit)
+        {
+          case GEGL_RESOLUTION_UNIT_DPI:
+            TIFFSetField (p->tiff, TIFFTAG_XRESOLUTION, xres);
+            TIFFSetField (p->tiff, TIFFTAG_YRESOLUTION, yres);
+            TIFFSetField (p->tiff, TIFFTAG_RESOLUTIONUNIT, RESUNIT_INCH);
+            break;
+          case GEGL_RESOLUTION_UNIT_DPM:
+            TIFFSetField (p->tiff, TIFFTAG_XRESOLUTION, xres / 100.0f);
+            TIFFSetField (p->tiff, TIFFTAG_YRESOLUTION, yres / 100.0f);
+            TIFFSetField (p->tiff, TIFFTAG_RESOLUTIONUNIT, RESUNIT_CENTIMETER);
+            break;
+          case GEGL_RESOLUTION_UNIT_NONE:
+          default:
+            TIFFSetField (p->tiff, TIFFTAG_XRESOLUTION, xres);
+            TIFFSetField (p->tiff, TIFFTAG_YRESOLUTION, yres);
+            TIFFSetField (p->tiff, TIFFTAG_RESOLUTIONUNIT, RESUNIT_NONE);
+            break;
+        }
+
+      //XXX make and model for scanner
+
+      SetFieldString (p->tiff, TIFFTAG_ARTIST,
+                      GEGL_METADATA (o->metadata), "Artist");
+      SetFieldString (p->tiff, TIFFTAG_COPYRIGHT,
+                      GEGL_METADATA (o->metadata), "Copyright");
+      SetFieldString (p->tiff, TIFFTAG_PAGENAME,
+                      GEGL_METADATA (o->metadata), "PageName");
+      SetFieldString (p->tiff, TIFFTAG_SOFTWARE,
+                      GEGL_METADATA (o->metadata), "Software");
+      SetFieldString (p->tiff, TIFFTAG_DATETIME,
+                      GEGL_METADATA (o->metadata), "DateTime");
+      SetFieldString (p->tiff, TIFFTAG_IMAGEDESCRIPTION,
+                      GEGL_METADATA (o->metadata), "ImageDescription");
+
+      gegl_metadata_unregister_map (GEGL_METADATA (o->metadata));
+    }
+
   return save_contiguous(operation, input, result, format);
 }
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]