Source code for spectrochempy.core.readers.read_wire

# ======================================================================================
# Copyright (©) 2015-2025 LCS - Laboratoire Catalyse et Spectrochimie, Caen, France.
# CeCILL-B FREE SOFTWARE LICENSE AGREEMENT
# See full LICENSE agreement in the root directory.
# ======================================================================================
"""
Module extends NDDataset with the import method for Renishaw WiRe generated data files.

Notes
-----
Code incorporated from py_wdf_reader package (MIT License)
(see https://github.com/alchem0x2A/py-wdf-reader).

The code has been modified to be adapted to the needs of SpectroChemPy

# MIT License
#
# Copyright (c) 2022 T.Tian
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

See Also
--------
The original code was inspired by Henderson, Alex DOI:10.5281/zenodo.495477
(see https://bitbucket.org/AlexHenderson/renishaw-file-formats/src/master/)

See also gwyddion for the DATA types
https://sourceforge.net/p/gwyddion/code/HEAD/tree/trunk/gwyddion/modules/file/renishaw.c

"""

__all__ = ["read_wdf", "read_wire"]
__dataset_methods__ = __all__

import datetime
import io
import struct
from enum import Enum
from enum import IntEnum

import numpy as np

from spectrochempy.application import debug_
from spectrochempy.application import error_
from spectrochempy.application import warning_
from spectrochempy.core.dataset.coord import Coord
from spectrochempy.core.dataset.coordset import CoordSet
from spectrochempy.core.readers.importer import Importer
from spectrochempy.core.readers.importer import _importer_method
from spectrochempy.core.readers.importer import _openfid
from spectrochempy.core.units import ur
from spectrochempy.utils.datetimeutils import windows_time_to_dt64
from spectrochempy.utils.docreps import _docstring
from spectrochempy.utils.file import fromfile
from spectrochempy.utils.objects import Adict

try:
    import PIL
    from PIL import Image
    from PIL.TiffImagePlugin import IFDRational
except ImportError:
    PIL = None


class _wdfReader:
    """
    Reader for Renishaw(TM) WiRE Raman spectroscopy files (.wdf format).

    The wdf file format is separated into several DataBlocks, with starting 4-char
    strings such as (incomplete list):
    `WDF1`: File header for information
    `DATA`: Spectra data
    `XLST`: Data for X-axis of data, usually the Raman shift or wavelength
    `YLST`: Data for Y-axis of data, possibly not important
    `WMAP`: Information for mapping, e.g. StreamLine or StreamLineHR mapping
    `MAP `: Mapping information(?)
    `ORGN`: Data for stage origin
    `TEXT`: Annotation text etc
    `WXDA`: ? TODO
    `WXDM`: ? TODO
    `ZLDC`: ? TODO
    `BKXL`: ? TODO
    `WXCS`: ? TODO
    `WXIS`: ? TODO
    `WHTL`: Whilte light image

    Following the block name, there are two indicators:
    Block uid: int32
    Block size: int64

    Parameters
    ----------
    fid : BytesIO object
        File object for the wdf file.
    dataset : `NDDataset`
        Dataset to fill with the data from the wdf file.

    Notes
    -----
    Metadata :
    title (str) : Title of measurement
    username (str) : Username
    application_name (str) : Default WiRE
    application_version (int,) x 4 : Version number, e.g. [4, 4, 0, 6602]
    measurement_type (int) : Type of measurement
                             0=unknown, 1=single, 2=multi, 3=mapping
    scan_type (int) : Scan of type, see values in scan_types
    laser_wavenumber (float32) : Wavenumber in cm^-1
    count (int) : Numbers of experiments (same type), can be smaller than capacity
    data (numpy.array) : Spectral data
    spectral_units (int) : Unit of spectra, see unit_types
    xlist_type (int) : See unit_types
    xlist_unit (int) : See unit_types
    xlist_length (int): Size for the xlist
    xdata (numpy.array): x-axis data
    ylist_type (int): Same as xlist_type
    ylist_unit (int): Same as xlist_unit
    ylist_length (int): Same as xlist_length
    ydata (numpy.array): y-data, possibly not used
    point_per_spectrum (int): Should be identical to xlist_length
    data_origin_count (int) : Number of rows in data origin list
    capacity (int) : Max number of spectra
    accumulation_count (int) : Single or multiple measurements
    block_info (dict) : Info block at least with following keys
                        DATA, XLST, YLST, ORGN
                        # TODO types?

    """

    # ----------------------------------------------------------------------------------
    # Initialization
    # ----------------------------------------------------------------------------------
    def __init__(self, fid, dataset):
        # The content to read
        self._fid = fid

        # The dataset to fill
        self._dataset = dataset

        # The metadata
        self._meta = self._dataset.meta

        # Parse the header section in the wdf file
        self._block_info = self._locate_all_blocks()

        # Parse individual blocks
        self._parse_header()  # File header -> metadata
        coord_x = self._parse_dimension("X")
        coord_meta = self._parse_dimension("Y")
        other_dimensions = self._parse_others()
        data = self._parse_data()
        # self._parse_img()

        if self._meta.measurement_type == MeasurementType.Mapping:
            # Reshape spectra after reading mapping information
            map_shape = self._parse_mapping(other_dimensions)
            data = self._reshape_data(data, map_shape)
        else:  # Single or Series
            data = self._reshape_data(data)

        # Fill the dataset with the data
        dataset.data = data
        dataset.title = "count"

        # Fill the dataset with the coordinates
        odim = list(other_dimensions.values())
        if self._meta.measurement_type == MeasurementType.Single:
            dataset.set_coordset(x=coord_x, y=odim[0], m=coord_meta)
        elif self._meta.measurement_type == MeasurementType.Series:
            dataset.set_coordset(x=coord_x, y=odim[::-1], m=coord_meta)
        elif self._meta.measurement_type == MeasurementType.Mapping:
            if self._meta.map_area_type == MapAreaType.Unspecified:
                self._meta.map_area_type = MapAreaType.ColumnMajor
                warning_(
                    "Map area type is not specified, "
                    "will assume a xy (column major) scan for the mapping data.",
                )
            # line scan
            if self._meta.map_area_type == MapAreaType.XYLine:
                # create a new coordinate distance
                X, Y, Time = odim[0], odim[1], odim[2]
                dist = np.sqrt(X.data**2 + Y.data**2)
                dist = dist - dist[0]
                distance = Coord(dist, units=X.units, title="distance")
                coord_y = CoordSet(distance, Time, Y, X)
                dataset.set_coordset(x=coord_x, y=coord_y, m=coord_meta)

            # xy column major scan
            elif self._meta.map_area_type == MapAreaType.ColumnMajor:
                # extract the coordinates
                X, Y, Time = odim[0], odim[1], odim[2]
                if np.all(np.array(map_shape) > 1):
                    X.data = X.data.reshape(map_shape[::-1])[0]
                    Y.data = Y.data.reshape(map_shape[::-1])[:, 0]
                dataset.set_coordset(x=coord_x, y=X, m=coord_meta, z=Y)

            # not implemented yet
            else:
                error_(
                    f"Map area type {self._meta.map_area_type.name} not implemented yet!",
                )

        # Finally close the fid
        self._close()

    # ----------------------------------------------------------------------------------
    # Public properties
    # ----------------------------------------------------------------------------------
    @property
    def dataset(self):
        return self._dataset

    # ----------------------------------------------------------------------------------
    # Private methods
    # ----------------------------------------------------------------------------------
    def _locate_all_blocks(self):
        """Get information for all data blocks and store them inside self._block_info."""
        block_info = {}
        curpos = 0
        finished = False
        while not finished:
            try:
                block_name, block_uid, block_size = self._locate_single_block(curpos)
                block_info[block_name] = (block_uid, curpos, block_size)
                curpos += block_size
            except (EOFError, UnicodeDecodeError):
                finished = True
        return block_info

    def _locate_single_block(self, pos):
        """Get block information starting at pos."""
        self._fid.seek(pos)
        block_name = self._fid.read(0x4).decode("ascii")
        if len(block_name) < 4:
            raise EOFError
        block_uid = self._read_type("int32")
        block_size = self._read_type("int64")
        return block_name, block_uid, block_size

    def _parse_header(self):
        """Solve block WDF1."""
        self._fid.seek(0)  # return to the head

        block_ID = self._fid.read(Offsets.block_id).decode("ascii")
        block_UID = self._read_type("int32")
        block_len = self._read_type("int64")

        # First block must be "WDF1"
        if (
            (block_ID != "WDF1")
            or (block_UID != 0 and block_UID != 1)
            or (block_len != Offsets.data_block)
        ):
            raise ValueError("The wdf file format is incorrect!")
        # TODO what are the digits in between?

        # The keys from the header
        self._fid.seek(Offsets.measurement_info)  # space

        self._meta.point_per_spectrum = self._read_type("int32")
        self._meta.capacity = self._read_type("int64")
        self._meta.count = self._read_type("int64")
        self._meta.accumulation_count = self._read_type("int32")
        self._y_size = self._read_type("int32")
        self._x_size = self._read_type("int32")
        self._other_data_count = self._read_type("int32")
        application_name = self._read_type("utf8", 24)  # Must be "WiRE"
        application_version = [0] * 4
        for i in range(4):
            application_version[i] = str(self._read_type("int16"))
        self._dataset.origin = f"{application_name} {'.'.join(application_version)}"
        self._meta.scan_type = ScanType(self._read_type("int32"))
        self._meta.measurement_type = MeasurementType(self._read_type("int32"))

        # For the units
        self._fid.seek(Offsets.spectral_info)
        self._dataset.units = str(UnitType(self._read_type("int32")))
        self._meta.laser_frequency = self._read_type("float") * ur("cm^-1")

        # Username and title
        self._fid.seek(Offsets.file_info)
        self._meta.username = self._read_type(
            "utf8",
            Offsets.usr_name - Offsets.file_info,
        )
        self._dataset.description = self._read_type(
            "utf8",
            Offsets.data_block - Offsets.usr_name,
        )

    def _parse_others(self):
        """
        Get information from ORGN block.

        Additional dimensions information is stored in the ORGN block.
        """
        try:
            _, pos, _ = self._block_info["ORGN"]
        except KeyError:
            debug_("Current measurement does not contain dimension information!")
            return None

        count = self._meta.count
        capacity = self._meta.capacity
        list_increment = Offsets.origin_increment + LenType.l_double.value * capacity
        curpos = pos + Offsets.origin_info

        dimensions = Adict()
        for i in range(self._other_data_count):
            self._fid.seek(curpos)

            # First index: don't know how to use this!
            p1 = self._read_type("int32")
            flag = (p1 >> 31 & 0b1) == 1
            if flag:
                debug_("Flag is set to True, don't know how to use this!")

            # Second: Data type of the row
            datatype = DataType(p1 & ~(0b1 << 31))
            if datatype == DataType.Checksum or datatype == DataType.Flags:
                continue  # skip these two types which are not useful (as of now)

            # Third: Unit
            units = str(UnitType(self._read_type("int32")))
            # Fourth: annotation
            title = self._read_type("utf8", 0x10)
            # Last: the actual data
            if datatype == DataType.Time:
                data = np.array(
                    [
                        windows_time_to_dt64(self._read_type("int64"))
                        for i in range(count)
                    ],
                )
                # set the acquisition time from the first time stamp
                self._meta.acquisition_time = data[0]
                data = data - data[0]
            else:
                data = np.array([self._read_type("double") for i in range(count)])

            # Now build the corresponding coordinates
            coord = Coord(data.astype(float), units=units, title=title)
            # TODO: leave timedeltas when coordinates can handle this

            dimensions[str(datatype)] = coord

            curpos += list_increment

        return dimensions

    def _parse_dimension(self, dim):
        """Get information from XLST or YLST blocks."""
        if dim.upper() not in ["X", "Y"]:
            raise ValueError("Direction argument `dir` must be X or Y!")

        block_name = dim.upper() + "LST"
        _, pos, size = self._block_info[block_name]
        offset = Offsets.block_data
        self._fid.seek(pos + offset)
        dimtype = DataType(self._read_type("int32"))
        units = str(UnitType(self._read_type("int32")))
        size = getattr(self, f"_{dim.lower()}_size")
        if size == 0:  # Possibly not started
            raise ValueError(f"{dim.upper()} array possibly not yet initialized!")

        data = fromfile(self._fid, dtype="float32", count=size)
        data = np.array(data, dtype=float, ndmin=1)

        # now build corresponding coordinates
        coord = Coord(data, units=units)
        coord.name = dim.lower()
        coord.title = str(dimtype).replace("_", " ").lower()

        return coord

    def _parse_mapping(self, others):
        """Get information about mapping in StreamLine and StreamLineHR."""
        try:
            _, pos, _ = self._block_info["WMAP"]
        except KeyError:
            debug_("Current measurement does not contain mapping information!")
            return None

        self._fid.seek(pos + Offsets.wmap_origin)
        self._meta.map_area_type = MapAreaType(self._read_type("int32"))
        _ = self._read_type("int32")
        x_offset = self._read_type("float")
        y_offset = self._read_type("float")
        z_offset = self._read_type("float")
        x_increment = self._read_type("float")
        y_increment = self._read_type("float")
        z_increment = self._read_type("float")
        x_size = self._read_type("int32")
        y_size = self._read_type("int32")
        z_size = self._read_type("int32")
        linefocus_size = self._read_type("int32")

        self._meta.map_info = Adict(
            x=Adict(offset=x_offset, increment=x_increment, size=x_size),
            y=Adict(offset=y_offset, increment=y_increment, size=y_size),
            z=Adict(offset=z_offset, increment=z_increment, size=z_size),
            linefocus_size=linefocus_size,
        )

        # return map shape
        return x_size, y_size

    def _parse_data(self, start=0, end=-1):
        """Get information from DATA block."""
        if end == -1:  # take all spectra
            end = self._meta.count - 1
        if (start not in range(self._meta.count)) or (
            end not in range(self._meta.count)
        ):
            raise ValueError("Wrong start and end indices of spectra!")
        if start > end:
            raise ValueError("Start cannot be larger than end!")

        # Determine how many points to read
        points = self._meta.point_per_spectrum

        # Determine start position
        _, pos, _ = self._block_info["DATA"]
        pos_start = pos + Offsets.block_data + LenType["l_float"].value * start * points
        n_row = end - start + 1
        self._fid.seek(pos_start)
        data = fromfile(self._fid, dtype="float32", count=n_row * points)
        return np.array(data, dtype=float, ndmin=2)

    def _parse_img(self):
        """
        Extract the white-light JPEG image.

        The size of while-light image is coded in its EXIF.
        Use PIL to parse the EXIF information.
        """
        try:
            _, pos, size = self._block_info["WHTL"]
        except KeyError:
            debug_("The wdf file does not contain an image")
            return

        # Read the bytes. `self._meta.img` is a wrapped IO object mimicking a file
        self._fid.seek(pos + Offsets.jpeg_header)
        img_bytes = self._fid.read(size - Offsets.jpeg_header)
        self._meta.img = io.BytesIO(img_bytes)
        # Handle image dimension if PIL is present
        if PIL is not None:
            pil_img = Image.open(self._meta.img)
            # Weird missing header keys when Pillow >= 8.2.0.
            # see https://pillow.readthedocs.io/en/stable/releasenotes/8.2.0.html#image-getexif-exif-and-gps-ifd
            # Use fall-back _getexif method instead
            exif_header = dict(pil_img._getexif())
            try:
                # Get the width and height of image
                w_ = exif_header[ExifTags.FocalPlaneXResolution]
                h_ = exif_header[ExifTags.FocalPlaneYResolution]
                x_org_, y_org_ = exif_header[ExifTags.FocalPlaneXYOrigins]

                def rational2float(v):
                    """Pillow<7.2.0 returns tuple, Pillow>=7.2.0 returns IFDRational."""
                    if not isinstance(v, IFDRational):
                        return v[0] / v[1]
                    return float(v)

                w_, h_ = rational2float(w_), rational2float(h_)
                x_org_, y_org_ = rational2float(x_org_), rational2float(y_org_)

                # The dimensions (width, height)
                # with unit `img_dimension_unit`
                self._meta.img_dimensions = np.array([w_, h_])
                # Origin of image is at upper right corner
                self._meta.img_origins = np.array([x_org_, y_org_])
                # Default is microns (5)
                self._meta.img_dimension_unit = UnitType(
                    exif_header[ExifTags.FocalPlaneResolutionUnit],
                )
                # Give the box for cropping
                # Following the PIL manual
                # (left, upper, right, lower)
                self._meta.img_cropbox = self.__calc_crop_box()

            except KeyError:
                error_("Some keys in white light image header cannot be read!")
        return

    def _close(self):
        self._fid.close()
        if hasattr(self._meta, "img") and self._meta.img is not None:
            self._meta.img.close()

    def _get_type_string(self, attr, data_type):
        """Get the enumerated-data_type as string."""
        val = getattr(self, attr)  # No error checking
        if data_type is None:
            return val
        return data_type(val).name

    def _read_type(self, type, size=1):
        """Unpack struct data for certain type."""
        if type in ["int16", "int32", "int64", "float", "double"]:
            if size > 1:
                raise NotImplementedError(
                    "Does not support read number type with size >1",
                )
            # unpack into unsigned values
            fmt_out = LenType["s_" + type].value
            fmt_in = LenType["l_" + type].value

            return struct.unpack(fmt_out, self._fid.read(fmt_in * size))[0]

        if type == "utf8":
            # Read utf8 string with determined size block
            return self._fid.read(size).decode("utf8").replace("\x00", "")

        raise ValueError("Unknown data length format!")

    @property
    def _is_completed(self):
        # If count < capacity, this measurement is not completed
        return self._meta.count == self._meta.capacity

    def __calc_crop_box(self):
        """Calculate crop box."""

        def _proportion(x, minmax, pixels):
            """Get proportional pixels."""
            min, max = minmax
            return int(pixels * (x - min) / (max - min))

        pil_img = PIL.Image.open(self._meta.img)
        w_, h_ = self._meta.img_dimensions
        x0_, y0_ = self._meta.img_origins
        pw = pil_img.width
        ph = pil_img.height
        map_xl = self.xpos.min()
        map_xr = self.xpos.max()
        map_yt = self.ypos.min()
        map_yb = self.ypos.max()
        left = _proportion(map_xl, (x0_, x0_ + w_), pw)
        right = _proportion(map_xr, (x0_, x0_ + w_), pw)
        top = _proportion(map_yt, (y0_, y0_ + h_), ph)
        bottom = _proportion(map_yb, (y0_, y0_ + h_), ph)
        return (left, top, right, bottom)

    def _reshape_data(self, data, map_shape=None):
        """Reshape spectra into w x h x points if mapping data else count x points."""
        count = self._meta.count
        points = self._meta.point_per_spectrum
        if not self._is_completed:
            warning_(
                "The measurement is not completed, "
                "will try to reshape spectra into count x pps.",
            )
            try:
                data = np.reshape(data, (count, points))
            except ValueError:
                error_("Reshaping spectra array failed..")
                return None

        elif map_shape is not None:
            # Is a mapping
            w, h = map_shape
            if w * h != count:
                debug_(
                    "Mapping information from WMAP block not"
                    " corresponding to ORGN block! ",
                )
                error_("Can't reshape the spectra with the given mapping information.")
                return None

            if w * h * points != data.size:
                debug_(
                    "Mapping information from WMAP"
                    " not corresponding to DATA! "
                    "Will not reshape the spectra",
                )
                error_("Reshaping spectra array failed.")
                return None

            # Should be h rows * w columns. np.ndarray is row first
            # Reshape to 3D matrix when doing 2D mapping
            if (h > 1) and (w > 1):
                data = np.reshape(data, (h, w, points))
            # otherwise it is a line scan
            else:
                data = np.reshape(data, (count, points))

        # For any other type of measurement, reshape into (counts, point_per_spectrum)
        # example: series scan
        elif count > 1:
            data = np.reshape(data, (count, points))

        return data


# --------------------------------------------------------------------------------------
# Declaration of DATA types
# --------------------------------------------------------------------------------------
class LenType(Enum):
    l_int16 = 2
    l_int32 = 4
    l_int64 = 8
    s_int16 = "<H"  # unsigned short int
    s_int32 = "<I"  # unsigned int32
    s_int64 = "<Q"  # unsigned int64
    l_float = 4
    s_float = "<f"
    l_double = 8
    s_double = "<d"


class MeasurementType(IntEnum):
    Unspecified = 0
    Single = 1
    Series = 2
    Mapping = 3

    def __str__(self):
        return self._name_


class ScanType(IntEnum):
    Unspecified = 0
    Static = 1
    Continuous = 2
    StepRepeat = 3
    FilterScan = 4
    FilterImage = 5
    StreamLine = 6
    StreamLineHR = 7
    PointDetector = 8

    def __str__(self):
        return self._name_


class UnitType(IntEnum):
    Arbitrary = 0
    RamanShift = 1  # cm^-1 by default
    Wavelength = 2  # nm
    Nanometre = 3
    ElectronVolt = 4
    Micron = 5  # same for EXIF units
    Counts = 6
    Electrons = 7
    Millimetres = 8
    Metres = 9
    Kelvin = 10
    Pascal = 11
    Seconds = 12
    Milliseconds = 13
    Hours = 14
    Days = 15
    Pixels = 16
    Intensity = 17
    RelativeIntensity = 18
    Degrees = 19
    Radians = 20
    Celsius = 21
    Fahrenheit = 22
    KelvinPerMinute = 23
    AcquisitionTime = 24
    Microseconds = 25

    def __str__(self):
        """Rewrite the unit name output."""
        unit_str = {
            "Arbitrary": "",
            "RamanShift": "1/cm",  # cm^-1 by default
            "Wavelength": "nm",  # nm
            "Nanometre": "nm",
            "ElectronVolt": "eV",
            "Micron": "um",  # same for EXIF units
            "Counts": "counts",
            "Electrons": "electrons",
            "Millimetres": "mm",
            "Metres": "m",
            "Kelvin": "K",
            "Pascal": "Pa",
            "Seconds": "s",
            "Milliseconds": "ms",
            "Hours": "h",
            "Days": "d",
            "Pixels": "px",
            "Intensity": "",
            "RelativeIntensity": "",
            "Degrees": "°",
            "Radians": "rad",
            "Celsius": "°C",
            "Fahrenheit": "°F",
            "KelvinPerMinute": "K/min",
            "AcquisitionTime": "us",
            "Microseconds": "us",
        }
        return unit_str[self._name_]


class MapAreaType(IntEnum):
    Unspecified = 0  # (find in some test files)
    RandomPoints = 1  # rectangle area
    ColumnMajor = 2  # X first then Y.
    Alternating = 4  # raster or snake
    LineFocusMapping = 8  # see also linefocus_height
    SurfaceProfile = 64  # Z data is non-regular (surface maps)
    XYLine = 128  # line or depth slice forming a single line along
    # the XY plane


class DataType(IntEnum):
    Arbitrary = 0
    Raman_Shift = 1
    Intensity = 2
    X = 3
    Y = 4
    Z = 5
    R = 6
    Theta = 7
    Phi = 8
    Temperature = 9
    Pressure = 10
    Time = 11
    Derived = 12
    Polarization = 13
    FocusTrack = 14
    RampRate = 15
    Checksum = 16
    Flags = 17
    ElapsedTime = 18
    Spectral = 19
    Mp_Well_Spatial_X = 22
    Mp_Well_Spatial_Y = 23
    Mp_LocationIndex = 24
    Mp_WellReference = 25
    EndMarker = 26
    ExposureTime = 27

    def __str__(self):
        return self._name_


class Offsets(IntEnum):
    """Offsets to the start of block."""

    # General offsets
    block_name = 0x0
    block_id = 0x4
    block_data = 0x10
    # offsets in WDF1 block
    measurement_info = 0x3C  #
    spectral_info = 0x98
    file_info = 0xD0
    usr_name = 0xF0
    data_block = 0x200
    # offsets in ORGN block
    origin_info = 0x14
    origin_increment = 0x18
    # offsets in WMAP block
    wmap_origin = 0x10
    wmap_wh = 0x30
    # offsets in WHTL block
    jpeg_header = 0x10


class ExifTags(IntEnum):
    """Customized EXIF TAGS."""

    # Standard EXIF TAGS
    FocalPlaneXResolution = 0xA20E
    FocalPlaneYResolution = 0xA20F
    FocalPlaneResolutionUnit = 0xA210
    # Customized EXIF TAGS from Renishaw
    FocalPlaneXYOrigins = 0xFEA0
    FieldOfViewXY = 0xFEA1


# ======================================================================================
# Public functions
# ======================================================================================
_docstring.delete_params("Importer.see_also", "read_wire")


[docs] @_docstring.dedent def read_wire(*paths, **kwargs): """ Read a single Raman spectrum or a series of Raman spectra. Files to open are :file:`.wdf` file created by Renishaw ``WiRe`` software. Parameters ---------- %(Importer.parameters)s Returns ------- %(Importer.returns)s Other Parameters ---------------- %(Importer.other_parameters)s See Also -------- %(Importer.see_also.no_read_wire)s """ kwargs["filetypes"] = ["Renishaw WiRE files (*.wdf)"] kwargs["protocol"] = ["wire", "wdf"] importer = Importer() return importer(*paths, **kwargs)
read_wdf = read_wire # ====================================================================================== # Private functions # ====================================================================================== @_importer_method def _read_wdf(*args, **kwargs): # read WiRe *.wdf files or series dataset, filename = args fid, kwargs = _openfid(filename, **kwargs) reader = _wdfReader(fid, dataset) dataset = reader.dataset if dataset is None: error_(f"The {filename.stem} file is not readable!") return None dataset.name = filename.stem dataset.filename = filename dataset.history = f"Imported from {filename} on {datetime.datetime.now()}" return dataset