[gegl] operations: Add gegl:rgbe-load and gegl:rgbe-save



commit bbdb9f68b5e6830966cd7ce2d3ace323498eb195
Author: Danny Robson <danny blubinc net>
Date:   Wed Jul 21 11:39:25 2010 +0200

    operations: Add gegl:rgbe-load and gegl:rgbe-save
    
    Add a radiance (rgbe) HDR image format reader and writer. Implements
    reading of uncompressed and new-rle scanlines, and writing of
    uncompressed scanlines.
    
    The RGBE load and save code is put in a separate library so it's easy
    to move out of the GEGL tree or replace with something else.
    
    Also add test cases for both loading and saving.

 Makefile.am                                  |    4 +
 configure.ac                                 |    2 +
 libs/.gitignore                              |    2 +
 libs/Makefile.am                             |    1 +
 libs/rgbe/.gitignore                         |    6 +
 libs/rgbe/Makefile.am                        |    3 +
 libs/rgbe/rgbe.c                             | 1011 ++++++++++++++++++++++++++
 libs/rgbe/rgbe.h                             |   96 +++
 operations/external/Makefile.am              |    9 +
 operations/external/rgbe-load.c              |  140 ++++
 operations/external/rgbe-save.c              |   91 +++
 tests/compositions/data/car-stack-eighth.hdr |  Bin 0 -> 50120 bytes
 tests/compositions/reference/rgbe-load.png   |  Bin 0 -> 53078 bytes
 tests/compositions/reference/rgbe-save.hdr   |  Bin 0 -> 50120 bytes
 tests/compositions/rgbe-load.xml             |    9 +
 tests/compositions/rgbe-save.xml             |   15 +
 16 files changed, 1389 insertions(+), 0 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 951acf0..fc230ac 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,6 +1,10 @@
 AUTOMAKE_OPTIONS = dist-bzip2
 
+# The libs directory needs to be before anything which can depend on its
+# libraries, otherwise any potentially rebuilt libs won't be picked up until
+# the next run of make.
 SUBDIRS=\
+	libs \
 	gegl \
 	operations \
 	bin \
diff --git a/configure.ac b/configure.ac
index e874f14..4bb568f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1006,6 +1006,8 @@ gegl/module/Makefile
 gegl/operation/Makefile
 gegl/process/Makefile
 gegl/property-types/Makefile
+libs/Makefile
+libs/rgbe/Makefile
 operations/Makefile
 operations/affine/Makefile
 operations/core/Makefile
diff --git a/libs/.gitignore b/libs/.gitignore
new file mode 100644
index 0000000..b336cc7
--- /dev/null
+++ b/libs/.gitignore
@@ -0,0 +1,2 @@
+/Makefile
+/Makefile.in
diff --git a/libs/Makefile.am b/libs/Makefile.am
new file mode 100644
index 0000000..933933d
--- /dev/null
+++ b/libs/Makefile.am
@@ -0,0 +1 @@
+SUBDIRS = rgbe
diff --git a/libs/rgbe/.gitignore b/libs/rgbe/.gitignore
new file mode 100644
index 0000000..47cdde9
--- /dev/null
+++ b/libs/rgbe/.gitignore
@@ -0,0 +1,6 @@
+/.deps
+/.libs
+/Makefile
+/Makefile.in
+/librgbe.la
+/librgbe_la-rgbe.lo
diff --git a/libs/rgbe/Makefile.am b/libs/rgbe/Makefile.am
new file mode 100644
index 0000000..593fa04
--- /dev/null
+++ b/libs/rgbe/Makefile.am
@@ -0,0 +1,3 @@
+noinst_LTLIBRARIES = librgbe.la
+librgbe_la_SOURCES = rgbe.c rgbe.h
+librgbe_la_CFLAGS  = $(DEP_CFLAGS) -fPIC
diff --git a/libs/rgbe/rgbe.c b/libs/rgbe/rgbe.c
new file mode 100644
index 0000000..e9772f9
--- /dev/null
+++ b/libs/rgbe/rgbe.c
@@ -0,0 +1,1011 @@
+/* This file is an image processing operation for 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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright 2010 Danny Robson <danny blubinc net>
+ */
+
+#include "config.h"
+
+#include "rgbe.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <math.h>
+
+
+/* Scanlines are limited to 2^(16 - 1), as RLE encoded lines only have the
+ * lower 15 bits of the R&G components to store its length.
+ */
+#define RGBE_MAX_SCANLINE_WIDTH    (1 << 15)
+
+#define RGBE_MAX_SOFTWARE_LEN      63
+#define RGBE_NUM_RGB               3
+#define RGBE_NUM_RGBE              4
+#define RGBE_MAX_VARIABLE_LINE_LEN 24
+
+
+/* Describes the colour space of the pixel components. These values must
+ * index correctly into the array RGBE_FORMAT_STRINGS for correct operation.
+ */
+typedef enum
+{
+  FORMAT_RGBE,
+  FORMAT_XYZE,
+  FORMAT_UNKNOWN,
+
+  NUM_RGBE_FORMATS = FORMAT_XYZE
+} rgbe_format;
+
+/* Describes the behaviour of indices across, and between scanlines. */
+typedef enum
+{
+  ORIENT_DECREASING,
+  ORIENT_INCREASING,
+  ORIENT_UNKNOWN
+} rgbe_orientation;
+
+enum
+{
+  OFFSET_R = 0, OFFSET_X = OFFSET_R,
+  OFFSET_G = 1, OFFSET_Y = OFFSET_G,
+  OFFSET_B = 2, OFFSET_Z = OFFSET_B,
+  OFFSET_E = 3,
+  OFFSET_A = 3
+};
+
+typedef struct
+{
+    rgbe_orientation orient;
+    guint16          size;
+} rgbe_axis;
+
+typedef struct
+{
+  rgbe_format   format;
+
+  gchar         software[RGBE_MAX_SOFTWARE_LEN + 1];
+
+  gfloat        exposure;
+  gfloat        colorcorr[RGBE_NUM_RGB];
+  /* TODO: xyz primaries   */
+
+  /* TODO: view parameters */
+
+  /* resolution parameters */
+  rgbe_axis     x_axis,
+                y_axis;
+  gfloat        pixel_aspect;
+} rgbe_header;
+
+struct _rgbe_file
+{
+  rgbe_header  header;
+
+  GMappedFile *file;
+  /* Stores the address of the scanlines, or NULL */
+  const void  *scanlines;
+};
+
+
+static const gchar RADIANCE_MAGIC[] = "#?RADIANCE";
+
+static const gchar *RGBE_FORMAT_STRINGS[] =
+{
+  "32-bit_rle_rgbe",
+  "32-bit_rle_xyze",
+  NULL
+};
+
+
+/**
+ * rgbe_mapped_file_remaining:
+ * @f:    the file to read the image data from
+ * @data: the current file read cursor
+ *
+ * Calculates the number of bytes remaining to be read in a mapped file.
+ **/
+static guint
+rgbe_mapped_file_remaining (GMappedFile *f,
+                            const void  *data)
+{
+  g_return_val_if_fail (f, 0);
+  g_return_val_if_fail (GPOINTER_TO_UINT (data) >
+                        GPOINTER_TO_UINT (g_mapped_file_get_contents (f)), 0);
+
+  return GPOINTER_TO_UINT (data) -
+         GPOINTER_TO_UINT (g_mapped_file_get_contents (f)) -
+         g_mapped_file_get_length (f);
+}
+
+static void
+rgbe_header_init (rgbe_header *header)
+{
+  g_return_if_fail (header);
+
+  header->format              = FORMAT_UNKNOWN;
+  memset (header->software, '\0', G_N_ELEMENTS (header->software));
+
+  header->exposure            = 1.0;
+  header->colorcorr[OFFSET_R] = 1.0;
+  header->colorcorr[OFFSET_G] = 1.0;
+  header->colorcorr[OFFSET_B] = 1.0;
+
+  header->pixel_aspect        = 1.0;
+
+  header->x_axis.orient = header->y_axis.orient = ORIENT_UNKNOWN;
+  header->x_axis.size   = header->x_axis.size   = 0;
+}
+
+static gboolean
+rgbe_file_init (rgbe_file   *file,
+                const gchar *path)
+{
+  g_return_val_if_fail (file != NULL, FALSE);
+
+  rgbe_header_init (&file->header);
+  file->file      = g_mapped_file_new (path, FALSE, NULL);
+  file->scanlines = NULL;
+
+  return file->file != NULL;
+}
+
+static rgbe_file*
+rgbe_file_new (const gchar *path)
+{
+  rgbe_file *file;
+
+  g_return_val_if_fail (path, NULL);
+
+  file = g_new (rgbe_file, 1);
+  if (!rgbe_file_init (file, path))
+    {
+      rgbe_file_free (file);
+      file = NULL;
+    }
+
+  return file;
+}
+
+void
+rgbe_file_free (rgbe_file *file)
+{
+  if (!file)
+      return;
+
+  g_mapped_file_unref (file->file);
+  file->scanlines = NULL;
+
+  g_free (file);
+}
+
+/* Parse the variable initialisations for an rgbe header. Returns the offset
+ * after the header delimiting newline in cursor. The incoming cursor should
+ * (most likely) be zero.
+ *
+ * Updates cursor on success.
+ */
+static gboolean
+rgbe_header_read_variables (rgbe_file *file,
+                            goffset   *cursor)
+{
+  const gchar *data;
+  gboolean     success = FALSE;
+
+  g_return_val_if_fail (file,                  FALSE);
+  g_return_val_if_fail (file->file,            FALSE);
+  g_return_val_if_fail (cursor && *cursor > 0, FALSE);
+
+  data = g_mapped_file_get_contents (file->file) + *cursor;
+
+  /* Keep iterating if it looks like there's enough data to satisfy another
+   * line (the estimate doesn't need to be exact, as we can run a little over
+   * due to required resolution specification which will be coming up next).
+   */
+  while (rgbe_mapped_file_remaining (file->file, data) > RGBE_MAX_VARIABLE_LINE_LEN)
+    {
+      /* Check the colourspace/type of pixels in the file */
+      if (g_str_has_prefix (data, "FORMAT="))
+        {
+          guint i;
+          data += strlen ("FORMAT=");
+
+          file->header.format = FORMAT_UNKNOWN;
+          for (i = 0; i < NUM_RGBE_FORMATS; ++i)
+            {
+              if (g_str_has_prefix (data, RGBE_FORMAT_STRINGS[i]))
+                {
+                  file->header.format = (rgbe_format)i;
+                  break;
+                }
+            }
+
+          if (file->header.format != FORMAT_RGBE)
+            {
+              g_warning ("Unsupported color format for rgbe format");
+              goto cleanup;
+            }
+          continue;
+        }
+
+      /* Check the exposure multiplier */
+      else if (g_str_has_prefix (data, "EXPOSURE="))
+        {
+          gdouble exposure;
+
+          data    += strlen ("EXPOSURE=");
+          exposure = g_ascii_strtod (data, NULL);
+
+          if (errno)
+            {
+              g_warning ("Invalid value for exposure in radiance image file");
+              goto cleanup;
+            }
+          else
+            {
+              file->header.exposure *= exposure;
+            }
+        }
+
+      /* Parse the component multipliers */
+      else if (g_str_has_prefix (data, "COLORCORR="))
+        {
+          guint i;
+          data += strlen ("COLORCORR=");
+
+          for (i = 0; i < RGBE_NUM_RGB; ++i)
+            {
+              gdouble multiplier = g_ascii_strtod (data, (gchar**)&data);
+              if (errno)
+                {
+                  g_warning ("Invalid value for COLORCORR");
+                  goto cleanup;
+                }
+
+              file->header.colorcorr[i] *= multiplier;
+            }
+        }
+
+      /* Generating software identifier */
+      else if (g_str_has_prefix (data, "SOFTWARE="))
+        {
+          gchar * lineend;
+
+          data    += strlen ("SOFTWARE=");
+          lineend  = g_strstr_len (data,
+                                   MIN (rgbe_mapped_file_remaining (file->file,
+                                                                    data),
+                                        G_N_ELEMENTS (file->header.software)),
+                                   "\n");
+
+          if (!lineend)
+            {
+              g_warning ("Cannot find a usable value for SOFTWARE, ignoring");
+            }
+          else
+            {
+              guint linesize = lineend - data;
+              strncpy (file->header.software, data,
+                       MIN (linesize, G_N_ELEMENTS (file->header.software) - 1));
+            }
+        }
+
+      /* Ratio of pixel height to width */
+      else if (g_str_has_prefix (data, "PIXASPECT="))
+        {
+          gdouble aspect;
+
+          data  += strlen ("PIXASPECT=");
+          aspect = g_ascii_strtod (data, (gchar **)&data);
+
+          if (errno)
+            {
+              g_warning ("Invalid pixel aspect ratio");
+              goto cleanup;
+            }
+          else
+            {
+              file->header.pixel_aspect *= aspect;
+            }
+        }
+
+      /* We reached a blank line, so it's the end of the header */
+      else if (!strncmp (data, "\n", strlen ("\n")))
+        {
+          data += strlen ("\n");
+          *cursor = GPOINTER_TO_UINT (data) -
+                    GPOINTER_TO_UINT (g_mapped_file_get_contents (file->file));
+          success = TRUE;
+          goto cleanup;
+        }
+
+      /* Skip past the end of the line for the next variable */
+      data = g_strstr_len (data,
+                           rgbe_mapped_file_remaining (file->file, data),
+                           "\n");
+      if (!data)
+          goto cleanup;
+      data += strlen ("\n");
+    }
+
+cleanup:
+  return success;
+}
+
+
+/* Convert from '-' or '+' to useful scanline index constants
+ */
+static rgbe_orientation
+rgbe_char_to_orientation (gchar c)
+{
+  switch (c)
+    {
+      case '-':
+        return ORIENT_DECREASING;
+
+      case '+':
+        return ORIENT_INCREASING;
+
+      default:
+        return ORIENT_UNKNOWN;
+    }
+}
+
+
+/* Return the axis which the scanline index character refers to.
+ */
+static rgbe_axis*
+rgbe_char_to_axis (rgbe_file *file,
+                   gchar      c)
+{
+  switch (c)
+    {
+      case 'y':
+      case 'Y':
+        return &file->header.y_axis;
+
+      case 'x':
+      case 'X':
+        return &file->header.x_axis;
+
+      default:
+        return NULL;
+    }
+}
+
+
+/* Parse the orientation/resolution line. The following format is repeated
+ * twice: "[+-][XY] \d+" It specifies column or row major ordering, and the
+ * direction of pixel indices (eg, mirrored).
+ *
+ * Updates cursor on success.
+ */
+static gboolean
+rgbe_header_read_orientation (rgbe_file *file,
+                              goffset   *cursor)
+{
+  const gchar      *data;
+  rgbe_orientation  orient;
+  rgbe_axis        *axis;
+  gchar             firstaxis = '?';
+  gboolean          success = FALSE;
+
+  g_return_val_if_fail (file,                  FALSE);
+  g_return_val_if_fail (file->file,            FALSE);
+  g_return_val_if_fail (cursor && *cursor > 0, FALSE);
+
+  data = g_mapped_file_get_contents (file->file) + *cursor;
+
+  /* Read each direction, axis, and size until a newline is reached */
+  do
+    {
+      orient = rgbe_char_to_orientation (*data++);
+      if (orient == ORIENT_UNKNOWN)
+          goto cleanup;
+
+      /* Axis can be ordered with X major, which we don't currently handle */
+      if (firstaxis == '?' && *data != 'Y' && *data != 'y')
+          goto cleanup;
+      else
+          firstaxis = *data;
+
+      axis = rgbe_char_to_axis (file, *data++);
+      if (!axis)
+          goto cleanup;
+      axis->orient = orient;
+
+      if (*data++ != ' ')
+          goto cleanup;
+
+      axis->size = g_ascii_strtoull (data, (gchar **)&data, 0);
+      if (errno)
+          goto cleanup;
+
+  /* The termination check is simplified to a space check, as each set of
+   * axis parameters are space seperated. We double check for a newline next
+   * though.
+   */
+  } while (*data++ == ' ');
+
+  if (data[-1] != '\n')
+      goto cleanup;
+
+  *cursor = data - g_mapped_file_get_contents (file->file);
+  success = TRUE;
+
+cleanup:
+  return success;
+}
+
+
+/* Read each component of an rgbe file header. A pointer to the scanlines,
+ * immediately after the header, is cached on success.
+ */
+static gboolean
+rgbe_header_read (rgbe_file *file)
+{
+  gchar    *data;
+  gboolean  success = FALSE;
+  goffset   cursor  = 0;
+
+  g_return_val_if_fail (file,                   FALSE);
+  g_return_val_if_fail (file->file,             FALSE);
+
+  rgbe_header_init (&file->header);
+
+  data = g_mapped_file_get_contents (file->file);
+  if (strncmp (&data[cursor], RADIANCE_MAGIC, strlen (RADIANCE_MAGIC)))
+      goto cleanup;
+  cursor += strlen (RADIANCE_MAGIC);
+
+  if (data[cursor] != '\n')
+      goto cleanup;
+  ++cursor;
+
+  if (!rgbe_header_read_variables (file, &cursor))
+      goto cleanup;
+
+  if (!rgbe_header_read_orientation (file, &cursor))
+      goto cleanup;
+
+  file->scanlines = &data[cursor];
+  success = TRUE;
+cleanup:
+  return success;
+}
+
+
+/* Convert an array of gfloat mantissas to their full values. Applies the
+ * exponent, exposure compensation, and color channel compensation.
+ */
+static void
+rgbe_apply_exponent (const rgbe_file *file,
+                     gfloat          *rgb,
+                     gfloat           e)
+{
+  gfloat mult;
+
+  g_return_if_fail (file);
+  g_return_if_fail (rgb);
+
+  if (e == 0)
+    {
+      rgb[OFFSET_R] = rgb[OFFSET_G] = rgb[OFFSET_B] = 0;
+      goto cleanup;
+    }
+
+  mult = ldexp (1.0, e - (128 + 8));
+  rgb[OFFSET_R] *= mult                  *
+                   file->header.exposure *
+                   file->header.colorcorr[OFFSET_R];
+  rgb[OFFSET_G] *= mult                  *
+                   file->header.exposure *
+                   file->header.colorcorr[OFFSET_G];
+  rgb[OFFSET_B] *= mult                  *
+                   file->header.exposure *
+                   file->header.colorcorr[OFFSET_B];
+  rgb[OFFSET_A]  = 1.0f;
+
+cleanup:
+  return;
+}
+
+
+/* Convert an array of RGBE uints to their floating point format (applying
+ * exponents and compensations as required).
+ */
+static void
+rgbe_rgbe_to_float (const rgbe_file *file,
+                    const guint8    *rgbe,
+                    gfloat          *output)
+{
+  g_return_if_fail (file);
+  g_return_if_fail (rgbe);
+  g_return_if_fail (output);
+
+  output[OFFSET_R] = rgbe[OFFSET_R];
+  output[OFFSET_G] = rgbe[OFFSET_G];
+  output[OFFSET_B] = rgbe[OFFSET_B];
+  output[OFFSET_A] = 1.0f;
+
+  rgbe_apply_exponent (file, output, rgbe[OFFSET_E]);
+}
+
+
+/* Read one uncompressed scanline row. Updates cursor on success. */
+static gboolean
+rgbe_read_uncompressed (const rgbe_file *file,
+                        goffset         *cursor,
+                        gfloat          *pixels)
+{
+  const guint8 *data;
+  guint         i;
+
+  g_return_val_if_fail (file,                  FALSE);
+  g_return_val_if_fail (file->file,            FALSE);
+  g_return_val_if_fail (cursor && *cursor > 0, FALSE);
+  g_return_val_if_fail (pixels,                FALSE);
+
+  data = (guint8 *)g_mapped_file_get_contents (file->file) + *cursor;
+
+  for (i = 0; i < file->header.x_axis.size; ++i)
+    {
+      rgbe_rgbe_to_float (file, data, pixels);
+      data   += RGBE_NUM_RGBE;
+      pixels += RGBE_NUM_RGBE;
+    }
+
+  *cursor   = GPOINTER_TO_UINT (data) -
+              GPOINTER_TO_UINT (g_mapped_file_get_contents (file->file));
+  return TRUE;
+}
+
+
+/* Read an old style rle scanline row. Unimplemented */
+static gboolean
+rgbe_read_old_rle (const rgbe_file *file,
+                   goffset         *cursor,
+                   gfloat          *pixels)
+{
+  /* const gchar * data = g_mapped_file_get_contents (f) + *cursor; */
+
+  g_return_val_if_fail (file,                  FALSE);
+  g_return_val_if_fail (file->file,            FALSE);
+  g_return_val_if_fail (cursor && *cursor > 0, FALSE);
+  g_return_val_if_fail (pixels,                FALSE);
+
+  g_return_val_if_reached (FALSE);
+}
+
+
+/* Read one new style rle scanline row. Updates cursor on success. */
+static gboolean
+rgbe_read_new_rle (const rgbe_file *file,
+                   goffset         *cursor,
+                   gfloat          *pixels)
+{
+  const guint8 *data;
+  guint16       linesize;
+  guint         i;
+  guint         component;
+  gfloat       *pixoffset[RGBE_NUM_RGBE] =
+    {
+      pixels + OFFSET_R,
+      pixels + OFFSET_G,
+      pixels + OFFSET_B,
+      pixels + OFFSET_E
+    };
+
+  g_return_val_if_fail (file,                  FALSE);
+  g_return_val_if_fail (file->file,            FALSE);
+  g_return_val_if_fail (cursor && *cursor > 0, FALSE);
+  g_return_val_if_fail (pixels,                FALSE);
+
+  /* Read the scanline header: two magic bytes, and two byte pixel count. We
+   * can assert on the magic as it should have been checked before
+   * dispatching to this decoding routine.
+   */
+  data     = (guint8 *)g_mapped_file_get_contents (file->file) + *cursor;
+  g_return_val_if_fail (data[OFFSET_R] == 2 && data[OFFSET_G] == 2, FALSE);
+  linesize = (data[OFFSET_B] << 8) | data[OFFSET_E];
+
+  data += RGBE_NUM_RGBE;
+
+  /* Decode the rle/dump sequences for each color channel, continuing until
+   * we've reached the expected offsets for each channel. Stores the exponent
+   * values in the alpha channel temporarily.
+   */
+  for (component = 0; component < RGBE_NUM_RGBE; ++component)
+    {
+      while (pixoffset[component] < pixels + RGBE_NUM_RGBE * linesize)
+        {
+          const guint HIGH_BIT = (1 << 7);
+          gboolean         rle = *data &  HIGH_BIT;
+          guint         length = *data & ~HIGH_BIT;
+
+          /* A dump/run of 0 is a special marker for dump 128 */
+          if (length == 0)
+            {
+              rle    = FALSE;
+              length = 128;
+            }
+
+          data++;
+
+          /* A compressed run */
+          if (rle)
+            {
+              for (i = 0; i < length; ++i)
+                {
+                  *pixoffset[component]  = *data;
+                   pixoffset[component] += RGBE_NUM_RGBE;
+                }
+
+              data++;
+            }
+          /* A dump of values */
+          else
+            {
+              for (i = 0; i < length; ++i)
+                {
+                   *pixoffset[component]  = *data;
+                    pixoffset[component] += RGBE_NUM_RGBE;
+                   data++;
+                }
+            }
+        }
+    }
+
+  /* Double check we encountered as many pixels as expected. Pixoffsets should
+   * have been incremented to just past the final pixel for each component.
+   */
+  for (component = 0; component < RGBE_NUM_RGBE; ++component)
+    {
+      g_warn_if_fail (pixoffset[component] == pixels + RGBE_NUM_RGBE * linesize + component);
+    }
+
+  /* Multiply the colours by the exponent. Remove 'transparency' by setting
+   * alpha high as a precaution, it should be discarded in any case.
+   */
+  for (i = 0; i < linesize; ++i)
+    {
+      gfloat *pixel = pixels + i * RGBE_NUM_RGBE;
+      rgbe_apply_exponent (file, pixel, pixel[OFFSET_E]);
+    }
+
+  *cursor = GPOINTER_TO_UINT (data) -
+            GPOINTER_TO_UINT (g_mapped_file_get_contents (file->file));
+
+  return TRUE;
+}
+
+
+/* Write a null terminated string (with user provided trailing newline) to
+ * the output file, freeing the line and returning an error if needed.
+ */
+static gboolean
+rgbe_write_line (FILE *f, gchar *line)
+{
+  size_t written;
+  guint  len = strlen (line);
+
+  g_return_val_if_fail (g_str_has_suffix (line, "\n"), FALSE);
+  written = fwrite (line, sizeof (line[0]), len, f);
+  g_free (line);
+
+  return written == len ? TRUE : FALSE;
+}
+
+
+/* Write all rgbe header variables (which aren't defaults) out to a file. */
+static gboolean
+rgbe_header_write (const rgbe_header *header,
+                   FILE              *f)
+{
+  gchar    *line    = NULL;
+  gboolean  success = FALSE;
+
+  g_return_val_if_fail (header, FALSE);
+  g_return_val_if_fail (f,      FALSE);
+
+  /* Magic header bytes */
+  line = g_strconcat (RADIANCE_MAGIC, "\n", NULL);
+  if (!rgbe_write_line (f, line))
+      goto cleanup;
+
+  /* Insert the package name as the software name if not present (zero len)
+   * or we don't have a null terminated name length.
+   */
+  if ( strnlen (header->software, RGBE_MAX_SOFTWARE_LEN) ==                0 ||
+      (strnlen (header->software, RGBE_MAX_SOFTWARE_LEN) == RGBE_MAX_SOFTWARE_LEN &&
+       header->software[RGBE_MAX_SOFTWARE_LEN - 1] != '\0'))
+    {
+      line = g_strconcat ("SOFTWARE=", PACKAGE_STRING, "\n", NULL);
+    }
+  else
+    {
+      line = g_strconcat ("SOFTWARE=", header->software, "\n", NULL);
+    }
+  if (!rgbe_write_line (f, line))
+      goto cleanup;
+
+  /* Type of pixel components */
+  g_return_val_if_fail (header->format < FORMAT_UNKNOWN,                     FALSE);
+  g_return_val_if_fail (header->format < G_N_ELEMENTS (RGBE_FORMAT_STRINGS), FALSE);
+  line = g_strconcat ("FORMAT=",
+                      RGBE_FORMAT_STRINGS[header->format],
+                      "\n", NULL);
+  if (!rgbe_write_line (f, line))
+      goto cleanup;
+
+  /* Exposure compensation */
+  if (header->exposure != 1.0)
+    {
+      gchar exp_line[G_ASCII_DTOSTR_BUF_SIZE + 1];
+      line = g_strconcat ("EXPOSURE=",
+                          g_ascii_dtostr (exp_line,
+                                          G_N_ELEMENTS (exp_line),
+                                          header->exposure),
+                          "\n",
+                          NULL);
+      if (!rgbe_write_line (f, line))
+          goto cleanup;
+    }
+
+  /* Color channel correction */
+  if (header->colorcorr [OFFSET_R] != 1.0 &&
+      header->colorcorr [OFFSET_G] != 1.0 &&
+      header->colorcorr [OFFSET_B] != 1.0)
+  {
+    gchar corr_line[G_ASCII_DTOSTR_BUF_SIZE + 1][RGBE_NUM_RGB];
+    line = g_strconcat ("COLORCORR=",
+                        g_ascii_dtostr (corr_line[OFFSET_R],
+                                        G_N_ELEMENTS (corr_line[OFFSET_R]),
+                                        header->colorcorr[OFFSET_R]), " ",
+                        g_ascii_dtostr (corr_line[OFFSET_G],
+                                        G_N_ELEMENTS (corr_line[OFFSET_G]),
+                                        header->colorcorr[OFFSET_G]), " ",
+                        g_ascii_dtostr (corr_line[OFFSET_B],
+                                        G_N_ELEMENTS (corr_line[OFFSET_B]),
+                                        header->colorcorr[OFFSET_R]),
+                        "\n",
+                        NULL);
+    if (!rgbe_write_line (f, line))
+        goto cleanup;
+  }
+
+
+  /* Resolution specifier */
+  {
+    const guint res_line_sz = strlen ("\n")
+                            + strlen ("-Y ") * 2
+                            + strlen (G_STRINGIFY (RGBE_MAX_SCANLINE_WIDTH)) * 2
+                            + strlen ("\n")
+                            + 1;
+    gint err;
+
+    line = g_malloc (res_line_sz * sizeof (line[0]));
+    err  = snprintf (line, res_line_sz,
+                     "\n-Y %hu +X %hu\n",
+                     header->y_axis.size,
+                     header->x_axis.size);
+    if (err < 0 || !rgbe_write_line (f, line))
+        goto cleanup;
+  }
+
+  success = TRUE;
+cleanup:
+  return success;
+}
+
+
+/* Convert an array of floats to rgbe components for file output */
+static void
+rgbe_float_to_rgbe (const gfloat *f,
+                    guint8       *rgbe)
+{
+  gint   exp;
+  gfloat frac, max;
+
+  g_return_if_fail (f);
+  g_return_if_fail (rgbe);
+
+  max =           f[OFFSET_R];
+  max = MAX (max, f[OFFSET_G]);
+  max = MAX (max, f[OFFSET_B]);
+
+  if (max < 1e-38)
+    {
+      rgbe[OFFSET_R] = rgbe[OFFSET_G] = rgbe[OFFSET_B] = 0;
+      goto cleanup;
+    }
+
+  frac  = frexp (max, &exp) * 256.0 / max;
+
+  rgbe[OFFSET_R] = f[OFFSET_R] * frac;
+  rgbe[OFFSET_G] = f[OFFSET_G] * frac;
+  rgbe[OFFSET_B] = f[OFFSET_B] * frac;
+
+  rgbe[OFFSET_E] = exp + 128;
+
+cleanup:
+  return;
+}
+
+
+/* Write the first scanline from pixels into the file. Does not use RLE. */
+static gboolean
+rgbe_write_uncompressed (const rgbe_header *header,
+                         const gfloat      *pixels,
+                         FILE              *f)
+{
+  guint    x, y;
+  guint8   rgbe[RGBE_NUM_RGBE];
+  gboolean success = TRUE;
+
+  g_return_val_if_fail (header, FALSE);
+  g_return_val_if_fail (pixels, FALSE);
+  g_return_val_if_fail (f,      FALSE);
+
+  for (y = 0; y < header->y_axis.size; ++y)
+      for (x = 0; x < header->x_axis.size; ++x)
+        {
+          rgbe_float_to_rgbe (pixels, rgbe);
+
+          /* Ensure we haven't inadvertantly triggered an rle scanline */
+          g_warn_if_fail (rgbe[0] != 2 || rgbe[1] != 2);
+          g_warn_if_fail (rgbe[0] != 1 || rgbe[1] != 1 || rgbe[2] != 1);
+
+          if (G_N_ELEMENTS (rgbe) != fwrite (rgbe, sizeof (rgbe[0]), G_N_ELEMENTS (rgbe), f))
+            success = FALSE;
+          pixels += RGBE_NUM_RGB;
+        }
+
+  return success;
+}
+
+
+gboolean
+rgbe_save_path (const gchar *path,
+                guint        width,
+                guint        height,
+                gfloat      *pixels)
+{
+  rgbe_header  header;
+  FILE        *f       = NULL;
+  gboolean     success = FALSE;
+
+  f = (!strcmp (path, "-") ? stdout : fopen(path, "wb"));
+  if (!f)
+      goto cleanup;
+
+  rgbe_header_init (&header);
+  header.x_axis.orient = ORIENT_INCREASING;
+  header.x_axis.size   = width;
+  header.y_axis.orient = ORIENT_DECREASING;
+  header.y_axis.size   = height;
+  header.format        = FORMAT_RGBE;
+
+  success = rgbe_header_write  (&header, f);
+  if (!success)
+      goto cleanup;
+
+  success = rgbe_write_uncompressed (&header, pixels, f);
+
+cleanup:
+  if (f)
+      fclose (f);
+
+  return success;
+}
+
+
+rgbe_file *
+rgbe_load_path (const gchar *path)
+{
+  gboolean success = FALSE;
+  rgbe_file *file;
+
+  file = rgbe_file_new (path);
+  if (!file)
+      goto cleanup;
+
+  if (!rgbe_header_read (file))
+      goto cleanup;
+  success = TRUE;
+
+cleanup:
+  if (!success)
+    {
+      rgbe_file_free (file);
+      file = NULL;
+    }
+  return file;
+}
+
+
+gboolean
+rgbe_get_size (rgbe_file *file,
+               guint     *x,
+               guint     *y)
+{
+  g_return_val_if_fail (file, FALSE);
+  *x = file->header.x_axis.size;
+  *y = file->header.y_axis.size;
+
+  return TRUE;
+}
+
+
+/* Peek on each scanline row to dispatch to decoders.
+ *
+ * - Assumes row major ordering.
+ * - Assumes cursor is at the start of a scanline
+ * - Updates cursor, which is undefined on error.
+ */
+gfloat *
+rgbe_read_scanlines (rgbe_file *file)
+{
+  guint     i;
+  gboolean  success = FALSE;
+  gfloat   *pixels  = NULL,
+           *pixel_cursor;
+  goffset   offset;
+
+  g_return_val_if_fail (file,            NULL);
+  g_return_val_if_fail (file->scanlines, NULL);
+
+  pixels = pixel_cursor = g_new (gfloat, file->header.x_axis.size *
+                                         file->header.y_axis.size *
+                                         RGBE_NUM_RGBE);
+  offset = GPOINTER_TO_UINT (file->scanlines) -
+           GPOINTER_TO_UINT (g_mapped_file_get_contents (file->file));
+
+  for (i = 0; i < file->header.y_axis.size; ++i)
+    {
+      const gchar *data = g_mapped_file_get_contents (file->file);
+
+      if (data[offset + OFFSET_R] == 1 &&
+          data[offset + OFFSET_G] == 1 &&
+          data[offset + OFFSET_B] == 1)
+        success = rgbe_read_old_rle      (file, &offset, pixel_cursor);
+      else if (data[offset + OFFSET_R] == 2 &&
+               data[offset + OFFSET_G] == 2)
+        success = rgbe_read_new_rle      (file, &offset, pixel_cursor);
+      else
+        success = rgbe_read_uncompressed (file, &offset, pixel_cursor);
+
+      if (!success)
+        {
+          g_warning ("Unable to parse rgbe scanlines, fail at row %u\n", i);
+          goto cleanup;
+        }
+      pixel_cursor += file->header.x_axis.size * RGBE_NUM_RGBE;
+    }
+
+  success = TRUE;
+
+cleanup:
+  if (!success)
+    {
+      g_free (pixels);
+      pixels = NULL;
+    }
+
+  return pixels;
+}
+
+
+
diff --git a/libs/rgbe/rgbe.h b/libs/rgbe/rgbe.h
new file mode 100644
index 0000000..e05f56b
--- /dev/null
+++ b/libs/rgbe/rgbe.h
@@ -0,0 +1,96 @@
+
+/* This file is an image processing operation for 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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright 2010 Danny Robson <danny blubinc net>
+ */
+
+
+#ifndef __RGBE_H__
+#define __RGBE_H__
+
+#include <glib.h>
+
+typedef struct _rgbe_file rgbe_file;
+
+/**
+ * rgbe_save_path:
+ * @param path:   the path to write the rgbe file to
+ * @param width:  the width of the image
+ * @param height: the height of the image
+ * @param pixels: RGB floating point pixel data
+ *
+ * Writes raw pixel data into an RGBE format file. The pixel data should be
+ * 'width x height' elements long, and consist of RGB floats.
+ *
+ * Returns TRUE on success.
+ */
+gboolean           rgbe_save_path      (const gchar *path,
+                                        guint        width,
+                                        guint        height,
+                                        gfloat      *pixels);
+
+
+/**
+ * rgbe_load_path:
+ * @param path: the path to an RGBE format image file
+ *
+ * Reads the metadata for an RGBE formatted image, and allows future access
+ * to the pixel data and some elements of the image metadata (see
+ * rgbe_get_size and rgbe_read_scanlines).
+ *
+ * The caller should use rgbe_file_free when finished with the file structure.
+ *
+ * Returns NULL on failure.
+ */
+rgbe_file        * rgbe_load_path      (const gchar *path);
+
+
+/**
+ * rgbe_file_free:
+ * @param file: file structure to dispose of
+ *
+ * Destroy resources associated with an RGBE file. The pointer will become
+ * invalid after this operation. It is safe to pass a NULL pointer to this
+ * function.
+ */
+void               rgbe_file_free      (rgbe_file *file);
+
+
+/**
+ * rgbe_get_size:
+ * @param file: the image file to query
+ * @param x:    pointer to store the width
+ * @param y:    pointer to store the height
+ *
+ * Query the image for its width and height.
+ *
+ * Returns TRUE on success.
+ */
+gboolean           rgbe_get_size       (rgbe_file *file,
+                                        guint     *x,
+                                        guint     *y);
+
+
+/**
+ * rgbe_read_scanlines:
+ * @param file: an open image structure to read from
+ *
+ * Returns the image's RGBA formatted pixel data. It is the user's
+ * responsibility to free the memory when finished.
+ */
+gfloat *           rgbe_read_scanlines (rgbe_file *file);
+
+#endif /* __RGBE_H__ */
diff --git a/operations/external/Makefile.am b/operations/external/Makefile.am
index b33746e..5d4e014 100644
--- a/operations/external/Makefile.am
+++ b/operations/external/Makefile.am
@@ -104,5 +104,14 @@ ppm_load_la_LIBADD = $(op_libs)
 ppm_save_la_SOURCES = ppm-save.c
 ppm_save_la_LIBADD = $(op_libs)
 
+# Dependencies are in our source tree
+ops += rgbe-load.la rgbe-save.la
+rgbe_load_la_SOURCES = rgbe-load.c
+rgbe_load_la_CFLAGS = $(AM_CFLAGS) -I $(top_srcdir)/libs
+rgbe_load_la_LIBADD = $(op_libs) $(top_builddir)/libs/rgbe/librgbe.la
+rgbe_save_la_SOURCES = rgbe-save.c
+rgbe_save_la_CFLAGS = $(AM_CFLAGS) -I $(top_srcdir)/libs
+rgbe_save_la_LIBADD = $(op_libs) $(top_builddir)/libs/rgbe/librgbe.la
+
 opdir = $(libdir)/gegl- GEGL_API_VERSION@
 op_LTLIBRARIES = $(ops)
diff --git a/operations/external/rgbe-load.c b/operations/external/rgbe-load.c
new file mode 100644
index 0000000..c06e71c
--- /dev/null
+++ b/operations/external/rgbe-load.c
@@ -0,0 +1,140 @@
+
+/* This file is an image processing operation for 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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright 2010 Danny Robson <danny blubinc net>
+ */
+
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#ifdef GEGL_CHANT_PROPERTIES
+
+gegl_chant_file_path (path, _("File"), "", _("Path of file to load."))
+
+#else
+
+#define GEGL_CHANT_TYPE_SOURCE
+#define GEGL_CHANT_C_FILE       "rgbe-load.c"
+
+#include "gegl-chant.h"
+
+#include "rgbe/rgbe.h"
+
+#include <errno.h>
+#include <stdio.h>
+
+
+static const gchar* FORMAT = "RGBA float";
+
+
+static GeglRectangle
+gegl_rgbe_load_get_bounding_box (GeglOperation *operation)
+{
+  GeglChantO       *o        = GEGL_CHANT_PROPERTIES (operation);
+  GeglRectangle     result   = {0,0,0,0};
+  rgbe_file        *file;
+  guint             width, height;
+
+  gegl_operation_set_format (operation,
+                             "output",
+                             babl_format (FORMAT));
+
+  file = rgbe_load_path (o->path);
+  if (!file)
+      goto cleanup;
+
+  if (!rgbe_get_size (file, &width, &height))
+      goto cleanup;
+
+  result.width  = width;
+  result.height = height;
+
+cleanup:
+  rgbe_file_free (file);
+  return result;
+}
+
+
+static gboolean
+gegl_rgbe_load_process (GeglOperation       *operation,
+                        GeglBuffer          *output,
+                        const GeglRectangle *result)
+{
+  GeglChantO       *o       = GEGL_CHANT_PROPERTIES (operation);
+  gboolean          success = FALSE;
+  gfloat           *pixels  = NULL;
+  rgbe_file        *file;
+  guint             width, height;
+
+  file = rgbe_load_path (o->path);
+  if (!file)
+      goto cleanup;
+
+  if (!rgbe_get_size (file, &width, &height))
+      goto cleanup;
+
+  if (width  != result->width  ||
+      height != result->height)
+      goto cleanup;
+
+  pixels = rgbe_read_scanlines (file);
+  if (!pixels)
+    goto cleanup;
+
+  gegl_buffer_set (output, result, babl_format (FORMAT), pixels,
+                   GEGL_AUTO_ROWSTRIDE);
+  success = TRUE;
+
+cleanup:
+  g_free         (pixels);
+  rgbe_file_free (file);
+
+  return success;
+}
+
+
+static GeglRectangle
+gegl_rgbe_load_get_cached_region (GeglOperation *operation,
+                                  const GeglRectangle *roi)
+{
+  return gegl_rgbe_load_get_bounding_box (operation);
+}
+
+
+static void
+gegl_chant_class_init (GeglChantClass *klass)
+{
+  GeglOperationClass       *operation_class;
+  GeglOperationSourceClass *source_class;
+
+  operation_class = GEGL_OPERATION_CLASS (klass);
+  source_class    = GEGL_OPERATION_SOURCE_CLASS (klass);
+
+  source_class->process = gegl_rgbe_load_process;
+  operation_class->get_bounding_box  = gegl_rgbe_load_get_bounding_box;
+  operation_class->get_cached_region = gegl_rgbe_load_get_cached_region;
+
+  operation_class->name        = "gegl:rgbe-load";
+  operation_class->categories  = "hidden";
+  operation_class->description = _("RGBE image loader (Radiance HDR format).");
+
+  gegl_extension_handler_register (".hdr", "gegl:rgbe-load");
+  gegl_extension_handler_register (".pic", "gegl:rgbe-load");
+}
+
+#endif
+
diff --git a/operations/external/rgbe-save.c b/operations/external/rgbe-save.c
new file mode 100644
index 0000000..2f65077
--- /dev/null
+++ b/operations/external/rgbe-save.c
@@ -0,0 +1,91 @@
+
+/* This file is an image processing operation for 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 <http://www.gnu.org/licenses/>.
+ *
+ * Copyright 2010 Danny Robson <danny blubinc net>
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+
+#ifdef GEGL_CHANT_PROPERTIES
+
+gegl_chant_string  (path, _("File"), "",
+                    _("Target path and filename, use '-' for stdout."))
+
+#else
+
+#define GEGL_CHANT_TYPE_SINK
+#define GEGL_CHANT_C_FILE       "rgbe-save.c"
+
+#include "gegl-chant.h"
+#include "rgbe/rgbe.h"
+
+
+static const gchar *FORMAT = "RGB float";
+
+
+static gboolean
+gegl_rgbe_save_process (GeglOperation       *operation,
+                        GeglBuffer          *input,
+                        const GeglRectangle *rect)
+{
+  GeglChantO *o       = GEGL_CHANT_PROPERTIES (operation);
+  gfloat     *pixels  = NULL;
+  gboolean    success = FALSE;
+
+  /* Write the scanlines */
+  pixels = g_malloc (rect->width        *
+                     rect->height       *
+                     sizeof (pixels[0]) *
+                     babl_format_get_n_components (babl_format (FORMAT)));
+
+  gegl_buffer_get (input, 1.0, rect, babl_format (FORMAT), pixels,
+                   GEGL_AUTO_ROWSTRIDE);
+
+  if (!rgbe_save_path (o->path, rect->width, rect->height, pixels))
+      goto cleanup;
+  success = TRUE;
+
+cleanup:
+  g_free (pixels);
+  return success;
+}
+
+
+static void
+gegl_chant_class_init (GeglChantClass *klass)
+{
+  GeglOperationClass     *operation_class;
+  GeglOperationSinkClass *sink_class;
+
+  operation_class = GEGL_OPERATION_CLASS (klass);
+  sink_class      = GEGL_OPERATION_SINK_CLASS (klass);
+
+  sink_class->process = gegl_rgbe_save_process;
+  sink_class->needs_full = TRUE;
+
+  operation_class->name        = "gegl:rgbe-save";
+  operation_class->categories  = "output";
+  operation_class->description =
+      _("RGBE image saver (Radiance HDR format)");
+
+  gegl_extension_handler_register_saver (".hdr", "gegl:rgbe-save");
+  gegl_extension_handler_register_saver (".pic", "gegl:rgbe-save");
+}
+
+#endif
+
diff --git a/tests/compositions/data/car-stack-eighth.hdr b/tests/compositions/data/car-stack-eighth.hdr
new file mode 100644
index 0000000..0887c6c
Binary files /dev/null and b/tests/compositions/data/car-stack-eighth.hdr differ
diff --git a/tests/compositions/reference/rgbe-load.png b/tests/compositions/reference/rgbe-load.png
new file mode 100644
index 0000000..9924755
Binary files /dev/null and b/tests/compositions/reference/rgbe-load.png differ
diff --git a/tests/compositions/reference/rgbe-save.hdr b/tests/compositions/reference/rgbe-save.hdr
new file mode 100644
index 0000000..0887c6c
Binary files /dev/null and b/tests/compositions/reference/rgbe-save.hdr differ
diff --git a/tests/compositions/rgbe-load.xml b/tests/compositions/rgbe-load.xml
new file mode 100644
index 0000000..3aba412
--- /dev/null
+++ b/tests/compositions/rgbe-load.xml
@@ -0,0 +1,9 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<gegl>
+  <node operation='gegl:rgbe-load'>
+    <params>
+      <param name='path'>data/car-stack-eighth.hdr</param>
+    </params>
+  </node>
+</gegl>
+
diff --git a/tests/compositions/rgbe-save.xml b/tests/compositions/rgbe-save.xml
new file mode 100644
index 0000000..17476d0
--- /dev/null
+++ b/tests/compositions/rgbe-save.xml
@@ -0,0 +1,15 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<gegl>
+  <node operation='gegl:scale'>
+    <params>
+      <param name='x'>0.25</param>
+      <param name='y'>0.25</param>
+    </params>
+  </node>
+  <node operation='gegl:load'>
+    <params>
+      <param name='path'>data/car-stack.png</param>
+    </params>
+  </node>
+</gegl>
+



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