# roifile.py

# Copyright (c) 2020, 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:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. 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.
#
# 3. Neither the name of the copyright holder nor the names of its
#    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 HOLDER 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.

"""Read and write ImageJ ROI format.

Roifile is a Python library to read, write, create, and plot `ImageJ`_ ROIs,
an undocumented and ImageJ application specific format to store regions of
interest, geometric shapes, paths, text, and whatnot for image overlays.

.. _ImageJ: https://imagej.net

:Author:
  `Christoph Gohlke <https://www.lfd.uci.edu/~gohlke/>`_

:Organization:
  Laboratory for Fluorescence Dynamics, University of California, Irvine

:License: BSD 3-Clause

:Version: 2020.5.1

Requirements
------------
* `CPython >= 3.6 <https://www.python.org>`_
* `Numpy 1.15.1 <https://www.numpy.org>`_
* `Tifffile 2020.2.16 <https://pypi.org/project/tifffile/>`_  (optional)
* `Matplotlib 3.1 <https://pypi.org/project/matplotlib/>`_  (optional)

Revisions
---------
2020.5.1
    Split positions from counters.
2020.2.12
    Initial release.

Notes
-----

Other Python packages handling ImageJ ROIs:

* `ijpython_roi <https://github.com/dwaithe/ijpython_roi>`_
* `imagej-tiff-meta <https://github.com/csachs/imagej-tiff-meta>`_
* `read-roi <https://github.com/hadim/read-roi/>`_

Examples
--------

Create a new ImagejRoi instance from an array of x, y coordinates:

>>> roi = ImagejRoi.frompoints([[1.1, 2.2], [3.3, 4.4], [5.4, 6.6]])
>>> roi.coordinates()
array([[1.1, 2.2],
       [3.3, 4.4],
       [5.4, 6.6]], dtype=float32)
>>> roi.left, roi.left, roi.right, roi.bottom
(1, 1, 5, 6)

Export the instance to an ImageJ ROI formatted bytes or file:

>>> out = roi.tobytes()
>>> out[:4]
b'Iout'
>>> roi.tofile('_test.roi')

Read the ImageJ ROI from the file:

>>> roi2 = ImagejRoi.fromfile('_test.roi')
>>> roi2 == roi
True

Plot the ROI using matplotlib:

>>> roi.plot()

To view the overlays stored in a ROI, ZIP, or TIFF file from a command line,
run ``python -m roifile _test.roi``.

"""

__version__ = '2020.5.1'

__all__ = ('ImagejRoi', 'ROI_TYPE',  'ROI_SUBTYPE', 'ROI_OPTIONS')

import sys
import enum
import struct

import numpy


class ROI_TYPE(enum.IntEnum):
    POLYGON = 0
    RECT = 1
    OVAL = 2
    LINE = 3
    FREELINE = 4
    POLYLINE = 5
    NOROI = 6
    FREEHAND = 7
    TRACED = 8
    ANGLE = 9
    POINT = 10


class ROI_SUBTYPE(enum.IntEnum):
    UNDEFINED = 0
    TEXT = 1
    ARROW = 2
    ELLIPSE = 3
    IMAGE = 4
    ROTATED_RECT = 5


class ROI_OPTIONS(enum.IntFlag):
    SPLINE_FIT = 1
    DOUBLE_HEADED = 2
    OUTLINE = 4
    OVERLAY_LABELS = 8
    OVERLAY_NAMES = 16
    OVERLAY_BACKGROUNDS = 32
    OVERLAY_BOLD = 64
    SUB_PIXEL_RESOLUTION = 128
    DRAW_OFFSET = 256
    ZERO_TRANSPARENT = 512
    SHOW_LABELS = 1024
    SCALE_LABELS = 2048
    PROMPT_BEFORE_DELETING = 4096


ROI_COLOR_NONE = b'\x00\x00\x00\x00'


class ImagejRoi:
    """Read and write ImageJ ROI format."""

    byteorder = '>'
    roitype = ROI_TYPE(0)
    subtype = ROI_SUBTYPE(0)
    options = ROI_OPTIONS(0)
    name = ''
    props = ''
    version = 217
    top = 0
    left = 0
    bottom = 0
    right = 0
    n_coordinates = 0
    stroke_width = 0
    shape_roi_size = 0
    stroke_color = ROI_COLOR_NONE
    fill_color = ROI_COLOR_NONE
    arrow_style_or_aspect_ratio = 0
    arrow_head_size = 0
    rounded_rect_arc_size = 0
    position = 0
    c_position = 0
    z_position = 0
    t_position = 0
    x1 = 0
    y1 = 0
    x2 = 0
    y2 = 0
    xd = 0.0
    yd = 0.0
    widthd = 0.0
    heightd = 0.0
    overlay_label_color = ROI_COLOR_NONE
    overlay_font_size = 0
    image_opacity = 0
    image_size = 0
    float_stroke_width = 0.0
    integer_coordinates = None
    subpixel_coordinates = None
    multi_coordinates = None
    counters = None
    counter_positions = None  # flat indices into TZC array
    text_size = 0
    text_style = 0
    text_justification = 0
    text_name = ''
    text = ''

    @classmethod
    def frompoints(cls, points=None, name=None, position=None, index=None,
                   c=None, z=None, t=None):
        """Return ImagejRoi instance from sequence of Point coordinates."""
        if points is None:
            return None

        self = cls()
        self.version = 226
        self.roitype = ROI_TYPE.FREEHAND
        self.options = (
            ROI_OPTIONS.OVERLAY_BACKGROUNDS |
            ROI_OPTIONS.OVERLAY_LABELS
        )
        if position is not None:
            self.position = position + 1
        if c is not None:
            self.c_position = c + 1
        if z is not None:
            self.z_position = z + 1
        if t is not None:
            self.t_position = t + 1
        if name is None:
            if index is None:
                index = numpy.random.randint(0, 2**31 - 1)
                name = f'F{self.t_position:02}-{index:x}'
            else:
                name = f'F{self.t_position:02}-C{index}'
        self.name = name

        points = numpy.array(points, copy=True)
        if points.dtype.kind == 'f':
            points = numpy.array(points, dtype='f4')
            self.options |= ROI_OPTIONS.SUB_PIXEL_RESOLUTION
        else:
            points = numpy.array(points, dtype='i4')

        left, top = points.min(axis=0)
        right, bottom = points.max(axis=0)

        if self.subpixelresolution:
            self.integer_coordinates = numpy.array(
                points - [int(left), int(top)], dtype='i4'
            )
            self.subpixel_coordinates = points
        else:
            points -= [int(left), int(top)]
            self.integer_coordinates = points

        self.n_coordinates = len(self.integer_coordinates)
        self.left = int(left)
        self.top = int(top)
        self.right = int(right)
        self.bottom = int(bottom)

        return self

    @classmethod
    def fromfile(cls, filename):
        """Return ImagejRoi instance from ROI, ZIP, or TIFF file.

        For ZIP or TIFF files, return a list of ImagejRoi.

        """
        if filename[-4:].lower() == '.tif':
            import tifffile
            with tifffile.TiffFile(filename) as tif:
                if not tif.is_imagej:
                    raise ValueError('file does not contain ImagejRoi')
                if 'Overlays' in tif.imagej_metadata:
                    overlays = tif.imagej_metadata['Overlays']
                    if isinstance(overlays, list):
                        rois = overlays
                    else:
                        rois = [overlays]
                if 'ROI' in tif.imagej_metadata:
                    roi = tif.imagej_metadata['ROI']
                    if isinstance(roi, list):
                        overlays.extend(roi)
                    else:
                        rois.append(roi)
                return [cls.frombytes(roi) for roi in rois]

        if filename[-4:].lower() == '.zip':
            import zipfile
            with zipfile.ZipFile(filename) as zf:
                return [cls.frombytes(zf.open(name).read())
                        for name in zf.namelist()]

        with open(filename, 'rb') as fh:
            data = fh.read()
        return cls.frombytes(data)

    @classmethod
    def frombytes(cls, data):
        """Return ImagejRoi instance from bytes.

        """
        if data[:4] != b'Iout':
            raise ValueError('not an ImageJ ROI')

        self = cls()

        (
            self.version,
            roitype,
            self.top,
            self.left,
            self.bottom,
            self.right,
            self.n_coordinates,
            # skip 16 bytes: x1,y1,x2,y2 or x,y,width,height or size
            self.stroke_width,
            self.shape_roi_size,
            self.stroke_color,
            self.fill_color,
            subtype,
            options,
            self.arrow_style_or_aspect_ratio,
            self.arrow_head_size,
            self.rounded_rect_arc_size,
            self.position,
            header2_offset,
        ) = struct.unpack(
            self.byteorder + 'hBxhhhhH16xhi4s4shhBBhii', data[4:64]
        )

        self.roitype = ROI_TYPE(roitype)
        self.subtype = ROI_SUBTYPE(subtype)
        self.options = ROI_OPTIONS(options)

        if self.subpixelrect:
            (
                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 and (
                self.subtype == ROI_SUBTYPE.ELLIPSE or
                self.subtype == ROI_SUBTYPE.ROTATED_RECT)
        ):
            (
                self.x1,
                self.y1,
                self.x2,
                self.y2
            ) = struct.unpack(self.byteorder + 'ffff', data[18:34])
        elif self.n_coordinates == 0:
            self.n_coordinates = struct.unpack(
                self.byteorder + 'i', data[18:22])[0]

        if 0 < header2_offset < len(data) - 52:
            (
                self.c_position,
                self.z_position,
                self.t_position,
                name_offset,
                name_length,
                self.overlay_label_color,
                self.overlay_font_size,
                available_byte1,
                self.image_opacity,
                self.image_size,
                self.float_stroke_width,
                roi_props_offset,
                roi_props_length,
                counters_offset
            ) = struct.unpack(self.byteorder + '4xiiiii4shBBifiii',
                              data[header2_offset: header2_offset+52])

            if name_offset > 0 and name_length > 0:
                name = data[name_offset: name_offset+name_length*2]
                self.name = name.decode(self.utf16)

            if roi_props_offset > 0 and roi_props_length > 0:
                props = data[roi_props_offset: name_offset+roi_props_length*2]
                self.props = props.decode(self.utf16)

            if counters_offset > 0:
                counters = numpy.ndarray(
                    shape=self.n_coordinates,
                    dtype=self.byteorder + 'u4',
                    buffer=data,
                    offset=counters_offset
                    )
                self.counters = (counters & 0xFF).astype('u1')
                self.counter_positions = counters >> 8

        if self.version >= 218 and self.subtype == ROI_SUBTYPE.TEXT:
            (
                self.text_size,
                style_and_justification,
                name_length,
                text_length,
            ) = struct.unpack(self.byteorder + 'iiii', data[64:80])
            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)
            off += name_length * 2
            self.text = data[off: off+text_length*2].decode(self.utf16)

        elif self.roitype in (
            ROI_TYPE.POLYGON,
            ROI_TYPE.FREEHAND,
            ROI_TYPE.TRACED,
            ROI_TYPE.POLYLINE,
            ROI_TYPE.FREELINE,
            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'
                ).copy()

            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()

        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()

        elif self.roitype not in (ROI_TYPE.RECT, ROI_TYPE.LINE, ROI_TYPE.OVAL):
            log_warning(f'cannot handle ImagejRoi type {self.roitype!r}')

        return self

    def tofile(self, filename):
        """Write ImagejRoi to a new ROI file."""
        with open(filename, 'wb') as fh:
            fh.write(self.tobytes())

    def tobytes(self):
        """Return ImagejRoi as bytes."""
        result = [b'Iout']

        result.append(
            struct.pack(
                self.byteorder + 'hBxhhhhH',
                self.version,
                self.roitype.value,
                self.top,
                self.left,
                self.bottom,
                self.right,
                self.n_coordinates if self.n_coordinates < 2**16 else 0
            ))

        if self.subpixelrect:
            result.append(
                struct.pack(
                    self.byteorder + 'ffff',
                    self.xd,
                    self.yd,
                    self.widthd,
                    self.heightd
                ))
        elif (
            self.roitype == ROI_TYPE.LINE or
            self.roitype == ROI_TYPE.FREEHAND and (
                self.subtype == ROI_SUBTYPE.ELLIPSE or
                self.subtype == ROI_SUBTYPE.ROTATED_RECT)
        ):
            result.append(
                struct.pack(
                    self.byteorder + 'ffff',
                    self.x1, self.y1, self.x2, self.y2)
            )
        elif self.n_coordinates >= 2**16:
            result.append(struct.pack(
                self.byteorder + 'i12x', self.n_coordinates
            ))
        else:
            result.append(b'\x00' * 16)

        result.append(
            struct.pack(
                self.byteorder + 'hi4s4shhBBhi',
                self.stroke_width,
                self.shape_roi_size,
                self.stroke_color,
                self.fill_color,
                self.subtype.value,
                self.options.value,
                self.arrow_style_or_aspect_ratio,
                self.arrow_head_size,
                self.rounded_rect_arc_size,
                self.position,
            ))

        extradata = b''

        if self.version >= 218 and self.subtype == ROI_SUBTYPE.TEXT:
            style_and_justification = self.text_style
            style_and_justification |= self.text_justification << 8
            extradata = struct.pack(
                self.byteorder + 'iiii',
                self.text_size,
                style_and_justification,
                len(self.text_name),
                len(self.text)
            )
            extradata += self.text_name.encode(self.utf16)
            extradata += self.text.encode(self.utf16)
            extradata += b'\x00' * 4  # ?

        elif self.roitype in (
            ROI_TYPE.POLYGON,
            ROI_TYPE.FREEHAND,
            ROI_TYPE.TRACED,
            ROI_TYPE.POLYLINE,
            ROI_TYPE.FREELINE,
            ROI_TYPE.ANGLE,
            ROI_TYPE.POINT
        ):
            if self.integer_coordinates is not None:
                coord = self.integer_coordinates.astype(self.byteorder + 'i2')
                extradata = coord.tobytes(order='F')
            if self.subpixel_coordinates is not None:
                coord = self.subpixel_coordinates.astype(self.byteorder + 'f4')
                extradata += coord.tobytes(order='F')

        elif self.composite and self.roitype == ROI_TYPE.RECT:
            coord = self.multi_coordinates.astype(self.byteorder + 'f4')
            extradata += coord.tobytes()

        header2_offset = 64 + len(extradata)
        result.append(struct.pack(self.byteorder + 'i', header2_offset))
        result.append(extradata)

        offset = header2_offset + 64
        name_length = len(self.name)
        name_offset = offset if name_length > 0 else 0
        offset += name_length * 2
        roi_props_length = len(self.props)
        roi_props_offset = offset if roi_props_length > 0 else 0
        offset += roi_props_length * 2
        counters_offset = offset if self.counters is not None else 0

        result.append(
            struct.pack(
                self.byteorder + '4xiiiii4shBBifiii12x',
                self.c_position,
                self.z_position,
                self.t_position,
                name_offset,
                name_length,
                self.overlay_label_color,
                self.overlay_font_size,
                0,  # available_byte1,
                self.image_opacity,
                self.image_size,
                self.float_stroke_width,
                roi_props_offset,
                roi_props_length,
                counters_offset,
            ))

        if name_length > 0:
            result.append(self.name.encode(self.utf16))
        if roi_props_length > 0:
            result.append(self.props.encode(self.utf16))
        if self.counters is not None:
            counters = self.counters.astype(self.byteorder + 'u4')
            if self.counter_positions is not None:
                indices = self.counter_positions.astype(self.byteorder + 'u4')
                counters = counters & 0xFF | indices << 8
                counters = counters.astype(self.byteorder + 'u4')
            result.append(counters.tobytes())

        return b''.join(result)

    def plot(self, ax=None, rois=None, title=None, bounds=False, **kwargs):
        """Plot coordinates using matplotlib."""
        roitype = self.roitype
        subtype = self.subtype

        if ax is None:
            from matplotlib import pyplot
            from matplotlib.patches import Rectangle

            fig, ax = pyplot.subplots()
            ax.set_aspect('equal')
            ax.set_facecolor((0.8, 0.8, 0.8))
            if title is None:
                fig.suptitle(f'{self.name!r}')
            else:
                fig.suptitle(title)
            if bounds and rois is None:
                ax.set_title(f'{roitype.name} {subtype.name}')
                ax.add_patch(Rectangle(
                    (self.left, self.top),
                    self.right - self.left,
                    self.bottom - self.top,
                    linewidth=1,
                    edgecolor='0.9',
                    facecolor='none'))
        else:
            fig = None

        if rois is not None:
            for roi in rois:
                roi.plot(ax=ax, **kwargs)
            pyplot.show()
            return

        if kwargs is None:
            kwargs = {}
        if 'color' not in kwargs and 'c' not in kwargs:
            kwargs['color'] = self.hexcolor(self.stroke_color)
        if 'linewidth' not in kwargs and 'lw' not in kwargs:
            if self.float_stroke_width > 0.0:
                kwargs['linewidth'] = self.float_stroke_width
            elif self.stroke_width > 0.0:
                kwargs['linewidth'] = self.stroke_width
        if roitype == ROI_TYPE.POINT:
            if 'marker' not in kwargs:
                kwargs['marker'] = 'x'
            if 'linestyle' not in kwargs:
                kwargs['linestyle'] = ''

        if roitype == ROI_TYPE.LINE and subtype == ROI_SUBTYPE.ARROW:
            line = self.coordinates()
            x, y = line[0]
            dx, dy = line[1] - line[0]
            if 'head_width' not in kwargs and self.arrow_head_size > 0:
                kwargs['head_width'] = self.arrow_head_size
            kwargs['length_includes_head'] = True
            ax.arrow(x, y, dx, dy, **kwargs)
            if self.options & ROI_OPTIONS.DOUBLE_HEADED:
                x, y = line[1]
                ax.arrow(x, y, -dx, -dy, **kwargs)
        elif roitype == ROI_TYPE.RECT and subtype == ROI_SUBTYPE.TEXT:
            point = self.coordinates()[0]
            if 'fontsize' not in kwargs and self.text_size > 0:
                kwargs['fontsize'] = self.text_size
            ax.text(point[0], point[1], self.text, **kwargs)
        else:
            for coords in self.coordinates(multi=True):
                ax.plot(coords[:, 0], coords[:, 1], **kwargs)

        ax.plot(self.left, self.bottom, '')
        ax.plot(self.right, self.top, '')

        if fig is not None:
            pyplot.show()

    def coordinates(self, multi=False):
        """Return x, y coordinates as numpy array for display."""
        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]
        elif self.multi_coordinates is not None:
            coords = self.path2coords(self.multi_coordinates)
            if not multi:
                coords = sorted(coords, key=lambda x: x.size)[-1]
            multi = False
        elif self.roitype == ROI_TYPE.LINE:
            coords = numpy.array(
                [[self.x1, self.y1], [self.x2, self.y2]], 'f4'
            )
        elif self.roitype == ROI_TYPE.OVAL:
            coords = oval([[self.left, self.top], [self.right, self.bottom]])
        elif self.roitype == ROI_TYPE.RECT:
            coords = numpy.array(
                [[self.left, self.top],
                 [self.left, self.bottom],
                 [self.right, self.bottom],
                 [self.right, self.top],
                 [self.left, self.top]], 'f4')
        else:
            coords = numpy.empty((0, 2), dtype=self.byteorder + 'i4')
        return [coords] if multi else coords

    def hexcolor(self, b):
        """Return color (bytes) as hex triplet or None if black."""
        if b == ROI_COLOR_NONE:
            return None
        if self.byteorder == '<':
            return f'#{b[0]:02x}{b[1]:02x}{b[2]:02x}'
        return f'#{b[3]:02x}{b[2]:02x}{b[1]:02x}'

    @staticmethod
    def path2coords(path):
        """Return list of coordinate arrays from 2D geometric path."""
        coordinates = []
        points = []
        n = 0
        m = 0
        while n < len(path):
            op = int(path[n])
            if op == 0:
                # MOVETO
                if n > 0:
                    coordinates.append(numpy.array(points))
                    points = []
                points.append([path[n + 1], path[n + 2]])
                m = len(points)
                n += 3
            elif op == 1:
                # LINETO
                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:
                # QUADTO or CUBICTO
                raise NotImplementedError(
                    f'PathIterator command {op!r} not supported')
            else:
                raise RuntimeError(f'invalid PathIterator command {op!r}')

        coordinates.append(numpy.array(points))
        return coordinates

    @property
    def composite(self):
        return self.shape_roi_size > 0

    @property
    def subpixelresolution(self):
        return (
            self.version >= 222 and
            self.options & ROI_OPTIONS.SUB_PIXEL_RESOLUTION
        )

    @property
    def drawoffset(self):
        return (
            self.subpixelresolution and
            self.options & ROI_OPTIONS.DRAW_OFFSET
        )

    @property
    def subpixelrect(self):
        return (
            self.version >= 223 and
            self.subpixelresolution and
            (
                self.roitype == ROI_TYPE.RECT or
                self.roitype == ROI_TYPE.OVAL
            ))

    @property
    def utf16(self):
        """Return UTF-16 codec depending on byteorder."""
        return 'utf-16' + ('be' if self.byteorder == '>' else 'le')

    def __eq__(self, other):
        """Return True if two ImagejRoi are the same."""
        try:
            return self.tobytes() == other.tobytes()
        except Exception:
            return False

    def __str__(self):
        """Return string with information about ImagejRoi."""
        return '\n'.join(f' {name} = {value!r}'
                         for name, value in self.__dict__.items())


def oval(rect, points=33, dtype='float32'):
    """Return coordinates of oval from rectangle corners."""
    rect = numpy.array(rect, dtype='float32')
    c = numpy.linspace(0.0, 2.0 * numpy.pi, num=points, dtype='float32')
    c = numpy.array([numpy.cos(c), numpy.sin(c)]).T
    r = rect[1] - rect[0]
    r /= 2.0
    c *= r
    c += rect[0] + r
    return c.astype(dtype)


def log_warning(msg, *args, **kwargs):
    """Log message with level WARNING."""
    import logging
    logging.getLogger(__name__).warning(msg, *args, **kwargs)


def test_imagejroi():
    """Test roifile.ImagejRoi class."""
    rois = ImagejRoi.fromfile('tests/ijzip.zip')
    assert len(rois) == 7
    for roi in rois:
        assert roi.tobytes() == ImagejRoi.frombytes(roi.tobytes()).tobytes()
        roi.coordinates()
        roi.__str__()

    roi = ImagejRoi.fromfile('tests/box_combined.roi')
    assert roi.tobytes() == ImagejRoi.frombytes(roi.tobytes()).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] == 588.0
    assert roi.multi_coordinates[0] == 0.0
    roi.__str__()

    for roi in ImagejRoi.fromfile('tests/IJMetadata.tif'):
        assert roi == ImagejRoi.frombytes(roi.tobytes())
        roi.coordinates()
        roi.__str__()

    assert ImagejRoi() == ImagejRoi()


def main(argv=None, test=False):
    """Roifile command line usage main function.

    Show all ImageJ ROIs in file or all files in directory:

    ``python -m roifile file_or_directory``

    """
    import os
    from glob import glob

    if argv is None:
        argv = sys.argv

    if len(argv) > 1 and '--test' in argv:
        if os.path.exists('../tests'):
            os.chdir('../')
        import doctest
        try:
            import roifile.roifile as m
        except ImportError:
            m = None
        if os.path.exists('tests'):
            print('running tests')
            test_imagejroi()
        print('running doctests')
        doctest.testmod(m)
        return

    if len(argv) == 1:
        files = glob('*.roi')
    elif '*' in argv[1]:
        files = glob(argv[1])
    elif os.path.isdir(argv[1]):
        files = glob(f'{argv[1]}/*.roi')
    else:
        files = argv[1:]

    for fname in files:
        print(fname)
        try:
            rois = ImagejRoi.fromfile(fname)
            title = os.path.split(fname)[-1]
            if isinstance(rois, list):
                for roi in rois:
                    print(roi)
                    print()
                rois[0].plot(rois=rois, title=title)
            else:
                print(rois)
                print()
                rois.plot(title=title)
        except ValueError as exc:
            # raise  # enable for debugging
            print(fname, exc)
            continue


if __name__ == '__main__':
    sys.exit(main())
