|
1 | 1 | """Input/Output of datasets based on DICOM Part10 files.""" |
2 | 2 | import logging |
| 3 | +from os import PathLike |
3 | 4 | import sys |
4 | 5 | import traceback |
5 | 6 | from typing_extensions import Self |
6 | 7 | from pathlib import Path |
7 | 8 | import weakref |
| 9 | +from typing import BinaryIO |
8 | 10 |
|
9 | 11 | import numpy as np |
10 | 12 | import pydicom |
11 | 13 | from pydicom.dataset import Dataset, FileDataset |
12 | 14 | from pydicom.encaps import parse_basic_offsets |
13 | | -from pydicom.filebase import DicomFile, DicomFileLike, DicomBytesIO |
| 15 | +from pydicom.filebase import ( |
| 16 | + DicomBytesIO, |
| 17 | + DicomFile, |
| 18 | + DicomIO, |
| 19 | + ReadableBuffer, |
| 20 | +) |
14 | 21 | from pydicom.filereader import ( |
15 | 22 | data_element_offset_to_value, |
16 | 23 | dcmread, |
17 | 24 | read_file_meta_info, |
18 | 25 | read_partial |
19 | 26 | ) |
20 | | -from pydicom.tag import TupleTag, ItemTag, SequenceDelimiterTag |
| 27 | +from pydicom.tag import ( |
| 28 | + ItemTag, |
| 29 | + SequenceDelimiterTag, |
| 30 | + TagListType, |
| 31 | + TupleTag, |
| 32 | +) |
21 | 33 | from pydicom.uid import UID, DeflatedExplicitVRLittleEndian |
22 | 34 |
|
23 | 35 | from highdicom.frame import decode_frame |
|
39 | 51 | _END_MARKERS = {_JPEG_EOI_MARKER, _JPEG2000_EOC_MARKER} |
40 | 52 |
|
41 | 53 |
|
42 | | -def _get_bot(fp: DicomFileLike, number_of_frames: int) -> list[int]: |
| 54 | +def _wrapped_dcmread( |
| 55 | + fp: str | PathLike | BinaryIO | ReadableBuffer | bytes, |
| 56 | + defer_size: str | int | float | None = None, |
| 57 | + stop_before_pixels: bool = False, |
| 58 | + force: bool = False, |
| 59 | + specific_tags: TagListType | None = None, |
| 60 | +) -> pydicom.Dataset: |
| 61 | + """A wrapper around dcmread to support reading from bytes. |
| 62 | +
|
| 63 | + Parameters match those of dcmread, but additional `fp` may be a raw bytes |
| 64 | + object containing the contents of a DICOM file. |
| 65 | +
|
| 66 | + """ |
| 67 | + if isinstance(fp, bytes): |
| 68 | + _fp = DicomBytesIO(fp) |
| 69 | + else: |
| 70 | + _fp = fp |
| 71 | + |
| 72 | + return dcmread( |
| 73 | + _fp, |
| 74 | + defer_size=defer_size, |
| 75 | + stop_before_pixels=stop_before_pixels, |
| 76 | + force=force, |
| 77 | + specific_tags=specific_tags, |
| 78 | + ) |
| 79 | + |
| 80 | + |
| 81 | +def _get_bot(fp: DicomIO, number_of_frames: int) -> list[int]: |
43 | 82 | """Tries to read the value of the Basic Offset Table (BOT) item and builds |
44 | 83 | it in case it is empty. |
45 | 84 |
|
46 | 85 | Parameters |
47 | 86 | ---------- |
48 | | - fp: pydicom.filebase.DicomFileLike |
| 87 | + fp: pydicom.filebase.DicomIO |
49 | 88 | Pointer for DICOM PS3.10 file stream positioned at the first byte of |
50 | 89 | the Pixel Data element |
51 | 90 | number_of_frames: int |
@@ -85,12 +124,12 @@ def _get_bot(fp: DicomFileLike, number_of_frames: int) -> list[int]: |
85 | 124 | return basic_offset_table |
86 | 125 |
|
87 | 126 |
|
88 | | -def _read_bot(fp: DicomFileLike) -> list[int]: |
| 127 | +def _read_bot(fp: DicomIO) -> list[int]: |
89 | 128 | """Read Basic Offset Table (BOT) item of encapsulated Pixel Data element. |
90 | 129 |
|
91 | 130 | Parameters |
92 | 131 | ---------- |
93 | | - fp: pydicom.filebase.DicomFileLike |
| 132 | + fp: pydicom.filebase.DicomIO |
94 | 133 | Pointer for DICOM PS3.10 file stream positioned at the first byte of |
95 | 134 | the Pixel Data element |
96 | 135 |
|
@@ -125,12 +164,12 @@ def _read_bot(fp: DicomFileLike) -> list[int]: |
125 | 164 | return offsets |
126 | 165 |
|
127 | 166 |
|
128 | | -def _build_bot(fp: DicomFileLike, number_of_frames: int) -> list[int]: |
| 167 | +def _build_bot(fp: DicomIO, number_of_frames: int) -> list[int]: |
129 | 168 | """Build Basic Offset Table (BOT) item of encapsulated Pixel Data element. |
130 | 169 |
|
131 | 170 | Parameters |
132 | 171 | ---------- |
133 | | - fp: pydicom.filebase.DicomFileLike |
| 172 | + fp: pydicom.filebase.DicomIO |
134 | 173 | Pointer for DICOM PS3.10 file stream positioned at the first byte of |
135 | 174 | the Pixel Data element following the empty Basic Offset Table (BOT) |
136 | 175 | number_of_frames: int |
@@ -295,33 +334,41 @@ class with lazy frame retrieval (e.g. as output by the |
295 | 334 |
|
296 | 335 | """ |
297 | 336 |
|
298 | | - def __init__(self, filename: str | Path | DicomFileLike): |
| 337 | + def __init__(self, filename: str | Path | DicomIO): |
299 | 338 | """ |
300 | 339 | Parameters |
301 | 340 | ---------- |
302 | | - filename: Union[str, pathlib.Path, pydicom.filebase.DicomfileLike] |
| 341 | + filename: Union[str, pathlib.Path, pydicom.filebase.DicomIO] |
303 | 342 | DICOM Part10 file containing a dataset of an image SOP Instance |
304 | 343 |
|
305 | 344 | """ |
306 | | - if isinstance(filename, (DicomFileLike, DicomBytesIO)): |
| 345 | + if isinstance(filename, DicomIO): |
307 | 346 | fp = filename |
308 | 347 | self._fp = fp |
309 | | - if isinstance(filename, DicomBytesIO): |
310 | | - self._filename = None |
311 | | - else: |
| 348 | + if hasattr(filename, "name") and filename.name is not None: |
312 | 349 | self._filename = Path(fp.name) |
| 350 | + else: |
| 351 | + self._filename = None |
| 352 | + |
| 353 | + # Since we did not open the file-like object, we should not close |
| 354 | + # it |
| 355 | + self._should_close = False |
313 | 356 | elif isinstance(filename, (str, Path)): |
314 | 357 | self._filename = Path(filename) |
315 | 358 | self._fp = None |
| 359 | + |
| 360 | + # Since we did open the file-like object, we should close it |
| 361 | + self._should_close = True |
316 | 362 | else: |
317 | 363 | raise TypeError( |
318 | | - 'Argument "filename" must be either an open DICOM file object ' |
| 364 | + 'Argument "filename" must be either an open DicomIO object ' |
319 | 365 | 'or the path to a DICOM file stored on disk.' |
320 | 366 | ) |
321 | 367 | self._metadata: Dataset | weakref.ReferenceType | None = None |
322 | 368 | self._voi_lut = None |
323 | 369 | self._palette_color_lut = None |
324 | 370 | self._modality_lut = None |
| 371 | + self._enter_depth = 0 |
325 | 372 |
|
326 | 373 | def _change_metadata_ownership(self) -> FileDataset: |
327 | 374 | """Set the metadata using a weakref. |
@@ -356,24 +403,25 @@ def filename(self) -> str: |
356 | 403 | return str(self._filename) |
357 | 404 |
|
358 | 405 | def __enter__(self) -> Self: |
359 | | - self.open() |
| 406 | + if self._enter_depth == 0: |
| 407 | + self.open() |
| 408 | + self._enter_depth += 1 |
360 | 409 | return self |
361 | 410 |
|
362 | 411 | def __exit__(self, except_type, except_value, except_trace) -> None: |
363 | | - try: |
364 | | - self._fp.close() |
365 | | - except AttributeError: |
366 | | - pass |
367 | | - else: |
368 | | - self._fp = None |
369 | | - if except_value: |
370 | | - sys.stderr.write( |
371 | | - f'Error while accessing file "{self._filename}":\n' |
372 | | - f'{except_value}' |
373 | | - ) |
374 | | - for tb in traceback.format_tb(except_trace): |
375 | | - sys.stderr.write(tb) |
376 | | - raise |
| 412 | + self._enter_depth -= 1 |
| 413 | + if self._enter_depth < 1: |
| 414 | + if self._should_close: |
| 415 | + self._fp.close() |
| 416 | + self._fp = None |
| 417 | + if except_value: |
| 418 | + sys.stderr.write( |
| 419 | + f'Error while accessing file "{self._filename}":\n' |
| 420 | + f'{except_value}' |
| 421 | + ) |
| 422 | + for tb in traceback.format_tb(except_trace): |
| 423 | + sys.stderr.write(tb) |
| 424 | + raise |
377 | 425 |
|
378 | 426 | def open(self) -> None: |
379 | 427 | """Open file for reading. |
@@ -406,19 +454,21 @@ def open(self) -> None: |
406 | 454 | raise OSError( |
407 | 455 | f'Could not open file for reading: "{self._filename}"' |
408 | 456 | ) from e |
409 | | - is_little_endian, is_implicit_VR = self._check_file_format(self._fp) |
410 | | - self._fp.is_little_endian = is_little_endian |
411 | | - self._fp.is_implicit_VR = is_implicit_VR |
| 457 | + if not hasattr(self._fp, 'is_implicit_VR'): |
| 458 | + self._fp.seek(0) |
| 459 | + is_little_endian, is_implicit_VR = self._check_file_format(self._fp) |
| 460 | + self._fp.is_little_endian = is_little_endian |
| 461 | + self._fp.is_implicit_VR = is_implicit_VR |
412 | 462 |
|
413 | 463 | def _check_file_format( |
414 | 464 | self, |
415 | | - fp: DicomFileLike |
| 465 | + fp: DicomIO |
416 | 466 | ) -> tuple[bool, bool]: |
417 | 467 | """Check whether file object represents a DICOM Part 10 file. |
418 | 468 |
|
419 | 469 | Parameters |
420 | 470 | ---------- |
421 | | - fp: pydicom.filebase.DicomFileLike |
| 471 | + fp: pydicom.filebase.DicomIO |
422 | 472 | DICOM file object |
423 | 473 |
|
424 | 474 | Returns |
@@ -626,10 +676,11 @@ def _bytes_per_frame_uncompressed(self) -> int: |
626 | 676 |
|
627 | 677 | def close(self) -> None: |
628 | 678 | """Closes file.""" |
629 | | - try: |
630 | | - self._fp.close() |
631 | | - except AttributeError: |
632 | | - return |
| 679 | + if self._should_close: |
| 680 | + try: |
| 681 | + self._fp.close() |
| 682 | + except AttributeError: |
| 683 | + return |
633 | 684 |
|
634 | 685 | def read_frame_raw(self, index: int) -> bytes: |
635 | 686 | """Reads the raw pixel data of an individual frame item. |
@@ -748,7 +799,7 @@ def read_frame(self, index: int, correct_color: bool = True) -> np.ndarray: |
748 | 799 | @property |
749 | 800 | def number_of_frames(self) -> int: |
750 | 801 | """int: Number of frames""" |
751 | | - try: |
752 | | - return int(self.metadata.NumberOfFrames) |
753 | | - except AttributeError: |
| 802 | + if 'NumberOfFrames' in self.metadata: |
| 803 | + return self.metadata.NumberOfFrames |
| 804 | + else: |
754 | 805 | return 1 |
0 commit comments