Ctrl K
ComunidadGuías…

src/libs/export.c

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*
    This file is part of darktable,
    Copyright (C) 2009-2026 darktable developers.

    darktable 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.

    darktable 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 darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "common/gdk_event_utils.h"

#include "bauhaus/bauhaus.h"
#include "common/collection.h"
#include "common/colorspaces.h"
#include "common/darktable.h"
#include "common/debug.h"
#include "common/presets.h"
#include "common/styles.h"
#include "control/conf.h"
#include "control/control.h"
#include "control/signal.h"
#include "develop/develop.h"
#include "dtgtk/button.h"
#include "dtgtk/stylemenu.h"
#include "gui/accelerators.h"
#include "gui/gtk.h"
#include "imageio/imageio_module.h"
#include "libs/lib.h"
#include "libs/lib_api.h"
#include "views/view.h"

#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include <stdlib.h>
#include <ctype.h>

#include <glib.h>

DT_MODULE(9)

typedef enum dt_export_batch_cols_t
{
  DT_EXPORT_BATCH_COL_ACTIVE = 0,   // active preset for batch export
  DT_EXPORT_BATCH_COL_NAME,         // preset name
  DT_EXPORT_BATCH_NUM_COLS
} dt_export_batch_cols_t;


#define EXPORT_MAX_IMAGE_SIZE UINT16_MAX
#define CONFIG_PREFIX "plugins/lighttable/export/"

typedef struct dt_lib_export_t
{
  GtkWidget *dimensions_type, *print_dpi, *print_height, *print_width;
  GtkWidget *unit_label;
  GtkWidget *width, *height;
  GtkWidget *px_size, *print_size, *scale, *size_in_px;
  GtkWidget *storage, *format;
  int format_lut[128];
  uint32_t max_allowed_width , max_allowed_height;
  GtkWidget *upscale, *profile, *pq_peak, *intent, *style, *style_mode;
  dt_gui_collapsible_section_t cs;
  GtkWidget *batch_treeview;
  GtkButton *export_button, *batch_export_button;
  GtkWidget *storage_extra_container, *format_extra_container;
  GtkWidget *high_quality;
  GtkWidget *export_masks;
  char *metadata_export;
  char *style_name;
  dt_imageio_module_storage_t *storage_module;
} dt_lib_export_t;


typedef enum dt_dimensions_type_t
{
  DT_DIMENSIONS_PIXELS = 0, // set dimensions exactly in pixels
  DT_DIMENSIONS_CM     = 1, // set dimensions from physical size in centimeters * DPI
  DT_DIMENSIONS_INCH   = 2,  // set dimensions from physical size in inch
  DT_DIMENSIONS_SCALE  = 3  // set dimensions by scale
} dt_dimensions_type_t;

char *dt_lib_export_metadata_configuration_dialog(char *list,
                                                  const gboolean ondisk);
/** Updates the combo box and shows only the supported formats of
 * current selected storage module */
static void _update_formats_combobox(dt_lib_export_t *d);
/** Sets the max dimensions based upon what storage and format supports */
static void _update_dimensions(dt_lib_export_t *d);
/** Update the combobox label for the style */
static void _update_style_label(dt_lib_export_t *d, const char *name);
/** get the max output dimension supported by combination of storage and format.. */
static void _get_max_output_dimension(dt_lib_export_t *d,
                                      uint32_t *width,
                                      uint32_t *height);
static void _resync_print_dimensions(dt_lib_export_t *self);
static void _resync_pixel_dimensions(dt_lib_export_t *self);

static inline gboolean _is_style_set(const char *name)
{
  return name && name[0] && strcmp(name, _("none")) && dt_styles_exists(name);
}

#define INCH_TO_CM (2.54f)

static inline float pixels2cm(dt_lib_export_t *self, const uint32_t pix)
{
  const int dpi = atoi(gtk_entry_get_text(GTK_ENTRY(self->print_dpi)));
  return ((float)pix * INCH_TO_CM) / (float)dpi;
}

static inline float pixels2inch(dt_lib_export_t *self, const uint32_t pix)
{
  const int dpi = atoi(gtk_entry_get_text(GTK_ENTRY(self->print_dpi)));
  return (float)pix / (float)dpi;
}

static inline uint32_t cm2pixels(dt_lib_export_t *self, const float cm)
{
  const int dpi = atoi(gtk_entry_get_text(GTK_ENTRY(self->print_dpi)));
  return ceilf((cm * (float)dpi) / INCH_TO_CM);
}

static inline uint32_t inch2pixels(dt_lib_export_t *self, const float inch)
{
  const int dpi = atoi(gtk_entry_get_text(GTK_ENTRY(self->print_dpi)));
  return ceilf(inch * (float)dpi);
}

static inline uint32_t print2pixels(dt_lib_export_t *self, const float value)
{
  const dt_dimensions_type_t d_type =
    (dt_dimensions_type_t)dt_bauhaus_combobox_get(self->dimensions_type);
  switch(d_type)
  {
    case(DT_DIMENSIONS_PIXELS):
      return ceilf(value);
    case(DT_DIMENSIONS_CM):
      return cm2pixels(self, value);
    case(DT_DIMENSIONS_INCH):
      return inch2pixels(self, value);
    case(DT_DIMENSIONS_SCALE):
      ;
  }

  // should never run this
  return ceilf(value);
}

static inline float pixels2print(dt_lib_export_t *self,
                                 const uint32_t pix)
{
  const dt_dimensions_type_t d_type =
    (dt_dimensions_type_t)dt_bauhaus_combobox_get(self->dimensions_type);
  switch(d_type)
  {
    case(DT_DIMENSIONS_PIXELS):
      return (float)pix;
    case(DT_DIMENSIONS_CM):
      return pixels2cm(self, pix);
    case(DT_DIMENSIONS_INCH):
      return pixels2inch(self, pix);
    case(DT_DIMENSIONS_SCALE):
      ;
  }

  // should never run this
  return (float)pix;
}

const char *name(dt_lib_module_t *self)
{
  return _("export");
}

gboolean has_preset_label(dt_lib_module_t *self)
{
  return TRUE;
}

const char *description(dt_lib_module_t *self)
{
  return _("create new files for the\n"
           "currently selected images\n"
           "which apply your edits");
}

dt_view_type_flags_t views(dt_lib_module_t *self)
{
  if(dt_conf_get_bool("plugins/darkroom/export/visible"))
    return DT_VIEW_LIGHTTABLE | DT_VIEW_DARKROOM;
  else
    return DT_VIEW_LIGHTTABLE;
}

uint32_t container(dt_lib_module_t *self)
{
  return DT_UI_CONTAINER_PANEL_RIGHT_CENTER;
}

void gui_update(dt_lib_module_t *self)
{
  const dt_lib_export_t *d = self->data;

  const gboolean has_act_on = (dt_act_on_get_images_nb(TRUE, FALSE) > 0);

  const char *format_name = dt_conf_get_string_const(CONFIG_PREFIX "format_name");
  const char *storage_name = dt_conf_get_string_const(CONFIG_PREFIX "storage_name");
  const int format_index =
    dt_imageio_get_index_of_format(dt_imageio_get_format_by_name(format_name));
  const int storage_index =
    dt_imageio_get_index_of_storage(dt_imageio_get_storage_by_name(storage_name));

  dt_imageio_module_storage_t *storage = dt_imageio_get_storage();

  gboolean export_enabled = TRUE;
  if(storage->export_enabled)
    export_enabled = storage->export_enabled(storage);

  gtk_widget_set_sensitive(GTK_WIDGET(d->export_button),
                           has_act_on
                           && format_index != -1
                           && storage_index != -1
                           && export_enabled);
}

static void _image_selection_changed_callback(gpointer instance,
                                              dt_lib_module_t *self)
{
  dt_lib_gui_queue_update(self);
}

static void _collection_updated_callback(gpointer instance,
                                         const dt_collection_change_t query_change,
                                         const dt_collection_properties_t changed_property,
                                         gpointer imgs,
                                         const int next,
                                         dt_lib_module_t *self)
{
  dt_lib_gui_queue_update(self);
}

static void _mouse_over_image_callback(gpointer instance, dt_lib_module_t *self)
{
  dt_lib_gui_queue_update(self);
}

static void _export_enable_callback(gpointer instance, dt_lib_module_t *self)
{
  dt_lib_gui_queue_update(self);
}

gboolean _is_int(double value)
{
  return (value == (int)value);
}

static void _scale_optim()
{
  double num = 1.0, denum = 1.0;
  dt_imageio_resizing_factor_get_and_parsing(&num, &denum);
  gchar *scale_str = dt_conf_get_string(CONFIG_PREFIX "resizing_factor");
  gchar _str[6] = "";

  gchar *pdiv = strchr(scale_str, '/');

  gchar scale_buf[64] = "";
  if(pdiv == NULL)
  {
    if(_is_int(num) && num > 0.0)
    {
      sprintf(_str, "%d", (int) num);
      g_strlcat(scale_buf, _str, sizeof(scale_buf));
    }
    else
    {
      g_strlcat(scale_buf, scale_str, sizeof(scale_buf));
    }
  }
  else if(pdiv-scale_str == 0)
  {
    if(_is_int(denum) && denum > 0.0)
    {
      sprintf(_str, "%d", (int) denum);
      g_strlcat(scale_buf, _str, sizeof(scale_buf));
    }
    else
    {
      g_strlcat(scale_buf, "1/", sizeof(scale_buf));
      g_strlcat(scale_buf, pdiv+1, sizeof(scale_buf));
    }
  }
  else
  {
    if(_is_int(num) && num > 0.0)
    {
      sprintf(_str, "%d", (int) num);
      g_strlcat(scale_buf, _str, sizeof(scale_buf));
    }
    else
    {
      g_strlcat(scale_buf, scale_str, sizeof(scale_buf));
    }
    g_strlcat(scale_buf, "/", sizeof(scale_buf));
    if(_is_int(denum) && denum > 0.0)
    {
      sprintf(_str, "%d", (int) denum);
      g_strlcat(scale_buf, _str, sizeof(scale_buf));
    }
    else
    {
      g_strlcat(scale_buf, pdiv+1, sizeof(scale_buf));
    }
  }
  dt_conf_set_string(CONFIG_PREFIX "resizing_factor", scale_buf);

  free(scale_str);
}

static void _export_with_current_settings(dt_lib_module_t *self)
{
  dt_lib_export_t *d = self->data;

  /* write current history changes so nothing gets lost, do that only
     in the darkroom as there is nothing to be saved when in the
     lighttable (and it would write over current history stack) */
  if(dt_view_get_current() == DT_VIEW_DARKROOM) dt_dev_write_history(darktable.develop);

  char style[128] = { 0 };

  // get the format_name and storage_name settings which are plug-ins
  // name and not necessary what is displayed on the combobox.  note
  // that we cannot take directly the combobox entry index as
  // depending on the storage some format are not listed.
  const char *format_name = dt_conf_get_string_const(CONFIG_PREFIX "format_name");
  const char *storage_name = dt_conf_get_string_const(CONFIG_PREFIX "storage_name");
  const int format_index =
    dt_imageio_get_index_of_format(dt_imageio_get_format_by_name(format_name));
  const int storage_index =
    dt_imageio_get_index_of_storage(dt_imageio_get_storage_by_name(storage_name));

  if(format_index == -1)
  {
    dt_control_log(_("invalid format for export selected"));
    return;
  }
  if(storage_index == -1)
  {
    dt_control_log(_("invalid storage for export selected"));
    return;
  }

  char *confirm_message = NULL;
  dt_imageio_module_storage_t *mstorage = dt_imageio_get_storage();
  if(mstorage->ask_user_confirmation)
    confirm_message = mstorage->ask_user_confirmation(mstorage);
  if(confirm_message)
  {

    gboolean res = dt_gui_show_yes_no_dialog
      (_("export to disk"),  "", "%s", confirm_message);
    g_free(confirm_message);
    confirm_message = NULL;

    if(!res)
    {
      return;
    }
  }

  // Let's get the max dimension restriction if any...
  uint32_t max_width = dt_conf_get_int(CONFIG_PREFIX "width");
  uint32_t max_height = dt_conf_get_int(CONFIG_PREFIX "height");

  const gboolean upscale = dt_conf_get_bool(CONFIG_PREFIX "upscale");
  const gboolean scaledimension =
    dt_conf_get_int(CONFIG_PREFIX "dimensions_type") == DT_DIMENSIONS_SCALE;
  const gboolean high_quality = dt_conf_get_bool(CONFIG_PREFIX "high_quality_processing");
  const gboolean export_masks = dt_conf_get_bool(CONFIG_PREFIX "export_masks");
  const gboolean style_append = dt_conf_get_bool(CONFIG_PREFIX "style_append");
  const char *tmp = dt_conf_get_string_const(CONFIG_PREFIX "style");
  if(tmp)
  {
    g_strlcpy(style, tmp, sizeof(style));
  }

  // scaling
  const gboolean is_scaling =
    dt_conf_is_equal("plugins/lighttable/export/resizing", "scaling");

  double _num, _denum;
  dt_imageio_resizing_factor_get_and_parsing(&_num, &_denum);
  const double scale_factor = is_scaling? _num / _denum : 1.0;

  // if upscale is activated and only one dimension is 0 we adjust it to ensure
  // that the up-scaling will happen. The null dimension is set to MAX_ASPECT_RATIO
  // time the other dimension, allowing for a ratio of max 1:100 exported images.

  if(upscale)
  {
    const uint32_t MAX_ASPECT_RATIO = 100;

    if(max_width == 0 && max_height != 0)
    {
      max_width = max_height * MAX_ASPECT_RATIO;
    }
    else if(max_height == 0 && max_width != 0)
    {
      max_height = max_width * MAX_ASPECT_RATIO;
    }
  }

  const dt_colorspaces_color_profile_type_t icc_type =
    dt_conf_get_int(CONFIG_PREFIX "icctype");
  gchar *icc_filename = dt_conf_get_string(CONFIG_PREFIX "iccprofile");
  const dt_iop_color_intent_t icc_intent = dt_conf_get_int(CONFIG_PREFIX "iccintent");
  const int pq_peak_nits = dt_conf_get_int(CONFIG_PREFIX "pq_peak_nits");

  GList *list = dt_act_on_get_images(TRUE, TRUE, TRUE);
  dt_control_export(list, max_width, max_height, format_index, storage_index,
                    high_quality, upscale, scaledimension,
                    is_scaling, scale_factor,
                    export_masks, style, style_append,
                    icc_type, icc_filename, icc_intent, pq_peak_nits,
                    d->metadata_export);

  g_free(icc_filename);

  _scale_optim();
  gtk_entry_set_text(GTK_ENTRY(d->scale),
                     dt_conf_get_string_const(CONFIG_PREFIX "resizing_factor"));
}

static void _export_with_preset(const gchar *preset_name, dt_lib_module_t *self)
{
  dt_lib_export_t *d = self->data;

  // get current settings from the GUI
  int params_size;
  void *params = get_params(self, &params_size);
  // not in params when the current profile cannot be PQ
  const int pq_peak_nits = dt_conf_get_int(CONFIG_PREFIX "pq_peak_nits");

  void *op_params = NULL;
  int op_params_size = 0;
  if(dt_presets_get_params_any_version("export", preset_name, &op_params, &op_params_size))
  {
    set_params(self, op_params, op_params_size);

    gboolean login = TRUE;
    if(d->storage_module->storage_login)
      login = d->storage_module->storage_login(d->storage_module);

    if(login)
      _export_with_current_settings(self);
    else
      dt_control_log(_("could not login to storage `%s'!"), d->storage_module->name(d->storage_module));
  }
  g_free(op_params);

  // restore previous settings
  set_params(self, params, params_size);
  dt_bauhaus_slider_set(d->pq_peak, pq_peak_nits);
}

static void _export_button_clicked(GtkWidget *widget, dt_lib_module_t *self)
{
  _export_with_current_settings(self);
}

static void _batch_export_button_clicked(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_export_t *d = (dt_lib_export_t *)self->data;

  GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->batch_treeview));
  GtkTreeIter iter;

  gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
  while(valid)
  {
    gboolean active;
    char *preset_name;

    gtk_tree_model_get(model, &iter,
                       DT_EXPORT_BATCH_COL_ACTIVE, &active,
                       DT_EXPORT_BATCH_COL_NAME, &preset_name,
                       -1);

    if(active)
      _export_with_preset(preset_name, self);

    valid = gtk_tree_model_iter_next(model, &iter);
  }
}

static void _scale_changed(GtkEntry *spin,
                           dt_lib_export_t *d)
{
  const char *validSign = ",.0123456789";
  const gchar *value = gtk_entry_get_text(spin);

  const int len = sizeof(value);
  int i, j = 0, idec = 0, idiv = 0, pdiv = 0;
  char new_value[30] = "";

  for(i = 0; i < len; i++)
  {
    char *val = strchr(validSign, value[i]);
    if(val == NULL)
    {
      if(idiv==0)
      {
        if(i == 0)
        {
          new_value[j++] = '1';
        }
        else
        {
          if(atof(value) == 0.0)
          {
            new_value[0] = '1';
          }
          idec = 0;
          idiv = 1;
          new_value[j++] = '/';
          pdiv = j;
        }
      }
    }
    else if((val[0] == '.') || (val[0] == ','))
    {
      if(idec == 0)
      {
        if((i == 0) || (i == pdiv))
        {
          new_value[j++] = '0';
        }
        else
        {
          idec = 1;
          new_value[j++] = value[i];
        }
      }
    }
    else if(value[i] == '\0')
    {
      break;
    }
    else
    {
      new_value[j++] = value[i];
    }
  }
  dt_conf_set_string(CONFIG_PREFIX "resizing_factor", new_value);
  gtk_entry_set_text(spin, new_value);
}

static void _width_changed(GtkEditable *entry, gpointer user_data);
static void _height_changed(GtkEditable *entry, gpointer user_data);

static void _scale_mdlclick_cb(GtkGestureSingle *gesture, int n_press,
                                  double x, double y,
                                  dt_lib_export_t *d)
{
  GtkWidget *spin = dt_gui_get_widget(gesture);
  if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_MIDDLE)
  {
    dt_conf_set_string(CONFIG_PREFIX "resizing_factor", "1");
    g_signal_handlers_block_by_func(spin, _scale_changed, d);
    gtk_entry_set_text(GTK_ENTRY(spin), "1");
    g_signal_handlers_unblock_by_func(spin, _scale_changed, d);
  }
  else
  {
    _scale_changed(GTK_ENTRY(spin), d);
  }
}

static void _widht_mdlclick_cb(GtkGestureSingle *gesture, int n_press,
                                  double x, double y,
                                  dt_lib_export_t *d)
{
  GtkWidget *spin = dt_gui_get_widget(gesture);
  if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_MIDDLE)
  {
    dt_conf_set_int(CONFIG_PREFIX "width", 0);
    g_signal_handlers_block_by_func(spin, _width_changed, d);
    gtk_entry_set_text(GTK_ENTRY(spin), "0");
    g_signal_handlers_unblock_by_func(spin, _width_changed, d);
  }
  else
  {
    _width_changed(GTK_EDITABLE(spin), d);
  }
}

static void _height_mdlclick_cb(GtkGestureSingle *gesture, int n_press,
                                   double x, double y,
                                   dt_lib_export_t *d)
{
  GtkWidget *spin = dt_gui_get_widget(gesture);
  if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_MIDDLE)
  {
    dt_conf_set_int(CONFIG_PREFIX "height", 0);
    g_signal_handlers_block_by_func(spin, _height_changed, d);
    gtk_entry_set_text(GTK_ENTRY(spin), "0");
    g_signal_handlers_unblock_by_func(spin, _height_changed, d);
  }
  else
  {
    _height_changed(GTK_EDITABLE(spin), d);
  }
}

static void _size_in_px_update(dt_lib_export_t *d)
{
  const dt_dimensions_type_t d_type =
    (dt_dimensions_type_t)dt_bauhaus_combobox_get(d->dimensions_type);

  if((d_type == DT_DIMENSIONS_SCALE) || (d_type == DT_DIMENSIONS_PIXELS))
  {
    gtk_widget_hide(d->size_in_px);
  }
  else
  {
    gtk_widget_show(d->size_in_px);
    gchar size_in_px_txt[120];
    snprintf(size_in_px_txt, sizeof(size_in_px_txt) / sizeof(size_in_px_txt[0]),
             _("which is equal to %s × %s px"),
             gtk_entry_get_text(GTK_ENTRY(d->width)),
             gtk_entry_get_text(GTK_ENTRY(d->height)));
    gtk_label_set_text(GTK_LABEL(d->size_in_px), size_in_px_txt);
  }
}

static void _set_dimensions(dt_lib_export_t *d,
                            const uint32_t max_width,
                            const uint32_t max_height,
                            const uint32_t print_dpi,
                            const gchar *scale)
{
  gchar *max_width_char = g_strdup_printf("%u", max_width);
  gchar *max_height_char = g_strdup_printf("%u", max_height);
  gchar *max_dpi_char = g_strdup_printf("%u", print_dpi);

  DT_ENTER_GUI_UPDATE();
  gtk_entry_set_text(GTK_ENTRY(d->width), max_width_char);
  gtk_entry_set_text(GTK_ENTRY(d->height), max_height_char);
  gtk_entry_set_text(GTK_ENTRY(d->print_dpi), max_dpi_char);
  gtk_entry_set_text(GTK_ENTRY(d->scale), scale);
  _size_in_px_update(d);
  DT_LEAVE_GUI_UPDATE();

  dt_conf_set_int(CONFIG_PREFIX "width", max_width);
  dt_conf_set_int(CONFIG_PREFIX "height", max_height);
  dt_conf_set_int(CONFIG_PREFIX "print_dpi", print_dpi);
  dt_conf_set_string(CONFIG_PREFIX "resizing_factor", scale);

  g_free(max_width_char);
  g_free(max_height_char);
  g_free(max_dpi_char);
  _resync_print_dimensions(d);
}


static void _size_update_display(dt_lib_export_t *self)
{
  const dt_dimensions_type_t d_type =
    (dt_dimensions_type_t)dt_bauhaus_combobox_get(self->dimensions_type);

  gtk_widget_set_visible(self->px_size, d_type == DT_DIMENSIONS_PIXELS);
  gtk_widget_set_visible(self->print_size, d_type == DT_DIMENSIONS_CM || d_type == DT_DIMENSIONS_INCH);
  gtk_widget_set_visible(self->scale, d_type == DT_DIMENSIONS_SCALE);

  gtk_label_set_text(GTK_LABEL(self->unit_label),
                     d_type == DT_DIMENSIONS_CM ? _("cm") : C_("unit", "in"));
  _size_in_px_update(self);
}

void gui_reset(dt_lib_module_t *self)
{
  // make sure we don't do anything useless:
  if(!dt_control_running()) return;
  dt_lib_export_t *d = self->data;
  gtk_entry_set_text(GTK_ENTRY(d->width),
                     dt_confgen_get(CONFIG_PREFIX "width", DT_DEFAULT));
  gtk_entry_set_text(GTK_ENTRY(d->height),
                     dt_confgen_get(CONFIG_PREFIX "height", DT_DEFAULT));
  dt_bauhaus_combobox_set(d->dimensions_type,
                          dt_confgen_get_int(CONFIG_PREFIX "dimensions_type", DT_DEFAULT));
  _size_update_display(d);

  // first reset the selected storage and format modules
  dt_imageio_module_format_t *mformat = dt_imageio_get_format();
  if(mformat) mformat->gui_reset(mformat);
  dt_imageio_module_storage_t *mstorage = dt_imageio_get_storage();
  if(mstorage) mstorage->gui_reset(mstorage);

  // Set default storage
  const int storage_index =
    dt_imageio_get_index_of_storage(dt_imageio_get_storage_by_name
                                    (dt_confgen_get(CONFIG_PREFIX "storage_name",
                                                    DT_DEFAULT)));
  dt_bauhaus_combobox_set(d->storage, storage_index);

  // Set default format
  const int format_index =
    dt_imageio_get_index_of_format(dt_imageio_get_format_by_name
                                   (dt_confgen_get(CONFIG_PREFIX "format_name",
                                                   DT_DEFAULT)));
  dt_bauhaus_combobox_set(d->format, format_index);

  dt_bauhaus_combobox_set(d->upscale,
                          dt_confgen_get_bool(CONFIG_PREFIX "upscale",
                                              DT_DEFAULT) ? 1 : 0);
  dt_bauhaus_combobox_set(d->high_quality,
                          dt_confgen_get_bool(CONFIG_PREFIX "high_quality_processing",
                                              DT_DEFAULT) ? 1 : 0);
  dt_bauhaus_combobox_set(d->export_masks,
                          dt_confgen_get_bool(CONFIG_PREFIX "export_masks",
                                              DT_DEFAULT) ? 1 : 0);

  dt_bauhaus_combobox_set(d->intent,
                          dt_confgen_get_int(CONFIG_PREFIX "iccintent",
                                             DT_DEFAULT) + 1);

  dt_bauhaus_slider_set(d->pq_peak, dt_confgen_get_int(CONFIG_PREFIX "pq_peak_nits",
                                                       DT_DEFAULT));

  // iccprofile
  const int icctype = dt_confgen_get_int(CONFIG_PREFIX "icctype", DT_DEFAULT);
  gchar *iccfilename = dt_conf_get_string(CONFIG_PREFIX "iccprofile");
  dt_bauhaus_combobox_set(d->profile, 0);
  if(icctype != DT_COLORSPACE_NONE)
  {
    for(GList *profiles = darktable.color_profiles->profiles;
        profiles;
        profiles = g_list_next(profiles))
    {
      const dt_colorspaces_color_profile_t *pp = profiles->data;
      if(pp->out_pos > -1
         && icctype == pp->type
         && (icctype != DT_COLORSPACE_FILE || !strcmp(iccfilename, pp->filename)))
      {
        dt_bauhaus_combobox_set(d->profile, pp->out_pos + 1);
        break;
      }
    }
  }

  g_free(iccfilename);

  // set style to 'none'
  _update_style_label(d, "");

  // reset style mode
  dt_bauhaus_combobox_set(d->style_mode,
                          dt_confgen_get_bool(CONFIG_PREFIX "style_append", DT_DEFAULT));

  gtk_widget_set_visible(GTK_WIDGET(d->style_mode), _is_style_set(d->style_name));

  // export metadata presets
  g_free(d->metadata_export);
  d->metadata_export = dt_lib_export_metadata_get_conf();

  dt_lib_gui_queue_update(self);
}

static void set_format_by_name(dt_lib_export_t *d,
                               const char *name)
{
  // Find the selected format plugin among all existing plugins
  dt_imageio_module_format_t *module = NULL;
  for(GList *it = darktable.imageio->plugins_format; it; it = g_list_next(it))
  {
    if(g_strcmp0(((dt_imageio_module_format_t *)it->data)->name(), name) == 0
       || g_strcmp0(((dt_imageio_module_format_t *)it->data)->plugin_name, name) == 0)
    {
      module = (dt_imageio_module_format_t *)it->data;
      break;
    }
  }

  if(!module)
  {
    gtk_widget_hide(d->format_extra_container);
    return;
  }
  else if(module->widget)
  {
    gtk_widget_show_all(d->format_extra_container);
    gtk_stack_set_visible_child(GTK_STACK(d->format_extra_container), module->widget);
  }
  else
  {
    gtk_widget_hide(d->format_extra_container);
  }

  // Store the new format
  dt_conf_set_string(CONFIG_PREFIX "format_name", module->plugin_name);

  if(dt_bauhaus_combobox_set_from_text(d->format, module->name()) == FALSE)
    dt_bauhaus_combobox_set(d->format, 0);

  // Let's also update combination of storage/format dimension restrictions
  _update_dimensions(d);

  // only some modules support export of masks set it to 0 when
  // insensitive and restore when making it sensitive again. this
  // doesn't survive dt restarts.
  const gboolean support_layers =
    (module->flags(NULL) & FORMAT_FLAGS_SUPPORT_LAYERS) == FORMAT_FLAGS_SUPPORT_LAYERS;

  const gboolean is_enabled = gtk_widget_get_sensitive(d->export_masks);
  if(support_layers && !is_enabled)
  {
    // combobox was disabled and shall be enabled. we want to restore
    // the old setting.
    const gboolean export_masks = dt_conf_get_bool(CONFIG_PREFIX "export_masks");
    gtk_widget_set_sensitive(d->export_masks, TRUE);
    dt_bauhaus_combobox_set(d->export_masks, export_masks ? 1 : 0);
  }
  else if(!support_layers && is_enabled)
  {
    // combobox was enabled but shall be disabled. we want to save the
    // current setting.
    const int export_masks = dt_bauhaus_combobox_get(d->export_masks);
    dt_bauhaus_combobox_set(d->export_masks, 0);
    dt_conf_set_bool(CONFIG_PREFIX "export_masks", export_masks == 1);
    gtk_widget_set_sensitive(d->export_masks, FALSE);
  }
}

static void _format_changed(GtkWidget *widget,
                            dt_lib_export_t *d)
{
  const gchar *name = dt_bauhaus_combobox_get_text(d->format);
  g_signal_handlers_block_by_func(widget, _format_changed, d);
  set_format_by_name(d, name);
  g_signal_handlers_unblock_by_func(widget, _format_changed, d);
}

static void _get_max_output_dimension(dt_lib_export_t *d,
                                      uint32_t *width,
                                      uint32_t *height)
{
  const char *storage_name = dt_conf_get_string_const(CONFIG_PREFIX "storage_name");
  dt_imageio_module_storage_t *storage = dt_imageio_get_storage_by_name(storage_name);

  const char *format_name = dt_conf_get_string_const(CONFIG_PREFIX "format_name");
  dt_imageio_module_format_t *format = dt_imageio_get_format_by_name(format_name);

  if(storage && format)
  {
    uint32_t fw, fh, sw, sh;
    fw = fh = sw = sh = 0; // We are all equals!!!
    storage->dimension(storage, NULL, &sw, &sh);
    format->dimension(format, NULL, &fw, &fh);

    if(sw == 0 || fw == 0)
      *width = sw > fw ? sw : fw;
    else
      *width = sw < fw ? sw : fw;

    if(sh == 0 || fh == 0)
      *height = sh > fh ? sh : fh;
    else
      *height = sh < fh ? sh : fh;
  }
}

static void _validate_dimensions(dt_lib_export_t *d)
{
  //reset dimensions to previously stored value if they exceed the maximum
  uint32_t width = atoi(gtk_entry_get_text(GTK_ENTRY(d->width)));
  uint32_t height = atoi(gtk_entry_get_text(GTK_ENTRY(d->height)));
  uint32_t print_dpi = atoi(gtk_entry_get_text(GTK_ENTRY(d->print_dpi)));
  const gchar *scale = gtk_entry_get_text(GTK_ENTRY(d->scale));
  if(width > d->max_allowed_width || height > d->max_allowed_height)
  {
    width  = width  > d->max_allowed_width
      ? dt_conf_get_int(CONFIG_PREFIX "width")
      : width;
    height = height > d->max_allowed_height
      ? dt_conf_get_int(CONFIG_PREFIX "height")
      : height;
    _set_dimensions(d, width, height, print_dpi, scale);
  }
}

static void _update_dimensions(dt_lib_export_t *d)
{
  uint32_t max_w = 0, max_h = 0;
  _get_max_output_dimension(d, &max_w, &max_h);
  d->max_allowed_width = max_w > 0 ? max_w : EXPORT_MAX_IMAGE_SIZE;
  d->max_allowed_height = max_h > 0 ? max_h : EXPORT_MAX_IMAGE_SIZE;
  _validate_dimensions(d);
}

static void set_storage_by_name(dt_lib_export_t *d,
                                const char *name)
{
  int k = -1;
  dt_imageio_module_storage_t *module = NULL;
  d->storage_module = NULL;

  for(const GList *it = darktable.imageio->plugins_storage; it; it = g_list_next(it))
  {
    dt_imageio_module_storage_t *storage = it->data;
    k++;
    if(strcmp(storage->name(storage), name) == 0
       || strcmp(storage->plugin_name, name) == 0)
    {
      module = storage;
      break;
    }
  }

  if(!module)
  {
    gtk_widget_hide(d->storage_extra_container);
    return;
  }
  else if(module->widget)
  {
    gtk_widget_show_all(d->storage_extra_container);
    gtk_stack_set_visible_child(GTK_STACK(d->storage_extra_container), module->widget);
  }
  else
  {
    gtk_widget_hide(d->storage_extra_container);
  }
  dt_bauhaus_combobox_set(d->storage, k);
  dt_conf_set_string(CONFIG_PREFIX "storage_name", module->plugin_name);
  d->storage_module = module;

  // Check if plugin recommends a max dimension and set
  // if not implemented the stored conf values are used..
  uint32_t w = 0, h = 0;
  module->recommended_dimension(module, NULL, &w, &h);

  const uint32_t cw = dt_conf_get_int(CONFIG_PREFIX "width");
  const uint32_t ch = dt_conf_get_int(CONFIG_PREFIX "height");
  const uint32_t pd = dt_conf_get_int(CONFIG_PREFIX "print_dpi");
  const gchar *scale = dt_conf_get_string_const(CONFIG_PREFIX "resizing_factor");

  // If user's selected value is below the max, select it
  if(w > cw || w == 0) w = cw;
  if(h > ch || h == 0) h = ch;

  // Set the recommended dimension
  _set_dimensions(d, w, h, pd, scale);

  // Let's update formats combobox with supported formats of selected storage module...
  _update_formats_combobox(d);

  // Lets try to set selected format if fail select first in list..
  const char *format_name = dt_conf_get_string_const(CONFIG_PREFIX "format_name");
  const dt_imageio_module_format_t *format = dt_imageio_get_format_by_name(format_name);

  if(format == NULL
     || dt_bauhaus_combobox_set_from_text(d->format, format->name()) == FALSE)
    dt_bauhaus_combobox_set(d->format, 0);
}

static void _storage_changed(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_export_t *d = self->data;

  const gchar *name = dt_bauhaus_combobox_get_text(d->storage);
  g_signal_handlers_block_by_func(widget, _storage_changed, self);
  if(name) set_storage_by_name(d, name);
  g_signal_handlers_unblock_by_func(widget, _storage_changed, self);
  dt_lib_gui_queue_update(self);
}

// the PQ peak only matters when the export may be PQ
static gboolean _export_may_be_pq(const dt_colorspaces_color_profile_type_t icctype)
{
  return icctype == DT_COLORSPACE_NONE || dt_colorspaces_hdr_transfer(icctype) == DT_HDR_PQ;
}

static void _update_pq_peak_visibility(dt_lib_export_t *d)
{
  gtk_widget_set_visible(d->pq_peak, _export_may_be_pq(dt_conf_get_int(CONFIG_PREFIX "icctype")));
}

static void _set_profile_conf(GtkWidget *widget)
{
  int pos = dt_bauhaus_combobox_get(widget);
  if(pos > 0)
  {
    pos--;
    for(GList *profiles = darktable.color_profiles->profiles;
        profiles;
        profiles = g_list_next(profiles))
    {
      const dt_colorspaces_color_profile_t *pp = profiles->data;
      if(pp->out_pos == pos)
      {
        dt_conf_set_int(CONFIG_PREFIX "icctype", pp->type);
        if(pp->type == DT_COLORSPACE_FILE)
          dt_conf_set_string(CONFIG_PREFIX "iccprofile", pp->filename);
        else
          dt_conf_set_string(CONFIG_PREFIX "iccprofile", "");
        return;
      }
    }
  }
  dt_conf_set_int(CONFIG_PREFIX "icctype", DT_COLORSPACE_NONE);
  dt_conf_set_string(CONFIG_PREFIX "iccprofile", "");
}

static void _profile_changed(GtkWidget *widget, dt_lib_export_t *d)
{
  _set_profile_conf(widget);
  _update_pq_peak_visibility(d);
}

static void _pq_peak_changed(GtkWidget *widget, dt_lib_export_t *d)
{
  dt_conf_set_int(CONFIG_PREFIX "pq_peak_nits", (int)roundf(dt_bauhaus_slider_get(widget)));
  // colorout reads the peak when it commits, and a darkroom on a PQ
  // display renders with it too
  if(dt_view_get_current() == DT_VIEW_DARKROOM)
    dt_dev_reprocess_all(darktable.develop);
}

static void _dimensions_type_changed(GtkWidget *widget,
                                     dt_lib_export_t *d)
{
  DT_GUARD_GUI_UPDATE();

  const dt_dimensions_type_t d_type =
    (dt_dimensions_type_t)dt_bauhaus_combobox_get(widget);

  dt_conf_set_int(CONFIG_PREFIX "dimensions_type", d_type);
  dt_conf_set_string(CONFIG_PREFIX "resizing",
                     d_type == DT_DIMENSIONS_SCALE ? "scaling" : "max_size");
  if(d_type == DT_DIMENSIONS_CM || d_type == DT_DIMENSIONS_INCH)
  {
    // set dpi to user-set dpi
    dt_conf_set_int("metadata/resolution",
                    dt_conf_get_int(CONFIG_PREFIX "print_dpi"));
    _resync_print_dimensions(d);
  }
  else
  {
    // reset export dpi to default value for scale/pixel specific export
    dt_conf_set_int("metadata/resolution",
                    dt_confgen_get_int("metadata/resolution", DT_DEFAULT));
  }
  _size_update_display(d);
}

static void _resync_print_dimensions(dt_lib_export_t *self)
{
  DT_TRY_GUI_UPDATE();

  const uint32_t width = dt_conf_get_int(CONFIG_PREFIX "width");
  const uint32_t height = dt_conf_get_int(CONFIG_PREFIX "height");
  const int dpi = atoi(gtk_entry_get_text(GTK_ENTRY(self->print_dpi)));

  const float p_width = pixels2print(self, width);
  const float p_height = pixels2print(self, height);

  gchar *pwidth = g_strdup_printf("%.2f", p_width);
  gchar *pheight = g_strdup_printf("%.2f", p_height);
  gchar *pdpi = g_strdup_printf("%d", dpi);
  gtk_entry_set_text(GTK_ENTRY(self->print_width), pwidth);
  gtk_entry_set_text(GTK_ENTRY(self->print_height), pheight);
  gtk_entry_set_text(GTK_ENTRY(self->print_dpi), pdpi);
  g_free(pwidth);
  g_free(pheight);
  g_free(pdpi);
  DT_LEAVE_GUI_UPDATE();
}

static void _resync_pixel_dimensions(dt_lib_export_t *self)
{
  DT_TRY_GUI_UPDATE();

  const float p_width = atof(gtk_entry_get_text(GTK_ENTRY(self->print_width)));
  const float p_height = atof(gtk_entry_get_text(GTK_ENTRY(self->print_height)));

  const uint32_t width = print2pixels(self, p_width);
  const uint32_t height = print2pixels(self, p_height);

  dt_conf_set_int(CONFIG_PREFIX "width", width);
  dt_conf_set_int(CONFIG_PREFIX "height", height);

  gchar *pwidth = g_strdup_printf("%u", width);
  gchar *pheight = g_strdup_printf("%u", height);
  gtk_entry_set_text(GTK_ENTRY(self->width), pwidth);
  gtk_entry_set_text(GTK_ENTRY(self->height), pheight);
  g_free(pwidth);
  g_free(pheight);
  DT_LEAVE_GUI_UPDATE();
}

static void _width_changed(GtkEditable *entry, gpointer user_data)
{
  DT_GUARD_GUI_UPDATE();

  const dt_lib_export_t *d = (dt_lib_export_t *)user_data;
  const uint32_t width = atoi(gtk_entry_get_text(GTK_ENTRY(d->width)));
  dt_conf_set_int(CONFIG_PREFIX "width", width);
}

static void _print_width_changed(GtkEditable *entry, gpointer user_data)
{
  DT_TRY_GUI_UPDATE();

  dt_lib_export_t *d = (dt_lib_export_t *)user_data;

  const float p_width = atof(gtk_entry_get_text(GTK_ENTRY(d->print_width)));
  const uint32_t width = print2pixels(d, p_width);
  dt_conf_set_int(CONFIG_PREFIX "width", width);

  gchar *pwidth = g_strdup_printf("%u", width);
  gtk_entry_set_text(GTK_ENTRY(d->width), pwidth);
  g_free(pwidth);
  _size_in_px_update(d);
  DT_LEAVE_GUI_UPDATE();
}

static void _height_changed(GtkEditable *entry, gpointer user_data)
{
  DT_GUARD_GUI_UPDATE();

  const dt_lib_export_t *d = (dt_lib_export_t *)user_data;
  const uint32_t height = atoi(gtk_entry_get_text(GTK_ENTRY(d->height)));
  dt_conf_set_int(CONFIG_PREFIX "height", height);
}

static void _print_height_changed(GtkEditable *entry, gpointer user_data)
{
  DT_TRY_GUI_UPDATE();

  dt_lib_export_t *d = (dt_lib_export_t *)user_data;

  const float p_height = atof(gtk_entry_get_text(GTK_ENTRY(d->print_height)));
  const uint32_t height = print2pixels(d, p_height);
  dt_conf_set_int(CONFIG_PREFIX "height", height);

  gchar *pheight = g_strdup_printf("%u", height);
  gtk_entry_set_text(GTK_ENTRY(d->height), pheight);
  g_free(pheight);
  _size_in_px_update(d);
  DT_LEAVE_GUI_UPDATE();
}

static void _print_dpi_changed(GtkWidget *widget, gpointer user_data)
{
  DT_GUARD_GUI_UPDATE();

  dt_lib_export_t *d = (dt_lib_export_t *)user_data;
  const int dpi = atoi(gtk_entry_get_text(GTK_ENTRY(d->print_dpi)));

  dt_conf_set_int(CONFIG_PREFIX "print_dpi", dpi);
  dt_conf_set_int("metadata/resolution", dpi);

  _resync_pixel_dimensions(d);
  _size_in_px_update(d);
}

static void _callback_bool(GtkWidget *widget, gpointer user_data)
{
  const char *key = (const char *)user_data;
  dt_conf_set_bool(key, dt_bauhaus_combobox_get(widget) == 1);
}

static void _intent_changed(GtkWidget *widget, dt_lib_export_t *d)
{
  const int pos = dt_bauhaus_combobox_get(widget);
  dt_conf_set_int(CONFIG_PREFIX "iccintent", pos - 1);
}

static void _update_style_label(dt_lib_export_t *d, const char *name)
{
  gtk_widget_set_visible(GTK_WIDGET(d->style_mode), _is_style_set(name));

  // We use the string "none" to indicate that we don't apply any style to the export
  char *localized_style = name[0]
    ? dt_util_localize_segmented_name(name, TRUE)
    : g_strdup(_("none"));

  // Use only the leaf part of the segmented style name in the tooltip
  char *leaf = strrchr(localized_style, '|');
  leaf = leaf ? leaf+1 : localized_style;
  gtk_label_set_text(GTK_LABEL(d->style), leaf);
  char *tooltip = g_strdup_printf(_("style to be applied on export:\n<b>%s</b>"),
                                  localized_style);
  g_free(localized_style);
  gtk_widget_set_tooltip_markup(d->style, tooltip);
  g_free(tooltip);
  g_free(d->style_name);
  d->style_name = g_strdup(name);
  dt_conf_set_string(CONFIG_PREFIX "style", d->style_name);
}

static void _update_style(const dt_stylemenu_data_t *menu_data)
{
  _update_style_label(menu_data->user_data,menu_data->name);
}

static void _apply_style_activate_callback(GSimpleAction *action,
                                           GVariant *parameter,
                                           gpointer user_data)
{
  const dt_stylemenu_data_t *menu_data = (dt_stylemenu_data_t *)g_variant_get_uint64(parameter);

  _update_style(menu_data);
  dtgtk_stylemenu_free_menu_data();
}

static void _style_popupmenu_callback(GtkWidget *w,
                                      gpointer user_data)
{
  GActionGroup *action_group = gtk_widget_get_action_group(w, "styles");
  if(action_group == NULL)
  {
    GActionEntry action_entries[] =
    {
      { "activate", _apply_style_activate_callback, "t", NULL },
    };

    action_group = G_ACTION_GROUP(g_simple_action_group_new());
    g_action_map_add_action_entries(G_ACTION_MAP(action_group),
                                    action_entries,
                                    G_N_ELEMENTS(action_entries),
                                    NULL);
    gtk_widget_insert_action_group(w, 
                                   "styles",
                                   G_ACTION_GROUP(action_group));
    g_object_unref(action_group);
  }

  /* if we got any styles, lets popup menu for selection */
  GtkWidget *popover_menu = dtgtk_build_style_popover_menu(w, TRUE, user_data);
  if(popover_menu)
  {
    gtk_popover_popup(GTK_POPOVER(popover_menu));
  }
  else
    dt_control_log(_("no styles have been created yet"));
}

int position(const dt_lib_module_t *self)
{
  return 0;
}

static void _update_formats_combobox(dt_lib_export_t *d)
{
  // Clear format combo box
  dt_bauhaus_combobox_clear(d->format);

  // Get current selected storage
  const char *storage_name = dt_conf_get_string_const(CONFIG_PREFIX "storage_name");
  dt_imageio_module_storage_t *storage = dt_imageio_get_storage_by_name(storage_name);

  // Add supported formats to combobox
  gboolean empty = TRUE;
  for(const GList *it = darktable.imageio->plugins_format; it; it = g_list_next(it))
  {
    dt_imageio_module_format_t *format = it->data;
    if(storage->supported(storage, format))
    {
      dt_bauhaus_combobox_add(d->format, format->name());
      empty = FALSE;
    }
  }

  gtk_widget_set_sensitive(d->format, !empty);
}

static void _on_storage_list_changed(gpointer instance,
                                     dt_lib_module_t *self)
{
  dt_lib_export_t *d = self->data;
  dt_imageio_module_storage_t *storage = dt_imageio_get_storage();
  dt_bauhaus_combobox_clear(d->storage);

  dt_gui_container_remove_children(GTK_CONTAINER(d->storage_extra_container));

  for(const GList *it = darktable.imageio->plugins_storage; it; it = g_list_next(it))
  {
    const dt_imageio_module_storage_t *module = it->data;
    dt_bauhaus_combobox_add(d->storage, module->name(module));
    if(module->widget)
    {
      gtk_container_add(GTK_CONTAINER(d->storage_extra_container), module->widget);
    }
  }
  dt_bauhaus_combobox_set(d->storage, dt_imageio_get_index_of_storage(storage));
}

static void _menuitem_preferences(GSimpleAction *action,
                                  GVariant *parameter,
                                  gpointer user_data)
{
  dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  dt_lib_export_t *d = self->data;
  const gchar *name = dt_bauhaus_combobox_get_text(d->storage);
  const gboolean ondisk = name
    && !g_strcmp0(name, _("file on disk")); // FIXME: NO!!!!!one!
  d->metadata_export =
    dt_lib_export_metadata_configuration_dialog(d->metadata_export, ondisk);
}

static gboolean _batch_preset_active(dt_lib_module_t *self)
{
  dt_lib_export_t *d = self->data;

  GtkTreeIter iter;
  GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->batch_treeview));
  gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
  gboolean batch_preset_active = FALSE;

  while(valid)
  {
    gboolean active;
    gtk_tree_model_get(model, &iter,
                       DT_EXPORT_BATCH_COL_ACTIVE, &active,
                       -1);
    if(active)
    {
      batch_preset_active = TRUE;
      break;
    }
    valid = gtk_tree_model_iter_next(model, &iter);
  }

  return batch_preset_active;
}

static void _batch_export_toggled_callback(GtkCellRendererToggle *cell_renderer,
                                           gchar *path_str,
                                           gpointer user_data)
{
  dt_lib_module_t *self = user_data;
  dt_lib_export_t *d = self->data;

  GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->batch_treeview));
  GtkTreeIter iter;
  GtkTreePath *path = gtk_tree_path_new_from_string(path_str);
  gboolean toggle;
  gchar *name;

  gtk_tree_model_get_iter(model, &iter, path);
  gtk_tree_model_get(model, &iter,
                     DT_EXPORT_BATCH_COL_ACTIVE, &toggle,
                     DT_EXPORT_BATCH_COL_NAME, &name,
                     -1);
  toggle = !toggle;
  gtk_list_store_set(GTK_LIST_STORE(model), &iter, DT_EXPORT_BATCH_COL_ACTIVE, toggle, -1);
  gtk_tree_path_free(path);
  gchar *setting = g_strdup_printf(CONFIG_PREFIX "batch_%s", name);
  dt_conf_set_bool(setting, toggle);
  g_free(setting);

  gtk_widget_set_sensitive(GTK_WIDGET(d->batch_export_button), _batch_preset_active(self));
}

static void _fill_batch_export_list(dt_lib_module_t *self)
{
  dt_lib_export_t *d = self->data;

  GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->batch_treeview));
  GtkTreeIter iter;

  // save all preset names from the store to check if presets were renamed/deleted
  GList *nonexistent_presets = NULL;
  gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
  while(valid)
  {
    char *preset_name;

    gtk_tree_model_get(model, &iter,
                       DT_EXPORT_BATCH_COL_NAME, &preset_name,
                       -1);

    nonexistent_presets = g_list_prepend(nonexistent_presets, g_strdup(preset_name));
    valid = gtk_tree_model_iter_next(model, &iter);
  }

  // clear and refill the store
  gtk_list_store_clear(GTK_LIST_STORE(model));
  gboolean has_active = FALSE;

  GList *names = dt_presets_version_names("export", self->version(), FALSE);
  for(const GList *l = names; l; l = g_list_next(l))
  {
    const char *name = l->data;

    gchar *setting = g_strdup_printf(CONFIG_PREFIX "batch_%s", name);
    gboolean active = dt_conf_get_bool(setting);
    g_free(setting);

    gtk_list_store_insert_with_values(GTK_LIST_STORE(model), NULL, -1,
                        DT_EXPORT_BATCH_COL_ACTIVE, active,
                        DT_EXPORT_BATCH_COL_NAME, name,
                        -1);
    has_active |= active;

    // remove unchanged preset names from the list so the list will
    // contain only preset names which no longer exist
    for(GList *preset_iter = nonexistent_presets; preset_iter; preset_iter = g_list_next(preset_iter))
    {
      const gchar *preset_name = (gchar *)preset_iter->data;
      if(!g_strcmp0(preset_name, name))
      {
        nonexistent_presets = g_list_remove_link(nonexistent_presets, preset_iter);
        g_list_free_full(preset_iter, g_free);
        break;
      }
    }
  }
  g_list_free_full(names, g_free);

  gtk_widget_set_sensitive(GTK_WIDGET(d->batch_export_button), has_active);

  // remove non-existent entries from config
  for(GList *preset_iter = nonexistent_presets; preset_iter; preset_iter = g_list_next(preset_iter))
  {
      const gchar *preset_name = (gchar *)preset_iter->data;
      gchar *setting = g_strdup_printf(CONFIG_PREFIX "batch_%s", preset_name);
      dt_conf_remove_key(setting);
      g_free(setting);
  }

  g_list_free_full(nonexistent_presets, g_free);
}

static void _export_presets_changed_callback(gpointer instance, gpointer module, dt_lib_module_t *self)
{
  if(!g_strcmp0(module, "export"))
    _fill_batch_export_list(self);
}

void set_preferences(GMenu *menu, GActionGroup *action_group, dt_lib_module_t *self)
{
  GSimpleAction *action = g_simple_action_new("preferences", NULL);
  g_signal_connect(action, "activate", G_CALLBACK(_menuitem_preferences), self);
  g_action_map_add_action(G_ACTION_MAP(action_group), G_ACTION(action));
  g_menu_append(menu, _("preferences..."), "presets.preferences");
}

void gui_init(dt_lib_module_t *self)
{
  dt_lib_export_t *d = malloc(sizeof(dt_lib_export_t));
  self->data = (void *)d;

  dt_action_insert_sorted(DT_ACTION(self), &darktable.control->actions_format);
  dt_action_insert_sorted(DT_ACTION(self), &darktable.control->actions_storage);

  d->storage = dt_bauhaus_combobox_new_action(DT_ACTION(self));
  dt_bauhaus_widget_set_label(d->storage, NULL, N_("target storage"));

  // add all storage widgets to the stack widget
  d->storage_extra_container = gtk_stack_new();
  gtk_stack_set_homogeneous(GTK_STACK(d->storage_extra_container),FALSE);

  for(const GList *it = darktable.imageio->plugins_storage;
      it;
      it = g_list_next(it))
  {
    const dt_imageio_module_storage_t *module = it->data;
    dt_bauhaus_combobox_add(d->storage, module->name(module));
    if(module->widget)
    {
      gtk_container_add(GTK_CONTAINER(d->storage_extra_container), module->widget);
    }
  }

  // postponed so we can do the two steps in one loop
  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_IMAGEIO_STORAGE_CHANGE, _on_storage_list_changed);
  g_signal_connect(G_OBJECT(d->storage), "value-changed",
                   G_CALLBACK(_storage_changed), self);

  d->format = dt_bauhaus_combobox_new_action(DT_ACTION(self));
  dt_bauhaus_widget_set_label(d->format, NULL, N_("file format"));
  gtk_widget_set_tooltip_markup(GTK_WIDGET(d->format),
    _("images will be exported according to the format specified here\n\n"
      "when exporting to AVIF, EXR, HEIF, JPEG XL, or XCF, selecting\n"
      "specific metadata is not currently possible\n\n"
      "for these formats, no metadata fields will be included\n"
      "unless the user selects <b>all</b> of the metadata checkboxes in\n"
      "the export module preferences"));
  g_signal_connect(G_OBJECT(d->format), "value-changed",
                   G_CALLBACK(_format_changed), (gpointer)d);

  // add all format widgets to the stack widget
  d->format_extra_container = gtk_stack_new();
  gtk_stack_set_homogeneous(GTK_STACK(d->format_extra_container),FALSE);
  for(const GList *it = darktable.imageio->plugins_format; it; it = g_list_next(it))
  {
    const dt_imageio_module_format_t *module = it->data;
    if(module->widget)
    {
      gtk_container_add(GTK_CONTAINER(d->format_extra_container), module->widget);
    }
  }

  DT_BAUHAUS_COMBOBOX_NEW_FULL(d->dimensions_type, self, NULL, N_("set size"),
                               _("choose a method for setting the output size"),
                               dt_conf_get_int(CONFIG_PREFIX "dimensions_type"),
                               _dimensions_type_changed, d,
                               N_("in pixels (for file)"),
                               N_("in cm (for print)"),
                               N_("in inch (for print)"),
                               N_("by scale (for file)"));

  d->print_width = dt_action_entry_new(DT_ACTION(self),
                                       N_("print width"),
                                       G_CALLBACK(_print_width_changed), d,
                                       _("maximum output width limit.\n"
                                         "click middle mouse button to reset to 0."),
                                       NULL);

  d->print_height = dt_action_entry_new(DT_ACTION(self), N_("print height"),
                                        G_CALLBACK(_print_height_changed), d,
                                       _("maximum output height limit.\n"
                                         "click middle mouse button to reset to 0."),
                                        NULL);

  d->print_dpi = dt_action_entry_new(DT_ACTION(self), N_("dpi"),
                                     G_CALLBACK(_print_dpi_changed), d,
                                     _("resolution in dot per inch"),
                                     dt_conf_get_string_const(CONFIG_PREFIX "print_dpi"));

  d->width = dt_action_entry_new(DT_ACTION(self), N_("width"),
                                 G_CALLBACK(_width_changed), d,
                                 _("maximum output width limit.\n"
                                   "click middle mouse button to reset to 0."),
                                 NULL);

  d->height = dt_action_entry_new(DT_ACTION(self), N_("height"),
                                  G_CALLBACK(_height_changed), d,
                                  _("maximum output height limit.\n"
                                    "click middle mouse button to reset to 0."),
                                  NULL);

  d->print_size = gtk_flow_box_new();
  gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(d->print_size), 5);
  gtk_flow_box_set_column_spacing (GTK_FLOW_BOX(d->print_size), 3);
  gtk_container_add(GTK_CONTAINER(d->print_size), d->print_width);
  gtk_container_add(GTK_CONTAINER(d->print_size), gtk_label_new(_("x")));
  gtk_container_add(GTK_CONTAINER(d->print_size), d->print_height);
  d->unit_label = gtk_label_new(_("cm"));
  gtk_container_add(GTK_CONTAINER(d->print_size), d->unit_label);
  GtkBox *dpi_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 3));
  gtk_box_pack_start(dpi_box, gtk_label_new(_("@")), FALSE, FALSE, 0);
  gtk_box_pack_start(dpi_box, d->print_dpi, TRUE, TRUE, 0);
  gtk_box_pack_start(dpi_box, gtk_label_new(_("dpi")), FALSE, FALSE, 0);
  gtk_container_add(GTK_CONTAINER(d->print_size), GTK_WIDGET(dpi_box));
  gtk_container_foreach(GTK_CONTAINER(d->print_size),
                        (GtkCallback)gtk_widget_set_can_focus, GINT_TO_POINTER(FALSE));

  d->px_size = gtk_flow_box_new();
  gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(d->px_size), 3);
  gtk_flow_box_set_column_spacing (GTK_FLOW_BOX(d->px_size), 3);
  gtk_container_add(GTK_CONTAINER(d->px_size), d->width);
  gtk_container_add(GTK_CONTAINER(d->px_size), gtk_label_new(_("x")));
  GtkBox *px_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 3));
  gtk_box_pack_start(px_box, d->height, TRUE, TRUE, 0);
  gtk_box_pack_start(px_box, gtk_label_new(_("px")), FALSE, FALSE, 0);
  gtk_container_add(GTK_CONTAINER(d->px_size), GTK_WIDGET(px_box));
  gtk_container_foreach(GTK_CONTAINER(d->px_size),
                        (GtkCallback)gtk_widget_set_can_focus, GINT_TO_POINTER(FALSE));

  d->scale = dt_action_entry_new
    (DT_ACTION(self), N_("scale"),
     G_CALLBACK(_scale_changed), d,
     _("it can be an integer, decimal number or simple fraction.\n"
       "zero or empty values are equal to 1.\n"
       "click middle mouse button to reset to 1."),
     dt_conf_get_string_const(CONFIG_PREFIX "resizing_factor"));
  gtk_widget_set_halign(GTK_WIDGET(d->scale), GTK_ALIGN_END);

  d->size_in_px = gtk_label_new("");
  gtk_label_set_ellipsize(GTK_LABEL(d->size_in_px), PANGO_ELLIPSIZE_START);
  gtk_widget_set_sensitive(GTK_WIDGET(d->size_in_px), FALSE);

  gtk_widget_set_halign(GTK_WIDGET(d->scale), GTK_ALIGN_FILL);
  gtk_widget_set_halign(GTK_WIDGET(d->size_in_px), GTK_ALIGN_END);

  DT_BAUHAUS_COMBOBOX_NEW_FULL
    (d->upscale, self, NULL,
     N_("allow upscaling"), NULL,
     dt_conf_get_bool(CONFIG_PREFIX "upscale") ? 1 : 0,
     _callback_bool,
     (gpointer)CONFIG_PREFIX "upscale",
     N_("no"), N_("yes"));

  DT_BAUHAUS_COMBOBOX_NEW_FULL
    (d->high_quality, self, NULL,
     N_("high quality resampling"),
     _("do high quality resampling during export"),
     dt_conf_get_bool(CONFIG_PREFIX "high_quality_processing") ? 1 : 0, _callback_bool,
     (gpointer)CONFIG_PREFIX "high_quality_processing",
     N_("no"), N_("yes"));

  DT_BAUHAUS_COMBOBOX_NEW_FULL
    (d->export_masks, self, NULL,
     N_("store masks"),
     _("store masks as layers in exported images. only works for some formats."),
     dt_conf_get_bool(CONFIG_PREFIX "export_masks") ? 1 : 0, _callback_bool,
     (gpointer)CONFIG_PREFIX "export_masks",
     N_("no"), N_("yes"));

  //  Add profile combo

  d->profile = dt_bauhaus_combobox_new_action(DT_ACTION(self));
  dt_bauhaus_widget_set_label(d->profile, NULL, N_("profile"));
  dt_bauhaus_combobox_add(d->profile, _("image settings"));
  for(GList *l = darktable.color_profiles->profiles; l; l = g_list_next(l))
  {
    const dt_colorspaces_color_profile_t *prof = l->data;
    if(prof->out_pos > -1)
      dt_bauhaus_combobox_add(d->profile, prof->name);
  }

  dt_bauhaus_combobox_set(d->profile, 0);

  char *tooltip = dt_ioppr_get_location_tooltip("out", _("output ICC profiles"));
  gtk_widget_set_tooltip_markup(d->profile, tooltip);
  g_free(tooltip);

  //  Add PQ peak slider

  d->pq_peak = dt_bauhaus_slider_new_action(DT_ACTION(self), 203.f, 10000.f, 1.f,
                                            dt_confgen_get_int(CONFIG_PREFIX "pq_peak_nits",
                                                               DT_DEFAULT), 0);
  dt_bauhaus_widget_set_label(d->pq_peak, NULL, N_("PQ peak"));
  dt_bauhaus_slider_set_log_curve(d->pq_peak);
  dt_bauhaus_slider_set_format(d->pq_peak, _(" nits"));
  dt_bauhaus_slider_set(d->pq_peak, dt_conf_get_int(CONFIG_PREFIX "pq_peak_nits"));
  gtk_widget_set_tooltip_text(d->pq_peak,
                              _("the brightest highlight of a PQ (HDR) export, in nits.\n"
                                "tone mappers that follow the output map their white to it.\n"
                                "HLG exports always peak at 1000 nits."));

  //  Add intent combo

  DT_BAUHAUS_COMBOBOX_NEW_FULL
    (d->intent, self, NULL, N_("intent"),
      _("• perceptual: smoothly moves out-of-gamut colors into gamut, preserving gradations,\n"
     "but distorts in-gamut colors in the process.\n"
     "note that perceptual is often a proprietary LUT that depends on the destination space."
     "\n\n"

     "• relative colorimetric: keeps luminance while reducing as little as possible\n"
     "saturation until colors fit in gamut."
     "\n\n"

     "• saturation: designed to present eye-catching business graphics\n"
     "by preserving the saturation. (not suited for photography)."
     "\n\n"

     "• absolute colorimetric: adapt white point of the image to the white point of the\n"
     "destination medium and do nothing else. mainly used when proofing colors.\n"
     "(not suited for photography)."),
      0, _intent_changed, self,
      N_("image settings"),
      N_("perceptual"),
      N_("relative colorimetric"),
      NC_("rendering intent", "saturation"),
      N_("absolute colorimetric"));

  //  Add check to control whether the style is to replace or append the current module

  DT_BAUHAUS_COMBOBOX_NEW_FULL
    (d->style_mode, self, NULL,
     N_("mode"),
     _("whether the style items are appended to the history or replacing the history"),
     dt_conf_get_bool(CONFIG_PREFIX "style_append") ? 1 : 0, _callback_bool,
     (gpointer)CONFIG_PREFIX "style_append",
     N_("replace history"), N_("append history"));

  //  Add style combo

  GtkWidget *styles_button = dtgtk_button_new_full(dtgtk_cairo_paint_styles, 0, NULL,
      &(dtgtk_button_config_t){
        .tooltip = _("select style to be applied on export"),
        .clicked_cb = G_CALLBACK(_style_popupmenu_callback),
        .clicked_data = (gpointer)d,
      });
  gtk_widget_set_halign(styles_button,GTK_ALIGN_END);
//  dt_gui_add_help_link(styles, "bottom_panel_styles");
  GtkBox *style_box = (GtkBox*)gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0);
  gtk_widget_set_tooltip_text(GTK_WIDGET(style_box), _("temporary style to use while exporting"));
  GtkWidget *styles_label = gtk_label_new(_("style"));
  gtk_box_pack_start(style_box, styles_label, FALSE, FALSE, 0);
  GtkWidget *current_style = gtk_label_new("");
  gtk_widget_set_halign(current_style,GTK_ALIGN_END);
  gtk_label_set_justify(GTK_LABEL(current_style), GTK_JUSTIFY_RIGHT);
  gtk_label_set_ellipsize(GTK_LABEL(current_style), PANGO_ELLIPSIZE_MIDDLE);
  gtk_box_pack_start(style_box, current_style, TRUE, TRUE, 0);
  gtk_box_pack_start(style_box, styles_button, FALSE, FALSE, 0);

  d->style = GTK_WIDGET(current_style);
  d->style_name = NULL;
  const char *stored_style = dt_conf_get_string_const(CONFIG_PREFIX "style");
  _update_style_label(d, stored_style ? stored_style : "");

  //  Set callback signals
  g_signal_connect(G_OBJECT(d->profile), "value-changed",
                   G_CALLBACK(_profile_changed), (gpointer)d);
  g_signal_connect(G_OBJECT(d->pq_peak), "value-changed",
                   G_CALLBACK(_pq_peak_changed), (gpointer)d);

  // Export button
  d->export_button = GTK_BUTTON(dt_action_button_new
                                (self, NC_("actionbutton", "start export"),
                                 _export_button_clicked, self,
                                 NULL,
                                 GDK_KEY_e, GDK_CONTROL_MASK));

  self->widget = dt_gui_vbox
    (dt_ui_section_label_new(C_("section", "storage options")),
     d->storage, d->storage_extra_container,
     dt_ui_section_label_new(C_("section", "format options")),
     d->format, d->format_extra_container,
     dt_ui_section_label_new(C_("section", "global options")),
     d->dimensions_type, d->px_size, d->print_size, d->scale, d->size_in_px,
     d->upscale, d->high_quality, d->export_masks,
     d->profile, d->pq_peak, d->intent,
     style_box, d->style_mode,
     d->export_button);

  // multi-preset export
  dt_gui_new_collapsible_section(&d->cs,
                                 "plugins/lighttable/export/batch_export_expanded",
                                 _("multi-preset export"),
                                 GTK_BOX(self->widget),
                                 DT_ACTION(self));
  gtk_widget_set_tooltip_text(d->cs.expander, _("export the selected images with multiple presets"));
  GtkListStore *store = gtk_list_store_new(DT_EXPORT_BATCH_NUM_COLS,
                                           G_TYPE_BOOLEAN,
                                           G_TYPE_STRING);
  GtkTreeModel *model = GTK_TREE_MODEL(store);
  GtkWidget *view = gtk_tree_view_new_with_model(model);
  g_object_unref(model);
  gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(view), FALSE);
  gtk_tree_selection_set_mode(gtk_tree_view_get_selection
                              (GTK_TREE_VIEW(view)), GTK_SELECTION_NONE);

  GtkTreeViewColumn *column = gtk_tree_view_column_new();
  gtk_tree_view_append_column(GTK_TREE_VIEW(view), column);
  GtkCellRenderer *renderer = gtk_cell_renderer_toggle_new();
  g_signal_connect(renderer, "toggled", G_CALLBACK(_batch_export_toggled_callback), self);
  gtk_tree_view_column_pack_start(column, renderer, FALSE);
  gtk_tree_view_column_add_attribute(column, renderer, "active", DT_EXPORT_BATCH_COL_ACTIVE);

  column = gtk_tree_view_column_new();
  gtk_tree_view_append_column(GTK_TREE_VIEW(view), column);
  renderer = gtk_cell_renderer_text_new();
  gtk_tree_view_column_pack_start(column, renderer, TRUE);
  gtk_tree_view_column_add_attribute(column, renderer, "text", DT_EXPORT_BATCH_COL_NAME);

  // multi-preset export button
  d->batch_export_button = GTK_BUTTON(dt_action_button_new
                                      (self, NC_("actionbutton", "start batch export"),
                                       _batch_export_button_clicked, self,
                                       NULL, 0, 0));

  d->batch_treeview = view;
  _fill_batch_export_list(self);

  dt_gui_box_add(d->cs.container, view, d->batch_export_button);

  dt_gui_connect_click_all(d->width, _widht_mdlclick_cb, NULL, d);
  dt_gui_connect_click_all(d->height, _height_mdlclick_cb, NULL, d);
  dt_gui_connect_click_all(d->print_width, _widht_mdlclick_cb, NULL, d);
  dt_gui_connect_click_all(d->print_height, _height_mdlclick_cb, NULL, d);
  dt_gui_connect_click_all(d->scale, _scale_mdlclick_cb, NULL, d);

  // this takes care of keeping hidden widgets hidden
  gtk_widget_show_all(self->widget);
  gtk_widget_set_no_show_all(self->widget, TRUE);

  const char* setting = dt_conf_get_string_const(CONFIG_PREFIX "width");
  gtk_entry_set_text(GTK_ENTRY(d->width), setting);
  setting = dt_conf_get_string_const(CONFIG_PREFIX "height");
  gtk_entry_set_text(GTK_ENTRY(d->height), setting);

  _size_update_display(d);

  // Set storage
  setting = dt_conf_get_string_const(CONFIG_PREFIX "storage_name");
  const int storage_index =
    dt_imageio_get_index_of_storage(dt_imageio_get_storage_by_name(setting));
  dt_bauhaus_combobox_set(d->storage, storage_index);

  dt_bauhaus_combobox_set(d->intent, dt_conf_get_int(CONFIG_PREFIX "iccintent") + 1);

  // iccprofile
  const int icctype = dt_conf_get_int(CONFIG_PREFIX "icctype");
  gchar *iccfilename = dt_conf_get_string(CONFIG_PREFIX "iccprofile");
  dt_bauhaus_combobox_set(d->profile, 0);
  if(icctype != DT_COLORSPACE_NONE)
  {
    for(GList *profiles = darktable.color_profiles->profiles;
        profiles;
        profiles = g_list_next(profiles))
    {
      const dt_colorspaces_color_profile_t *pp = profiles->data;
      if(pp->out_pos > -1
         && icctype == pp->type
         && (icctype != DT_COLORSPACE_FILE || !strcmp(iccfilename, pp->filename)))
      {
        dt_bauhaus_combobox_set(d->profile, pp->out_pos + 1);
        break;
      }
    }
  }

  g_free(iccfilename);
  _update_pq_peak_visibility(d);

  // style
  // set it to none if the var is not set or the style doesn't exist anymore
  setting = dt_conf_get_string_const(CONFIG_PREFIX "style");
  const gboolean is_style_set = _is_style_set(setting);

  g_free(d->style_name);
  d->style_name = g_strdup(is_style_set ? setting : "");

  // style mode to overwrite as it was the initial behavior
  gtk_widget_set_no_show_all(d->style_mode, TRUE);
  gtk_widget_set_visible(d->style_mode, is_style_set);

  // export metadata presets
  d->metadata_export = dt_lib_export_metadata_get_conf();

  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_SELECTION_CHANGED, _image_selection_changed_callback);
  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE, _mouse_over_image_callback);
  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_COLLECTION_CHANGED, _collection_updated_callback);
  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_IMAGEIO_STORAGE_EXPORT_ENABLE, _export_enable_callback);
  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_PRESETS_CHANGED, _export_presets_changed_callback);
}

void gui_cleanup(dt_lib_module_t *self)
{
  dt_lib_export_t *d = self->data;

  for(const GList *it = darktable.imageio->plugins_storage; it; it = g_list_next(it))
  {
    dt_imageio_module_storage_t *module = it->data;
    if(module->widget)
      gtk_container_remove(GTK_CONTAINER(d->storage_extra_container), module->widget);
  }

  for(const GList *it = darktable.imageio->plugins_format; it; it = g_list_next(it))
  {
    dt_imageio_module_format_t *module = it->data;
    if(module->widget)
      gtk_container_remove(GTK_CONTAINER(d->format_extra_container), module->widget);
  }

  g_free(d->style_name);
  g_free(d->metadata_export);

  free(self->data);
  self->data = NULL;
}

// the params blob packs strings between its int32_t fields, so these sit at
// any byte offset and must not be dereferenced in place
static int32_t _read_int32(const char **buf)
{
  int32_t v;
  memcpy(&v, *buf, sizeof(v));
  *buf += sizeof(v);
  return v;
}

void init_presets(dt_lib_module_t *self)
{
  // TODO: store presets in db: dt_lib_presets_add(const char *name,
  // const char *plugin_name, const void *params, const int32_t
  // params_size)


  // I know that it is super ugly to have this inside a module, but
  // then is export not your average module since it handles the
  // params blobs of imageio libs.  - get all existing presets for
  // export from db, - extract the versions of the embedded
  // format/storage blob - check if it's up to date - if older than
  // the module -> call its legacy_params and update the preset - drop
  // presets that cannot be updated

  const int version = self->version();

  // read before updating them: the loop rewrites the rows it reads
  GList *presets = dt_presets_list_stored("export");
  for(const GList *l = presets; l; l = g_list_next(l))
  {
    const dt_presets_params_t *preset = l->data;
    const int rowid = preset->rowid;
    const int op_version = preset->op_version;
    const void *op_params = preset->op_params;
    const size_t op_params_size = preset->op_params_size;
    const char *name = preset->name;

    if(op_version != version)
    {
      // shouldn't happen, we run legacy_params on the lib level
      // before calling this
      dt_print(DT_DEBUG_ALWAYS,
               "[export_init_presets] found export preset '%s' with version %d,"
               " version %d was expected. dropping preset",
               name, op_version, version);
      dt_presets_delete_at_rowid(rowid);
    }
    else
    {
      // extract the interesting parts from the blob
      const char *buf = (const char *)op_params;

      // skip 10*int32_t: max_width, max_height, upscale, high_quality,
      // export_masks, iccintent, icctype, dimensions_type, print_dpi,
      // pq_peak_nits
      buf += 10 * sizeof(int32_t);
      // skip scale string
      buf += strlen(buf) + 1;
      // skip metadata presets string
      buf += strlen(buf) + 1;
      // next skip iccfilename
      buf += strlen(buf) + 1;

      // parse both names to '\0'
      const char *fname = buf;
      buf += strlen(fname) + 1;
      const char *sname = buf;
      buf += strlen(sname) + 1;

      // get module by name and skip if not there.
      dt_imageio_module_format_t *fmod = dt_imageio_get_format_by_name(fname);
      dt_imageio_module_storage_t *smod = dt_imageio_get_storage_by_name(sname);
      if(!fmod || !smod) continue;

      // next we have fversion, sversion, fsize, ssize, fdata, sdata
      // which is the stuff that might change
      size_t copy_over_part = (void *)buf - (void *)op_params;

      const int fversion = _read_int32(&buf);
      const int sversion = _read_int32(&buf);
      const int fsize = _read_int32(&buf);
      const int ssize = _read_int32(&buf);

      const void *fdata = buf;
      buf += fsize;
      const void *sdata = buf;

      void *new_fdata = NULL;
      void *new_sdata = NULL;
      size_t new_fsize = fsize;
      size_t new_ssize = ssize;
      const int32_t new_fversion = fmod->version();
      const int32_t new_sversion = smod->version();

      int32_t cversion = fversion;
      int32_t new_version = new_fversion;
      size_t new_size = 0;
      void *data = (void *)malloc(fsize);
      memcpy(data, fdata, fsize);
      while(cversion < new_fversion)
      {
        if(fmod->legacy_params
           && (new_fdata = fmod->legacy_params(fmod, data, new_fsize,
                                               cversion, &new_version, &new_size))
                != NULL)
        {
          free(data);
          data = new_fdata;
          new_fsize = new_size;
          cversion = new_version;
        }
        else
        {
          goto delete_preset;
        }
      }

      cversion = sversion;
      new_version = new_sversion;
      new_size = 0;
      data = (void *)malloc(ssize);
      memcpy(data, sdata, ssize);
      while(cversion < new_sversion)
      {
        if(smod->legacy_params
           && (new_sdata = smod->legacy_params(smod, data, new_ssize,
                                               cversion, &new_version, &new_size))
           != NULL)
        {
          free(data);
          data = new_sdata;
          new_ssize = new_size;
          cversion = new_version;
        }
        else
        {
          goto delete_preset;
        }
      }

      if(new_fdata || new_sdata)
      {
        // we got an updated blob -> reassemble the parts and update the preset
        const size_t new_params_size =
          op_params_size - (fsize + ssize) + (new_fsize + new_ssize);
        void *new_params = malloc(new_params_size);
        memcpy(new_params, op_params, copy_over_part);
        // next we have fversion, sversion, fsize, ssize, fdata, sdata
        // which is the stuff that might change
        size_t pos = copy_over_part;
        memcpy((uint8_t *)new_params + pos, &new_fversion, sizeof(int32_t));
        pos += sizeof(int32_t);
        memcpy((uint8_t *)new_params + pos, &new_sversion, sizeof(int32_t));
        pos += sizeof(int32_t);
        memcpy((uint8_t *)new_params + pos, &new_fsize, sizeof(int32_t));
        pos += sizeof(int32_t);
        memcpy((uint8_t *)new_params + pos, &new_ssize, sizeof(int32_t));
        pos += sizeof(int32_t);
        if(new_fdata)
          memcpy((uint8_t *)new_params + pos, new_fdata, new_fsize);
        else
          memcpy((uint8_t *)new_params + pos, fdata, fsize);
        pos += new_fsize;
        if(new_sdata)
          memcpy((uint8_t *)new_params + pos, new_sdata, new_ssize);
        else
          memcpy((uint8_t *)new_params + pos, sdata, ssize);

        // write the updated preset back to db
        dt_print(DT_DEBUG_ALWAYS,
                 "[export_init_presets] updating export preset '%s'"
                 " from versions %d/%d to versions %d/%d",
                 name, fversion, sversion, new_fversion, new_sversion);
        dt_presets_set_op_params_at_rowid(rowid, new_params, new_params_size);

        free(new_fdata);
        free(new_sdata);
        free(new_params);
      }

      continue;

    delete_preset:
      free(new_fdata);
      free(new_sdata);
      dt_print(DT_DEBUG_ALWAYS,
               "[export_init_presets] export preset '%s' can't be updated"
               " from versions %d/%d to versions %d/%d. dropping preset",
               name, fversion, sversion, new_fversion, new_sversion);
      dt_presets_delete_at_rowid(rowid);
    }
  }
  g_list_free_full(presets, dt_presets_params_free);
}

void *legacy_params(dt_lib_module_t *self,
                    const void *const old_params,
                    const size_t old_params_size,
                    const int old_version,
                    int *new_version,
                    size_t *new_size)
{
  if(old_version == 1)
  {
    // add version of format & storage to params
    const size_t new_params_size = old_params_size + 2 * sizeof(int32_t);
    void *new_params = malloc(new_params_size);

    const char *buf = (const char *)old_params;

    // skip 3*int32_t: max_width, max_height and iccintent
    buf += 3 * sizeof(int32_t);
    // next skip iccprofile
    buf += strlen(buf) + 1;

    // parse both names to '\0'
    const char *fname = buf;
    buf += strlen(fname) + 1;
    const char *sname = buf;
    buf += strlen(sname) + 1;

    // get module by name and fail if not there.
    dt_imageio_module_format_t *fmod = dt_imageio_get_format_by_name(fname);
    dt_imageio_module_storage_t *smod = dt_imageio_get_storage_by_name(sname);
    if(!fmod || !smod)
    {
      free(new_params);
      return NULL;
    }

    // now we are just behind the module/storage names and before
    // their param sizes. this is the place where we want their
    // versions copy everything until here to the new params
    size_t first_half = (void *)buf - (void *)old_params;
    memcpy(new_params, old_params, first_half);
    // add the versions. at the time this code was added all modules
    // were at version 1, except of picasa which was at 2.  every
    // newer version of the imageio modules should result in a preset
    // that is not going through this code.
    int32_t fversion = 1;
    int32_t sversion = (strcmp(sname, "picasa") == 0 ? 2 : 1);
    memcpy((uint8_t *)new_params + first_half, &fversion, sizeof(int32_t));
    memcpy((uint8_t *)new_params + first_half + sizeof(int32_t),
           &sversion, sizeof(int32_t));
    // copy the rest of the old params over
    memcpy((uint8_t *)new_params + first_half + sizeof(int32_t) * 2,
           buf, old_params_size - first_half);

    *new_size = new_params_size;
    *new_version = 2;
    return new_params;
  }
  else if(old_version == 2)
  {
    // add upscale to params
    const size_t new_params_size = old_params_size + sizeof(int32_t);
    void *new_params = calloc(1, new_params_size);

    memcpy(new_params, old_params, sizeof(int32_t) * 2);
    memcpy((uint8_t *)new_params + sizeof(int32_t) * 3,
           (uint8_t *)old_params + sizeof(int32_t) * 2,
           old_params_size - sizeof(int32_t) * 2);

    *new_size = new_params_size;
    *new_version = 3;
    return new_params;
  }
  else if(old_version == 3)
  {
    // replace iccprofile by type + filename
    // format of v3:
    //  - 4 x int32_t (max_width, max_height, upscale, iccintent)
    //  - char* (iccprofile)
    //  - rest
    // format of v4:
    //  - 5 x int32_t (max_width, max_height, upscale, iccintent, icctype)
    //  - char* (iccfilename)
    //  - old rest

    const char *buf = (const char *)old_params;

    // first get the old iccprofile to find out how big our new blob has to be
    const char *iccprofile = buf + 4 * sizeof(int32_t);

    size_t new_params_size = old_params_size - strlen(iccprofile) + sizeof(int32_t);
    int icctype;
    const char *iccfilename = "";

    if(!strcmp(iccprofile, "image"))
      icctype = DT_COLORSPACE_NONE;
    else if(!strcmp(iccprofile, "sRGB"))
      icctype = DT_COLORSPACE_SRGB;
    else if(!strcmp(iccprofile, "linear_rec709_rgb") || !strcmp(iccprofile, "linear_rgb"))
      icctype = DT_COLORSPACE_LIN_REC709;
    else if(!strcmp(iccprofile, "linear_rec2020_rgb"))
      icctype = DT_COLORSPACE_LIN_REC2020;
    else if(!strcmp(iccprofile, "adobergb"))
      icctype = DT_COLORSPACE_ADOBERGB;
    else
    {
      icctype = DT_COLORSPACE_FILE;
      iccfilename = iccprofile;
      new_params_size += strlen(iccfilename);
    }

    void *new_params = calloc(1, new_params_size);
    size_t pos = 0;
    memcpy(new_params, old_params, sizeof(int32_t) * 4);
    pos += 4 * sizeof(int32_t);
    memcpy((uint8_t *)new_params + pos, &icctype, sizeof(int32_t));
    pos += sizeof(int32_t);
    memcpy((uint8_t *)new_params + pos, iccfilename, strlen(iccfilename) + 1);
    pos += strlen(iccfilename) + 1;
    size_t old_pos = 4 * sizeof(int32_t) + strlen(iccprofile) + 1;
    memcpy((uint8_t *)new_params + pos,
           (uint8_t *)old_params + old_pos,
           old_params_size - old_pos);

    *new_size = new_params_size;
    *new_version = 4;
    return new_params;
  }
  else if(old_version == 4)
  {
    // add high_quality to params

    // format of v4:
    //  - 5 x int32_t (max_width, max_height, upscale, iccintent, icctype)
    //  - char* (iccfilename)
    //  - old rest
    // format of v5:
    //  - 6 x int32_t (max_width, max_height, upscale, high_quality, iccintent, icctype)
    //  - char* (iccfilename)
    //  - old rest

    const size_t new_params_size = old_params_size + sizeof(int32_t);
    void *new_params = calloc(1, new_params_size);

    size_t pos = 0;
    memcpy(new_params, old_params, sizeof(int32_t) * 3);
    pos += 4 * sizeof(int32_t);
    memcpy((uint8_t *)new_params + pos,
           (uint8_t *)old_params + pos - sizeof(int32_t),
           old_params_size - sizeof(int32_t) * 3);

    *new_size = new_params_size;
    *new_version = 5;
    return new_params;
  }
  else if(old_version == 5)
  {
    // add metadata preset string

    // format of v5:
    //  - 6 x int32_t (max_width, max_height, upscale, high_quality, iccintent, icctype)
    //  - char* (iccfilename)
    //  - old rest
    // format of v6:
    //  - 6 x int32_t (max_width, max_height, upscale, high_quality, iccintent, icctype)
    //  - char* (metadata_export)
    //  - char* (iccfilename)
    //  - old rest

    const gboolean omit = dt_conf_get_bool("omit_tag_hierarchy");
    gchar *flags = g_strdup_printf
      ("%x", dt_lib_export_metadata_default_flags() | (omit ? DT_META_OMIT_HIERARCHY : 0));
    const int flags_size = strlen(flags) + 1;
    const size_t new_params_size = old_params_size + flags_size;
    void *new_params = calloc(1, new_params_size);
    size_t pos = 0;
    memcpy(new_params, old_params, sizeof(int32_t) * 6);
    pos += 6 * sizeof(int32_t);
    memcpy((uint8_t *)new_params + pos, flags, flags_size);
    pos += flags_size;
    memcpy((uint8_t *)new_params + pos,
           (uint8_t *)old_params + pos - flags_size,
           old_params_size - sizeof(int32_t) * 6);

    g_free(flags);
    *new_size = new_params_size;
    *new_version = 6;
    return new_params;
  }
  else if(old_version == 6)
  {
    // add export_masks

    // format of v6:
    //  - 6 x int32_t (max_width, max_height, upscale, high_quality, iccintent, icctype)
    //  - old rest
    // format of v7:
    //  - 7 x int32_t (max_width, max_height, upscale, high_quality,
    //                 export_masks, iccintent, icctype)
    //  - old rest

    const size_t new_params_size = old_params_size + sizeof(int32_t);
    void *new_params = calloc(1, new_params_size);

    size_t pos = 0;
    memcpy(new_params, old_params, sizeof(int32_t) * 4);
    pos += 5 * sizeof(int32_t);
    memcpy((uint8_t *)new_params + pos,
           (uint8_t *)old_params + pos - sizeof(int32_t),
           old_params_size - sizeof(int32_t) * 4);

    *new_size = new_params_size;
    *new_version = 7;
    return new_params;
  }
  else if(old_version == 7)
  {
    // add dimension_type, print_dpi and scale

    // format of v7:
    //  - 7 x int32_t (max_width, max_height, upscale, high_quality,
    //                 export_masks, iccintent, icctype)
    //  - old rest
    // format of v8:
    //  - 9 x int32_t (max_width, max_height, upscale, high_quality,
    //                 export_masks, iccintent, icctype, dimensions_type, print_dpi)
    //  - char* (scale)
    //  - old rest

    const char *scale = "1.0";
    const int scale_size = strlen(scale) + 1;
    const int print_dpi = dt_confgen_get_int("plugins/lighttable/export/print_dpi", DT_DEFAULT);

    const size_t new_params_size = old_params_size
                                   + sizeof(int32_t) * 2
                                   + scale_size;
    void *new_params = calloc(1, new_params_size);

    size_t pos = 0;
    memcpy(new_params, old_params, sizeof(int32_t) * 7);
    pos += 7 * sizeof(int32_t);
    pos += sizeof(int32_t);   // dimensions_type
    memcpy((uint8_t *)new_params + pos, &print_dpi, sizeof(int32_t));
    pos += sizeof(int32_t);
    memcpy((uint8_t *)new_params + pos, scale, scale_size);
    pos += scale_size;
    memcpy((uint8_t *)new_params + pos,
           (uint8_t *)old_params + pos - sizeof(int32_t) * 2 - scale_size,
           old_params_size - sizeof(int32_t) * 7);
    *new_size = new_params_size;
    *new_version = 8;
    return new_params;
  }
  else if(old_version == 8)
  {
    // add pq_peak_nits

    // format of v8:
    //  - 9 x int32_t (max_width, max_height, upscale, high_quality,
    //                 export_masks, iccintent, icctype, dimensions_type, print_dpi)
    //  - old rest
    // format of v9:
    //  - 10 x int32_t (max_width, max_height, upscale, high_quality,
    //                  export_masks, iccintent, icctype, dimensions_type, print_dpi,
    //                  pq_peak_nits)
    //  - old rest

    if(old_params_size < sizeof(int32_t) * 9) return NULL;

    // 0: applying the preset leaves the PQ peak as it is, as before
    const int32_t pq_peak_nits = 0;

    const size_t new_params_size = old_params_size + sizeof(int32_t);
    void *new_params = calloc(1, new_params_size);

    memcpy(new_params, old_params, sizeof(int32_t) * 9);
    memcpy((uint8_t *)new_params + sizeof(int32_t) * 9, &pq_peak_nits, sizeof(int32_t));
    memcpy((uint8_t *)new_params + sizeof(int32_t) * 10,
           (uint8_t *)old_params + sizeof(int32_t) * 9,
           old_params_size - sizeof(int32_t) * 9);
    *new_size = new_params_size;
    *new_version = 9;
    return new_params;
  }

  return NULL;
}

void *get_params(dt_lib_module_t *self, int *size)
{
  dt_lib_export_t *d = self->data;
  // concat storage and format, size is max + header
  dt_imageio_module_format_t *mformat = dt_imageio_get_format();
  dt_imageio_module_storage_t *mstorage = dt_imageio_get_storage();
  if(!mformat || !mstorage) return NULL;

  // size will be only as large as needed to remove random pointers
  // from params (stored at the end).
  size_t fsize = mformat->params_size(mformat);
  dt_imageio_module_data_t *fdata = mformat->get_params(mformat);
  size_t ssize = mstorage->params_size(mstorage);
  void *sdata = mstorage->get_params(mstorage);
  const int32_t fversion = mformat->version();
  const int32_t sversion = mstorage->version();
  // we allow null pointers (plugin not ready for export in current
  // state), and just don't copy back the settings later:
  if(!sdata) ssize = 0;
  if(!fdata) fsize = 0;
  if(fdata)
  {
    // clean up format global params (need to set all bytes to
    // reliably detect which preset is active).  we happen to want to
    // set it all to 0
    memset(fdata, 0, sizeof(dt_imageio_module_data_t));
  }

  // FIXME: also the web preset has to be applied twice to be known as
  // preset! (other dimension magic going on here?)  TODO: get this
  // stuff from gui and not from conf, so it will be sanity-checked
  // (you can never delete an insane preset)?  also store icc
  // profile/intent here.
  const int32_t iccintent = dt_conf_get_int(CONFIG_PREFIX "iccintent");
  const int32_t icctype = dt_conf_get_int(CONFIG_PREFIX "icctype");
  const int32_t max_width = dt_conf_get_int(CONFIG_PREFIX "width");
  const int32_t max_height = dt_conf_get_int(CONFIG_PREFIX "height");
  const int32_t upscale = dt_conf_get_bool(CONFIG_PREFIX "upscale") ? 1 : 0;
  const int32_t dimensions_type = dt_conf_get_int(CONFIG_PREFIX "dimensions_type");
  const int32_t print_dpi = dt_conf_get_int(CONFIG_PREFIX "print_dpi");
  // 0 when the export cannot be PQ, so that applying the preset does not
  // change a PQ peak that is hidden and unused for its profile
  const int32_t pq_peak_nits =
    _export_may_be_pq(icctype) ? dt_conf_get_int(CONFIG_PREFIX "pq_peak_nits") : 0;
  gchar *scale = dt_conf_get_string(CONFIG_PREFIX "resizing_factor");
  const int32_t high_quality =
    dt_conf_get_bool(CONFIG_PREFIX "high_quality_processing") ? 1 : 0;
  const int32_t export_masks = dt_conf_get_bool(CONFIG_PREFIX "export_masks") ? 1 : 0;
  gchar *iccfilename = dt_conf_get_string(CONFIG_PREFIX "iccprofile");
  gchar *style = dt_conf_get_string(CONFIG_PREFIX "style");
  const gboolean style_append = dt_conf_get_bool(CONFIG_PREFIX "style_append");
  const char *metadata_export = d->metadata_export ? d->metadata_export : "";

  if(fdata)
  {
    g_strlcpy(fdata->style, style, sizeof(fdata->style));
    fdata->style_append = style_append;
  }

  if(icctype != DT_COLORSPACE_FILE)
  {
    g_free(iccfilename);
    iccfilename = NULL;
  }

  if(!iccfilename) iccfilename = g_strdup("");

  const char *fname = mformat->plugin_name;
  const char *sname = mstorage->plugin_name;
  const int32_t fname_len = strlen(fname);
  const int32_t sname_len = strlen(sname);

  *size = fname_len + 1
          + sname_len + 1
          + 4 * sizeof(int32_t)
          + fsize
          + ssize
          + 10 * sizeof(int32_t)
          + strlen(scale) + 1
          + strlen(iccfilename) + 1
          + strlen(metadata_export) + 1;

  //??? WARNING: Any change here must be also done on get_params AND init_presets
  //             if some parameters are added before fname & sname
  char *params = (char *)calloc(1, *size);
  int pos = 0;
  memcpy(params + pos, &max_width, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &max_height, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &upscale, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &high_quality, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &export_masks, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &iccintent, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &icctype, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &dimensions_type, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &print_dpi, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &pq_peak_nits, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, scale, strlen(scale) + 1);
  pos += strlen(scale) + 1;
  memcpy(params + pos, metadata_export, strlen(metadata_export) + 1);
  pos += strlen(metadata_export) + 1;
  memcpy(params + pos, iccfilename, strlen(iccfilename) + 1);
  pos += strlen(iccfilename) + 1;
  memcpy(params + pos, fname, fname_len + 1);
  pos += fname_len + 1;
  memcpy(params + pos, sname, sname_len + 1);
  pos += sname_len + 1;
  memcpy(params + pos, &fversion, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &sversion, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &fsize, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params + pos, &ssize, sizeof(int32_t));
  pos += sizeof(int32_t);
  if(fdata != NULL) // otherwise fsize == 0, but clang doesn't like it ...
  {
    memcpy(params + pos, fdata, fsize);
    pos += fsize;
  }
  if(sdata != NULL) // see above
  {
    memcpy(params + pos, sdata, ssize);
    pos += ssize;
  }
  g_assert(pos == *size);

  g_free(scale);
  g_free(iccfilename);
  g_free(style);

  if(fdata) mformat->free_params(mformat, fdata);
  if(sdata) mstorage->free_params(mstorage, sdata);
  return params;
}

int set_params(dt_lib_module_t *self,
               const void *params,
               const int size)
{
  dt_lib_export_t *d = self->data;
  // apply these stored presets again (parse blob)
  const char *buf = (const char *)params;

  //??? WARNING: Any change here must be also done on get_params AND init_presets
  //             if some parameters are added before fname & sname
  const int max_width = _read_int32(&buf);
  const int max_height = _read_int32(&buf);
  const int upscale = _read_int32(&buf);
  const int high_quality = _read_int32(&buf);
  const int export_masks = _read_int32(&buf);
  const int iccintent = _read_int32(&buf);
  const int icctype = _read_int32(&buf);
  const int dimensions_type = _read_int32(&buf);
  const int print_dpi = _read_int32(&buf);
  const int pq_peak_nits = _read_int32(&buf);
  const char *scale = buf;
  buf += strlen(scale) + 1;
  const char *metadata_export = buf;
  buf += strlen(metadata_export) + 1;
  g_free(d->metadata_export);
  d->metadata_export = g_strdup(metadata_export);
  dt_lib_export_metadata_set_conf(d->metadata_export);
  const char *iccfilename = buf;
  buf += strlen(iccfilename) + 1;

  // reverse these by setting the gui, not the conf vars!
  dt_bauhaus_combobox_set(d->intent, iccintent + 1);

  dt_bauhaus_combobox_set(d->profile, 0);
  if(icctype != DT_COLORSPACE_NONE)
  {
    for(GList *iter = darktable.color_profiles->profiles;
        iter;
        iter = g_list_next(iter))
    {
      const dt_colorspaces_color_profile_t *pp = iter->data;
      if(pp->out_pos > -1
         && icctype == pp->type
         && (icctype != DT_COLORSPACE_FILE || !strcmp(iccfilename, pp->filename)))
      {
        dt_bauhaus_combobox_set(d->profile, pp->out_pos + 1);
        break;
      }
    }
  }

  // parse both names to '\0'
  const char *fname = buf;
  buf += strlen(fname) + 1;
  const char *sname = buf;
  buf += strlen(sname) + 1;

  // get module by name and fail if not there.
  dt_imageio_module_format_t *fmod = dt_imageio_get_format_by_name(fname);
  dt_imageio_module_storage_t *smod = dt_imageio_get_storage_by_name(sname);
  if(!fmod || !smod) return 1;

  const int32_t fversion = _read_int32(&buf);
  const int32_t sversion = _read_int32(&buf);

  const int fsize = _read_int32(&buf);
  const int ssize = _read_int32(&buf);

  if(size
     != strlen(fname) + 1
        + strlen(sname) + 1
        + 4 * sizeof(int32_t)
        + fsize
        + ssize
        + 10 * sizeof(int32_t)
        + strlen(scale) + 1
        + strlen(metadata_export) + 1
        + strlen(iccfilename) + 1)
    return 1;
  if(fversion != fmod->version() || sversion != smod->version()) return 1;

  // the format and storage modules read their structs in place, so hand
  // them aligned copies rather than pointers into the blob
  dt_imageio_module_data_t *fdata = NULL;
  if(fsize)
  {
    fdata = malloc(fsize);
    memcpy(fdata, buf, fsize);
    if(fsize >= (int)sizeof(dt_imageio_module_data_t))
    {
      _update_style_label(d, fdata->style);
      dt_bauhaus_combobox_set(d->style_mode, fdata->style_append ? 1 : 0);
    }
  }

  buf += fsize;
  void *sdata = NULL;
  if(ssize)
  {
    sdata = malloc(ssize);
    memcpy(sdata, buf, ssize);
  }

  // switch modules
  set_storage_by_name(d, sname);
  set_format_by_name(d, fname);

  // set dimensions after switching, to have new range ready.
  _set_dimensions(d, max_width, max_height, print_dpi, scale);
  dt_bauhaus_combobox_set(d->upscale, upscale ? 1 : 0);
  dt_bauhaus_combobox_set(d->high_quality, high_quality ? 1 : 0);
  dt_bauhaus_combobox_set(d->export_masks, export_masks ? 1 : 0);
  dt_bauhaus_combobox_set(d->dimensions_type, dimensions_type);
  if(pq_peak_nits > 0) dt_bauhaus_slider_set(d->pq_peak, pq_peak_nits);
  _size_update_display(d);

  // propagate to modules
  int res = 0;
  if(ssize)
    res += smod->set_params(smod, sdata, ssize);
  if(fsize)
    res += fmod->set_params(fmod, fdata, fsize);
  free(fdata);
  free(sdata);
  return res;
}

// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on