1# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
2# vi: set ft=python sts=4 ts=4 sw=4 et:
3### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
4#
5#   See COPYING file distributed along with the NiBabel package for the
6#   copyright and license terms.
7#
8### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
9# module imports
10""" Utilities to load and save image objects """
11
12import os
13import numpy as np
14
15from .filename_parser import splitext_addext, _stringify_path
16from .openers import ImageOpener
17from .filebasedimages import ImageFileError
18from .imageclasses import all_image_classes
19from .arrayproxy import is_proxy
20from .deprecated import deprecate_with_version
21
22
23def load(filename, **kwargs):
24    r""" Load file given filename, guessing at file type
25
26    Parameters
27    ----------
28    filename : str or os.PathLike
29       specification of file to load
30    \*\*kwargs : keyword arguments
31        Keyword arguments to format-specific load
32
33    Returns
34    -------
35    img : ``SpatialImage``
36       Image of guessed type
37    """
38    filename = _stringify_path(filename)
39
40    # Check file exists and is not empty
41    try:
42        stat_result = os.stat(filename)
43    except OSError:
44        raise FileNotFoundError(f"No such file or no access: '{filename}'")
45    if stat_result.st_size <= 0:
46        raise ImageFileError(f"Empty file: '{filename}'")
47
48    sniff = None
49    for image_klass in all_image_classes:
50        is_valid, sniff = image_klass.path_maybe_image(filename, sniff)
51        if is_valid:
52            img = image_klass.from_filename(filename, **kwargs)
53            return img
54
55    raise ImageFileError(f'Cannot work out file type of "{filename}"')
56
57
58@deprecate_with_version('guessed_image_type deprecated.', '3.2', '5.0')
59def guessed_image_type(filename):
60    """ Guess image type from file `filename`
61
62    Parameters
63    ----------
64    filename : str
65        File name containing an image
66
67    Returns
68    -------
69    image_class : class
70        Class corresponding to guessed image type
71    """
72    sniff = None
73    for image_klass in all_image_classes:
74        is_valid, sniff = image_klass.path_maybe_image(filename, sniff)
75        if is_valid:
76            return image_klass
77
78    raise ImageFileError(f'Cannot work out file type of "{filename}"')
79
80
81def save(img, filename):
82    """ Save an image to file adapting format to `filename`
83
84    Parameters
85    ----------
86    img : ``SpatialImage``
87       image to save
88    filename : str or os.PathLike
89       filename (often implying filenames) to which to save `img`.
90
91    Returns
92    -------
93    None
94    """
95    filename = _stringify_path(filename)
96
97    # Save the type as expected
98    try:
99        img.to_filename(filename)
100    except ImageFileError:
101        pass
102    else:
103        return
104
105    # Be nice to users by making common implicit conversions
106    froot, ext, trailing = splitext_addext(filename, ('.gz', '.bz2'))
107    lext = ext.lower()
108
109    # Special-case Nifti singles and Pairs
110    # Inline imports, as this module really shouldn't reference any image type
111    from .nifti1 import Nifti1Image, Nifti1Pair
112    from .nifti2 import Nifti2Image, Nifti2Pair
113
114    klass = None
115    converted = None
116
117    if type(img) == Nifti1Image and lext in ('.img', '.hdr'):
118        klass = Nifti1Pair
119    elif type(img) == Nifti2Image and lext in ('.img', '.hdr'):
120        klass = Nifti2Pair
121    elif type(img) == Nifti1Pair and lext == '.nii':
122        klass = Nifti1Image
123    elif type(img) == Nifti2Pair and lext == '.nii':
124        klass = Nifti2Image
125    else:  # arbitrary conversion
126        valid_klasses = [klass for klass in all_image_classes
127                         if ext in klass.valid_exts]
128        if not valid_klasses:  # if list is empty
129            raise ImageFileError(f'Cannot work out file type of "{filename}"')
130
131        # Got a list of valid extensions, but that's no guarantee
132        #   the file conversion will work. So, try each image
133        #   in order...
134        for klass in valid_klasses:
135            try:
136                converted = klass.from_image(img)
137                break
138            except Exception as e:
139                err = e
140        # ... and if none of them work, raise an error.
141        if converted is None:
142            raise err
143
144    # Here, we either have a klass or a converted image.
145    if converted is None:
146        converted = klass.from_image(img)
147    converted.to_filename(filename)
148
149
150@deprecate_with_version('read_img_data deprecated. '
151                        'Please use ``img.dataobj.get_unscaled()`` instead.',
152                        '3.2',
153                        '5.0')
154def read_img_data(img, prefer='scaled'):
155    """ Read data from image associated with files
156
157    If you want unscaled data, please use ``img.dataobj.get_unscaled()``
158    instead.  If you want scaled data, use ``img.get_fdata()`` (which will cache
159    the loaded array) or ``np.array(img.dataobj)`` (which won't cache the
160    array). If you want to load the data as for a modified header, save the
161    image with the modified header, and reload.
162
163    Parameters
164    ----------
165    img : ``SpatialImage``
166       Image with valid image file in ``img.file_map``.  Unlike the
167       ``img.get_fdata()`` method, this function returns the data read
168       from the image file, as specified by the *current* image header
169       and *current* image files.
170    prefer : str, optional
171       Can be 'scaled' - in which case we return the data with the
172       scaling suggested by the format, or 'unscaled', in which case we
173       return, if we can, the raw data from the image file, without the
174       scaling applied.
175
176    Returns
177    -------
178    arr : ndarray
179       array as read from file, given parameters in header
180
181    Notes
182    -----
183    Summary: please use the ``get_data`` method of `img` instead of this
184    function unless you are sure what you are doing.
185
186    In general, you will probably prefer ``prefer='scaled'``, because
187    this gives the data as the image format expects to return it.
188
189    Use `prefer` == 'unscaled' with care; the modified Analyze-type
190    formats such as SPM formats, and nifti1, specify that the image data
191    array is given by the raw data on disk, multiplied by a scalefactor
192    and maybe with the addition of a constant.  This function, with
193    ``unscaled`` returns the data on the disk, without these
194    format-specific scalings applied.  Please use this funciton only if
195    you absolutely need the unscaled data, and the magnitude of the
196    data, as given by the scalefactor, is not relevant to your
197    application.  The Analyze-type formats have a single scalefactor +/-
198    offset per image on disk. If you do not care about the absolute
199    values, and will be removing the mean from the data, then the
200    unscaled values will have preserved intensity ratios compared to the
201    mean-centered scaled data.  However, this is not necessarily true of
202    other formats with more complicated scaling - such as MINC.
203    """
204    if prefer not in ('scaled', 'unscaled'):
205        raise ValueError(f'Invalid string "{prefer}" for "prefer"')
206    hdr = img.header
207    if not hasattr(hdr, 'raw_data_from_fileobj'):
208        # We can only do scaled
209        if prefer == 'unscaled':
210            raise ValueError("Can only do unscaled for Analyze types")
211        return np.array(img.dataobj)
212    # Analyze types
213    img_fh = img.file_map['image']
214    img_file_like = (img_fh.filename if img_fh.fileobj is None
215                     else img_fh.fileobj)
216    if img_file_like is None:
217        raise ImageFileError('No image file specified for this image')
218    # Check the consumable values in the header
219    hdr = img.header
220    dao = img.dataobj
221    default_offset = hdr.get_data_offset() == 0
222    default_scaling = hdr.get_slope_inter() == (None, None)
223    # If we have a proxy object and the header has any consumed fields, we load
224    # the consumed values back from the proxy
225    if is_proxy(dao) and (default_offset or default_scaling):
226        hdr = hdr.copy()
227        if default_offset and dao.offset != 0:
228            hdr.set_data_offset(dao.offset)
229        if default_scaling and (dao.slope, dao.inter) != (1, 0):
230            hdr.set_slope_inter(dao.slope, dao.inter)
231    with ImageOpener(img_file_like) as fileobj:
232        if prefer == 'scaled':
233            return hdr.data_from_fileobj(fileobj)
234        return hdr.raw_data_from_fileobj(fileobj)
235
236
237@deprecate_with_version('which_analyze_type deprecated.', '3.2', '4.0')
238def which_analyze_type(binaryblock):
239    """ Is `binaryblock` from NIfTI1, NIfTI2 or Analyze header?
240
241    Parameters
242    ----------
243    binaryblock : bytes
244        The `binaryblock` is 348 bytes that might be NIfTI1, NIfTI2, Analyze,
245        or None of the the above.
246
247    Returns
248    -------
249    hdr_type : str
250        * a nifti1 header (pair or single) -> return 'nifti1'
251        * a nifti2 header (pair or single) -> return 'nifti2'
252        * an Analyze header -> return 'analyze'
253        * None of the above -> return None
254
255    Notes
256    -----
257    Algorithm:
258
259    * read in the first 4 bytes from the file as 32-bit int ``sizeof_hdr``
260    * if ``sizeof_hdr`` is 540 or byteswapped 540 -> assume nifti2
261    * Check for 'ni1', 'n+1' magic -> assume nifti1
262    * if ``sizeof_hdr`` is 348 or byteswapped 348 assume Analyze
263    * Return None
264    """
265    from .nifti1 import header_dtype
266    hdr_struct = np.ndarray(shape=(), dtype=header_dtype, buffer=binaryblock)
267    bs_hdr_struct = hdr_struct.byteswap()
268    sizeof_hdr = hdr_struct['sizeof_hdr']
269    bs_sizeof_hdr = bs_hdr_struct['sizeof_hdr']
270    if 540 in (sizeof_hdr, bs_sizeof_hdr):
271        return 'nifti2'
272    if hdr_struct['magic'] in (b'ni1', b'n+1'):
273        return 'nifti1'
274    if 348 in (sizeof_hdr, bs_sizeof_hdr):
275        return 'analyze'
276    return None
277