/*
 *  $Id: sensofar.c 29611 2026-03-17 10:31:06Z yeti-dn $
 *  Copyright (C) 2004-2025 David Necas (Yeti), Petr Klapetek, Jan Horak.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net, xhorak@gmail.com.
 *
 *  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.
 */
/* TODO: xyz maps, metadata */

/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-sensofar-spm">
 *   <comment>Sensofar PLu data</comment>
 *   <glob pattern="*.plu"/>
 *   <glob pattern="*.PLU"/>
 *   <glob pattern="*.apx"/>
 *   <glob pattern="*.APX"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * Sensofar PLu
 * .plu, .apx
 * Read
 **/

#include "config.h"
#include <glib/gi18n-lib.h>
#include <gwy.h>

#include "get.h"
#include "err.h"

#define Micrometer (1e-6)

enum {
    DATE_SIZE    = 128,
    COMMENT_SIZE = 256,
    HEADER_SIZE  = 500,
    LOST_PIXELS  = 1000001,
};

typedef enum {
    MES_IMATGE                 = 0,
    MES_PERFIL                 = 1,
    MES_MULTIPERFIL            = 2,
    MES_TOPO                   = 3,
    MES_COORD_MULTIPLE_PROFILE = 4,
    MES_GRUIX                  = 5,
    MES_CUSTOM                 = 6,
    MES_COORD_TOPO_MAP         = 7,
    MES_COORD_THICKNESS_MAP    = 8,
} MeasurementType;

typedef enum {
    ALG_INTEN            = 0,
    ALG_GRADIENT         = 1,
    ALG_PSI              = 2,
    ALG_VSI              = 3,
    ALG_EPSI             = 4,
    ALG_THICK_CONFOCAL   = 5,
    ALG_THICK_INTERF     = 6,
    ALG_FOCUS_VARIATION1 = 7,
    ALG_TRACKING         = 8,
    ALG_SCANNING         = 9,
    ALG_FOCUS_VARIATION2 = 10,
    ALG_VSI_SNR          = 11,
    ALG_VSI_CM           = 12,
    ALG_CSSS             = 13,
    ALG_CSDS             = 14,
    ALG_CSQS             = 15,
    ALG_RP               = 16,
} AcquisitionAlgorithm;

typedef enum {
    ACQ_T_NORMAL    = 0,
    ACQ_T_STITCHING = 1,
} AcquisitionMethodTopo;

typedef enum {
    ACQ_IM_CONVENCIONAL  = 0,
    ACQ_IM_CONFOCAL      = 1,
    ACQ_IM_EXTENDED      = 2,
    ACQ_IM_CONFOCALCOLOR = 3,
} AcquisitionMethodImatge;

typedef enum {
    ACQ_P_1LINIA  = 0,
    ACQ_P_NLINIA  = 1,
} AcquisitionMethodPerfil;

typedef enum {
    ACQ_MP_NORMAL    = 0,
    ACQ_MP_STITCHING = 1,
} AcquisitionMethodMultiPerfil;

typedef enum {
    ACQ_MAP_PROFILE_0           = 0,
    ACQ_MAP_PROFILES_0_90       = 1,
    ACQ_MAP_MULTIPLE_PROFILES_A = 2,
    ACQ_MAP_GRID                = 3,
} AcquisitionMethodMap;

typedef enum {
    OBJ_DESCONEGUT           = 0,
    OBJ_SLWD_20X             = 1,
    OBJ_SLWD_50X             = 2,
    OBJ_SLWD_100X            = 3,
    OBJ_EPI_20X              = 4,
    OBJ_EPI_50X              = 5,
    OBJ_EPI_10X              = 6,
    OBJ_EPI_100X             = 7,
    OBJ_ELWD_10X             = 8,
    OBJ_ELWD_20X             = 9,
    OBJ_ELWD_50X             = 10,
    OBJ_ELWD_100X            = 11,
    OBJ_TI_2_5X              = 12,
    OBJ_TI_5X                = 13,
    OBJ_DI_10X               = 14,
    OBJ_DI_20X               = 15,
    OBJ_DI_50X               = 16,
    OBJ_EPI_5X               = 17,
    OBJ_EPI_150X             = 18,
    OBJ_EPI_50X_PLANAPO      = 19,
    OBJ_EPI_1_5X             = 20,
    OBJ_EPI_2_5X             = 21,
    OBJ_EPI_100X_PLANAPO     = 22,
    OBJ_EPI_200X             = 23,
    OBJ_WATER_10X            = 24,
    OBJ_WATER_20X            = 25,
    OBJ_WATER_150X           = 26,
    OBJ_CRLCD_20X_ELWD       = 27,
    OBJ_CRLCD_20X            = 28,
    OBJ_CRLCD_50X            = 29,
    OBJ_CRLCD_100X_A         = 30,
    OBJ_CRLCD_100X_B         = 31,
    OBJ_EPI_2_5X_LEICA       = 32,
    OBJ_EPI_5X_LEICA         = 33,
    OBJ_EPI_10X_LEICA        = 34,
    OBJ_EPI_20X_LEICA        = 35,
    OBJ_EPI_50X_LEICA        = 36,
    OBJ_EPI_50X_0_9_LEICA    = 37,
    OBJ_EPI_100X_LEICA       = 38,
    OBJ_EPI_150X_LEICA       = 39,
    OBJ_LWD_10X_LEICA        = 40,
    OBJ_LWD_20X_LEICA        = 41,
    OBJ_LWD_50X_LEICA        = 42,
    OBJ_LWD_100X_LEICA       = 43,
    OBJ_MICHELSON_5X_LEICA   = 44,
    OBJ_MIRAU_10X_LEICA      = 45,
    OBJ_MIRAU_20X_LEICA      = 46,
    OBJ_MIRAU_50X_LEICA      = 47,
    OBJ_LINNIK_EPI_20X_NIKON = 48,
    OBJ_DI_100X              = 49,
    OBJ_EPI_1_25X_LEICA      = 50,
    OBJ_EPI_20X_LNA_LEICA    = 51,
    OBJ_EPI_40X_LEICA        = 52,
    OBJ_EPI_50X_LNA_LEICA    = 53,
    OBJ_EPI_100X_HNA_LEICA   = 54,
    OBJ_WATER_20X_LEICA      = 55,
    OBJ_WATER_40X_LEICA      = 56,
    OBJ_WATER_63X_LEICA      = 57,
    OBJ_CRLCD_20X_LEICA      = 58,
    OBJ_CRLCD_40X_LEICA      = 59,
    OBJ_MIRAU_5X_SR_LEICA    = 60,
    OBJ_MIRAU_10X_SR_        = 61,
    OBJ_MIRAU_20X_SR_        = 62,
    OBJ_MIRAU_50X_SR_        = 63,
    OBJ_MIRAU_100X_SR_       = 64,
    OBJ_EPI_50X_0_8_LEICA    = 72,
    OBJ_EPI_100X_0_9_LEICA   = 73,
    OBJ_EPI_1X_v35           = 74,
    OBJ_EPI_2_5X_v35         = 75,
    OBJ_EPI_5X_v35           = 76,
    OBJ_EPI_10X_v35          = 77,
    OBJ_EPI_20X_v35          = 78,
    OBJ_EPI_50X_v35          = 79,
    OBJ_EPI_100X_v35         = 80,
    OBJ_EPI_150X_v35         = 81,
    OBJ_ELWD_20X_v35         = 82,
    OBJ_ELWD_50X_v35         = 83,
    OBJ_ELWD_100X_v35        = 84,
    OBJ_SLWD_10X_v35         = 85,
    OBJ_SLWD_20X_v35         = 86,
    OBJ_SLWD_50X_v35         = 87,
    OBJ_SLWD_100X_v35        = 88,
    OBJ_WATER_60X_v35        = 89,
    OBJ_EPI_50X_v50          = 90,
    OBJ_EPI_100X_v50         = 91,
    OBJ_EPI_150X_v50         = 92,
} ObjectiveType;

typedef enum {
    AREA_128         = 0,
    AREA_256         = 1,
    AREA_512         = 2,
    AREA_MAX         = 3,  /* According to hardware below. */
    AREA_L256        = 4,
    AREA_L128        = 5,
    AREA_COORDINATES = 6,
} AreaType;

typedef enum {
    /* 768x576 */
    HWCONF_PLU             = 0,
    HWCONF_PLU_2300_XGA    = 1,
    HWCONF_PLU_2300_XGA_T5 = 2,
    HWCONF_PLU_2300_SXGA   = 3,
    HWCONF_PLU_3300        = 4,
    HWCONF_DCM_3D          = 5,
    HWCONF_PLU_NEOX        = 6,
    HWCONF_DCM_3D_R2       = 7,
    HWCONF_PLU_APEX_P      = 8,
    HWCONF_PLU_NEOX_R2     = 9,
    /* 1360x1024 */
    HWCONF_DCM_3D_R3       = 10,
    HWCONF_PLU_APEX        = 11,
} HardwareConfiguration;

typedef enum {
    FORMAT_VERSION_2000  = 0x00,
    FORMAT_VERSION_2013  = 0xfa,
    FORMAT_VERSION_2012  = 0xfb,
    FORMAT_VERSION_2011B = 0xfc,
    FORMAT_VERSION_2011  = 0xfd,
    FORMAT_VERSION_2010A = 0xfe,
    FORMAT_VERSION_2006  = 0xff,
} FormatVersion;

typedef struct {
    gchar str[DATE_SIZE];
    time_t t;
} SensofarDate;

typedef struct {
    gint32 xres_area;
    gint32 yres_area;
    gint32 xres;
    gint32 yres;
    gint32 na;
    gdouble incr_z;
    gdouble range;
    gint32 n_planes;
    gint32 tpc_umbral_F;
} SensofarFOVScanSettings;

typedef struct {
    gdouble tracking_range;
    gdouble tracking_speed;
    gint32 tracking_direction;
    gdouble tracking_threshold;
    gdouble tracking_min_angle;
    gint32 confocal_scan_type;
    gdouble confocal_scan_range;
    gdouble confocal_speed_factor;
    gdouble confocal_threshold;
    guchar reserved[4];
} SensofarPointScanSettings;

typedef struct {
    MeasurementType type;
    AcquisitionAlgorithm algorithm;
    guint method;  /* Some of the Method enums, depending on MeasurementType. */
    ObjectiveType objective;
    AreaType area_type;
    /* For AREA_COORDINATES it is point; otherwise it is fov. */
    union {
        SensofarFOVScanSettings fov;
        SensofarPointScanSettings point;
    } settings;
    gboolean restore;
    guint num_layers;
    FormatVersion version;
    HardwareConfiguration config_hardware;
    guint num_images;
    guint reserved;
    gint32 factor_delmacio;
} SensofarConfigMesura;

typedef struct  {
    guint32 yres;
    guint32 xres;
    guint32 N_tall;
    gdouble dy_multip;
    gdouble mppx;
    gdouble mppy;
    /* (x_0, y_0, z_0) is used as the origin also for XYZ data (maps), but
     * not the rest of the struct. */
    gdouble x_0;
    gdouble y_0;
    gdouble mpp_tall;
    gdouble z_0;
} SensofarCalibratEixos_Arxiu;

typedef struct {
    SensofarDate date;
    gchar user_comment[COMMENT_SIZE];
    SensofarCalibratEixos_Arxiu axes_config;
    SensofarConfigMesura measure_config;
} SensofarDataDesc;

static gboolean module_register       (void);
static gint     detect_file           (const GwyFileDetectInfo *fileinfo,
                                       gboolean only_name);
static GwyFile* load_file             (const gchar *filename,
                                       GwyRunModeFlags mode,
                                       GError **error);
static gboolean read_calibration_block(const guchar **p,
                                       gsize size,
                                       SensofarCalibratEixos_Arxiu *axes_config,
                                       GError **error);
static gboolean read_config_mesura    (const guchar **p,
                                       gsize size,
                                       SensofarConfigMesura *measure_config,
                                       GError **error);
static gboolean read_float_field      (SensofarDataDesc *data_desc,
                                       guint nrgb,
                                       const gchar *filename,
                                       GwyFile *file,
                                       guint *channelno,
                                       const guchar **p,
                                       gsize size,
                                       GError **error);
static gboolean read_rgb_field        (SensofarDataDesc *data_desc,
                                       const gchar *filename,
                                       GwyFile *file,
                                       guint *channelno,
                                       const guchar **p,
                                       gsize size,
                                       GError **error);
static gboolean read_rgb_data         (SensofarDataDesc *data_desc,
                                       guint xres,
                                       guint yres,
                                       const gchar *filename,
                                       GwyFile *file,
                                       guint *channelno,
                                       const guchar **p,
                                       gsize size,
                                       GError **error);
static gboolean read_profiles         (SensofarDataDesc *data_desc,
                                       const gchar *filename,
                                       GwyFile *file,
                                       guint *channelno,
                                       const guchar **p,
                                       gsize size,
                                       GError **error);
static void     add_image_meta        (SensofarDataDesc *data_desc,
                                       GwyFile *data,
                                       guint channelno);
static gboolean parses_as_date        (const gchar *str);

static const GwyEnum versions[] = {
    { "2000",  FORMAT_VERSION_2000,  },
    { "2013",  FORMAT_VERSION_2013,  },
    { "2012",  FORMAT_VERSION_2012,  },
    { "2011B", FORMAT_VERSION_2011B, },
    { "2011",  FORMAT_VERSION_2011,  },
    { "2010A", FORMAT_VERSION_2010A, },
    { "2006",  FORMAT_VERSION_2006,  },
};

static const GwyEnum meas_types[] = {
    { "Confocal Image",               MES_IMATGE,                 },
    { "Profile",                      MES_PERFIL,                 },
    { "Multiple profile",             MES_MULTIPERFIL,            },
    { "Topography",                   MES_TOPO,                   },
    { "Coordinates Multiple Profile", MES_COORD_MULTIPLE_PROFILE, },
    { "Single Point Thickness",       MES_GRUIX,                  },
    { "Custom Application",           MES_CUSTOM,                 },
    { "Coordinates Topography Map",   MES_COORD_TOPO_MAP,         },
    { "Coordinates Thickness Map",    MES_COORD_THICKNESS_MAP,    },
};

static const GwyEnum acq_methods_topo[] = {
    { "Topography",          ACQ_T_NORMAL,    },
    { "Extended Topography", ACQ_T_STITCHING, },
};

static const GwyEnum acq_methods_imatge[] = {
    { "Conventional Image", ACQ_IM_CONVENCIONAL,  },
    { "Confocal Image",     ACQ_IM_CONFOCAL,      },
    { "Extended Image",     ACQ_IM_EXTENDED,      },
    { "Confocal RGB",       ACQ_IM_CONFOCALCOLOR, },
};

static const GwyEnum acq_methods_perfil[] = {
    { "Single Profile",   ACQ_P_1LINIA },
    { "Extended Profile", ACQ_P_NLINIA },
};

static const GwyEnum acq_methods_multiperfil[] = {
    { "Multiple Profile",          ACQ_MP_NORMAL,    },
    { "Extended Multiple Profile", ACQ_MP_STITCHING, },
};

static const GwyEnum acq_methods_map[] = {
    { "0° profile method",              ACQ_MAP_PROFILE_0,           },
    { "0° and 90° method",              ACQ_MAP_PROFILES_0_90,       },
    { "Multiple profiles at any angle", ACQ_MAP_MULTIPLE_PROFILES_A, },
    { "Map using Grid",                 ACQ_MAP_GRID,                },
};

static const GwyEnum algorithms[] = {
    { "Confocal Intensity",                        ALG_INTEN,            },
    { "Confocal Gradient",                         ALG_GRADIENT,         },
    { "Interferometric PSI",                       ALG_PSI,              },
    { "Interferometric VSI",                       ALG_VSI,              },
    { "Interferometric ePSI",                      ALG_EPSI,             },
    { "Confocal thickness",                        ALG_THICK_CONFOCAL,   },
    { "Interferometric thickness",                 ALG_THICK_INTERF,     },
    { "Focus Variation",                           ALG_FOCUS_VARIATION1, },
    { "Tracking & confocal (to measure apex)",     ALG_TRACKING,         },
    { "Confocal only (PLu apex system)",           ALG_SCANNING,         },
    { "Focus Variation",                           ALG_FOCUS_VARIATION2, },
    { "Interferometric VSI Smart Noise Reduction", ALG_VSI_SNR,          },
    { "Interferometric VSI Centre of Mass",        ALG_VSI_CM,           },
    { "Confocal Coarse Shift Single Sampling",     ALG_CSSS,             },
    { "Confocal Coarse Shift Double Sampling",     ALG_CSDS,             },
    { "Confocal Coarse Shift Quadrupe Sampling",   ALG_CSQS,             },
    { "Confocal Random Points",                    ALG_RP,               },
};

static const GwyEnum area_types[] = {
    { "128×128 pixels",                      AREA_128,         },
    { "256×256 pixels",                      AREA_256,         },
    { "512×512 pixels",                      AREA_512,         },
    { "Camera rows × Camera columns pixels", AREA_MAX,         },
    { "256 × Col. CCD columns pixels",       AREA_L256,        },
    { "128 × Col. CCD columns pixels",       AREA_L128,        },
    { "Coordinates",                         AREA_COORDINATES, },
};

static const GwyEnum config_hardwares[] = {
    { "PLµ",                       HWCONF_PLU,             },
    { "PLµ 2300, XGA (2003)",      HWCONF_PLU_2300_XGA,    },
    { "PLµ 2300, XGA T5 (2004)",   HWCONF_PLU_2300_XGA_T5, },
    { "PLµ 2300, SXGA (2006)",     HWCONF_PLU_2300_SXGA,   },
    { "PLµ 3300 (2006)",           HWCONF_PLU_3300,        },
    { "DCM 3D (2008)",             HWCONF_DCM_3D,          },
    { "PLu Neox (2009)",           HWCONF_PLU_NEOX,        },
    { "DCM 3D rev 2 (2009)",       HWCONF_DCM_3D_R2,       },
    { "PLu Apex prototype (2010)", HWCONF_PLU_APEX_P,      },
    { "S neox (2013)",             HWCONF_PLU_NEOX_R2,     },
    { "DCM8 (2013)",               HWCONF_DCM_3D_R3,       },
    { "PLu Apex (2012)",           HWCONF_PLU_APEX,        },
};

static const GwyEnum objectives[] = {
    { "Unknown",                                                OBJ_DESCONEGUT,           },
    { "Nikon CFI Fluor Plan EPI SLWD 20x",                      OBJ_SLWD_20X,             },
    { "Nikon CFI Fluor Plan EPI SLWD 50x",                      OBJ_SLWD_50X,             },
    { "Nikon CFI Fluor Plan EPI SLWD 100x",                     OBJ_SLWD_100X,            },
    { "Nikon CFI Fluor Plan EPI 20x",                           OBJ_EPI_20X,              },
    { "Nikon CFI Fluor Plan EPI 50x",                           OBJ_EPI_50X,              },
    { "Nikon CFI Fluor Plan EPI 10x",                           OBJ_EPI_10X,              },
    { "Nikon CFI Fluor Plan EPI 100x",                          OBJ_EPI_100X,             },
    { "Nikon CFI Fluor Plan EPI ELWD 10x",                      OBJ_ELWD_10X,             },
    { "Nikon CFI Fluor Plan EPI ELWD 20x",                      OBJ_ELWD_20X,             },
    { "Nikon CFI Fluor Plan EPI ELWD 50x",                      OBJ_ELWD_50X,             },
    { "Nikon CFI Fluor Plan EPI ELWD 100x",                     OBJ_ELWD_100X,            },
    { "Nikon CFI Plan Interferential 2.5X",                     OBJ_TI_2_5X,              },
    { "Nikon CFI Plan Interferential 5X T",                     OBJ_TI_5X,                },
    { "Nikon CFI Plan Interferential 10X",                      OBJ_DI_10X,               },
    { "Nikon CFI Plan Interferential 20X",                      OBJ_DI_20X,               },
    { "Nikon CFI Plan Interferential 50X",                      OBJ_DI_50X,               },
    { "Nikon CFI Fluor Plan EPI 5X",                            OBJ_EPI_5X,               },
    { "Nikon CFI Fluor Plan EPI 150X",                          OBJ_EPI_150X,             },
    { "Nikon CFI Fluor Plan Apo EPI 50X",                       OBJ_EPI_50X_PLANAPO,      },
    { "Nikon CFI Fluor Plan EPI 1.5X",                          OBJ_EPI_1_5X,             },
    { "Nikon CFI Fluor Plan EPI 2.5X",                          OBJ_EPI_2_5X,             },
    { "Nikon CFI Fluor Plan Apo EPI 100X",                      OBJ_EPI_100X_PLANAPO,     },
    { "Nikon CFI Fluor Plan EPI 200X",                          OBJ_EPI_200X,             },
    { "Nikon CFI Plan Water Immersion 10X",                     OBJ_WATER_10X,            },
    { "Nikon CFI Plan Water Immersion 20X",                     OBJ_WATER_20X,            },
    { "Nikon CFI Plan Water Immersion 150X",                    OBJ_WATER_150X,           },
    { "Nikon CFI Plan EPI CR ELWD 10X",                         OBJ_CRLCD_20X_ELWD,       },
    { "Nikon CFI Plan EPI CR 20X",                              OBJ_CRLCD_20X,            },
    { "Nikon CFI Plan EPI CR 50X",                              OBJ_CRLCD_50X,            },
    { "Nikon CFI Plan EPI CR 100X A",                           OBJ_CRLCD_100X_A,         },
    { "Nikon CFI Plan EPI CR 100X B",                           OBJ_CRLCD_100X_B,         },
    { "Leica HCX FL Plan 2.5X",                                 OBJ_EPI_2_5X_LEICA,       },
    { "Leica HC PL Fluotar EPI 5X",                             OBJ_EPI_5X_LEICA,         },
    { "Leica HC PL Fluotar EPI 10X",                            OBJ_EPI_10X_LEICA,        },
    { "Leica HC PL Fluotar EPI 20X",                            OBJ_EPI_20X_LEICA,        },
    { "Leica HC PL Fluotar EPI 50X",                            OBJ_EPI_50X_LEICA,        },
    { "Leica HC PL Fluotar EPI 50X HNA",                        OBJ_EPI_50X_0_9_LEICA,    },
    { "Leica HC PL Fluotar EPI 100X",                           OBJ_EPI_100X_LEICA,       },
    { "Leica HC PL Fluotar EPI 50X",                            OBJ_EPI_150X_LEICA,       },
    { "Leica N Plan EPI LWD 10X",                               OBJ_LWD_10X_LEICA,        },
    { "Leica N Plan EPI LWD 20X",                               OBJ_LWD_20X_LEICA,        },
    { "Leica HCX PL Fluotar LWD 50X",                           OBJ_LWD_50X_LEICA,        },
    { "Leica HCX PL Fluotar LWD 100X",                          OBJ_LWD_100X_LEICA,       },
    { "Leica HC PL Fluotar – Interferential Michelson MR 5X", OBJ_MICHELSON_5X_LEICA,   },
    { "Leica HC PL Fluotar – Interferential Mirau MR 10X",    OBJ_MIRAU_10X_LEICA,      },
    { "Leica N PLAN H - Interferential Mirau MR 20X",           OBJ_MIRAU_20X_LEICA,      },
    { "Leica N PLAN H -Interferential Mirau MR 50X",            OBJ_MIRAU_50X_LEICA,      },
    { "Nikon Interferential Linnik EPI 20X",                    OBJ_LINNIK_EPI_20X_NIKON, },
    { "Nikon CFI Plan Interferential 100X DI",                  OBJ_DI_100X,              },
    { "Leica HCX PL FLUOTAR 1.25X",                             OBJ_EPI_1_25X_LEICA,      },
    { "Leica N PLAN EPI 20X",                                   OBJ_EPI_20X_LNA_LEICA,    },
    { "Leica N PLAN EPI 40X",                                   OBJ_EPI_40X_LEICA,        },
    { "Leica N PLAN L 50X",                                     OBJ_EPI_50X_LNA_LEICA,    },
    { "Leica PL APO 100X",                                      OBJ_EPI_100X_HNA_LEICA,   },
    { "Leica HCX APO L U-V-I 20X",                              OBJ_WATER_20X_LEICA,      },
    { "Leica HCX APO L U-V-I 40X",                              OBJ_WATER_40X_LEICA,      },
    { "Leica HCX APO L U-V-I 63X",                              OBJ_WATER_63X_LEICA,      },
    { "Leica HCX PL FLUOTAR 20X",                               OBJ_CRLCD_20X_LEICA,      },
    { "Leica N PLAN L 40X",                                     OBJ_CRLCD_40X_LEICA,      },
    { "Leica Interferential Mirau SR 5X",                       OBJ_MIRAU_5X_SR_LEICA,    },
    { "Leica Interferential Mirau SR 10X",                      OBJ_MIRAU_10X_SR_,        },
    { "Leica Interferential Mirau SR 20X",                      OBJ_MIRAU_20X_SR_,        },
    { "Leica Interferential Mirau SR 50X",                      OBJ_MIRAU_50X_SR_,        },
    { "Leica Interferential Mirau SR 100X",                     OBJ_MIRAU_100X_SR_,       },
    { "Leica HC PL Fluotar EPI 50X 0.8",                        OBJ_EPI_50X_0_8_LEICA,    },
    { "Leica HC PL Fluotar EPI 100X 0.9",                       OBJ_EPI_100X_0_9_LEICA,   },
    { "Nikon CFI T Plan EPI 1X",                                OBJ_EPI_1X_v35,           },
    { "Nikon CFI T Plan EPI 2.5X",                              OBJ_EPI_2_5X_v35,         },
    { "Nikon CFI TU Plan Fluor EPI 5X",                         OBJ_EPI_5X_v35,           },
    { "Nikon CFI TU Plan Fluor EPI 10X",                        OBJ_EPI_10X_v35,          },
    { "Nikon CFI TU Plan Fluor EPI 20X",                        OBJ_EPI_20X_v35,          },
    { "Nikon CFI LU Plan Fluor EPI 50X",                        OBJ_EPI_50X_v35,          },
    { "Nikon CFI TU Plan Fluor EPI 100X",                       OBJ_EPI_100X_v35,         },
    { "Nikon CFI EPI Plan Apo 150X",                            OBJ_EPI_150X_v35,         },
    { "Nikon CFI T Plan EPI ELWD 20X (AV 3.5)",                 OBJ_ELWD_20X_v35,         },
    { "Nikon CFI T Plan EPI ELWD 50X (AV 3.5)",                 OBJ_ELWD_50X_v35,         },
    { "Nikon CFI T Plan EPI ELWD 100X (AV 3.5)",                OBJ_ELWD_100X_v35,        },
    { "Nikon CFI T Plan EPI SLWD 10X (AV 3.5)",                 OBJ_SLWD_10X_v35,         },
    { "Nikon CFI T Plan EPI SLWD 20X (AV 3.5)",                 OBJ_SLWD_20X_v35,         },
    { "Nikon CFI T Plan EPI SLWD 50X (AV 3.5)",                 OBJ_SLWD_50X_v35,         },
    { "Nikon CFI T Plan EPI SLWD 100X (AV 3.5)",                OBJ_SLWD_100X_v35,        },
    { "Nikon CFI Fluor Water Immersion 63X",                    OBJ_WATER_60X_v35,        },
    { "Nikon CFI TU Plan Fluor EPI 50X",                        OBJ_EPI_50X_v50,          },
    { "Nikon CFI TU Plan Apo EPI 100X",                         OBJ_EPI_100X_v50,         },
    { "Nikon CFI TU Plan Apo EPI 150X",                         OBJ_EPI_150X_v50,         },
};

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports Sensofar PLu file format, version 2000 or newer."),
    "Jan Hořák <xhorak@gmail.com>, Yeti <yeti@gwyddion.net>",
    "2.1",
    "David Nečas (Yeti) & Jan Hořák",
    "2008",
};

GWY_MODULE_QUERY2(module_info, sensofar)

static gboolean
module_register(void)
{
    gwy_file_func_register("sensofar",
                           N_("Sensofar PLu files (.plu, .apx)"),
                           detect_file, load_file, NULL, NULL);
    return TRUE;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name)
        return g_str_has_suffix(fileinfo->name_lowercase, ".plu") ? 20 : 0;

    /* Byte 490 is the version. */
    if (fileinfo->file_size >= HEADER_SIZE + 12
        && fileinfo->buffer_len >= HEADER_SIZE && parses_as_date(fileinfo->head)
        && (fileinfo->head[490] == 0 || (guint)(fileinfo->head[490]) >= 0xf0))
        return 85;

    return 0;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    SensofarDataDesc data_desc;
    SensofarConfigMesura *measure_config;
    MeasurementType meas_type;
    GwyFile *file = NULL;
    const guchar *p, *buffer = NULL;
    gsize size = 0;
    GError *err = NULL;
    gboolean image_is_grey;
    guint i, channelno;

    if (!gwy_map_file_contents(filename, &buffer, &size, &err)) {
        err_GET_FILE_CONTENTS(error, &err);
        return NULL;
    }
    if (size < HEADER_SIZE + 12) {
        err_TRUNCATED_HEADER(error);
        goto fail;
    }

    /* Date block */
    p = buffer;
    memcpy(&data_desc.date.str, p, DATE_SIZE);
    data_desc.date.str[DATE_SIZE-1] = '\0';
    p += DATE_SIZE;
    data_desc.date.t = gwy_get_guint32_le(&p);

    /* Comment block */
    memcpy(&data_desc.user_comment, p, COMMENT_SIZE);
    data_desc.user_comment[COMMENT_SIZE-1] = '\0';
    p += COMMENT_SIZE;

    if (!read_calibration_block(&p, size - (p - buffer), &data_desc.axes_config, error))
        goto fail;

    measure_config = &data_desc.measure_config;
    if (!read_config_mesura(&p, size - (p - buffer), measure_config, error))
        goto fail;

    gwy_debug("Format version=%d, date=<%s>",
              measure_config->version, data_desc.date.str);
    gwy_debug("Data type=%d, num_layers=%d, num_images=%d",
              measure_config->type, measure_config->num_layers, measure_config->num_images);
    gwy_debug("Res xres=%d, yres=%d",
              measure_config->settings.fov.xres, measure_config->settings.fov.yres);
    gwy_debug("Acquisition method=%d, algorithm=%d",
              measure_config->method, measure_config->algorithm);

    if (!measure_config->num_layers) {
        err_NO_DATA(error);
        goto fail;
    }
    meas_type = measure_config->type;
    image_is_grey = (measure_config->version == FORMAT_VERSION_2000 || measure_config->version == FORMAT_VERSION_2006);

    file = gwy_file_new_in_construction();
    channelno = 0;

    for (i = 0; i < measure_config->num_layers; i++) {
        if (meas_type == MES_TOPO) {
            if (!read_float_field(&data_desc, measure_config->num_images, filename, file, &channelno,
                                  &p, size - (p - buffer), error))
                goto fail;
        }
        else if (image_is_grey && meas_type == MES_IMATGE) {
            if (!read_float_field(&data_desc, 0, filename, file, &channelno, &p, size - (p - buffer), error))
                goto fail;
        }
        else if (!image_is_grey && meas_type == MES_IMATGE) {
            if (!read_rgb_field(&data_desc, filename, file, &channelno, &p, size - (p - buffer), error))
                goto fail;
        }
        else if (meas_type == MES_PERFIL
                 || meas_type == MES_GRUIX
                 || meas_type == MES_MULTIPERFIL) {
            if (!read_profiles(&data_desc, filename, file, &channelno, &p, size - (p - buffer), error))
                goto fail;
        }
        else {
            err_DATA_TYPE(error, measure_config->type);
            goto fail;
        }
    }

    /* Since v2011B there is some additional information after the data, in particular operator and saple name.  The
     * specs does not seem agree with reality though. */
    gwy_debug("remaining data length: %lu", (gulong)(size - (p - buffer)));

    return file;

fail:
    gwy_unmap_file_contents(buffer);
    g_clear_object(&file);
    return NULL;
}

static gboolean
read_calibration_block(const guchar **p, gsize size,
                       SensofarCalibratEixos_Arxiu *axes_config,
                       GError **error)
{
    if (size < 40) {
        err_TRUNCATED_PART(error, "tCalibratEixos_Arxiu");
        return FALSE;
    }

    /* Calbration block */
    axes_config->yres = gwy_get_guint32_le(p);
    axes_config->xres = gwy_get_guint32_le(p);
    axes_config->N_tall = gwy_get_guint32_le(p);
    axes_config->dy_multip = gwy_get_gfloat_le(p);
    axes_config->mppx = gwy_get_gfloat_le(p);
    axes_config->mppy = gwy_get_gfloat_le(p);
    axes_config->x_0 = gwy_get_gfloat_le(p);
    axes_config->y_0 = gwy_get_gfloat_le(p);
    axes_config->mpp_tall = gwy_get_gfloat_le(p);
    axes_config->z_0 = gwy_get_gfloat_le(p);

    return TRUE;
}

static gboolean
read_config_mesura(const guchar **p, gsize size,
                   SensofarConfigMesura *measure_config,
                   GError **error)
{
    if (size < 72) {
        err_TRUNCATED_PART(error, "tConfigMesura");
        return FALSE;
    }

    /* Measurement block */
    measure_config->type = gwy_get_guint32_le(p);
    measure_config->algorithm = gwy_get_guint32_le(p);
    measure_config->method = gwy_get_guint32_le(p);
    measure_config->objective = gwy_get_guint32_le(p);
    measure_config->area_type = gwy_get_guint32_le(p);
    if (measure_config->area_type == AREA_COORDINATES) {
        SensofarPointScanSettings *settings = &measure_config->settings.point;

        settings->tracking_range = gwy_get_gfloat_le(p);
        settings->tracking_speed = gwy_get_gfloat_le(p);
        settings->tracking_direction = gwy_get_guint32_le(p);
        settings->tracking_threshold = gwy_get_gfloat_le(p);
        settings->tracking_min_angle = gwy_get_gfloat_le(p);
        settings->confocal_scan_type = gwy_get_guint32_le(p);
        settings->confocal_scan_range = gwy_get_gfloat_le(p);
        settings->confocal_speed_factor = gwy_get_gfloat_le(p);
        settings->confocal_threshold = gwy_get_gfloat_le(p);
        get_CHARS(settings->reserved, p, 4);
    }
    else {
        SensofarFOVScanSettings *settings = &measure_config->settings.fov;

        settings->xres_area = gwy_get_guint32_le(p);
        settings->yres_area = gwy_get_guint32_le(p);
        settings->xres = gwy_get_guint32_le(p);
        settings->yres = gwy_get_guint32_le(p);
        settings->na = gwy_get_guint32_le(p);
        settings->incr_z = gwy_get_gdouble_le(p);
        settings->range = gwy_get_gfloat_le(p);
        settings->n_planes = gwy_get_guint32_le(p);
        settings->tpc_umbral_F = gwy_get_guint32_le(p);
    }
    measure_config->restore = gwy_get_gboolean8(p);
    measure_config->num_layers = *((*p)++);
    measure_config->version = *((*p)++);
    measure_config->config_hardware = *((*p)++);
    measure_config->num_images = *((*p)++);
    measure_config->reserved = *((*p)++);
    *p += 2; // struct padding
    measure_config->factor_delmacio = gwy_get_guint32_le(p);

    return TRUE;
}

static gboolean
read_float_field(SensofarDataDesc *data_desc, guint nrgb,
                 const gchar *filename,
                 GwyFile *file, guint *channelno,
                 const guchar **p, gsize size,
                 GError **error)
{
    GwyField *field;
    GwyNield *mask;
    guint xres, yres, i;
    gdouble *data;
    gint *mdata;
    const guchar *buf = *p;

    yres = gwy_get_guint32_le(p);
    xres = gwy_get_guint32_le(p);
    gwy_debug("Data size: %dx%d", xres, yres);
    if (err_SIZE_MISMATCH(error, (xres*yres + 2)*sizeof(gfloat), size - 2*sizeof(guint32), FALSE))
        return FALSE;
    if (err_DIMENSION(error, xres) || err_DIMENSION(error, yres))
        return FALSE;
    sanitise_real_size(&data_desc->axes_config.mppx, "x size");
    sanitise_real_size(&data_desc->axes_config.mppy, "y size");

    field = gwy_field_new(xres, yres,
                          data_desc->axes_config.mppx * xres * Micrometer,
                          data_desc->axes_config.mppy * yres * Micrometer,
                          FALSE);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(field), "m");

    mask = gwy_field_new_nield_alike(field);

    /* In older files we can get here with both TOPO and IMGTE types. */
    if (data_desc->measure_config.type == MES_TOPO)
        gwy_unit_set_from_string(gwy_field_get_unit_z(field), "m");

    data = gwy_field_get_data(field);
    mdata = gwy_nield_get_data(mask);
    gwy_convert_raw_data(p, xres*yres, 1, GWY_RAW_DATA_FLOAT, GWY_BYTE_ORDER_LITTLE_ENDIAN, data, 1.0, 0.0);
    for (i = 0; i < xres*yres; i++)
        mdata[i] = (data[i] == LOST_PIXELS);

    *p += (xres*yres + 2)*sizeof(gfloat);   /* Plus data min and data max. */

    if (data_desc->measure_config.type == MES_TOPO)
        gwy_field_multiply(field, Micrometer);

    gwy_debug("Offset: %g %g",
              data_desc->axes_config.x_0, data_desc->axes_config.y_0);
    //FIXME: offset later, support of offset determined by version?
    //gwy_field_set_xoffset(d, gwy_exp10(power10)*data_desc.axes_config.x_0);
    //gwy_field_set_yoffset(d, gwy_exp10(power10)*data_desc.axes_config.y_0);

    mask = gwy_remove_bad_image_data(field, mask);

    gwy_file_pass_image(file, *channelno, field);
    if (mask)
        gwy_file_pass_image_mask(file, *channelno, mask);
    gwy_image_title_fall_back(file, *channelno);
    add_image_meta(data_desc, file, *channelno);
    gwy_log_add_import(file, GWY_FILE_IMAGE, *channelno, NULL, filename);
    (*channelno)++;

    if (!nrgb)
        return TRUE;

    size -= (*p - buf);
    for (i = 0; i < nrgb; i++) {
        if (!read_rgb_data(data_desc, xres, yres, filename, file, channelno, p, size, error))
            return FALSE;
        size -= 3*xres*yres;
    }

    return TRUE;
}

static gboolean
read_rgb_field(SensofarDataDesc *data_desc,
               const gchar *filename,
               GwyFile *file, guint *channelno,
               const guchar **p, gsize size,
               GError **error)
{
    guint xres, yres;

    if (size < 2*sizeof(guint32)) {
        err_TRUNCATED_PART(error, "RGB data");
        return FALSE;
    }
    yres = gwy_get_guint32_le(p);
    xres = gwy_get_guint32_le(p);
    gwy_debug("Data size: %dx%d", xres, yres);

    return read_rgb_data(data_desc, xres, yres, filename, file, channelno, p, size - 2*sizeof(guint32), error);
}

static gboolean
read_rgb_data(SensofarDataDesc *data_desc,
              guint xres, guint yres,
              const gchar *filename,
              GwyFile *file, guint *channelno,
              const guchar **p, gsize size,
              GError **error)
{
    GwyField *rfield, *gfield, *bfield;
    guint k;
    gdouble *rdata, *gdata, *bdata;
    gboolean is_grey;

    if (err_SIZE_MISMATCH(error, xres*yres*3, size, FALSE))
        return FALSE;
    if (err_DIMENSION(error, xres) || err_DIMENSION(error, yres))
        return FALSE;
    sanitise_real_size(&data_desc->axes_config.mppx, "x size");
    sanitise_real_size(&data_desc->axes_config.mppy, "y size");

    rfield = gwy_field_new(xres, yres,
                           data_desc->axes_config.mppx * xres * Micrometer,
                           data_desc->axes_config.mppy * yres * Micrometer,
                           FALSE);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(rfield), "m");

    gfield = gwy_field_new_alike(rfield, FALSE);
    bfield = gwy_field_new_alike(rfield, FALSE);

    rdata = gwy_field_get_data(rfield);
    gdata = gwy_field_get_data(gfield);
    bdata = gwy_field_get_data(bfield);
    is_grey = TRUE;

    for (k = 0; k < xres*yres; k++) {
        rdata[k] = *((*p)++);
        gdata[k] = *((*p)++);
        bdata[k] = *((*p)++);
        if (rdata[k] != gdata[k] || gdata[k] != bdata[k])
            is_grey = FALSE;
    }

    gwy_debug("Offset: %g %g",
              data_desc->axes_config.x_0, data_desc->axes_config.y_0);
    //FIXME: offset later, support of offset determined by version?
    //gwy_field_set_xoffset(d, gwy_exp10(power10)*data_desc.axes_config.x_0);
    //gwy_field_set_yoffset(d, gwy_exp10(power10)*data_desc.axes_config.y_0);

    /* Do not create three images when they are all the same. */
    if (is_grey) {
        gwy_file_set_image(file, *channelno, rfield);
        gwy_file_set_palette(file, GWY_FILE_IMAGE, *channelno, "Gray");
        gwy_file_set_title(file, GWY_FILE_IMAGE, *channelno, "Gray", FALSE);
        add_image_meta(data_desc, file, *channelno);
        gwy_log_add_import(file, GWY_FILE_IMAGE, *channelno, NULL, filename);
        (*channelno)++;
    }
    else {
        gwy_file_set_image(file, *channelno, rfield);
        gwy_file_set_palette(file, GWY_FILE_IMAGE, *channelno, "RGB-Red");
        gwy_file_set_title(file, GWY_FILE_IMAGE, *channelno, "Red", FALSE);
        add_image_meta(data_desc, file, *channelno);
        gwy_log_add_import(file, GWY_FILE_IMAGE, *channelno, NULL, filename);
        (*channelno)++;

        gwy_file_set_image(file, *channelno, gfield);
        gwy_file_set_palette(file, GWY_FILE_IMAGE, *channelno, "RGB-Green");
        gwy_file_set_title(file, GWY_FILE_IMAGE, *channelno, "Green", FALSE);
        add_image_meta(data_desc, file, *channelno);
        gwy_log_add_import(file, GWY_FILE_IMAGE, *channelno, NULL, filename);
        (*channelno)++;

        gwy_file_set_image(file, *channelno, bfield);
        gwy_file_set_palette(file, GWY_FILE_IMAGE, *channelno, "RGB-Blue");
        gwy_file_set_title(file, GWY_FILE_IMAGE, *channelno, "Blue", FALSE);
        add_image_meta(data_desc, file, *channelno);
        gwy_log_add_import(file, GWY_FILE_IMAGE, *channelno, NULL, filename);
        (*channelno)++;
    }
    g_object_unref(bfield);
    g_object_unref(gfield);
    g_object_unref(rfield);

    return TRUE;
}

static gboolean
read_profiles(SensofarDataDesc *data_desc,
              const gchar *filename,
              GwyFile *file, guint *channelno,
              const guchar **p, gsize size,
              GError **error)
{
    GwyGraphModel *gmodel;
    GwyGraphCurveModel *gcmodel;
    guint xres, yres, i, j, n;
    GwyUnit *units = NULL;
    gdouble *xdata, *ydata;
    gchar *s;
    gdouble dx;

    /* The yres is present and currectly set to 1 for MES_PERFIL and MES_GRUIX so we can process single and multiple
     * profiles the same. */
    yres = gwy_get_guint32_le(p);
    xres = gwy_get_guint32_le(p);
    gwy_debug("Data size: %dx%d", xres, yres);
    if (err_SIZE_MISMATCH(error, (xres*yres + 2)*sizeof(gfloat), size - 2*sizeof(guint32), FALSE))
        return FALSE;
    if (err_DIMENSION(error, xres) || err_DIMENSION(error, yres))
        return FALSE;
    sanitise_real_size(&data_desc->axes_config.mppx, "x size");

    xdata = g_new(gdouble, xres);
    ydata = g_new(gdouble, xres);
    dx = data_desc->axes_config.mppx * Micrometer;

    gmodel = gwy_graph_model_new();
    g_object_set(gmodel, "title", _("Profile"), NULL);

    units = gwy_unit_new("m"); /* values are in µm only */
    g_object_set(gmodel, "unit-x", units, NULL);
    g_object_unref(units);

    units = gwy_unit_new("m"); /* values are in µm only */
    g_object_set(gmodel, "unit-y", units, NULL);
    g_object_unref(units);

    for (i = 0; i < yres; i++) {
        for (j = n = 0; j < xres; j++) {
            gdouble v = gwy_get_gfloat_le(p);
            if (v != LOST_PIXELS) {
                xdata[n] = dx*j;
                ydata[n] = v*Micrometer;
                n++;
            }
        }

        if (!n)
            continue;

        gcmodel = gwy_graph_curve_model_new();
        gwy_graph_curve_model_set_data(gcmodel, xdata, ydata, n);
        g_object_set(gcmodel,
                     "mode", GWY_GRAPH_CURVE_LINE,
                     "color", gwy_graph_get_preset_color(i),
                     NULL);
        if (yres == 1)
            g_object_set(gcmodel, "description", _("Profile"), NULL);
        else {
            s = g_strdup_printf(_("Profile %u"), i+1);
            g_object_set(gcmodel, "description", s, NULL);
            g_free(s);
        }
        gwy_graph_model_add_curve(gmodel, gcmodel);
        g_object_unref(gcmodel);
    }
    *p += 2*sizeof(gfloat);   /* Data min and data max. */

    g_free(xdata);
    g_free(ydata);

    if (!gwy_graph_model_get_n_curves(gmodel)) {
        g_object_unref(gmodel);
        err_NO_DATA(error);
        return FALSE;
    }

    gwy_file_pass_graph(file, *channelno, gmodel);
    gwy_log_add_import(file, GWY_FILE_GRAPH, *channelno, NULL, filename);

    (*channelno)++;

    return TRUE;
}

static void
set_meta_enum(GwyDict *meta, guint value, const gchar *name,
              const GwyEnum *table, guint n)
{
    const gchar *s;

    if ((s = gwy_enum_to_string(value, table, n)) && *s)
        gwy_dict_set_string_by_name(meta, name, s);
}

static void
add_image_meta(SensofarDataDesc *data_desc,
               GwyFile *file, guint channelno)
{
    SensofarCalibratEixos_Arxiu *axes_config = &data_desc->axes_config;
    SensofarConfigMesura *measure_config = &data_desc->measure_config;
    SensofarFOVScanSettings *fov = &measure_config->settings.fov;
    GwyDict *meta = gwy_dict_new_in_construction();

    gwy_dict_set_string_by_name(meta, "Date", data_desc->date.str);
    if (*(data_desc->user_comment)) {
        gwy_dict_set_string_by_name(meta, "Comment", data_desc->user_comment);
    }
    gwy_dict_pass_string_by_name(meta, "X0", g_strdup_printf("%g µm", axes_config->x_0));
    gwy_dict_pass_string_by_name(meta, "Y0", g_strdup_printf("%g µm", axes_config->y_0));
    gwy_dict_pass_string_by_name(meta, "Z0", g_strdup_printf("%g µm", axes_config->z_0));
    gwy_dict_pass_string_by_name(meta, "Number of FOVs", g_strdup_printf("%d", fov->na));
    gwy_dict_pass_string_by_name(meta, "Dz step", g_strdup_printf("%g µm", fov->incr_z));
    gwy_dict_pass_string_by_name(meta, "Scan Z range", g_strdup_printf("%g µm", fov->range));
    gwy_dict_pass_string_by_name(meta, "Number of planes", g_strdup_printf("%d", fov->n_planes));
    gwy_dict_pass_string_by_name(meta, "Acquisition threshold", g_strdup_printf("%d %%", fov->tpc_umbral_F));
    gwy_dict_pass_string_by_name(meta, "Number of layers", g_strdup_printf("%d", measure_config->num_layers));
    gwy_dict_pass_string_by_name(meta, "Decimation factor", g_strdup_printf("%d", measure_config->factor_delmacio));

    set_meta_enum(meta, measure_config->version, "Format version", versions, G_N_ELEMENTS(versions));
    set_meta_enum(meta, measure_config->type, "Measurement type", meas_types, G_N_ELEMENTS(meas_types));
    set_meta_enum(meta, measure_config->algorithm, "Algorithm", algorithms, G_N_ELEMENTS(algorithms));
    set_meta_enum(meta, measure_config->objective, "Objective", objectives, G_N_ELEMENTS(objectives));
    set_meta_enum(meta, measure_config->area_type, "Area type", area_types, G_N_ELEMENTS(area_types));
    set_meta_enum(meta, measure_config->area_type, "Area type", area_types, G_N_ELEMENTS(area_types));

    /* Field incorrect in older versions. */
    if (measure_config->version != FORMAT_VERSION_2000
        && measure_config->version <= FORMAT_VERSION_2012) {
        set_meta_enum(meta, measure_config->config_hardware, "Hardware configurations",
                      config_hardwares, G_N_ELEMENTS(config_hardwares));
    }

    if (measure_config->type == MES_IMATGE) {
        set_meta_enum(meta, measure_config->method, "Acquisition method",
                      acq_methods_imatge, G_N_ELEMENTS(acq_methods_imatge));
    }
    else if (measure_config->type == MES_PERFIL) {
        set_meta_enum(meta, measure_config->method, "Acquisition method",
                      acq_methods_perfil, G_N_ELEMENTS(acq_methods_perfil));
    }
    else if (measure_config->type == MES_MULTIPERFIL) {
        set_meta_enum(meta, measure_config->method, "Acquisition method",
                      acq_methods_multiperfil, G_N_ELEMENTS(acq_methods_multiperfil));
    }
    else if (measure_config->type == MES_IMATGE) {
        set_meta_enum(meta, measure_config->method, "Acquisition method",
                      acq_methods_topo, G_N_ELEMENTS(acq_methods_topo));
    }
    if (measure_config->type == MES_COORD_MULTIPLE_PROFILE
        || measure_config->type == MES_COORD_TOPO_MAP
        || measure_config->type == MES_COORD_THICKNESS_MAP) {
        set_meta_enum(meta, measure_config->method, "Acquisition method",
                      acq_methods_map, G_N_ELEMENTS(acq_methods_map));
    }

    gwy_file_pass_meta(file, GWY_FILE_IMAGE, channelno, meta);
}

/* File starts with date, try to parse it.
 * FIXME: this is stupid */
static gboolean
parses_as_date(const gchar *str)
{
    char day_name[4], month_name[4];
    int month_day, hour, min, sec, year;

    if (str[24] != '\0' && !g_ascii_isspace(str[24]))
        return FALSE;

    if (sscanf(str, "%3s %3s %u %u:%u:%u %u",
               day_name, month_name, &month_day, &hour, &min, &sec, &year) != 7)
        return FALSE;

    if (strlen(day_name) != 3 || strlen(month_name) != 3)
        return FALSE;

    if (!gwy_stramong(day_name, "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", NULL))
        return FALSE;

    if (!gwy_stramong(month_name,
                      "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", NULL))
        return FALSE;

    return TRUE;
}

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