diff --git a/.github/workflows/pytest-builds.yml b/.github/workflows/pytest-builds.yml index 83eaa6d..540826e 100644 --- a/.github/workflows/pytest-builds.yml +++ b/.github/workflows/pytest-builds.yml @@ -1,10 +1,9 @@ -name: Run pytest +name: unit-tests on: push: branches: [ master ] pull_request: - branches: [ master ] jobs: pytest: diff --git a/.github/workflows/release-wheels.yml b/.github/workflows/release-wheels.yml index 25a324f..35a8001 100644 --- a/.github/workflows/release-wheels.yml +++ b/.github/workflows/release-wheels.yml @@ -1,68 +1,234 @@ -name: Build wheels and deploy to PyPI +name: release-deploy on: release: types: [ published ] jobs: - build_wheels: - name: Build wheels for ${{ matrix.os }} + build-sdist: + name: Build source distribution + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v2 + with: + submodules: true + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: '3.10' + + - name: Build sdist + run: | + python -m pip install -U pip + python -m pip install -U setuptools-rust + python -m pip install . + python setup.py sdist + + - name: Store artifacts + uses: actions/upload-artifact@v2 + with: + name: wheels + path: ./dist + + build-wheels: + name: Build wheel for cp${{ matrix.python }}-${{ matrix.platform_id }}-${{ matrix.manylinux_image }} runs-on: ${{ matrix.os }} - env: - CIBW_SKIP: "cp36-* *-musl*" strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.10"] + include: + # Windows 32 bit + - os: windows-latest + python: 37 + platform_id: win32 + - os: windows-latest + python: 38 + platform_id: win32 + - os: windows-latest + python: 39 + platform_id: win32 + - os: windows-latest + python: 310 + platform_id: win32 + + # Windows 64 bit + - os: windows-latest + python: 37 + platform_id: win_amd64 + - 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 + + # Linux 64 bit manylinux2010 + - os: ubuntu-latest + python: 37 + platform_id: manylinux_x86_64 + manylinux_image: manylinux2010 + - os: ubuntu-latest + python: 38 + platform_id: manylinux_x86_64 + manylinux_image: manylinux2010 + - os: ubuntu-latest + python: 39 + platform_id: manylinux_x86_64 + manylinux_image: manylinux2010 + - os: ubuntu-latest + python: 310 + platform_id: manylinux_x86_64 + manylinux_image: manylinux2010 + + # Linux 64 bit manylinux2014 + - os: ubuntu-latest + python: 37 + platform_id: manylinux_x86_64 + manylinux_image: 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 + manylinux_image: manylinux2014 + + # MacOS x86_64 + - os: macos-latest + python: 37 + platform_id: macosx_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 + + # 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 steps: - uses: actions/checkout@v2 with: submodules: true - # Install rust - uses: actions-rs/toolchain@v1 with: toolchain: stable - # Install Python - uses: actions/setup-python@v2 name: Install Python with: - python-version: ${{ matrix.python-version }} - - - name: Install requirements - run: | - pip install -U pip - pip install cibuildwheel==2.3.1 - pip install setuptools-rust + python-version: '3.9' - - name: Build sdist + - name: Install cibuildwheel run: | - python setup.py sdist + python -m pip install -U pip + python -m pip install -U setuptools-rust + python -m pip install cibuildwheel==2.3.1 - name: Build wheels + env: + CIBW_BUILD: cp${{ matrix.python }}-${{ matrix.platform_id }} + CIBW_ARCHS: all + CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux_image }} + CIBW_MANYLINUX_I686_IMAGE: ${{ matrix.manylinux_image }} + CIBW_BUILD_VERBOSITY: 1 + CIBW_BEFORE_ALL: | + curl -sSf https://sh.rustup.rs | sh -s -- --default-toolchain stable -y && rustup target add i686-pc-windows-msvc + CIBW_ENVIRONMENT: 'PATH="$PATH:$HOME/.cargo/bin"' run: | python --version python -m cibuildwheel --output-dir dist - env: - CIBW_BEFORE_ALL: | - curl -sSf https://sh.rustup.rs | sh -s -- --default-toolchain stable -y && rustup target add i686-pc-windows-msvc - CIBW_ENVIRONMENT: 'PATH="$PATH:$HOME/.cargo/bin"' - - uses: actions/upload-artifact@v2 + - name: Store artifacts + uses: actions/upload-artifact@v2 with: name: wheels path: ./dist + test-package: + name: Test built package + needs: [ build-wheels, build-sdist ] + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10'] + + steps: + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Download the wheels + uses: actions/download-artifact@v2 + with: + name: wheels + path: dist/ + + - name: Install from package wheels and test + run: | + python -m venv testwhl + source testwhl/bin/activate + python -m pip install -U pip + python -m pip install -U pytest pydicom pylibjpeg + python -m pip uninstall -y pylibjpeg-rle + python -m pip install git+https://github.com/pydicom/pylibjpeg-data + python -m pip install -U --find-links dist/ pylibjpeg-rle + python -c "import pytest; pytest.main(['--pyargs', 'rle.tests'])" + deactivate + + - name: Install from package tarball and test + run: | + python -m venv testsrc + source testsrc/bin/activate + python -m pip install -U pip + python -m pip install -U pytest pydicom pylibjpeg + python -m pip uninstall -y pylibjpeg-rle + python -m pip install git+https://github.com/pydicom/pylibjpeg-data + export PATH="$PATH:$HOME/.cargo/bin" + python -m pip install -U dist/pylibjpeg-rle-*.tar.gz + python -c "import pytest; pytest.main(['--pyargs', 'rle.tests'])" + deactivate + # The pypi upload fails with non-linux containers, so grab the uploaded # artifacts and run using those # See: https://github.com/pypa/gh-action-pypi-publish/discussions/15 deploy: name: Upload wheels to PyPI - needs: - - build_wheels + needs: [ test-package ] runs-on: ubuntu-latest steps: @@ -72,13 +238,6 @@ jobs: name: wheels path: dist/ - #- name: Publish package to Test PyPi - # uses: pypa/gh-action-pypi-publish@master - # with: - # user: __token__ - # password: ${{ secrets.TEST_PYPI_PASSWORD }} - # repository_url: https://test.pypi.org/legacy/ - - name: Publish package to PyPi uses: pypa/gh-action-pypi-publish@master with: diff --git a/README.md b/README.md index 4ae1fd6..d2d2562 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![Build Status](https://github.com/pydicom/pylibjpeg-rle/workflows/unit-tests/badge.svg)](https://github.com/pydicom/pylibjpeg-rle/actions?query=workflow%3Aunit-tests) +[![codecov](https://codecov.io/gh/pydicom/pylibjpeg-rle/branch/master/graph/badge.svg)](https://codecov.io/gh/pydicom/pylibjpeg-rle) +[![PyPI version](https://badge.fury.io/py/pylibjpeg-rle.svg)](https://badge.fury.io/py/pylibjpeg-rle) +[![Python versions](https://img.shields.io/pypi/pyversions/pylibjpeg-rle.svg)](https://img.shields.io/pypi/pyversions/pylibjpeg-rle.svg) ## pylibjpeg-rle @@ -18,7 +22,7 @@ Make sure [Python](https://www.python.org/), [Git](https://git-scm.com/) and ```bash git clone https://github.com/pydicom/pylibjpeg-rle cd pylibjpeg-rle -python -m setup.py develop +python -m pip install . ``` ### Supported Transfer Syntaxes @@ -31,14 +35,11 @@ python -m setup.py develop #### Decoding ##### With pylibjpeg -Because pydicom defaults to its own RLE decoder you must specify the use -of pylibjpeg when decompressing: ```python from pydicom import dcmread from pydicom.data import get_testdata_file ds = dcmread(get_testdata_file("OBXXXX1A_rle.dcm")) -ds.decompress("pylibjpeg") arr = ds.pixel_array ``` diff --git a/docs/release_notes/v1.3.0.rst b/docs/release_notes/v1.3.0.rst new file mode 100644 index 0000000..fc3ce37 --- /dev/null +++ b/docs/release_notes/v1.3.0.rst @@ -0,0 +1,9 @@ +.. _v1.3.0: + +1.3.0 +===== + +Enhancements +............ + +* Update decoding interface to allow passing kwargs instead of a dataset diff --git a/pyproject.toml b/pyproject.toml index 451041e..64504f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] requires = [ + "setuptools>=18.0", "setuptools-rust", - "wheel" + "wheel", ] diff --git a/rle/_version.py b/rle/_version.py index 7d11fa0..9e4d210 100644 --- a/rle/_version.py +++ b/rle/_version.py @@ -3,7 +3,7 @@ import re -__version__ = '1.2.0' +__version__ = '1.3.0' VERSION_PATTERN = r""" diff --git a/rle/tests/test_handler.py b/rle/tests/test_handler.py index 826e1ff..b521840 100644 --- a/rle/tests/test_handler.py +++ b/rle/tests/test_handler.py @@ -19,6 +19,22 @@ @pytest.mark.skipif(not HAVE_PYDICOM, reason="No pydicom") class TestDecodePixelData: """Tests for the plugin's decoder interface.""" + def test_no_dataset_kwargs_raises(self): + """Test plugin decoder with no dataset or kwargs raises""" + ds = INDEX["OBXXXX1A_rle.dcm"]['ds'] + assert ds.file_meta.TransferSyntaxUID == RLELossless + assert 8 == ds.BitsAllocated + assert 1 == ds.SamplesPerPixel + assert 0 == ds.PixelRepresentation + assert 600 == ds.Rows + assert 800 == ds.Columns + assert 1 == getattr(ds, 'NumberOfFrames', 1) + + frame = next(generate_pixel_data_frame(ds.PixelData)) + msg = r"Either `ds` or `\*\*kwargs` must be used" + with pytest.raises(ValueError, match=msg): + decode_pixel_data(frame) + def test_u8_1s_1f(self): """Test plugin decoder for 8 bit, 1 sample, 1 frame data.""" ds = INDEX["OBXXXX1A_rle.dcm"]['ds'] diff --git a/rle/utils.py b/rle/utils.py index bed019a..371d490 100644 --- a/rle/utils.py +++ b/rle/utils.py @@ -8,7 +8,9 @@ from rle._rle import decode_frame, decode_segment, encode_frame, encode_segment -def decode_pixel_data(src: bytes, ds: "Dataset", **kwargs) -> "np.ndarray": +def decode_pixel_data( + src: bytes, ds: Optional["Dataset"] = None, **kwargs +) -> "np.ndarray": """Return the decoded RLE Lossless data as a :class:`numpy.ndarray`. Intended for use with *pydicom* ``Dataset`` objects. @@ -17,10 +19,19 @@ def decode_pixel_data(src: bytes, ds: "Dataset", **kwargs) -> "np.ndarray": ---------- src : bytes A single encoded image frame to be decoded. - ds : pydicom.dataset.Dataset + ds : pydicom.dataset.Dataset, optional A :class:`~pydicom.dataset.Dataset` containing the group ``0x0028`` - elements corresponding to the image frame. + elements corresponding to the image frame. If not used then `kwargs` + must be supplied. **kwargs + Required keys if `ds` is not supplied: + + * ``"rows"``: :class:`int` - the number of rows in the decoded image + * ``"columns"``: :class:`int` - the number of columns in the decoded + image + * ``"bits_allocated"``: :class:`int` - the number of bits allocated + to each pixel + Current decoding options are: * ``{'byteorder': str}`` specify the byte ordering for the decoded data @@ -41,8 +52,19 @@ def decode_pixel_data(src: bytes, ds: "Dataset", **kwargs) -> "np.ndarray": """ byteorder = kwargs.get('byteorder', '<') + columns = kwargs.get("columns") + rows = kwargs.get("rows") + bits_allocated = kwargs.get("bits_allocated") + no_kwargs = None in (columns, rows, bits_allocated) + if ds is None and no_kwargs: + raise ValueError("Either `ds` or `**kwargs` must be used") + + columns = ds.get("Columns", columns) + rows = ds.get("Rows", rows) + bits_allocated = ds.get("BitsAllocated", bits_allocated) + return np.frombuffer( - decode_frame(src, ds.Rows * ds.Columns, ds.BitsAllocated, byteorder), + decode_frame(src, rows * columns, bits_allocated, byteorder), dtype='uint8' ) diff --git a/setup.py b/setup.py index 76458da..4128a01 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,10 @@ from setuptools_rust import Binding, RustExtension -VERSION_FILE = Path(__file__).parent / "rle" / '_version.py' -with open(VERSION_FILE) as f: +PACKAGE_DIR = Path(__file__).parent / "rle" + + +with open(PACKAGE_DIR / '_version.py') as f: exec(f.read()) with open('README.md', 'r') as f: @@ -24,10 +26,7 @@ author_email = "scaramallion@users.noreply.github.com", url = "https://github.com/pydicom/pylibjpeg-rle", license = "MIT", - keywords = ( - "dicom pydicom python medicalimaging radiotherapy oncology imaging " - "radiology nuclearmedicine rle pylibjpeg rust" - ), + keywords = "dicom pydicom python rle pylibjpeg rust", classifiers = [ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Intended Audience :: Developers", @@ -51,8 +50,7 @@ include_package_data = True, zip_safe = False, python_requires = ">=3.7", - setup_requires = ['setuptools>=18.0', 'setuptools-rust'], - install_requires = ["numpy"], + install_requires = ["numpy>=1.20"], extras_require = { 'tests': ["pytest", "pydicom", "numpy"], 'benchmarks': ["pydicom", "numpy", "asv"],