/* 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 "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/gimpdrawable.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/gimplayer.h"
#include "core/gimplayermask.h"
#include "core/gimpparasitelist.h"
#include "core/gimpprogress.h"
#include "core/gimpsamplepoint.h"
#include "core/gimpsymmetry.h"

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

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

#include "vectors/gimpanchor.h"
#include "vectors/gimpstroke.h"
#include "vectors/gimpbezierstroke.h"
#include "vectors/gimpvectors.h"
#include "vectors/gimpvectors-compat.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"


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_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_buffer        (XcfInfo           *info,
                                        GeglBuffer        *buffer,
                                        GError           **error);
static gboolean xcf_save_level         (XcfInfo           *info,
                                        GeglBuffer        *buffer,
                                        GError           **error);
static gboolean xcf_save_tile          (XcfInfo           *info,
                                        GeglBuffer        *buffer,
                                        GeglRectangle     *tile_rect,
                                        const Babl        *format,
                                        GError           **error);
static gboolean xcf_save_tile_rle      (XcfInfo           *info,
                                        GeglBuffer        *buffer,
                                        GeglRectangle     *tile_rect,
                                        const Babl        *format,
                                        guchar            *rlebuf,
                                        GError           **error);
static gboolean xcf_save_tile_zlib     (XcfInfo           *info,
                                        GeglBuffer        *buffer,
                                        GeglRectangle     *tile_rect,
                                        const Babl        *format,
                                        GError           **error);
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_vectors       (XcfInfo           *info,
                                        GimpImage         *image,
                                        GError           **error);


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

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

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

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

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

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

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

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

#define xcf_check_error(x) G_STMT_START { \
  if (! (x))                              \
    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   *list;
  goffset  saved_pos;
  goffset  offset;
  guint32  value;
  guint    n_layers;
  guint    n_channels;
  guint    progress = 0;
  guint    max_progress;
  gchar    version_tag[16];
  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);
    }

  /* 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);

  max_progress = 1 + n_layers + n_channels;

  /* 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);

  /* '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);
    }

  /* 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);

  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 (image))
    xcf_check_error (xcf_save_prop (info, image, PROP_COLORMAP, error,
                                    gimp_image_get_colormap_size (image),
                                    gimp_image_get_colormap (image)));

  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 (unit < gimp_unit_get_number_of_built_in_units ())
    xcf_check_error (xcf_save_prop (info, image, PROP_UNIT, error, unit));

  if (gimp_container_get_n_children (gimp_image_get_vectors (image)) > 0)
    {
      if (gimp_vectors_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 (unit >= gimp_unit_get_number_of_built_in_units ())
    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);

      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_name (grid_parasite));
      gimp_parasite_free (grid_parasite);
    }

  if (meta_parasite)
    {
      gimp_parasite_list_remove (private->parasites,
                                 gimp_parasite_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_name (parasite));
    }
  g_list_free_full (symmetry_parasites,
                    (GDestroyNotify) gimp_parasite_free);

  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;
  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 (layer == gimp_image_get_active_layer (image))
    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_LINKED, error,
                                  gimp_item_get_linked (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))));

  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));
    }

  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));
    }

  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;

  if (channel == gimp_image_get_active_channel (image))
    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_LINKED, error,
                                  gimp_item_get_linked (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_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));
    }

  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);
      xcf_write_int32_check_error (info, &size, 1);
      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);
        xcf_write_int32_check_error (info, &size, 1);

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

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

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

    case PROP_FLOATING_SELECTION:
      size = info->bytes_per_offset;

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

      info->floating_sel_offset = info->cp;
      xcf_write_zero_offset_check_error (info, 1);
      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);
        xcf_write_int32_check_error (info, &size, 1);

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

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

        size = 4;

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

        xcf_write_float_check_error (info, &opacity, 1);
      }
      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);
        xcf_write_int32_check_error (info, &size, 1);

        xcf_write_int32_check_error (info, (guint32 *) &mode, 1);
      }
      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);
        xcf_write_int32_check_error (info, &size, 1);

        xcf_write_int32_check_error (info, (guint32 *) &blend_space, 1);
      }
      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);
        xcf_write_int32_check_error (info, &size, 1);

        xcf_write_int32_check_error (info, (guint32 *) &composite_space, 1);
      }
      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);
        xcf_write_int32_check_error (info, &size, 1);

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

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

        size = 4;

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

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

    case PROP_LINKED:
      {
        guint32 linked = va_arg (args, guint32);

        size = 4;

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

        xcf_write_int32_check_error (info, &linked, 1);
      }
      break;

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

        size = 4;

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

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

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

        size = 4;

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

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

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

        size = 4;

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

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

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

        size = 4;

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

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

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

        size = 4;

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

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

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

        size = 4;

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

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

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

        size = 4;

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

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

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

        size = 4;

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

        xcf_write_int32_check_error (info, &show_masked, 1);
      }
      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);
        xcf_write_int32_check_error (info, &size, 1);

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

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

        gimp_rgb_get_uchar (color, &col[0], &col[1], &col[2]);

        size = 3;

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

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

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

        col[0] = color->r;
        col[1] = color->g;
        col[2] = color->b;

        size = 3 * 4;

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

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

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

        size = 1;

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

        xcf_write_int8_check_error (info, &compression, 1);
      }
      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);
            xcf_write_int32_check_error (info, &size, 1);

            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);
                xcf_write_int8_check_error  (info, (guint8 *)  &orientation, 1);
              }
          }
      }
      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);
        xcf_write_int32_check_error (info, &size, 1);

        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);
            xcf_write_int32_check_error (info, (guint32 *) &y,         1);
            xcf_write_int32_check_error (info, (guint32 *) &pick_mode, 1);
            xcf_write_int32_check_error (info, (guint32 *) padding,    2);
          }
      }
      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);
        xcf_write_int32_check_error (info, &size, 1);

        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);
            xcf_write_int32_check_error (info, (guint32 *) &y, 1);
          }
      }
      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);
        xcf_write_int32_check_error (info, &size, 1);

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

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

        size = 4;

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

        xcf_write_int32_check_error (info, &tattoo, 1);
      }
      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);

            /* 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);

            base = info->cp;

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

            size = info->cp - base;

            /* go back to the saved position and write the length */
            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));
          }
      }
      break;

    case PROP_UNIT:
      {
        guint32 unit = va_arg (args, guint32);

        size = 4;

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

        xcf_write_int32_check_error (info, &unit, 1);
      }
      break;

    case PROP_PATHS:
      {
        goffset base;
        goffset pos;

        size = 0;

        xcf_write_prop_type_check_error (info, prop_type);

        /* 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);

        base = info->cp;

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

        size = info->cp - base;

        /* go back to the saved position and write the length */
        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));
      }
      break;

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

        /* write the entire unit definition */
        unit_strings[0] = gimp_unit_get_identifier (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);
        unit_strings[3] = gimp_unit_get_singular (unit);
        unit_strings[4] = gimp_unit_get_plural (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 +
          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);
        xcf_write_int32_check_error (info, &size, 1);

        xcf_write_float_check_error  (info, &factor,                 1);
        xcf_write_int32_check_error  (info, &digits,                 1);
        xcf_write_string_check_error (info, (gchar **) unit_strings, 5);
      }
      break;

    case PROP_VECTORS:
      {
        goffset base;
        goffset pos;

        size = 0;

        xcf_write_prop_type_check_error (info, prop_type);

        /* 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);

        base = info->cp;

        xcf_check_error (xcf_save_vectors (info, image, error));

        size = info->cp - base;

        /* go back to the saved position and write the length */
        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));
      }
      break;

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

        size = 4;

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

        xcf_write_int32_check_error (info, &flags, 1);
      }
      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);
        xcf_write_int32_check_error (info, &size, 1);

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

            xcf_write_int32_check_error (info, &index, 1);

            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);
        xcf_write_int32_check_error (info, &size, 1);
        xcf_write_int32_check_error (info, &flags, 1);
      }
      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;
  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));
    }

  /* 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 */
  offset = info->cp + 2 * 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);

  xcf_check_error (xcf_save_buffer (info,
                                    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));
    }

  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,
                                    gimp_drawable_get_buffer (GIMP_DRAWABLE (channel)),
                                    error));

  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,
                 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, 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,
                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        i;
  guchar     *rlebuf    = NULL;
  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);

  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 */;

  /* allocate a temporary buffer to store the rle data before it is
   * written to disk
   */
  if (info->compression == COMPRESS_RLE)
    rlebuf = g_alloca (max_data_length);

  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;

  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_RLE:
          xcf_check_error (xcf_save_tile_rle (info, buffer, &rect, format,
                                              rlebuf, error));
          break;
        case COMPRESS_ZLIB:
          xcf_check_error (xcf_save_tile_zlib (info, buffer, &rect, format,
                                               error));
          break;
        case COMPRESS_FRACTAL:
          g_warning ("xcf: fractal compression unimplemented");
          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 gboolean
xcf_save_tile_rle (XcfInfo        *info,
                   GeglBuffer     *buffer,
                   GeglRectangle  *tile_rect,
                   const Babl     *format,
                   guchar         *rlebuf,
                   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);
  gint    len       = 0;
  gint    i, j;
  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 >= 12)
    {
      gint n_components = babl_format_get_n_components (format);

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

  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);
    }

  xcf_write_int8_check_error (info, rlebuf, len);

  return TRUE;
}

static gboolean
xcf_save_tile_zlib (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);
  /* The buffer for compressed data. */
  guchar   *buf       = g_alloca (tile_size);
  GError   *tmp_error = NULL;
  z_stream  strm;
  int       action;
  int       status;

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

  if (info->file_version >= 12)
    {
      gint n_components = babl_format_get_n_components (format);

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

  /* 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 FALSE;

  strm.next_in   = tile_data;
  strm.avail_in  = tile_size;
  strm.next_out  = buf;
  strm.avail_out = tile_size;

  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 = tile_size - strm.avail_out;

          xcf_write_int8_check_error (info, buf, write_size);

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

  deflateEnd (&strm);
  return TRUE;
}

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;

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

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

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

      xcf_write_int8_check_error (info,
                                  gimp_parasite_data (parasite),
                                  gimp_parasite_data_size (parasite));
    }

  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;
}

static gboolean
xcf_save_old_paths (XcfInfo    *info,
                    GimpImage  *image,
                    GError    **error)
{
  GimpVectors *active_vectors;
  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_vectors (image));

  active_vectors = gimp_image_get_active_vectors (image);

  if (active_vectors)
    active_index = gimp_container_get_child_index (gimp_image_get_vectors (image),
                                                   GIMP_OBJECT (active_vectors));

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

  for (list = gimp_image_get_vectors_iter (image);
       list;
       list = g_list_next (list))
    {
      GimpVectors            *vectors = list->data;
      gchar                  *name;
      guint32                 locked;
      guint8                  state;
      guint32                 version;
      guint32                 pathtype;
      guint32                 tattoo;
      GimpVectorsCompatPoint *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_vectors_compat_get_points (vectors,
                                               (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 (vectors);
      locked   = gimp_item_get_linked (GIMP_ITEM (vectors));
      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 (vectors));

      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;
}

static gboolean
xcf_save_vectors (XcfInfo    *info,
                  GimpImage  *image,
                  GError    **error)
{
  GimpVectors *active_vectors;
  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:-
   */

  active_vectors = gimp_image_get_active_vectors (image);

  if (active_vectors)
    active_index = gimp_container_get_child_index (gimp_image_get_vectors (image),
                                                   GIMP_OBJECT (active_vectors));

  num_paths = gimp_container_get_n_children (gimp_image_get_vectors (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_vectors_iter (image);
       list;
       list = g_list_next (list))
    {
      GimpVectors      *vectors = 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 (vectors);
      visible       = gimp_item_get_visible (GIMP_ITEM (vectors));
      linked        = gimp_item_get_linked (GIMP_ITEM (vectors));
      tattoo        = gimp_item_get_tattoo (GIMP_ITEM (vectors));
      parasites     = gimp_item_get_parasites (GIMP_ITEM (vectors));
      num_parasites = gimp_parasite_list_persistent_length (parasites);
      num_strokes   = g_queue_get_length (vectors->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 (vectors->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;
}
