roifile 2023.8.30__py3-none-any.whl → 2024.3.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of roifile might be problematic. Click here for more details.

roifile/roifile.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # roifile.py
2
2
 
3
- # Copyright (c) 2020-2023, Christoph Gohlke
3
+ # Copyright (c) 2020-2024, Christoph Gohlke
4
4
  # All rights reserved.
5
5
  #
6
6
  # Redistribution and use in source and binary forms, with or without
@@ -39,7 +39,7 @@ interest, geometric shapes, paths, text, and whatnot for image overlays.
39
39
 
40
40
  :Author: `Christoph Gohlke <https://www.cgohlke.com>`_
41
41
  :License: BSD 3-Clause
42
- :Version: 2023.8.30
42
+ :Version: 2024.3.20
43
43
  :DOI: `10.5281/zenodo.6941603 <https://doi.org/10.5281/zenodo.6941603>`_
44
44
 
45
45
  Quickstart
@@ -48,7 +48,7 @@ Quickstart
48
48
  Install the roifile package and all dependencies from the
49
49
  `Python Package Index <https://pypi.org/project/roifile/>`_::
50
50
 
51
- python -m pip install -U roifile[all]
51
+ python -m pip install -U "roifile[all]"
52
52
 
53
53
  View overlays stored in a ROI, ZIP, or TIFF file::
54
54
 
@@ -65,14 +65,25 @@ Requirements
65
65
  This revision was tested with the following requirements and dependencies
66
66
  (other versions may work):
67
67
 
68
- - `CPython <https://www.python.org>`_ 3.9.13, 3.10.11, 3.11.5, 3.12rc
69
- - `Numpy <https://pypi.org/project/numpy/>`_ 1.25.2
70
- - `Tifffile <https://pypi.org/project/tifffile/>`_ 2023.8.30 (optional)
71
- - `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.7.2 (optional)
68
+ - `CPython <https://www.python.org>`_ 3.9.13, 3.10.11, 3.11.8, 3.12.2
69
+ - `Numpy <https://pypi.org/project/numpy/>`_ 1.26.4
70
+ - `Tifffile <https://pypi.org/project/tifffile/>`_ 2024.2.12 (optional)
71
+ - `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.8.3 (optional)
72
72
 
73
73
  Revisions
74
74
  ---------
75
75
 
76
+ 2024.3.20
77
+
78
+ - Fix writing generator of ROIs (#9).
79
+
80
+ 2024.1.10
81
+
82
+ - Support text rotation.
83
+ - Improve text rendering.
84
+ - Avoid array copies.
85
+ - Limit size read from files.
86
+
76
87
  2023.8.30
77
88
 
78
89
  - Fix linting issues.
@@ -95,47 +106,9 @@ Revisions
95
106
 
96
107
  2022.7.29
97
108
 
98
- - Update metadata.
99
-
100
- 2022.3.18
101
-
102
- - Fix creating ROIs from float coordinates exceeding int16 range (#7).
103
- - Fix bottom-right bounds in ImagejRoi.frompoints.
104
-
105
- 2022.2.2
106
-
107
- - Add type hints.
108
- - Change ImagejRoi to dataclass.
109
- - Drop support for Python 3.7 and numpy < 1.19 (NEP29).
110
-
111
- 2021.6.6
112
-
113
- - Add enums for point types and sizes.
114
-
115
- 2020.11.28
116
-
117
- - Support group attribute.
118
- - Add roiread and roiwrite functions (#3).
119
- - Use UUID as default name of ROI in ImagejRoi.frompoints (#2).
120
-
121
- 2020.8.13
109
+ -
122
110
 
123
- - Support writing to ZIP file.
124
- - Support os.PathLike file names.
125
-
126
- 2020.5.28
127
-
128
- - Fix int32 to hex color conversion.
129
- - Fix coordinates of closing path.
130
- - Fix reading TIFF files with no overlays.
131
-
132
- 2020.5.1
133
-
134
- - Split positions from counters.
135
-
136
- 2020.2.12
137
-
138
- - Initial release.
111
+ Refer to the CHANGES file for older revisions.
139
112
 
140
113
  Notes
141
114
  -----
@@ -197,7 +170,7 @@ View the overlays stored in a ROI, ZIP, or TIFF file from a command line::
197
170
 
198
171
  from __future__ import annotations
199
172
 
200
- __version__ = '2023.8.30'
173
+ __version__ = '2024.3.20'
201
174
 
202
175
  __all__ = [
203
176
  'roiread',
@@ -213,31 +186,37 @@ __all__ = [
213
186
 
214
187
  import dataclasses
215
188
  import enum
189
+ import logging
216
190
  import os
217
191
  import struct
218
192
  import sys
193
+ import uuid
219
194
  from typing import TYPE_CHECKING
220
195
 
221
196
  import numpy
222
197
 
223
198
  if TYPE_CHECKING:
224
199
  from collections.abc import Iterable
225
- from typing import Any, Literal, Union
200
+ from typing import Any, Literal
226
201
 
227
202
  from numpy.typing import ArrayLike, NDArray
228
203
 
229
- ZipFileMode = Union[Literal['r'], Literal['w'], Literal['x'], Literal['a']]
230
-
231
204
 
232
205
  def roiread(
233
- filename: os.PathLike[Any] | str, /, *, min_int_coord: int | None = None
206
+ filename: os.PathLike[Any] | str,
207
+ /,
208
+ *,
209
+ min_int_coord: int | None = None,
210
+ maxsize: int = 268435456, # 256 MB
234
211
  ) -> ImagejRoi | list[ImagejRoi]:
235
212
  """Return ImagejRoi instance(s) from ROI, ZIP, or TIFF file.
236
213
 
237
214
  For ZIP or TIFF files, return a list of ImagejRoi.
238
215
 
239
216
  """
240
- return ImagejRoi.fromfile(filename, min_int_coord=min_int_coord)
217
+ return ImagejRoi.fromfile(
218
+ filename, min_int_coord=min_int_coord, maxsize=maxsize
219
+ )
241
220
 
242
221
 
243
222
  def roiwrite(
@@ -246,7 +225,7 @@ def roiwrite(
246
225
  /,
247
226
  *,
248
227
  name: str | Iterable[str] | None = None,
249
- mode: ZipFileMode | None = None,
228
+ mode: Literal['r', 'w', 'x', 'a'] | None = None,
250
229
  ) -> None:
251
230
  """Write ImagejRoi instance(s) to ROI or ZIP file.
252
231
 
@@ -263,14 +242,20 @@ def roiwrite(
263
242
  if mode is None:
264
243
  mode = 'a' if os.path.exists(filename) else 'w'
265
244
 
266
- if name is None:
267
- name = [r.name if r.name else r.autoname for r in roi]
268
- name = [n if n[-4:].lower() == '.roi' else n + '.roi' for n in name]
245
+ if name is not None:
246
+ if isinstance(name, str):
247
+ raise ValueError("'name' is not an iterable of str")
248
+ name = iter(name)
269
249
 
270
250
  import zipfile
271
251
 
272
252
  with zipfile.ZipFile(filename, mode) as zf:
273
- for n, r in zip(name, roi):
253
+ for r in roi:
254
+ if name is None:
255
+ n = r.name if r.name else r.autoname
256
+ else:
257
+ n = next(name)
258
+ n = n if n[-4:].lower() == '.roi' else n + '.roi'
274
259
  with zf.open(n, 'w') as fh:
275
260
  fh.write(r.tobytes())
276
261
  return None
@@ -351,7 +336,7 @@ ROI_COLOR_NONE = b'\x00\x00\x00\x00'
351
336
  class ImagejRoi:
352
337
  """Read and write ImageJ ROI format."""
353
338
 
354
- byteorder: Literal['>'] | Literal['<'] = '>'
339
+ byteorder: Literal['>', '<'] = '>'
355
340
  roitype: ROI_TYPE = ROI_TYPE.POLYGON
356
341
  subtype: ROI_SUBTYPE = ROI_SUBTYPE.UNDEFINED
357
342
  options: ROI_OPTIONS = ROI_OPTIONS(0)
@@ -391,6 +376,7 @@ class ImagejRoi:
391
376
  text_size: int = 0
392
377
  text_style: int = 0
393
378
  text_justification: int = 0
379
+ text_angle: float = 0.0
394
380
  text_name: str = ''
395
381
  text: str = ''
396
382
  counters: NDArray[numpy.uint8] | None = None
@@ -440,8 +426,6 @@ class ImagejRoi:
440
426
  self.t_position = t + 1
441
427
  if name is None:
442
428
  if index is None:
443
- import uuid
444
-
445
429
  name = str(uuid.uuid1())
446
430
  else:
447
431
  name = f'F{self.t_position:02}-C{index}'
@@ -452,11 +436,11 @@ class ImagejRoi:
452
436
  numpy.any(coords > 60000) or numpy.any(coords < -5000)
453
437
  ):
454
438
  self.options |= ROI_OPTIONS.SUB_PIXEL_RESOLUTION
455
- self.subpixel_coordinates = coords.astype('f4', copy=True)
439
+ self.subpixel_coordinates = coords.astype(numpy.float32, copy=True)
456
440
  if coords.dtype.kind == 'f':
457
441
  coords = numpy.round(coords)
458
442
 
459
- coords = numpy.array(coords, dtype='i4')
443
+ coords = numpy.array(coords, dtype=numpy.int32)
460
444
 
461
445
  left_top = coords.min(axis=0)
462
446
  right_bottom = coords.max(axis=0)
@@ -479,6 +463,7 @@ class ImagejRoi:
479
463
  /,
480
464
  *,
481
465
  min_int_coord: int | None = None,
466
+ maxsize: int = 268435456, # 256 MB
482
467
  ) -> ImagejRoi | list[ImagejRoi]:
483
468
  """Return ImagejRoi instance from ROI, ZIP, or TIFF file.
484
469
 
@@ -516,13 +501,14 @@ class ImagejRoi:
516
501
  with zipfile.ZipFile(filename) as zf:
517
502
  return [
518
503
  cls.frombytes(
519
- zf.open(name).read(), min_int_coord=min_int_coord
504
+ zf.open(name).read(maxsize),
505
+ min_int_coord=min_int_coord,
520
506
  )
521
507
  for name in zf.namelist()
522
508
  ]
523
509
 
524
510
  with open(filename, 'rb') as fh:
525
- data = fh.read()
511
+ data = fh.read(maxsize)
526
512
  return cls.frombytes(data, min_int_coord=min_int_coord)
527
513
 
528
514
  @classmethod
@@ -535,7 +521,7 @@ class ImagejRoi:
535
521
  ) -> ImagejRoi:
536
522
  """Return ImagejRoi instance from bytes."""
537
523
  if data[:4] != b'Iout':
538
- raise ValueError('not an ImageJ ROI')
524
+ raise ValueError(f'not an ImageJ ROI {data[:4]!r}')
539
525
 
540
526
  self = cls()
541
527
 
@@ -638,8 +624,10 @@ class ImagejRoi:
638
624
  buffer=data,
639
625
  offset=counters_offset,
640
626
  )
641
- self.counters = (counters & 0xFF).astype('u1')
642
- self.counter_positions = (counters >> 8).astype('u4')
627
+ self.counters = (counters & 0xFF).astype(numpy.uint8)
628
+ self.counter_positions = (counters >> 8).astype(
629
+ numpy.uint32, copy=False
630
+ )
643
631
 
644
632
  if self.version >= 218 and self.subtype == ROI_SUBTYPE.TEXT:
645
633
  (
@@ -656,6 +644,11 @@ class ImagejRoi:
656
644
  )
657
645
  off += name_length * 2
658
646
  self.text = data[off : off + text_length * 2].decode(self.utf16)
647
+ if self.version >= 225:
648
+ off += text_length * 2
649
+ self.text_angle = struct.unpack(
650
+ self.byteorder + 'f', data[off : off + 4]
651
+ )[0]
659
652
 
660
653
  elif self.roitype in (
661
654
  ROI_TYPE.POLYGON,
@@ -672,7 +665,7 @@ class ImagejRoi:
672
665
  buffer=data,
673
666
  offset=64,
674
667
  order='F',
675
- ).astype('i4')
668
+ ).astype(numpy.int32)
676
669
 
677
670
  select = self.integer_coordinates < min_int_coord
678
671
  self.integer_coordinates[select] += 65536
@@ -695,7 +688,7 @@ class ImagejRoi:
695
688
  ).copy()
696
689
 
697
690
  elif self.roitype not in (ROI_TYPE.RECT, ROI_TYPE.LINE, ROI_TYPE.OVAL):
698
- log_warning(f'cannot handle ImagejRoi type {self.roitype!r}')
691
+ logger().warning(f'cannot handle ImagejRoi type {self.roitype!r}')
699
692
 
700
693
  return self
701
694
 
@@ -703,7 +696,7 @@ class ImagejRoi:
703
696
  self,
704
697
  filename: os.PathLike[Any] | str,
705
698
  name: str | None = None,
706
- mode: ZipFileMode | None = None,
699
+ mode: Literal['r', 'w', 'x', 'a'] | None = None,
707
700
  ) -> None:
708
701
  """Write ImagejRoi to ROI or ZIP file.
709
702
 
@@ -736,10 +729,10 @@ class ImagejRoi:
736
729
  self.byteorder + 'hBxhhhhH',
737
730
  self.version,
738
731
  self.roitype.value,
739
- numpy.array(self.top).astype('i2'),
740
- numpy.array(self.left).astype('i2'),
741
- numpy.array(self.bottom).astype('i2'),
742
- numpy.array(self.right).astype('i2'),
732
+ numpy.array(self.top).astype(numpy.int16),
733
+ numpy.array(self.left).astype(numpy.int16),
734
+ numpy.array(self.bottom).astype(numpy.int16),
735
+ numpy.array(self.right).astype(numpy.int16),
743
736
  self.n_coordinates if self.n_coordinates < 2**16 else 0,
744
737
  )
745
738
  )
@@ -801,7 +794,7 @@ class ImagejRoi:
801
794
  )
802
795
  extradata += self.text_name.encode(self.utf16)
803
796
  extradata += self.text.encode(self.utf16)
804
- extradata += b'\x00' * 4 # ?
797
+ extradata += struct.pack(self.byteorder + 'f', self.text_angle)
805
798
 
806
799
  elif self.roitype in (
807
800
  ROI_TYPE.POLYGON,
@@ -819,7 +812,9 @@ class ImagejRoi:
819
812
  f'{self.integer_coordinates.shape} '
820
813
  f'!= ({self.n_coordinates}, 2)'
821
814
  )
822
- coord = self.integer_coordinates.astype(self.byteorder + 'i2')
815
+ coord = self.integer_coordinates.astype(
816
+ self.byteorder + 'i2', copy=False
817
+ )
823
818
  extradata = coord.tobytes(order='F')
824
819
  if self.subpixel_coordinates is not None:
825
820
  if self.subpixel_coordinates.shape != (self.n_coordinates, 2):
@@ -828,12 +823,16 @@ class ImagejRoi:
828
823
  f'{self.subpixel_coordinates.shape} '
829
824
  f'!= ({self.n_coordinates}, 2)'
830
825
  )
831
- coord = self.subpixel_coordinates.astype(self.byteorder + 'f4')
826
+ coord = self.subpixel_coordinates.astype(
827
+ self.byteorder + 'f4', copy=False
828
+ )
832
829
  extradata += coord.tobytes(order='F')
833
830
 
834
831
  elif self.composite and self.roitype == ROI_TYPE.RECT:
835
832
  assert self.multi_coordinates is not None
836
- coord = self.multi_coordinates.astype(self.byteorder + 'f4')
833
+ coord = self.multi_coordinates.astype(
834
+ self.byteorder + 'f4', copy=False
835
+ )
837
836
  extradata += coord.tobytes()
838
837
 
839
838
  header2_offset = 64 + len(extradata)
@@ -882,12 +881,12 @@ class ImagejRoi:
882
881
  self.counter_positions, dtype=self.byteorder + 'u4'
883
882
  )
884
883
  counters = counters & 0xFF | indices << 8
885
- counters = counters.astype(self.byteorder + 'u4')
884
+ counters = counters.astype(self.byteorder + 'u4', copy=False)
886
885
  result.append(counters.tobytes())
887
886
 
888
887
  return b''.join(result)
889
888
 
890
- def plot( # type: ignore
889
+ def plot(
891
890
  self,
892
891
  ax: Any | None = None,
893
892
  *,
@@ -897,7 +896,7 @@ class ImagejRoi:
897
896
  invert_yaxis: bool | None = None,
898
897
  **kwargs,
899
898
  ) -> None:
900
- """Plot coordinates using matplotlib."""
899
+ """Plot a draft of coordinates using matplotlib."""
901
900
  roitype = self.roitype
902
901
  subtype = self.subtype
903
902
 
@@ -967,10 +966,25 @@ class ImagejRoi:
967
966
  x, y = line[1]
968
967
  ax.arrow(x, y, -dx, -dy, **kwargs)
969
968
  elif roitype == ROI_TYPE.RECT and subtype == ROI_SUBTYPE.TEXT:
970
- point = self.coordinates()[0]
969
+ coords = self.coordinates(True)[0]
971
970
  if 'fontsize' not in kwargs and self.text_size > 0:
972
971
  kwargs['fontsize'] = self.text_size
973
- ax.text(point[0], point[1], self.text, **kwargs)
972
+ text = ax.text(
973
+ *coords[1],
974
+ self.text,
975
+ va='center_baseline',
976
+ rotation=self.text_angle,
977
+ rotation_mode='anchor',
978
+ **kwargs,
979
+ )
980
+ scale_text(text, width=abs(coords[2, 0] - coords[0, 0]))
981
+ # ax.plot(
982
+ # coords[:, 0],
983
+ # coords[:, 1],
984
+ # linewidth=1,
985
+ # color=kwargs.get('color', 0.9),
986
+ # ls=':',
987
+ # )
974
988
  else:
975
989
  for coords in self.coordinates(multi=True):
976
990
  ax.plot(coords[:, 0], coords[:, 1], **kwargs)
@@ -1013,7 +1027,7 @@ class ImagejRoi:
1013
1027
  return coordslist
1014
1028
  elif self.roitype == ROI_TYPE.LINE:
1015
1029
  coords = numpy.array(
1016
- [[self.x1, self.y1], [self.x2, self.y2]], 'f4'
1030
+ [[self.x1, self.y1], [self.x2, self.y2]], numpy.float32
1017
1031
  )
1018
1032
  elif self.roitype == ROI_TYPE.OVAL:
1019
1033
  coords = oval([[self.left, self.top], [self.right, self.bottom]])
@@ -1026,7 +1040,7 @@ class ImagejRoi:
1026
1040
  [self.right, self.top],
1027
1041
  [self.left, self.top],
1028
1042
  ],
1029
- 'f4',
1043
+ numpy.float32,
1030
1044
  )
1031
1045
  else:
1032
1046
  coords = numpy.empty((0, 2), dtype=self.byteorder + 'i4')
@@ -1055,7 +1069,9 @@ class ImagejRoi:
1055
1069
  if op == 0:
1056
1070
  # MOVETO
1057
1071
  if n > 0:
1058
- coordinates.append(numpy.array(points, dtype='f4'))
1072
+ coordinates.append(
1073
+ numpy.array(points, dtype=numpy.float32)
1074
+ )
1059
1075
  points = []
1060
1076
  points.append([path[n + 1], path[n + 2]])
1061
1077
  m = len(points) - 1
@@ -1076,7 +1092,7 @@ class ImagejRoi:
1076
1092
  else:
1077
1093
  raise RuntimeError(f'invalid PathIterator command {op!r}')
1078
1094
 
1079
- coordinates.append(numpy.array(points, dtype='f4'))
1095
+ coordinates.append(numpy.array(points, dtype=numpy.float32))
1080
1096
  return coordinates
1081
1097
 
1082
1098
  @staticmethod
@@ -1091,7 +1107,7 @@ class ImagejRoi:
1091
1107
  return -5000
1092
1108
  if -32768 <= value <= 0:
1093
1109
  return int(value)
1094
- raise ValueError('min_int_coord out of range')
1110
+ raise ValueError(f'{value=} out of range')
1095
1111
 
1096
1112
  @property
1097
1113
  def composite(self) -> bool:
@@ -1160,6 +1176,29 @@ class ImagejRoi:
1160
1176
  return indent(*info, end='\n)')
1161
1177
 
1162
1178
 
1179
+ def scale_text(text: Any, width: float) -> None:
1180
+ """Scale matplotlib text to width in data coordinates."""
1181
+ from matplotlib.patheffects import AbstractPathEffect
1182
+ from matplotlib.transforms import Bbox
1183
+
1184
+ class TextScaler(AbstractPathEffect):
1185
+ def __init__(self, text, width):
1186
+ self._text = text
1187
+ self._width = width
1188
+
1189
+ def draw_path(self, renderer, gc, tpath, affine, rgbFace=None):
1190
+ ax = self._text.axes
1191
+ renderer = ax.get_figure().canvas.get_renderer()
1192
+ bbox = text.get_window_extent(renderer=renderer)
1193
+ bbox = Bbox(ax.transData.inverted().transform(bbox))
1194
+ if self._width > 0 and bbox.width > 0:
1195
+ scale = self._width / bbox.width
1196
+ affine = affine.from_values(scale, 0, 0, scale, 0, 0) + affine
1197
+ renderer.draw_path(gc, tpath, affine, rgbFace)
1198
+
1199
+ text.set_path_effects([TextScaler(text, width)])
1200
+
1201
+
1163
1202
  def oval(rect: ArrayLike, /, points: int = 33) -> NDArray[numpy.float32]:
1164
1203
  """Return coordinates of oval from rectangle corners."""
1165
1204
  arr = numpy.array(rect, dtype=numpy.float32)
@@ -1205,11 +1244,9 @@ def enumstr(v: enum.Enum | None, /) -> str:
1205
1244
  return s
1206
1245
 
1207
1246
 
1208
- def log_warning(msg: object, *args: object, **kwargs: Any) -> None:
1209
- """Log message with level WARNING."""
1210
- import logging
1211
-
1212
- logging.getLogger(__name__).warning(msg, *args, **kwargs)
1247
+ def logger() -> logging.Logger:
1248
+ """Return logging.getLogger('roifile')."""
1249
+ return logging.getLogger(__name__.replace('roifile.roifile', 'roifile'))
1213
1250
 
1214
1251
 
1215
1252
  def test(verbose: bool = False) -> None:
@@ -1230,7 +1267,12 @@ def test(verbose: bool = False) -> None:
1230
1267
  os.remove('_test.zip')
1231
1268
  except OSError:
1232
1269
  pass
1233
- roiwrite('_test.zip', rois)
1270
+
1271
+ def roi_iter():
1272
+ # issue #9
1273
+ yield from rois
1274
+
1275
+ roiwrite('_test.zip', roi_iter())
1234
1276
  assert roiread('_test.zip') == rois
1235
1277
 
1236
1278
  # verify box_combined
@@ -1250,6 +1292,9 @@ def test(verbose: bool = False) -> None:
1250
1292
  assert coords[-1][-1][-1] == 587.0
1251
1293
  assert roi.multi_coordinates is not None
1252
1294
  assert roi.multi_coordinates[0] == 0.0
1295
+ with open('tests/box_combined.roi', 'rb') as fh:
1296
+ expected = fh.read()
1297
+ assert roi.tobytes() == expected
1253
1298
  str(roi)
1254
1299
 
1255
1300
  roi = ImagejRoi.frompoints([[1, 2], [3, 4], [5, 6]])
@@ -1274,7 +1319,9 @@ def test(verbose: bool = False) -> None:
1274
1319
  assert roi.bottom == 65535, roi.bottom
1275
1320
 
1276
1321
  # issue #7
1277
- roi = ImagejRoi.frompoints(numpy.load('tests/issue7.npy').astype('f4'))
1322
+ roi = ImagejRoi.frompoints(
1323
+ numpy.load('tests/issue7.npy').astype(numpy.float32)
1324
+ )
1278
1325
  assert roi == ImagejRoi.frombytes(roi.tobytes())
1279
1326
  assert roi.left == 28357, roi.left
1280
1327
  assert roi.top == 42200, roi.top # not -23336
@@ -1288,6 +1335,28 @@ def test(verbose: bool = False) -> None:
1288
1335
  assert roi.subpixel_coordinates[0, 0] == 28357.0
1289
1336
  assert roi.subpixel_coordinates[0, 1] == 42215.0
1290
1337
 
1338
+ # rotated text
1339
+ rois = roiread('tests/text_rotated.roi')
1340
+ assert isinstance(rois, ImagejRoi)
1341
+ roi = rois
1342
+ if verbose:
1343
+ print(roi)
1344
+ assert roi == ImagejRoi.frombytes(roi.tobytes())
1345
+ assert roi.name == 'Rotated'
1346
+ assert roi.roitype == ROI_TYPE.RECT
1347
+ assert roi.subtype == ROI_SUBTYPE.TEXT
1348
+ assert roi.version == 228
1349
+ assert (roi.top, roi.left, roi.bottom, roi.right) == (252, 333, 280, 438)
1350
+ assert roi.stroke_color == b'\xff\x00\x00\xff'
1351
+ assert roi.text_size == 20
1352
+ assert roi.text_justification == 1
1353
+ assert roi.text_name == 'SansSerif'
1354
+ assert roi.text == 'Enter text...\n'
1355
+ with open('tests/text_rotated.roi', 'rb') as fh:
1356
+ expected = fh.read()
1357
+ assert roi.tobytes() == expected
1358
+ str(roi)
1359
+
1291
1360
  # read a ROI from a TIFF file
1292
1361
  rois = roiread('tests/IJMetadata.tif')
1293
1362
  assert isinstance(rois, list)
@@ -1,6 +1,6 @@
1
1
  BSD 3-Clause License
2
2
 
3
- Copyright (c) 2020-2023, Christoph Gohlke
3
+ Copyright (c) 2020-2024, Christoph Gohlke
4
4
  All rights reserved.
5
5
 
6
6
  Redistribution and use in source and binary forms, with or without
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: roifile
3
- Version: 2023.8.30
3
+ Version: 2024.3.20
4
4
  Summary: Read and write ImageJ ROI format
5
5
  Home-page: https://www.cgohlke.com
6
6
  Author: Christoph Gohlke
@@ -38,7 +38,7 @@ interest, geometric shapes, paths, text, and whatnot for image overlays.
38
38
 
39
39
  :Author: `Christoph Gohlke <https://www.cgohlke.com>`_
40
40
  :License: BSD 3-Clause
41
- :Version: 2023.8.30
41
+ :Version: 2024.3.20
42
42
  :DOI: `10.5281/zenodo.6941603 <https://doi.org/10.5281/zenodo.6941603>`_
43
43
 
44
44
  Quickstart
@@ -47,7 +47,7 @@ Quickstart
47
47
  Install the roifile package and all dependencies from the
48
48
  `Python Package Index <https://pypi.org/project/roifile/>`_::
49
49
 
50
- python -m pip install -U roifile[all]
50
+ python -m pip install -U "roifile[all]"
51
51
 
52
52
  View overlays stored in a ROI, ZIP, or TIFF file::
53
53
 
@@ -64,14 +64,25 @@ Requirements
64
64
  This revision was tested with the following requirements and dependencies
65
65
  (other versions may work):
66
66
 
67
- - `CPython <https://www.python.org>`_ 3.9.13, 3.10.11, 3.11.5, 3.12rc
68
- - `Numpy <https://pypi.org/project/numpy/>`_ 1.25.2
69
- - `Tifffile <https://pypi.org/project/tifffile/>`_ 2023.8.30 (optional)
70
- - `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.7.2 (optional)
67
+ - `CPython <https://www.python.org>`_ 3.9.13, 3.10.11, 3.11.8, 3.12.2
68
+ - `Numpy <https://pypi.org/project/numpy/>`_ 1.26.4
69
+ - `Tifffile <https://pypi.org/project/tifffile/>`_ 2024.2.12 (optional)
70
+ - `Matplotlib <https://pypi.org/project/matplotlib/>`_ 3.8.3 (optional)
71
71
 
72
72
  Revisions
73
73
  ---------
74
74
 
75
+ 2024.3.20
76
+
77
+ - Fix writing generator of ROIs (#9).
78
+
79
+ 2024.1.10
80
+
81
+ - Support text rotation.
82
+ - Improve text rendering.
83
+ - Avoid array copies.
84
+ - Limit size read from files.
85
+
75
86
  2023.8.30
76
87
 
77
88
  - Fix linting issues.
@@ -94,47 +105,9 @@ Revisions
94
105
 
95
106
  2022.7.29
96
107
 
97
- - Update metadata.
98
-
99
- 2022.3.18
100
-
101
- - Fix creating ROIs from float coordinates exceeding int16 range (#7).
102
- - Fix bottom-right bounds in ImagejRoi.frompoints.
103
-
104
- 2022.2.2
105
-
106
- - Add type hints.
107
- - Change ImagejRoi to dataclass.
108
- - Drop support for Python 3.7 and numpy < 1.19 (NEP29).
109
-
110
- 2021.6.6
111
-
112
- - Add enums for point types and sizes.
113
-
114
- 2020.11.28
115
-
116
- - Support group attribute.
117
- - Add roiread and roiwrite functions (#3).
118
- - Use UUID as default name of ROI in ImagejRoi.frompoints (#2).
119
-
120
- 2020.8.13
121
-
122
- - Support writing to ZIP file.
123
- - Support os.PathLike file names.
124
-
125
- 2020.5.28
126
-
127
- - Fix int32 to hex color conversion.
128
- - Fix coordinates of closing path.
129
- - Fix reading TIFF files with no overlays.
130
-
131
- 2020.5.1
132
-
133
- - Split positions from counters.
134
-
135
- 2020.2.12
108
+ -
136
109
 
137
- - Initial release.
110
+ Refer to the CHANGES file for older revisions.
138
111
 
139
112
  Notes
140
113
  -----
@@ -0,0 +1,10 @@
1
+ roifile/__init__.py,sha256=leII9J_JWVpi0O9sAnG8s4SpcdwdqEeisFEFsnmN56c,101
2
+ roifile/__main__.py,sha256=Mfn3wm-4cRRChIRonbEcZZT7eRX6RFlS31S8wS1JAVM,132
3
+ roifile/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ roifile/roifile.py,sha256=E2-7K86Bk28V7OL75dNDjxfVF3CPaHfBVpt1mU3MXN4,47036
5
+ roifile-2024.3.20.dist-info/LICENSE,sha256=4bDcZTEFGF584Dw8M-07T5qfat7bsrfr8uuffsbFJYU,1559
6
+ roifile-2024.3.20.dist-info/METADATA,sha256=qqhZMep2Kjp_DDjC3HYC6tZhsylVxgXguyTGpf92XAc,4566
7
+ roifile-2024.3.20.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
8
+ roifile-2024.3.20.dist-info/entry_points.txt,sha256=xP8cwEUbAUeROLXNRanJnAIl13tagbjSSDGfVWf2vh0,49
9
+ roifile-2024.3.20.dist-info/top_level.txt,sha256=QlfLomxPxuYNU0TTR7MXoVBAEAXCj2WJyKvoCJxNwek,8
10
+ roifile-2024.3.20.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- roifile/__init__.py,sha256=leII9J_JWVpi0O9sAnG8s4SpcdwdqEeisFEFsnmN56c,101
2
- roifile/__main__.py,sha256=Mfn3wm-4cRRChIRonbEcZZT7eRX6RFlS31S8wS1JAVM,132
3
- roifile/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- roifile/roifile.py,sha256=2pyfow3BNUF-S3tr6M5jwxTFQ4vfVDQ0v9rl7bleMRk,44238
5
- roifile-2023.8.30.dist-info/LICENSE,sha256=TZQ7rylWoUOIsKyBDs85pF-6cpBRh_-A6ssn1nPBHos,1559
6
- roifile-2023.8.30.dist-info/METADATA,sha256=LKItVCq1LTaZYUnKflTG839ctXzrH9dYVJo-BCBEncE,5119
7
- roifile-2023.8.30.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
8
- roifile-2023.8.30.dist-info/entry_points.txt,sha256=xP8cwEUbAUeROLXNRanJnAIl13tagbjSSDGfVWf2vh0,49
9
- roifile-2023.8.30.dist-info/top_level.txt,sha256=QlfLomxPxuYNU0TTR7MXoVBAEAXCj2WJyKvoCJxNwek,8
10
- roifile-2023.8.30.dist-info/RECORD,,