From 2111bf2cca4aa0dc6ef92526fa8250a4dec3c489 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 25 Jul 2025 11:41:11 +1200 Subject: [PATCH 01/19] Renamce OC_EXPORTER_RENDERER to CMLIBS_EXPORTER_RENDERER. --- README.rst | 2 +- src/cmlibs/exporter/baseimage.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index cddf4c4..5407884 100644 --- a/README.rst +++ b/README.rst @@ -18,7 +18,7 @@ To install the thumbnail exporter with support for software rendering install *c To force the use of the software renderer even when hardware rendering is available, set an environment variable like so:: - OC_EXPORTER_RENDERER=osmesa + CMLIBS_EXPORTER_RENDERER=osmesa either in the environment the exporter is run in or before calling the export thumbnail method. diff --git a/src/cmlibs/exporter/baseimage.py b/src/cmlibs/exporter/baseimage.py index cdae871..60ede36 100644 --- a/src/cmlibs/exporter/baseimage.py +++ b/src/cmlibs/exporter/baseimage.py @@ -16,7 +16,7 @@ class BaseImageExporter(BaseExporter): By default the export will be use PySide6 to render the scene. An alternative is to use OSMesa for software rendering. To use OSMesa as the renderer either set the environment variable - OC_EXPORTER_RENDERER to 'osmesa' or not have PySide6 available in the + CMLIBS_EXPORTER_RENDERER to 'osmesa' or not have PySide6 available in the calling environment. """ @@ -61,7 +61,7 @@ def export_image(self): Export graphics into an image format. """ pyside6_opengl_failed = True - if "OC_EXPORTER_RENDERER" not in os.environ or os.environ["OC_EXPORTER_RENDERER"] != "osmesa": + if "CMLIBS_EXPORTER_RENDERER" not in os.environ or os.environ["CMLIBS_EXPORTER_RENDERER"] != "osmesa": try: from PySide6 import QtGui From b0ce046868573b9f0a0a5a4d28370bfe04b31b00 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 28 Aug 2025 14:33:13 +1200 Subject: [PATCH 02/19] Add a run tests GitHub actions workflow. --- .github/workflows/run_tests.yaml | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/run_tests.yaml diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml new file mode 100644 index 0000000..098aa89 --- /dev/null +++ b/.github/workflows/run_tests.yaml @@ -0,0 +1,38 @@ +name: Run Unit Tests + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ${{ matrix.os }} + continue-on-error: true + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.9', '3.11', '3.12'] + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + # configure python + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install required system libraries + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt install mesa-utils -y + # install deps + - name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }} + run: | + python -m pip install --upgrade pip + pip install -e . + + # find and run all unit tests + - name: Run unit tests + run: python -m unittest discover -s tests From f1b85aff7104b0a270cc18f435833168d4717bf3 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 28 Aug 2025 14:38:09 +1200 Subject: [PATCH 03/19] Install hardware and software requirements for image export. --- .github/workflows/run_tests.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 098aa89..303312c 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -9,6 +9,8 @@ on: jobs: build: runs-on: ${{ matrix.os }} + #if: github.repository == 'cmlibs-python/cmlibs.exporter' + if: github.repository == 'hsorby/cmlibs.exporter' continue-on-error: true strategy: matrix: @@ -31,7 +33,11 @@ jobs: - name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }} run: | python -m pip install --upgrade pip - pip install -e . + if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then + pip install -e ".[opengl_software]" + else + pip install -e ".[opengl_hardware]" + fi # find and run all unit tests - name: Run unit tests From fde5943ba7eb41abdda14c22de441f6e98827f67 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 28 Aug 2025 14:52:03 +1200 Subject: [PATCH 04/19] Try use moderngl package for offscreen rendering. --- .github/workflows/run_tests.yaml | 1 + pyproject.toml | 2 +- src/cmlibs/exporter/baseimage.py | 44 ++++++++++++++------------------ 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 303312c..53b2fce 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -31,6 +31,7 @@ jobs: sudo apt install mesa-utils -y # install deps - name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }} + shell: bash run: | python -m pip install --upgrade pip if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then diff --git a/pyproject.toml b/pyproject.toml index a6f12d8..f6a7c09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,6 @@ Repository = "https://github.com/CMLibs-Python/cmlibs.exporter" [project.optional-dependencies] opengl_hardware = ["PySide6"] -opengl_software = ["PyOpenGL"] +opengl_software = ["moderngl"] [tool.setuptools_scm] diff --git a/src/cmlibs/exporter/baseimage.py b/src/cmlibs/exporter/baseimage.py index 60ede36..f9e5cf0 100644 --- a/src/cmlibs/exporter/baseimage.py +++ b/src/cmlibs/exporter/baseimage.py @@ -61,7 +61,7 @@ def export_image(self): Export graphics into an image format. """ pyside6_opengl_failed = True - if "CMLIBS_EXPORTER_RENDERER" not in os.environ or os.environ["CMLIBS_EXPORTER_RENDERER"] != "osmesa": + if os.environ.get("CMLIBS_EXPORTER_RENDERER", "") != "offscreen": try: from PySide6 import QtGui @@ -79,33 +79,27 @@ def export_image(self): except ImportError: pyside6_opengl_failed = True - mesa_context = None + os_context = None + fbo = None mesa_opengl_failed = True if pyside6_opengl_failed: try: - from OpenGL import GL - from OpenGL import arrays - from OpenGL.osmesa import ( - OSMesaCreateContextAttribs, OSMesaMakeCurrent, OSMESA_FORMAT, - OSMESA_RGBA, OSMESA_PROFILE, OSMESA_COMPAT_PROFILE, - OSMESA_CONTEXT_MAJOR_VERSION, OSMESA_CONTEXT_MINOR_VERSION, - OSMESA_DEPTH_BITS - ) - - attrs = arrays.GLintArray.asArray([ - OSMESA_FORMAT, OSMESA_RGBA, - OSMESA_DEPTH_BITS, 24, - OSMESA_PROFILE, OSMESA_COMPAT_PROFILE, - OSMESA_CONTEXT_MAJOR_VERSION, 2, - OSMESA_CONTEXT_MINOR_VERSION, 1, - 0 - ]) - mesa_context = OSMesaCreateContextAttribs(attrs, None) - mesa_buffer = arrays.GLubyteArray.zeros((self._width, self._height, 4)) - result = OSMesaMakeCurrent(mesa_context, mesa_buffer, GL.GL_UNSIGNED_BYTE, self._width, self._height) + os.environ["GALLIUM_DRIVER"] = "llvmpipe" + # os.environ["MES"] + import moderngl + + os_context = moderngl.create_standalone_context(require="210") + print("Using moderngl context:", os_context["GL_VENDOR"]) + print("OpenGL version:", os_context.version_code) + print("OpenGL Renderer:", os_context["GL_RENDERER"]) + fbo = os_context.framebuffer(color_attachment=[os_context.texture((self._width, self._height), 4)]) + fbo.use() + result = True if result: mesa_opengl_failed = False except ImportError: + os_context = None + fbo = None mesa_opengl_failed = True if pyside6_opengl_failed and mesa_opengl_failed: @@ -148,6 +142,6 @@ def export_image(self): sceneviewer.writeImageToFile(os.path.join(self._output_target, f'{self._prefix}_{name}_{self._name_postfix}.jpeg'), False, self._width, self._height, 4, 0) - if mesa_context is not None: - from OpenGL.osmesa import OSMesaDestroyContext - OSMesaDestroyContext(mesa_context) + if os_context is not None: + fbo.release() + os_context.release() From 76b13abd1cc791e72fe6cf3c86b0cac9ef9742e4 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 29 Aug 2025 17:03:15 +1200 Subject: [PATCH 05/19] Update run_tests.yaml --- .github/workflows/run_tests.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 53b2fce..fa62ac5 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -39,6 +39,8 @@ jobs: else pip install -e ".[opengl_hardware]" fi + pip uninstall cmlibs.zinc + pip install https://github.com/cmlibs-dependencies/prebuilt-library-cache/releases/download/cache/cmlibs_zinc-4.2.1-cp312-cp312-linux_x86_64.whl # find and run all unit tests - name: Run unit tests From 12b3ecdda8f804ad384f9b594bf36459c96a9249 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 29 Aug 2025 17:17:58 +1200 Subject: [PATCH 06/19] Update run_tests.yaml --- .github/workflows/run_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index fa62ac5..0847322 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -39,7 +39,7 @@ jobs: else pip install -e ".[opengl_hardware]" fi - pip uninstall cmlibs.zinc + pip uninstall -y cmlibs.zinc pip install https://github.com/cmlibs-dependencies/prebuilt-library-cache/releases/download/cache/cmlibs_zinc-4.2.1-cp312-cp312-linux_x86_64.whl # find and run all unit tests From 416c155c22c77314a537bae936360fadb55c4245 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 29 Aug 2025 17:43:42 +1200 Subject: [PATCH 07/19] Update run_tests.yaml --- .github/workflows/run_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 0847322..16e6d45 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -28,7 +28,7 @@ jobs: - name: Install required system libraries if: matrix.os == 'ubuntu-latest' run: | - sudo apt install mesa-utils -y + sudo apt install libosmesa6 libgl1-mesa libglu1-mesa -y # install deps - name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }} shell: bash From 0b3491fae800a25758147717dbaeca3c15727341 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 29 Aug 2025 17:47:28 +1200 Subject: [PATCH 08/19] Update run_tests.yaml --- .github/workflows/run_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 16e6d45..3c30f92 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -28,7 +28,7 @@ jobs: - name: Install required system libraries if: matrix.os == 'ubuntu-latest' run: | - sudo apt install libosmesa6 libgl1-mesa libglu1-mesa -y + sudo apt install libosmesa6 libglu1-mesa -y # install deps - name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }} shell: bash From 810608596ec647b73a4267e4d958622b043ae4bc Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 29 Aug 2025 18:01:27 +1200 Subject: [PATCH 09/19] Revert "Try use moderngl package for offscreen rendering." This reverts commit fde5943ba7eb41abdda14c22de441f6e98827f67. --- .github/workflows/run_tests.yaml | 1 - pyproject.toml | 2 +- src/cmlibs/exporter/baseimage.py | 44 ++++++++++++++++++-------------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 3c30f92..0b66d2a 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -31,7 +31,6 @@ jobs: sudo apt install libosmesa6 libglu1-mesa -y # install deps - name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }} - shell: bash run: | python -m pip install --upgrade pip if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then diff --git a/pyproject.toml b/pyproject.toml index f6a7c09..a6f12d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,6 @@ Repository = "https://github.com/CMLibs-Python/cmlibs.exporter" [project.optional-dependencies] opengl_hardware = ["PySide6"] -opengl_software = ["moderngl"] +opengl_software = ["PyOpenGL"] [tool.setuptools_scm] diff --git a/src/cmlibs/exporter/baseimage.py b/src/cmlibs/exporter/baseimage.py index f9e5cf0..60ede36 100644 --- a/src/cmlibs/exporter/baseimage.py +++ b/src/cmlibs/exporter/baseimage.py @@ -61,7 +61,7 @@ def export_image(self): Export graphics into an image format. """ pyside6_opengl_failed = True - if os.environ.get("CMLIBS_EXPORTER_RENDERER", "") != "offscreen": + if "CMLIBS_EXPORTER_RENDERER" not in os.environ or os.environ["CMLIBS_EXPORTER_RENDERER"] != "osmesa": try: from PySide6 import QtGui @@ -79,27 +79,33 @@ def export_image(self): except ImportError: pyside6_opengl_failed = True - os_context = None - fbo = None + mesa_context = None mesa_opengl_failed = True if pyside6_opengl_failed: try: - os.environ["GALLIUM_DRIVER"] = "llvmpipe" - # os.environ["MES"] - import moderngl - - os_context = moderngl.create_standalone_context(require="210") - print("Using moderngl context:", os_context["GL_VENDOR"]) - print("OpenGL version:", os_context.version_code) - print("OpenGL Renderer:", os_context["GL_RENDERER"]) - fbo = os_context.framebuffer(color_attachment=[os_context.texture((self._width, self._height), 4)]) - fbo.use() - result = True + from OpenGL import GL + from OpenGL import arrays + from OpenGL.osmesa import ( + OSMesaCreateContextAttribs, OSMesaMakeCurrent, OSMESA_FORMAT, + OSMESA_RGBA, OSMESA_PROFILE, OSMESA_COMPAT_PROFILE, + OSMESA_CONTEXT_MAJOR_VERSION, OSMESA_CONTEXT_MINOR_VERSION, + OSMESA_DEPTH_BITS + ) + + attrs = arrays.GLintArray.asArray([ + OSMESA_FORMAT, OSMESA_RGBA, + OSMESA_DEPTH_BITS, 24, + OSMESA_PROFILE, OSMESA_COMPAT_PROFILE, + OSMESA_CONTEXT_MAJOR_VERSION, 2, + OSMESA_CONTEXT_MINOR_VERSION, 1, + 0 + ]) + mesa_context = OSMesaCreateContextAttribs(attrs, None) + mesa_buffer = arrays.GLubyteArray.zeros((self._width, self._height, 4)) + result = OSMesaMakeCurrent(mesa_context, mesa_buffer, GL.GL_UNSIGNED_BYTE, self._width, self._height) if result: mesa_opengl_failed = False except ImportError: - os_context = None - fbo = None mesa_opengl_failed = True if pyside6_opengl_failed and mesa_opengl_failed: @@ -142,6 +148,6 @@ def export_image(self): sceneviewer.writeImageToFile(os.path.join(self._output_target, f'{self._prefix}_{name}_{self._name_postfix}.jpeg'), False, self._width, self._height, 4, 0) - if os_context is not None: - fbo.release() - os_context.release() + if mesa_context is not None: + from OpenGL.osmesa import OSMesaDestroyContext + OSMesaDestroyContext(mesa_context) From 82eebe40f6c43ef9809bba74877431c99a339e95 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 29 Aug 2025 18:05:05 +1200 Subject: [PATCH 10/19] Use offscreen for CMLIBS_EXPORTER_RENDERER to br more generic. --- .github/workflows/run_tests.yaml | 1 + src/cmlibs/exporter/baseimage.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 0b66d2a..3c30f92 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -31,6 +31,7 @@ jobs: sudo apt install libosmesa6 libglu1-mesa -y # install deps - name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }} + shell: bash run: | python -m pip install --upgrade pip if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then diff --git a/src/cmlibs/exporter/baseimage.py b/src/cmlibs/exporter/baseimage.py index 60ede36..d509cc6 100644 --- a/src/cmlibs/exporter/baseimage.py +++ b/src/cmlibs/exporter/baseimage.py @@ -61,7 +61,7 @@ def export_image(self): Export graphics into an image format. """ pyside6_opengl_failed = True - if "CMLIBS_EXPORTER_RENDERER" not in os.environ or os.environ["CMLIBS_EXPORTER_RENDERER"] != "osmesa": + if os.environ.get("CMLIBS_EXPORTER_RENDERER", "") != "offscreen": try: from PySide6 import QtGui From 02900abad976054cdce22e24d4d58673836a7840 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 29 Aug 2025 18:39:05 +1200 Subject: [PATCH 11/19] Specifically set pyopengl platform to osmesa. --- src/cmlibs/exporter/baseimage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmlibs/exporter/baseimage.py b/src/cmlibs/exporter/baseimage.py index d509cc6..0753015 100644 --- a/src/cmlibs/exporter/baseimage.py +++ b/src/cmlibs/exporter/baseimage.py @@ -83,6 +83,7 @@ def export_image(self): mesa_opengl_failed = True if pyside6_opengl_failed: try: + os.environ["PYOPENGL_PLATFORM"] = "osmesa" from OpenGL import GL from OpenGL import arrays from OpenGL.osmesa import ( From 64f0c18dfd13a1a75c8aaec49765c2ddaf75e1d6 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 29 Aug 2025 20:50:14 +1200 Subject: [PATCH 12/19] Update run_tests.yaml --- .github/workflows/run_tests.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 3c30f92..6516ba5 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -17,10 +17,8 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ['3.9', '3.11', '3.12'] steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v4 - # configure python - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -29,7 +27,7 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | sudo apt install libosmesa6 libglu1-mesa -y - # install deps + - name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }} shell: bash run: | @@ -39,9 +37,12 @@ jobs: else pip install -e ".[opengl_hardware]" fi + + - name: Install OSMESA cmlibs.zinc + if: matrix.os == 'ubuntu-latest' + run: | pip uninstall -y cmlibs.zinc pip install https://github.com/cmlibs-dependencies/prebuilt-library-cache/releases/download/cache/cmlibs_zinc-4.2.1-cp312-cp312-linux_x86_64.whl - # find and run all unit tests - name: Run unit tests run: python -m unittest discover -s tests From 35cb211e909671e30a87bc412a674cfba0ae3a91 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Mon, 1 Sep 2025 09:21:57 +1200 Subject: [PATCH 13/19] Install libgl1-mesa-de instead of mesa-utils in run_tests.yaml. --- .github/workflows/run_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 53b2fce..62dcef7 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -28,7 +28,7 @@ jobs: - name: Install required system libraries if: matrix.os == 'ubuntu-latest' run: | - sudo apt install mesa-utils -y + sudo apt install libgl1-mesa-dev -y # install deps - name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }} shell: bash From 7157fdfba61dede6b3c1b005092fde35d17bf927 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Mon, 1 Sep 2025 09:52:42 +1200 Subject: [PATCH 14/19] Install osmesa cmlibs zinc when running tests. --- .github/workflows/run_tests.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 6516ba5..eae8ab8 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -33,16 +33,13 @@ jobs: run: | python -m pip install --upgrade pip if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then + PY_VERSION_NODOT=$(echo "${{ matrix.python-version }}" | tr -d '.') + WHEEL_URL="https://github.com/cmlibs/swigzinc/releases/download/osmesa/cmlibs_zinc-4.2.1-cp${PY_VERSION_NODOT}-cp${PY_VERSION_NODOT}-linux_x86_64.whl" + pip install "${WHEEL_URL}" pip install -e ".[opengl_software]" else pip install -e ".[opengl_hardware]" fi - - - name: Install OSMESA cmlibs.zinc - if: matrix.os == 'ubuntu-latest' - run: | - pip uninstall -y cmlibs.zinc - pip install https://github.com/cmlibs-dependencies/prebuilt-library-cache/releases/download/cache/cmlibs_zinc-4.2.1-cp312-cp312-linux_x86_64.whl - name: Run unit tests run: python -m unittest discover -s tests From 0467827f30e724cbdd9c9d2ac0abfe885c83e511 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Mon, 1 Sep 2025 10:19:40 +1200 Subject: [PATCH 15/19] Use python version 3.10 instead of 3.9 in testing. --- .github/workflows/run_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index eae8ab8..23dc98a 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.9', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 From 7ebcb3c3009c43d61b660ab1677827612a74236b Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 11 Sep 2025 16:00:45 +1200 Subject: [PATCH 16/19] Update flatmapsvg exporter to make use of annotations in a JSON scaffold settings file. --- pyproject.toml | 1 + src/cmlibs/exporter/flatmapsvg.py | 34 +++++++++++++++- tests/test_flatmapsvg.py | 65 ++++++++++++++++--------------- 3 files changed, 66 insertions(+), 34 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a6f12d8..1d1807e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ 'cmlibs.argon >= 0.4.0', 'cmlibs.zinc', 'exf2mbfxml', + 'packaging', 'svgpathtools_light', ] description = "Export CMLibs Zinc data to common mesh formats." diff --git a/src/cmlibs/exporter/flatmapsvg.py b/src/cmlibs/exporter/flatmapsvg.py index 24cec1e..619c766 100644 --- a/src/cmlibs/exporter/flatmapsvg.py +++ b/src/cmlibs/exporter/flatmapsvg.py @@ -11,6 +11,7 @@ import random from decimal import Decimal +from packaging.version import Version from svgpathtools import svg2paths from xml.dom.minidom import parseString @@ -54,6 +55,7 @@ def __init__(self, output_target=None, output_prefix=None): super(ArgonSceneExporter, self).__init__("ArgonSceneExporterWavefrontSVG" if output_prefix is None else output_prefix) self._output_target = output_target self._annotations_csv_file = None + self._annotations_json_file = None def export(self, output_target=None): """ @@ -145,6 +147,9 @@ def export_flatmapsvg_from_scene(self, scene, scene_filter=None): def set_annotations_csv_file(self, filename): self._annotations_csv_file = filename + def set_annotations_json_file(self, filename): + self._annotations_json_file = filename + def _read_reversed_annotations_map(self): reversed_map = None if self._annotations_csv_file is not None: @@ -155,7 +160,17 @@ def _read_reversed_annotations_map(self): if is_annotation_csv_file: fh.seek(0) - reversed_map = _reverse_map_annotations(result) + reversed_map = _reverse_map_annotations_csv(result) + + if self._annotations_json_file is not None: + with open(self._annotations_json_file) as fh: + try: + result = json.load(fh) + except json.decoder.JSONDecodeError: + result = None + + if result is not None: + reversed_map = _reverse_map_annotations_json(result) return reversed_map @@ -851,7 +866,7 @@ def _define_background_regions(boundaries, view_box): return f'{brain_rect}{cervical_rect}{thoracic_rect}{lumbar_rect}', features -def _reverse_map_annotations(csv_reader): +def _reverse_map_annotations_csv(csv_reader): reverse_map = {} if csv_reader: first = True @@ -865,6 +880,21 @@ def _reverse_map_annotations(csv_reader): return reverse_map +def _reverse_map_annotations_json(json_data): + reverse_map = {} + if json_data: + if json_data.get('id', '') == 'scaffold creator settings' and _known_version(json_data.get('version', '0.0.0')): + metadata = json_data.get('metadata', {'annotations': []}) + for annotation in metadata.get('annotations', []): + reverse_map[annotation['name']] = annotation['id'] + + return reverse_map + + +def _known_version(version_in): + return not Version(version_in) < Version('0.1.0') + + def _label_has_annotations(entry, annotation_map): return entry in annotation_map and annotation_map[entry] and annotation_map[entry] != "None" diff --git a/tests/test_flatmapsvg.py b/tests/test_flatmapsvg.py index 889680a..6a7af27 100644 --- a/tests/test_flatmapsvg.py +++ b/tests/test_flatmapsvg.py @@ -15,6 +15,9 @@ def _resource_path(resource_name): return os.path.join(here, "resources", resource_name) +NULL = [0, 0] + + class Exporter(unittest.TestCase): def test_flatmap_svg(self): @@ -67,10 +70,21 @@ def test_write_svg_outline(self): os.remove(simple_svg_file) +def _define_test_points(): + p1 = [-38.76407990290047, 136.95711038948954] + p2 = [-38.66539406842078, 135.33885440116572] + p3 = [-38.66539406842079, 135.3388544011657] + p4 = [-38.57638526850839, 133.76842178271903] + p5 = [-38.57638526850839, 133.768421782719] + p6 = [-38.50103491179091, 132.19996302635846] + p7 = [-38.5010349117909, 132.1999630263585] + return p1, p2, p3, p4, p5, p6, p7 + + class FindConnectedSet(unittest.TestCase): def test_simple(self): - null = [0, 0] + p1 = [1, 1] p2 = [2, 2] p3 = [3, 3] @@ -79,18 +93,24 @@ def test_simple(self): p6 = [6, 6] p7 = [7, 7] - c1 = [[p1, null, null, p2], [p2, null, null, p3], [p3, null, null, p4], [p4, null, null, p5]] - c2 = [[p1, null, null, p2], [p2, null, null, p3], [p5, null, null, p6], [p6, null, null, p7]] - c3 = [[p2, null, null, p3], [p3, null, null, p4], [p1, null, null, p2], [p4, null, null, p5]] - - self.assertEqual(1, len(_connected_segments(c1))) - self.assertEqual(2, len(_connected_segments(c2))) + c3 = self._setup_data(p1, p2, p3, p4, p5, p6, p7) segmented_c3 = _connected_segments(c3) self.assertEqual(1, len(segmented_c3)) self.assertEqual(p1, segmented_c3[0][0][0]) + def _setup_data(self, p1, p2, p3, p4, p5, p6, p7, alt_c3=False): + c1 = [[p1, NULL, NULL, p2], [p2, NULL, NULL, p3], [p3, NULL, NULL, p4], [p4, NULL, NULL, p5]] + c2 = [[p1, NULL, NULL, p2], [p2, NULL, NULL, p3], [p5, NULL, NULL, p6], [p6, NULL, NULL, p7]] + if alt_c3: + c3 = [[p3, NULL, NULL, p4], [p2, NULL, NULL, p3], [p4, NULL, NULL, p5], [p1, NULL, NULL, p2]] + else: + c3 = [[p2, NULL, NULL, p3], [p3, NULL, NULL, p4], [p1, NULL, NULL, p2], [p4, NULL, NULL, p5]] + + self.assertEqual(1, len(_connected_segments(c1))) + self.assertEqual(2, len(_connected_segments(c2))) + return c3 + def test_real_data(self): - null = [0, 0] p1 = [-38.76407990290047, 136.95711038948954] p2 = [-38.66539406842079, 135.3388544011657] p3 = [-38.66539406842079, 135.3388544011657] @@ -99,40 +119,21 @@ def test_real_data(self): p6 = [-38.50103491179091, 132.19996302635846] p7 = [-38.5010349117909, 132.1999630263585] - c1 = [[p1, null, null, p2], [p2, null, null, p3], [p3, null, null, p4], [p4, null, null, p5]] - c2 = [[p1, null, null, p2], [p2, null, null, p3], [p5, null, null, p6], [p6, null, null, p7]] - c3 = [[p3, null, null, p4], [p2, null, null, p3], [p4, null, null, p5], [p1, null, null, p2]] - - self.assertEqual(1, len(_connected_segments(c1))) - self.assertEqual(2, len(_connected_segments(c2))) + c3 = self._setup_data(p1, p2, p3, p4, p5, p6, p7, alt_c3=True) segmented_c3 = _connected_segments(c3) self.assertEqual(2, len(segmented_c3)) self.assertEqual(p2, segmented_c3[0][0][0]) def test_real_data_single_section(self): - null = [0, 0] - p1 = [-38.76407990290047, 136.95711038948954] - p2 = [-38.66539406842078, 135.33885440116572] - p3 = [-38.66539406842079, 135.3388544011657] - p4 = [-38.57638526850839, 133.76842178271903] - p5 = [-38.57638526850839, 133.768421782719] - p6 = [-38.50103491179091, 132.19996302635846] - p7 = [-38.5010349117909, 132.1999630263585] + p1, p2, p3, p4, p5, p6, p7 = _define_test_points() - c1 = [[p1, null, null, p2], [p2, null, null, p3], [p3, null, null, p4], [p4, null, null, p5], [p6, null, null, p7]] + c1 = [[p1, NULL, NULL, p2], [p2, NULL, NULL, p3], [p3, NULL, NULL, p4], [p4, NULL, NULL, p5], [p6, NULL, NULL, p7]] self.assertEqual(2, len(_connected_segments(c1))) def test_real_data_fork(self): - null = [0, 0] - p1 = [-38.76407990290047, 136.95711038948954] - p2 = [-38.66539406842078, 135.33885440116572] - p3 = [-38.66539406842079, 135.3388544011657] - p4 = [-38.57638526850839, 133.76842178271903] - p5 = [-38.57638526850839, 133.768421782719] - p6 = [-38.50103491179091, 132.19996302635846] - p7 = [-38.5010349117909, 132.1999630263585] + p1, p2, p3, p4, p5, p6, p7 = _define_test_points() - c1 = [[p1, null, null, p2], [p2, null, null, p3], [p3, null, null, p4], [p4, null, null, p5], [p3, null, null, p6], [p6, null, null, p7]] + c1 = [[p1, NULL, NULL, p2], [p2, NULL, NULL, p3], [p3, NULL, NULL, p4], [p4, NULL, NULL, p5], [p3, NULL, NULL, p6], [p6, NULL, NULL, p7]] self.assertEqual(2, len(_connected_segments(c1))) From a11c2c359c357d334f3eb763624c06f8471d2303 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 11 Sep 2025 17:43:48 +1200 Subject: [PATCH 17/19] Change github respository to run only when cmlibs-python. --- .github/workflows/run_tests.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 23dc98a..0ba761e 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -9,8 +9,7 @@ on: jobs: build: runs-on: ${{ matrix.os }} - #if: github.repository == 'cmlibs-python/cmlibs.exporter' - if: github.repository == 'hsorby/cmlibs.exporter' + if: github.repository == 'cmlibs-python/cmlibs.exporter' continue-on-error: true strategy: matrix: From e7a1eeaee392b8af8e3f22cda7ae9a11f6f671c3 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 11 Sep 2025 17:55:49 +1200 Subject: [PATCH 18/19] Run tests in bash shell, add verbose output. --- .github/workflows/run_tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 0ba761e..719735b 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -41,4 +41,5 @@ jobs: fi - name: Run unit tests - run: python -m unittest discover -s tests + shell: bash + run: python -m unittest discover -s tests -v From 1d0bf94fb572a237f1b83e7140391fc99033b4ab Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Fri, 12 Sep 2025 08:53:33 +1200 Subject: [PATCH 19/19] Run test command in multiline mode. --- .github/workflows/run_tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 719735b..82fb066 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -42,4 +42,5 @@ jobs: - name: Run unit tests shell: bash - run: python -m unittest discover -s tests -v + run: | + python -m unittest discover -s tests -v