13
13
14
14
from __future__ import annotations
15
15
16
+ import typing as ty
16
17
import warnings
17
18
from io import BytesIO
18
19
19
20
import numpy as np
20
21
import numpy .linalg as npl
22
+ from typing_extensions import TypeVar # PY312
21
23
22
24
from . import analyze # module import
23
25
from .arrayproxy import get_obj_dtype
31
33
from .spm99analyze import SpmAnalyzeHeader
32
34
from .volumeutils import Recoder , endian_codes , make_dt_codes
33
35
34
- pdcm , have_dicom , _ = optional_package ('pydicom' )
36
+ if ty .TYPE_CHECKING :
37
+ import pydicom as pdcm
38
+
39
+ have_dicom = True
40
+ DicomDataset = pdcm .Dataset
41
+ else :
42
+ pdcm , have_dicom , _ = optional_package ('pydicom' )
43
+ if have_dicom :
44
+ DicomDataset = pdcm .Dataset
45
+ else :
46
+ DicomDataset = ty .Any
47
+
48
+ T = TypeVar ('T' , default = bytes )
35
49
36
50
# nifti1 flat header definition for Analyze-like first 348 bytes
37
51
# first number in comments indicates offset in file header in bytes
283
297
)
284
298
285
299
286
- class Nifti1Extension :
287
- """Baseclass for NIfTI1 header extensions.
300
+ class NiftiExtension ( ty . Generic [ T ]) :
301
+ """Base class for NIfTI header extensions."""
288
302
289
- This class is sufficient to handle very simple text-based extensions, such
290
- as `comment`. More sophisticated extensions should/will be supported by
291
- dedicated subclasses.
292
- """
303
+ code : int
304
+ encoding : ty . Optional [ str ] = None
305
+ _content : bytes
306
+ _object : ty . Optional [ T ] = None
293
307
294
- def __init__ (self , code , content ):
308
+ def __init__ (
309
+ self ,
310
+ code : ty .Union [int , str ],
311
+ content : bytes ,
312
+ ) -> None :
295
313
"""
296
314
Parameters
297
315
----------
298
316
code : int or str
299
317
Canonical extension code as defined in the NIfTI standard, given
300
318
either as integer or corresponding label
301
319
(see :data:`~nibabel.nifti1.extension_codes`)
302
- content : str
303
- Extension content as read from the NIfTI file header. This content is
304
- converted into a runtime representation.
320
+ content : bytes
321
+ Extension content as read from the NIfTI file header. This content may
322
+ be converted into a runtime representation.
305
323
"""
306
324
try :
307
- self ._code = extension_codes .code [code ]
325
+ self .code = extension_codes .code [code ] # type: ignore[assignment ]
308
326
except KeyError :
309
- # XXX or fail or at least complain?
310
- self ._code = code
311
- self ._content = self ._unmangle (content )
327
+ self .code = code # type: ignore[assignment]
328
+ self ._content = content
312
329
313
- def _unmangle (self , value ):
314
- """Convert the extension content into its runtime representation.
330
+ # Handle (de)serialization of extension content
331
+ # Subclasses may implement these methods to provide an alternative
332
+ # view of the extension content. If left unimplemented, the content
333
+ # must be bytes and is not modified.
334
+ def _mangle (self , obj : T ) -> bytes :
335
+ raise NotImplementedError
315
336
316
- The default implementation does nothing at all.
337
+ def _unmangle (self , content : bytes ) -> T :
338
+ raise NotImplementedError
317
339
318
- Parameters
319
- ----------
320
- value : str
321
- Extension content as read from file.
340
+ def _sync (self ) -> None :
341
+ """Synchronize content with object.
322
342
323
- Returns
324
- -------
325
- The same object that was passed as `value`.
326
-
327
- Notes
328
- -----
329
- Subclasses should reimplement this method to provide the desired
330
- unmangling procedure and may return any type of object.
343
+ This permits the runtime representation to be modified in-place
344
+ and updates the bytes representation accordingly.
331
345
"""
332
- return value
333
-
334
- def _mangle (self , value ):
335
- """Convert the extension content into NIfTI file header representation.
346
+ if self ._object is not None :
347
+ self ._content = self ._mangle (self ._object )
336
348
337
- The default implementation does nothing at all.
338
-
339
- Parameters
340
- ----------
341
- value : str
342
- Extension content in runtime form.
349
+ def __repr__ (self ) -> str :
350
+ try :
351
+ code = extension_codes .label [self .code ]
352
+ except KeyError :
353
+ # deal with unknown codes
354
+ code = self .code
355
+ return f'{ self .__class__ .__name__ } ({ code } , { self ._content !r} )'
343
356
344
- Returns
345
- -------
346
- str
357
+ def __eq__ (self , other : object ) -> bool :
358
+ return (
359
+ isinstance (other , self .__class__ )
360
+ and self .code == other .code
361
+ and self .content == other .content
362
+ )
347
363
348
- Notes
349
- -----
350
- Subclasses should reimplement this method to provide the desired
351
- mangling procedure.
352
- """
353
- return value
364
+ def __ne__ (self , other ):
365
+ return not self == other
354
366
355
367
def get_code (self ):
356
368
"""Return the canonical extension type code."""
357
- return self ._code
369
+ return self .code
358
370
359
- def get_content (self ):
360
- """Return the extension content in its runtime representation."""
371
+ @property
372
+ def content (self ) -> bytes :
373
+ """Return the extension content as raw bytes."""
374
+ self ._sync ()
361
375
return self ._content
362
376
363
- def get_sizeondisk (self ):
377
+ def get_content (self ) -> T :
378
+ """Return the extension content in its runtime representation.
379
+
380
+ This method may return a different type for each extension type.
381
+ """
382
+ if self ._object is None :
383
+ self ._object = self ._unmangle (self ._content )
384
+ return self ._object
385
+
386
+ def get_sizeondisk (self ) -> int :
364
387
"""Return the size of the extension in the NIfTI file."""
388
+ self ._sync ()
365
389
# need raw value size plus 8 bytes for esize and ecode
366
- size = len (self ._mangle (self ._content ))
367
- size += 8
390
+ size = len (self ._content ) + 8
368
391
# extensions size has to be a multiple of 16 bytes
369
392
if size % 16 != 0 :
370
393
size += 16 - (size % 16 )
371
394
return size
372
395
373
- def __repr__ (self ):
374
- try :
375
- code = extension_codes .label [self ._code ]
376
- except KeyError :
377
- # deal with unknown codes
378
- code = self ._code
379
-
380
- s = f"Nifti1Extension('{ code } ', '{ self ._content } ')"
381
- return s
382
-
383
- def __eq__ (self , other ):
384
- return (self ._code , self ._content ) == (other ._code , other ._content )
385
-
386
- def __ne__ (self , other ):
387
- return not self == other
388
-
389
- def write_to (self , fileobj , byteswap ):
396
+ def write_to (self , fileobj : ty .BinaryIO , byteswap : bool = False ) -> None :
390
397
"""Write header extensions to fileobj
391
398
392
399
Write starts at fileobj current file position.
@@ -402,22 +409,74 @@ def write_to(self, fileobj, byteswap):
402
409
-------
403
410
None
404
411
"""
412
+ self ._sync ()
405
413
extstart = fileobj .tell ()
406
414
rawsize = self .get_sizeondisk ()
407
415
# write esize and ecode first
408
- extinfo = np .array ((rawsize , self ._code ), dtype = np .int32 )
416
+ extinfo = np .array ((rawsize , self .code ), dtype = np .int32 )
409
417
if byteswap :
410
418
extinfo = extinfo .byteswap ()
411
419
fileobj .write (extinfo .tobytes ())
412
420
# followed by the actual extension content
413
421
# XXX if mangling upon load is implemented, it should be reverted here
414
- fileobj .write (self ._mangle ( self . _content ) )
422
+ fileobj .write (self ._content )
415
423
# be nice and zero out remaining part of the extension till the
416
424
# next 16 byte border
417
425
fileobj .write (b'\x00 ' * (extstart + rawsize - fileobj .tell ()))
418
426
419
427
420
- class Nifti1DicomExtension (Nifti1Extension ):
428
+ class Nifti1Extension (NiftiExtension [T ]):
429
+ """Baseclass for NIfTI1 header extensions.
430
+
431
+ This class is sufficient to handle very simple text-based extensions, such
432
+ as `comment`. More sophisticated extensions should/will be supported by
433
+ dedicated subclasses.
434
+ """
435
+
436
+ def _unmangle (self , value : bytes ) -> T :
437
+ """Convert the extension content into its runtime representation.
438
+
439
+ The default implementation does nothing at all.
440
+
441
+ Parameters
442
+ ----------
443
+ value : str
444
+ Extension content as read from file.
445
+
446
+ Returns
447
+ -------
448
+ The same object that was passed as `value`.
449
+
450
+ Notes
451
+ -----
452
+ Subclasses should reimplement this method to provide the desired
453
+ unmangling procedure and may return any type of object.
454
+ """
455
+ return value # type: ignore[return-value]
456
+
457
+ def _mangle (self , value : T ) -> bytes :
458
+ """Convert the extension content into NIfTI file header representation.
459
+
460
+ The default implementation does nothing at all.
461
+
462
+ Parameters
463
+ ----------
464
+ value : str
465
+ Extension content in runtime form.
466
+
467
+ Returns
468
+ -------
469
+ str
470
+
471
+ Notes
472
+ -----
473
+ Subclasses should reimplement this method to provide the desired
474
+ mangling procedure.
475
+ """
476
+ return value # type: ignore[return-value]
477
+
478
+
479
+ class Nifti1DicomExtension (Nifti1Extension [DicomDataset ]):
421
480
"""NIfTI1 DICOM header extension
422
481
423
482
This class is a thin wrapper around pydicom to read a binary DICOM
@@ -427,7 +486,12 @@ class Nifti1DicomExtension(Nifti1Extension):
427
486
header.
428
487
"""
429
488
430
- def __init__ (self , code , content , parent_hdr = None ):
489
+ def __init__ (
490
+ self ,
491
+ code : ty .Union [int , str ],
492
+ content : ty .Union [bytes , DicomDataset , None ] = None ,
493
+ parent_hdr : ty .Optional [Nifti1Header ] = None ,
494
+ ) -> None :
431
495
"""
432
496
Parameters
433
497
----------
@@ -452,50 +516,48 @@ def __init__(self, code, content, parent_hdr=None):
452
516
code should always be 2 for DICOM.
453
517
"""
454
518
455
- self ._code = code
456
- if parent_hdr :
457
- self ._is_little_endian = parent_hdr .endianness == '<'
458
- else :
459
- self ._is_little_endian = True
519
+ self ._is_little_endian = parent_hdr is None or parent_hdr .endianness == '<'
520
+
521
+ bytes_content : bytes
460
522
if isinstance (content , pdcm .dataset .Dataset ):
461
523
self ._is_implicit_VR = False
462
- self ._raw_content = self . _mangle ( content )
463
- self . _content = content
524
+ self ._object = content
525
+ bytes_content = self . _mangle ( content )
464
526
elif isinstance (content , bytes ): # Got a byte string - unmangle it
465
- self ._raw_content = content
466
- self ._is_implicit_VR = self ._guess_implicit_VR ()
467
- ds = self ._unmangle (content , self ._is_implicit_VR , self ._is_little_endian )
468
- self ._content = ds
527
+ self ._is_implicit_VR = self ._guess_implicit_VR (content )
528
+ self ._object = self ._unmangle (content )
529
+ bytes_content = content
469
530
elif content is None : # initialize a new dicom dataset
470
531
self ._is_implicit_VR = False
471
- self ._content = pdcm .dataset .Dataset ()
532
+ self ._object = pdcm .dataset .Dataset ()
533
+ bytes_content = self ._mangle (self ._object )
472
534
else :
473
535
raise TypeError (
474
536
f'content must be either a bytestring or a pydicom Dataset. '
475
537
f'Got { content .__class__ } '
476
538
)
539
+ super ().__init__ (code , bytes_content )
477
540
478
- def _guess_implicit_VR (self ):
541
+ @staticmethod
542
+ def _guess_implicit_VR (content ) -> bool :
479
543
"""Try to guess DICOM syntax by checking for valid VRs.
480
544
481
545
Without a DICOM Transfer Syntax, it's difficult to tell if Value
482
546
Representations (VRs) are included in the DICOM encoding or not.
483
547
This reads where the first VR would be and checks it against a list of
484
548
valid VRs
485
549
"""
486
- potential_vr = self ._raw_content [4 :6 ].decode ()
487
- if potential_vr in pdcm .values .converters .keys ():
488
- implicit_VR = False
489
- else :
490
- implicit_VR = True
491
- return implicit_VR
492
-
493
- def _unmangle (self , value , is_implicit_VR = False , is_little_endian = True ):
494
- bio = BytesIO (value )
495
- ds = pdcm .filereader .read_dataset (bio , is_implicit_VR , is_little_endian )
496
- return ds
550
+ potential_vr = content [4 :6 ].decode ()
551
+ return potential_vr not in pdcm .values .converters .keys ()
552
+
553
+ def _unmangle (self , obj : bytes ) -> DicomDataset :
554
+ return pdcm .filereader .read_dataset (
555
+ BytesIO (obj ),
556
+ self ._is_implicit_VR ,
557
+ self ._is_little_endian ,
558
+ )
497
559
498
- def _mangle (self , dataset ) :
560
+ def _mangle (self , dataset : DicomDataset ) -> bytes :
499
561
bio = BytesIO ()
500
562
dio = pdcm .filebase .DicomFileLike (bio )
501
563
dio .is_implicit_VR = self ._is_implicit_VR
0 commit comments