Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-roifile for openSUSE:Factory checked in at 2026-02-03 21:35:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-roifile (Old) and /work/SRC/openSUSE:Factory/.python-roifile.new.1995 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-roifile" Tue Feb 3 21:35:57 2026 rev:10 rq:1330813 version:2026.1.22 Changes: -------- --- /work/SRC/openSUSE:Factory/python-roifile/python-roifile.changes 2024-11-09 20:59:38.265581977 +0100 +++ /work/SRC/openSUSE:Factory/.python-roifile.new.1995/python-roifile.changes 2026-02-03 21:36:43.029769442 +0100 @@ -1,0 +2,14 @@ +Tue Jan 27 16:08:15 UTC 2026 - Dirk Müller <[email protected]> + +- update to 2026.1.22: + * Fix boolean codec in ImagejRoi.properties. + * Fix reading ImagejRoi.props. + * Add ImagejRoi.properties property to decode and encode + ImagejRoi.props. + * Improve code quality. + * Drop support for Python 3.10. + * Move tests to separate module. + * Support Python 3.14. + * Drop support for Python 3.9. + +------------------------------------------------------------------- Old: ---- roifile-2024.9.15.tar.gz New: ---- roifile-2026.1.22.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-roifile.spec ++++++ --- /var/tmp/diff_new_pack.ixCv79/_old 2026-02-03 21:36:43.505789469 +0100 +++ /var/tmp/diff_new_pack.ixCv79/_new 2026-02-03 21:36:43.509789637 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-roifile # -# Copyright (c) 2024 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,7 +18,7 @@ %define packagename roifile Name: python-roifile -Version: 2024.9.15 +Version: 2026.1.22 Release: 0 Summary: Read and write ImageJ ROI format License: BSD-3-Clause ++++++ roifile-2024.9.15.tar.gz -> roifile-2026.1.22.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/.gitignore new/roifile-2026.1.22/.gitignore --- old/roifile-2024.9.15/.gitignore 2024-09-15 18:53:51.000000000 +0200 +++ new/roifile-2026.1.22/.gitignore 2026-01-22 21:42:33.000000000 +0100 @@ -32,7 +32,7 @@ MANIFEST setup.cfg PKG-INFO - +mypy.ini # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. @@ -56,6 +56,7 @@ *.py,cover .hypothesis/ .pytest_cache/ +cover/ # Translations *.mo @@ -78,6 +79,7 @@ docs/_build/ # PyBuilder +.pybuilder/ target/ # Jupyter Notebook @@ -88,6 +90,8 @@ ipython_config.py # pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: .python-version # pipenv @@ -97,8 +101,35 @@ # install all needed dependencies. #Pipfile.lock -# celery beat schedule file +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff celerybeat-schedule +celerybeat.pid # SageMath parsed files *.sage.py @@ -129,3 +160,19 @@ # Pyre type checker .pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# PyPI configuration file +.pypirc diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/CHANGES.rst new/roifile-2026.1.22/CHANGES.rst --- old/roifile-2024.9.15/CHANGES.rst 2024-09-15 18:53:51.000000000 +0200 +++ new/roifile-2026.1.22/CHANGES.rst 2026-01-22 21:42:33.000000000 +0100 @@ -1,6 +1,32 @@ Revisions --------- +2026.1.22 + +- Fix boolean codec in ImagejRoi.properties. + +2026.1.20 + +- Fix reading ImagejRoi.props. +- Add ImagejRoi.properties property to decode and encode ImagejRoi.props. + +2026.1.8 + +- Improve code quality. +- Drop support for Python 3.10. + +2025.12.12 + +- Move tests to separate module. + +2025.5.10 + +- Support Python 3.14. + +2025.2.20 + +- Drop support for Python 3.9. + 2024.9.15 - Improve typing. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/LICENSE new/roifile-2026.1.22/LICENSE --- old/roifile-2024.9.15/LICENSE 2024-09-15 18:53:51.000000000 +0200 +++ new/roifile-2026.1.22/LICENSE 2026-01-22 21:42:33.000000000 +0100 @@ -1,6 +1,6 @@ -BSD 3-Clause License +BSD-3-Clause license -Copyright (c) 2020-2024, Christoph Gohlke +Copyright (c) 2020-2026, Christoph Gohlke All rights reserved. Redistribution and use in source and binary forms, with or without diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/MANIFEST.in new/roifile-2026.1.22/MANIFEST.in --- old/roifile-2024.9.15/MANIFEST.in 2024-09-15 18:53:51.000000000 +0200 +++ new/roifile-2026.1.22/MANIFEST.in 2026-01-22 21:42:33.000000000 +0100 @@ -1,11 +1,16 @@ include LICENSE include README.rst include CHANGES.rst +include pyproject.toml include roifile_demo.py include roifile/py.typed +exclude .env exclude *.cmd +exclude *.yaml +exclude mypy.ini +exclude ruff.toml recursive-exclude doc * recursive-exclude docs * recursive-exclude test * @@ -14,3 +19,6 @@ recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-exclude * *Copy* + +include tests/conftest.py +include tests/test_roifile.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/README.rst new/roifile-2026.1.22/README.rst --- old/roifile-2024.9.15/README.rst 2024-09-15 18:53:51.000000000 +0200 +++ new/roifile-2026.1.22/README.rst 2026-01-22 21:42:33.000000000 +0100 @@ -11,8 +11,8 @@ .. _ImageJ: https://imagej.net :Author: `Christoph Gohlke <https://www.cgohlke.com>`_ -:License: BSD 3-Clause -:Version: 2024.9.15 +:License: BSD-3-Clause +:Version: 2026.1.22 :DOI: `10.5281/zenodo.6941603 <https://doi.org/10.5281/zenodo.6941603>`_ Quickstart @@ -38,55 +38,41 @@ This revision was tested with the following requirements and dependencies (other versions may work): -- `CPython <https://www.python.org>`_ 3.10.11, 3.11.9, 3.12.5, 3.13.0rc2 -- `Numpy <https://pypi.org/project/numpy/>`_ 2.2.1 -- `Tifffile <https://pypi.org/project/tifffile/>`_ 2024.8.30 (optional) -- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.9.2 (optional) +- `CPython <https://www.python.org>`_ 3.11.9, 3.12.10, 3.13.11, 3.14.2 64-bit +- `NumPy <https://pypi.org/project/numpy>`_ 2.4.1 +- `Tifffile <https://pypi.org/project/tifffile/>`_ 2026.1.14 (optional) +- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.10.8 (optional) Revisions --------- -2024.9.15 - -- Improve typing. -- Deprecate Python 3.9, support Python 3.13. - -2024.5.24 +2026.1.22 -- Fix docstring examples not correctly rendered on GitHub. +- Fix boolean codec in ImagejRoi.properties. -2024.3.20 +2026.1.20 -- Fix writing generator of ROIs (#9). +- Fix reading ImagejRoi.props. +- Add ImagejRoi.properties property to decode and encode ImagejRoi.props. -2024.1.10 +2026.1.8 -- Support text rotation. -- Improve text rendering. -- Avoid array copies. -- Limit size read from files. +- Improve code quality. +- Drop support for Python 3.10. -2023.8.30 +2025.12.12 -- Fix linting issues. -- Add py.typed marker. +- Move tests to separate module. -2023.5.12 +2025.5.10 -- Improve object repr and type hints. -- Drop support for Python 3.8 and numpy < 1.21 (NEP29). +- Support Python 3.14. -2023.2.12 +2025.2.20 -- Delay import of zipfile. -- Verify shape of coordinates on write. +- Drop support for Python 3.9. -2022.9.19 - -- Fix integer coordinates to -5000..60536 conforming with ImageJ (breaking). -- Add subpixel_coordinates in frompoints for out-of-range integer coordinates. - -2022.7.29 +2024.9.15 - … @@ -170,6 +156,7 @@ .. code-block:: python + >>> import numpy >>> import tifffile >>> tifffile.imwrite( ... '_test.tif', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/pyproject.toml new/roifile-2026.1.22/pyproject.toml --- old/roifile-2024.9.15/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 +++ new/roifile-2026.1.22/pyproject.toml 2026-01-22 21:42:33.000000000 +0100 @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 79 +target-version = ["py311", "py312", "py313", "py314"] +skip-string-normalization = true + +[tool.isort] +known_first_party = ["roifile"] +profile = "black" +line_length = 79 \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/roifile/__init__.py new/roifile-2026.1.22/roifile/__init__.py --- old/roifile-2024.9.15/roifile/__init__.py 2024-09-15 18:53:51.000000000 +0200 +++ new/roifile-2026.1.22/roifile/__init__.py 2026-01-22 21:42:33.000000000 +0100 @@ -1,4 +1,9 @@ # roifile/__init__.py -from .roifile import __doc__, __all__, __version__ from .roifile import * +from .roifile import __all__, __doc__, __version__ + +# constants are repeated for documentation + +__version__ = __version__ +"""Roifile version string.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/roifile/roifile.py new/roifile-2026.1.22/roifile/roifile.py --- old/roifile-2024.9.15/roifile/roifile.py 2024-09-15 18:53:51.000000000 +0200 +++ new/roifile-2026.1.22/roifile/roifile.py 2026-01-22 21:42:33.000000000 +0100 @@ -1,6 +1,6 @@ # roifile.py -# Copyright (c) 2020-2024, Christoph Gohlke +# Copyright (c) 2020-2026, Christoph Gohlke # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -38,8 +38,8 @@ .. _ImageJ: https://imagej.net :Author: `Christoph Gohlke <https://www.cgohlke.com>`_ -:License: BSD 3-Clause -:Version: 2024.9.15 +:License: BSD-3-Clause +:Version: 2026.1.22 :DOI: `10.5281/zenodo.6941603 <https://doi.org/10.5281/zenodo.6941603>`_ Quickstart @@ -65,55 +65,41 @@ This revision was tested with the following requirements and dependencies (other versions may work): -- `CPython <https://www.python.org>`_ 3.10.11, 3.11.9, 3.12.5, 3.13.0rc2 -- `Numpy <https://pypi.org/project/numpy/>`_ 2.2.1 -- `Tifffile <https://pypi.org/project/tifffile/>`_ 2024.8.30 (optional) -- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.9.2 (optional) +- `CPython <https://www.python.org>`_ 3.11.9, 3.12.10, 3.13.11, 3.14.2 64-bit +- `NumPy <https://pypi.org/project/numpy>`_ 2.4.1 +- `Tifffile <https://pypi.org/project/tifffile/>`_ 2026.1.14 (optional) +- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.10.8 (optional) Revisions --------- -2024.9.15 - -- Improve typing. -- Deprecate Python 3.9, support Python 3.13. - -2024.5.24 +2026.1.22 -- Fix docstring examples not correctly rendered on GitHub. +- Fix boolean codec in ImagejRoi.properties. -2024.3.20 +2026.1.20 -- Fix writing generator of ROIs (#9). +- Fix reading ImagejRoi.props. +- Add ImagejRoi.properties property to decode and encode ImagejRoi.props. -2024.1.10 +2026.1.8 -- Support text rotation. -- Improve text rendering. -- Avoid array copies. -- Limit size read from files. +- Improve code quality. +- Drop support for Python 3.10. -2023.8.30 +2025.12.12 -- Fix linting issues. -- Add py.typed marker. +- Move tests to separate module. -2023.5.12 +2025.5.10 -- Improve object repr and type hints. -- Drop support for Python 3.8 and numpy < 1.21 (NEP29). +- Support Python 3.14. -2023.2.12 +2025.2.20 -- Delay import of zipfile. -- Verify shape of coordinates on write. +- Drop support for Python 3.9. -2022.9.19 - -- Fix integer coordinates to -5000..60536 conforming with ImageJ (breaking). -- Add subpixel_coordinates in frompoints for out-of-range integer coordinates. - -2022.7.29 +2024.9.15 - … @@ -183,6 +169,7 @@ Write the ROIs to an ImageJ formatted TIFF file: +>>> import numpy >>> import tifffile >>> tifffile.imwrite( ... '_test.tif', @@ -206,34 +193,36 @@ from __future__ import annotations -__version__ = '2024.9.15' +__version__ = '2026.1.22' __all__ = [ - 'roiread', - 'roiwrite', - 'ImagejRoi', - 'ROI_TYPE', - 'ROI_SUBTYPE', + 'ROI_COLOR_NONE', 'ROI_OPTIONS', - 'ROI_POINT_TYPE', 'ROI_POINT_SIZE', - 'ROI_COLOR_NONE', + 'ROI_POINT_TYPE', + 'ROI_SUBTYPE', + 'ROI_TYPE', + 'ImagejRoi', + '__version__', + 'roiread', + 'roiwrite', ] -import dataclasses +import contextlib import enum import logging import os import struct import sys import uuid +from dataclasses import dataclass from typing import TYPE_CHECKING import numpy if TYPE_CHECKING: from collections.abc import Iterable - from typing import Any, Iterator, Literal + from typing import Any, Literal from matplotlib.axes import Axes from numpy.typing import ArrayLike, NDArray @@ -281,11 +270,13 @@ if name is not None: if isinstance(name, str): - raise ValueError("'name' is not an iterable of str") + msg = "'name' is not an iterable of str" + raise ValueError(msg) name = iter(name) import zipfile + assert mode is not None with zipfile.ZipFile(filename, mode) as zf: for r in roi: if name is None: @@ -328,6 +319,7 @@ class ROI_OPTIONS(enum.IntFlag): """ImageJ ROI options.""" + NONE = 0 SPLINE_FIT = 1 DOUBLE_HEADED = 2 OUTLINE = 4 @@ -366,17 +358,17 @@ ROI_COLOR_NONE = b'\x00\x00\x00\x00' -"""No color or Black.""" +"""No color or black.""" [email protected] +@dataclass class ImagejRoi: """Read and write ImageJ ROI format.""" byteorder: Literal['>', '<'] = '>' roitype: ROI_TYPE = ROI_TYPE.POLYGON subtype: ROI_SUBTYPE = ROI_SUBTYPE.UNDEFINED - options: ROI_OPTIONS = ROI_OPTIONS(0) + options: ROI_OPTIONS = ROI_OPTIONS.NONE name: str = '' props: str = '' version: int = 217 @@ -513,18 +505,19 @@ with tifffile.TiffFile(filename) as tif: if tif.imagej_metadata is None: - raise ValueError('file does not contain ImagejRoi') - rois = [] + msg = 'file does not contain ImagejRoi' + raise ValueError(msg) + rois: list[bytes] = [] if 'Overlays' in tif.imagej_metadata: overlays = tif.imagej_metadata['Overlays'] - if isinstance(overlays, list): + if isinstance(overlays, (list, tuple)): rois.extend(overlays) else: rois.append(overlays) if 'ROI' in tif.imagej_metadata: roi = tif.imagej_metadata['ROI'] - if isinstance(roi, list): - overlays.extend(roi) + if isinstance(roi, (list, tuple)): + rois.extend(roi) else: rois.append(roi) return [ @@ -558,7 +551,8 @@ ) -> ImagejRoi: """Return ImagejRoi instance from bytes.""" if data[:4] != b'Iout': - raise ValueError(f'not an ImageJ ROI {data[:4]!r}') + msg = f'not an ImageJ ROI {data[:4]!r}' + raise ValueError(msg) self = cls() @@ -607,15 +601,14 @@ self.options = ROI_OPTIONS(options) if self.subpixelrect: - (self.xd, self.yd, self.widthd, self.heightd) = struct.unpack( + self.xd, self.yd, self.widthd, self.heightd = struct.unpack( self.byteorder + 'ffff', data[18:34] ) - elif ( - self.roitype == ROI_TYPE.LINE - or self.roitype == ROI_TYPE.FREEHAND + elif self.roitype == ROI_TYPE.LINE or ( + self.roitype == ROI_TYPE.FREEHAND and self.subtype in {ROI_SUBTYPE.ELLIPSE, ROI_SUBTYPE.ROTATED_RECT} ): - (self.x1, self.y1, self.x2, self.y2) = struct.unpack( + self.x1, self.y1, self.x2, self.y2 = struct.unpack( self.byteorder + 'ffff', data[18:34] ) elif self.n_coordinates == 0: @@ -650,7 +643,7 @@ if roi_props_offset > 0 and roi_props_length > 0: props = data[ - roi_props_offset : name_offset + roi_props_length * 2 + roi_props_offset : roi_props_offset + roi_props_length * 2 ] self.props = props.decode(self.utf16) @@ -732,6 +725,8 @@ def tofile( self, filename: os.PathLike[Any] | str, + /, + *, name: str | None = None, mode: Literal['r', 'w', 'x', 'a'] | None = None, ) -> None: @@ -750,9 +745,12 @@ mode = 'a' if os.path.exists(filename) else 'w' import zipfile - with zipfile.ZipFile(filename, mode) as zf: - with zf.open(name, 'w') as fh: - fh.write(self.tobytes()) + assert mode is not None + with ( + zipfile.ZipFile(filename, mode) as zf, + zf.open(name, 'w') as fh, + ): + fh.write(self.tobytes()) else: with open(filename, 'wb') as fh: fh.write(self.tobytes()) @@ -784,9 +782,8 @@ self.heightd, ) ) - elif ( - self.roitype == ROI_TYPE.LINE - or self.roitype == ROI_TYPE.FREEHAND + elif self.roitype == ROI_TYPE.LINE or ( + self.roitype == ROI_TYPE.FREEHAND and self.subtype in {ROI_SUBTYPE.ELLIPSE, ROI_SUBTYPE.ROTATED_RECT} ): result.append( @@ -844,22 +841,24 @@ ): if self.integer_coordinates is not None: if self.integer_coordinates.shape != (self.n_coordinates, 2): - raise ValueError( + msg = ( 'integer_coordinates.shape ' f'{self.integer_coordinates.shape} ' f'!= ({self.n_coordinates}, 2)' ) + raise ValueError(msg) coord = self.integer_coordinates.astype( self.byteorder + 'i2', copy=False ) extradata = coord.tobytes(order='F') if self.subpixel_coordinates is not None: if self.subpixel_coordinates.shape != (self.n_coordinates, 2): - raise ValueError( + msg = ( 'subpixel_coordinates.shape ' f'{self.subpixel_coordinates.shape} ' f'!= ({self.n_coordinates}, 2)' ) + raise ValueError(msg) coord = self.subpixel_coordinates.astype( self.byteorder + 'f4', copy=False ) @@ -931,9 +930,10 @@ title: str | None = None, bounds: bool = False, invert_yaxis: bool | None = None, + show: bool = True, **kwargs: Any, ) -> None: - """Plot a draft of coordinates using matplotlib.""" + """Plot draft of coordinates using matplotlib.""" fig: Any roitype = self.roitype subtype = self.subtype @@ -973,7 +973,8 @@ roi.plot(ax=ax, **kwargs) if invert_yaxis: ax.invert_yaxis() - pyplot.show() + if show: + pyplot.show() return if 'color' not in kwargs and 'c' not in kwargs: @@ -1002,7 +1003,7 @@ x, y = line[1] ax.arrow(x, y, -dx, -dy, **kwargs) elif roitype == ROI_TYPE.RECT and subtype == ROI_SUBTYPE.TEXT: - coords = self.coordinates(True)[0] + coords = self.coordinates(multi=True)[0] if 'fontsize' not in kwargs and self.text_size > 0: kwargs['fontsize'] = self.text_size text = ax.text( @@ -1045,18 +1046,23 @@ if invert_yaxis: ax.invert_yaxis() - if fig is not None: + if show and fig is not None: pyplot.show() def coordinates( - self, multi: bool = False + self, + *, + multi: bool = False, ) -> NDArray[Any] | list[NDArray[Any]]: """Return x, y coordinates as numpy array for display.""" coords: Any if self.subpixel_coordinates is not None: coords = self.subpixel_coordinates.copy() elif self.integer_coordinates is not None: - coords = self.integer_coordinates + [self.left, self.top] + coords = self.integer_coordinates + [ # noqa: RUF005 + self.left, + self.top, + ] elif self.multi_coordinates is not None: coordslist = self.path2coords(self.multi_coordinates) if not multi: @@ -1084,7 +1090,7 @@ return [coords] if multi else coords def hexcolor(self, b: bytes, /, default: str | None = None) -> str | None: - """Return color (bytes) as hex triplet or None if black.""" + """Return color (bytes) as hex triplet or default if black.""" if b == ROI_COLOR_NONE: return default if self.byteorder == '>': @@ -1096,9 +1102,11 @@ multi_coordinates: NDArray[numpy.float32], / ) -> list[NDArray[numpy.float32]]: """Return list of coordinate arrays from 2D geometric path.""" - coordinates = [] - points: list[list[float]] = [] - path: list[float] = multi_coordinates.tolist() + coordinates: list[NDArray[numpy.float32]] = [] + points: list[tuple[float, float]] = [] + path: list[float] = [] + + path = multi_coordinates.tolist() n = 0 m = 0 while n < len(path): @@ -1110,30 +1118,30 @@ numpy.array(points, dtype=numpy.float32) ) points = [] - points.append([path[n + 1], path[n + 2]]) + points.append((path[n + 1], path[n + 2])) m = len(points) - 1 n += 3 elif op == 1: # LINETO - points.append([path[n + 1], path[n + 2]]) + points.append((path[n + 1], path[n + 2])) n += 3 elif op == 4: # CLOSE points.append(points[m]) n += 1 - elif op == 2 or op == 3: + elif op == 2 or op == 3: # noqa: PLR1714 # QUADTO or CUBICTO - raise NotImplementedError( - f'PathIterator command {op!r} not supported' - ) + msg = f'PathIterator command {op!r} not supported' + raise NotImplementedError(msg) else: - raise RuntimeError(f'invalid PathIterator command {op!r}') + msg = f'invalid PathIterator command {op!r}' + raise RuntimeError(msg) coordinates.append(numpy.array(points, dtype=numpy.float32)) return coordinates @staticmethod - def min_int_coord(value: int | None = None) -> int: + def min_int_coord(value: int | None = None, /) -> int: """Return minimum integer coordinate value. The default, -5000, is used by ImageJ. @@ -1144,26 +1152,31 @@ return -5000 if -32768 <= value <= 0: return int(value) - raise ValueError(f'{value=} out of range') + msg = f'{value=} out of range' + raise ValueError(msg) @property def composite(self) -> bool: + """ROI is composite shape.""" return self.shape_roi_size > 0 @property def subpixelresolution(self) -> bool: + """ROI has subpixel resolution.""" return self.version >= 222 and bool( self.options & ROI_OPTIONS.SUB_PIXEL_RESOLUTION ) @property def drawoffset(self) -> bool: + """ROI has draw offset.""" return self.subpixelresolution and bool( self.options & ROI_OPTIONS.DRAW_OFFSET ) @property def subpixelrect(self) -> bool: + """ROI has subpixel rectangle.""" return ( self.version >= 223 and self.subpixelresolution @@ -1182,10 +1195,57 @@ return name @property + def properties(self) -> dict[str, Any]: + """Return ImagejRoi.props as dictionary.""" + val: Any + props = {} + for line in self.props.splitlines(): + if ':' in line: + key, val = line.split(':', 1) + key = key.strip() + val = val.strip() + if val == 'true': + val = True + elif val == 'false': + val = False + else: + try: + val = int(val) + except ValueError: + with contextlib.suppress(ValueError): + val = float(val) + props[key] = val + return props + + @properties.setter + def properties(self, value: dict[str, Any], /) -> None: + """Set ImagejRoi.props from dictionary.""" + lines = [] + for item in sorted(value.items()): + key, val = item + if isinstance(val, bool): + val = 'true' if val else 'false' + # TODO: does float need specific format? + lines.append(f'{key}: {val}\n') + self.props = ''.join(lines) + + @property def utf16(self) -> str: """UTF-16 codec depending on byteorder.""" return 'utf-16' + ('be' if self.byteorder == '>' else 'le') + def __hash__(self) -> int: + """Return hash of ImagejRoi.""" + return hash( + ( + self.tobytes(), + self.left, + self.top, + self.right, + self.bottom, + ) + ) + def __eq__(self, other: object) -> bool: """Return True if two ImagejRoi are the same.""" return ( @@ -1201,9 +1261,9 @@ info = [f'{self.__class__.__name__}('] for name, value in self.__dict__.items(): if isinstance(value, numpy.ndarray): - value = repr(value).replace(' ', ' ') - value = value.replace('([[', '([\n [') - info.append(f'{name}=numpy.{value},') + v = repr(value).replace(' ', ' ') + v = v.replace('([[', '([\n [') + info.append(f'{name}=numpy.{v},') elif value == getattr(ImagejRoi, name): pass elif isinstance(value, enum.Enum): @@ -1242,7 +1302,7 @@ gc: Any, tpath: Any, affine: Any, - rgbFace: Any = None, + rgbFace: Any = None, # noqa: N803 ) -> None: ax = self._text.axes renderer = ax.get_figure().canvas.get_renderer() @@ -1258,7 +1318,7 @@ def oval(rect: ArrayLike, /, points: int = 33) -> NDArray[numpy.float32]: """Return coordinates of oval from rectangle corners.""" - arr = numpy.array(rect, dtype=numpy.float32) + arr = numpy.asarray(rect, dtype=numpy.float32) c = numpy.linspace(0.0, 2.0 * numpy.pi, num=points, dtype=numpy.float32) c = numpy.array([numpy.cos(c), numpy.sin(c)]).T r = arr[1] - arr[0] @@ -1302,129 +1362,8 @@ def logger() -> logging.Logger: - """Return logging.getLogger('roifile').""" - return logging.getLogger(__name__.replace('roifile.roifile', 'roifile')) - - -def test(verbose: bool = False) -> None: - """Test roifile.ImagejRoi class.""" - # test ROIs from a ZIP file - rois: Any = ImagejRoi.fromfile('tests/ijzip.zip') - assert isinstance(rois, list) - assert len(rois) == 7 - for roi in rois: - assert roi == ImagejRoi.frombytes(roi.tobytes()) - roi.coordinates() - if verbose: - print(roi) - str(roi) - - # re-write ROIs to a ZIP file - try: - os.remove('_test.zip') - except OSError: - pass - - def roi_iter() -> Iterator[ImagejRoi]: - # issue #9 - yield from rois - - roiwrite('_test.zip', roi_iter()) - assert roiread('_test.zip') == rois - - # verify box_combined - rois = roiread('tests/box_combined.roi') - assert isinstance(rois, ImagejRoi) - roi = rois - if verbose: - print(roi) - assert roi == ImagejRoi.frombytes(roi.tobytes()) - assert roi.name == '0464-0752' - assert roi.roitype == ROI_TYPE.RECT - assert roi.version == 227 - assert (roi.top, roi.left, roi.bottom, roi.right) == (316, 692, 612, 812) - coords = roi.coordinates(multi=True) - assert len(coords) == 31 - assert coords[0][0][0] == 767.0 - assert coords[-1][-1][-1] == 587.0 - assert roi.multi_coordinates is not None - assert roi.multi_coordinates[0] == 0.0 - with open('tests/box_combined.roi', 'rb') as fh: - expected = fh.read() - assert roi.tobytes() == expected - str(roi) - - roi = ImagejRoi.frompoints([[1, 2], [3, 4], [5, 6]]) - assert roi == ImagejRoi.frombytes(roi.tobytes()) - assert roi.left == 1 - assert roi.top == 2 - assert roi.right == 6 - assert roi.bottom == 7 - - roi = ImagejRoi.frompoints([[1.1, 2.2], [3.3, 4.4], [5.5, 6.6]]) - assert roi == ImagejRoi.frombytes(roi.tobytes()) - assert roi.left == 1 - assert roi.top == 2 - assert roi.right == 7 - assert roi.bottom == 8 - - roi = ImagejRoi.frompoints([[-5000, 60535], [60534, 65534]]) - assert roi == ImagejRoi.frombytes(roi.tobytes()) - assert roi.left == -5000, roi.left - assert roi.top == 60535, roi.top - assert roi.right == 60535, roi.right - assert roi.bottom == 65535, roi.bottom - - # issue #7 - roi = ImagejRoi.frompoints( - numpy.load('tests/issue7.npy').astype(numpy.float32) - ) - assert roi == ImagejRoi.frombytes(roi.tobytes()) - assert roi.left == 28357, roi.left - assert roi.top == 42200, roi.top # not -23336 - assert roi.right == 28453, roi.right - assert roi.bottom == 42284, roi.bottom # not -23252 - coords = roi.coordinates() - assert roi.integer_coordinates is not None - assert roi.subpixel_coordinates is not None - assert roi.integer_coordinates[0, 0] == 0 - assert roi.integer_coordinates[0, 1] == 15 - assert roi.subpixel_coordinates[0, 0] == 28357.0 - assert roi.subpixel_coordinates[0, 1] == 42215.0 - - # rotated text - rois = roiread('tests/text_rotated.roi') - assert isinstance(rois, ImagejRoi) - roi = rois - if verbose: - print(roi) - assert roi == ImagejRoi.frombytes(roi.tobytes()) - assert roi.name == 'Rotated' - assert roi.roitype == ROI_TYPE.RECT - assert roi.subtype == ROI_SUBTYPE.TEXT - assert roi.version == 228 - assert (roi.top, roi.left, roi.bottom, roi.right) == (252, 333, 280, 438) - assert roi.stroke_color == b'\xff\x00\x00\xff' - assert roi.text_size == 20 - assert roi.text_justification == 1 - assert roi.text_name == 'SansSerif' - assert roi.text == 'Enter text...\n' - with open('tests/text_rotated.roi', 'rb') as fh: - expected = fh.read() - assert roi.tobytes() == expected - str(roi) - - # read a ROI from a TIFF file - rois = roiread('tests/IJMetadata.tif') - assert isinstance(rois, list) - for roi in rois: - assert roi == ImagejRoi.frombytes(roi.tobytes()) - roi.coordinates() - if verbose: - print(roi) - str(roi) - - assert ImagejRoi() == ImagejRoi() + """Return logger for roifile module.""" + return logging.getLogger('roifile') def main(argv: list[str] | None = None) -> int: @@ -1440,25 +1379,6 @@ if argv is None: argv = sys.argv - if len(argv) > 1 and '--test' in argv: - if os.path.exists('../tests'): - os.chdir('../') - import doctest - - m: Any - try: - import roifile.roifile - - m = roifile.roifile - except ImportError: - m = None - if os.path.exists('tests'): - print('running tests') - test() - print('running doctests') - doctest.testmod(m) - return 0 - if len(argv) == 1: files = glob('*.roi') files += glob('*.zip') @@ -1473,28 +1393,26 @@ files = argv[1:] for fname in files: - print(fname) + print(fname) # noqa: T201 try: rois = ImagejRoi.fromfile(fname) title = os.path.split(fname)[-1] if isinstance(rois, list): for roi in rois: - print(roi) - print() + print(roi, '\n') # noqa: T201 if sys.flags.dev_mode: assert roi == ImagejRoi.frombytes(roi.tobytes()) if rois: rois[0].plot(rois=rois, title=title) else: - print(rois) - print() + print(rois, '\n') # noqa: T201 if sys.flags.dev_mode: assert rois == ImagejRoi.frombytes(rois.tobytes()) rois.plot(title=title) except ValueError as exc: if sys.flags.dev_mode: raise - print(fname, exc) + print(fname, exc) # noqa: T201 continue return 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/roifile_demo.py new/roifile-2026.1.22/roifile_demo.py --- old/roifile-2024.9.15/roifile_demo.py 2024-09-15 18:53:51.000000000 +0200 +++ new/roifile-2026.1.22/roifile_demo.py 2026-01-22 21:42:33.000000000 +0100 @@ -9,14 +9,15 @@ import numpy from matplotlib import pyplot -from roifile import ImagejRoi from skimage.measure import find_contours, label, regionprops from tifffile import TiffFile, imwrite +from roifile import ImagejRoi + def plot_image_overlays(image, overlays, **kwargs): """Plot image and overlays (bytes) using matplotlib.""" - fig, ax = pyplot.subplots() + _fig, ax = pyplot.subplots() ax.imshow(image, cmap='gray') if not isinstance(overlays, list): overlays = [overlays] @@ -29,7 +30,7 @@ # open an ImageJ TIFF file and read the image and overlay data # https://github.com/csachs/imagej-tiff-meta/ # blob/b6a74daa8c2adf7023d20a447d9a2799614c857a/box.tif -with TiffFile('tests/box.tif') as tif: +with TiffFile('tests/data/box.tif') as tif: image = tif.pages[0].asarray() assert tif.imagej_metadata is not None overlays = tif.imagej_metadata['Overlays'] @@ -39,9 +40,7 @@ # segment the image with scikit-image labeled = label(image > 0.5 * image.max()) for region in regionprops(labeled): - if region.area > 10000: - labeled[labeled == region.label] = 0 - elif region.area < 100: + if not 100 < region.area < 10000: labeled[labeled == region.label] = 0 segmentation = 1.0 * (labeled > 0) @@ -51,7 +50,9 @@ for contour in find_contours(segmentation, level=0.9999) ] -plot_image_overlays(image, overlays, lw=5) +plot_image_overlays(image, overlays, linewidth=5) # write the image and overlays to a new ImageJ TIFF file imwrite('roi_test.tif', image, imagej=True, metadata={'Overlays': overlays}) + +# mypy: allow-untyped-defs, allow-untyped-calls diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/setup.py new/roifile-2026.1.22/setup.py --- old/roifile-2024.9.15/setup.py 2024-09-15 18:53:51.000000000 +0200 +++ new/roifile-2026.1.22/setup.py 2026-01-22 21:42:33.000000000 +0100 @@ -8,15 +8,16 @@ from setuptools import setup -def search(pattern, string, flags=0): +def search(pattern: str, string: str, flags: int = 0) -> str: """Return first match of pattern in string.""" match = re.search(pattern, string, flags) if match is None: - raise ValueError(f'{pattern!r} not found') + msg = f'{pattern=!r} not found' + raise ValueError(msg) return match.groups()[0] -def fix_docstring_examples(docstring): +def fix_docstring_examples(docstring: str) -> str: """Return docstring with examples fixed for GitHub.""" start = True indent = False @@ -47,7 +48,7 @@ re.MULTILINE | re.DOTALL, ) readme = '\n'.join( - [description, '=' * len(description)] + readme.splitlines()[1:] + [description, '=' * len(description), *readme.splitlines()[1:]] ) if 'sdist' in sys.argv: @@ -64,7 +65,7 @@ license = license.replace('# ', '').replace('#', '') with open('LICENSE', 'w', encoding='utf-8') as fh: - fh.write('BSD 3-Clause License\n\n') + fh.write('BSD-3-Clause license\n\n') fh.write(license) revisions = search( @@ -84,7 +85,7 @@ setup( name='roifile', version=version, - license='BSD', + license='BSD-3-Clause', description=description, long_description=readme, long_description_content_type='text/x-rst', @@ -99,20 +100,19 @@ packages=['roifile'], package_data={'roifile': ['py.typed']}, entry_points={'console_scripts': ['roifile = roifile.roifile:main']}, - python_requires='>=3.9', + python_requires='>=3.11', install_requires=['numpy'], extras_require={'all': ['matplotlib', 'tifffile']}, platforms=['any'], classifiers=[ 'Development Status :: 4 - Beta', - 'License :: OSI Approved :: BSD License', 'Intended Audience :: Science/Research', 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', ], ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/tests/conftest.py new/roifile-2026.1.22/tests/conftest.py --- old/roifile-2024.9.15/tests/conftest.py 1970-01-01 01:00:00.000000000 +0100 +++ new/roifile-2026.1.22/tests/conftest.py 2026-01-22 21:42:33.000000000 +0100 @@ -0,0 +1,26 @@ +# roifile/tests/conftest.py + +"""Pytest configuration.""" + +import os +import sys + +if os.environ.get('VSCODE_CWD'): + # work around pytest not using PYTHONPATH in VSCode + sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + ) + + +def pytest_report_header(config: object) -> str: + """Return pytest report header.""" + try: + import roifile + + return ( + f'Python {sys.version.splitlines()[0]}\n' + f'packagedir: {roifile.__path__[0]}\n' + f'version: roifile {roifile.__version__}' + ) + except Exception as exc: + return f'pytest_report_header failed: {exc!s}' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/roifile-2024.9.15/tests/test_roifile.py new/roifile-2026.1.22/tests/test_roifile.py --- old/roifile-2024.9.15/tests/test_roifile.py 1970-01-01 01:00:00.000000000 +0100 +++ new/roifile-2026.1.22/tests/test_roifile.py 2026-01-22 21:42:33.000000000 +0100 @@ -0,0 +1,355 @@ +# test_roifile.py + +# Copyright (c) 2020-2026, Christoph Gohlke +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the copyright holders nor the names of any +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Unittests for the roifile package. + +:Version: 2026.1.22 + +""" + +import contextlib +import glob +import io +import os +import pathlib +import pickle +import sys +from collections.abc import Iterator +from typing import Any + +import numpy +import pytest +from matplotlib import pyplot + +import roifile +from roifile import ( + ROI_COLOR_NONE, + ROI_OPTIONS, + ROI_POINT_SIZE, + ROI_POINT_TYPE, + ROI_SUBTYPE, + ROI_TYPE, + ImagejRoi, + __version__, + roiread, + roiwrite, +) + +HERE = pathlib.Path(os.path.dirname(__file__)) +DATA = HERE / 'data' + + [email protected](__doc__ is None, reason='__doc__ is None') +def test_version(): + """Assert roifile versions match docstrings.""" + ver = ':Version: ' + __version__ + assert ver in __doc__ + assert ver in roifile.__doc__ + + +def test_read_zip_file(): + """Test reading ROIs from a ZIP file.""" + rois: Any = ImagejRoi.fromfile(DATA / 'ijzip.zip') + assert isinstance(rois, list) + assert len(rois) == 7 + for roi in rois: + assert roi == ImagejRoi.frombytes(roi.tobytes()) + roi.coordinates() + _ = roi.properties + str(roi) + + +def test_write_zip_file(): + """Test writing ROIs to a ZIP file.""" + rois: Any = ImagejRoi.fromfile(DATA / 'ijzip.zip') + + with contextlib.suppress(OSError): + os.remove('_test.zip') + + def roi_iter() -> Iterator[ImagejRoi]: + yield from rois + + roiwrite('_test.zip', roi_iter()) + assert roiread('_test.zip') == rois + + +def test_read_tiff_file(): + """Test reading ROIs from a TIFF file.""" + rois = roiread(DATA / 'IJMetadata.tif') + assert isinstance(rois, list) + for roi in rois: + assert roi == ImagejRoi.frombytes(roi.tobytes()) + roi.coordinates() + str(roi) + + +def test_empty_roi(): + """Test empty ROI equality.""" + assert ImagejRoi() == ImagejRoi() + + +def test_box_combined(): + """Test box_combined composite ROI.""" + rois = roiread(DATA / 'box_combined.roi') + assert isinstance(rois, ImagejRoi) + roi = rois + str(roi) + assert roi == ImagejRoi.frombytes(roi.tobytes()) + assert roi.name == '0464-0752' + assert roi.roitype == ROI_TYPE.RECT + assert roi.version == 227 + assert (roi.top, roi.left, roi.bottom, roi.right) == (316, 692, 612, 812) + coords = roi.coordinates(multi=True) + assert len(coords) == 31 + assert coords[0][0][0] == 767.0 + assert coords[-1][-1][-1] == 587.0 + assert roi.multi_coordinates is not None + assert roi.multi_coordinates[0] == 0.0 + with open(DATA / 'box_combined.roi', 'rb') as fh: + expected = fh.read() + assert roi.properties == {} + assert roi.tobytes() == expected + str(roi) + + +def test_frompoints_integer(): + """Test creating ROI from integer coordinates.""" + roi = ImagejRoi.frompoints([[1, 2], [3, 4], [5, 6]]) + assert roi == ImagejRoi.frombytes(roi.tobytes()) + assert roi.left == 1 + assert roi.top == 2 + assert roi.right == 6 + assert roi.bottom == 7 + + +def test_frompoints_float(): + """Test creating ROI from float coordinates.""" + roi = ImagejRoi.frompoints([[1.1, 2.2], [3.3, 4.4], [5.5, 6.6]]) + assert roi == ImagejRoi.frombytes(roi.tobytes()) + assert roi.left == 1 + assert roi.top == 2 + assert roi.right == 7 + assert roi.bottom == 8 + + +def test_frompoints_large_coordinates(): + """Test creating ROI from large coordinate values.""" + roi = ImagejRoi.frompoints([[-5000, 60535], [60534, 65534]]) + assert roi == ImagejRoi.frombytes(roi.tobytes()) + assert roi.left == -5000, roi.left + assert roi.top == 60535, roi.top + assert roi.right == 60535, roi.right + assert roi.bottom == 65535, roi.bottom + + +def test_rotated_text(): + """Test reading rotated text ROI.""" + rois = roiread(DATA / 'text_rotated.roi') + assert isinstance(rois, ImagejRoi) + roi = rois + str(roi) + assert roi == ImagejRoi.frombytes(roi.tobytes()) + assert roi.name == 'Rotated' + assert roi.roitype == ROI_TYPE.RECT + assert roi.subtype == ROI_SUBTYPE.TEXT + assert roi.version == 228 + assert (roi.top, roi.left, roi.bottom, roi.right) == (252, 333, 280, 438) + assert roi.stroke_color == b'\xff\x00\x00\xff' + assert roi.text_size == 20 + assert roi.text_justification == 1 + assert roi.text_name == 'SansSerif' + assert roi.text == 'Enter text...\n' + with open(DATA / 'text_rotated.roi', 'rb') as fh: + expected = fh.read() + assert roi.tobytes() == expected + str(roi) + + +def test_properties(): + """Test ROI properties.""" + rois = ImagejRoi.fromfile(DATA / '27197299958_88cf5966d3_b.tif') + assert isinstance(rois, list) + assert len(rois) == 8 + + for roi in rois: + roi.props.endswith('\n') + assert roi == ImagejRoi.frombytes(roi.tobytes()) + roi.coordinates() + str(roi) + + roi = rois[0] + assert roi.roitype == ROI_TYPE.LINE + assert roi.subtype == ROI_SUBTYPE.UNDEFINED + assert roi.options == ( + ROI_OPTIONS.OVERLAY_LABELS | ROI_OPTIONS.OVERLAY_BACKGROUNDS + ) + assert roi.version == 228 + assert roi.name == 'Plat' + assert roi.stroke_color == b'\xff\xff\x00\x00' + assert roi.fill_color == ROI_COLOR_NONE + assert len(roi.props) == 418 + assert roi.props.startswith('%Area: 0\nAbutment_implant_misfit: No\n') + assert roi.props.endswith('X: 48\nY: 98.500\n') + + props = roi.properties + assert isinstance(props, dict) + assert len(props) == 26 + assert props['%Area'] == 0 + assert props['Y'] == 98.5 + assert props['Angle'] == -1.193 + assert props['Roi'] == 'Plat' + assert props['Poor_Quality'] == 'No' + + roi.properties = props # encode roi.props + assert roi.properties == props # decode roi.props + assert len(roi.props) == 415 + assert roi.props.startswith('%Area: 0\nAbutment_implant_misfit: No\n') + assert roi.props.endswith('X: 48\nY: 98.5\n') # float formatting different + assert roi == ImagejRoi.frombytes(roi.tobytes()) + + props['Bool'] = False + roi.properties = props + assert roi.properties['Bool'] is False + assert 'BY: 98\nBool: false\n' in roi.props # sorted by key + + +def test_zipfile(): + """Test ROIs from ZIP file.""" + rois = ImagejRoi.fromfile(DATA / 'ijzip.zip') + assert isinstance(rois, list) + assert len(rois) == 7 + for roi in rois: + assert roi == ImagejRoi.frombytes(roi.tobytes()) + roi.coordinates() + str(roi) + + +def test_pickle(): + """Test pickling ROI.""" + rois = ImagejRoi.fromfile(DATA / 'ijzip.zip') + fh = io.BytesIO() + pickle.dump(rois, fh) + for roi0, roi1 in zip( + rois, pickle.loads(fh.getvalue()), strict=True # noqa: S301 + ): + assert roi0 == roi1 + + +def test_issue_7(): + """Test issue #7: large coordinate values.""" + roi = ImagejRoi.frompoints( + numpy.load(DATA / 'issue7.npy').astype(numpy.float32) + ) + assert roi == ImagejRoi.frombytes(roi.tobytes()) + assert roi.left == 28357, roi.left + assert roi.top == 42200, roi.top # not -23336 + assert roi.right == 28453, roi.right + assert roi.bottom == 42284, roi.bottom # not -23252 + _ = roi.coordinates() + assert roi.integer_coordinates is not None + assert roi.subpixel_coordinates is not None + assert roi.integer_coordinates[0, 0] == 0 + assert roi.integer_coordinates[0, 1] == 15 + assert roi.subpixel_coordinates[0, 0] == 28357.0 + assert roi.subpixel_coordinates[0, 1] == 42215.0 + + +def test_issue_9(): + """Test issue #9: roiwrite with iterable input.""" + # re-write ROIs to a ZIP file + rois: Any = ImagejRoi.fromfile(DATA / 'ijzip.zip') + assert len(rois) == 7 + + # write list + with contextlib.suppress(OSError): + os.remove('_test.zip') + roiwrite('_test.zip', rois) + assert roiread('_test.zip') == rois + + # provide names + with contextlib.suppress(OSError): + os.remove('_test.zip') + roiwrite('_test.zip', rois, name=[str(i) for i in range(len(rois))]) + assert roiread('_test.zip') == rois + + # write generator, issue #9 + def roi_iter() -> Iterator[ImagejRoi]: + yield from rois + + with contextlib.suppress(OSError): + os.remove('_test.zip') + roiwrite('_test.zip', roi_iter()) + assert roiread('_test.zip') == rois + + [email protected]( + 'fname', glob.glob('*.roi', root_dir=DATA, recursive=False) +) +def test_glob_roi(fname): + """Test read all ROI files.""" + if 'defective' in fname: + pytest.xfail(reason='file is marked defective') + fname = DATA / fname + roi = ImagejRoi.fromfile(fname) + assert isinstance(roi, ImagejRoi) + str(roi) + roi.plot(show=False) + pyplot.close() + + [email protected]( + 'fname', glob.glob('*.zip', root_dir=DATA, recursive=False) +) +def test_glob_zip(fname): + """Test read all ZIP files.""" + if 'defective' in fname: + pytest.xfail(reason='file is marked defective') + fname = DATA / fname + rois = roiread(fname) + assert isinstance(rois, list) + for roi in rois: + str(roi) + roi.plot(show=False) + pyplot.close() + + +if __name__ == '__main__': + import warnings + + # warnings.simplefilter('always') + warnings.filterwarnings('ignore', category=ImportWarning) + argv = sys.argv + argv.append('--cov-report=html') + argv.append('--cov=roifile') + argv.append('--verbose') + sys.exit(pytest.main(argv)) + + +# mypy: allow-untyped-defs, allow-untyped-calls +# mypy: disable-error-code="arg-type"
