/*
 *  $Id: openexr.cc 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2011-2026 David Necas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

/**
 * [FILE-MAGIC-USERGUIDE]
 * OpenEXR images
 * .exr
 * Read Export
 **/

/**
 * [FILE-MAGIC-MISSING]
 * Avoding clash with a standard file format.
 **/

#include "config.h"
#include <string.h>
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <glib/gi18n-lib.h>
#include <glib/gstdio.h>
#include <gtk/gtk.h>

#ifdef HAVE_EXR
#include <exception>
#include <OpenEXRConfig.h>
#include <half.h>
#include <ImfNamespace.h>
#include <ImfChannelList.h>
#include <ImfOutputFile.h>
#include <ImfInputFile.h>
#include <ImfRgbaFile.h>
#include <ImfFrameBuffer.h>
#include <ImfArray.h>
#include <ImfDoubleAttribute.h>
#include <ImfStringAttribute.h>
#else
#  define HALF_MIN 5.96046448e-08
#  define HALF_NRM_MIN 6.10351562e-05
#  define HALF_MAX 65504.0
#  define half guint16
#endif

#include <gwy.h>

#include "err.h"
#include "image-keys.h"

#define EXR_EXTENSION ".exr"
#define EXR_MAGIC "\x76\x2f\x31\x01"
#define EXR_MAGIC_SIZE sizeof(EXR_MAGIC)-1

enum {
    PREVIEW_SIZE = 320,
    RESPONSE_USE_SUGGESTED = GWY_RESPONSE_USER,
};

enum {
    PIXMAP_HAS_COLOURS = 1u << 0,   /* Unset if there are RGB, but all are identical. */
    PIXMAP_HAS_ALPHA   = 1u << 1,
};

enum {
    PARAM_XREAL,
    PARAM_YREAL,
    PARAM_ZREAL,
    PARAM_XYMEASUREEQ,
    PARAM_SIZE_IN_PIXELS,
    PARAM_XYUNIT,
    PARAM_ZUNIT,
    PARAM_AS_VOLUME,

    WIDGET_IMAGE_INFO,
};

enum {
    PARAM_BIT_DEPTH,
    PARAM_ZSCALE,

    WIDGET_RANGES,
    BUTTON_USE_SUGGESTED,
};

typedef enum {
    /* Used with common image formats supporting 16bit greyscale */
    GWY_BIT_DEPTH_INT16 = 16,
    /* Used with HDR greyscale images */
    GWY_BIT_DEPTH_HALF  = 17,
    GWY_BIT_DEPTH_INT32 = 32,
    GWY_BIT_DEPTH_FLOAT = 33,
} GwyBitDepth;

typedef enum {
    BAD_FILE    = 0,
    PLAIN_IMAGE = 1,
    GWY_META    = 2,
} DetectionResult;

typedef struct {
    GwyParams *params;
    GwyField *field;
    /* Cached input data properties */
    gdouble pmin;
    gdouble pmax;
    gdouble pcentre;
    gdouble min;
    gdouble max;
} ExportArgs;

typedef struct {
    ExportArgs *args;
    GtkWidget *dialog;
    GwyResults *results;
    GwyParamTable *table;
} ExportGUI;

typedef struct {
    GwyParams *params;
    GwyField *field;
    /* Cached input properties. */
    gchar *channels;
    GwyRawDataType datatype;
    guint npages;
    gboolean pages_are_uniform;
} ImportArgs;

typedef struct {
    ImportArgs *args;
    GtkWidget *dialog;
    GwyParamTable *table_lateral;
    GwyParamTable *table_values;
} ImportGUI;

static gboolean         module_register        (void);
static gint             exr_detect             (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static GwyFile*         exr_load               (const gchar *filename,
                                                GwyRunModeFlags mode,
                                                GError **error);
static GwyFile*         exr_load_image         (const gchar *filename,
                                                GwyRunModeFlags mode,
                                                GSList **objects,
                                                GSList **buffers,
                                                GError **error);
static GwyDialogOutcome run_import_gui         (ImportArgs *args,
                                                const gchar *name);
static void             field_props_from_params(GwyParams *params,
                                                gdouble *xreal,
                                                gdouble *yreal,
                                                GwyUnit **xyunit,
                                                gdouble *zmax,
                                                GwyUnit **zunit);
static void             import_param_changed   (ImportGUI *gui,
                                                gint id);
static gboolean         exr_export             (GwyFile *data,
                                                const gchar *filename,
                                                GwyRunModeFlags mode,
                                                GError **error);
static GwyDialogOutcome run_export_gui         (ExportArgs *args,
                                                const gchar *name);
static void             export_param_changed   (ExportGUI *gui,
                                                gint id);
static void             export_dialog_response (ExportGUI *gui,
                                                gint id);
static void             exr_write_image        (GwyField *field,
                                                gchar *imagedata,
                                                const gchar *filename,
                                                const gchar *title,
                                                GwyBitDepth bit_depth,
                                                gdouble zscale);
static gdouble          suggest_zscale         (GwyBitDepth bit_depth,
                                                gdouble pmin,
                                                gdouble pmax,
                                                gdouble pcentre);
static void             representable_range    (GwyBitDepth bit_depth,
                                                gdouble zscale,
                                                gdouble *min,
                                                gdouble *max);
static void             find_range             (GwyField *field,
                                                gdouble *fmin,
                                                gdouble *fmax,
                                                gdouble *pmin,
                                                gdouble *pmax,
                                                gdouble *pcentre);
static gchar*           create_image_data      (GwyField *field,
                                                GwyBitDepth bit_depth,
                                                gdouble zscale,
                                                gdouble zmin,
                                                gdouble zmax);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports and exports OpenEXR images."),
    "Yeti <yeti@gwyddion.net>",
    "1.0",
    "David Nečas (Yeti)",
    "2026",
};

GWY_MODULE_QUERY2(module_info, openexr)

static gboolean
module_register(void)
{
    gwy_file_func_register("openexr",
                           N_("OpenEXR images (.exr)"),
                           &exr_detect, &exr_load, NULL, &exr_export);

    return TRUE;
}

static GwyParamDef*
define_import_params(void)
{
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    /* Share keys with the pixmap module so that people get the same paramters for low-depth and high-depth images. */
    gwy_param_def_set_function_name(paramdef, "pixmap");
    gwy_param_def_add_double(paramdef, PARAM_XREAL, "xreal", _("_Horizontal size"), G_MINDOUBLE, G_MAXDOUBLE, 1.0);
    gwy_param_def_add_double(paramdef, PARAM_YREAL, "yreal", _("_Vertical size"), G_MINDOUBLE, G_MAXDOUBLE, 1.0);
    gwy_param_def_add_double(paramdef, PARAM_ZREAL, "zreal", _("_Z-scale (per sample unit)"),
                             -G_MAXDOUBLE, G_MAXDOUBLE, 1.0);
    gwy_param_def_add_boolean(paramdef, PARAM_XYMEASUREEQ, "xymeasureeq", _("_Square pixels"), TRUE);
    gwy_param_def_add_boolean(paramdef, PARAM_SIZE_IN_PIXELS, "size_in_pixels", _("Just use _pixels"), FALSE);
    gwy_param_def_add_unit(paramdef, PARAM_XYUNIT, "xyunit", _("_Dimensions unit"), NULL);
    gwy_param_def_add_unit(paramdef, PARAM_ZUNIT, "zunit", _("_Value unit"), NULL);
    gwy_param_def_add_boolean(paramdef, PARAM_AS_VOLUME, "as_volume", _("Import as _volume data"), TRUE);
    return paramdef;
}

static GwyParamDef*
define_export_params(void)
{
    static const GwyEnum bit_depths[] = {
        { _("Half (16bit float)"), GWY_BIT_DEPTH_HALF,  },
        { _("Float (32bit)"),      GWY_BIT_DEPTH_FLOAT, },
        { _("Integer (32bit)"),    GWY_BIT_DEPTH_INT32, },
    };
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, gwy_file_func_current());
    gwy_param_def_add_gwyenum(paramdef, PARAM_BIT_DEPTH, "bit_depth", _("_Data format"),
                              bit_depths, G_N_ELEMENTS(bit_depths), GWY_BIT_DEPTH_HALF);
    gwy_param_def_add_double(paramdef, PARAM_ZSCALE, "zscale", _("_Z-scale"), G_MINDOUBLE, G_MAXDOUBLE, 1.0);
    return paramdef;
}

static gint
exr_detect(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    gint score = 0;

    if (only_name)
        return g_str_has_suffix(fileinfo->name_lowercase, EXR_EXTENSION) ? 20 : 0;

    if (fileinfo->buffer_len > EXR_MAGIC_SIZE && memcmp(fileinfo->head, EXR_MAGIC, EXR_MAGIC_SIZE) == 0)
        score = 100;

    return score;
}

static gboolean
exr_export(GwyFile *file,
           const gchar *filename,
           GwyRunModeFlags mode,
           GError **error)
{
    ExportArgs args;
    gboolean ok = FALSE;
    GwyDialogOutcome outcome;
    gchar *title = NULL, *imagedata = NULL;
    GwyBitDepth bit_depth;
    gdouble zscale;
    gint id;

    gwy_clear(&args, 1);

    gwy_data_browser_get_current(GWY_APP_FILE, &file,
                                 GWY_APP_FIELD, &args.field,
                                 GWY_APP_FIELD_ID, &id,
                                 0);
    if (!args.field) {
        err_NO_CHANNEL_EXPORT(error);
        return FALSE;
    }

    args.params = gwy_params_new_from_settings(define_export_params());
    find_range(args.field, &args.min, &args.max, &args.pmin, &args.pmax, &args.pcentre);

    if (mode == GWY_RUN_INTERACTIVE) {
        outcome = run_export_gui(&args, "OpenEXR");
        gwy_params_save_to_settings(args.params);
        if (outcome == GWY_DIALOG_CANCEL) {
            err_CANCELLED(error);
            goto end;
        }
    }

    bit_depth = (GwyBitDepth)gwy_params_get_enum(args.params, PARAM_BIT_DEPTH);
    zscale = gwy_params_get_double(args.params, PARAM_ZSCALE);
    imagedata = create_image_data(args.field, bit_depth, zscale, args.min, args.max);
    title = gwy_file_get_display_title(file, GWY_FILE_IMAGE, id);

    try {
        exr_write_image(args.field, imagedata, filename, title, bit_depth, zscale);
        ok = TRUE;
    }
    catch (const std::exception &exc) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_IO,
                    _("EXR image writing failed with libImf error: %s"), exc.what());
    }

end:
    g_object_unref(args.params);
    g_free(imagedata);
    g_free(title);

    return ok;
}

static GwyDialogOutcome
run_export_gui(ExportArgs *args, const gchar *name)
{
    ExportGUI gui;
    GwyParamTable *table;
    GwyDialog *dialog;
    GwyDialogOutcome outcome;
    GwyResults *results;
    gchar *title;

    gwy_clear(&gui, 1);
    gui.args = args;

    /* TRANSLATORS: Dialog title; %s is PNG, TIFF, ... */
    title = g_strdup_printf(_("Export %s"), name);
    gui.dialog = gwy_dialog_new(title);
    g_free(title);
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    results = gui.results = gwy_results_new();
    gwy_results_add_format(results, "datarange", N_("Data range"), FALSE, "%{zmin}v – %{zmax}v", NULL);
    gwy_results_add_format(results, "reprange", N_("Representable range"), FALSE, "%{rmin}v – %{rmax}v", NULL);
    gwy_results_add_value_plain(results, "suggscale", N_("Suggested Z-scale"));
    /* The rest is filled in param-update. */
    gwy_results_fill_format(results, "datarange", "zmin", args->min, "zmax", args->max, NULL);

    table = gui.table = gwy_param_table_new(args->params);
    gwy_param_table_append_radio(table, PARAM_BIT_DEPTH);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_entry(table, PARAM_ZSCALE);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_results(table, WIDGET_RANGES, results, "datarange", "reprange", "suggscale", NULL);
    gwy_param_table_append_button(table, BUTTON_USE_SUGGESTED, -1, RESPONSE_USE_SUGGESTED, _("_Use Suggested"));
    gwy_dialog_add_content(dialog, gwy_param_table_widget(table), TRUE, TRUE, 0);

    g_signal_connect_swapped(table, "param-changed", G_CALLBACK(export_param_changed), &gui);
    g_signal_connect_swapped(dialog, "response", G_CALLBACK(export_dialog_response), &gui);

    outcome = gwy_dialog_run(dialog);

    g_object_unref(gui.results);

    return outcome;
}

static void
export_param_changed(ExportGUI *gui, gint id)
{
    ExportArgs *args = gui->args;
    GwyParams *params = args->params;
    GwyParamTable *table = gui->table;
    GwyBitDepth bit_depth = (GwyBitDepth)gwy_params_get_enum(params, PARAM_BIT_DEPTH);
    gdouble zscale = gwy_params_get_double(params, PARAM_ZSCALE);
    gdouble suggscale, rmin, rmax;

    if (id < 0 || id == PARAM_BIT_DEPTH) {
        gwy_param_table_set_sensitive(table, PARAM_ZSCALE, bit_depth == GWY_BIT_DEPTH_HALF);
        gwy_param_table_set_sensitive(table, WIDGET_RANGES, bit_depth == GWY_BIT_DEPTH_HALF);
        gwy_param_table_set_sensitive(table, BUTTON_USE_SUGGESTED, bit_depth == GWY_BIT_DEPTH_HALF);

        if (bit_depth != GWY_BIT_DEPTH_INT32) {
            suggscale = suggest_zscale(bit_depth, args->pmin, args->pmax, args->pcentre);
            gwy_results_fill_values(gui->results, "suggscale", suggscale, NULL);
        }
        else
            gwy_results_set_na(gui->results, "suggscale", NULL);
    }

    if (bit_depth != GWY_BIT_DEPTH_INT32) {
        representable_range(bit_depth, zscale, &rmin, &rmax);
        gwy_results_fill_format(gui->results, "reprange", "rmin", rmin, "rmax", rmax, NULL);
    }
    else
        gwy_results_set_na(gui->results, "reprange", NULL);

    gwy_param_table_results_fill(gui->table, WIDGET_RANGES);
}

static void
export_dialog_response(ExportGUI *gui, gint id)
{
    if (id == RESPONSE_USE_SUGGESTED) {
        ExportArgs *args = gui->args;
        GwyBitDepth bit_depth = (GwyBitDepth)gwy_params_get_enum(args->params, PARAM_BIT_DEPTH);
        gdouble suggscale = suggest_zscale(bit_depth, args->pmin, args->pmax, args->pcentre);
        gwy_param_table_set_double(gui->table, PARAM_ZSCALE, suggscale);
    }
}

// NB: This function raises a C++ exception instead of reporting the error via GError.  The caller must catch it.
static void
exr_write_image(GwyField *field,
                gchar *imagedata,
                const gchar *filename,
                const gchar *title,
                GwyBitDepth bit_depth,
                gdouble zscale)
{
    guint xres = gwy_field_get_xres(field);
    guint yres = gwy_field_get_yres(field);

    Imf::Header header(xres, yres);
    header.lineOrder() = Imf::INCREASING_Y;

    Imf::PixelType pixel_type;
    if (bit_depth == GWY_BIT_DEPTH_HALF)
        pixel_type = Imf::HALF;
    else if (bit_depth == GWY_BIT_DEPTH_FLOAT)
        pixel_type = Imf::FLOAT;
    else if (bit_depth == GWY_BIT_DEPTH_INT32)
        pixel_type = Imf::UINT;
    else {
        g_assert_not_reached();
    }

    gdouble v;
    v = gwy_field_get_xreal(field);
    header.insert(GWY_IMGKEY_XREAL, Imf::DoubleAttribute(v));
    v = gwy_field_get_xreal(field);
    header.insert(GWY_IMGKEY_YREAL, Imf::DoubleAttribute(v));
    if (bit_depth == GWY_BIT_DEPTH_INT32) {
        gdouble zmin, zmax;
        gwy_field_min_max(field, &zmin, &zmax);
        header.insert(GWY_IMGKEY_ZMIN, Imf::DoubleAttribute(zmin));
        header.insert(GWY_IMGKEY_ZMAX, Imf::DoubleAttribute(zmax));
    }
    else if (zscale != 1.0)
        header.insert(GWY_IMGKEY_ZSCALE, Imf::DoubleAttribute(zscale));
    if ((v = gwy_field_get_xoffset(field)))
        header.insert(GWY_IMGKEY_XOFFSET, Imf::DoubleAttribute(v));
    if ((v = gwy_field_get_yoffset(field)))
        header.insert(GWY_IMGKEY_YOFFSET, Imf::DoubleAttribute(v));

    header.insert(GWY_IMGKEY_TITLE, Imf::StringAttribute(title));
    header.insert("Software", Imf::StringAttribute("Gwyddion"));

    gchar *s;
    s = gwy_unit_get_string(gwy_field_get_unit_xy(field), GWY_UNIT_FORMAT_PLAIN);
    header.insert(GWY_IMGKEY_XYUNIT, Imf::StringAttribute(s));
    g_free(s);

    s = gwy_unit_get_string(gwy_field_get_unit_z(field), GWY_UNIT_FORMAT_PLAIN);
    header.insert(GWY_IMGKEY_ZUNIT, Imf::StringAttribute(s));
    g_free(s);

    header.channels().insert("Y", Imf::Channel(pixel_type));

    Imf::OutputFile outfile(filename, header);
    Imf::FrameBuffer framebuffer;

    if (pixel_type == Imf::HALF)
        framebuffer.insert("Y", Imf::Slice(pixel_type, imagedata, sizeof(half), xres*sizeof(half)));
    else if (pixel_type == Imf::FLOAT)
        framebuffer.insert("Y", Imf::Slice(pixel_type, imagedata, sizeof(float), xres*sizeof(float)));
    else if (pixel_type == Imf::UINT)
        framebuffer.insert("Y", Imf::Slice(pixel_type, imagedata, sizeof(guint32), xres*sizeof(guint32)));
    else {
        g_assert_not_reached();
    }

    outfile.setFrameBuffer(framebuffer);
    outfile.writePixels(yres);
}

static GwyFile*
exr_load(const gchar *filename,
         GwyRunModeFlags mode,
         GError **error)
{
    GwyFile *file = NULL;
    GSList *objects = NULL, *buffers = NULL;

    try {
        file = exr_load_image(filename, mode, &objects, &buffers, error);
    }
    catch (const std::exception &exc) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_IO,
                    _("EXR image loading failed with libImf error: %s"), exc.what());
    }

    for (GSList *l = buffers; l; l = g_slist_next(l)) {
        g_free(l->data);
    }
    g_slist_free(buffers);

    for (GSList *l = objects; l; l = g_slist_next(l)) {
        g_object_unref(l->data);
    }
    g_slist_free(objects);

    return file;
}

static const Imf::DoubleAttribute*
exr_get_double_attr(const Imf::InputFile &infile,
                    const gchar *name)
{
    const Imf::DoubleAttribute *attr = infile.header().findTypedAttribute<Imf::DoubleAttribute>(name);

    if (attr) {
        gwy_debug("%s = %g", name, attr->value());
    }
    return attr;
}

static const Imf::StringAttribute*
exr_get_string_attr(const Imf::InputFile &infile,
                    const gchar *name)
{
    const Imf::StringAttribute *attr = infile.header().findTypedAttribute<Imf::StringAttribute>(name);

    if (attr) {
        gwy_debug("%s = <%s>", name, attr->value().c_str());
    }
    return attr;
}

static inline GwyRawDataType
exr_type_to_gwy_type(Imf::PixelType type)
{
    if (type == Imf::UINT)
        return GWY_RAW_DATA_UINT32;
    else if (type == Imf::HALF)
        return GWY_RAW_DATA_HALF;
    else if (type == Imf::FLOAT)
        return GWY_RAW_DATA_FLOAT;

    g_return_val_if_reached((GwyRawDataType)0);
}

static gchar*
exr_format_channel_names(const Imf::ChannelList &channels)
{
    GString *str = g_string_new(NULL);

    for (Imf::ChannelList::ConstIterator i = channels.begin(); i != channels.end(); ++i) {
        if (str->len)
            g_string_append(str, ", ");
        g_string_append(str, i.name());
    }
    return g_string_free(str, FALSE);
}

// NB: This function either raises a C++ exception or it reports the error via via GError.  In the latter case the
// return value is NULL.
static GwyFile*
exr_load_image(const gchar *filename,
               GwyRunModeFlags mode,
               GSList **objects,
               GSList **buffers,
               GError **error)
{
    GwyFile *file = NULL;
    Imf::InputFile infile(filename);

    Imath::Box2i dw = infile.header().dataWindow();
    gint width = dw.max.x - dw.min.x + 1;
    gint height = dw.max.y - dw.min.y + 1;
    gwy_debug("width: %d, height: %d", width, height);

    const Imf::DoubleAttribute
        *xreal_attr = exr_get_double_attr(infile, GWY_IMGKEY_XREAL),
        *yreal_attr = exr_get_double_attr(infile, GWY_IMGKEY_YREAL),
        *xoff_attr = exr_get_double_attr(infile, GWY_IMGKEY_XOFFSET),
        *yoff_attr = exr_get_double_attr(infile, GWY_IMGKEY_YOFFSET),
        *zscale_attr = exr_get_double_attr(infile, GWY_IMGKEY_ZSCALE),
        *zmin_attr = exr_get_double_attr(infile, GWY_IMGKEY_ZMIN),
        *zmax_attr = exr_get_double_attr(infile, GWY_IMGKEY_ZMAX);
    const Imf::StringAttribute
        *xyunit_attr = exr_get_string_attr(infile, GWY_IMGKEY_XYUNIT),
        *zunit_attr = exr_get_string_attr(infile, GWY_IMGKEY_ZUNIT),
        *title_attr = exr_get_string_attr(infile, GWY_IMGKEY_TITLE);

    const Imf::ChannelList &channels = infile.header().channels();
    Imf::FrameBuffer framebuffer;
    guint nchannels = 0;
    ImportArgs args;

    gwy_clear(&args, 1);
    for (Imf::ChannelList::ConstIterator i = channels.begin(); i != channels.end(); ++i, ++nchannels) {
        const Imf::Channel &channel = i.channel();
        guint xs = channel.xSampling, ys = channel.ySampling;
        // Round up; it's better to allocate too large buffer than too small.
        guint xres = (width + xs-1)/xs, yres = (height + ys-1)/ys;
        gwy_debug("channel: <%s>, type: %u", i.name(), (guint)channel.type);
        gwy_debug("samplings: %u, %u", xs, ys);

        if (channel.type == Imf::UINT) {
            guint32 *buffer = g_new(guint32, xres*yres);
            char *base = (char*)(buffer - dw.min.x - xres*dw.min.y);
            *buffers = g_slist_append(*buffers, (gpointer)buffer);
            framebuffer.insert(i.name(),
                               Imf::Slice(Imf::UINT, base, sizeof(buffer[0]), xres*sizeof(buffer[0]), xs, ys));
            args.datatype = GWY_RAW_DATA_UINT32;
        }
        else if (channel.type == Imf::HALF) {
            half *buffer = g_new(half, xres*yres);
            char *base = (char*)(buffer - dw.min.x - xres*dw.min.y);
            *buffers = g_slist_append(*buffers, (gpointer)buffer);
            framebuffer.insert(i.name(),
                               Imf::Slice(Imf::HALF, base, sizeof(buffer[0]), xres*sizeof(buffer[0]), xs, ys));
            args.datatype = GWY_RAW_DATA_HALF;
        }
        else if (channel.type == Imf::FLOAT) {
            gfloat *buffer = g_new(gfloat, xres*yres);
            char *base = (char*)(buffer - dw.min.x - xres*dw.min.y);
            *buffers = g_slist_append(*buffers, (gpointer)buffer);
            framebuffer.insert(i.name(),
                               Imf::Slice(Imf::FLOAT, base, sizeof(buffer[0]), xres*sizeof(buffer[0]), xs, ys));
            args.datatype = GWY_RAW_DATA_FLOAT;
        }
        else {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("OpenEXR data type %u is invalid or unsupported."), (guint)channel.type);
            return NULL;
        }

    }

    if (!nchannels) {
        err_NO_DATA(error);
        return NULL;
    }

    infile.setFrameBuffer(framebuffer);
    infile.readPixels(dw.min.y, dw.max.y);

    gdouble xreal, yreal, xoff = 0.0, yoff = 0.0;
    gdouble q = 1.0, z0 = 0.0;
    GwyUnit *unitxy = NULL, *unitz = NULL;
    gboolean have_both_real_attr = xreal_attr && yreal_attr;

    args.params = gwy_params_new_from_settings(define_import_params());
    if (have_both_real_attr) {
        gwy_debug("Found Gwyddion image keys, using for direct import.");

        xreal = xreal_attr->value();
        yreal = yreal_attr->value();
        if (xoff_attr)
            xoff = xoff_attr->value();
        if (yoff_attr)
            yoff = yoff_attr->value();

        /* We set zmin and zmax only for UINT data type. */
        if (zmin_attr && zmax_attr) {
            z0 = zmin_attr->value();
            q = (zmax_attr->value() - z0)/(G_MAXUINT32 + 0.999);
        }
        else if (zmax_attr) {
            q = zmax_attr->value()/(G_MAXUINT32 + 0.999);
        }
        else if (zscale_attr) {
            q = zscale_attr->value();
        }

        gint power10;

        if (xyunit_attr) {
            unitxy = gwy_unit_new_parse(xyunit_attr->value().c_str(), &power10);
            *objects = g_slist_prepend(*objects, (gpointer)unitxy);
            xreal *= gwy_exp10(power10);
            yreal *= gwy_exp10(power10);
            xoff *= gwy_exp10(power10);
            yoff *= gwy_exp10(power10);
        }

        if (zunit_attr) {
            unitz = gwy_unit_new_parse(zunit_attr->value().c_str(), &power10);
            *objects = g_slist_prepend(*objects, (gpointer)unitz);
            q *= gwy_exp10(power10);
            z0 *= gwy_exp10(power10);
        }
    }
    else if (mode == GWY_RUN_INTERACTIVE) {
        // XXX: This is sort of completely wrong as each channel can have a different scaling.  But presenting half
        // a dozen physical scale choosers is hardly better.  Just import the data and let the user sort it out.  For
        // plain images, only lateral measurements will probably make sense anyway.
        gwy_debug("Manual import is necessary.");

        Imf::ChannelList::ConstIterator first = channels.begin();
        const Imf::Channel &channel = first.channel();
        guint xs = channel.xSampling, ys = channel.ySampling;
        guint xres = (width + xs-1)/xs, yres = (height + ys-1)/ys;
        GwyRawDataType rawdatatype = exr_type_to_gwy_type(channel.type);

        args.field = gwy_field_new(xres, yres, 1.0, 1.0, FALSE);
        gdouble *d = gwy_field_get_data(args.field);
        gwy_convert_raw_data((*buffers)->data, xres*yres, 1, rawdatatype, GWY_BYTE_ORDER_NATIVE, d, 1.0, 0.0);

        gchar *channel_names = exr_format_channel_names(channels);
        args.npages = 1;
        args.pages_are_uniform = TRUE;
        args.channels = channel_names;

        GwyDialogOutcome outcome = run_import_gui(&args, "OpenEXR");
        gwy_params_save_to_settings(args.params);
        g_clear_object(&args.field);
        GWY_FREE(args.channels);
        if (outcome == GWY_DIALOG_CANCEL) {
            g_object_unref(args.params);
            err_CANCELLED(error);
            return NULL;
        }
    }

    if (!have_both_real_attr)
        field_props_from_params(args.params, &xreal, &yreal, &unitxy, &q, &unitz);
    g_clear_object(&args.params);

    file = gwy_file_new_in_construction();
    *objects = g_slist_prepend(*objects, (gpointer)file);

    GSList *l = *buffers;
    gint id = 0;

    for (Imf::ChannelList::ConstIterator i = channels.begin(); i != channels.end(); ++i, ++id, l = g_slist_next(l)) {
        const Imf::Channel &channel = i.channel();
        guint xs = channel.xSampling, ys = channel.ySampling;
        guint xres = (width + xs-1)/xs, yres = (height + ys-1)/ys;
        GwyRawDataType rawdatatype = exr_type_to_gwy_type(channel.type);

        g_assert(l);

        GwyField *dfield = gwy_field_new(xres, yres, xreal, yreal, FALSE);
        gdouble *d = gwy_field_get_data(dfield);
        *objects = g_slist_prepend(*objects, (gpointer)dfield);
        gwy_convert_raw_data(l->data, xres*yres, 1, rawdatatype, GWY_BYTE_ORDER_NATIVE, d, q, z0);
        GwyNield *mask = gwy_mask_nans_in_field(dfield, TRUE);

        if (unitxy)
            gwy_unit_assign(gwy_field_get_unit_xy(dfield), unitxy);
        if (unitz)
            gwy_unit_assign(gwy_field_get_unit_z(dfield), unitz);

        gwy_file_set_image(file, id, dfield);

        if (mask)
            gwy_file_pass_image_mask(file, id, mask);

        gchar *title;
        if (title_attr && nchannels > 1)
            title = g_strconcat(title_attr->value().c_str(), " ", i.name(), NULL);
        else if (title_attr)
            title = g_strdup(title_attr->value().c_str());
        else
            title = g_strdup(i.name());
        gwy_file_pass_title(file, GWY_FILE_IMAGE, id, title);

        gwy_log_add_import(file, GWY_FILE_IMAGE, id, "openexr", filename);
    }

    // We have file on the unref-me-list so another reference must be taken to retain it.
    g_object_ref(file);

    return file;
}

static gdouble
suggest_zscale(GwyBitDepth bit_depth,
               gdouble pmin, gdouble pmax, gdouble pcentre)
{
    if (bit_depth == GWY_BIT_DEPTH_FLOAT)
        return 1.0;

    g_return_val_if_fail(bit_depth == GWY_BIT_DEPTH_HALF, 1.0);

    // Range OK as-is
    if (pmin >= HALF_NRM_MIN && pmax <= HALF_MAX)
        return 1.0;

    // Range OK if scaled
    if (pmax/pmin < (double)HALF_MAX/HALF_NRM_MIN)
        return sqrt(pmax/HALF_MAX * pmin/HALF_NRM_MIN);

    // Range not OK, may need a bit more sopistication here...
    return pcentre;
}

static void
representable_range(GwyBitDepth bit_depth, gdouble zscale,
                    gdouble *min, gdouble *max)
{
    if (bit_depth == GWY_BIT_DEPTH_FLOAT) {
        *min = zscale*G_MINFLOAT;
        *max = zscale*G_MAXFLOAT;
    }
    else if (bit_depth == GWY_BIT_DEPTH_HALF) {
        *min = zscale*HALF_NRM_MIN;
        *max = zscale*HALF_MAX;
    }
    else {
        g_assert_not_reached();
    }
}

static gchar*
create_image_data(GwyField *field,
                  GwyBitDepth bit_depth,
                  gdouble zscale,
                  gdouble zmin,
                  gdouble zmax)
{
    guint xres = gwy_field_get_xres(field);
    guint yres = gwy_field_get_yres(field);
    const gdouble *d = gwy_field_get_data_const(field);
    gchar *retval = NULL;
    guint i;

    if (zscale == GWY_BIT_DEPTH_INT16) {
        guint16 *imagedata = g_new(guint16, xres*yres);
        gdouble q = (G_MAXUINT16 + 0.999)/(zmax - zmin);
        retval = (gchar*)imagedata;

        for (i = xres*yres; i; i--, d++, imagedata++)
            *imagedata = (guint16)CLAMP(q*(*d - zmin), 0.0, G_MAXUINT16 + 0.999);
    }
    else if (bit_depth == GWY_BIT_DEPTH_INT32) {
        guint32 *imagedata = g_new(guint32, xres*yres);
        gdouble q = (G_MAXUINT32 + 0.999)/(zmax - zmin);
        retval = (gchar*)imagedata;

        for (i = xres*yres; i; i--, d++, imagedata++)
            *imagedata = (guint32)CLAMP(q*(*d - zmin), 0.0, G_MAXUINT32 + 0.999);
    }
    else if (bit_depth == GWY_BIT_DEPTH_FLOAT) {
        gfloat *imagedata = g_new(gfloat, xres*yres);
        retval = (gchar*)imagedata;

        for (i = xres*yres; i; i--, d++, imagedata++)
            *imagedata = (gfloat)(*d/zscale);
    }
    else if (bit_depth == GWY_BIT_DEPTH_HALF) {
        half *imagedata = g_new(half, xres*yres);
        retval = (gchar*)imagedata;

        for (i = xres*yres; i; i--, d++, imagedata++)
            *imagedata = (half)(*d/zscale);
    }
    else {
        g_assert_not_reached();
    }

    return retval;
}

static void
find_range(GwyField *field,
           gdouble *fmin, gdouble *fmax,
           gdouble *pmin, gdouble *pmax, gdouble *pcentre)
{
    gdouble min = G_MAXDOUBLE, max = G_MINDOUBLE, logcentre = 0.0;
    guint i, nc = 0;
    guint xres = gwy_field_get_xres(field),
          yres = gwy_field_get_yres(field);
    const gdouble *d = gwy_field_get_data_const(field);
    gdouble v;

    for (i = xres*yres; i; i--, d++) {
        if (!(v = *d))
            continue;

        v = fabs(v);
        if (v < min)
            min = v;
        if (v > max)
            max = v;
        logcentre += log(v);
        nc++;
    }

    *pmax = max;
    *pmin = min;
    *pcentre = exp(logcentre/nc);

    gwy_field_min_max(field, fmin, fmax);
}

/***************************************************************************************************************
 *
 * Manual high-depth image loading
 *
 ***************************************************************************************************************/

static GwyDialogOutcome
run_import_gui(ImportArgs *args, const gchar *name)
{
    ImportGUI gui;
    GwyParamTable *table, *infotable;
    GtkWidget *hbox, *view;
    GwyDialog *dialog;
    GwyDialogOutcome outcome;
    GwyResults *results;
    gint xres, yres;
    gchar *title;
    const GEnumValue *enumval;
    GEnumClass *eclass;

    gwy_clear(&gui, 1);
    gui.args = args;

    xres = gwy_field_get_xres(args->field);
    yres = gwy_field_get_yres(args->field);

    /* TRANSLATORS: Dialog title; %s is PNG, TIFF, ... */
    title = g_strdup_printf(_("Import %s"), name);
    gui.dialog = gwy_dialog_new(title);
    g_free(title);
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 20);
    gwy_dialog_add_content(dialog, hbox, FALSE, FALSE, 0);

    results = gwy_results_new();
    gwy_results_add_value(results, "xres", N_("Horizontal size"),
                          "type", GWY_RESULTS_VALUE_INT,
                          "unit-str", "px",
                          NULL);
    gwy_results_add_value(results, "yres", N_("Vertical size"),
                          "type", GWY_RESULTS_VALUE_INT,
                          "unit-str", "px",
                          NULL);
    gwy_results_add_value_str(results, "channels", N_("Channels"));
    gwy_results_add_value_str(results, "depth", N_("Depth"));
    gwy_results_add_value_int(results, "pages", N_("Pages"));
    eclass = (GEnumClass*)g_type_class_ref(GWY_TYPE_RAW_DATA_TYPE);
    enumval = g_enum_get_value(eclass, args->datatype);
    gwy_results_fill_values(results, "xres", xres, "yres", yres, "pages", args->npages,
                            "channels", args->channels,
                            "depth", enumval ? enumval->value_nick : "?",
                            NULL);
    g_type_class_unref(eclass);

    infotable = gwy_param_table_new(args->params);
    gwy_param_table_append_header(infotable, -1, _("Image Information"));
    gwy_param_table_append_results(infotable, WIDGET_IMAGE_INFO, results,
                                   "xres", "yres", "channels", "depth", "pages",
                                   NULL);
    gwy_param_table_results_fill(infotable, WIDGET_IMAGE_INFO);
    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(infotable), TRUE, TRUE, 0);

    view = gwy_create_preview(args->field, NULL, PREVIEW_SIZE);
    gtk_box_pack_start(GTK_BOX(hbox), view, TRUE, TRUE, 0);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 20);
    gwy_dialog_add_content(dialog, hbox, TRUE, TRUE, 0);

    table = gui.table_lateral = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Physical Dimensions"));
    gwy_param_table_append_checkbox(table, PARAM_SIZE_IN_PIXELS);
    gwy_param_table_append_entry(table, PARAM_XREAL);
    gwy_param_table_append_entry(table, PARAM_YREAL);
    gwy_param_table_append_checkbox(table, PARAM_XYMEASUREEQ);
    gwy_param_table_append_unit_chooser(table, PARAM_XYUNIT);
    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), TRUE, TRUE, 0);

    table = gui.table_values = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Value Mapping"));
    gwy_param_table_append_entry(table, PARAM_ZREAL);
    gwy_param_table_append_unit_chooser(table, PARAM_ZUNIT);
    if (args->npages > 1 && args->pages_are_uniform) {
        gwy_param_table_append_header(table, -1, _("Image Stack"));
        gwy_param_table_append_checkbox(table, PARAM_AS_VOLUME);
    }
    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), TRUE, TRUE, 0);

    g_signal_connect_swapped(infotable, "param-changed", G_CALLBACK(import_param_changed), &gui);
    g_signal_connect_swapped(gui.table_lateral, "param-changed", G_CALLBACK(import_param_changed), &gui);
    g_signal_connect_swapped(gui.table_values, "param-changed", G_CALLBACK(import_param_changed), &gui);

    outcome = gwy_dialog_run(dialog);

    g_object_unref(results);

    return outcome;
}

static void
import_param_changed(ImportGUI *gui, gint id)
{
    ImportArgs *args = gui->args;
    GwyParams *params = args->params;
    gboolean size_in_pixels = gwy_params_get_boolean(params, PARAM_SIZE_IN_PIXELS);
    gboolean xymeasureeq = gwy_params_get_boolean(params, PARAM_XYMEASUREEQ);
    gint xres = gwy_field_get_xres(args->field);
    gint yres = gwy_field_get_yres(args->field);
    GwyUnit *unit;
    gint power10;
    GwyValueFormat *vf = NULL;

    if (id < 0 || id == PARAM_SIZE_IN_PIXELS) {
        if (size_in_pixels) {
            gwy_param_table_set_string(gui->table_lateral, PARAM_XYUNIT, NULL);
            gwy_param_table_set_boolean(gui->table_lateral, PARAM_XYMEASUREEQ, (xymeasureeq = TRUE));
            gwy_param_table_set_double(gui->table_lateral, PARAM_XREAL, xres);
            gwy_param_table_set_double(gui->table_lateral, PARAM_YREAL, yres);
            id = -1;
        }
        gwy_param_table_set_sensitive(gui->table_lateral, PARAM_XYUNIT, !size_in_pixels);
        gwy_param_table_set_sensitive(gui->table_lateral, PARAM_XREAL, !size_in_pixels);
        gwy_param_table_set_sensitive(gui->table_lateral, PARAM_YREAL, !size_in_pixels);
        gwy_param_table_set_sensitive(gui->table_lateral, PARAM_XYMEASUREEQ, !size_in_pixels);
    }

    if (xymeasureeq) {
        if (id < 0 || id == PARAM_XYMEASUREEQ || id == PARAM_XREAL) {
            gdouble xreal = gwy_params_get_double(params, PARAM_XREAL);
            gwy_param_table_set_double(gui->table_lateral, PARAM_YREAL, yres*xreal/xres);
        }
        else if (id == PARAM_YREAL) {
            gdouble yreal = gwy_params_get_double(params, PARAM_YREAL);
            gwy_param_table_set_double(gui->table_lateral, PARAM_XREAL, xres*yreal/yres);
        }
    }

    if (id < 0 || id == PARAM_XYUNIT) {
        unit = gwy_params_get_unit(params, PARAM_XYUNIT, &power10);
        vf = gwy_unit_get_format_for_power10(unit, GWY_UNIT_FORMAT_VFMARKUP, power10, vf);
        gwy_param_table_set_unitstr(gui->table_lateral, PARAM_XREAL, vf->units);
        gwy_param_table_set_unitstr(gui->table_lateral, PARAM_YREAL, vf->units);
    }

    if (id < 0 || id == PARAM_ZUNIT) {
        unit = gwy_params_get_unit(params, PARAM_ZUNIT, &power10);
        vf = gwy_unit_get_format_for_power10(unit, GWY_UNIT_FORMAT_VFMARKUP, power10, vf);
        gwy_param_table_set_unitstr(gui->table_values, PARAM_ZREAL, vf->units);
    }

    GWY_FREE_VALUE_FORMAT(vf);
}

static void
field_props_from_params(GwyParams *params,
                        gdouble *xreal, gdouble *yreal, GwyUnit **xyunit,
                        gdouble *zmax, GwyUnit **zunit)
{
    gint power10;

    gwy_debug("Using parameters from settings.");
    *xyunit = gwy_unit_copy(gwy_params_get_unit(params, PARAM_XYUNIT, &power10));
    *xreal = gwy_params_get_double(params, PARAM_XREAL)*gwy_exp10(power10);
    *yreal = gwy_params_get_double(params, PARAM_YREAL)*gwy_exp10(power10);
    *zunit = gwy_unit_copy(gwy_params_get_unit(params, PARAM_ZUNIT, &power10));
    *zmax = gwy_params_get_double(params, PARAM_ZREAL)*gwy_exp10(power10);
}

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
