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-10-26 10:04:32
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-roifile (Old)
 and      /work/SRC/openSUSE:Factory/.python-roifile.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-roifile"

Mon Oct 26 10:04:32 2026 rev:13 rq:1392628 version:2026.9.22

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-roifile/python-roifile.changes    
2026-05-12 19:31:22.350784467 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-roifile.new.2252680/python-roifile.changes   
    2026-10-26 10:05:40.339979491 +0100
@@ -1,0 +2,12 @@
+Mon Oct 26 07:07:26 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2026.9.22:
+  * Accept buffer objects and avoid unnecessary copies in
+    ImagejRoi.frombytes.
+  * Use Java compatible rounding for subpixel coordinates
+    (breaking).
+  * Recover roi names stored as Latin-1 instead of UTF-16.
+  * Drop support for Python 3.11 and numpy 2.0 (SPEC0).
+  * Support Python 3.15.
+
+-------------------------------------------------------------------

Old:
----
  roifile-2026.2.10.tar.gz

New:
----
  roifile-2026.9.22.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-roifile.spec ++++++
--- /var/tmp/diff_new_pack.sbQp8L/_old  2026-10-26 10:05:41.977048117 +0100
+++ /var/tmp/diff_new_pack.sbQp8L/_new  2026-10-26 10:05:41.980048243 +0100
@@ -17,9 +17,8 @@
 
 
 %define packagename roifile
-%global skip_python311 1
 Name:           python-roifile
-Version:        2026.2.10
+Version:        2026.9.22
 Release:        0
 Summary:        Read and write ImageJ ROI format
 License:        BSD-3-Clause

++++++ roifile-2026.2.10.tar.gz -> roifile-2026.9.22.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/roifile-2026.2.10/CHANGES.rst 
new/roifile-2026.9.22/CHANGES.rst
--- old/roifile-2026.2.10/CHANGES.rst   2026-02-11 20:54:35.000000000 +0100
+++ new/roifile-2026.9.22/CHANGES.rst   2026-09-21 22:11:57.000000000 +0200
@@ -1,6 +1,17 @@
 Revisions
 ---------
 
+2026.9.22
+
+- Accept buffer objects and avoid unnecessary copies in ImagejRoi.frombytes.
+
+2026.7.30
+
+- Use Java compatible rounding for subpixel coordinates (breaking).
+- Recover roi names stored as Latin-1 instead of UTF-16.
+- Drop support for Python 3.11 and numpy 2.0 (SPEC0).
+- Support Python 3.15.
+
 2026.2.10
 
 - Revise wrapping of integer coordinates again (breaking).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/roifile-2026.2.10/README.rst 
new/roifile-2026.9.22/README.rst
--- old/roifile-2026.2.10/README.rst    2026-02-11 20:54:35.000000000 +0100
+++ new/roifile-2026.9.22/README.rst    2026-09-21 22:11:57.000000000 +0200
@@ -12,7 +12,7 @@
 
 :Author: `Christoph Gohlke <https://www.cgohlke.com>`_
 :License: BSD-3-Clause
-:Version: 2026.2.10
+:Version: 2026.9.22
 :DOI: `10.5281/zenodo.6941603 <https://doi.org/10.5281/zenodo.6941603>`_
 
 Quickstart
@@ -27,7 +27,8 @@
 
     python -m roifile file.roi
 
-See `Examples`_ for using the programming interface.
+See `Examples`_ and `Documentation <https://www.cgohlke.com/docs/roifile/>`_
+for using the programming interface.
 
 Source code, examples, and support are available on
 `GitHub <https://github.com/cgohlke/roifile>`_.
@@ -38,15 +39,26 @@
 This revision was tested with the following requirements and dependencies
 (other versions may work):
 
-- `CPython <https://www.python.org>`_ 3.11.9, 3.12.10, 3.13.12, 3.14.3 64-bit
-- `NumPy <https://pypi.org/project/numpy>`_ 2.4.2
-- `Tifffile <https://pypi.org/project/tifffile/>`_ 2026.1.28 (optional)
-- `Imagecodecs <https://pypi.org/project/imagecodecs/>`_ 2026.1.14 (optional)
-- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.10.8 (optional)
+- `CPython <https://www.python.org>`_ 3.12.10, 3.13.15, 3.14.7, 3.15.0rc 64-bit
+- `Numpy <https://pypi.org/project/numpy>`_ 2.5.3
+- `Tifffile <https://pypi.org/project/tifffile/>`_ 2026.9.20 (optional)
+- `Imagecodecs <https://pypi.org/project/imagecodecs/>`_ 2026.8.16 (optional)
+- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.11.2 (optional)
 
 Revisions
 ---------
 
+2026.9.22
+
+- Accept buffer objects and avoid unnecessary copies in ImagejRoi.frombytes.
+
+2026.7.30
+
+- Use Java compatible rounding for subpixel coordinates (breaking).
+- Recover roi names stored as Latin-1 instead of UTF-16.
+- Drop support for Python 3.11 and numpy 2.0 (SPEC0).
+- Support Python 3.15.
+
 2026.2.10
 
 - Revise wrapping of integer coordinates again (breaking).
@@ -109,6 +121,19 @@
 Examples
 --------
 
+Import functions and classes used in these examples:
+
+.. code-block:: python
+
+    >>> from roifile import (
+    ...     ImagejRoi,
+    ...     roiread,
+    ...     roiwrite,
+    ...     ROI_TYPE,
+    ...     ROI_POINT_SIZE,
+    ...     ROI_OPTIONS,
+    ... )
+
 Create a new ImagejRoi instance from an array of x, y coordinates,
 then set ROI properties:
 
@@ -186,7 +211,7 @@
     >>> rois = roiread('_test.tif')
     >>> assert len(rois) == 2 and rois[0] == roi and rois[1].name == 'test'
 
-View the overlays stored in a ROI, ZIP, or TIFF file from a command line::
+View the overlays stored in a ROI, ZIP, or TIFF file from the command line::
 
     python -m roifile _test.roi
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/roifile-2026.2.10/pyproject.toml 
new/roifile-2026.9.22/pyproject.toml
--- old/roifile-2026.2.10/pyproject.toml        2026-02-11 20:54:35.000000000 
+0100
+++ new/roifile-2026.9.22/pyproject.toml        2026-09-21 22:11:57.000000000 
+0200
@@ -4,7 +4,7 @@
 
 [tool.black]
 line-length = 79
-target-version = ["py311", "py312", "py313", "py314"]
+target-version = ["py312", "py313", "py314"]
 skip-string-normalization = true
 
 [tool.isort]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/roifile-2026.2.10/roifile/roifile.py 
new/roifile-2026.9.22/roifile/roifile.py
--- old/roifile-2026.2.10/roifile/roifile.py    2026-02-11 20:54:35.000000000 
+0100
+++ new/roifile-2026.9.22/roifile/roifile.py    2026-09-21 22:11:57.000000000 
+0200
@@ -39,7 +39,7 @@
 
 :Author: `Christoph Gohlke <https://www.cgohlke.com>`_
 :License: BSD-3-Clause
-:Version: 2026.2.10
+:Version: 2026.9.22
 :DOI: `10.5281/zenodo.6941603 <https://doi.org/10.5281/zenodo.6941603>`_
 
 Quickstart
@@ -54,7 +54,8 @@
 
     python -m roifile file.roi
 
-See `Examples`_ for using the programming interface.
+See `Examples`_ and `Documentation <https://www.cgohlke.com/docs/roifile/>`_
+for using the programming interface.
 
 Source code, examples, and support are available on
 `GitHub <https://github.com/cgohlke/roifile>`_.
@@ -65,15 +66,26 @@
 This revision was tested with the following requirements and dependencies
 (other versions may work):
 
-- `CPython <https://www.python.org>`_ 3.11.9, 3.12.10, 3.13.12, 3.14.3 64-bit
-- `NumPy <https://pypi.org/project/numpy>`_ 2.4.2
-- `Tifffile <https://pypi.org/project/tifffile/>`_ 2026.1.28 (optional)
-- `Imagecodecs <https://pypi.org/project/imagecodecs/>`_ 2026.1.14 (optional)
-- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.10.8 (optional)
+- `CPython <https://www.python.org>`_ 3.12.10, 3.13.15, 3.14.7, 3.15.0rc 64-bit
+- `Numpy <https://pypi.org/project/numpy>`_ 2.5.3
+- `Tifffile <https://pypi.org/project/tifffile/>`_ 2026.9.20 (optional)
+- `Imagecodecs <https://pypi.org/project/imagecodecs/>`_ 2026.8.16 (optional)
+- `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.11.2 (optional)
 
 Revisions
 ---------
 
+2026.9.22
+
+- Accept buffer objects and avoid unnecessary copies in ImagejRoi.frombytes.
+
+2026.7.30
+
+- Use Java compatible rounding for subpixel coordinates (breaking).
+- Recover roi names stored as Latin-1 instead of UTF-16.
+- Drop support for Python 3.11 and numpy 2.0 (SPEC0).
+- Support Python 3.15.
+
 2026.2.10
 
 - Revise wrapping of integer coordinates again (breaking).
@@ -136,6 +148,17 @@
 Examples
 --------
 
+Import functions and classes used in these examples:
+
+>>> from roifile import (
+...     ImagejRoi,
+...     roiread,
+...     roiwrite,
+...     ROI_TYPE,
+...     ROI_POINT_SIZE,
+...     ROI_OPTIONS,
+... )
+
 Create a new ImagejRoi instance from an array of x, y coordinates,
 then set ROI properties:
 
@@ -197,7 +220,7 @@
 >>> rois = roiread('_test.tif')
 >>> assert len(rois) == 2 and rois[0] == roi and rois[1].name == 'test'
 
-View the overlays stored in a ROI, ZIP, or TIFF file from a command line::
+View the overlays stored in a ROI, ZIP, or TIFF file from the command line::
 
     python -m roifile _test.roi
 
@@ -207,7 +230,7 @@
 
 from __future__ import annotations
 
-__version__ = '2026.2.10'
+__version__ = '2026.9.22'
 
 __all__ = [
     'ROI_COLOR_NONE',
@@ -236,7 +259,7 @@
 import numpy
 
 if TYPE_CHECKING:
-    from collections.abc import Iterable
+    from collections.abc import Buffer, Iterable
     from typing import Any, Literal
 
     from matplotlib.axes import Axes
@@ -332,26 +355,37 @@
 
     UNKNOWN = -1
     """Undocumented or unknown ROI type."""
+
     POLYGON = 0
     """Polygon with straight edges."""
+
     RECT = 1
     """Rectangle."""
+
     OVAL = 2
     """Oval/ellipse."""
+
     LINE = 3
     """Straight line."""
+
     FREELINE = 4
     """Freehand line."""
+
     POLYLINE = 5
     """Polyline with straight segments."""
+
     NOROI = 6
     """No ROI."""
+
     FREEHAND = 7
     """Freehand polygon."""
+
     TRACED = 8
     """Traced polygon."""
+
     ANGLE = 9
     """Angle measurement."""
+
     POINT = 10
     """Point or multi-point."""
 
@@ -368,16 +402,22 @@
 
     UNKNOWN = -1
     """Undocumented or unknown ROI subtype."""
+
     UNDEFINED = 0
     """No subtype specified."""
+
     TEXT = 1
     """Text overlay."""
+
     ARROW = 2
     """Arrow overlay."""
+
     ELLIPSE = 3
     """Ellipse (fitted)."""
+
     IMAGE = 4
     """Embedded image overlay."""
+
     ROTATED_RECT = 5
     """Rotated rectangle."""
 
@@ -394,36 +434,52 @@
 
     NONE = 0
     """No options."""
+
     SPLINE_FIT = 1
     """Spline fit enabled."""
+
     DOUBLE_HEADED = 2
     """Double-headed arrow."""
+
     OUTLINE = 4
     """Outline only (no fill)."""
+
     OVERLAY_LABELS = 8
     """Show overlay labels."""
+
     OVERLAY_NAMES = 16
     """Show overlay names."""
+
     OVERLAY_BACKGROUNDS = 32
     """Show overlay backgrounds."""
+
     OVERLAY_BOLD = 64
     """Bold overlay text."""
+
     SUB_PIXEL_RESOLUTION = 128
     """Subpixel resolution coordinates."""
+
     DRAW_OFFSET = 256
     """Draw with offset."""
+
     ZERO_TRANSPARENT = 512
     """Zero values transparent."""
+
     SHOW_LABELS = 1024
     """Show point labels."""
+
     SCALE_LABELS = 2048
     """Scale labels with zoom."""
+
     PROMPT_BEFORE_DELETING = 4096
     """Prompt before deletion."""
+
     SCALE_STROKE_WIDTH = 8192
     """Scale stroke width with zoom."""
+
     UNKNOWN_14 = 16384
     """Undocumented or unknown option (bit 14)."""
+
     UNKNOWN_15 = 32768
     """Undocumented or unknown option (bit 15)."""
 
@@ -433,13 +489,17 @@
 
     UNKNOWN = -1
     """Undocumented or unknown point type."""
+
     HYBRID = 0
     """Hybrid marker (cross with dot)."""
+
     CROSS = 1
     """Cross/crosshair marker."""
     # CROSSHAIR = 1  # alias for CROSS in ImageJ, not needed here
+
     DOT = 2
     """Dot/circle marker (filled)."""
+
     CIRCLE = 3
     """Circle marker (outline)."""
 
@@ -456,18 +516,25 @@
 
     UNKNOWN = -1
     """Undocumented or unknown point size."""
+
     TINY = 1
     """Tiny marker (1px)."""
+
     SMALL = 3
     """Small marker (3px)."""
+
     MEDIUM = 5
     """Medium marker (5px)."""
+
     LARGE = 7
     """Large marker (7px)."""
+
     EXTRA_LARGE = 11
     """Extra large marker (11px)."""
+
     XXL = 17
     """XXL marker (17px)."""
+
     XXXL = 25
     """XXXL marker (25px)."""
 
@@ -489,100 +556,148 @@
 
     byteorder: Literal['>', '<'] = '>'
     """Byte order: '>' for big-endian, '<' for little-endian."""
+
     roitype: ROI_TYPE = ROI_TYPE.POLYGON
     """ROI type (polygon, rect, oval, line, point, etc)."""
+
     subtype: ROI_SUBTYPE = ROI_SUBTYPE.UNDEFINED
     """Subtype for specialized ROIs (text, arrow, ellipse, image, etc)."""
+
     options: ROI_OPTIONS = ROI_OPTIONS.NONE
     """Bit flags for ROI options and features."""
+
     name: str = ''
     """ROI name."""
+
     props: str = ''
     """Properties string containing key:value pairs."""
+
     version: int = 229
     """File format version number."""
+
     top: int = 0
     """Bounding rectangle top coordinate."""
+
     left: int = 0
     """Bounding rectangle left coordinate."""
+
     bottom: int = 0
     """Bounding rectangle bottom coordinate."""
+
     right: int = 0
     """Bounding rectangle right coordinate."""
+
     n_coordinates: int = 0
     """Number of coordinate pairs."""
+
     stroke_width: int = 0
     """Stroke width in pixels (also point_size for POINT ROIs version 226+)."""
+
     shape_roi_size: int = 0
     """Composite shape data size in floats."""
+
     stroke_color: bytes = ROI_COLOR_NONE
     """Stroke/outline color as ARGB bytes."""
+
     fill_color: bytes = ROI_COLOR_NONE
     """Fill color as ARGB bytes."""
+
     arrow_style_or_aspect_ratio: int = 0
     """Arrow style, aspect ratio for ellipse, or point_type for POINT ROIs."""
+
     arrow_head_size: int = 0
     """Arrow head size in pixels."""
+
     rounded_rect_arc_size: int = 0
     """Arc size for rounded rectangle corners."""
+
     position: int = 0
     """Position in stack (1-based, 0 means not set)."""
+
     c_position: int = 0
     """Channel position (1-based, 0 means not set)."""
+
     z_position: int = 0
     """Z-slice position (1-based, 0 means not set)."""
+
     t_position: int = 0
     """Time frame position (1-based, 0 means not set)."""
+
     x1: float = 0.0
     """First X coordinate for line ROIs or X for subpixel rectangles."""
+
     y1: float = 0.0
     """First Y coordinate for line ROIs or Y for subpixel rectangles."""
+
     x2: float = 0.0
     """Second X coordinate for line ROIs."""
+
     y2: float = 0.0
     """Second Y coordinate for line ROIs."""
+
     xd: float = 0.0
     """X coordinate for subpixel rectangles (double precision)."""
+
     yd: float = 0.0
     """Y coordinate for subpixel rectangles (double precision)."""
+
     widthd: float = 0.0
     """Width for subpixel rectangles (double precision)."""
+
     heightd: float = 0.0
     """Height for subpixel rectangles (double precision)."""
+
     overlay_label_color: bytes = ROI_COLOR_NONE
     """Overlay label color as ARGB bytes."""
+
     overlay_font_size: int = 0
     """Overlay label font size in points."""
+
     group: int = 0
+
     """Group number for grouping related ROIs."""
     image_opacity: int = 0
     """Opacity for image ROIs (0-255)."""
+
     image_size: int = 0
     """Embedded image data size in bytes."""
+
     image_data: bytes | None = None
     """Embedded image data for IMAGE subtype ROIs."""
+
     float_stroke_width: float = 0.0
     """Floating point stroke width for precise rendering."""
+
     text_size: int = 0
     """Text ROI font size in points."""
+
     text_style: int = 0
     """Text ROI font style flags (bold, italic, etc)."""
+
     text_justification: int = 0
     """Text ROI alignment (left, center, right)."""
+
     text_angle: float = 0.0
     """Text ROI rotation angle in degrees."""
+
     text_name: str = ''
     """Text ROI font name."""
+
     text: str = ''
     """Text ROI content."""
+
     counters: NDArray[numpy.uint8] | None = None
     """Counter values for each coordinate point."""
+
     counter_positions: NDArray[numpy.uint32] | None = None
     """Counter positions for each coordinate point."""
+
     integer_coordinates: NDArray[numpy.int32] | None = None
     """Integer coordinate pairs relative to bounding box."""
+
     subpixel_coordinates: NDArray[numpy.float32] | None = None
     """Floating point coordinate pairs for subpixel precision."""
+
     multi_coordinates: NDArray[numpy.float32] | None = None
     """Path data for composite shapes (MOVETO, LINETO, CLOSE operations)."""
 
@@ -656,7 +771,8 @@
             self.options |= ROI_OPTIONS.SUB_PIXEL_RESOLUTION
             self.subpixel_coordinates = coords.astype(numpy.float32, copy=True)
             if coords.dtype.kind == 'f':
-                coords = numpy.round(coords)
+                # ImageJ/Java use floor(x+0.5), not banker's rounding
+                coords = numpy.floor(coords + 0.5)
 
         coords = numpy.array(coords, dtype=numpy.int32)
 
@@ -741,26 +857,28 @@
     @classmethod
     def frombytes(
         cls,
-        data: bytes,
+        data: Buffer,
         /,
         *,
         min_int_coord: int | None = None,
     ) -> ImagejRoi:
-        """Return ImagejRoi instance from bytes.
+        """Return ImagejRoi instance from buffer.
 
         Parameters:
-            data: Bytes in ImageJ ROI format.
+            data: Buffer in ImageJ ROI format.
             min_int_coord: Minimum integer coordinate for unwrapping.
 
         Returns:
-            ImagejRoi instance decoded from bytes.
+            ImagejRoi instance decoded from buffer.
 
         """
-        if len(data) < 64:
-            msg = f'ImageJ ROI data too short: {len(data)} < 64 bytes'
+        data = memoryview(data)
+        size = len(data)
+        if size < 64:
+            msg = f'ImageJ ROI data too short: {size} < 64 bytes'
             raise ValueError(msg)
-        if data[:4] != b'Iout':
-            msg = f'not an ImageJ ROI {data[:4]!r}'
+        if data[:4].tobytes() != b'Iout':
+            msg = f'not an ImageJ ROI {data[:4].tobytes()!r}'
             raise ValueError(msg)
 
         self = cls()
@@ -785,8 +903,8 @@
             self.rounded_rect_arc_size,
             self.position,
             header2_offset,
-        ) = struct.unpack(
-            self.byteorder + 'hBxhhhhH16xhi4s4shhBBhii', data[4:64]
+        ) = struct.unpack_from(
+            self.byteorder + 'hBxhhhhH16xhi4s4shhBBhii', data, 4
         )
 
         min_int_coord = ImagejRoi.min_int_coord(min_int_coord)
@@ -815,22 +933,22 @@
         self.options = ROI_OPTIONS(options)
 
         if self.subpixelrect:
-            self.xd, self.yd, self.widthd, self.heightd = struct.unpack(
-                self.byteorder + 'ffff', data[18:34]
+            self.xd, self.yd, self.widthd, self.heightd = struct.unpack_from(
+                self.byteorder + 'ffff', data, 18
             )
         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.byteorder + 'ffff', data[18:34]
+            self.x1, self.y1, self.x2, self.y2 = struct.unpack_from(
+                self.byteorder + 'ffff', data, 18
             )
         elif self.n_coordinates == 0:
-            self.n_coordinates = struct.unpack(
-                self.byteorder + 'i', data[18:22]
+            self.n_coordinates = struct.unpack_from(
+                self.byteorder + 'i', data, 18
             )[0]
 
-        if 0 < header2_offset < len(data) - 52:
+        if 0 < header2_offset < size - 52:
             (
                 self.c_position,
                 self.z_position,
@@ -846,47 +964,54 @@
                 roi_props_offset,
                 roi_props_length,
                 counters_offset,
-            ) = struct.unpack(
-                self.byteorder + '4xiiiii4shBBifiii',
-                data[header2_offset : header2_offset + 52],
+            ) = struct.unpack_from(
+                self.byteorder + '4xiiiii4shBBifiii', data, header2_offset
             )
 
             # handle extended group for version >= 229 (groups > 255)
             if self.version >= 229 and self.group == 0:
                 group_offset = header2_offset + 52
-                if group_offset + 2 <= len(data):
-                    self.group = struct.unpack(
-                        self.byteorder + 'H',
-                        data[group_offset : group_offset + 2],
+                if group_offset + 2 <= size:
+                    self.group = struct.unpack_from(
+                        self.byteorder + 'H', data, group_offset
                     )[0]
 
             if name_offset > 0 and name_length > 0:
                 name_end = name_offset + name_length * 2
-                if name_end <= len(data):
-                    name = data[name_offset:name_end]
-                    self.name = name.decode(self.utf16)
-                else:
+                if name_end <= size:
+                    self.name = str(
+                        data[name_offset:name_end], encoding=self.utf16
+                    )
+                elif name_offset < size:
+                    available = data[name_offset:]
+                    if len(available) == name_length:
+                        # name stored as Latin-1 instead of UTF-16
+                        self.name = str(available, encoding='latin-1')
+                    else:
+                        n = (len(available) // 2) * 2
+                        self.name = str(available[:n], encoding=self.utf16)
                     logger().warning(
                         f'ImagejRoi name exceeds data size: '
-                        f'{name_end} > {len(data)}'
+                        f'{name_end} > {size}'
                     )
 
             if roi_props_offset > 0 and roi_props_length > 0:
                 props_end = roi_props_offset + roi_props_length * 2
-                if props_end <= len(data):
-                    props = data[roi_props_offset:props_end]
-                    self.props = props.decode(self.utf16)
+                if props_end <= size:
+                    self.props = str(
+                        data[roi_props_offset:props_end], encoding=self.utf16
+                    )
                 else:
                     logger().warning(
                         f'ImagejRoi props exceeds data size: '
-                        f'{props_end} > {len(data)}'
+                        f'{props_end} > {size}'
                     )
 
             if counters_offset > 0:
-                counters: NDArray[numpy.uint32] = numpy.ndarray(
-                    shape=self.n_coordinates,
+                counters: NDArray[numpy.uint32] = numpy.frombuffer(
+                    data,
                     dtype=self.byteorder + 'u4',
-                    buffer=data,
+                    count=self.n_coordinates,
                     offset=counters_offset,
                 )
                 self.counters = (counters & 0xFF).astype(numpy.uint8)
@@ -900,28 +1025,30 @@
                 style_and_justification,
                 name_length,
                 text_length,
-            ) = struct.unpack(self.byteorder + 'iiii', data[64:80])
+            ) = struct.unpack_from(self.byteorder + 'iiii', data, 64)
             self.text_style = style_and_justification & 255
             self.text_justification = (style_and_justification >> 8) & 3
             off = 80
-            self.text_name = data[off : off + name_length * 2].decode(
-                self.utf16
+            self.text_name = str(
+                data[off : off + name_length * 2], encoding=self.utf16
             )
             off += name_length * 2
-            self.text = data[off : off + text_length * 2].decode(self.utf16)
+            self.text = str(
+                data[off : off + text_length * 2], encoding=self.utf16
+            )
             if self.version >= 225:
                 off += text_length * 2
-                self.text_angle = struct.unpack(
-                    self.byteorder + 'f', data[off : off + 4]
+                self.text_angle = struct.unpack_from(
+                    self.byteorder + 'f', data, off
                 )[0]
 
         elif self.version >= 221 and self.subtype == ROI_SUBTYPE.IMAGE:
-            if 0 < self.image_size <= len(data) - 64:
-                self.image_data = data[64 : 64 + self.image_size]
+            if 0 < self.image_size <= size - 64:
+                self.image_data = data[64 : 64 + self.image_size].tobytes()
             else:
                 logger().warning(
                     'ImagejRoi image data invalid: '
-                    f'size={self.image_size}, data length={len(data)}'
+                    f'size={self.image_size}, data length={size}'
                 )
 
         elif self.roitype in (
@@ -933,34 +1060,44 @@
             ROI_TYPE.ANGLE,
             ROI_TYPE.POINT,
         ):
-            self.integer_coordinates = numpy.ndarray(
-                shape=(self.n_coordinates, 2),
-                dtype=self.byteorder + 'i2',
-                buffer=data,
-                offset=64,
-                order='F',
-            ).astype(numpy.int32)
+            self.integer_coordinates = (
+                numpy.frombuffer(
+                    data,
+                    dtype=self.byteorder + 'i2',
+                    count=self.n_coordinates * 2,
+                    offset=64,
+                )
+                .reshape((self.n_coordinates, 2), order='F')
+                .astype(numpy.int32)
+            )
 
             # unwrap negative integer_coordinates (wrapped uint16 values)
             select = self.integer_coordinates < 0
             self.integer_coordinates[select] += 65536
 
             if self.subpixelresolution:
-                self.subpixel_coordinates = numpy.ndarray(
-                    shape=(self.n_coordinates, 2),
-                    dtype=self.byteorder + 'f4',
-                    buffer=data,
-                    offset=64 + self.n_coordinates * 4,
-                    order='F',
-                ).copy()
+                self.subpixel_coordinates = (
+                    numpy.frombuffer(
+                        data,
+                        dtype=self.byteorder + 'f4',
+                        count=self.n_coordinates * 2,
+                        offset=64 + self.n_coordinates * 4,
+                    )
+                    .reshape((self.n_coordinates, 2), order='F')
+                    .copy()
+                )
 
         elif self.composite and self.roitype == ROI_TYPE.RECT:
-            self.multi_coordinates = numpy.ndarray(
-                shape=self.shape_roi_size,
-                dtype=self.byteorder + 'f4',
-                buffer=data,
-                offset=64,
-            ).copy()
+            self.multi_coordinates = (
+                numpy.frombuffer(
+                    data,
+                    dtype=self.byteorder + 'f4',
+                    count=numpy.prod(self.shape_roi_size),
+                    offset=64,
+                )
+                .reshape(self.shape_roi_size)
+                .copy()
+            )
 
         elif self.roitype not in (ROI_TYPE.RECT, ROI_TYPE.LINE, ROI_TYPE.OVAL):
             logger().warning(f'cannot handle ImagejRoi type {self.roitype!r}')
@@ -1402,7 +1539,7 @@
         elif self.multi_coordinates is not None:
             coordslist = self.path2coords(self.multi_coordinates)
             if not multi:
-                return sorted(coordslist, key=lambda x: x.size)[-1]
+                return max(coordslist, key=lambda x: x.size)
             return coordslist
         elif self.roitype == ROI_TYPE.LINE:
             coords = numpy.array(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/roifile-2026.2.10/roifile_demo.py 
new/roifile-2026.9.22/roifile_demo.py
--- old/roifile-2026.2.10/roifile_demo.py       2026-02-11 20:54:35.000000000 
+0100
+++ new/roifile-2026.9.22/roifile_demo.py       2026-09-21 22:11:57.000000000 
+0200
@@ -1,5 +1,8 @@
 # roifile_demo.py
 
+# Copyright (c) Christoph Gohlke
+# SPDX-License-Identifier: BSD-3-Clause
+
 """Demonstrate the roifile module.
 
 Use the roifile and tifffile modules to read and write the results of image
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/roifile-2026.2.10/setup.py 
new/roifile-2026.9.22/setup.py
--- old/roifile-2026.2.10/setup.py      2026-02-11 20:54:35.000000000 +0100
+++ new/roifile-2026.9.22/setup.py      2026-09-21 22:11:57.000000000 +0200
@@ -95,15 +95,15 @@
     project_urls={
         'Bug Tracker': 'https://github.com/cgohlke/roifile/issues',
         'Source Code': 'https://github.com/cgohlke/roifile',
-        # 'Documentation': 'https://',
+        'Documentation': 'https://www.cgohlke.com/docs/roifile/',
     },
     packages=['roifile'],
     package_data={'roifile': ['py.typed']},
     entry_points={'console_scripts': ['roifile = roifile.roifile:main']},
-    python_requires='>=3.11',
-    install_requires=['numpy'],
+    python_requires='>=3.12',
+    install_requires=['numpy>=2.1'],
     extras_require={
-        'all': ['matplotlib', 'tifffile', 'imagecodecs>=2026.1.14']
+        'all': ['matplotlib', 'tifffile', 'imagecodecs>=2026.3.6']
     },
     platforms=['any'],
     classifiers=[
@@ -112,9 +112,9 @@
         'Intended Audience :: Developers',
         'Operating System :: OS Independent',
         'Programming Language :: Python :: 3 :: Only',
-        'Programming Language :: Python :: 3.11',
         'Programming Language :: Python :: 3.12',
         'Programming Language :: Python :: 3.13',
         'Programming Language :: Python :: 3.14',
+        'Programming Language :: Python :: 3.15',
     ],
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/roifile-2026.2.10/tests/test_roifile.py 
new/roifile-2026.9.22/tests/test_roifile.py
--- old/roifile-2026.2.10/tests/test_roifile.py 2026-02-11 20:54:35.000000000 
+0100
+++ new/roifile-2026.9.22/tests/test_roifile.py 2026-09-21 22:11:57.000000000 
+0200
@@ -29,7 +29,7 @@
 
 """Unittests for the roifile package.
 
-:Version: 2026.2.10
+:Version: 2026.9.22
 
 """
 
@@ -187,6 +187,17 @@
     assert roi.bottom == 8
 
 
+def test_frompoints_rounding():
+    """Test float coordinates are rounded using floor(x+0.5), not banker's."""
+    # 0.5 -> 1, 2.5 -> 3 (banker's rounding gives 0 and 2)
+    roi = ImagejRoi.frompoints([[0.5, 0.5], [2.5, 2.5]])
+    assert roi.left == 1
+    assert roi.top == 1
+    assert roi.right == 4
+    assert roi.bottom == 4
+    assert numpy.array_equal(roi.integer_coordinates, [[0, 0], [2, 2]])
+
+
 def test_frompoints_large_coordinates():
     """Test creating ROI from large coordinate values."""
     roi = ImagejRoi.frompoints([[-5000, 60535], [60534, 65534]])
@@ -197,6 +208,15 @@
     assert roi.bottom == 65535, roi.bottom
 
 
[email protected]('buffertype', [bytes, bytearray, memoryview])
+def test_frombytes_buffer(buffertype):
+    """Test reading ROI from buffer-protocol inputs."""
+    roi = ImagejRoi.frompoints([[1, 2], [3, 4], [5, 6]])
+    roi2 = ImagejRoi.frombytes(buffertype(roi.tobytes()))
+    assert roi2 == roi
+    assert numpy.array_equal(roi2.coordinates(), roi.coordinates())
+
+
 @pytest.mark.parametrize(
     'points',
     [
@@ -428,6 +448,100 @@
     assert roiread('_test.zip') == rois
 
 
+def test_latin1_name_recovery():
+    """Test reading ROI names stored as Latin-1 instead of UTF-16."""
+    rois = roiread(DATA / 'yn294a001_ROI_ImageJ.zip')
+    assert len(rois) == 12
+    for i, roi in enumerate(rois, 1):
+        assert roi.name == f'ROI-{i}'
+
+
[email protected](
+    'group_number',
+    [
+        255,  # boundary: fits in one byte
+        256,  # just above one byte limit
+        512,  # power of 2
+        1000,  # arbitrary value
+        32767,  # max signed int16
+        32768,  # beyond signed int16
+        65535,  # max unsigned int16
+    ],
+)
+def test_group_numbers_above_255(group_number):
+    """Test that group numbers > 255 are correctly handled."""
+    roi = ImagejRoi.frompoints([[10, 20], [30, 40], [50, 60]])
+    roi.version = 229  # minimum version for extended group support
+    roi.group = group_number
+    roi.name = f'Group {group_number}'
+
+    data = roi.tobytes()
+    roi_restored = ImagejRoi.frombytes(data)
+
+    assert roi.group == group_number
+    assert roi_restored.group == group_number
+    assert roi_restored == roi
+    assert numpy.array_equal(roi.coordinates(), roi_restored.coordinates())
+
+    str(roi)
+    str(roi_restored)
+
+
+def test_group_number_mixed_values():
+    """Test multiple ROIs with different group numbers including > 255."""
+    # create multiple ROIs with different group numbers
+    points = [[10, 20], [30, 40], [50, 60]]
+    group_numbers = [0, 1, 100, 255, 256, 512, 65535]
+
+    rois = []
+    for group_num in group_numbers:
+        roi = ImagejRoi.frompoints(points)
+        roi.version = 229
+        roi.group = group_num
+        roi.name = f'Group {group_num}'
+        rois.append(roi)
+
+    # test round-trip for each ROI
+    for original_roi in rois:
+        data = original_roi.tobytes()
+        restored_roi = ImagejRoi.frombytes(data)
+
+        assert restored_roi.group == original_roi.group, (
+            f'Group number not preserved: expected {original_roi.group}, '
+            f'got {restored_roi.group}'
+        )
+        assert restored_roi == original_roi
+
+
+def test_group_number_zip_file():
+    """Test writing and reading ROIs with group numbers > 255 to/from ZIP."""
+    # create ROIs with various group numbers
+    rois = []
+    for group_num in [100, 256, 512, 1000]:
+        roi = ImagejRoi.frompoints(
+            [[10 + group_num, 20], [30, 40 + group_num]]
+        )
+        roi.version = 229
+        roi.group = group_num
+        roi.name = f'ROI_Group_{group_num}'
+        rois.append(roi)
+
+    # write to ZIP file
+    with contextlib.suppress(OSError):
+        os.remove('_test.zip')
+
+    roiwrite('_test.zip', rois)
+
+    # read back from ZIP file
+    restored_rois = roiread('_test.zip')
+
+    # verify all ROIs and their group numbers are preserved
+    assert len(restored_rois) == len(rois)
+    for original, restored in zip(rois, restored_rois, strict=True):
+        assert restored.group == original.group
+        assert restored == original
+
+
 @pytest.mark.parametrize(
     'filename', glob.glob('*.roi', root_dir=DATA, recursive=False)
 )
@@ -448,8 +562,8 @@
 )
 def test_glob_zip(filename):
     """Test read all ZIP files."""
-    if 'defective' in filename:
-        pytest.xfail(reason='file is marked defective')
+    if 'defective' in filename or 'Maro_RoiSet' in filename:
+        pytest.xfail(reason='file is excluded')
     filename = DATA / filename
     rois = roiread(filename)
     assert isinstance(rois, list)

Reply via email to