/*
 *  $Id: hdrimage.c 30151 2026-06-25 13:17: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-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>
#include <png.h>

#include <gwy.h>

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

enum {
    PREVIEW_SIZE = 320,
};

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 properties. */
    gchar *channels;
    GwyRawDataType datatype;
    guint npages;
    gboolean pages_are_uniform;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GwyParamTable *table_lateral;
    GwyParamTable *table_values;
} ModuleGUI;

static gboolean         module_register        (void);
#ifdef HAVE_PNG
static gint             png16_detect           (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static GwyFile*         png16_load             (const gchar *filename,
                                                GwyRunModeFlags mode,
                                                GError **error);
#endif
static gint             pgm16_detect           (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static GwyFile*         pgm16_load             (const gchar *filename,
                                                GwyRunModeFlags mode,
                                                GError **error);
static gint             tiffbig_detect         (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static GwyFile*         tiffbig_load           (const gchar *filename,
                                                GwyRunModeFlags mode,
                                                GError **error);
static GwyDialogOutcome run_gui                (ModuleArgs *args,
                                                const gchar *name);
static void             param_changed          (ModuleGUI *gui,
                                                gint id);
static void             field_props_from_params(GwyParams *params,
                                                gdouble *xreal,
                                                gdouble *yreal,
                                                GwyUnit **xyunit,
                                                gdouble *zmax,
                                                GwyUnit **zunit);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports high-depth PPM, PNG and TIFF images."),
    "Yeti <yeti@gwyddion.net>",
    "5.0",
    "David Nečas (Yeti)",
    "2011",
};

GWY_MODULE_QUERY2(module_info, hdrimage)

static gboolean
module_register(void)
{
#ifdef HAVE_PNG
    gwy_file_func_register("png16",
                           N_("PNG images with 16bit depth (.png)"),
                           &png16_detect, &png16_load, NULL, NULL);
#endif
    gwy_file_func_register("pgm16",
                           N_("PGM images with 16bit depth (.pgm)"),
                           &pgm16_detect, &pgm16_load, NULL, NULL);
    gwy_file_func_register("tiffbig",
                           N_("TIFF and BigTIFF images with high depth (.tiff)"),
                           &tiffbig_detect, &tiffbig_load, NULL, NULL);

    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 gchar*
describe_channels(guint flags)
{
    if (flags & PIXMAP_HAS_COLOURS)
        return g_strdup((flags & PIXMAP_HAS_ALPHA) ? "R, G, B, A" : "R, G, B");
    return g_strdup((flags & PIXMAP_HAS_ALPHA) ? "G, A" : "G");
}

static const gchar*
channel_name(guint nchannels, guint id)
{
    if (nchannels == 1)
        return "Gray";

    if (nchannels == 2)
        return id ? "Alpha" : "Gray";

    if (nchannels == 3)
        return id ? (id == 1 ? "Green" : "Blue") : "Red";

    if (nchannels == 4)
        return id ? (id == 1 ? "Green" : (id == 2 ? "Blue" : "Alpha")) : "Red";

    return NULL;
}

/***************************************************************************************************************
 *
 * PNG
 *
 ***************************************************************************************************************/

#ifdef HAVE_PNG
static gint
png16_detect(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    typedef struct {
        guint width;
        guint height;
        guint bit_depth;
        guint colour_type;
        guint compression_method;
        guint filter_method;
        guint interlace_method;
    } IHDR;

    IHDR header;
    const guchar *p;

    // Export is done in imgexport.c, we cannot have multiple exporters of the same type (unlike loaders).
    if (only_name)
        return 0;

    if (fileinfo->buffer_len < 64)
        return 0;
    if (memcmp(fileinfo->head, "\x89PNG\r\n\x1a\n\x00\x00\x00\x0dIHDR", 16)
        != 0)
        return 0;

    p = fileinfo->head + 16;
    header.width = gwy_get_guint32_be(&p);
    header.height = gwy_get_guint32_be(&p);
    header.bit_depth = *(p++);
    header.colour_type = *(p++);
    header.compression_method = *(p++);
    header.filter_method = *(p++);
    header.interlace_method = *(p++);
    if (!header.width || !header.height || header.bit_depth != 16)
        return 0;

    return 95;
}

static gboolean
get_png_text_double(const png_textp text_chunks, guint ncomments,
                    const gchar *key, gdouble *value)
{
    guint i;

    for (i = 0; i < ncomments; i++) {
        if (gwy_strequal(text_chunks[i].key, key)) {
            *value = g_ascii_strtod(text_chunks[i].text, NULL);
            return TRUE;
        }
    }
    return FALSE;
}

static const gchar*
get_png_text_string(const png_textp text_chunks, guint ncomments,
                    const gchar *key)
{
    guint i;

    for (i = 0; i < ncomments; i++) {
        if (gwy_strequal(text_chunks[i].key, key))
            return text_chunks[i].text;
    }
    return NULL;
}

static GwyFile*
png16_load(const gchar *filename,
           GwyRunModeFlags mode,
           GError **error)
{
    png_structp reader = NULL;
    png_infop reader_info = NULL;
    png_bytepp rows = NULL;
    png_textp text_chunks = NULL;
    png_int_32 pcal_X0, pcal_X1;
    png_charp pcal_purpose, pcal_units;
    png_charpp pcal_params;
#if (G_BYTE_ORDER == G_LITTLE_ENDIAN)
    guint transform_flags = PNG_TRANSFORM_SWAP_ENDIAN;
#endif
#if (G_BYTE_ORDER == G_BIG_ENDIAN)
    guint transform_flags = PNG_TRANSFORM_IDENTITY;
#endif
    GwyFile *file = NULL;
    GwyField **fields = NULL;
    gdouble **data = NULL;
    FILE *fr = NULL;
    GwyUnit *unitxy = NULL, *unitz = NULL;
    gboolean have_sCAL, have_pCAL, manual_import = TRUE;
    guint xres, yres, bit_depth, nchannels, ncomments;
    G_GNUC_UNUSED guint colour_type, rowbytes;
    guint id, i, j;
    int scal_unit, pcal_type, pcal_nparams, power10;
    gdouble xreal, yreal, xoff = 0.0, yoff = 0.0, zmin, zmax, q, scal_xreal, scal_yreal;
    ModuleArgs args;
    const gchar *title = NULL;
    png_byte magic[8];

    gwy_clear1(args);
    if (!(fr = g_fopen(filename, "rb"))) {
        err_OPEN_READ(error);
        goto fail;
    }
    if (fread(magic, 1, sizeof(magic), fr) != sizeof(magic)) {
        err_READ(error);
        goto fail;
    }
    if (png_sig_cmp(magic, 0, sizeof(magic)) != 0) {
        err_FILE_TYPE(error, "PNG");
        goto fail;
    }

    reader = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!reader) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_SPECIFIC,
                    _("libpng initialization error (in %s)"),
                    "png_create_read_struct");
        goto fail;
    }

    reader_info = png_create_info_struct(reader);
    if (!reader_info) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_SPECIFIC,
                    _("libpng initialization error (in %s)"),
                    "png_create_info_struct");
        goto fail;
    }

    if (setjmp(png_jmpbuf(reader))) {
        /* FIXME: Not very helpful.  Thread-unsafe. */
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_SPECIFIC,
                    _("libpng error occurred"));
        goto fail;
    }

    png_init_io(reader, fr);
    png_set_sig_bytes(reader, sizeof(magic));
    /* The same as in err_DIMENSIONS(). */
    png_set_user_limits(reader, 1 << 15, 1 << 15);
    png_read_png(reader, reader_info, transform_flags, NULL);
    /* png_get_IHDR() causes trouble with the too type-picky C++ compiler. */
    xres = png_get_image_width(reader, reader_info);
    yres = png_get_image_height(reader, reader_info);
    bit_depth = png_get_bit_depth(reader, reader_info);
    if (bit_depth != 16) {
        err_BPP(error, bit_depth);
        goto fail;
    }
    colour_type = png_get_color_type(reader, reader_info);
    nchannels = png_get_channels(reader, reader_info);
    gwy_debug("xres: %u, yres: %u, bit_depth: %u, type: %u, nchannels: %u",
              xres, yres, bit_depth, colour_type, nchannels);
    rowbytes = png_get_rowbytes(reader, reader_info);
    ncomments = png_get_text(reader, reader_info, &text_chunks, NULL);
    have_sCAL = png_get_sCAL(reader, reader_info, &scal_unit, &scal_xreal, &scal_yreal);
    have_pCAL = png_get_pCAL(reader, reader_info, &pcal_purpose, &pcal_X0, &pcal_X1, &pcal_type,
                             &pcal_nparams, &pcal_units, &pcal_params);
    gwy_debug("ncomments: %u, sCAL: %d, pCAL: %d", ncomments, have_sCAL, have_pCAL);
    rows = png_get_rows(reader, reader_info);

    /* Gwyddion tEXT chunks. */
    if (get_png_text_double(text_chunks, ncomments, GWY_IMGKEY_XREAL, &xreal)
        && get_png_text_double(text_chunks, ncomments, GWY_IMGKEY_YREAL, &yreal)
        && get_png_text_double(text_chunks, ncomments, GWY_IMGKEY_ZMIN, &zmin)
        && get_png_text_double(text_chunks, ncomments, GWY_IMGKEY_ZMAX, &zmax)) {
        gwy_debug("Found Gwyddion image keys, using for direct import.");
        xoff = yoff = 0.0;
        get_png_text_double(text_chunks, ncomments, GWY_IMGKEY_XOFFSET, &xoff);
        get_png_text_double(text_chunks, ncomments, GWY_IMGKEY_YOFFSET, &yoff);
        unitxy = gwy_unit_new_parse(get_png_text_string(text_chunks, ncomments, GWY_IMGKEY_XYUNIT), &power10);
        q = gwy_exp10(power10);
        xreal *= q;
        yreal *= q;
        xoff *= q;
        yoff *= q;
        unitz = gwy_unit_new_parse(get_png_text_string(text_chunks, ncomments, GWY_IMGKEY_ZUNIT), &power10);
        q = gwy_exp10(power10);
        zmin *= q;
        zmax *= q;
        title = get_png_text_string(text_chunks, ncomments, GWY_IMGKEY_TITLE);

        sanitise_real_size(&xreal, "x size");
        sanitise_real_size(&yreal, "y size");
        manual_import = FALSE;
    }
    else if (have_sCAL && have_pCAL && pcal_nparams == 2 && gwy_strequal(pcal_purpose, "Z")) {
        gwy_debug("Found sCAL and pCAL chnunks, using for direct import.");
        if (pcal_X0 != 0 || pcal_X1 != G_MAXUINT16)
            g_warning("PNG pCAL X0 and X1 transform is not implemented");

        xreal = scal_xreal;
        yreal = scal_yreal;
        xoff = yoff = 0.0;
        zmin = g_ascii_strtod(pcal_params[0], NULL);
        zmax = zmin + G_MAXUINT16*g_ascii_strtod(pcal_params[0], NULL);

        unitxy = gwy_unit_new("m");
        unitz = gwy_unit_new_parse(pcal_units, &power10);
        q = gwy_exp10(power10);
        zmin *= q;
        zmax *= q;

        sanitise_real_size(&xreal, "x size");
        sanitise_real_size(&yreal, "y size");
        manual_import = FALSE;
    }

    if (!title)
        title = get_png_text_string(text_chunks, ncomments, "Title");

    args.params = gwy_params_new_from_settings(define_import_params());
    // Loading alpha from a separate chunk is not supported
    args.npages = 1;
    args.datatype = GWY_RAW_DATA_UINT16;
    args.pages_are_uniform = TRUE;
    args.channels = describe_channels(nchannels > 1 ? PIXMAP_HAS_COLOURS : 0);
    if (mode == GWY_RUN_INTERACTIVE && manual_import) {
        gwy_debug("Manual import is necessary.");
        args.field = gwy_field_new(xres, yres, 1.0, 1.0, FALSE);
        gdouble *d = gwy_field_get_data(args.field);

        // Use the first channel for preview.
        for (i = 0; i < yres; i++) {
            const guint16 *row = (const guint16*)rows[i];
            for (j = 0; j < xres; j++) {
                d[i*xres + j] = row[j*nchannels];
            }
        }

        GwyDialogOutcome outcome = run_gui(&args, "PNG");
        gwy_params_save_to_settings(args.params);
        g_clear_object(&args.field);
        if (outcome == GWY_DIALOG_CANCEL) {
            err_CANCELLED(error);
            goto fail;
        }
    }
    if (manual_import) {
        zmin = 0.0;
        field_props_from_params(args.params, &xreal, &yreal, &unitxy, &zmax, &unitz);
    }

    fields = g_new(GwyField*, nchannels);
    data = g_new(gdouble*, nchannels);
    for (id = 0; id < nchannels; id++) {
        GwyField *f;

        fields[id] = f = gwy_field_new(xres, yres, xreal, yreal, FALSE);
        gwy_unit_assign(gwy_field_get_unit_xy(f), unitxy);
        gwy_unit_assign(gwy_field_get_unit_z(f), unitz);
        gwy_field_set_xoffset(f, xoff);
        gwy_field_set_yoffset(f, yoff);
        data[id] = gwy_field_get_data(f);
    }

    q = (zmax - zmin)/G_MAXUINT16;
    for (i = 0; i < yres; i++) {
        const guint16 *row = (const guint16*)rows[i];
        for (j = 0; j < xres; j++) {
            for (id = 0; id < nchannels; id++) {
                data[id][i*xres + j] = q*row[j*nchannels + id] + zmin;
            }
        }
    }

    file = gwy_file_new_in_construction();
    for (id = 0; id < nchannels; id++) {
        const gchar *basetitle;
        gchar *t = NULL;

        gwy_file_pass_image(file, id, fields[id]);

        basetitle = channel_name(nchannels, id);
        if (title && (nchannels == 1 || !basetitle))
            t = g_strdup(title);
        else if (title)
            t = g_strdup_printf("%s %s", _(basetitle), title);
        else if (basetitle)
            t = g_strdup(_(basetitle));

        if (t)
            gwy_file_pass_title(file, GWY_FILE_IMAGE, id, t);

        if (basetitle && gwy_stramong(basetitle, "Red", "Green", "Blue", NULL))
            gwy_dict_pass_string(file, gwy_app_get_data_palette_key_for_id(id), g_strconcat("RGB-", basetitle, NULL));

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

fail:
    g_free(args.channels);
    g_free(data);
    g_free(fields);
    g_clear_object(&args.params);
    g_clear_object(&unitxy);
    g_clear_object(&unitz);
    if (reader)
        png_destroy_read_struct(&reader, reader_info ? &reader_info : NULL, NULL);
    if (fr)
        fclose(fr);
    return file;
}
#endif

/***************************************************************************************************************
 *
 * PGM
 *
 ***************************************************************************************************************/

/* Pixel properties are set if detection is successfull, real properties are set only if return value is GWY_META. */
static DetectionResult
read_pgm_head(const gchar *buffer, gsize len, guint *headersize,
              guint *xres, guint *yres, guint *maxval,
              gdouble *xreal, gdouble *yreal,
              gdouble *yoff, gdouble *xoff,
              gdouble *zmin, gdouble *zmax,
              GwyUnit **unitxy, GwyUnit **unitz,
              gchar **title)
{
    const gchar *p = buffer, *q;
    gboolean seen_comments = FALSE,
             seen_xreal = FALSE, seen_yreal = FALSE,
             seen_zmin = FALSE, seen_zmax = FALSE;
    gint power10xy = 0, power10z = 0;
    gchar *text, *line, *s, *t;
    guint i;

    /* Quickly weed out non-PGM files */
    if (len < 3)
        return BAD_FILE;
    if (p[0] != 'P' || p[1] != '5' || !g_ascii_isspace(p[2]))
        return BAD_FILE;
    p += 3;

    for (i = 0; i < 3; i++) {
        if (p == buffer)
            return BAD_FILE;

        while (TRUE) {
            /* Skip whitespace */
            while ((gsize)(p - buffer) < len && g_ascii_isspace(*p))
                p++;
            if (p == buffer)
                return BAD_FILE;

            /* Possibly skip comments */
            if (*p != '#')
                break;

            seen_comments = TRUE;
            while ((gsize)(p - buffer) < len && *p != '\n' && *p != '\r')
                p++;
            if (p == buffer)
                return BAD_FILE;
        }

        /* Find the number */
        if (!g_ascii_isdigit(*p))
            return BAD_FILE;
        q = p;
        while ((gsize)(p - buffer) < len && g_ascii_isdigit(*p))
            p++;
        if (p == buffer)
            return BAD_FILE;
        if (!g_ascii_isspace(*p))
            return BAD_FILE;

        /* Store the number */
        if (i == 0)
            *xres = atoi(q);
        else if (i == 1)
            *yres = atoi(q);
        else if (i == 2)
            *maxval = atoi(q);
        else {
            g_assert_not_reached();
        }
    }

    /* If i == 3 and we got here then p points to the single white space character after the last number (maxval). */
    p++;
    *headersize = p - buffer;

    /* Sanity check. */
    if (*maxval < 0x100 || *maxval >= 0x10000)
        return BAD_FILE;
    if (*xres < 1 || *xres >= 1 << 15)
        return BAD_FILE;
    if (*yres < 1 || *yres >= 1 << 15)
        return BAD_FILE;

    if (!seen_comments)
        return PLAIN_IMAGE;

    *xoff = *yoff = 0.0;
    *unitxy = *unitz = NULL;
    *title = NULL;
    text = t = g_strndup(buffer, *headersize);
    for (line = gwy_str_next_line(&t); line; line = gwy_str_next_line(&t)) {
        g_strstrip(line);
        if (line[0] != '#')
            continue;
        line++;
        while (g_ascii_isspace(*line))
            line++;
        s = line;
        while (g_ascii_isalnum(*line) || *line == ':')
            line++;
        *line = '\0';
        line++;
        while (g_ascii_isspace(*line))
            line++;

        if (gwy_strequal(s, GWY_IMGKEY_XREAL)) {
            *xreal = g_ascii_strtod(line, NULL);
            seen_xreal = TRUE;
        }
        else if (gwy_strequal(s, GWY_IMGKEY_YREAL)) {
            *yreal = g_ascii_strtod(line, NULL);
            seen_yreal = TRUE;
        }
        else if (gwy_strequal(s, GWY_IMGKEY_ZMIN)) {
            *zmin = g_ascii_strtod(line, NULL);
            seen_zmin = TRUE;
        }
        else if (gwy_strequal(s, GWY_IMGKEY_ZMAX)) {
            *zmax = g_ascii_strtod(line, NULL);
            seen_zmax = TRUE;
        }
        else if (gwy_strequal(s, GWY_IMGKEY_XOFFSET))
            *xoff = g_ascii_strtod(line, NULL);
        else if (gwy_strequal(s, GWY_IMGKEY_YOFFSET))
            *yoff = g_ascii_strtod(line, NULL);
        else if (gwy_strequal(s, GWY_IMGKEY_XYUNIT)) {
            g_clear_object(unitxy);
            *unitxy = gwy_unit_new_parse(line, &power10xy);
        }
        else if (gwy_strequal(s, GWY_IMGKEY_ZUNIT)) {
            g_clear_object(unitz);
            *unitz = gwy_unit_new_parse(line, &power10z);
        }
        else if (gwy_strequal(s, GWY_IMGKEY_TITLE)) {
            g_free(*title);
            *title = *line ? g_strdup(line) : NULL;
        }
    }

    g_free(text);

    if (seen_xreal && seen_yreal && seen_zmin && seen_zmax) {
        *xreal *= gwy_exp10(power10xy);
        *yreal *= gwy_exp10(power10xy);
        *xoff *= gwy_exp10(power10xy);
        *yoff *= gwy_exp10(power10xy);
        *zmin *= gwy_exp10(power10z);
        *zmax *= gwy_exp10(power10z);
        return GWY_META;
    }

    g_clear_object(unitxy);
    g_clear_object(unitz);
    g_free(*title);
    return PLAIN_IMAGE;
}

static gint
pgm16_detect(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    GwyUnit *unitxy = NULL, *unitz = NULL;
    gchar *title = NULL;
    gdouble xreal, yreal, xoff, yoff, zmin, zmax;
    guint xres, yres, maxval, headersize;

    // Export is done in imgexport.c, we cannot have multiple exporters of the same type (unlike loaders).
    if (only_name)
        return 0;

    if (!read_pgm_head(fileinfo->head, fileinfo->buffer_len, &headersize,
                       &xres, &yres, &maxval, &xreal, &yreal, &yoff, &xoff,
                       &zmin, &zmax, &unitxy, &unitz, &title))
        return 0;

    g_clear_object(&unitxy);
    g_clear_object(&unitz);
    g_free(title);

    return 95;
}

static GwyFile*
pgm16_load(const gchar *filename,
           GwyRunModeFlags mode,
           GError **error)
{
    GwyFile *file = NULL;
    GwyField *field = NULL;
    GError *err = NULL;
    const guchar *buffer = NULL;
    gchar *title = NULL;
    GwyUnit *unitxy = NULL, *unitz = NULL;
    gdouble xreal, yreal, xoff = 0.0, yoff = 0.0, zmin, zmax, q;
    guint xres, yres, maxval, headersize;
    DetectionResult detected;
    ModuleArgs args;
    gsize size = 0;

    if (!gwy_map_file_contents(filename, &buffer, &size, &err)) {
        err_GET_FILE_CONTENTS(error, &err);
        return NULL;
    }
    gwy_clear1(args);
    args.params = gwy_params_new_from_settings(define_import_params());

    // TODO: Read images in cycle, PNM is a multi-image format.
    detected = read_pgm_head(buffer, size, &headersize,
                             &xres, &yres, &maxval, &xreal, &yreal, &yoff, &xoff,
                             &zmin, &zmax, &unitxy, &unitz, &title);

    gwy_debug("Detected: %s", detected == GWY_META ? "Gwyddion image keys" : "Plain image");

    args.npages = 1;
    args.datatype = GWY_RAW_DATA_UINT16;
    args.pages_are_uniform = TRUE;
    args.channels = describe_channels(0);
    if (detected != GWY_META && mode == GWY_RUN_INTERACTIVE) {
        gwy_debug("Manual import is necessary.");
        args.field = gwy_field_new(xres, yres, 1.0, 1.0, FALSE);
        gwy_convert_raw_data(buffer + headersize, xres*yres, 1,
                             GWY_RAW_DATA_UINT16, GWY_BYTE_ORDER_BIG_ENDIAN,
                             gwy_field_get_data(args.field), 1.0, 0.0);
        GwyDialogOutcome outcome = run_gui(&args, "PGM");
        gwy_params_save_to_settings(args.params);
        g_clear_object(&args.field);
        if (outcome == GWY_DIALOG_CANCEL) {
            err_CANCELLED(error);
            goto fail;
        }
    }
    if (detected != GWY_META) {
        zmin = 0.0;
        field_props_from_params(args.params, &xreal, &yreal, &unitxy, &zmax, &unitz);
    }

    if (err_SIZE_MISMATCH(error, 2*xres*yres + headersize, size, FALSE))
        goto fail;

    sanitise_real_size(&xreal, "x size");
    sanitise_real_size(&yreal, "y size");

    field = gwy_field_new(xres, yres, xreal, yreal, FALSE);
    gwy_unit_assign(gwy_field_get_unit_xy(field), unitxy);
    gwy_unit_assign(gwy_field_get_unit_z(field), unitz);
    gwy_field_set_xoffset(field, xoff);
    gwy_field_set_yoffset(field, yoff);

    q = (zmax - zmin)/G_MAXUINT16;
    gwy_convert_raw_data(buffer + headersize, xres*yres, 1,
                         GWY_RAW_DATA_UINT16, GWY_BYTE_ORDER_BIG_ENDIAN,
                         gwy_field_get_data(field), q, zmin);

    file = gwy_file_new_in_construction();
    gwy_file_set_image(file, 0, field);
    g_object_unref(field);
    if (title) {
        gwy_file_pass_title(file, GWY_FILE_IMAGE, 0, title);
        title = NULL;
    }

    gwy_log_add_import(file, GWY_FILE_IMAGE, 0, "pgm16", filename);

fail:
    gwy_unmap_file_contents(buffer);
    g_clear_object(&args.params);
    g_clear_object(&unitxy);
    g_clear_object(&unitz);
    g_free(args.channels);
    g_free(title);

    return file;
}

/***************************************************************************************************************
 *
 * TIFF
 *
 ***************************************************************************************************************/

static gint
tiffbig_detect(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    // Export is done in imgexport.c, we cannot have multiple exporters of the same type (unlike loaders).
    if (only_name)
        return 0;

    if (!gwy_tiff_detect(fileinfo->head, fileinfo->buffer_len, NULL, NULL))
        return 0;

    GwyTIFF *tiff = gwy_tiff_load(fileinfo->name, NULL);
    if (!tiff)
        return 0;

    gwy_tiff_allow_compressed(tiff, TRUE);
    GwyTIFFImageReader *reader = gwy_tiff_get_image_reader(tiff, 0, G_MAXUINT, NULL);

    guint score = 0;
    if (reader) {
        // If nothing else wants to load the image we can give it a try.
        score = 20;
        // A bit larger value than in imgexport.c for more bits.
        if (reader->bits_per_sample > 8)
            score = 75;
        // Also for more channels.
        if (reader->samples_per_pixel > 4)
            score = 75;
        // More than one image for which GdkPixbuf does not provide sane interface.
        if (gwy_tiff_get_n_dirs(tiff) > 1)
            score = 75;
        // An even larger value for BigTIFF, but still permit specific BigTIFF sub-formats to get a higher score.
        if (tiff->version == GWY_TIFF_BIG)
            score = 85;
    }

    gwy_tiff_image_reader_free(reader);
    gwy_tiff_free(tiff);

    return score;
}

static gboolean
load_tiff_channels(GwyFile *file,
                   const GwyTIFF *tiff,
                   GwyTIFFImageReader *reader,
                   const gchar *filename,
                   gdouble xreal, gdouble yreal, gdouble zreal,
                   GwyUnit *unitxy, GwyUnit *unitz,
                   guint *id,
                   GError **error)
{
    guint xres = reader->width;
    guint yres = reader->height;
    guint nchannels = reader->samples_per_pixel;

    for (guint cid = 0; cid < nchannels; cid++) {
        GwyField *dfield = gwy_field_new(xres, yres, xreal, yreal, FALSE);
        gdouble *d = gwy_field_get_data(dfield);
        const gchar *title;

        for (guint i = 0; i < yres; i++) {
            if (!gwy_tiff_read_image_row(tiff, reader, cid, i, zreal, 0.0, d + i*xres)) {
                g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                            _("Failed to read image data."));
                g_object_unref(dfield);
                return FALSE;
            }
        }

        GwyNield *mask = NULL;
        if (reader->sample_format == GWY_TIFF_SAMPLE_FORMAT_FLOAT)
            mask = gwy_mask_nans_in_field(dfield, TRUE);

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

        gwy_file_set_image(file, *id, dfield);
        g_object_unref(dfield);

        title = channel_name(nchannels, cid);
        if (title)
            gwy_file_set_title(file, GWY_FILE_IMAGE, *id, title, FALSE);
        else
            gwy_file_pass_title(file, GWY_FILE_IMAGE, *id, g_strdup_printf("%s %u", _("Channel"), *id));

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

        if (title && gwy_stramong(title, "Red", "Green", "Blue", NULL)) {
            gchar *palette = g_strconcat("RGB-", title, NULL);
            gwy_file_set_palette(file, GWY_FILE_IMAGE, *id, palette);
            g_free(palette);
        }

        gwy_log_add_import(file, GWY_FILE_IMAGE, *id, "tiffbig", filename);

        (*id)++;
    }

    return TRUE;
}

static gboolean
load_tiff_brick_levels(const GwyTIFF *tiff, GwyTIFFImageReader *reader, guint nchannels,
                       gdouble zreal, gint idx,
                       GwyBrick **bricks, GError **error)
{
    for (guint cid = 0; cid < nchannels; cid++) {
        GwyBrick *brick = bricks[cid];
        guint xres = gwy_brick_get_xres(brick), yres = gwy_brick_get_yres(brick);
        gdouble *d = gwy_brick_get_data(brick);

        for (guint i = 0; i < yres; i++) {
            if (!gwy_tiff_read_image_row(tiff, reader, cid, i, zreal, 0.0, d + xres*(i + idx*yres))) {
                g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                            _("Failed to read image data."));
                return FALSE;
            }
        }

        /* FIXME: Should do something similar for bricks.
        if (reader->sample_format == GWY_TIFF_SAMPLE_FORMAT_FLOAT)
            mask = gwy_app_channel_mask_of_nans(dfield, TRUE);
            */
    }
    return TRUE;
}

static gboolean
tiff_images_are_uniform(const GwyTIFF *tiff)
{
    guint64 xres, yres, i, n = gwy_tiff_get_n_dirs(tiff);
    guint samples_per_pixel, bits_per_sample;

    if (n == 1)
        return TRUE;

    /* We will fail properly later… */
    if (!gwy_tiff_get_size(tiff, 0, GWY_TIFFTAG_IMAGE_WIDTH, &xres)
        || !gwy_tiff_get_size(tiff, 0, GWY_TIFFTAG_IMAGE_LENGTH, &yres)
        || !gwy_tiff_get_uint(tiff, 0, GWY_TIFFTAG_SAMPLES_PER_PIXEL, &samples_per_pixel)
        || !gwy_tiff_get_uint(tiff, 0, GWY_TIFFTAG_BITS_PER_SAMPLE, &bits_per_sample))
        return FALSE;

    for (i = 1; i < n; i++) {
        guint64 xres_i, yres_i;
        guint samples_per_pixel_i, bits_per_sample_i;

        if (!gwy_tiff_get_size(tiff, 0, GWY_TIFFTAG_IMAGE_WIDTH, &xres_i)
            || !gwy_tiff_get_size(tiff, 0, GWY_TIFFTAG_IMAGE_LENGTH, &yres_i))
            return FALSE;
        if (xres_i != xres || yres_i != yres)
            return FALSE;
        if (!gwy_tiff_get_uint(tiff, 0, GWY_TIFFTAG_SAMPLES_PER_PIXEL, &samples_per_pixel_i)
            || !gwy_tiff_get_uint(tiff, 0, GWY_TIFFTAG_BITS_PER_SAMPLE, &bits_per_sample_i))
            return FALSE;
        if (samples_per_pixel_i != samples_per_pixel || bits_per_sample_i != bits_per_sample)
            return FALSE;
    }
    return TRUE;
}

static GwyFile*
tiffbig_load(const gchar *filename,
             GwyRunModeFlags mode,
             GError **error)
{
    GwyFile *file = NULL;
    guint id, idx, nchannels, xres, yres;
    gdouble xreal, yreal, zreal;
    GwyUnit *unitxy = NULL, *unitz = NULL;
    GwyTIFF *tiff = NULL;
    GwyTIFFImageReader *reader = NULL;
    GwyBrick **bricks = NULL;
    gboolean load_as_volume;
    ModuleArgs args;

    if (!(tiff = gwy_tiff_load(filename, error)))
        return NULL;

    gwy_clear1(args);
    args.params = gwy_params_new_from_settings(define_import_params());

    gwy_tiff_allow_compressed(tiff, TRUE);
    if (!(reader = gwy_tiff_get_image_reader(tiff, 0, G_MAXUINT, error)))
        goto fail;

    // Use the first channel for preview.
    nchannels = reader->samples_per_pixel;
    args.npages = gwy_tiff_get_n_dirs(tiff);
    args.datatype = reader->rawtype;
    args.pages_are_uniform = tiff_images_are_uniform(tiff);
    if (nchannels > 4)
        args.channels = g_strdup_printf("%u", nchannels);
    else {
        args.channels = describe_channels((nchannels > 2 ? PIXMAP_HAS_COLOURS : 0)
                                          | (nchannels % 2 == 0 ? PIXMAP_HAS_ALPHA : 0));
    }
    xres = reader->width;
    yres = reader->height;
    if (mode == GWY_RUN_INTERACTIVE) {
        gwy_debug("Manual import is necessary.");
        args.field = gwy_field_new(xres, yres, 1.0, 1.0, FALSE);
        gdouble *d = gwy_field_get_data(args.field);
        for (guint i = 0; i < yres; i++) {
            if (!gwy_tiff_read_image_row(tiff, reader, 0, i, 1.0, 0.0, d + i*xres)) {
                g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA, _("Failed to read image data."));
                g_clear_object(&args.field);
                goto fail;
            }
        }
        GwyDialogOutcome outcome = run_gui(&args, "TIFF");
        gwy_params_save_to_settings(args.params);
        g_clear_object(&args.field);
        if (outcome == GWY_DIALOG_CANCEL) {
            err_CANCELLED(error);
            goto fail;
        }
    }
    field_props_from_params(args.params, &xreal, &yreal, &unitxy, &zreal, &unitz);

    load_as_volume = args.npages > 1 && args.pages_are_uniform && gwy_params_get_boolean(args.params, PARAM_AS_VOLUME);
    if (load_as_volume) {
        bricks = g_new(GwyBrick*, nchannels);
        bricks[0] = gwy_brick_new(xres, yres, args.npages, xreal, yreal, args.npages, FALSE);
        if (unitxy) {
            gwy_unit_assign(gwy_brick_get_unit_x(bricks[0]), unitxy);
            gwy_unit_assign(gwy_brick_get_unit_y(bricks[0]), unitxy);
        }
        if (unitz)
            gwy_unit_assign(gwy_brick_get_unit_w(bricks[0]), unitz);
        for (guint i = 1; i < nchannels; i++)
            bricks[i] = gwy_brick_new_alike(bricks[0], FALSE);
    }

    file = gwy_file_new_in_construction();

    for (idx = id = 0; idx < args.npages; idx++) {
        GError *err = NULL;

        reader = gwy_tiff_image_reader_free(reader);
        reader = gwy_tiff_get_image_reader(tiff, idx, G_MAXUINT, &err);
        if (!reader) {
            g_warning("Ignoring directory %u: %s.", idx, err->message);
            g_clear_error(&err);
            continue;
        }

        if (load_as_volume) {
            if (!load_tiff_brick_levels(tiff, reader, nchannels, zreal, idx, bricks, error)) {
                g_clear_object(&file);
                goto fail;
            }
        }
        else {
            if (!load_tiff_channels(file, tiff, reader, filename,
                                    xreal, yreal, zreal, unitxy, unitz, &id, error)) {
                g_clear_object(&file);
                goto fail;
            }
        }
    }

    if (file && load_as_volume) {
        for (guint i = 0; i < nchannels; i++) {
            gwy_file_set_volume(file, i, bricks[i]);

            const gchar *title = channel_name(nchannels, i);
            if (title)
                gwy_file_set_title(file, GWY_FILE_VOLUME, i, title, FALSE);
            else {
                gwy_file_pass_title(file, GWY_FILE_VOLUME, i, g_strdup_printf("%s %u", _("Channel"), i));
            }

            if (title && gwy_stramong(title, "Red", "Green", "Blue", NULL)) {
                gchar *palette = g_strconcat("RGB-", title, NULL);
                gwy_file_set_palette(file, GWY_FILE_VOLUME, i, palette);
                g_free(palette);
            }
            gwy_log_add_import(file, GWY_FILE_VOLUME, i, "tiffbig", filename);
        }
        id = nchannels;
    }

    if (!id) {
        err_NO_DATA(error);
        g_clear_object(&file);
    }

fail:
    g_free(args.channels);
    if (bricks) {
        for (guint i = 0; i < nchannels; i++)
            g_clear_object(bricks + i);
        g_free(bricks);
    }
    g_clear_object(&args.params);
    g_clear_object(&unitxy);
    g_clear_object(&unitz);
    gwy_tiff_image_reader_free(reader);
    gwy_tiff_free(tiff);

    return file;
}

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

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

    gwy_clear1(gui);
    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(param_changed), &gui);
    g_signal_connect_swapped(gui.table_lateral, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.table_values, "param-changed", G_CALLBACK(param_changed), &gui);

    outcome = gwy_dialog_run(dialog);

    g_object_unref(results);

    return outcome;
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    ModuleArgs *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 : */
