Initial commit

This commit is contained in:
René Mathieu
2026-01-17 13:49:51 +01:00
commit 0fef8d96c5
1897 changed files with 396119 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
from pydicom.pixels.decoders.base import (
ImplicitVRLittleEndianDecoder,
ExplicitVRLittleEndianDecoder,
ExplicitVRBigEndianDecoder,
DeflatedExplicitVRLittleEndianDecoder,
JPEGBaseline8BitDecoder,
JPEGExtended12BitDecoder,
JPEGLosslessDecoder,
JPEGLosslessSV1Decoder,
JPEGLSLosslessDecoder,
JPEGLSNearLosslessDecoder,
JPEG2000LosslessDecoder,
JPEG2000Decoder,
HTJ2KLosslessDecoder,
HTJ2KLosslessRPCLDecoder,
HTJ2KDecoder,
RLELosslessDecoder,
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
# Copyright 2024 pydicom authors. See LICENSE file for details.
"""Use GDCM <https://github.com/malaterre/GDCM> to decompress encoded
*Pixel Data*.
This module is not intended to be used directly.
"""
from typing import cast
from pydicom import uid
from pydicom.pixels.decoders.base import DecodeRunner
from pydicom.pixels.common import PhotometricInterpretation as PI
try:
import gdcm
GDCM_VERSION = tuple(int(x) for x in gdcm.Version.GetVersion().split("."))
HAVE_GDCM = True
except ImportError:
HAVE_GDCM = False
DECODER_DEPENDENCIES = {
uid.JPEGBaseline8Bit: ("gdcm>=3.0.10",),
uid.JPEGExtended12Bit: ("gdcm>=3.0.10",),
uid.JPEGLossless: ("gdcm>=3.0.10",),
uid.JPEGLosslessSV1: ("gdcm>=3.0.10",),
uid.JPEGLSLossless: ("gdcm>=3.0.10",),
uid.JPEGLSNearLossless: ("gdcm>=3.0.10",),
uid.JPEG2000Lossless: ("gdcm>=3.0.10",),
uid.JPEG2000: ("gdcm>=3.0.10",),
}
def is_available(uid: str) -> bool:
"""Return ``True`` if a pixel data decoder for `uid` is available for use,
``False`` otherwise.
"""
if not HAVE_GDCM or GDCM_VERSION < (3, 0):
return False
return uid in DECODER_DEPENDENCIES
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytes:
"""Return the decoded `src` as :class:`bytes`.
Parameters
----------
src : bytes
An encoded pixel data frame.
runner : pydicom.pixels.decoders.base.DecodeRunner
The runner managing the decoding.
Returns
-------
bytes
The decoded pixel data frame.
"""
tsyntax = runner.transfer_syntax
photometric_interpretation = runner.photometric_interpretation
bits_stored = runner.bits_stored
if tsyntax == uid.JPEGExtended12Bit and bits_stored != 8:
raise NotImplementedError(
"GDCM does not support 'JPEG Extended' for samples with 12-bit precision"
)
if (
tsyntax == uid.JPEGLSNearLossless
and runner.pixel_representation == 1
and bits_stored < 8
):
raise ValueError(
"Unable to decode signed lossy JPEG-LS pixel data with a sample "
"precision less than 8 bits"
)
if tsyntax in uid.JPEGLSTransferSyntaxes and bits_stored in (6, 7):
raise ValueError(
"Unable to decode unsigned JPEG-LS pixel data with a sample "
"precision of 6 or 7 bits"
)
fragment = gdcm.Fragment()
fragment.SetByteStringValue(src)
fragments = gdcm.SequenceOfFragments.New()
fragments.AddFragment(fragment)
elem = gdcm.DataElement(gdcm.Tag(0x7FE0, 0x0010))
elem.SetValue(fragments.__ref__())
img = gdcm.Image()
img.SetNumberOfDimensions(2)
img.SetDimensions((runner.columns, runner.rows, 1))
img.SetDataElement(elem)
pi_type = gdcm.PhotometricInterpretation.GetPIType(photometric_interpretation)
img.SetPhotometricInterpretation(gdcm.PhotometricInterpretation(pi_type))
if runner.samples_per_pixel > 1:
img.SetPlanarConfiguration(runner.planar_configuration)
ts_type = gdcm.TransferSyntax.GetTSType(str.__str__(tsyntax))
img.SetTransferSyntax(gdcm.TransferSyntax(ts_type))
if tsyntax in uid.JPEGLSTransferSyntaxes:
# GDCM always returns JPEG-LS data as color-by-pixel
runner.set_option("planar_configuration", 0)
bits_stored = runner.get_option("jls_precision", bits_stored)
if 0 < bits_stored <= 8:
runner.set_option("bits_allocated", 8)
elif 8 < bits_stored <= 16:
runner.set_option("bits_allocated", 16)
if tsyntax in uid.JPEG2000TransferSyntaxes:
# GDCM pixel container size is based on precision
bits_stored = runner.get_option("j2k_precision", bits_stored)
if 0 < bits_stored <= 8:
runner.set_option("bits_allocated", 8)
elif 8 < bits_stored <= 16:
runner.set_option("bits_allocated", 16)
elif 16 < bits_stored <= 32:
runner.set_option("bits_allocated", 32)
pixel_format = gdcm.PixelFormat(
runner.samples_per_pixel,
runner.bits_allocated,
bits_stored,
bits_stored - 1,
runner.pixel_representation,
)
img.SetPixelFormat(pixel_format)
# GDCM returns char* as str, so re-encode it to bytes
frame = img.GetBuffer().encode("utf-8", "surrogateescape")
# GDCM returns YBR_ICT and YBR_RCT as RGB
if tsyntax in uid.JPEG2000TransferSyntaxes and photometric_interpretation in (
PI.YBR_ICT,
PI.YBR_RCT,
):
runner.set_option("photometric_interpretation", PI.RGB)
return cast(bytes, frame)

View File

@@ -0,0 +1,127 @@
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
"""Use Pillow <https://github.com/python-pillow/Pillow> to decompress encoded
*Pixel Data*.
This module is not intended to be used directly.
"""
from io import BytesIO
from typing import cast
from pydicom import uid
from pydicom.pixels.utils import _passes_version_check
from pydicom.pixels.common import PhotometricInterpretation as PI
from pydicom.pixels.decoders.base import DecodeRunner
try:
from PIL import Image, features
except ImportError:
pass
try:
import numpy as np
HAVE_NP = True
except ImportError:
HAVE_NP = False
DECODER_DEPENDENCIES = {
uid.JPEGBaseline8Bit: ("pillow>=10.0",),
uid.JPEGExtended12Bit: ("pillow>=10.0",),
uid.JPEG2000Lossless: ("numpy", "pillow>=10.0"),
uid.JPEG2000: ("numpy", "pillow>=10.0"),
}
_LIBJPEG_SYNTAXES = [uid.JPEGBaseline8Bit, uid.JPEGExtended12Bit]
_OPENJPEG_SYNTAXES = [uid.JPEG2000Lossless, uid.JPEG2000]
def is_available(uid: str) -> bool:
"""Return ``True`` if a pixel data decoder for `uid` is available for use,
``False`` otherwise.
"""
if not _passes_version_check("PIL", (10, 0)):
return False
if uid in _LIBJPEG_SYNTAXES:
return bool(features.check_codec("jpg")) # type: ignore[no-untyped-call]
if uid in _OPENJPEG_SYNTAXES:
return bool(features.check_codec("jpg_2000")) and HAVE_NP # type: ignore[no-untyped-call]
return False
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytes:
"""Return the decoded image data in `src` as a :class:`bytes`."""
tsyntax = runner.transfer_syntax
# libjpeg only supports 8-bit JPEG Extended (can be 8 or 12 in the JPEG standard)
if tsyntax == uid.JPEGExtended12Bit and runner.bits_stored != 8:
raise NotImplementedError(
"Pillow does not support 'JPEG Extended' for samples with 12-bit precision"
)
image = Image.open(BytesIO(src), formats=("JPEG", "JPEG2000"))
if tsyntax in _LIBJPEG_SYNTAXES:
if runner.samples_per_pixel != 1:
# If the Adobe APP14 marker is not present then Pillow assumes
# that JPEG images were transformed into YCbCr color space prior
# to compression, so setting the image mode to YCbCr signals we
# don't want any color transformations.
# Any color transformations would be inconsistent with the
# behavior required by the `raw` flag
if "adobe_transform" not in image.info:
image.draft("YCbCr", image.size) # type: ignore[no-untyped-call]
return cast(bytes, image.tobytes())
# JPEG 2000
# The precision from the J2K codestream is more appropriate because the
# decoder will use it to create the output integers
precision = runner.get_option("j2k_precision", runner.bits_stored)
# pillow's pixel container size is based on precision
if 0 < precision <= 8:
runner.set_option("bits_allocated", 8)
elif 8 < precision <= 16:
# Pillow converts >= 9-bit RGB/YCbCr data to 8-bit
if runner.samples_per_pixel > 1:
raise ValueError(
f"Pillow cannot decode {precision}-bit multi-sample data correctly"
)
runner.set_option("bits_allocated", 16)
else:
raise ValueError(
"only (0028,0101) 'Bits Stored' values of up to 16 are supported"
)
# Pillow converts N-bit signed/unsigned data to 8- or 16-bit unsigned data
# See Pillow src/libImaging/Jpeg2KDecode.c::j2ku_gray_i
buffer = bytearray(image.tobytes()) # so the array is writeable
del image
dtype = runner.pixel_dtype
arr = np.frombuffer(buffer, dtype=f"u{dtype.itemsize}")
is_signed = runner.pixel_representation
if runner.get_option("apply_j2k_sign_correction", False):
is_signed = runner.get_option("j2k_is_signed", is_signed)
if is_signed and runner.pixel_representation == 1:
# Re-view the unsigned integers as signed
# e.g. [0, 127, 128, 255] -> [0, 127, -128, -1]
arr = arr.view(dtype)
# Level-shift to match the unsigned integers range
# e.g. [0, 127, -128, -1] -> [-128, -1, 0, 127]
arr -= np.int32(2 ** (runner.bits_allocated - 1))
if bit_shift := (runner.bits_allocated - precision):
# Bit shift to undo the upscaling of N-bit to 8- or 16-bit
np.right_shift(arr, bit_shift, out=arr)
# pillow returns YBR_ICT and YBR_RCT as RGB
if runner.photometric_interpretation in (PI.YBR_ICT, PI.YBR_RCT):
runner.set_option("photometric_interpretation", PI.RGB)
return cast(bytes, arr.tobytes())

View File

@@ -0,0 +1,47 @@
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
"""Use pyjpegls <https://github.com/pydicom/pyjpegls> to decompress encoded
*Pixel Data*.
This module is not intended to be used directly.
"""
from typing import cast
from pydicom import uid
from pydicom.pixels.utils import _passes_version_check
from pydicom.pixels.decoders.base import DecodeRunner
try:
import jpeg_ls
except ImportError:
pass
DECODER_DEPENDENCIES = {
uid.JPEGLSLossless: ("numpy", "pyjpegls>=1.2"),
uid.JPEGLSNearLossless: ("numpy", "pyjpegls>=1.2"),
}
def is_available(uid: str) -> bool:
"""Return ``True`` if the decoder has its dependencies met, ``False`` otherwise"""
return _passes_version_check("jpeg_ls", (1, 2))
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray:
"""Return the decoded image data in `src` as a :class:`bytearray`."""
buffer, info = jpeg_ls.decode_pixel_data(src)
# Interleave mode 0 is colour-by-plane, 1 and 2 are colour-by-pixel
if info["components"] > 1:
if info["interleave_mode"] == 0:
runner.set_option("planar_configuration", 1)
else:
runner.set_option("planar_configuration", 0)
precision = info["bits_per_sample"]
if 0 < precision <= 8:
runner.set_option("bits_allocated", 8)
elif 8 < precision <= 16:
runner.set_option("bits_allocated", 16)
return cast(bytearray, buffer)

View File

@@ -0,0 +1,115 @@
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
"""Use pylibjpeg <https://github.com/pydicom/pylibjpeg> to decompress encoded
*Pixel Data*.
This module is not intended to be used directly.
"""
from typing import cast
from pydicom import uid
from pydicom.pixels.decoders.base import DecodeRunner
from pydicom.pixels.utils import _passes_version_check
from pydicom.pixels.common import PhotometricInterpretation as PI
try:
from pylibjpeg.utils import get_pixel_data_decoders, Decoder
# {UID: {plugin name: function}}
_DECODERS = cast(
dict[uid.UID, dict[str, "Decoder"]], get_pixel_data_decoders(version=2)
)
except ImportError:
_DECODERS = {}
DECODER_DEPENDENCIES = {
uid.JPEGBaseline8Bit: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
uid.JPEGExtended12Bit: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
uid.JPEGLossless: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
uid.JPEGLosslessSV1: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
uid.JPEGLSLossless: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
uid.JPEGLSNearLossless: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
uid.JPEG2000Lossless: ("pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.0"),
uid.JPEG2000: ("pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.0"),
uid.HTJ2KLossless: ("pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.0"),
uid.HTJ2KLosslessRPCL: ("pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.0"),
uid.HTJ2K: ("pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.0"),
uid.RLELossless: ("pylibjpeg>=2.0", "pylibjpeg-rle>=2.0"),
}
_LIBJPEG_SYNTAXES = [
uid.JPEGBaseline8Bit,
uid.JPEGExtended12Bit,
uid.JPEGLossless,
uid.JPEGLosslessSV1,
uid.JPEGLSLossless,
uid.JPEGLSNearLossless,
]
_OPENJPEG_SYNTAXES = [
uid.JPEG2000Lossless,
uid.JPEG2000,
uid.HTJ2KLossless,
uid.HTJ2KLosslessRPCL,
uid.HTJ2K,
]
_RLE_SYNTAXES = [uid.RLELossless]
def is_available(uid: str) -> bool:
"""Return ``True`` if the decoder has its dependencies met, ``False`` otherwise"""
if not _passes_version_check("pylibjpeg", (2, 0)):
return False
if uid in _LIBJPEG_SYNTAXES:
return _passes_version_check("libjpeg", (2, 0, 2))
if uid in _OPENJPEG_SYNTAXES:
return _passes_version_check("openjpeg", (2, 0))
if uid in _RLE_SYNTAXES:
return _passes_version_check("rle", (2, 0))
return False
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray: # type: ignore[return]
"""Return the decoded image data in `src` as a :class:`bytearray`."""
tsyntax = runner.transfer_syntax
# Currently only one pylibjpeg plugin is available per UID
# so decode using the first available decoder
for _, func in sorted(_DECODERS[tsyntax].items()):
# `version=2` to return frame as bytearray
frame = cast(bytearray, func(src, version=2, **runner.options))
# pylibjpeg-rle returns decoded data as planar configuration 1
if tsyntax == uid.RLELossless:
runner.set_option("planar_configuration", 1)
if tsyntax in _OPENJPEG_SYNTAXES:
# pylibjpeg-openjpeg returns YBR_ICT and YBR_RCT as RGB
if runner.photometric_interpretation in (PI.YBR_ICT, PI.YBR_RCT):
runner.set_option("photometric_interpretation", PI.RGB)
# pylibjpeg-openjpeg pixel container size is based on J2K precision
precision = runner.get_option("j2k_precision", runner.bits_stored)
if 0 < precision <= 8:
runner.set_option("bits_allocated", 8)
elif 8 < precision <= 16:
runner.set_option("bits_allocated", 16)
elif 16 < precision <= 32:
runner.set_option("bits_allocated", 32)
if tsyntax in uid.JPEGLSTransferSyntaxes:
# pylibjpeg-libjpeg always returns JPEG-LS data as color-by-pixel
runner.set_option("planar_configuration", 0)
# pylibjpeg-libjpeg pixel container size is based on JPEG-LS precision
precision = runner.get_option("jls_precision", runner.bits_stored)
if 0 < precision <= 8:
runner.set_option("bits_allocated", 8)
elif 8 < precision <= 16:
runner.set_option("bits_allocated", 16)
return frame

View File

@@ -0,0 +1,278 @@
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
"""Use Python to decode RLE Lossless encoded *Pixel Data*.
This module is not intended to be used directly.
"""
from struct import unpack
from pydicom.misc import warn_and_log
from pydicom.pixels.decoders.base import DecodeRunner
from pydicom.uid import RLELossless
DECODER_DEPENDENCIES = {RLELossless: ()}
def is_available(uid: str) -> bool:
"""Return ``True`` if a pixel data decoder for `uid` is available for use,
``False`` otherwise.
"""
return uid in DECODER_DEPENDENCIES
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray:
"""Wrapper for use with the decoder interface.
Parameters
----------
src : bytes
A single frame of RLE encoded data.
runner : pydicom.pixels.decoders.base.DecodeRunner
Required parameters:
* `rows`: int
* `columns`: int
* `samples_per_pixel`: int
* `bits_allocated`: int
Optional parameters:
* `rle_segment_order`: str, "<" for little endian segment order, or
">" for big endian (default)
Returns
-------
bytearray
The decoded frame, ordered as planar configuration 1.
"""
frame = _rle_decode_frame(
src,
runner.rows,
runner.columns,
runner.samples_per_pixel,
runner.bits_allocated,
runner.get_option("rle_segment_order", ">"),
)
# Update the runner options to ensure the reshaping is correct
# Only do this if we successfully decoded the frame
runner.set_option("planar_configuration", 1)
return frame
def _rle_decode_frame(
src: bytes,
rows: int,
columns: int,
nr_samples: int,
nr_bits: int,
segment_order: str = ">",
) -> bytearray:
"""Decodes a single frame of RLE encoded data.
Each frame may contain up to 15 segments of encoded data.
Parameters
----------
src : bytes
The RLE frame data
rows : int
The number of output rows
columns : int
The number of output columns
nr_samples : int
Number of samples per pixel (e.g. 3 for RGB data).
nr_bits : int
Number of bits per sample - must be a multiple of 8
segment_order : str
The segment order of the `data`, '>' for big endian (default),
'<' for little endian (non-conformant).
Returns
-------
bytearray
The frame's decoded data in little endian and planar configuration 1
byte ordering (i.e. for RGB data this is all red pixels then all
green then all blue, with the bytes for each pixel ordered from
MSB to LSB when reading left to right).
"""
if nr_bits % 8:
raise NotImplementedError(
f"Unable to decode RLE encoded pixel data with {nr_bits} bits allocated"
)
# Parse the RLE Header
offsets = _rle_parse_header(src[:64])
nr_segments = len(offsets)
# Check that the actual number of segments is as expected
bytes_per_sample = nr_bits // 8
if nr_segments != nr_samples * bytes_per_sample:
raise ValueError(
"The number of RLE segments in the pixel data doesn't match the "
f"expected amount ({nr_segments} vs. {nr_samples * bytes_per_sample} "
"segments)"
)
# Ensure the last segment gets decoded
offsets.append(len(src))
# Preallocate with null bytes
decoded = bytearray(rows * columns * nr_samples * bytes_per_sample)
# Example:
# RLE encoded data is ordered like this (for 16-bit, 3 sample):
# Segment: 0 | 1 | 2 | 3 | 4 | 5
# R MSB | R LSB | G MSB | G LSB | B MSB | B LSB
# A segment contains only the MSB or LSB parts of all the sample pixels
# To minimise the amount of array manipulation later, and to make things
# faster we interleave each segment in a manner consistent with a planar
# configuration of 1 (and use little endian byte ordering):
# All red samples | All green samples | All blue
# Pxl 1 Pxl 2 ... Pxl N | Pxl 1 Pxl 2 ... Pxl N | ...
# LSB MSB LSB MSB ... LSB MSB | LSB MSB LSB MSB ... LSB MSB | ...
# `stride` is the total number of bytes of each sample plane
stride = bytes_per_sample * rows * columns
for sample_number in range(nr_samples):
le_gen = range(bytes_per_sample)
byte_offsets = le_gen if segment_order == "<" else reversed(le_gen)
for byte_offset in byte_offsets:
# Decode the segment
ii = sample_number * bytes_per_sample + byte_offset
# ii is 1, 0, 3, 2, 5, 4 for the example above
# This is where the segment order correction occurs
segment = _rle_decode_segment(src[offsets[ii] : offsets[ii + 1]])
# Check that the number of decoded bytes is correct
actual_length = len(segment)
if actual_length < rows * columns:
raise ValueError(
"The amount of decoded RLE segment data doesn't match the "
f"expected amount ({actual_length} vs. {rows * columns} bytes)"
)
elif actual_length != rows * columns:
warn_and_log(
"The decoded RLE segment contains non-conformant padding "
f"- {actual_length} vs. {rows * columns} bytes expected"
)
if segment_order == ">":
byte_offset = bytes_per_sample - byte_offset - 1
# For 100 pixel/plane, 32-bit, 3 sample data, `start` will be
# 0, 1, 2, 3, 400, 401, 402, 403, 800, 801, 802, 803
start = byte_offset + (sample_number * stride)
decoded[start : start + stride : bytes_per_sample] = segment[
: rows * columns
]
return decoded
def _rle_decode_segment(src: bytes) -> bytearray:
"""Return a single segment of decoded RLE data as bytearray.
Parameters
----------
buffer : bytes
The segment data to be decoded.
Returns
-------
bytearray
The decoded segment.
"""
result = bytearray()
pos = 0
result_extend = result.extend
try:
while True:
# header_byte is N + 1
header_byte = src[pos] + 1
pos += 1
if header_byte > 129:
# Extend by copying the next byte (-N + 1) times
# however since using uint8 instead of int8 this will be
# (256 - N + 1) times
result_extend(src[pos : pos + 1] * (258 - header_byte))
pos += 1
elif header_byte < 129:
# Extend by literally copying the next (N + 1) bytes
result_extend(src[pos : pos + header_byte])
pos += header_byte
except IndexError:
pass
return result
def _rle_parse_header(header: bytes) -> list[int]:
"""Return a list of byte offsets for the segments in RLE data.
**RLE Header Format**
The RLE Header contains the number of segments for the image and the
starting offset of each segment. Each of these numbers is represented as
an unsigned long stored in little-endian. The RLE Header is 16 long words
in length (i.e. 64 bytes) which allows it to describe a compressed image
with up to 15 segments. All unused segment offsets shall be set to zero.
As an example, the table below describes an RLE Header with 3 segments as
would typically be used with 8-bit RGB or YCbCr data (with 1 segment per
channel).
+--------------+---------------------------------+------------+
| Byte offset | Description | Value |
+==============+=================================+============+
| 0 | Number of segments | 3 |
+--------------+---------------------------------+------------+
| 4 | Offset of segment 1, N bytes | 64 |
+--------------+---------------------------------+------------+
| 8 | Offset of segment 2, M bytes | 64 + N |
+--------------+---------------------------------+------------+
| 12 | Offset of segment 3 | 64 + N + M |
+--------------+---------------------------------+------------+
| 16 | Offset of segment 4 (not used) | 0 |
+--------------+---------------------------------+------------+
| ... | ... | 0 |
+--------------+---------------------------------+------------+
| 60 | Offset of segment 15 (not used) | 0 |
+--------------+---------------------------------+------------+
Parameters
----------
header : bytes
The RLE header data (i.e. the first 64 bytes of an RLE frame).
Returns
-------
list of int
The byte offsets for each segment in the RLE data.
Raises
------
ValueError
If there are more than 15 segments or if the header is not 64 bytes
long.
References
----------
DICOM Standard, Part 5, :dcm:`Annex G<part05/chapter_G.html>`
"""
if len(header) != 64:
raise ValueError("The RLE header can only be 64 bytes long")
nr_segments = unpack("<L", header[:4])[0]
if nr_segments > 15:
raise ValueError(
f"The RLE header specifies an invalid number of segments ({nr_segments})"
)
return list(unpack(f"<{nr_segments}L", header[4 : 4 * (nr_segments + 1)]))