/*
 *  $Id: pixmap.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2004-2025 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]
 * Pixmap images
 * .png, .jpeg, .tiff, .tga, .pnm, .bmp
 * Read[1] Export[2]
 * [1] Import support relies on Gdk-Pixbuf and hence may vary among systems.
 * [2] Usually lossy, intended for presentational purposes.  16bit grayscale export is possible to PNG, TIFF and PNM.
 **/

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

#include "config.h"
#include <glib/gi18n-lib.h>
#include <string.h>
#include <errno.h>
#include <math.h>
#include <stdarg.h>
#include <gdk/gdk.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "err.h"
#include "gwytiff.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_MAP_TYPE,
    PARAM_HUE_OFFSET,
    PARAM_XREAL,
    PARAM_YREAL,
    PARAM_ZREAL,
    PARAM_XYMEASUREEQ,
    PARAM_SIZE_IN_PIXELS,
    PARAM_XYUNIT,
    PARAM_ZUNIT,

    WIDGET_IMAGE_INFO,
};

/* What value is used when importing an image. */
typedef enum {
    PIXMAP_MAP_RED = 1,
    PIXMAP_MAP_GREEN,
    PIXMAP_MAP_BLUE,
    PIXMAP_MAP_VALUE,
    PIXMAP_MAP_SUM,
    PIXMAP_MAP_ALPHA,
    PIXMAP_MAP_LUMA,
    PIXMAP_MAP_ALL,
    PIXMAP_MAP_HUE,
    PIXMAP_MAP_GREY,
    PIXMAP_MAP_NTYPES = PIXMAP_MAP_GREY,
} PixmapMapType;

typedef struct {
    GwyParams *params;
    GdkPixbuf *pixbuf;
    /* Cached properties. */
    guint flags;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GwyParamTable *table_lateral;
    GwyParamTable *table_values;
    GdkPixbuf *small_pixbuf;
    GwyField *field;
} ModuleGUI;

typedef gboolean (*PixmapFilterFunc)(const GwyFileDetectInfo *fileinfo);

/* Static data about known (whitelisted) formats. */
typedef struct {
    const gchar *name;
    const gchar *description;
    GwyFileDetectFunc detect_file;
} PixmapKnownFormat;

/* Actually registered formats. */
typedef struct {
    const gchar *name;
    const gchar *description;
    const GdkPixbufFormat *pixbuf_format;
    GwyFileDetectFunc detect_file;
} PixmapFormatInfo;

static gboolean          module_register       (void);
static gint              detect_png            (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_jpeg           (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_tiff           (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_pnm            (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_bmp            (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_tga            (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_gif            (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_jpeg2000       (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_jpegxl         (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_pcx            (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_xpm            (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_ras            (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_icns           (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_webp           (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static gint              detect_heif           (const GwyFileDetectInfo *fileinfo,
                                                gboolean only_name);
static GwyFile*          load_file             (const gchar *filename,
                                                GwyRunModeFlags mode,
                                                GError **error);
static GdkPixbuf*        pixmap_load_pixbuf    (const gchar *filename,
                                                const gchar *name,
                                                GError **error);
static void              pixmap_set_field      (GwyFile *file,
                                                gint id,
                                                ModuleArgs *args,
                                                PixmapMapType maptype);
static void              pixmap_pixbuf_to_field(GdkPixbuf *pixbuf,
                                                GwyField *field,
                                                PixmapMapType maptype,
                                                gdouble hue_offset);
static GwyDialogOutcome  run_gui               (ModuleArgs *args,
                                                const gchar *name);
static void              param_changed         (ModuleGUI *gui,
                                                gint id);
static void              preview               (gpointer user_data);
static gboolean          mapping_type_filter   (const GwyEnum *enumval,
                                                gpointer user_data);
static void              pixmap_add_import_log (GwyFile *data,
                                                gint id,
                                                const gchar *filetype,
                                                const gchar *filename);
static PixmapFormatInfo* find_format           (const gchar *name);
static void              sanitise_params       (ModuleArgs *args);

static const GwyEnum map_types[PIXMAP_MAP_NTYPES] = {
    { N_("All channels"), PIXMAP_MAP_ALL,   },
    { N_("Red"),          PIXMAP_MAP_RED,   },
    { N_("Green"),        PIXMAP_MAP_GREEN, },
    { N_("Blue"),         PIXMAP_MAP_BLUE,  },
    { N_("Gray"),         PIXMAP_MAP_GREY,  },
    { N_("Value (max)"),  PIXMAP_MAP_VALUE, },
    { N_("RGB sum"),      PIXMAP_MAP_SUM,   },
    { N_("Luma"),         PIXMAP_MAP_LUMA,  },
    { N_("Hue"),          PIXMAP_MAP_HUE,   },
    { N_("Alpha"),        PIXMAP_MAP_ALPHA, },
};

/* Use a whitelist of safe formats for which we have at least basic weed-out pre-detection function.  GdkPixbuf
 * loaders tend to accept any rubbish as their format and then crash completely surprised when it isn't. */
static const PixmapKnownFormat known_formats[] = {
    { "png",      N_("Portable Network Graphics (.png)"),          detect_png,      },
    { "jpeg",     N_("JPEG (.jpeg,.jpg)"),                         detect_jpeg,     },
#ifndef __WIN64
    /* The TIFF loader (supposedly GDI-based) crashes on Win64.  Unclear why.  TIFF is madness.  There is a fallback
     * GwyTIFF-based loader in hdrimage which will take over when we exclude the GdkPixbuf-based one.  */
    { "tiff",     N_("TIFF (.tiff,.tif)"),                         detect_tiff,     },
#endif
    { "pnm",      N_("Portable Pixmap (.ppm,.pnm)"),               detect_pnm,      },
    { "bmp",      N_("Windows or OS2 Bitmap (.bmp)"),              detect_bmp,      },
    { "tga",      N_("TARGA (.tga,.targa)"),                       detect_tga,      },
    { "gif",      N_("Graphics Interchange Format GIF (.gif)"),    detect_gif,      },
    { "jpeg2000", N_("JPEG 2000 (.jpx)"),                          detect_jpeg2000, },
    { "jxl",      N_("JPEG XL (.jxl)"),                            detect_jpegxl,   },
    { "pcx",      N_("PCX (.pcx)"),                                detect_pcx,      },
    { "xpm",      N_("X Pixmap (.xpm)"),                           detect_xpm,      },
    { "ras",      N_("Sun raster image (.ras)"),                   detect_ras,      },
    { "icns",     N_("Apple icon (.icns)"),                        detect_icns,     },
    { "webp",     N_("WebP (.webp)"),                              detect_webp,     },
    { "heif",     N_("High Efficiency Image File Format (.heif)"), detect_heif,     },
};

/* List of PixmapFormatInfo for all formats.  Created in module_register() and never freed. */
static GSList *pixmap_formats = NULL;

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports data from low-depth pixmap images (PNG, TIFF, JPEG, ...).  The set of available formats depends on "
       "available GDK pixbuf loaders."),
    "Yeti <yeti@gwyddion.net>",
    "11.0",
    "David Nečas (Yeti)",
    "2004-2014",
};

GWY_MODULE_QUERY2(module_info, pixmap)

static gboolean
module_register(void)
{
    GSList *formats, *l;

    formats = gdk_pixbuf_get_formats();
    for (l = formats; l; l = g_slist_next(l)) {
        GdkPixbufFormat *pixbuf_format = (GdkPixbufFormat*)l->data;
        const PixmapKnownFormat *known_format = NULL;
        PixmapFormatInfo *format_info;
        gchar *fmtname;
        guint i;

        fmtname = gdk_pixbuf_format_get_name(pixbuf_format);
        if (gwy_strequal(fmtname, "heif/avif"))
            gwy_assign_string(&fmtname, "heif");
        gwy_debug("Found format %s", fmtname);
        for (i = 0; i < G_N_ELEMENTS(known_formats); i++) {
            if (gwy_strequal(fmtname, known_formats[i].name)) {
                known_format = known_formats + i;
                break;
            }
        }
        if (!known_format) {
            gwy_debug("Ignoring GdkPixbuf format %s because it is not on the whitelist.", fmtname);
            g_free(fmtname);
            continue;
        }

        gwy_debug("Format %s is known and whitelisted.  Proceeding.", fmtname);
        format_info = g_new0(PixmapFormatInfo, 1);
        format_info->name = fmtname;
        format_info->pixbuf_format = pixbuf_format;
        /* Copy from static data. */
        format_info->description = known_format->description;
        format_info->detect_file = known_format->detect_file;

        gwy_debug("Found GdkPixbuf loader for new type: %s", fmtname);
        gwy_file_func_register(format_info->name, format_info->description,
                               format_info->detect_file, load_file, NULL, NULL);
        pixmap_formats = g_slist_append(pixmap_formats, format_info);
    }

    g_slist_free(formats);

    return TRUE;
}

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

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    /* Try to keep this in sync with hrdimage which uses the same keys. */
    gwy_param_def_set_function_name(paramdef, "pixmap");
    gwy_param_def_add_gwyenum(paramdef, PARAM_MAP_TYPE, "maptype", _("Use"),
                              map_types, G_N_ELEMENTS(map_types), PIXMAP_MAP_VALUE);
    gwy_param_def_add_double(paramdef, PARAM_HUE_OFFSET, "hue_offset", _("_Hue offset"), 0.0, 6.0, 0.0);
    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);
    return paramdef;
}

static gint
pixbuf_detect_try_loader(const GwyFileDetectInfo *fileinfo, const gchar *loadername, gint score)
{
#ifdef DEBUG
    GError *err = NULL;
    GError **perr = &err;
#else
    GError **perr = NULL;
#endif

    GdkPixbufLoader *loader = gdk_pixbuf_loader_new_with_type(loadername, NULL);
    gwy_debug("Loader for type %s: %p", loadername, loader);
    if (!loader)
        return 0;

    if (!gdk_pixbuf_loader_write(loader, fileinfo->head, fileinfo->buffer_len, perr)) {
#ifdef DEBUG
        gwy_debug("%s", err->message);
        g_clear_error(&err);
#endif
        score = 0;
    }
    gdk_pixbuf_loader_close(loader, NULL);
    g_object_unref(loader);

    return score;
}

static GwyFile*
load_file(const gchar *filename,
          GwyRunModeFlags mode,
          GError **error)
{
    static const PixmapMapType rgbtypes[] = { PIXMAP_MAP_RED, PIXMAP_MAP_GREEN, PIXMAP_MAP_BLUE, PIXMAP_MAP_ALPHA };
    static const PixmapMapType greytypes[] = { PIXMAP_MAP_GREY, PIXMAP_MAP_ALPHA };

    GwyFile *data = NULL;
    GdkPixbuf *pixbuf;
    guchar *pixels, *p;
    gint bpp, i, j, width, height, rowstride, nimages;
    gboolean has_alpha;
    GwyParams *params;
    PixmapMapType maptype;
    const PixmapMapType *imgtypes;
    GwyDialogOutcome outcome;
    ModuleArgs args;
    const gchar *name = gwy_file_func_current();

    gwy_clear1(args);
    if (!(pixbuf = pixmap_load_pixbuf(filename, name, error)))
        return NULL;
    args.pixbuf = pixbuf;

    width = gdk_pixbuf_get_width(pixbuf);
    height = gdk_pixbuf_get_height(pixbuf);
    rowstride = gdk_pixbuf_get_rowstride(pixbuf);
    has_alpha = gdk_pixbuf_get_has_alpha(pixbuf);
    pixels = gdk_pixbuf_get_pixels(pixbuf);
    bpp = has_alpha ? 4 : 3;

    /* Check which value mapping methods seem feasible. */
    for (i = 0; i < height && !args.flags; i++) {
        p = pixels + i*rowstride;
        for (j = 0; j < width; j++) {
            guchar red = p[bpp*j], green = p[bpp*j+1], blue = p[bpp*j+2];
            if ((green ^ red) | (red ^ blue)) {
                args.flags |= PIXMAP_HAS_COLOURS;
                break;
            }
        }
    }
    if (has_alpha)
        args.flags |= PIXMAP_HAS_ALPHA;

    args.params = params = gwy_params_new_from_settings(define_module_params());
    sanitise_params(&args);

    if (mode == GWY_RUN_INTERACTIVE) {
        outcome = run_gui(&args, name);
        gwy_params_save_to_settings(params);
        if (outcome == GWY_DIALOG_CANCEL) {
            err_CANCELLED(error);
            goto end;
        }
    }

    data = gwy_file_new_in_construction();
    maptype = gwy_params_get_enum(params, PARAM_MAP_TYPE);
    if (maptype == PIXMAP_MAP_ALL) {
        if (args.flags & PIXMAP_HAS_COLOURS) {
            imgtypes = rgbtypes;
            nimages = 3;
        }
        else {
            imgtypes = greytypes;
            nimages = 1;
        }
        if (args.flags & PIXMAP_HAS_ALPHA)
            nimages++;
    }
    else {
        imgtypes = &maptype;
        nimages = 1;
    }

    for (i = 0; i < nimages; i++) {
        pixmap_set_field(data, i, &args, imgtypes[i]);
        pixmap_add_import_log(data, i, name, filename);
    }

end:
    g_object_unref(pixbuf);
    g_object_unref(params);

    return data;
}

static GdkPixbuf*
pixmap_load_pixbuf(const gchar *filename,
                   const gchar *name,
                   GError **error)
{
    enum { buffer_length = 4096 };
    guchar pixmap_buf[buffer_length];
    GdkPixbufLoader *loader;
    GdkPixbuf *pixbuf;
    PixmapFormatInfo *format_info;
    const gchar *loadername;
    GError *err = NULL;
    FILE *fh;
    guint n;

    gwy_debug("Loading <%s> as %s", filename, name);

    format_info = find_format(name);
    if (!format_info) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_UNIMPLEMENTED,
                    _("Pixmap has not registered file type `%s'."), name);
        return NULL;
    }

    if (!(fh = gwy_fopen(filename, "rb"))) {
        err_READ(error);
        return NULL;
    }

    loadername = (gwy_strequal(name, "heif") ? "heif/avif" : name);
    gwy_debug("Creating a loader for type %s", loadername);
    loader = gdk_pixbuf_loader_new_with_type(loadername, &err);
    if (!loader) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_SPECIFIC,
                    _("Cannot get pixbuf loader: %s."), err->message);
        g_clear_error(&err);
        fclose(fh);
        return NULL;
    }

    gwy_debug("Reading file content.");
    do {
        n = fread(pixmap_buf, 1, buffer_length, fh);
        gwy_debug("loaded %u bytes", n);
        if (!gdk_pixbuf_loader_write(loader, pixmap_buf, n, &err)) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Pixbuf loader refused data: %s."), err->message);
            g_clear_error(&err);
            g_object_unref(loader);
            fclose(fh);
            return NULL;
        }
    } while (n == buffer_length);
    fclose(fh);

    gwy_debug("Closing the loader.");
    if (!gdk_pixbuf_loader_close(loader, &err)) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Pixbuf loader refused data: %s."), err->message);
        g_clear_error(&err);
        g_object_unref(loader);
        return NULL;
    }

    gwy_debug("Trying to get the pixbuf.");
    pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
    gwy_debug("Pixbuf is: %p.", pixbuf);
    g_assert(pixbuf);
    g_object_ref(pixbuf);
    gwy_debug("Finalizing loader.");
    g_object_unref(loader);

    return pixbuf;
}

static void
pixmap_set_field(GwyFile *file,
                 gint id,
                 ModuleArgs *args,
                 PixmapMapType maptype)
{
    GwyParams *params = args->params;
    GdkPixbuf *pixbuf = args->pixbuf;
    GwyField *field;
    gint power10xy, power10z;

    field = gwy_field_new(gdk_pixbuf_get_width(pixbuf), gdk_pixbuf_get_height(pixbuf), 1.0, 1.0, FALSE);
    pixmap_pixbuf_to_field(pixbuf, field, maptype, gwy_params_get_double(params, PARAM_HUE_OFFSET)/6.0);

    gwy_unit_assign(gwy_field_get_unit_xy(field), gwy_params_get_unit(params, PARAM_XYUNIT, &power10xy));
    gwy_unit_assign(gwy_field_get_unit_z(field), gwy_params_get_unit(params, PARAM_ZUNIT, &power10z));
    gwy_field_set_xreal(field, gwy_params_get_double(params, PARAM_XREAL)*gwy_exp10(power10xy));
    gwy_field_set_yreal(field, gwy_params_get_double(params, PARAM_YREAL)*gwy_exp10(power10xy));
    gwy_field_multiply(field, gwy_params_get_double(params, PARAM_ZREAL)*gwy_exp10(power10z));

    gwy_file_pass_image(file, id, field);
    gwy_file_set_title(file, GWY_FILE_IMAGE, id, gwy_enum_to_string(maptype, map_types, PIXMAP_MAP_NTYPES), FALSE);
}

static void
pixmap_pixbuf_to_field(GdkPixbuf *pixbuf,
                       GwyField *field,
                       PixmapMapType maptype,
                       gdouble hue_offset)
{
    gwy_debug("%d", maptype);

    guchar *pixels = gdk_pixbuf_get_pixels(pixbuf);
    gint width = gdk_pixbuf_get_width(pixbuf);
    gint height = gdk_pixbuf_get_height(pixbuf);
    gint rowstride = gdk_pixbuf_get_rowstride(pixbuf);
    gint bpp = gdk_pixbuf_get_has_alpha(pixbuf) ? 4 : 3;
    gwy_field_resize(field, width, height);
    gdouble *val = gwy_field_get_data(field);

#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(pixels,val,width,height,rowstride,bpp,maptype,hue_offset)
#endif
    for (gint i = 0; i < height; i++) {
        guchar *p = pixels + i*rowstride;
        gdouble *r = val + i*width;

        switch (maptype) {
            case PIXMAP_MAP_ALPHA:
            p++;
            case PIXMAP_MAP_BLUE:
            p++;
            case PIXMAP_MAP_GREEN:
            p++;
            case PIXMAP_MAP_RED:
            case PIXMAP_MAP_GREY:
            for (gint j = 0; j < width; j++)
                r[j] = p[bpp*j]/255.0;
            break;

            case PIXMAP_MAP_VALUE:
            for (gint j = 0; j < width; j++) {
                guchar red = p[bpp*j], green = p[bpp*j+1], blue = p[bpp*j+2];
                guchar v = MAX(MAX(red, green), blue);

                r[j] = v/255.0;
            }
            break;

            case PIXMAP_MAP_SUM:
            for (gint j = 0; j < width; j++) {
                guchar red = p[bpp*j], green = p[bpp*j+1], blue = p[bpp*j+2];

                r[j] = (red + green + blue)/(3*255.0);
            }
            break;

            case PIXMAP_MAP_LUMA:
            for (gint j = 0; j < width; j++) {
                guchar red = p[bpp*j], green = p[bpp*j+1], blue = p[bpp*j+2];

                r[j] = (0.2126*red + 0.7152*green + 0.0722*blue)/255.0;
            }
            break;

            case PIXMAP_MAP_HUE:
            for (gint j = 0; j < width; j++) {
                guchar red = p[bpp*j], green = p[bpp*j+1], blue = p[bpp*j+2];
                gint cmax = MAX(MAX(red, green), blue);
                gint cmin = MIN(MIN(red, green), blue);
                gint delta = cmax - cmin;

                if (!delta)
                    r[j] = 0.0;
                else if (cmax == red)
                    r[j] = fmod(1.0/6.0*(green - blue)/delta + 1.0, 1.0);
                else if (cmax == green)
                    r[j] = 1.0/6.0*(blue - red)/delta + 1.0/3.0;
                else
                    r[j] = 1.0/6.0*(red - green)/delta + 2.0/3.0;

                r[j] -= hue_offset;
                if (r[j] < 0.0)
                    r[j] += 1.0;
            }
            break;

            default:
            g_assert_not_reached();
            break;
        }
    }
}

static const gchar*
describe_channels(guint flags)
{
    if (flags & PIXMAP_HAS_COLOURS)
        return (flags & PIXMAP_HAS_ALPHA) ? "R, G, B, A" : "R, G, B";
    else
        return (flags & PIXMAP_HAS_ALPHA) ? "G, A" : "G";
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, const gchar *name)
{
    ModuleGUI gui;
    GwyParamTable *table, *infotable;
    GtkWidget *hbox, *view, *label;
    GwyDialog *dialog;
    GwyDialogOutcome outcome;
    GwyResults *results;
    gint xres, yres, sxres, syres;
    gchar *s, *title;
    gdouble zoom;

    gwy_clear1(gui);
    gui.args = args;

    xres = gdk_pixbuf_get_width(args->pixbuf);
    yres = gdk_pixbuf_get_height(args->pixbuf);

    zoom = PREVIEW_SIZE/(gdouble)MAX(xres, yres);
    sxres = MAX(GWY_ROUND(zoom*xres), 1);
    syres = MAX(GWY_ROUND(zoom*yres), 1);
    gui.small_pixbuf = gdk_pixbuf_scale_simple(args->pixbuf, sxres, syres, GDK_INTERP_TILES);
    gui.field = gwy_field_new(sxres, syres, sxres, syres, TRUE);

    s = g_ascii_strup(name, -1);
    /* TRANSLATORS: Dialog title; %s is PNG, TIFF, ... */
    title = g_strdup_printf(_("Import %s"), s);
    g_free(s);
    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_fill_values(results, "xres", xres, "yres", yres, "channels", describe_channels(args->flags), NULL);

    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", NULL);
    /* TODO: If the file contains resultion/size in physical units, show it here. */
    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(gui.field, NULL, PREVIEW_SIZE);
    gtk_widget_set_halign(view, GTK_ALIGN_START);
    gtk_widget_set_valign(view, GTK_ALIGN_START);
    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);
    /* TODO: Add a button for taking dimensions from file. */
    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);
    gwy_param_table_append_combo(table, PARAM_MAP_TYPE);
    gwy_param_table_set_unitstr(table, PARAM_MAP_TYPE, _("as data"));
    gwy_param_table_combo_set_filter(table, PARAM_MAP_TYPE, mapping_type_filter, GUINT_TO_POINTER(args->flags), NULL);
    if (args->flags & PIXMAP_HAS_COLOURS)
        gwy_param_table_append_slider(table, PARAM_HUE_OFFSET);
    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), TRUE, TRUE, 0);

    if (args->flags & PIXMAP_HAS_COLOURS) {
        label = gtk_label_new(_("Warning: Colorful images cannot be reliably mapped to meaningful values."));
        gtk_label_set_xalign(GTK_LABEL(label), 0.0);
        gwy_set_widget_margins(label, 4, 4, 6, 6);
        gwy_dialog_add_content(dialog, label, FALSE, FALSE, 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);
    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_IMMEDIATE, preview, &gui, NULL);

    outcome = gwy_dialog_run(dialog);

    g_object_unref(gui.small_pixbuf);
    g_object_unref(gui.field);

    return outcome;
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    ModuleArgs *args = gui->args;
    GwyParams *params = args->params;
    PixmapMapType maptype = gwy_params_get_enum(params, PARAM_MAP_TYPE);

    if (maptype == PIXMAP_MAP_ALL)
        maptype = (args->flags & PIXMAP_HAS_COLOURS) ? PIXMAP_MAP_RED : PIXMAP_MAP_GREY;
    pixmap_pixbuf_to_field(gui->small_pixbuf, gui->field, maptype, gwy_params_get_double(params, PARAM_HUE_OFFSET)/6.0);
    gwy_field_data_changed(gui->field);
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    ModuleArgs *args = gui->args;
    GwyParams *params = args->params;
    PixmapMapType maptype = gwy_params_get_enum(params, PARAM_MAP_TYPE);
    gboolean size_in_pixels = gwy_params_get_boolean(params, PARAM_SIZE_IN_PIXELS);
    gboolean xymeasureeq = gwy_params_get_boolean(params, PARAM_XYMEASUREEQ);
    gint xres = gdk_pixbuf_get_width(args->pixbuf);
    gint yres = gdk_pixbuf_get_height(args->pixbuf);
    GwyUnit *unit;
    gint power10;
    GwyValueFormat *vf = NULL;

    if (id < 0 || id == PARAM_MAP_TYPE)
        gwy_param_table_set_sensitive(gui->table_values, PARAM_HUE_OFFSET, maptype == PIXMAP_MAP_HUE);

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

    if (id < 0 || id == PARAM_MAP_TYPE || id == PARAM_HUE_OFFSET)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static gboolean
mapping_type_filter(const GwyEnum *enumval, gpointer user_data)
{
    guint flags = GPOINTER_TO_UINT(user_data);

    if (enumval->value == PIXMAP_MAP_ALPHA)
        return flags & PIXMAP_HAS_ALPHA;
    if (enumval->value == PIXMAP_MAP_ALL)
        return flags;
    if (enumval->value == PIXMAP_MAP_GREY)
        return !(flags & PIXMAP_HAS_COLOURS);
    return flags & PIXMAP_HAS_COLOURS;
}

static void
pixmap_add_import_log(GwyFile *file,
                      gint id,
                      const gchar *filetype,
                      const gchar *filename)
{
    GQuark quark;
    gchar *myfilename = NULL, *fskey, *qualname;

    g_return_if_fail(filename);
    g_return_if_fail(filetype);
    g_return_if_fail(file);

    if (g_utf8_validate(filename, -1, NULL))
        myfilename = g_strdup(filename);
    if (!myfilename)
        myfilename = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
    if (!myfilename)
        myfilename = g_strescape(filename, NULL);

    fskey = g_strdup_printf("/module/%s/filename", filetype);
    quark = g_quark_from_string(fskey);
    g_free(fskey);

    /* Eats myfilename. */
    GwyDict *settings = gwy_app_settings_get();
    gwy_dict_pass_string(settings, quark, myfilename);

    qualname = g_strconcat("file::", filetype, NULL);
    gwy_log_add_full(file, GWY_FILE_IMAGE, -1, id, qualname, NULL);
    g_free(qualname);

    /* We know pixmap functions have no such setting as "filename". */
    gwy_dict_remove(settings, quark);
}

static PixmapFormatInfo*
find_format(const gchar *name)
{
    GSList *l;

    for (l = pixmap_formats; l; l = g_slist_next(l)) {
        PixmapFormatInfo *format_info = (PixmapFormatInfo*)l->data;
        if (gwy_strequal(format_info->name, name))
            return format_info;
    }

    return NULL;
}

static void
sanitise_params(ModuleArgs *args)
{
    GwyParams *params = args->params;
    PixmapMapType maptype = gwy_params_get_enum(params, PARAM_MAP_TYPE);
    gint xres = gdk_pixbuf_get_width(args->pixbuf);
    gint yres = gdk_pixbuf_get_height(args->pixbuf);
    guint i;

    if (gwy_params_get_boolean(params, PARAM_SIZE_IN_PIXELS)) {
        gwy_params_set_unit(params, PARAM_XYUNIT, NULL);
        gwy_params_set_boolean(params, PARAM_XYMEASUREEQ, TRUE);
        gwy_params_set_double(params, PARAM_XREAL, xres);
        gwy_params_set_double(params, PARAM_YREAL, yres);
    }
    else if (gwy_params_get_boolean(params, PARAM_XYMEASUREEQ)) {
        gdouble xreal = gwy_params_get_double(params, PARAM_XREAL);
        gwy_params_set_double(params, PARAM_YREAL, yres*xreal/xres);
    }

    for (i = 0; i < G_N_ELEMENTS(map_types); i++) {
        const GwyEnum *enumval = map_types + i;
        if (enumval->value == maptype) {
            if (!mapping_type_filter(enumval, GUINT_TO_POINTER(args->flags))) {
                gwy_params_set_enum(params, PARAM_MAP_TYPE,
                                    (args->flags & PIXMAP_HAS_COLOURS) ? PIXMAP_MAP_VALUE : PIXMAP_MAP_GREY);
            }
            return;
        }
    }
    g_assert_not_reached();
}

/* Gdk-pixbuf is crap at detecting file types. And creating loaders is a big overhead.
 *
 * Use quick filters to get rid of most files which are clearly not in the right format. Then try to feed the initial
 * part of the file to a pixbuf loader. This is expensive but we only do it on a likely match. */
static gint
detect_png(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 8 || memcmp(fileinfo->head, "\x89PNG\r\n\x1a\n", 8))
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "png", 70);
}

static gint
detect_jpeg(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 4 || memcmp(fileinfo->head, "\xff\xd8\xff", 3))
        return 0;
    /* We do not know what the next thing is, but the marker values (the second byte after 0xff) come from a
     * a relatively limited set. */
    if (fileinfo->head[3] < 0xd0 && fileinfo->head[3] != 0xc0 && fileinfo->head[3] != 0xc2 && fileinfo->head[3] != 0xc4)
        return 0;
    if (fileinfo->head[3] > 0xf0 && fileinfo->head[3] != 0xfe)
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "jpeg", 70);
}

/* Unused on Win64 */
G_GNUC_UNUSED
static gint
detect_tiff(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name)
        return FALSE;

    /* The pixbuf loader is unlikely to load BigTIFFs any time soon. */
    GwyTIFFVersion version = GWY_TIFF_CLASSIC;
    gwy_debug("Checking TIFF header");
    if (!gwy_tiff_detect(fileinfo->head, fileinfo->buffer_len, &version, NULL))
        return 0;
    gwy_debug("TIFF header OK (type %.2s)", fileinfo->head);
    /* The TIFF loaders (both libTIFF and GDI-based) seem to crash on broken TIFFs a way too often.  Do not try to
     * feed anything to it just accept the file is a TIFF and hope some other loader of a TIFF-based format will claim
     * it with a higher score. */
    gwy_debug("Avoiding feeding data to TIFF pixbuf loader because it is broken as hell.");
    /* Please someone return a higher score… */
    return 60;
}

static gint
detect_pnm(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 3
        || fileinfo->head[0] != 'P' || !g_ascii_isdigit(fileinfo->head[1]) || !g_ascii_isspace(fileinfo->head[2]))
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "pnm", 60);
}

static gint
detect_bmp(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 2 || memcmp(fileinfo->head, "BM", 2))
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "bmp", 60);
}

static gint
detect_tga(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 3)
        return 0;

    guint8 cmtype = fileinfo->head[1], dtype = fileinfo->head[2];
    if (dtype == 1 || dtype == 9 || dtype == 32 || dtype == 33) {
        if (cmtype != 1)
            return 0;
    }
    else if (dtype == 2 || dtype == 3 || dtype == 10 || dtype == 11) {
        if (cmtype != 0)
            return 0;
    }
    else
        return 0;

    return pixbuf_detect_try_loader(fileinfo, "tga", 70);
}

static gint
detect_gif(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 4 || memcmp(fileinfo->head, "GIF8", 4))
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "gif", 70);
}

static gint
detect_jpeg2000(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    static const guchar magic[] =
        "\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A\x00\x00\x00\x14\x66\x74\x79\x70\x6A\x70\x32";

    if (only_name || fileinfo->buffer_len < 23 || memcmp(fileinfo->head, magic, 23))
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "jpeg2000", 70);
}

static gint
detect_jpegxl(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    /* The two-byte one is silly, but used for standalone images in the wild. */
    static const guchar magic[] = "\x00\x00\x00\x0C\x4A\x58\x4C\x20\x0D\x0A\x87\x0A";
    static const guchar magic2[] = "\xFF\x0A";

    if (only_name || fileinfo->buffer_len < 12)
        return 0;

    if (!memcmp(fileinfo->head, magic, 12))
        return pixbuf_detect_try_loader(fileinfo, "jxl", 70);
    if (!memcmp(fileinfo->head, magic2, 2))
        return pixbuf_detect_try_loader(fileinfo, "jxl", 60);
    return 0;
}

static gint
detect_pcx(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 2 || fileinfo->head[0] != '\x0a' || fileinfo->head[1] > 0x05)
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "pcx", 60);
}

static gint
detect_xpm(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 9 || memcmp(fileinfo->head, "/* XPM */", 9))
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "xpm", 70);
}

static gint
detect_ras(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 4 || memcmp(fileinfo->head, "\x59\xa6\x6a\x95", 4))
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "ras", 70);
}

static gint
detect_icns(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 4 || memcmp(fileinfo->head, "icns", 4))
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "icns", 70);
}

static gint
detect_webp(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name || fileinfo->buffer_len < 15
        || memcmp(fileinfo->head, "RIFF", 4) || memcmp(fileinfo->head + 8, "WEBPVP8", 7))
        return 0;
    return pixbuf_detect_try_loader(fileinfo, "webp", 70);
}

static gint
detect_heif(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    const guchar *h8 = fileinfo->head + 8;
    if (only_name || fileinfo->buffer_len < 12 || memcmp(fileinfo->head + 4, "ftyp", 4)
        || (memcmp(h8, "mif1", 4) && memcmp(h8, "heic", 4) && memcmp(h8, "heix", 4)))
        return 0;
    /* The pixbuf format name is heif/avif. What a silly name. */
    return pixbuf_detect_try_loader(fileinfo, "heif/avif", 70);
}

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