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)