diff --git a/.github/workflows/pytest-builds.yml b/.github/workflows/pytest-builds.yml index e2e3a68..cf73b49 100644 --- a/.github/workflows/pytest-builds.yml +++ b/.github/workflows/pytest-builds.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] os: [ubuntu-latest, windows-latest, macos-latest] steps: diff --git a/.github/workflows/release-wheels.yml b/.github/workflows/release-wheels.yml index da26dca..95dc10f 100644 --- a/.github/workflows/release-wheels.yml +++ b/.github/workflows/release-wheels.yml @@ -46,12 +46,6 @@ jobs: matrix: include: # Windows 32 bit - - os: windows-latest - python: 38 - platform_id: win32 - - os: windows-latest - python: 39 - platform_id: win32 - os: windows-latest python: 310 platform_id: win32 @@ -61,14 +55,11 @@ jobs: - os: windows-latest python: 312 platform_id: win32 + - os: windows-latest + python: 313 + platform_id: win32 # Windows 64 bit - - os: windows-latest - python: 38 - platform_id: win_amd64 - - os: windows-latest - python: 39 - platform_id: win_amd64 - os: windows-latest python: 310 platform_id: win_amd64 @@ -78,16 +69,11 @@ jobs: - os: windows-latest python: 312 platform_id: win_amd64 + - os: windows-latest + python: 313 + platform_id: win_amd64 # Linux 64 bit manylinux2014 - - os: ubuntu-latest - python: 38 - platform_id: manylinux_x86_64 - manylinux_image: manylinux2014 - - os: ubuntu-latest - python: 39 - platform_id: manylinux_x86_64 - manylinux_image: manylinux2014 - os: ubuntu-latest python: 310 platform_id: manylinux_x86_64 @@ -100,14 +86,12 @@ jobs: python: 312 platform_id: manylinux_x86_64 manylinux_image: manylinux2014 + - os: ubuntu-latest + python: 313 + platform_id: manylinux_x86_64 + manylinux_image: manylinux2014 # Linux aarch64 - - os: ubuntu-latest - python: 38 - platform_id: manylinux_aarch64 - - os: ubuntu-latest - python: 39 - platform_id: manylinux_aarch64 - os: ubuntu-latest python: 310 platform_id: manylinux_aarch64 @@ -117,14 +101,11 @@ jobs: - os: ubuntu-latest python: 312 platform_id: manylinux_aarch64 + - os: ubuntu-latest + python: 313 + platform_id: manylinux_aarch64 # MacOS x86_64 - - os: macos-latest - python: 38 - platform_id: macosx_x86_64 - - os: macos-latest - python: 39 - platform_id: macosx_x86_64 - os: macos-latest python: 310 platform_id: macosx_x86_64 @@ -134,14 +115,11 @@ jobs: - os: macos-latest python: 312 platform_id: macosx_x86_64 + - os: macos-latest + python: 313 + platform_id: macosx_x86_64 # MacOS arm64 - - os: macos-latest - python: 38 - platform_id: macosx_arm64 - - os: macos-latest - python: 39 - platform_id: macosx_arm64 - os: macos-latest python: 310 platform_id: macosx_arm64 @@ -151,6 +129,9 @@ jobs: - os: macos-latest python: 312 platform_id: macosx_arm64 + - os: macos-latest + python: 313 + platform_id: macosx_arm64 steps: - uses: actions/checkout@v4 @@ -170,7 +151,7 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: '3.9' + python-version: '3.10' - name: Install cibuildwheel run: | @@ -206,7 +187,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - name: Install Rust (stable) diff --git a/Cargo.lock b/Cargo.lock index 578574f..77d35d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,18 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "cfg-if" @@ -22,97 +16,71 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indoc" -version = "2.0.4" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" - -[[package]] -name = "lock_api" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" -dependencies = [ - "autocfg", - "scopeguard", -] +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "memoffset" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "parking_lot_core" -version = "0.9.9" +name = "portable-atomic" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "proc-macro2" -version = "1.0.75" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "pylibjpeg-rle" -version = "1.0.0" +version = "1.1.0" dependencies = [ "pyo3", ] [[package]] name = "pyo3" -version = "0.20.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0" +checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" dependencies = [ "cfg-if", "indoc", "libc", "memoffset", - "parking_lot", + "once_cell", + "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", @@ -121,9 +89,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.20.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be" +checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" dependencies = [ "once_cell", "target-lexicon", @@ -131,9 +99,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.20.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1" +checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" dependencies = [ "libc", "pyo3-build-config", @@ -141,9 +109,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.20.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f738b4e40d50b5711957f142878cfa0f28e054aa0ebdfc3fd137a843f74ed3" +checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -153,51 +121,31 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.20.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f" +checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" dependencies = [ "heck", "proc-macro2", + "pyo3-build-config", "quote", "syn", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "smallvec" -version = "1.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" - [[package]] name = "syn" -version = "2.0.48" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -206,75 +154,18 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.13" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unindent" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/Cargo.toml b/Cargo.toml index 2d24621..a78c8e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "pylibjpeg-rle" -version = "1.0.0" +version = "1.1.0" authors = ["scaramallion "] -edition = "2021" +edition = "2024" exclude = [".github", "docs", ".codecov.yml", "asv.*", ".gitignore", ".coveragerc"] [lib] @@ -11,4 +11,4 @@ crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.20.2", features = ["extension-module"] } +pyo3 = { version = "0.24.2", features = ["extension-module"] } diff --git a/docs/release_notes/v2.1.0.rst b/docs/release_notes/v2.1.0.rst new file mode 100644 index 0000000..3029b6a --- /dev/null +++ b/docs/release_notes/v2.1.0.rst @@ -0,0 +1,16 @@ +.. _v2.1.0: + +2.1.0 +===== + +Changes +....... + +* Updated to PyO3 v0.24 and Rust v2024 +* Dropped support for Python 3.8 and 3.9, added support for Python 3.13 +* Requires NumPy > v2.0 + +Enhancements +............ + +* The row encoder has been rewritten and is about 20% faster diff --git a/pyproject.toml b/pyproject.toml index 0df2b7a..859c0af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,11 +14,10 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Natural Language :: English", "Programming Language :: Rust", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX :: Linux", "Operating System :: Microsoft :: Windows", @@ -33,10 +32,16 @@ keywords = ["dicom pydicom python rle pylibjpeg rust"] license = "MIT" name = "pylibjpeg-rle" readme = "README.md" -version = "2.1.0.dev0" -requires-python = ">=3.8" +version = "2.1.0" +requires-python = ">=3.10" dependencies = [ - "numpy>=1.24", + "numpy>=2.0", +] + +[project.optional-dependencies] +test = [ + "pytest==8.3.5", + "pydicom>=3.0", ] [project.urls] diff --git a/rle/benchmarks/bench_encode.py b/rle/benchmarks/bench_encode.py index c952a1c..9715b2a 100644 --- a/rle/benchmarks/bench_encode.py +++ b/rle/benchmarks/bench_encode.py @@ -3,14 +3,8 @@ from pydicom import dcmread from pydicom.data import get_testdata_file -from pydicom.encaps import generate_pixel_data_frame -from pydicom.pixel_data_handlers.rle_handler import ( - get_pixeldata, - _rle_decode_frame, - _rle_encode_row, - rle_encode_frame, -) -from pydicom.pixel_data_handlers.util import reshape_pixel_array +from pydicom.pixels.encoders.native import _rle_encode_row, rle_encode_frame +from pydicom.pixels.utils import reshape_pixel_array from pydicom.uid import RLELossless from ljdata import get_indexed_datasets diff --git a/rle/tests/test_decode.py b/rle/tests/test_decode.py index 19283b6..36df11e 100644 --- a/rle/tests/test_decode.py +++ b/rle/tests/test_decode.py @@ -8,13 +8,6 @@ try: from pydicom import dcmread - from pydicom.encaps import generate_pixel_data_frame - from pydicom.pixel_data_handlers.rle_handler import ( - _parse_rle_header, - _rle_decode_frame, - _rle_decode_segment, - ) - from pydicom.pixel_data_handlers.util import pixel_dtype, reshape_pixel_array from pydicom.uid import RLELossless HAVE_PYDICOM = True diff --git a/rle/tests/test_encode.py b/rle/tests/test_encode.py index 7137e1c..4548c10 100644 --- a/rle/tests/test_encode.py +++ b/rle/tests/test_encode.py @@ -7,21 +7,20 @@ try: from pydicom import dcmread - from pydicom.data import get_testdata_file from pydicom.dataset import Dataset, FileMetaDataset - from pydicom.encaps import generate_pixel_data_frame, defragment_data - from pydicom.pixel_data_handlers.rle_handler import ( - _parse_rle_header, - _rle_decode_frame, - _rle_decode_segment, - ) - from pydicom.pixel_data_handlers.util import pixel_dtype, reshape_pixel_array + from pydicom.encaps import generate_frames + from pydicom.pixels.utils import pixel_dtype, reshape_pixel_array from pydicom.uid import RLELossless HAVE_PYDICOM = True except ImportError: HAVE_PYDICOM = False +try: + from pydicom.pixels.decoders.native import _rle_decode_frame +except ImportError: + from pydicom.pixels.decoders.rle import _rle_decode_frame + from rle.data import get_indexed_datasets from rle.rle import ( @@ -74,14 +73,17 @@ pytest.param([0, 1] * 64 * 5, (b"\x7f" + b"\x00\x01" * 64) * 5, id="16"), # Combination run tests # 2 literal, 1(min) literal - # or 1 (min) literal, 1 (min) replicate b'\x00\x00\xff\x01' - pytest.param([0, 1, 1], b"\x01\x00\x01\x00\x01", id="17"), + # pytest.param([0, 1, 1], b"\x01\x00\x01\x00\x01", id="17"), + # or 1 (min) literal, 1 (min) replicate b'\x00\x00\xff\x01' <-- this + pytest.param([0, 1, 1], b"\x00\x00\xff\x01", id="17"), # 2 literal, 127 replicate - # or 1 (min) literal, 128 (max) replicate - pytest.param([0] + [1] * 128, b"\x01\x00\x01\x82\x01", id="18"), + # pytest.param([0] + [1] * 128, b"\x01\x00\x01\x82\x01", id="18"), + # or 1 (min) literal, 128 (max) replicate <-- this + pytest.param([0] + [1] * 128, b"\x00\x00\x81\x01", id="18"), # 2 literal, 128 (max) replicate - # or 1 (min) literal, 128 (max) replicate, 1 (min) literal - pytest.param([0] + [1] * 129, b"\x01\x00\x01\x81\x01", id="18b"), + # pytest.param([0] + [1] * 129, b"\x01\x00\x01\x81\x01", id="18b"), + # or 1 (min) literal, 128 (max) replicate, 1 (min) literal <-- this + pytest.param([0] + [1] * 129, b"\x00\x00\x81\x01\x00\x01", id="18b"), # 128 (max) literal, 2 (min) replicate # 128 (max literal) pytest.param( @@ -118,7 +120,7 @@ class TestEncodeSegment: def test_one_row(self): """Test encoding data that contains only a single row.""" ds = INDEX_RLE["OBXXXX1A_rle.dcm"]["ds"] - pixel_data = defragment_data(ds.PixelData) + pixel_data = b"".join(generate_frames(ds.PixelData)) decoded = decode_segment(pixel_data[64:]) assert ds.Rows * ds.Columns == len(decoded) arr = np.frombuffer(decoded, "uint8").reshape(ds.Rows, ds.Columns) @@ -136,7 +138,7 @@ def test_one_row(self): def test_cycle(self): """Test the decoded data remains the same after encoding/decoding.""" ds = INDEX_RLE["OBXXXX1A_rle.dcm"]["ds"] - pixel_data = defragment_data(ds.PixelData) + pixel_data = b"".join(generate_frames(ds.PixelData)) decoded = decode_segment(pixel_data[64:]) assert ds.Rows * ds.Columns == len(decoded) arr = np.frombuffer(decoded, "uint8").reshape(ds.Rows, ds.Columns) diff --git a/rle/tests/test_handler.py b/rle/tests/test_handler.py index 80162a8..1665dc5 100644 --- a/rle/tests/test_handler.py +++ b/rle/tests/test_handler.py @@ -5,7 +5,7 @@ try: from pydicom import dcmread - from pydicom.encaps import generate_pixel_data_frame + from pydicom.encaps import generate_fragmented_frames from pydicom.uid import RLELossless HAVE_PYDICOM = True @@ -33,7 +33,7 @@ def test_no_dataset_kwargs_raises(self): assert 800 == ds.Columns assert 1 == getattr(ds, "NumberOfFrames", 1) - frame = next(generate_pixel_data_frame(ds.PixelData)) + frame = next(generate_fragmented_frames(ds.PixelData)) msg = r"Either `ds` or `\*\*kwargs` must be used" with pytest.raises(ValueError, match=msg): decode_pixel_data(frame) @@ -49,7 +49,7 @@ def test_u8_1s_1f(self): assert 800 == ds.Columns assert 1 == getattr(ds, "NumberOfFrames", 1) - frame = next(generate_pixel_data_frame(ds.PixelData)) + frame = b"".join(next(generate_fragmented_frames(ds.PixelData))) arr = decode_pixel_data(frame, ds) assert (480000,) == arr.shape assert arr.flags.writeable @@ -66,7 +66,7 @@ def test_u32_3s_1f(self): assert 100 == ds.Columns assert 1 == getattr(ds, "NumberOfFrames", 1) - frame = next(generate_pixel_data_frame(ds.PixelData)) + frame = b"".join(next(generate_fragmented_frames(ds.PixelData))) arr = decode_pixel_data(frame, ds) assert (120000,) == arr.shape assert arr.flags.writeable @@ -81,7 +81,7 @@ def test_v2(self): "rows": ds.Rows, "columns": ds.Columns, } - frame = next(generate_pixel_data_frame(ds.PixelData)) + frame = b"".join(next(generate_fragmented_frames(ds.PixelData))) buffer = decode_pixel_data(frame, version=2, **opts) assert isinstance(buffer, bytearray) arr = np.frombuffer(buffer, dtype="u1") @@ -97,7 +97,7 @@ def test_v2_missing_kwarg_raises(self): "rows": ds.Rows, # "columns": ds.Columns, } - frame = next(generate_pixel_data_frame(ds.PixelData)) + frame = next(generate_fragmented_frames(ds.PixelData)) msg = "Missing expected keyword arguments: columns" with pytest.raises(AttributeError, match=msg): buffer = decode_pixel_data(frame, version=2, **opts) diff --git a/rle/tests/test_utils.py b/rle/tests/test_utils.py index 8c94ea8..5371d88 100644 --- a/rle/tests/test_utils.py +++ b/rle/tests/test_utils.py @@ -5,14 +5,16 @@ try: from pydicom import dcmread - from pydicom.encaps import generate_pixel_data_frame - from pydicom.pixel_data_handlers.rle_handler import _rle_decode_frame - from pydicom.pixel_data_handlers.util import pixel_dtype, reshape_pixel_array + from pydicom.pixels.utils import pixel_dtype from pydicom.uid import RLELossless HAVE_PYDICOM = True except ImportError: HAVE_PYDICOM = False +try: + from pydicom.pixels.decoders.native import _rle_decode_frame +except ImportError: + from pydicom.pixels.decoders.rle import _rle_decode_frame from rle.data import get_indexed_datasets from rle.utils import encode_pixel_data, encode_array, pixel_data, pixel_array diff --git a/rle/utils.py b/rle/utils.py index c336842..63c78ef 100644 --- a/rle/utils.py +++ b/rle/utils.py @@ -264,8 +264,8 @@ def generate_frames(ds: "Dataset", reshape: bool = True) -> Iterator[np.ndarray] """ import numpy as np - from pydicom.encaps import generate_pixel_data_frame - from pydicom.pixel_data_handlers.util import pixel_dtype + from pydicom.encaps import generate_frames + from pydicom.pixels.utils import pixel_dtype from pydicom.uid import RLELossless if ds.file_meta.TransferSyntaxUID != RLELossless: @@ -295,7 +295,7 @@ def generate_frames(ds: "Dataset", reshape: bool = True) -> Iterator[np.ndarray] bpp = ds.BitsAllocated dtype = pixel_dtype(ds) - for frame in generate_pixel_data_frame(ds.PixelData, nr_frames): + for frame in generate_frames(ds.PixelData, number_of_frames=nr_frames): arr = np.frombuffer(decode_frame(frame, r * c, bpp, "<"), dtype=dtype) if not reshape: @@ -328,7 +328,7 @@ def pixel_array(ds: "Dataset") -> "np.ndarray": components), (frames, rows, columns), or (frames, rows, columns, components) depending on the dataset. """ - from pydicom.pixel_data_handlers.util import ( + from pydicom.pixels.utils import ( get_expected_length, reshape_pixel_array, pixel_dtype, diff --git a/src/lib.rs b/src/lib.rs index 5236970..ecd05d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,14 +10,14 @@ use pyo3::exceptions::{PyValueError}; // Python rle module members #[pymodule] -fn rle(_: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(parse_header, m)?).unwrap(); - m.add_function(wrap_pyfunction!(decode_segment, m)?).unwrap(); - m.add_function(wrap_pyfunction!(decode_frame, m)?).unwrap(); +fn rle(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(parse_header, m)?); + m.add_function(wrap_pyfunction!(decode_segment, m)?); + m.add_function(wrap_pyfunction!(decode_frame, m)?); - m.add_function(wrap_pyfunction!(encode_row, m)?).unwrap(); - m.add_function(wrap_pyfunction!(encode_segment, m)?).unwrap(); - m.add_function(wrap_pyfunction!(encode_frame, m)?).unwrap(); + m.add_function(wrap_pyfunction!(encode_row, m)?); + m.add_function(wrap_pyfunction!(encode_segment, m)?); + m.add_function(wrap_pyfunction!(encode_frame, m)?); Ok(()) } @@ -81,9 +81,9 @@ fn _parse_header(src: &[u8; 64]) -> [u32; 15] { #[pyfunction] -fn decode_frame<'a>( - src: &[u8], nr_pixels: u32, bpp: u8, byteorder: char, py: Python<'a> -) -> PyResult<&'a PyByteArray> { +fn decode_frame<'py>( + src: &[u8], nr_pixels: u32, bpp: u8, byteorder: char, py: Python<'py> +) -> PyResult> { /* Return the decoded frame. Parameters @@ -386,7 +386,7 @@ fn _decode_segment_into_frame( #[pyfunction] -fn decode_segment<'a>(src: &[u8], py: Python<'a>) -> PyResult<&'a PyBytes> { +fn decode_segment<'py>(src: &[u8], py: Python<'py>) -> PyResult> { /* Return a decoded RLE segment as bytes. Parameters @@ -461,9 +461,9 @@ fn _decode_segment(src: &[u8], dst: &mut Vec) -> Result<(), Box> // ------------ #[pyfunction] -fn encode_frame<'a>( - src: &[u8], rows: u16, cols: u16, spp: u8, bpp: u8, byteorder: char, py: Python<'a> -) -> PyResult<&'a PyBytes> { +fn encode_frame<'py>( + src: &[u8], rows: u16, cols: u16, spp: u8, bpp: u8, byteorder: char, py: Python<'py> +) -> PyResult> { /* Return RLE encoded `src` as bytes. Parameters @@ -620,35 +620,10 @@ fn _encode_frame( } -fn _encode_segment_from_vector( - src: Vec, dst: &mut Vec, cols: u16 -) -> Result<(), Box> { - /* RLE encode a segment. - - Parameters - ---------- - src - The data to be encoded. - dst - The destination for the encoded data. - cols - The length of each row in the `src`. - */ - let row_len: usize = usize::try_from(cols).unwrap(); - for row_idx in 0..(src.len() / row_len) { - let offset = row_idx * row_len; - _encode_row(&src[offset..offset + row_len], dst)?; - } - - // Each segment must be even length or padded to even length with zero - if dst.len() % 2 != 0 { dst.push(0); } - - Ok(()) -} - - #[pyfunction] -fn encode_segment<'a>(src: &[u8], cols: u16, py: Python<'a>) -> PyResult<&'a PyBytes> { +fn encode_segment<'py>( + src: &[u8], cols: u16, py: Python<'py> +) -> PyResult> { /* Return an RLE encoded segment as bytes. Parameters @@ -710,8 +685,35 @@ fn _encode_segment_from_array( } +fn _encode_segment_from_vector( + src: Vec, dst: &mut Vec, cols: u16 +) -> Result<(), Box> { + /* RLE encode a segment. + + Parameters + ---------- + src + The data to be encoded. + dst + The destination for the encoded data. + cols + The length of each row in the `src`. + */ + let row_len: usize = usize::try_from(cols).unwrap(); + for row_idx in 0..(src.len() / row_len) { + let offset = row_idx * row_len; + _encode_row(&src[offset..offset + row_len], dst)?; + } + + // Each segment must be even length or padded to even length with zero + if dst.len() % 2 != 0 { dst.push(0); } + + Ok(()) +} + + #[pyfunction] -fn encode_row<'a>(src: &[u8], py: Python<'a>) -> PyResult<&'a PyBytes> { +fn encode_row<'py>(src: &[u8], py: Python<'py>) -> PyResult> { /* Return `src` as RLE encoded bytes. Parameters @@ -732,7 +734,6 @@ fn encode_row<'a>(src: &[u8], py: Python<'a>) -> PyResult<&'a PyBytes> { } -#[allow(overflowing_literals)] fn _encode_row(src: &[u8], dst: &mut Vec) -> Result<(), Box> { /* RLE encode `src` into `dst` @@ -763,94 +764,51 @@ fn _encode_row(src: &[u8], dst: &mut Vec) -> Result<(), Box> { _ => {} } - // Maximum length of a literal/replicate run - let max_run_length: u8 = 128; - let src_length = src.len(); - - // `replicate` and `literal` are the length of the current run - let mut literal: u8 = 0; - let mut replicate: u8 = 0; - - let mut previous: u8 = src[0]; - let mut current: u8 = src[1]; - let mut ii: usize = 1; - - // Account for the first item - if current == previous { replicate = 1; } - else { literal = 1; } - - loop { - current = src[ii]; - - // Run type switching/control - if current == previous { - if literal == 1 { - // Switch over to a replicate run - literal = 0; - replicate = 1; - } else if literal > 1 { - // Write out literal run and reset - // `literal` must be at least 1 or we undeflow - dst.push(literal - 1u8); - dst.extend(&src[ii - usize::from(literal)..ii]); - literal = 0; - } - replicate += 1; + let mut literal: Vec = Vec::new(); + // Chunk the source into groups of identical values + for group in src.chunk_by(|a, b| a == b) { + if group.len() == 1 { + // Only a single value in the group -> add it to the saved literal values + literal.extend(group); } else { - if replicate == 1 { - // Switch over to a literal run - literal = 1; - replicate = 0; - } else if replicate > 1 { - // Write out replicate run and reset - // `replicate` must be at least 2 to avoid overflow - dst.push(257u8.wrapping_sub(replicate)); - // Note use of `previous` item - dst.push(previous); - replicate = 0; - } - literal += 1; - } + // Multiple values in the group so one or more replicate runs are required + + // If `literal` is not empty then add N literal runs to the output first + if !literal.is_empty() { + for chunk in literal.chunks(128) { + // 1 >= chunk.len() >= 128: usize -> u8 + dst.push((chunk.len() - 1).try_into().unwrap()); + dst.extend(chunk); + } - // If the run length is maxed, write out and reset - if replicate == max_run_length { // Should be more frequent - // Write out replicate run and reset - dst.push(129); - dst.push(previous); - replicate = 0; - } else if literal == max_run_length { - // Write out literal run and reset - dst.push(127); - // The indexing is different here because ii is the `current` - // item, not previous - dst.extend(&src[ii + 1 - usize::from(literal)..ii + 1]); - literal = 0; - } // 128 is noop! - - previous = current; - ii += 1; - - if ii == src_length { break; } - } + // Reset the saved literal run values + literal.clear(); + } - // Handle cases where the last byte is continuation of a replicate run - // such as when 129 0x00 bytes -> replicate(128) + literal(1) - if replicate == 1 { - replicate = 0; - literal = 1; + // Replicate run(s) + for chunk in group.chunks(128) { + if chunk.len() > 1 { + // Replicate runs if the chunks have more than 1 value + // 1 >= chunk.len() >= 128: usize -> u8 + dst.push((257 - chunk.len()).try_into().unwrap()); + dst.push(chunk[0]); + } else { + // If the final chunk is only 1 value long do a literal run instead + dst.push(0); + dst.push(chunk[0]); + } + } + } } - if replicate > 1 { - // Write out and return - // `replicate` must be at least 2 or we overflow - dst.push(257u8.wrapping_sub(replicate)); - dst.push(current); - } else if literal > 0 { - // Write out and return - // `literal` must be at least 1 or we undeflow - dst.push(literal - 1u8); - dst.extend(&src[src_length - usize::from(literal)..]); + // Final literal run(s) if literal isn't followed by a replicate run + if !literal.is_empty() { + for chunk in literal.chunks(128) { + // 1 >= chunk.len() >= 128: usize -> u8 + dst.push((chunk.len() - 1).try_into().unwrap()); + dst.extend(chunk); + } } Ok(())