/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string.h>
#include <zlib.h>

#include <cairo.h>
#include <gegl.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

#include "libgimpbase/gimpbase.h"
#include "libgimpcolor/gimpcolor.h"
#include "libgimpconfig/gimpconfig.h"

#include "config/gimpgeglconfig.h"

#include "core/core-types.h"

#include "gegl/gimp-babl-compat.h"
#include "gegl/gimp-gegl-tile-compat.h"

#include "core/gimp.h"
#include "core/gimpcontainer.h"
#include "core/gimpchannel.h"
#include "core/gimpdashpattern.h"
#include "core/gimpdata.h"
#include "core/gimpdrawable.h"
#include "core/gimpdrawable-filters.h"
#include "core/gimpdrawablefilter.h"
#include "core/gimpfilloptions.h"
#include "core/gimpgrid.h"
#include "core/gimpguide.h"
#include "core/gimpimage.h"
#include "core/gimpimage-colormap.h"
#include "core/gimpimage-grid.h"
#include "core/gimpimage-guides.h"
#include "core/gimpimage-metadata.h"
#include "core/gimpimage-private.h"
#include "core/gimpimage-sample-points.h"
#include "core/gimpimage-symmetry.h"
#include "core/gimpitemlist.h"
#include "core/gimplayer.h"
#include "core/gimplayermask.h"
#include "core/gimplink.h"
#include "core/gimplinklayer.h"
#include "core/gimplist.h"
#include "core/gimpparasitelist.h"
#include "core/gimpprogress.h"
#include "core/gimprasterizable.h"
#include "core/gimpsamplepoint.h"
#include "core/gimpstrokeoptions.h"
#include "core/gimpsymmetry.h"

#include "operations/layer-modes/gimp-layer-modes.h"

#include "path/gimpanchor.h"
#include "path/gimpbezierstroke.h"
#include "path/gimppath.h"
#include "path/gimpstroke.h"
#include "path/gimppath-compat.h"
#include "path/gimpvectorlayer.h"
#include "path/gimpvectorlayeroptions.h"

#include "text/gimptextlayer.h"
#include "text/gimptextlayer-xcf.h"

#include "xcf-private.h"
#include "xcf-read.h"
#include "xcf-save.h"
#include "xcf-seek.h"
#include "xcf-write.h"

#include "gimp-intl.h"

typedef void (* CompressTileFunc) (GeglRectangle  *tile_rect,
                                   guchar         *tile_data,
                                   const Babl     *format,
                                   guchar         *out_data,
                                   gint            out_data_max_len,
                                   gint           *lenptr);

/* Per thread data for xcf_save_tile_rle */
typedef struct
{
  /* Common to all jobs. */
  GeglBuffer       *buffer;
  gint              file_version;
  gint              max_out_data_len;
  CompressTileFunc  compress;

  /* Job specific. */
  gint              tile;
  gint              batch_size;

  /* Temp data to avoid too many allocations. */
  guchar           *tile_data;

  /* Return data. */
  guchar           *out_data;
  gint              out_data_len[XCF_TILE_SAVE_BATCH_SIZE];
} XcfJobData;

static gboolean xcf_save_image_props   (XcfInfo           *info,
                                        GimpImage         *image,
                                        GError           **error);
static gboolean xcf_save_layer_props   (XcfInfo           *info,
                                        GimpImage         *image,
                                        GimpLayer         *layer,
                                        GError           **error);
static gboolean xcf_save_channel_props (XcfInfo           *info,
                                        GimpImage         *image,
                                        GimpChannel       *channel,
                                        GError           **error);
static gboolean xcf_save_effect_props  (XcfInfo           *info,
                                        GimpImage         *image,
                                        GimpFilter        *filter,
                                        GError           **error);
static gboolean xcf_save_path_props    (XcfInfo           *info,
                                        GimpImage         *image,
                                        GimpPath          *path,
                                        GError           **error);
static gboolean xcf_save_prop          (XcfInfo           *info,
                                        GimpImage         *image,
                                        PropType           prop_type,
                                        GError           **error,
                                        ...);
static gboolean xcf_save_layer         (XcfInfo           *info,
                                        GimpImage         *image,
                                        GimpLayer         *layer,
                                        GError           **error);
static gboolean xcf_save_channel       (XcfInfo           *info,
                                        GimpImage         *image,
                                        GimpChannel       *channel,
                                        GError           **error);
static gboolean xcf_save_effect        (XcfInfo           *info,
                                        GimpImage         *image,
                                        GimpFilter        *filter,
                                        GError           **error);
static gboolean xcf_save_color         (XcfInfo           *info,
                                        GeglColor         *color,
                                        GError           **error);
static gboolean xcf_save_fill_options  (XcfInfo           *info,
                                        GimpFillOptions   *fill_options,
                                        GError           **error);
static gboolean xcf_save_stroke_options (XcfInfo          *info,
                                        GimpStrokeOptions *stroke_options,
                                        GError           **error);
static gboolean xcf_save_path          (XcfInfo           *info,
                                        GimpImage         *image,
                                        GimpPath          *path,
                                        GError           **error);
static gboolean xcf_save_buffer        (XcfInfo           *info,
                                        GimpImage         *image,
                                        GeglBuffer        *buffer,
                                        GError           **error);
static gboolean xcf_save_level         (XcfInfo           *info,
                                        GimpImage         *image,
                                        GeglBuffer        *buffer,
                                        GError           **error);
static gboolean xcf_save_tile          (XcfInfo           *info,
                                        GeglBuffer        *buffer,
                                        GeglRectangle     *tile_rect,
                                        const Babl        *format,
                                        GError           **error);
static void     xcf_save_free_job_data (XcfJobData        *data);
static gint     xcf_save_sort_job_data (XcfJobData        *data1,
                                        XcfJobData        *data2,
                                        gpointer           user_data);
static void     xcf_save_tile_parallel (XcfJobData        *job_data,
                                        GAsyncQueue       *queue);
static void     xcf_save_tile_rle      (GeglRectangle     *tile_rect,
                                        guchar            *tile_data,
                                        const Babl        *format,
                                        guchar            *rlebuf,
                                        gint               rlebuf_max_len,
                                        gint              *lenptr);
static void     xcf_save_tile_zlib     (GeglRectangle     *tile_rect,
                                        guchar            *tile_data,
                                        const Babl        *format,
                                        guchar            *zlib_data,
                                        gint               zlib_data_max_len,
                                        gint              *lenptr);
static gboolean xcf_save_parasite      (XcfInfo           *info,
                                        GimpParasite      *parasite,
                                        GError           **error);
static gboolean xcf_save_parasite_list (XcfInfo           *info,
                                        GimpParasiteList  *parasite,
                                        GError           **error);
static gboolean xcf_save_old_paths     (XcfInfo           *info,
                                        GimpImage         *image,
                                        GError           **error);
static gboolean xcf_save_old_vectors   (XcfInfo           *info,
                                        GimpImage         *image,
                                        GError           **error);


/* private convenience macros */
#define xcf_write_int32_check_error(info, data, count, cleanup_code) G_STMT_START { \
  xcf_write_int32 (info, data, count, &tmp_error);                    \
  if (tmp_error)                                                      \
    {                                                                 \
      g_propagate_error (error, tmp_error);                           \
      cleanup_code;                                                   \
      return FALSE;                                                   \
    }                                                                 \
  } G_STMT_END

#define xcf_write_offset_check_error(info, data, count, cleanup_code) G_STMT_START { \
  xcf_write_offset (info, data, count, &tmp_error);                    \
  if (tmp_error)                                                       \
    {                                                                  \
      g_propagate_error (error, tmp_error);                            \
      cleanup_code;                                                    \
      return FALSE;                                                    \
    }                                                                  \
  } G_STMT_END

#define xcf_write_zero_offset_check_error(info, count, cleanup_code) G_STMT_START { \
  xcf_write_zero_offset (info, count, &tmp_error);                    \
  if (tmp_error)                                                      \
    {                                                                 \
      g_propagate_error (error, tmp_error);                           \
      cleanup_code;                                                   \
      return FALSE;                                                   \
    }                                                                 \
  } G_STMT_END

#define xcf_write_int8_check_error(info, data, count, cleanup_code) G_STMT_START { \
  xcf_write_int8 (info, data, count, &tmp_error);                    \
  if (tmp_error)                                                     \
    {                                                                \
      g_propagate_error (error, tmp_error);                          \
      cleanup_code;                                                  \
      return FALSE;                                                  \
    }                                                                \
  } G_STMT_END

#define xcf_write_float_check_error(info, data, count, cleanup_code) G_STMT_START { \
  xcf_write_float (info, data, count, &tmp_error);                    \
  if (tmp_error)                                                      \
    {                                                                 \
      g_propagate_error (error, tmp_error);                           \
      cleanup_code;                                                   \
      return FALSE;                                                   \
    }                                                                 \
  } G_STMT_END

#define xcf_write_string_check_error(info, data, count, cleanup_code) G_STMT_START { \
  xcf_write_string (info, data, count, &tmp_error);                    \
  if (tmp_error)                                                       \
    {                                                                  \
      g_propagate_error (error, tmp_error);                            \
      cleanup_code;                                                    \
      return FALSE;                                                    \
    }                                                                  \
  } G_STMT_END

#define xcf_write_component_check_error(info, bpc, data, count, cleanup_code) G_STMT_START { \
  xcf_write_component (info, bpc, data, count, &tmp_error);          \
  if (tmp_error)                                                     \
    {                                                                \
      g_propagate_error (error, tmp_error);                          \
      cleanup_code;                                                  \
      return FALSE;                                                  \
    }                                                                \
  } G_STMT_END

#define xcf_write_prop_type_check_error(info, prop_type, cleanup_code) G_STMT_START { \
  guint32 _prop_int32 = prop_type;                                                    \
  xcf_write_int32_check_error (info, &_prop_int32, 1, cleanup_code);                  \
  } G_STMT_END

#define xcf_check_error(x, cleanup_code) G_STMT_START { \
  if (! (x))                              \
    {                                     \
      cleanup_code;                       \
      return FALSE;                       \
    }                                     \
  } G_STMT_END

#define xcf_progress_update(info) G_STMT_START  \
  {                                             \
    progress++;                                 \
    if (info->progress)                         \
      gimp_progress_set_value (info->progress,  \
                               (gdouble) progress / (gdouble) max_progress); \
  } G_STMT_END


gboolean
xcf_save_image (XcfInfo    *info,
                GimpImage  *image,
                GError    **error)
{
  GList   *all_layers;
  GList   *all_channels;
  GList   *all_paths = NULL;
  GList   *list;
  goffset  saved_pos;
  goffset  offset;
  guint32  value;
  guint    n_layers;
  guint    n_channels;
  guint    n_paths  = 0;
  guint    progress = 0;
  guint    max_progress;
  gchar    version_tag[16];
  gboolean write_paths = FALSE;
  GError  *tmp_error   = NULL;

  /* write out the tag information for the image */
  if (info->file_version > 0)
    {
      g_snprintf (version_tag, sizeof (version_tag),
                  "gimp xcf v%03d", info->file_version);
    }
  else
    {
      strcpy (version_tag, "gimp xcf file");
    }

  xcf_write_int8_check_error (info, (guint8 *) version_tag, 14, ;);

  /* write out the width, height and image type information for the image */
  value = gimp_image_get_width (image);
  xcf_write_int32_check_error (info, (guint32 *) &value, 1, ;);

  value = gimp_image_get_height (image);
  xcf_write_int32_check_error (info, (guint32 *) &value, 1, ;);

  value = gimp_image_get_base_type (image);
  xcf_write_int32_check_error (info, &value, 1, ;);

  if (info->file_version >= 4)
    {
      value = gimp_image_get_precision (image);
      xcf_write_int32_check_error (info, &value, 1, ;);
    }

  if (info->file_version >= 18)
    write_paths = TRUE;

  /* determine the number of layers and channels in the image */
  all_layers   = gimp_image_get_layer_list (image);
  all_channels = gimp_image_get_channel_list (image);

  /* check and see if we have to save out the selection */
  if (! gimp_channel_is_empty (gimp_image_get_mask (image)))
    {
      all_channels = g_list_append (all_channels, gimp_image_get_mask (image));
    }

  n_layers   = (guint) g_list_length (all_layers);
  n_channels = (guint) g_list_length (all_channels);

  if (write_paths)
    {
      all_paths = gimp_image_get_path_list (image);
      n_paths   = (guint) g_list_length (all_paths);
    }

  max_progress = 1 + n_layers + n_channels + n_paths;

  /* write the property information for the image */
  xcf_check_error (xcf_save_image_props (info, image, error), ;);

  xcf_progress_update (info);

  /* 'saved_pos' is the next slot in the offset table */
  saved_pos = info->cp;

  /* write an empty offset table */
  xcf_write_zero_offset_check_error (info,
                                     n_layers + n_channels + 2 +
                                     (write_paths ? n_paths + 1 : 0), ;);

  /* 'offset' is where we will write the next layer or channel */
  offset = info->cp;

  for (list = all_layers; list; list = g_list_next (list))
    {
      GimpLayer *layer = list->data;

      /* seek back to the next slot in the offset table and write the
       * offset of the layer
       */
      xcf_check_error (xcf_seek_pos (info, saved_pos, error), ;);
      xcf_write_offset_check_error (info, &offset, 1, ;);

      /* remember the next slot in the offset table */
      saved_pos = info->cp;

      /* seek to the layer offset and save the layer */
      xcf_check_error (xcf_seek_pos (info, offset, error), ;);
      xcf_check_error (xcf_save_layer (info, image, layer, error), ;);

      /* the next layer's offset is after the layer we just wrote */
      offset = info->cp;

      xcf_progress_update (info);
    }

  /* skip a '0' in the offset table to indicate the end of the layer
   * offsets
   */
  saved_pos += info->bytes_per_offset;

  for (list = all_channels; list; list = g_list_next (list))
    {
      GimpChannel *channel = list->data;

      /* seek back to the next slot in the offset table and write the
       * offset of the channel
       */
      xcf_check_error (xcf_seek_pos (info, saved_pos, error), ;);
      xcf_write_offset_check_error (info, &offset, 1, ;);

      /* remember the next slot in the offset table */
      saved_pos = info->cp;

      /* seek to the channel offset and save the channel */
      xcf_check_error (xcf_seek_pos (info, offset, error), ;);
      xcf_check_error (xcf_save_channel (info, image, channel, error), ;);

      /* the next channels's offset is after the channel we just wrote */
      offset = info->cp;

      xcf_progress_update (info);
    }

  if (write_paths)
    {
      /* skip a '0' in the offset table to indicate the end of the channel
       * offsets
       */
      saved_pos += info->bytes_per_offset;

      for (list = all_paths; list; list = g_list_next (list))
        {
          GimpPath *path = list->data;

          /* seek back to the next slot in the offset table and write the
           * offset of the channel
           */
          xcf_check_error (xcf_seek_pos (info, saved_pos, error), ;);
          xcf_write_offset_check_error (info, &offset, 1, ;);

          /* remember the next slot in the offset table */
          saved_pos = info->cp;

          /* seek to the channel offset and save the channel */
          xcf_check_error (xcf_seek_pos (info, offset, error), ;);
          xcf_check_error (xcf_save_path (info, image, path, error), ;);

          /* the next channels's offset is after the channel we just wrote */
          offset = info->cp;

          xcf_progress_update (info);
        }
    }

  /* there is already a '0' at the end of the offset table to indicate
   * the end of the channel offsets
   */

  g_list_free (all_layers);
  g_list_free (all_channels);
  g_list_free (all_paths);

  return ! g_output_stream_is_closed (info->output);
}

static gboolean
xcf_save_image_props (XcfInfo    *info,
                      GimpImage  *image,
                      GError    **error)
{
  GimpImagePrivate *private       = GIMP_IMAGE_GET_PRIVATE (image);
  GimpParasite     *grid_parasite = NULL;
  GimpParasite     *meta_parasite = NULL;
  GList            *symmetry_parasites = NULL;
  GList            *iter;
  GimpUnit         *unit          = gimp_image_get_unit (image);
  gdouble           xres;
  gdouble           yres;

  gimp_image_get_resolution (image, &xres, &yres);

  /* check and see if we should save the colormap property */
  if (gimp_image_get_colormap_palette (image))
    {
      gint    n_colors;
      guint8 *colormap = _gimp_image_get_colormap (image, &n_colors);
      xcf_check_error (xcf_save_prop (info, image, PROP_COLORMAP, error,
                                      n_colors, colormap), ;);
      g_free (colormap);
    }

  if (info->compression != COMPRESS_NONE)
    xcf_check_error (xcf_save_prop (info, image, PROP_COMPRESSION, error,
                                    info->compression), ;);

  if (gimp_image_get_guides (image))
    xcf_check_error (xcf_save_prop (info, image, PROP_GUIDES, error,
                                    gimp_image_get_guides (image)), ;);

  if (gimp_image_get_sample_points (image))
    {
      /* save the new property before the old one, so loading can skip
       * the latter
       */
      xcf_check_error (xcf_save_prop (info, image, PROP_SAMPLE_POINTS, error,
                                      gimp_image_get_sample_points (image)), ;);
      xcf_check_error (xcf_save_prop (info, image, PROP_OLD_SAMPLE_POINTS, error,
                                      gimp_image_get_sample_points (image)), ;);
    }

  xcf_check_error (xcf_save_prop (info, image, PROP_RESOLUTION, error,
                                  xres, yres), ;);

  xcf_check_error (xcf_save_prop (info, image, PROP_TATTOO, error,
                                  gimp_image_get_tattoo_state (image)), ;);

  if (gimp_unit_is_built_in (unit))
    xcf_check_error (xcf_save_prop (info, image, PROP_UNIT, error, unit), ;);

  if (gimp_container_get_n_children (gimp_image_get_paths (image)) > 0 &&
      info->file_version < 18)
    {
      if (gimp_path_compat_is_compatible (image))
        xcf_check_error (xcf_save_prop (info, image, PROP_PATHS, error), ;);
      else
        xcf_check_error (xcf_save_prop (info, image, PROP_VECTORS, error), ;);
    }

  if (! gimp_unit_is_built_in (unit))
    xcf_check_error (xcf_save_prop (info, image, PROP_USER_UNIT, error, unit), ;);

  if (gimp_image_get_grid (image))
    {
      GimpGrid *grid = gimp_image_get_grid (image);

      /* Set the XCF version so that the grid colors are written as GimpRGB
       * values when saving in legacy (2.10 and below) XCF formats */
      gimp_config_set_xcf_version (GIMP_CONFIG (grid), info->file_version);
      grid_parasite = gimp_grid_to_parasite (grid);
      gimp_parasite_list_add (private->parasites, grid_parasite);
    }

  if (gimp_image_get_metadata (image))
    {
      GimpMetadata *metadata = gimp_image_get_metadata (image);
      gchar        *meta_string;

      meta_string = gimp_metadata_serialize (metadata);

      if (meta_string)
        {
          meta_parasite = gimp_parasite_new ("gimp-image-metadata",
                                             GIMP_PARASITE_PERSISTENT,
                                             strlen (meta_string) + 1,
                                             meta_string);
          gimp_parasite_list_add (private->parasites, meta_parasite);
          g_free (meta_string);
        }
    }

  if (g_list_length (gimp_image_symmetry_get (image)))
    {
      GimpParasite *parasite  = NULL;
      GimpSymmetry *symmetry;

      for (iter = gimp_image_symmetry_get (image); iter; iter = g_list_next (iter))
        {
          symmetry = GIMP_SYMMETRY (iter->data);
          if (G_TYPE_FROM_INSTANCE (symmetry) == GIMP_TYPE_SYMMETRY)
            /* Do not save the identity symmetry. */
            continue;
          parasite = gimp_symmetry_to_parasite (GIMP_SYMMETRY (iter->data));
          gimp_parasite_list_add (private->parasites, parasite);
          symmetry_parasites = g_list_prepend (symmetry_parasites, parasite);
        }
    }

  if (gimp_parasite_list_length (private->parasites) > 0)
    {
      xcf_check_error (xcf_save_prop (info, image, PROP_PARASITES, error,
                                      private->parasites), ;);
    }

  if (grid_parasite)
    {
      gimp_parasite_list_remove (private->parasites,
                                 gimp_parasite_get_name (grid_parasite));
      gimp_parasite_free (grid_parasite);
    }

  if (meta_parasite)
    {
      gimp_parasite_list_remove (private->parasites,
                                 gimp_parasite_get_name (meta_parasite));
      gimp_parasite_free (meta_parasite);
    }

  for (iter = symmetry_parasites; iter; iter = g_list_next (iter))
    {
      GimpParasite *parasite = iter->data;

      gimp_parasite_list_remove (private->parasites,
                                 gimp_parasite_get_name (parasite));
    }
  g_list_free_full (symmetry_parasites,
                    (GDestroyNotify) gimp_parasite_free);

  info->layer_sets = gimp_image_get_stored_item_sets (image, GIMP_TYPE_LAYER);
  info->channel_sets = gimp_image_get_stored_item_sets (image, GIMP_TYPE_CHANNEL);

  for (iter = info->layer_sets; iter; iter = iter->next)
    xcf_check_error (xcf_save_prop (info, image, PROP_ITEM_SET, error, iter->data), ;);
  for (iter = info->channel_sets; iter; iter = iter->next)
    xcf_check_error (xcf_save_prop (info, image, PROP_ITEM_SET, error, iter->data), ;);

  xcf_check_error (xcf_save_prop (info, image, PROP_END, error), ;);

  return TRUE;
}

static gboolean
xcf_save_layer_props (XcfInfo    *info,
                      GimpImage  *image,
                      GimpLayer  *layer,
                      GError    **error)
{
  GimpParasiteList *parasites;
  GList            *iter;
  gint              offset_x;
  gint              offset_y;

  if (gimp_viewable_get_children (GIMP_VIEWABLE (layer)))
    xcf_check_error (xcf_save_prop (info, image, PROP_GROUP_ITEM, error), ;);

  if (gimp_viewable_get_parent (GIMP_VIEWABLE (layer)))
    {
      GList *path;

      path = gimp_item_get_path (GIMP_ITEM (layer));
      xcf_check_error (xcf_save_prop (info, image, PROP_ITEM_PATH, error,
                                      path), ;);
      g_list_free (path);
    }

  if (g_list_find (gimp_image_get_selected_layers (image), layer))
    xcf_check_error (xcf_save_prop (info, image, PROP_ACTIVE_LAYER, error), ;);

  if (layer == gimp_image_get_floating_selection (image))
    {
      info->floating_sel_drawable = gimp_layer_get_floating_sel_drawable (layer);
      xcf_check_error (xcf_save_prop (info, image, PROP_FLOATING_SELECTION,
                                      error), ;);
    }

  xcf_check_error (xcf_save_prop (info, image, PROP_OPACITY, error,
                                  gimp_layer_get_opacity (layer)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_FLOAT_OPACITY, error,
                                  gimp_layer_get_opacity (layer)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_VISIBLE, error,
                                  gimp_item_get_visible (GIMP_ITEM (layer))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_COLOR_TAG, error,
                                  gimp_item_get_color_tag (GIMP_ITEM (layer))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_CONTENT, error,
                                  gimp_item_get_lock_content (GIMP_ITEM (layer))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_ALPHA, error,
                                  gimp_layer_get_lock_alpha (layer)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_POSITION, error,
                                  gimp_item_get_lock_position (GIMP_ITEM (layer))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_VISIBILITY, error,
                                  gimp_item_get_lock_visibility (GIMP_ITEM (layer))), ;);

  if (gimp_layer_get_mask (layer))
    {
      xcf_check_error (xcf_save_prop (info, image, PROP_APPLY_MASK, error,
                                      gimp_layer_get_apply_mask (layer)), ;);
      xcf_check_error (xcf_save_prop (info, image, PROP_EDIT_MASK, error,
                                      gimp_layer_get_edit_mask (layer)), ;);
      xcf_check_error (xcf_save_prop (info, image, PROP_SHOW_MASK, error,
                                      gimp_layer_get_show_mask (layer)), ;);
    }
  else
    {
      xcf_check_error (xcf_save_prop (info, image, PROP_APPLY_MASK, error,
                                      FALSE), ;);
      xcf_check_error (xcf_save_prop (info, image, PROP_EDIT_MASK, error,
                                      FALSE), ;);
      xcf_check_error (xcf_save_prop (info, image, PROP_SHOW_MASK, error,
                                      FALSE), ;);
    }

  gimp_item_get_offset (GIMP_ITEM (layer), &offset_x, &offset_y);

  xcf_check_error (xcf_save_prop (info, image, PROP_OFFSETS, error,
                                  offset_x, offset_y), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_MODE, error,
                                  gimp_layer_get_mode (layer)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_BLEND_SPACE, error,
                                  gimp_layer_get_mode (layer),
                                  gimp_layer_get_blend_space (layer)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_COMPOSITE_SPACE, error,
                                  gimp_layer_get_mode (layer),
                                  gimp_layer_get_composite_space (layer)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_COMPOSITE_MODE, error,
                                  gimp_layer_get_mode (layer),
                                  gimp_layer_get_composite_mode (layer)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_TATTOO, error,
                                  gimp_item_get_tattoo (GIMP_ITEM (layer))), ;);

  if (GIMP_IS_TEXT_LAYER (layer) && GIMP_TEXT_LAYER (layer)->text)
    {
      GimpTextLayer *text_layer = GIMP_TEXT_LAYER (layer);
      guint32        flags      = gimp_text_layer_get_xcf_flags (text_layer);

      gimp_text_layer_xcf_save_prepare (text_layer);

      if (flags)
        xcf_check_error (xcf_save_prop (info,
                                        image, PROP_TEXT_LAYER_FLAGS, error,
                                        flags), ;);
    }
  else if (GIMP_IS_VECTOR_LAYER (layer))
    {
      xcf_check_error (xcf_save_prop (info, image, PROP_VECTOR_LAYER, error, layer), ;);
    }
  else if (GIMP_IS_LINK_LAYER (layer))
    {
      xcf_check_error (xcf_save_prop (info, image, PROP_LINK_LAYER, error, layer), ;);
      if (gimp_link_layer_get_transform (GIMP_LINK_LAYER (layer), NULL, NULL, NULL, NULL))
        xcf_check_error (xcf_save_prop (info, image, PROP_TRANSFORM, error, layer), ;);
    }

  if (gimp_viewable_get_children (GIMP_VIEWABLE (layer)))
    {
      gint32 flags = 0;

      if (gimp_viewable_get_expanded (GIMP_VIEWABLE (layer)))
        flags |= XCF_GROUP_ITEM_EXPANDED;

      xcf_check_error (xcf_save_prop (info,
                                      image, PROP_GROUP_ITEM_FLAGS, error,
                                      flags), ;);
    }

  parasites = gimp_item_get_parasites (GIMP_ITEM (layer));

  if (gimp_parasite_list_length (parasites) > 0)
    {
      xcf_check_error (xcf_save_prop (info, image, PROP_PARASITES, error,
                                      parasites), ;);
    }

  for (iter = info->layer_sets; iter; iter = iter->next)
    {
      GimpItemList *set = iter->data;

      if (! gimp_item_list_is_pattern (set, NULL))
        {
          GList *items = gimp_item_list_get_items (set, NULL);

          if (g_list_find (items, GIMP_ITEM (layer)))
            xcf_check_error (xcf_save_prop (info, image, PROP_ITEM_SET_ITEM, error,
                                            g_list_position (info->layer_sets, iter)), ;);

          g_list_free (items);
        }
    }

  xcf_check_error (xcf_save_prop (info, image, PROP_END, error), ;);

  return TRUE;
}

static gboolean
xcf_save_channel_props (XcfInfo      *info,
                        GimpImage    *image,
                        GimpChannel  *channel,
                        GError      **error)
{
  GimpParasiteList *parasites;
  GList            *iter;

  if (g_list_find (gimp_image_get_selected_channels (image), channel))
    xcf_check_error (xcf_save_prop (info, image, PROP_ACTIVE_CHANNEL, error), ;);

  if (channel == gimp_image_get_mask (image))
    xcf_check_error (xcf_save_prop (info, image, PROP_SELECTION, error), ;);

  xcf_check_error (xcf_save_prop (info, image, PROP_OPACITY, error,
                                  gimp_channel_get_opacity (channel)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_FLOAT_OPACITY, error,
                                  gimp_channel_get_opacity (channel)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_VISIBLE, error,
                                  gimp_item_get_visible (GIMP_ITEM (channel))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_COLOR_TAG, error,
                                  gimp_item_get_color_tag (GIMP_ITEM (channel))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_CONTENT, error,
                                  gimp_item_get_lock_content (GIMP_ITEM (channel))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_POSITION, error,
                                  gimp_item_get_lock_position (GIMP_ITEM (channel))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_VISIBILITY, error,
                                  gimp_item_get_lock_visibility (GIMP_ITEM (channel))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_SHOW_MASKED, error,
                                  gimp_channel_get_show_masked (channel)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_COLOR, error,
                                  channel->color), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_FLOAT_COLOR, error,
                                  channel->color), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_TATTOO, error,
                                  gimp_item_get_tattoo (GIMP_ITEM (channel))), ;);

  parasites = gimp_item_get_parasites (GIMP_ITEM (channel));

  if (gimp_parasite_list_length (parasites) > 0)
    {
      xcf_check_error (xcf_save_prop (info, image, PROP_PARASITES, error,
                                      parasites), ;);
    }

  for (iter = info->channel_sets; iter; iter = iter->next)
    {
      GimpItemList *set = iter->data;

      if (! gimp_item_list_is_pattern (set, NULL))
        {
          GList *items = gimp_item_list_get_items (set, NULL);

          if (g_list_find (items, GIMP_ITEM (channel)))
            xcf_check_error (xcf_save_prop (info, image, PROP_ITEM_SET_ITEM, error,
                                            g_list_position (info->channel_sets, iter)), ;);

          g_list_free (items);
        }
    }

  xcf_check_error (xcf_save_prop (info, image, PROP_END, error), ;);

  return TRUE;
}

static gboolean
xcf_save_effect_props (XcfInfo      *info,
                       GimpImage    *image,
                       GimpFilter   *filter,
                       GError      **error)
{
  GParamSpec **pspecs;
  guint        n_pspecs;
  GeglNode    *node;
  gchar       *operation;

  xcf_check_error (xcf_save_prop (info, image, PROP_VISIBLE, error,
                                  gimp_filter_get_active (filter)), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_FLOAT_OPACITY, error,
                                  gimp_drawable_filter_get_opacity (GIMP_DRAWABLE_FILTER (filter))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_MODE, error,
                                  gimp_drawable_filter_get_paint_mode (GIMP_DRAWABLE_FILTER (filter))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_BLEND_SPACE, error,
                                  gimp_drawable_filter_get_blend_space (GIMP_DRAWABLE_FILTER (filter))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_COMPOSITE_SPACE, error,
                                  gimp_drawable_filter_get_composite_space (GIMP_DRAWABLE_FILTER (filter))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_COMPOSITE_MODE, error,
                                  gimp_drawable_filter_get_composite_mode (GIMP_DRAWABLE_FILTER (filter))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_FILTER_CLIP, error,
                                  gimp_drawable_filter_get_clip (GIMP_DRAWABLE_FILTER (filter))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_FILTER_REGION, error,
                                  gimp_drawable_filter_get_region (GIMP_DRAWABLE_FILTER (filter))), ;);

  /* Save each GEGL property individually */
  node = gimp_drawable_filter_get_operation (GIMP_DRAWABLE_FILTER (filter));

  gegl_node_get (node,
                 "operation", &operation,
                 NULL);

  pspecs = gegl_operation_list_properties (operation, &n_pspecs);

  for (gint i = 0; i < n_pspecs; i++)
    {
      GParamSpec     *pspec       = pspecs[i];
      GValue          value       = G_VALUE_INIT;
      FilterPropType  filter_type = FILTER_PROP_UNKNOWN;

      g_value_init (&value, pspec->value_type);
      gegl_node_get_property (node, pspec->name,
                              &value);

      switch (G_VALUE_TYPE (&value))
        {
          case G_TYPE_INT:
            filter_type = FILTER_PROP_INT;
            break;

          case G_TYPE_UINT:
            filter_type = FILTER_PROP_UINT;
            break;

          case G_TYPE_BOOLEAN:
            filter_type = FILTER_PROP_BOOL;
            break;

          case G_TYPE_FLOAT:
          case G_TYPE_DOUBLE:
            filter_type = FILTER_PROP_FLOAT;
            break;

          case G_TYPE_STRING:
            filter_type = FILTER_PROP_STRING;
            break;

          default:
            if (g_type_is_a (G_VALUE_TYPE (&value), GIMP_TYPE_CONFIG))
              {
                filter_type = FILTER_PROP_CONFIG;
              }
            else if (g_type_is_a (G_VALUE_TYPE (&value), G_TYPE_ENUM))
              {
                filter_type = FILTER_PROP_ENUM;
              }
            else if (g_type_is_a (G_VALUE_TYPE (&value), GEGL_TYPE_COLOR))
              {
                filter_type = FILTER_PROP_COLOR;
              }
            else
              {
                gimp_message (info->gimp, G_OBJECT (info->progress),
                              GIMP_MESSAGE_WARNING,
                              "XCF Warning: argument \"%s\" of filter %s has "
                              "unsupported type %s. It was discarded.",
                              pspec->name, operation,
                              g_type_name (G_VALUE_TYPE (&value)));
              }
            break;
        }

      if (filter_type != FILTER_PROP_UNKNOWN)
        xcf_check_error (xcf_save_prop (info, image, PROP_FILTER_ARGUMENT, error,
                         pspec->name, filter_type, value), ;);

      g_value_unset (&value);
    }
  g_free (operation);
  g_free (pspecs);

  xcf_check_error (xcf_save_prop (info, image, PROP_END, error), ;);

  return TRUE;
}

static gboolean
xcf_save_path_props (XcfInfo      *info,
                     GimpImage    *image,
                     GimpPath     *path,
                     GError      **error)
{
  GimpParasiteList *parasites;

  if (g_list_find (gimp_image_get_selected_paths (image), path))
    xcf_check_error (xcf_save_prop (info, image, PROP_SELECTED_PATH, error), ;);

  xcf_check_error (xcf_save_prop (info, image, PROP_VISIBLE, error,
                                  gimp_item_get_visible (GIMP_ITEM (path))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_COLOR_TAG, error,
                                  gimp_item_get_color_tag (GIMP_ITEM (path))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_CONTENT, error,
                                  gimp_item_get_lock_content (GIMP_ITEM (path))), ;);
  xcf_check_error (xcf_save_prop (info, image, PROP_LOCK_POSITION, error,
                                  gimp_item_get_lock_position (GIMP_ITEM (path))), ;);

  xcf_check_error (xcf_save_prop (info, image, PROP_TATTOO, error,
                                  gimp_item_get_tattoo (GIMP_ITEM (path))), ;);

  parasites = gimp_item_get_parasites (GIMP_ITEM (path));

  if (gimp_parasite_list_length (parasites) > 0)
    {
      xcf_check_error (xcf_save_prop (info, image, PROP_PARASITES, error,
                                      parasites), ;);
    }

#if 0
  for (iter = info->path_sets; iter; iter = iter->next)
    {
      GimpItemList *set = iter->data;

      if (! gimp_item_list_is_pattern (set, NULL))
        {
          GList *items = gimp_item_list_get_items (set, NULL);

          if (g_list_find (items, GIMP_ITEM (path)))
            xcf_check_error (xcf_save_prop (info, image, PROP_ITEM_SET_ITEM, error,
                                            g_list_position (info->layer_sets, iter)), ;);

          g_list_free (items);
        }
    }
#endif

  xcf_check_error (xcf_save_prop (info, image, PROP_END, error), ;);

  return TRUE;
}

static gboolean
xcf_save_prop (XcfInfo    *info,
               GimpImage  *image,
               PropType    prop_type,
               GError    **error,
               ...)
{
  guint32  size;
  va_list  args;
  GError  *tmp_error = NULL;

  va_start (args, error);

  switch (prop_type)
    {
    case PROP_END:
      size = 0;

      xcf_write_prop_type_check_error (info, prop_type, va_end (args));
      xcf_write_int32_check_error (info, &size, 1, va_end (args));
      break;

    case PROP_COLORMAP:
      {
        guint32  n_colors = va_arg (args, guint32);
        guchar  *colors   = va_arg (args, guchar *);

        size = 4 + n_colors * 3;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &n_colors, 1, va_end (args));
        xcf_write_int8_check_error  (info, colors,    n_colors * 3, va_end (args));
      }
      break;

    case PROP_ACTIVE_LAYER:
    case PROP_ACTIVE_CHANNEL:
    case PROP_SELECTED_PATH:
    case PROP_SELECTION:
    case PROP_GROUP_ITEM:
      size = 0;

      xcf_write_prop_type_check_error (info, prop_type, va_end (args));
      xcf_write_int32_check_error (info, &size, 1, va_end (args));
      break;

    case PROP_FLOATING_SELECTION:
      size = info->bytes_per_offset;

      xcf_write_prop_type_check_error (info, prop_type, va_end (args));
      xcf_write_int32_check_error (info, &size, 1, va_end (args));

      info->floating_sel_offset = info->cp;
      xcf_write_zero_offset_check_error (info, 1, va_end (args));
      break;

    case PROP_OPACITY:
      {
        gdouble opacity      = va_arg (args, gdouble);
        guint32 uint_opacity = opacity * 255.999;

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &uint_opacity, 1, va_end (args));
      }
      break;

    case PROP_FLOAT_OPACITY:
      {
        gfloat opacity = va_arg (args, gdouble);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_float_check_error (info, &opacity, 1, va_end (args));
      }
      break;

    case PROP_MODE:
      {
        gint32 mode = va_arg (args, gint32);

        size = 4;

        if (mode == GIMP_LAYER_MODE_OVERLAY_LEGACY)
          mode = GIMP_LAYER_MODE_SOFTLIGHT_LEGACY;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, (guint32 *) &mode, 1, va_end (args));
      }
      break;

    case PROP_BLEND_SPACE:
      {
        GimpLayerMode mode        = va_arg (args, GimpLayerMode);
        gint32        blend_space = va_arg (args, gint32);

        G_STATIC_ASSERT (GIMP_LAYER_COLOR_SPACE_AUTO == 0);

        /* if blend_space is AUTO, save the negative of the actual value AUTO
         * maps to for the given mode, so that we can correctly load it even if
         * the mapping changes in the future.
         */
        if (blend_space == GIMP_LAYER_COLOR_SPACE_AUTO)
          blend_space = -gimp_layer_mode_get_blend_space (mode);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, (guint32 *) &blend_space, 1, va_end (args));
      }
      break;

    case PROP_COMPOSITE_SPACE:
      {
        GimpLayerMode mode            = va_arg (args, GimpLayerMode);
        gint32        composite_space = va_arg (args, gint32);

        G_STATIC_ASSERT (GIMP_LAYER_COLOR_SPACE_AUTO == 0);

        /* if composite_space is AUTO, save the negative of the actual value
         * AUTO maps to for the given mode, so that we can correctly load it
         * even if the mapping changes in the future.
         */
        if (composite_space == GIMP_LAYER_COLOR_SPACE_AUTO)
          composite_space = -gimp_layer_mode_get_composite_space (mode);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, (guint32 *) &composite_space, 1, va_end (args));
      }
      break;

    case PROP_COMPOSITE_MODE:
      {
        GimpLayerMode mode           = va_arg (args, GimpLayerMode);
        gint32        composite_mode = va_arg (args, gint32);

        G_STATIC_ASSERT (GIMP_LAYER_COMPOSITE_AUTO == 0);

        /* if composite_mode is AUTO, save the negative of the actual value
         * AUTO maps to for the given mode, so that we can correctly load it
         * even if the mapping changes in the future.
         */
        if (composite_mode == GIMP_LAYER_COMPOSITE_AUTO)
          composite_mode = -gimp_layer_mode_get_composite_mode (mode);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, (guint32 *) &composite_mode, 1, va_end (args));
      }
      break;

    case PROP_VISIBLE:
      {
        guint32 visible = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &visible, 1, va_end (args));
      }
      break;

    case PROP_LINKED:
      /* This code should not be called any longer. */
      g_return_val_if_reached (FALSE);
#if 0
      {
        guint32 linked = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &linked, 1, va_end (args));
      }
#endif
      break;

    case PROP_COLOR_TAG:
      {
        guint32 color_tag = va_arg (args, gint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &color_tag, 1, va_end (args));
      }
      break;

    case PROP_LOCK_CONTENT:
      {
        guint32 lock_content = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &lock_content, 1, va_end (args));
      }
      break;

    case PROP_LOCK_ALPHA:
      {
        guint32 lock_alpha = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &lock_alpha, 1, va_end (args));
      }
      break;

    case PROP_LOCK_POSITION:
      {
        guint32 lock_position = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &lock_position, 1, va_end (args));
      }
      break;

    case PROP_LOCK_VISIBILITY:
      {
        guint32 lock_visibility = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &lock_visibility, 1, va_end (args));
      }
      break;

    case PROP_APPLY_MASK:
      {
        guint32 apply_mask = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &apply_mask, 1, va_end (args));
      }
      break;

    case PROP_EDIT_MASK:
      {
        guint32 edit_mask = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &edit_mask, 1, va_end (args));
      }
      break;

    case PROP_SHOW_MASK:
      {
        guint32 show_mask = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &show_mask, 1, va_end (args));
      }
      break;

    case PROP_SHOW_MASKED:
      {
        guint32 show_masked = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &show_masked, 1, va_end (args));
      }
      break;

    case PROP_OFFSETS:
      {
        gint32 offsets[2];

        offsets[0] = va_arg (args, gint32);
        offsets[1] = va_arg (args, gint32);

        size = 8;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, (guint32 *) offsets, 2, va_end (args));
      }
      break;

    case PROP_COLOR:
      {
        GeglColor *color = va_arg (args, GeglColor *);
        guchar     col[3];

        gegl_color_get_pixel (color, babl_format ("R'G'B' u8"), col);

        size = 3;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int8_check_error (info, col, 3, va_end (args));
      }
      break;

    case PROP_FLOAT_COLOR:
      {
        GeglColor *color = va_arg (args, GeglColor *);
        gfloat     col[3];

        gegl_color_get_pixel (color, babl_format ("R'G'B' float"), col);

        size = 3 * 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_float_check_error (info, col, 3, va_end (args));
      }
      break;

    case PROP_COMPRESSION:
      {
        guint8 compression = (guint8) va_arg (args, guint32);

        size = 1;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int8_check_error (info, &compression, 1, va_end (args));
      }
      break;

    case PROP_GUIDES:
      {
        GList *guides   = va_arg (args, GList *);
        gint   n_guides = g_list_length (guides);
        GList *iter;

        for (iter = guides; iter; iter = g_list_next (iter))
          {
            /* Do not save custom guides. */
            if (gimp_guide_is_custom (GIMP_GUIDE (iter->data)))
              n_guides--;
          }

        if (n_guides > 0)
          {
            size = n_guides * (4 + 1);

            xcf_write_prop_type_check_error (info, prop_type, va_end (args));
            xcf_write_int32_check_error (info, &size, 1, va_end (args));

            for (; guides; guides = g_list_next (guides))
              {
                GimpGuide *guide    = guides->data;
                gint32     position = gimp_guide_get_position (guide);
                gint8      orientation;

                if (gimp_guide_is_custom (guide))
                  continue;

                switch (gimp_guide_get_orientation (guide))
                  {
                  case GIMP_ORIENTATION_HORIZONTAL:
                    orientation = XCF_ORIENTATION_HORIZONTAL;
                    break;

                  case GIMP_ORIENTATION_VERTICAL:
                    orientation = XCF_ORIENTATION_VERTICAL;
                    break;

                  default:
                    g_warning ("%s: skipping guide with bad orientation",
                               G_STRFUNC);
                    continue;
                  }

                xcf_write_int32_check_error (info, (guint32 *) &position,    1, va_end (args));
                xcf_write_int8_check_error  (info, (guint8 *)  &orientation, 1, va_end (args));
              }
          }
      }
      break;

    case PROP_SAMPLE_POINTS:
      {
        GList *sample_points   = va_arg (args, GList *);
        gint   n_sample_points = g_list_length (sample_points);

        size = n_sample_points * (5 * 4);

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        for (; sample_points; sample_points = g_list_next (sample_points))
          {
            GimpSamplePoint   *sample_point = sample_points->data;
            gint32             x, y;
            GimpColorPickMode  pick_mode;
            guint32            padding[2] = { 0, };

            gimp_sample_point_get_position (sample_point, &x, &y);
            pick_mode = gimp_sample_point_get_pick_mode (sample_point);

            xcf_write_int32_check_error (info, (guint32 *) &x,         1, va_end (args));
            xcf_write_int32_check_error (info, (guint32 *) &y,         1, va_end (args));
            xcf_write_int32_check_error (info, (guint32 *) &pick_mode, 1, va_end (args));
            xcf_write_int32_check_error (info, (guint32 *) padding,    2, va_end (args));
          }
      }
      break;

    case PROP_OLD_SAMPLE_POINTS:
      {
        GList *sample_points   = va_arg (args, GList *);
        gint   n_sample_points = g_list_length (sample_points);

        size = n_sample_points * (4 + 4);

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        for (; sample_points; sample_points = g_list_next (sample_points))
          {
            GimpSamplePoint *sample_point = sample_points->data;
            gint32           x, y;

            gimp_sample_point_get_position (sample_point, &x, &y);

            xcf_write_int32_check_error (info, (guint32 *) &x, 1, va_end (args));
            xcf_write_int32_check_error (info, (guint32 *) &y, 1, va_end (args));
          }
      }
      break;

    case PROP_RESOLUTION:
      {
        gfloat resolution[2];

        resolution[0] = va_arg (args, double);
        resolution[1] = va_arg (args, double);

        size = 2 * 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_float_check_error (info, resolution, 2, va_end (args));
      }
      break;

    case PROP_TATTOO:
      {
        guint32 tattoo = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &tattoo, 1, va_end (args));
      }
      break;

    case PROP_PARASITES:
      {
        GimpParasiteList *list = va_arg (args, GimpParasiteList *);

        if (gimp_parasite_list_persistent_length (list) > 0)
          {
            goffset base;
            goffset pos;

            size = 0;

            xcf_write_prop_type_check_error (info, prop_type, va_end (args));

            /* because we don't know how much room the parasite list
             * will take we save the file position and write the
             * length later
             */
            pos = info->cp;
            xcf_write_int32_check_error (info, &size, 1, va_end (args));

            base = info->cp;

            xcf_check_error (xcf_save_parasite_list (info, list, error), va_end (args));

            size = info->cp - base;

            /* go back to the saved position and write the length */
            xcf_check_error (xcf_seek_pos (info, pos, error), va_end (args));
            xcf_write_int32_check_error (info, &size, 1, va_end (args));

            xcf_check_error (xcf_seek_pos (info, base + size, error), va_end (args));
          }
      }
      break;

    case PROP_UNIT:
      {
        GimpUnit *unit       = va_arg (args, GimpUnit *);
        guint32   unit_index = gimp_unit_get_id (unit);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &unit_index, 1, va_end (args));
      }
      break;

    case PROP_PATHS:
      {
        goffset base;
        goffset pos;

        size = 0;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));

        /* because we don't know how much room the paths list will
         * take we save the file position and write the length later
         */
        pos = info->cp;
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        base = info->cp;

        xcf_check_error (xcf_save_old_paths (info, image, error), va_end (args));

        size = info->cp - base;

        /* go back to the saved position and write the length */
        xcf_check_error (xcf_seek_pos (info, pos, error), va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_check_error (xcf_seek_pos (info, base + size, error), va_end (args));
      }
      break;

    case PROP_USER_UNIT:
      {
        GimpUnit    *unit = va_arg (args, GimpUnit *);
        const gchar *unit_strings[5];
        gfloat       factor;
        guint32      digits;

        /* write the entire unit definition */
        unit_strings[0] = gimp_unit_get_name (unit);
        factor          = gimp_unit_get_factor (unit);
        digits          = gimp_unit_get_digits (unit);
        unit_strings[1] = gimp_unit_get_symbol (unit);
        unit_strings[2] = gimp_unit_get_abbreviation (unit);
        /* Singular and plural forms were deprecated in XCF 21. Just use
         * the unit name as bogus (yet reasonable) replacements.
         */
        unit_strings[3] = gimp_unit_get_name (unit);
        unit_strings[4] = gimp_unit_get_name (unit);

        size =
          2 * 4 +
          strlen (unit_strings[0]) ? strlen (unit_strings[0]) + 5 : 4 +
          strlen (unit_strings[1]) ? strlen (unit_strings[1]) + 5 : 4 +
          strlen (unit_strings[2]) ? strlen (unit_strings[2]) + 5 : 4;

        if (info->file_version < 21)
          size +=
            strlen (unit_strings[3]) ? strlen (unit_strings[3]) + 5 : 4 +
            strlen (unit_strings[4]) ? strlen (unit_strings[4]) + 5 : 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_float_check_error  (info, &factor,                 1, va_end (args));
        xcf_write_int32_check_error  (info, &digits,                 1, va_end (args));
        if (info->file_version < 21)
          xcf_write_string_check_error (info, (gchar **) unit_strings, 5, va_end (args));
        else
          xcf_write_string_check_error (info, (gchar **) unit_strings, 3, va_end (args));
      }
      break;

    case PROP_VECTORS:
      {
        goffset base;
        goffset pos;

        size = 0;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));

        /* because we don't know how much room the paths list will
         * take we save the file position and write the length later
         */
        pos = info->cp;
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        base = info->cp;

        xcf_check_error (xcf_save_old_vectors (info, image, error), va_end (args));

        size = info->cp - base;

        /* go back to the saved position and write the length */
        xcf_check_error (xcf_seek_pos (info, pos, error), va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_check_error (xcf_seek_pos (info, base + size, error), va_end (args));
      }
      break;

    case PROP_TEXT_LAYER_FLAGS:
      {
        guint32 flags = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &flags, 1, va_end (args));
      }
      break;

    case PROP_VECTOR_LAYER:
      {
        GimpVectorLayer        *vector_layer;
        GimpVectorLayerOptions *options;
        guint32                 uint_val;
        goffset                 base;
        goffset                 pos;

        vector_layer = va_arg (args, GimpVectorLayer *);
        options      = vector_layer->options;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));

        size = 0;
        pos = info->cp;
        xcf_write_int32_check_error (info, &size, 1, va_end (args));
        base = info->cp;

        uint_val = (guint32) gimp_rasterizable_is_rasterized (GIMP_RASTERIZABLE (vector_layer));
        xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, va_end (args));

        uint_val = gimp_item_get_tattoo (GIMP_ITEM (options->path));
        xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, va_end (args));

        uint_val = (guint32) options->enable_fill;
        xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, va_end (args));

        uint_val = (guint32) options->enable_stroke;
        xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, va_end (args));

        xcf_check_error (xcf_save_fill_options (info, options->fill_options, error), va_end (args));
        xcf_check_error (xcf_save_stroke_options (info, options->stroke_options, error), va_end (args));

        size = info->cp - base;
        /* go back to the saved position and write the length */
        xcf_check_error (xcf_seek_pos (info, pos, error), va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));
        xcf_check_error (xcf_seek_pos (info, base + size, error), va_end (args));
      }
    break;

    case PROP_LINK_LAYER:
      {
        GimpLinkLayer *layer = va_arg (args, GimpLinkLayer *);
        gchar         *path  = NULL;
        gint           width;
        gint           height;
        guint32        dimensions[2];
        guint32        flags;

        flags = gimp_link_layer_get_xcf_flags (layer);
        gimp_link_get_file (gimp_link_layer_get_link (layer), info->file, &path);

        size = 3 * 4 + (strlen (path) ? strlen (path) + 1 : 4);

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &flags, 1, va_end (args));
        xcf_write_string_check_error (info, (gchar **) &path, 1, va_end (args));

        gimp_link_get_size (gimp_link_layer_get_link (layer), &width, &height);
        dimensions[0] = width;
        dimensions[1] = height;
        xcf_write_int32_check_error (info, dimensions, 2, va_end (args));

        g_free (path);
      }
      break;

    case PROP_TRANSFORM:
      {
        GimpLinkLayer         *layer = va_arg (args, GimpLinkLayer *);
        GimpMatrix3            matrix;
        gint                   offset_x;
        gint                   offset_y;
        GimpInterpolationType  interpolation;
        gint32                 int_val;
        guint32                uint_val;
        gfloat                 mfloat[9];

        gimp_link_layer_get_transform (layer, &matrix, &offset_x, &offset_y, &interpolation);

        size = 4 * 12;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        int_val = (gint32) offset_x;
        xcf_write_int32_check_error (info, (guint32 *) &int_val, 1, va_end (args));
        int_val = (gint32) offset_y;
        xcf_write_int32_check_error (info, (guint32 *) &int_val, 1, va_end (args));
        uint_val = (guint32) interpolation;
        xcf_write_int32_check_error (info, &uint_val, 1, va_end (args));

        mfloat[0] = matrix.coeff[0][0];
        mfloat[1] = matrix.coeff[0][1];
        mfloat[2] = matrix.coeff[0][2];
        mfloat[3] = matrix.coeff[1][0];
        mfloat[4] = matrix.coeff[1][1];
        mfloat[5] = matrix.coeff[1][2];
        mfloat[6] = matrix.coeff[2][0];
        mfloat[7] = matrix.coeff[2][1];
        mfloat[8] = matrix.coeff[2][2];
        xcf_write_float_check_error (info, mfloat, 9, va_end (args));
      }
      break;

    case PROP_ITEM_PATH:
      {
        GList *path = va_arg (args, GList *);

        size = 4 * g_list_length (path);

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        while (path)
          {
            guint32 index = GPOINTER_TO_UINT (path->data);

            xcf_write_int32_check_error (info, &index, 1, va_end (args));

            path = g_list_next (path);
          }
      }
      break;

    case PROP_GROUP_ITEM_FLAGS:
      {
        guint32 flags = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));
        xcf_write_int32_check_error (info, &flags, 1, va_end (args));
      }
      break;

    case PROP_ITEM_SET:
      {
        GimpItemList *set = va_arg (args, GimpItemList *);
        const gchar  *string;
        guint32       method;
        guint32       item_type;
        goffset       base;
        goffset       pos;

        size = 0;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        pos = info->cp;
        xcf_write_int32_check_error (info, &size, 1, va_end (args));
        base = info->cp;

        if (gimp_item_list_get_item_type (set) == GIMP_TYPE_LAYER)
          item_type = 0;
        else if (gimp_item_list_get_item_type (set) == GIMP_TYPE_CHANNEL)
          item_type = 1;
        else if (gimp_item_list_get_item_type (set) == GIMP_TYPE_PATH)
          item_type = 2;
        else
          g_return_val_if_reached (FALSE);
        xcf_write_int32_check_error (info, &item_type, 1, va_end (args));

        if (! gimp_item_list_is_pattern (set, &method))
          method = G_MAXUINT32;
        xcf_write_int32_check_error (info, &method, 1, va_end (args));

        string = gimp_object_get_name (set);
        xcf_write_string_check_error (info, (gchar **) &string, 1, va_end (args));

        /* go back to the saved position and write the length */
        size = info->cp - base;
        xcf_check_error (xcf_seek_pos (info, pos, error), va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_check_error (xcf_seek_pos (info, base + size, error), va_end (args));
      }
      break;

    case PROP_ITEM_SET_ITEM:
      {
        guint32 set_n = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));
        xcf_write_int32_check_error (info, &set_n, 1, va_end (args));
      }
      break;

    case PROP_FILTER_REGION:
      {
        guint32 filter_region = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &filter_region, 1, va_end (args));
      }
      break;

    case PROP_FILTER_ARGUMENT:
      {
        const gchar *string      = va_arg (args, const gchar *);
        guint32      filter_type = va_arg (args, guint32);
        GValue       filter_value;
        goffset      pos;
        goffset      base;

        size = 0;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        pos = info->cp;
        xcf_write_int32_check_error (info, &size, 1, va_end (args));
        base = info->cp;
        filter_value = va_arg (args, GValue);

        xcf_write_string_check_error (info, (gchar **) &string, 1, va_end (args));
        xcf_write_int32_check_error (info, &filter_type, 1, va_end (args));

        switch (filter_type)
          {
            case FILTER_PROP_INT:
            case FILTER_PROP_UINT:
            case FILTER_PROP_ENUM:
              {
                guint32 value;

                if (filter_type == FILTER_PROP_INT)
                  value = g_value_get_int (&filter_value);
                else if (filter_type == FILTER_PROP_UINT)
                  value = g_value_get_uint (&filter_value);
                else
                  value = g_value_get_enum (&filter_value);

                xcf_write_int32_check_error (info, &value, 1, va_end (args));
              }
              break;

            case FILTER_PROP_BOOL:
              {
                gboolean value = g_value_get_boolean (&filter_value);

                xcf_write_int32_check_error (info, (guint32 *) &value, 1, va_end (args));
              }
              break;

            case FILTER_PROP_FLOAT:
              {
                gfloat value = g_value_get_double (&filter_value);

                xcf_write_float_check_error (info, &value, 1, va_end (args));
              }
              break;

            case FILTER_PROP_STRING:
              {
                const gchar *value = g_value_get_string (&filter_value);

                xcf_write_string_check_error (info, (gchar **) &value, 1, va_end (args));
              }
              break;

            case FILTER_PROP_COLOR:
              {
                GeglColor *color = g_value_get_object (&filter_value);
                xcf_check_error (xcf_save_color (info, color, error), va_end (args));
              }
              break;

            case FILTER_PROP_CONFIG:
              {
                GimpConfig *config = g_value_get_object (&filter_value);
                gchar      *value  = gimp_config_serialize_to_string (config, NULL);

                xcf_write_string_check_error (info, (gchar **) &value, 1, va_end (args));
                g_free (value);
              }
              break;

            default:
              break;
          }

        /* go back to the saved position and write the length */
        size = info->cp - base;
        xcf_check_error (xcf_seek_pos (info, pos, error), va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_check_error (xcf_seek_pos (info, base + size, error), va_end (args));
      }
      break;

    case PROP_FILTER_CLIP:
      {
        guint32 visible = va_arg (args, guint32);

        size = 4;

        xcf_write_prop_type_check_error (info, prop_type, va_end (args));
        xcf_write_int32_check_error (info, &size, 1, va_end (args));

        xcf_write_int32_check_error (info, &visible, 1, va_end (args));
      }
      break;
    }

  va_end (args);

  return TRUE;
}

static gboolean
xcf_save_layer (XcfInfo    *info,
                GimpImage  *image,
                GimpLayer  *layer,
                GError    **error)
{
  goffset        saved_pos;
  goffset        offset;
  guint32        value;
  const gchar   *string;
  GimpContainer *filters;
  GList         *filter_list;
  guint32        num_effects = 0;
  GList         *list;
  GError        *tmp_error   = NULL;

  /* check and see if this is the drawable that the floating
   *  selection is attached to.
   */
  if (GIMP_DRAWABLE (layer) == info->floating_sel_drawable)
    {
      saved_pos = info->cp;
      xcf_check_error (xcf_seek_pos (info, info->floating_sel_offset, error), ;);
      xcf_write_offset_check_error (info, &saved_pos, 1, ;);
      xcf_check_error (xcf_seek_pos (info, saved_pos, error), ;);
    }

  /* Get filter information */
  filters = gimp_drawable_get_filters (GIMP_DRAWABLE (layer));
  /* Since floating selections are also stored in the filter stack,
   * we need to verify what's in there to get the correct count */
  for (filter_list = GIMP_LIST (filters)->queue->tail; filter_list;
       filter_list = g_list_previous (filter_list))
    {
      if (GIMP_IS_DRAWABLE_FILTER (filter_list->data) &&
          ! gimp_drawable_filter_get_temporary (filter_list->data))
        {
          GimpDrawableFilter     *filter  = filter_list->data;
          GimpDrawableFilterMask *mask    = NULL;
          GeglNode               *op_node = NULL;

          mask    = gimp_drawable_filter_get_mask (filter);
          op_node = gimp_drawable_filter_get_operation (filter);

          /* For now, prevent tool-based filters from being saved */
          if (mask    != NULL &&
              op_node != NULL &&
              strcmp (gegl_node_get_operation (op_node), "GraphNode") != 0)
            num_effects++;
        }
    }

  /* write out the width, height and image type information for the layer */
  value = gimp_item_get_width (GIMP_ITEM (layer));
  xcf_write_int32_check_error (info, &value, 1, ;);

  value = gimp_item_get_height (GIMP_ITEM (layer));
  xcf_write_int32_check_error (info, &value, 1, ;);

  value = gimp_babl_format_get_image_type (gimp_drawable_get_format (GIMP_DRAWABLE (layer)));
  xcf_write_int32_check_error (info, &value, 1, ;);

  /* write out the layers name */
  string = gimp_object_get_name (layer);
  xcf_write_string_check_error (info, (gchar **) &string, 1, ;);

  /* write out the layer properties */
  xcf_save_layer_props (info, image, layer, error);

  /* write out the layer tile hierarchy and effects */
  offset = info->cp + (2 + num_effects + 1) * info->bytes_per_offset;
  xcf_write_offset_check_error (info, &offset, 1, ;);

  saved_pos = info->cp;

  /* write a zero layer mask offset */
  xcf_write_zero_offset_check_error (info, 1, ;);

  /* write out zero effect and effect mask offset(s) */
  for (gint i = 0; i < num_effects + 1; i++)
    xcf_write_zero_offset_check_error (info, 1, ;);

  xcf_check_error (xcf_save_buffer (info, image,
                                    gimp_drawable_get_buffer (GIMP_DRAWABLE (layer)),
                                    error), ;);

  offset = info->cp;

  /* write out the layer mask */
  if (gimp_layer_get_mask (layer))
    {
      GimpLayerMask *mask = gimp_layer_get_mask (layer);

      xcf_check_error (xcf_seek_pos (info, saved_pos, error), ;);
      xcf_write_offset_check_error (info, &offset, 1, ;);

      xcf_check_error (xcf_seek_pos (info, offset, error), ;);
      xcf_check_error (xcf_save_channel (info, image, GIMP_CHANNEL (mask),
                                         error), ;);
    }

  /* write out any layer effects and effect masks */
  if (num_effects > 0)
    {
      saved_pos += info->bytes_per_offset;

      for (list = GIMP_LIST (filters)->queue->head; list;
           list = g_list_next (list))
        {
          if (GIMP_IS_DRAWABLE_FILTER (list->data) &&
              ! gimp_drawable_filter_get_temporary (list->data))
            {
              GimpDrawableFilter     *filter  = list->data;
              GimpDrawableFilterMask *mask    = NULL;
              GeglNode               *op_node = NULL;

              mask    = gimp_drawable_filter_get_mask (filter);
              op_node = gimp_drawable_filter_get_operation (filter);

               /* For now, prevent tool-based filters from being saved */
              if (mask    != NULL &&
                  op_node != NULL &&
                  strcmp (gegl_node_get_operation (op_node), "GraphNode") != 0)
                {
                  offset = info->cp;

                  xcf_check_error (xcf_seek_pos (info, saved_pos, error), ;);
                  xcf_write_offset_check_error (info, &offset, 1, ;);

                  saved_pos = info->cp;

                  xcf_check_error (xcf_seek_pos (info, offset, error), ;);
                  xcf_check_error (xcf_save_effect (info, image,
                                                    GIMP_FILTER (filter),
                                                    error), ;);
                }
            }
        }
      g_list_free (list);
    }

  return TRUE;
}

static gboolean
xcf_save_channel (XcfInfo      *info,
                  GimpImage    *image,
                  GimpChannel  *channel,
                  GError      **error)
{
  goffset      saved_pos;
  goffset      offset;
  guint32      value;
  const gchar *string;
  GError      *tmp_error = NULL;

  /* check and see if this is the drawable that the floating
   *  selection is attached to.
   */
  if (GIMP_DRAWABLE (channel) == info->floating_sel_drawable)
    {
      saved_pos = info->cp;
      xcf_check_error (xcf_seek_pos (info, info->floating_sel_offset, error), ;);
      xcf_write_offset_check_error (info, &saved_pos, 1, ;);
      xcf_check_error (xcf_seek_pos (info, saved_pos, error), ;);
    }

  /* write out the width and height information for the channel */
  value = gimp_item_get_width (GIMP_ITEM (channel));
  xcf_write_int32_check_error (info, &value, 1, ;);

  value = gimp_item_get_height (GIMP_ITEM (channel));
  xcf_write_int32_check_error (info, &value, 1, ;);

  /* write out the channels name */
  string = gimp_object_get_name (channel);
  xcf_write_string_check_error (info, (gchar **) &string, 1, ;);

  /* write out the channel properties */
  xcf_save_channel_props (info, image, channel, error);

  /* write out the channel tile hierarchy */
  offset = info->cp + info->bytes_per_offset;
  xcf_write_offset_check_error (info, &offset, 1, ;);

  xcf_check_error (xcf_save_buffer (info, image,
                                    gimp_drawable_get_buffer (GIMP_DRAWABLE (channel)),
                                    error), ;);

  return TRUE;
}

static gboolean
xcf_save_effect (XcfInfo     *info,
                 GimpImage   *image,
                 GimpFilter  *filter,
                 GError     **error)
{
  gchar              *name;
  gchar              *icon;
  gboolean            has_custom_name;
  GimpDrawableFilter *filter_drawable;
  GeglNode           *node;
  gchar              *operation;
  const gchar        *op_version;
  GimpChannel        *effect_mask;
  goffset             offset;
  GError             *tmp_error = NULL;

  filter_drawable = GIMP_DRAWABLE_FILTER (filter);
  node            = gimp_drawable_filter_get_operation (filter_drawable);

  gegl_node_get (node,
                 "operation", &operation,
                 NULL);

  g_object_get (filter,
                "name",        &name,
                "icon-name",   &icon,
                "custom-name", &has_custom_name,
                NULL);

  /* Write out effect name */
  if (has_custom_name)
    {
      xcf_write_string_check_error (info, (gchar **) &name, 1, ;);
    }
  else
    {
      gchar *empty = NULL;

      xcf_write_string_check_error (info, (gchar **) &empty, 1, ;);
    }
  g_free (name);

  /* Write out effect icon */
  xcf_write_string_check_error (info, (gchar **) &icon, 1, ;);
  g_free (icon);

  /* Write out GEGL operation name */
  xcf_write_string_check_error (info, (gchar **) &operation, 1, ;);

  if (info->file_version >= 22)
    {
      /* Write out GEGL operation version */
      op_version = gegl_operation_get_op_version (operation);
      xcf_write_string_check_error (info, (gchar **) &op_version, 1, ;);
    }
  g_free (operation);

  /* write out the effect properties */
  xcf_save_effect_props (info, image, filter, error);

  /* write a zero effect mask offset */
  offset = info->cp + info->bytes_per_offset;
  xcf_write_offset_check_error (info, &offset, 1, ;);

  effect_mask = GIMP_CHANNEL (gimp_drawable_filter_get_mask (filter_drawable));
  xcf_check_error (xcf_save_channel (info, image, effect_mask,
                                     error), ;);

  return TRUE;
}

static gboolean
xcf_save_color (XcfInfo    *info,
                GeglColor  *color,
                GError    **error)
{
  GError *tmp_error = NULL;

  if (color)
    {
      const gchar   *encoding;
      const Babl    *format = gegl_color_get_format (color);
      const Babl    *space;
      GBytes        *bytes;
      gconstpointer  data;
      gsize          data_length;
      int            profile_length = 0;

      if (babl_format_is_palette (format))
        {
          guint8     pixel[40];
          GeglColor *palette_color;

          /* As a special case, we don't want to serialize
           * palette colors, because they are just too much
           * dependent on external data and cannot be
           * deserialized back safely. So we convert them first.
           */
          palette_color = gegl_color_duplicate (color);

          format = babl_format_with_space ("R'G'B'A u8", format);
          gegl_color_get_pixel (palette_color, format, pixel);
          gegl_color_set_pixel (color, format, pixel);

          g_object_unref (palette_color);
        }

      encoding = babl_format_get_encoding (format);
      xcf_write_string_check_error (info, (gchar **) &encoding, 1, ;);

      bytes = gegl_color_get_bytes (color, format);
      data  = (guint8 *) g_bytes_get_data (bytes, &data_length);

      xcf_write_int32_check_error (info, (guint32 *) &data_length, 1, ;);
      xcf_write_int8_check_error (info, (const guint8 *) data, data_length, ;);
      g_bytes_unref (bytes);

      space = babl_format_get_space (format);
      if (space != babl_space ("sRGB"))
        {
          guint8 *profile_data;

          profile_data = (guint8 *) babl_space_get_icc (babl_format_get_space (format),
                                                        &profile_length);
          xcf_write_int32_check_error (info, (guint32 *) &profile_length, 1, ;);

          if (profile_data)
            xcf_write_int8_check_error (info, profile_data, profile_length, ;);
        }
      else
        {
          xcf_write_int32_check_error (info, (guint32 *) &profile_length, 1, ;);
        }
    }
  else
    {
      guint32 uint_val = 0;

      xcf_write_int32_check_error (info, &uint_val, 1, ;);
    }

  return TRUE;
}

static gboolean
xcf_save_data_id (XcfInfo   *info,
                  GimpData  *data,
                  GError   **error)
{
  /* XXX Fonts may want to be special-cased because there may be more
   * than just the standard GimpData ID which would make reasonable
   * heuristic to retrieve a font. See gimp_font_serialize().
   *
   * TODO: in future updates of XCF, embedding the full data itself
   * rather than just point to it may be a worthy option too.
   */
  gchar    *name;
  gchar    *collection_id;
  gboolean  is_internal;
  guint32   uint_val;
  GError   *tmp_error = NULL;

  if (data)
    {
      gimp_data_get_identifiers  (data, &name, &collection_id, &is_internal);

      xcf_write_string_check_error (info, (gchar **) &name, 1, ;);
      xcf_write_string_check_error (info, (gchar **) &collection_id, 1, ;);
      uint_val = is_internal;
      xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, ;);

      g_free (name);
      g_free (collection_id);
    }
  else
    {
      guint32 uint_val = 0;

      xcf_write_int32_check_error (info, &uint_val, 1, ;);
    }

  return TRUE;
}

static gboolean
xcf_save_fill_options (XcfInfo          *info,
                       GimpFillOptions  *options,
                       GError          **error)
{
  GeglColor   *color;
  GimpPattern *pattern;
  guint32      uint_val;
  GError      *tmp_error = NULL;

  uint_val = (guint32) gimp_fill_options_get_custom_style (options);
  xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, ;);

  uint_val = (guint32) gimp_fill_options_get_antialias (options);
  xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, ;);

  color = gimp_context_get_foreground (GIMP_CONTEXT (options));
  xcf_check_error (xcf_save_color (info, color, error), ;);

  pattern = gimp_context_get_pattern (GIMP_CONTEXT (options));
  xcf_check_error (xcf_save_data_id (info, GIMP_DATA (pattern), error), ;);

  return TRUE;
}

static gboolean
xcf_save_stroke_options (XcfInfo            *info,
                         GimpStrokeOptions  *options,
                         GError            **error)
{
  GeglColor   *color;
  GimpPattern *pattern;
  guint32      uint_val;
  gfloat       float_val;
  GError      *tmp_error = NULL;
  GArray      *dash_info;
  gdouble     *dashes;
  gsize        n_dashes  = 0;

  uint_val = (guint32) gimp_fill_options_get_custom_style (GIMP_FILL_OPTIONS (options));
  xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, ;);

  uint_val = (guint32) gimp_fill_options_get_antialias (GIMP_FILL_OPTIONS (options));
  xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, ;);

  color = gimp_context_get_foreground (GIMP_CONTEXT (options));
  xcf_check_error (xcf_save_color (info, color, error), ;);

  pattern = gimp_context_get_pattern (GIMP_CONTEXT (options));
  xcf_check_error (xcf_save_data_id (info, GIMP_DATA (pattern), error), ;);

  float_val = (gfloat) gimp_stroke_options_get_width (options);
  xcf_write_float_check_error (info, (gfloat *) &float_val, 1, ;);

  uint_val = (guint32) gimp_stroke_options_get_cap_style (options);
  xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, ;);

  uint_val = (guint32) gimp_stroke_options_get_join_style (options);
  xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, ;);

  float_val = (gfloat) gimp_stroke_options_get_miter_limit (options);
  xcf_write_float_check_error (info, (gfloat *) &float_val, 1, ;);

  dash_info = gimp_stroke_options_get_dash_info (options);
  dashes = gimp_dash_pattern_to_double_array (dash_info, &n_dashes);
  uint_val = (guint32) n_dashes;
  xcf_write_int32_check_error (info, (guint32 *) &uint_val, 1, ;);

  for (gint i = 0; i < n_dashes; i++)
    {
      float_val = (gfloat) dashes[i];
      xcf_write_float_check_error (info, (gfloat *) &float_val, 1, ;);
    }
  g_free (dashes);

  return TRUE;
}

static gint
xcf_calc_levels (gint size,
                 gint tile_size)
{
  gint levels;

  levels = 1;
  while (size > tile_size)
    {
      size /= 2;
      levels += 1;
    }

  return levels;
}


static gboolean
xcf_save_buffer (XcfInfo     *info,
                 GimpImage   *image,
                 GeglBuffer  *buffer,
                 GError     **error)
{
  const Babl *format;
  goffset     saved_pos;
  goffset     offset;
  guint32     width;
  guint32     height;
  guint32     bpp;
  gint        i;
  gint        nlevels;
  gint        tmp1, tmp2;
  GError     *tmp_error = NULL;

  format = gegl_buffer_get_format (buffer);

  width  = gegl_buffer_get_width (buffer);
  height = gegl_buffer_get_height (buffer);
  bpp    = babl_format_get_bytes_per_pixel (format);

  xcf_write_int32_check_error (info, (guint32 *) &width,  1, ;);
  xcf_write_int32_check_error (info, (guint32 *) &height, 1, ;);
  xcf_write_int32_check_error (info, (guint32 *) &bpp,    1, ;);

  tmp1 = xcf_calc_levels (width,  XCF_TILE_WIDTH);
  tmp2 = xcf_calc_levels (height, XCF_TILE_HEIGHT);
  nlevels = MAX (tmp1, tmp2);

  /* 'saved_pos' is the next slot in the offset table */
  saved_pos = info->cp;

  /* write an empty offset table */
  xcf_write_zero_offset_check_error (info, nlevels + 1, ;);

  /* 'offset' is where we will write the next level */
  offset = info->cp;

  for (i = 0; i < nlevels; i++)
    {
      /* seek back to the next slot in the offset table and write the
       * offset of the level
       */
      xcf_check_error (xcf_seek_pos (info, saved_pos, error), ;);
      xcf_write_offset_check_error (info, &offset, 1, ;);

      /* remember the next slot in the offset table */
      saved_pos = info->cp;

      /* seek to the level offset and save the level */
      xcf_check_error (xcf_seek_pos (info, offset, error), ;);

      if (i == 0)
        {
          /* write out the level. */
          xcf_check_error (xcf_save_level (info, image, buffer, error), ;);
        }
      else
        {
          /* fake an empty level */
          tmp1 = 0;
          width  /= 2;
          height /= 2;
          xcf_write_int32_check_error (info, (guint32 *) &width,  1, ;);
          xcf_write_int32_check_error (info, (guint32 *) &height, 1, ;);

          /* NOTE:  this should be an offset, not an int32!  however...
           * since there are already 64-bit-offsets XCFs out there in
           * which this field is 32-bit, and since it's not actually
           * being used, we're going to keep this field 32-bit for the
           * dummy levels, to remain consistent.  if we ever make use
           * of levels above the first, we should turn this field into
           * an offset, and bump the xcf version.
           */
          xcf_write_int32_check_error (info, (guint32 *) &tmp1,   1, ;);
        }

      /* the next level's offset if after the level we just wrote */
      offset = info->cp;
    }

  /* there is already a '0' at the end of the offset table to indicate
   * the end of the level offsets
   */

  return TRUE;
}

static gboolean
xcf_save_level (XcfInfo     *info,
                GimpImage   *image,
                GeglBuffer  *buffer,
                GError     **error)
{
  const Babl *format;
  goffset    *offset_table;
  goffset    *next_offset;
  goffset     saved_pos;
  goffset     offset;
  goffset     max_data_length;
  guint32     width;
  guint32     height;
  gint        bpp;
  gint        n_tile_rows;
  gint        n_tile_cols;
  guint       ntiles;
  gint        num_processors;
  gint        i, j, k;
  GError     *tmp_error = NULL;

  num_processors = GIMP_GEGL_CONFIG (image->gimp->config)->num_processors;

  format = gegl_buffer_get_format (buffer);

  width  = gegl_buffer_get_width (buffer);
  height = gegl_buffer_get_height (buffer);
  bpp    = babl_format_get_bytes_per_pixel (format);

  xcf_write_int32_check_error (info, (guint32 *) &width,  1, ;);
  xcf_write_int32_check_error (info, (guint32 *) &height, 1, ;);

  saved_pos = info->cp;

  /* maximal allowable size of on-disk tile data.  make it somewhat bigger than
   * the uncompressed tile size, to allow for the possibility of negative
   * compression.  xcf_load_level() enforces this limit.
   */
  max_data_length = XCF_TILE_WIDTH * XCF_TILE_HEIGHT * bpp *
                    XCF_TILE_MAX_DATA_LENGTH_FACTOR /* = 1.5, currently */;

  n_tile_rows = gimp_gegl_buffer_get_n_tile_rows (buffer, XCF_TILE_HEIGHT);
  n_tile_cols = gimp_gegl_buffer_get_n_tile_cols (buffer, XCF_TILE_WIDTH);

  ntiles = n_tile_rows * n_tile_cols;

  /* allocate an offset table so we don't have to seek back after each
   * tile, see bug #686862. allocate ntiles + 1 slots because a zero
   * offset indicates the offset table's end.
   * Do not use g_alloca since it may cause Stack Overflow on
   * large images, see issue #6138.
   */
  offset_table = g_malloc0 ((ntiles + 1) * sizeof (goffset));
  next_offset = offset_table;

  /* 'saved_pos' is the offset of the tile offset table  */
  saved_pos = info->cp;

  /* write an empty offset table */
  xcf_write_zero_offset_check_error (info, ntiles + 1, ;);

  /* 'offset' is where we will write the next tile */
  offset = info->cp;

  if (info->compression == COMPRESS_RLE ||
      info->compression == COMPRESS_ZLIB)
    {
      /* parallel implementation */
      XcfJobData  *job_data;
      guchar      *switch_out_data;
      gint         out_data_len[XCF_TILE_SAVE_BATCH_SIZE];

      GThreadPool *pool;
      GAsyncQueue *queue;
      gint         num_tasks = num_processors * 2;
      gint         tile_size = XCF_TILE_WIDTH * XCF_TILE_HEIGHT * bpp;
      gint         out_data_max_size;
      gint         next_tile = 0;

      out_data_max_size = tile_size * XCF_TILE_MAX_DATA_LENGTH_FACTOR;
      /* Prepare an additional out_data to quickly switch. */
      switch_out_data   = g_malloc (out_data_max_size * XCF_TILE_SAVE_BATCH_SIZE);

      /* The free function passed to the queue and thread pool will likely never
       * be used. It would mean the thread pool is unfinidhed or the result
       * queue still has data which would mean we had to interrupt the save,
       * i.e. there is a bug in our code.
       */
      queue = g_async_queue_new_full ((GDestroyNotify) xcf_save_free_job_data);
      pool  = g_thread_pool_new_full ((GFunc) xcf_save_tile_parallel,
                                      queue,
                                      (GDestroyNotify) xcf_save_free_job_data,
                                      num_processors, TRUE, NULL);

      i = 0;
      /* We push more tasks than there are threads, ensuring threads always have
       * something to do!
       */
      for (j = 0; j < num_tasks && i < ntiles; j++)
        {
          job_data = g_malloc (sizeof (XcfJobData ));
          job_data->buffer        = buffer;
          job_data->file_version  = info->file_version;
          job_data->max_out_data_len = out_data_max_size;
          job_data->compress      = (info->compression == COMPRESS_RLE) ?
                                      xcf_save_tile_rle : xcf_save_tile_zlib;
          job_data->tile_data     = g_malloc (tile_size);
          job_data->out_data      = g_malloc (out_data_max_size * XCF_TILE_SAVE_BATCH_SIZE);

          job_data->tile       = i;
          job_data->batch_size = MIN (XCF_TILE_SAVE_BATCH_SIZE, ntiles - i);
          i += job_data->batch_size;

          g_thread_pool_push (pool, job_data, NULL);
        }

      /* Continue pushing tasks and writing tasks as long as we have tiles to
       * process.
       */
      while (i < ntiles)
        {
          while ((job_data = g_async_queue_pop (queue)))
            {
              if (next_tile == job_data->tile)
                {
                  guchar *tmp_out_data;
                  gint    batch_size;

                  tmp_out_data = job_data->out_data;
                  job_data->out_data = switch_out_data;
                  switch_out_data = tmp_out_data;

                  batch_size = job_data->batch_size;

                  for (k = 0; k < batch_size; k++)
                    out_data_len[k] = job_data->out_data_len[k];

                  /* First immediately push a new task for the thread pool,
                   * ensuring it always has work to do.
                   */
                  job_data->tile       = i;
                  job_data->batch_size = MIN (XCF_TILE_SAVE_BATCH_SIZE, ntiles - i);
                  i += job_data->batch_size;

                  g_thread_pool_push (pool, job_data, NULL);

                  /* Now write the data. */
                  for (k = 0; k < batch_size; k++)
                    {
                      *next_offset++ = offset;
                      xcf_write_int8_check_error (info,
                                                  switch_out_data + out_data_max_size * k,
                                                  out_data_len[k], ;);
                      if (info->cp < offset || info->cp - offset > max_data_length)
                        {
                          g_message ("xcf: invalid tile data length: %" G_GOFFSET_FORMAT,
                                     info->cp - offset);
                          g_thread_pool_free (pool, TRUE, TRUE);
                          g_async_queue_unref (queue);
                          g_free (offset_table);
                          return FALSE;
                        }
                      offset = info->cp;
                    }
                  next_tile += batch_size;
                  break;
                }
              else
                {
                  g_async_queue_push_sorted (queue, job_data,
                                             (GCompareDataFunc) xcf_save_sort_job_data,
                                             NULL);
                }
            }
        }
      g_free (switch_out_data);

      /* Finally wait for all remaining tasks to write. */
      while ((job_data = g_async_queue_pop (queue)))
        {
          if (next_tile == job_data->tile)
            {
              gboolean done = FALSE;

              for (k = 0; k < job_data->batch_size; k++)
                {
                  *next_offset++ = offset;
                  xcf_write_int8_check_error (info,
                                              job_data->out_data + out_data_max_size * k,
                                              job_data->out_data_len[k], ;);
                  if (info->cp < offset || info->cp - offset > max_data_length)
                    {
                      g_message ("xcf: invalid tile data length: %" G_GOFFSET_FORMAT,
                                 info->cp - offset);
                      g_thread_pool_free (pool, TRUE, TRUE);
                      g_async_queue_unref (queue);
                      g_free (offset_table);
                      return FALSE;
                    }
                  offset = info->cp;
                }
              next_tile += job_data->batch_size;

              if (job_data->tile + job_data->batch_size >= ntiles)
                done = TRUE;

              xcf_save_free_job_data (job_data);

              if (done)
                break;
            }
          else
            {
              g_async_queue_push_sorted (queue, job_data,
                                         (GCompareDataFunc) xcf_save_sort_job_data,
                                         NULL);
            }
        }

      g_thread_pool_free (pool, FALSE, TRUE);
      g_async_queue_unref (queue);
    }
  else
    {
      /* non parallel implementation */
      for (i = 0; i < ntiles; i++)
        {
          GeglRectangle rect;

          /* store the offset in the table and increment the next pointer */
          *next_offset++ = offset;

          gimp_gegl_buffer_get_tile_rect (buffer,
                                          XCF_TILE_WIDTH, XCF_TILE_HEIGHT,
                                          i, &rect);

          /* write out the tile. */
          switch (info->compression)
            {
            case COMPRESS_NONE:
              xcf_check_error (xcf_save_tile (info, buffer, &rect, format,
                                              error), ;);
              break;
            case COMPRESS_FRACTAL:
              g_warning ("xcf: fractal compression unimplemented");
              g_free (offset_table);
              return FALSE;
            default:
              g_warning ("xcf: unsupported compression algorithm");
              g_free (offset_table);
              return FALSE;
            }

          /* make sure the on-disk tile data didn't end up being too big.
           * xcf_load_level() would refuse to load the file if it did.
           */
          if (info->cp < offset || info->cp - offset > max_data_length)
            {
              g_message ("xcf: invalid tile data length: %" G_GOFFSET_FORMAT,
                         info->cp - offset);
              g_free (offset_table);
              return FALSE;
            }

          /* the next tile's offset is after the tile we just wrote */
          offset = info->cp;
        }
    }

  /* seek back to the offset table and write it  */
  xcf_check_error (xcf_seek_pos (info, saved_pos, error), ;);
  xcf_write_offset_check_error (info, offset_table, ntiles + 1, ;);

  /* seek to the end of the file */
  xcf_check_error (xcf_seek_pos (info, offset, error), ;);

  g_free (offset_table);

  return TRUE;
}

static gboolean
xcf_save_tile (XcfInfo        *info,
               GeglBuffer     *buffer,
               GeglRectangle  *tile_rect,
               const Babl     *format,
               GError        **error)
{
  gint    bpp       = babl_format_get_bytes_per_pixel (format);
  gint    tile_size = bpp * tile_rect->width * tile_rect->height;
  guchar *tile_data = g_alloca (tile_size);
  GError *tmp_error = NULL;

  gegl_buffer_get (buffer, tile_rect, 1.0, format, tile_data,
                   GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);

  if (info->file_version <= 11)
    {
      xcf_write_int8_check_error (info, tile_data, tile_size, ;);
    }
  else
    {
      gint n_components = babl_format_get_n_components (format);

      xcf_write_component_check_error (info, bpp / n_components, tile_data,
                                       tile_size / bpp * n_components, ;);
    }

  return TRUE;
}

static void
xcf_save_free_job_data (XcfJobData *data)
{
  g_free (data->out_data);
  g_free (data->tile_data);
  g_free (data);
}

static gint
xcf_save_sort_job_data (XcfJobData *data1,
                        XcfJobData *data2,
                        gpointer    user_data)
{
  if (data1->tile < data2->tile)
    return -1;
  else if (data1->tile > data2->tile)
    return 1;
  else
    return 0;
}

static void
xcf_save_tile_parallel (XcfJobData  *job_data,
                        GAsyncQueue *queue)
{
  const Babl    *format;
  GeglRectangle  tile_rect;
  gint           bpp;

  format = gegl_buffer_get_format (job_data->buffer);
  bpp    = babl_format_get_bytes_per_pixel (format);

  for (gint i = 0; i < job_data->batch_size; ++i)
    {
      gimp_gegl_buffer_get_tile_rect (job_data->buffer,
                                      XCF_TILE_WIDTH,
                                      XCF_TILE_HEIGHT,
                                      job_data->tile + i,
                                      &tile_rect);
      /* only single thread can create tile data when cache miss */
      gegl_buffer_get (job_data->buffer, &tile_rect, 1.0, format,
                       job_data->tile_data,
                       GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);

      if (job_data->file_version >= 12)
        {
          gint n_components = babl_format_get_n_components (format);
          gint tile_size    = bpp * tile_rect.width * tile_rect.height;

          xcf_write_to_be (bpp / n_components, job_data->tile_data,
                           tile_size / bpp * n_components);
        }

      job_data->compress (&tile_rect, job_data->tile_data, format,
                          job_data->out_data + job_data->max_out_data_len * i,
                          job_data->max_out_data_len,
                          job_data->out_data_len + i);
    }

  g_async_queue_push_sorted (queue, job_data,
                             (GCompareDataFunc) xcf_save_sort_job_data,
                             NULL);
}

static void
xcf_save_tile_rle (GeglRectangle  *tile_rect,
                   guchar         *tile_data,
                   const Babl     *format,
                   guchar         *rlebuf,
                   gint            rlebuf_max_len,
                   gint           *lenptr)
{
  gint bpp = babl_format_get_bytes_per_pixel (format);
  gint len = 0;
  gint i, j;

  for (i = 0; i < bpp; i++)
    {
      const guchar *data   = tile_data + i;
      gint          state  = 0;
      gint          length = 0;
      gint          count  = 0;
      gint          size   = tile_rect->width * tile_rect->height;
      guint         last   = -1;

      while (size > 0)
        {
          switch (state)
            {
            case 0:
              /* in state 0 we try to find a long sequence of
               *  matching values.
               */
              if ((length == 32768) ||
                  ((size - length) <= 0) ||
                  ((length > 1) && (last != *data)))
                {
                  count += length;

                  if (length >= 128)
                    {
                      rlebuf[len++] = 127;
                      rlebuf[len++] = (length >> 8);
                      rlebuf[len++] = length & 0x00FF;
                      rlebuf[len++] = last;
                    }
                  else
                    {
                      rlebuf[len++] = length - 1;
                      rlebuf[len++] = last;
                    }

                  size -= length;
                  length = 0;
                }
              else if ((length == 1) && (last != *data))
                {
                  state = 1;
                }
              break;

            case 1:
              /* in state 1 we try and find a long sequence of
               *  non-matching values.
               */
              if ((length == 32768) ||
                  ((size - length) == 0) ||
                  ((length > 0) && (last == *data) &&
                   ((size - length) == 1 || last == data[bpp])))
                {
                  const guchar *t;

                  /* if we came here because of a new run, backup one */
                  if (!((length == 32768) || ((size - length) == 0)))
                    {
                      length--;
                      data -= bpp;
                    }

                  count += length;
                  state = 0;

                  if (length >= 128)
                    {
                      rlebuf[len++] = 255 - 127;
                      rlebuf[len++] = (length >> 8);
                      rlebuf[len++] = length & 0x00FF;
                    }
                  else
                    {
                      rlebuf[len++] = 255 - (length - 1);
                    }

                  t = data - length * bpp;

                  for (j = 0; j < length; j++)
                    {
                      rlebuf[len++] = *t;
                      t += bpp;
                    }

                  size -= length;
                  length = 0;
                }
              break;
            }

          if (size > 0)
            {
              length += 1;
              last = *data;
              data += bpp;
            }
        }

      if (count != (tile_rect->width * tile_rect->height))
        g_message ("xcf: uh oh! xcf rle tile saving error: %d", count);
    }

  *lenptr = len;
}

static void
xcf_save_tile_zlib (GeglRectangle  *tile_rect,
                    guchar         *tile_data,
                    const Babl     *format,
                    guchar         *zlib_data,
                    gint            zlib_data_max_len,
                    gint           *lenptr)
{
  gint      bpp       = babl_format_get_bytes_per_pixel (format);
  gint      tile_size = bpp * tile_rect->width * tile_rect->height;
  /* The buffer for compressed data. */
  z_stream  strm;
  int       action;
  int       status;

  *lenptr = 0;

  /* allocate deflate state */
  strm.zalloc = Z_NULL;
  strm.zfree  = Z_NULL;
  strm.opaque = Z_NULL;

  status = deflateInit (&strm, Z_DEFAULT_COMPRESSION);
  if (status != Z_OK)
    return;

  strm.next_in   = tile_data;
  strm.avail_in  = tile_size;
  strm.next_out  = zlib_data;
  strm.avail_out = zlib_data_max_len;

  action = Z_NO_FLUSH;

  while (status == Z_OK || status == Z_BUF_ERROR)
    {
      if (strm.avail_in == 0)
        {
          /* Finish the encoding. */
          action = Z_FINISH;
        }

      status = deflate (&strm, action);

      if (status == Z_STREAM_END || status == Z_BUF_ERROR)
        {
          size_t write_size = zlib_data_max_len - strm.avail_out;

          *lenptr = write_size;

          /* Reset next_out and avail_out. */
          strm.next_out  = zlib_data;
          strm.avail_out = zlib_data_max_len;
        }
      else if (status != Z_OK)
        {
          g_printerr ("xcf: tile compression failed: %s", zError (status));
          deflateEnd (&strm);
          return;
        }
    }

  deflateEnd (&strm);
}

static gboolean
xcf_save_parasite (XcfInfo       *info,
                   GimpParasite  *parasite,
                   GError       **error)
{
  if (gimp_parasite_is_persistent (parasite))
    {
      guint32        value;
      const gchar   *string;
      GError        *tmp_error = NULL;
      gconstpointer  parasite_data;

      string = gimp_parasite_get_name (parasite);
      xcf_write_string_check_error (info, (gchar **) &string, 1, ;);

      value = gimp_parasite_get_flags (parasite);
      xcf_write_int32_check_error (info, &value, 1, ;);

      parasite_data = gimp_parasite_get_data (parasite, &value);
      xcf_write_int32_check_error (info, &value, 1, ;);

      xcf_write_int8_check_error (info, parasite_data, value, ;);
    }

  return TRUE;
}

typedef struct
{
  XcfInfo *info;
  GError  *error;
} XcfParasiteData;

static void
xcf_save_parasite_func (gchar           *key,
                        GimpParasite    *parasite,
                        XcfParasiteData *data)
{
  if (! data->error)
    xcf_save_parasite (data->info, parasite, &data->error);
}

static gboolean
xcf_save_parasite_list (XcfInfo           *info,
                        GimpParasiteList  *list,
                        GError           **error)
{
  XcfParasiteData data;

  data.info  = info;
  data.error = NULL;

  gimp_parasite_list_foreach (list, (GHFunc) xcf_save_parasite_func, &data);

  if (data.error)
    {
      g_propagate_error (error, data.error);
      return FALSE;
    }

  return TRUE;
}

/* This is the oldest way to save paths. */
static gboolean
xcf_save_old_paths (XcfInfo    *info,
                    GimpImage  *image,
                    GError    **error)
{
  GimpPath *active_path = NULL;
  guint32   num_paths;
  guint32   active_index = 0;
  GList    *list;
  GError   *tmp_error = NULL;

  /* Write out the following:-
   *
   * last_selected_row (gint)
   * number_of_paths (gint)
   *
   * then each path:-
   */

  num_paths = gimp_container_get_n_children (gimp_image_get_paths (image));

  if (gimp_image_get_selected_paths (image))
    {
      active_path = gimp_image_get_selected_paths (image)->data;
      /* Having more than 1 selected paths should not have happened in this
       * code path but let's not break saving, only produce a critical.
       */
      if (g_list_length (gimp_image_get_selected_paths (image)) > 1)
        g_critical ("%s: this code path should not happen with multiple paths selected",
                    G_STRFUNC);
    }

  if (active_path)
    active_index = gimp_container_get_child_index (gimp_image_get_paths (image),
                                                   GIMP_OBJECT (active_path));

  xcf_write_int32_check_error (info, &active_index, 1, ;);
  xcf_write_int32_check_error (info, &num_paths,    1, ;);

  for (list = gimp_image_get_path_iter (image);
       list;
       list = g_list_next (list))
    {
      GimpPath               *path = list->data;
      gchar                  *name;
      guint32                 locked;
      guint8                  state;
      guint32                 version;
      guint32                 pathtype;
      guint32                 tattoo;
      GimpPathCompatPoint    *points;
      guint32                 num_points;
      guint32                 closed;
      gint                    i;

      /*
       * name (string)
       * locked (gint)
       * state (gchar)
       * closed (gint)
       * number points (gint)
       * version (gint)
       * pathtype (gint)
       * tattoo (gint)
       * then each point.
       */

      points = gimp_path_compat_get_points (path,
                                            (gint32 *) &num_points,
                                            (gint32 *) &closed);

      /* if no points are generated because of a faulty path we should
       * skip saving the path - this is unfortunately impossible, because
       * we already saved the number of paths and I won't start seeking
       * around to fix that cruft  */

      name     = (gchar *) gimp_object_get_name (path);
      /* The 'linked' concept does not exist anymore in GIMP 3.0 and over. */
      locked   = 0;
      state    = closed ? 4 : 2;  /* EDIT : ADD  (editing state, 1.2 compat) */
      version  = 3;
      pathtype = 1;  /* BEZIER  (1.2 compat) */
      tattoo   = gimp_item_get_tattoo (GIMP_ITEM (path));

      xcf_write_string_check_error (info, &name,       1, ;);
      xcf_write_int32_check_error  (info, &locked,     1, ;);
      xcf_write_int8_check_error   (info, &state,      1, ;);
      xcf_write_int32_check_error  (info, &closed,     1, ;);
      xcf_write_int32_check_error  (info, &num_points, 1, ;);
      xcf_write_int32_check_error  (info, &version,    1, ;);
      xcf_write_int32_check_error  (info, &pathtype,   1, ;);
      xcf_write_int32_check_error  (info, &tattoo,     1, ;);

      for (i = 0; i < num_points; i++)
        {
          gfloat x;
          gfloat y;

          x = points[i].x;
          y = points[i].y;

          /*
           * type (gint)
           * x (gfloat)
           * y (gfloat)
           */

          xcf_write_int32_check_error (info, &points[i].type, 1, ;);
          xcf_write_float_check_error (info, &x,              1, ;);
          xcf_write_float_check_error (info, &y,              1, ;);
        }

      g_free (points);
    }

  return TRUE;
}

/* This is an older way to save paths, though more recent than
 * xcf_save_old_paths(). It used to be the normal path storing format until all
 * 2.10 versions. It changed with GIMP 3.0.
 */
static gboolean
xcf_save_old_vectors (XcfInfo    *info,
                      GimpImage  *image,
                      GError    **error)
{
  GimpPath *active_path = NULL;
  guint32   version        = 1;
  guint32   active_index   = 0;
  guint32   num_paths;
  GList    *list;
  GList    *stroke_list;
  GError   *tmp_error = NULL;

  /* Write out the following:-
   *
   * version (gint)
   * active_index (gint)
   * num_paths (gint)
   *
   * then each path:-
   */

  if (gimp_image_get_selected_paths (image))
    {
      active_path = gimp_image_get_selected_paths (image)->data;
      /* Having more than 1 selected paths should not have happened in this
       * code path but let's not break saving, only produce a critical.
       */
      if (g_list_length (gimp_image_get_selected_paths (image)) > 1)
        g_critical ("%s: this code path should not happen with multiple paths selected",
                    G_STRFUNC);
    }

  if (active_path)
    active_index = gimp_container_get_child_index (gimp_image_get_paths (image),
                                                   GIMP_OBJECT (active_path));

  num_paths = gimp_container_get_n_children (gimp_image_get_paths (image));

  xcf_write_int32_check_error (info, &version,      1, ;);
  xcf_write_int32_check_error (info, &active_index, 1, ;);
  xcf_write_int32_check_error (info, &num_paths,    1, ;);

  for (list = gimp_image_get_path_iter (image);
       list;
       list = g_list_next (list))
    {
      GimpPath         *path = list->data;
      GimpParasiteList *parasites;
      const gchar      *name;
      guint32           tattoo;
      guint32           visible;
      guint32           linked;
      guint32           num_parasites;
      guint32           num_strokes;

      /*
       * name (string)
       * tattoo (gint)
       * visible (gint)
       * linked (gint)
       * num_parasites (gint)
       * num_strokes (gint)
       *
       * then each parasite
       * then each stroke
       */

      name          = gimp_object_get_name (path);
      visible       = gimp_item_get_visible (GIMP_ITEM (path));
      /* The 'linked' concept does not exist anymore in GIMP 3.0 and over. */
      linked        = 0;
      tattoo        = gimp_item_get_tattoo (GIMP_ITEM (path));
      parasites     = gimp_item_get_parasites (GIMP_ITEM (path));
      num_parasites = gimp_parasite_list_persistent_length (parasites);
      num_strokes   = g_queue_get_length (path->strokes);

      xcf_write_string_check_error (info, (gchar **) &name, 1, ;);
      xcf_write_int32_check_error  (info, &tattoo,          1, ;);
      xcf_write_int32_check_error  (info, &visible,         1, ;);
      xcf_write_int32_check_error  (info, &linked,          1, ;);
      xcf_write_int32_check_error  (info, &num_parasites,   1, ;);
      xcf_write_int32_check_error  (info, &num_strokes,     1, ;);

      xcf_check_error (xcf_save_parasite_list (info, parasites, error), ;);

      for (stroke_list = g_list_first (path->strokes->head);
           stroke_list;
           stroke_list = g_list_next (stroke_list))
        {
          GimpStroke *stroke = stroke_list->data;
          guint32     stroke_type;
          guint32     closed;
          guint32     num_axes;
          GArray     *control_points;
          gint        i;

          guint32     type;
          gfloat      coords[6];

          /*
           * stroke_type (gint)
           * closed (gint)
           * num_axes (gint)
           * num_control_points (gint)
           *
           * then each control point.
           */

          if (GIMP_IS_BEZIER_STROKE (stroke))
            {
              stroke_type = XCF_STROKETYPE_BEZIER_STROKE;
              num_axes = 2;   /* hardcoded, might be increased later */
            }
          else
            {
              g_printerr ("Skipping unknown stroke type!\n");
              continue;
            }

          control_points = gimp_stroke_control_points_get (stroke,
                                                           (gint32 *) &closed);

          xcf_write_int32_check_error (info, &stroke_type,         1, ;);
          xcf_write_int32_check_error (info, &closed,              1, ;);
          xcf_write_int32_check_error (info, &num_axes,            1, ;);
          xcf_write_int32_check_error (info, &control_points->len, 1, ;);

          for (i = 0; i < control_points->len; i++)
            {
              GimpAnchor *anchor;

              anchor = & (g_array_index (control_points, GimpAnchor, i));

              type      = anchor->type;
              coords[0] = anchor->position.x;
              coords[1] = anchor->position.y;
              coords[2] = anchor->position.pressure;
              coords[3] = anchor->position.xtilt;
              coords[4] = anchor->position.ytilt;
              coords[5] = anchor->position.wheel;

              /*
               * type (gint)
               *
               * the first num_axis elements of:
               * [0] x (gfloat)
               * [1] y (gfloat)
               * [2] pressure (gfloat)
               * [3] xtilt (gfloat)
               * [4] ytilt (gfloat)
               * [5] wheel (gfloat)
               */

              xcf_write_int32_check_error (info, &type,  1, ;);
              xcf_write_float_check_error (info, coords, num_axes, ;);
            }

          g_array_free (control_points, TRUE);
        }
    }

  return TRUE;
}

static gboolean
xcf_save_path (XcfInfo      *info,
               GimpImage    *image,
               GimpPath     *path,
               GError      **error)
{
  const gchar *string;
  GList       *stroke_list;
  GError      *tmp_error = NULL;
  /* Version of the path format is always 1 for now. */
  guint32      version   = 1;
  guint32      num_strokes;
  guint32      size;
  goffset      base;
  goffset      pos;

  /* write out the path name */
  string = gimp_object_get_name (path);
  xcf_write_string_check_error (info, (gchar **) &string, 1, ;);

  /* Payload size */
  size = 0;
  pos = info->cp;
  xcf_write_int32_check_error (info, &size, 1, ;);
  base = info->cp;

  /* write out the path properties */
  xcf_save_path_props (info, image, path, error);

  /* Path version */
  xcf_write_int32_check_error (info, &version, 1, ;);

  /* Write out the number of strokes. */
  num_strokes = g_queue_get_length (path->strokes);
  xcf_write_int32_check_error  (info, &num_strokes, 1, ;);

  for (stroke_list = g_list_first (path->strokes->head);
       stroke_list;
       stroke_list = g_list_next (stroke_list))
    {
      GimpStroke *stroke = stroke_list->data;
      guint32     stroke_type;
      guint32     closed;
      guint32     num_axes;
      GArray     *control_points;
      gint        i;

      guint32     type;
      gfloat      coords[6];

      /*
       * stroke_type (gint)
       * closed (gint)
       * num_axes (gint)
       * num_control_points (gint)
       *
       * then each control point.
       */

      if (GIMP_IS_BEZIER_STROKE (stroke))
        {
          stroke_type = XCF_STROKETYPE_BEZIER_STROKE;
          num_axes = 2;   /* hardcoded, might be increased later */
        }
      else
        {
          g_printerr ("Skipping unknown stroke type!\n");
          continue;
        }

      control_points = gimp_stroke_control_points_get (stroke,
                                                       (gint32 *) &closed);

      /* Stroke type. */
      xcf_write_int32_check_error (info, &stroke_type,         1, ;);
      /* close path or not? */
      xcf_write_int32_check_error (info, &closed,              1, ;);
      /* Number of floats given for each point. */
      xcf_write_int32_check_error (info, &num_axes,            1, ;);
      /* Number of control points. */
      xcf_write_int32_check_error (info, &control_points->len, 1, ;);

      for (i = 0; i < control_points->len; i++)
        {
          GimpAnchor *anchor;

          anchor = & (g_array_index (control_points, GimpAnchor, i));

          type      = anchor->type;
          coords[0] = anchor->position.x;
          coords[1] = anchor->position.y;
          coords[2] = anchor->position.pressure;
          coords[3] = anchor->position.xtilt;
          coords[4] = anchor->position.ytilt;
          coords[5] = anchor->position.wheel;

          /*
           * type (gint)
           *
           * the first num_axis elements of:
           * [0] x (gfloat)
           * [1] y (gfloat)
           * [2] pressure (gfloat)
           * [3] xtilt (gfloat)
           * [4] ytilt (gfloat)
           * [5] wheel (gfloat)
           */

          xcf_write_int32_check_error (info, &type,  1, ;);
          xcf_write_float_check_error (info, coords, num_axes, ;);
        }

      g_array_free (control_points, TRUE);
    }

  /* go back to the saved position and write the length */
  size = info->cp - base;
  xcf_check_error (xcf_seek_pos (info, pos, error), ;);
  xcf_write_int32_check_error (info, &size, 1, ;);

  xcf_check_error (xcf_seek_pos (info, base + size, error), ;);

  return TRUE;
}
