コード例 #1
0
    def __init__(self,
                 parent=None,
                 cursor: AnimatedCursor = None,
                 frame=0,
                 *args,
                 **kwargs):
        super().__init__(parent, *args, **kwargs)
        self._frame = 0
        self._frame_img = None
        self._pressed = False

        if (cursor is None) or (len(cursor) == 0):
            tmp_img = Image.fromarray(
                np.zeros(self.VIEW_SIZE + (4, ), dtype=np.uint8))
            tmp_cursor = Cursor([CursorIcon(tmp_img, 0, 0)])
            self._cursor = AnimatedCursor([tmp_cursor], [100])
        else:
            self._cursor = cursor
            self._cursor.normalize([self.VIEW_SIZE])

        self.__painter = QtGui.QPainter()
        self.frame = frame
        self.setCursor(QtCore.Qt.CrossCursor)

        self.setMinimumSize(QtCore.QSize(*self.VIEW_SIZE))
        self.setSizePolicy(QtWidgets.QSizePolicy.Fixed,
                           QtWidgets.QSizePolicy.Fixed)
        self.resize(self.minimumSize())
コード例 #2
0
    def read(cls, cur_file: BinaryIO) -> AnimatedCursor:
        """
        Read a windows .ani file from disk to an AnimatedCursor.

        :param cur_file: The file buffer pointer to the windows .ani data.
        :return: An AnimatedCursor object storing all cursor info.
        """
        magic_header = cur_file.read(12)

        if (not cls.check(magic_header)):
            raise SyntaxError("Not a .ani file!")

        ani_data: Dict[str, Any] = {"header": None}

        for chunk_id, chunk_len, chunk_data in read_chunks(
                cur_file, {b"fram"}, {b"LIST"}):
            if (chunk_id in cls.CHUNKS):
                cls.CHUNKS[chunk_id](ani_data["header"], chunk_data, ani_data)

        ani_cur = AnimatedCursor()

        for idx, rate in zip(ani_data["seq"], ani_data["rate"]):
            # We have to convert the rate to milliseconds. Normally stored in jiffies(1/60ths of a second)
            ani_cur.append((ani_data["list"][idx], int((rate * 1000) / 60)))

        return ani_cur
コード例 #3
0
    def __unify_frames(
        cls, cur: AnimatedCursor
    ) -> Tuple[int, float, Vec2D, Vec2D, List[Image.Image]]:
        """
        Private method, takes a cursor and converts it into a vertically tiled image with a single delay and single
        hotspot. Uses some hacky shenanigans to do this. :)......

        :param cur: An AnimatedCursor
        :return: A tuple with:
                    - Integer: Number of frames
                    - Float: Delay in seconds for each frame.
                    - Vec2D: Hotspot location.
                    - Vec2D: Size of image
                    - List[PIL.Image]: Image representations, at 1x, 2x, and 5x...
        """
        cur = cur.copy()

        delays = np.array([delay for sub_cur, delay in cur])
        cumulative_delay = np.cumsum(delays)
        half_avg = int(np.mean(delays) / 4)
        gcd_of_em = np.gcd.reduce(delays)

        unified_delay = max(gcd_of_em, half_avg)
        num_frames = cumulative_delay[-1] // unified_delay

        cur.restrict_to_sizes(cls.CURSOR_EXPORT_SIZES)
        new_images = [
            Image.new("RGBA", (size[0] * 2, (size[1] * 2) * num_frames),
                      (0, 0, 0, 0)) for size in cls.CURSOR_EXPORT_SIZES
        ]

        next_ani_frame = 1
        for current_out_frame in range(num_frames):
            time_in = current_out_frame * unified_delay
            while (next_ani_frame < len(cumulative_delay)) and (
                    time_in >= cumulative_delay[next_ani_frame]):
                next_ani_frame += 1

            for i, img in enumerate(new_images):
                current_size = cls.CURSOR_EXPORT_SIZES[i]
                current_cur = cur[next_ani_frame - 1][0][current_size]
                x_off = current_size[0] - current_cur.hotspot[0]
                y_off = ((current_size[1] * 2) * current_out_frame) + (
                    current_size[1] - current_cur.hotspot[1])
                img.paste(current_cur.image, (x_off, y_off))

        final_hotspot = (cls.CURSOR_EXPORT_SIZES[0][0],
                         cls.CURSOR_EXPORT_SIZES[0][1])
        final_dims = (
            cls.CURSOR_EXPORT_SIZES[0][0] * 2,
            cls.CURSOR_EXPORT_SIZES[0][1] * 2,
        )

        return (
            int(num_frames),
            float(unified_delay) / 1000,
            final_hotspot,
            final_dims,
            new_images,
        )
コード例 #4
0
ファイル: cursor_util.py プロジェクト: Terafugia/CursorCreate
def load_cursor_from_image(file: BinaryIO) -> AnimatedCursor:
    """
    Load a cursor from an image. Image is expected to be square. If it is not square, this method will slide
    horizontally across the image using a height * height square, loading each following square as next frame of the
    cursor with a delay of 100 and hotspots of (0, 0). Note this method supports all formats supported by the
    PIL or pillow Image library. If the image passed is an animated image format like .gif or .apng, this method
    will avoid loading in horizontal square tiles and will rather load in each frame of the image as each frame of
    the cursor.

    :param file: The file handler pointing to the image data.
    :return: An AnimatedCursor object, representing an animated cursor. Static cursors will only have 1 frame.
    """
    image: Image.Image = Image.open(file)

    if (hasattr(image, "is_animated") and (image.is_animated)):
        # If this is an animated file, load in each frame as the frames of the cursor (Ex, ".gif")
        min_dim = min(image.size)  # We fit the image to a square...
        images_durations = [(ImageOps.fit(image, (min_dim, min_dim)),
                             frame.info.get("duration", 100))
                            for frame in ImageSequence.Iterator(image)]
    else:
        # Separate all frames (Assumed to be stored horizontally)
        height = image.size[1]
        num_frames = image.size[0] // height

        if (num_frames == 0):
            raise ValueError(
                "Image width is smaller then height so this will load as a 0 frame cursor!!!"
            )

        images_durations = [(image.crop(
            (i * height, 0, i * height + height, height)), 100)
                            for i in range(num_frames)]

    # Now convert images into the cursors, resizing them to match all the default sizes...
    final_cursor = AnimatedCursor()

    for img, delay in images_durations:
        frame = Cursor()

        for size in DEFAULT_SIZES:
            frame.add(CursorIcon(img.resize(size, Image.LANCZOS), 0, 0))

        final_cursor.append((frame, delay))

    return final_cursor
コード例 #5
0
ファイル: theme_util.py プロジェクト: Terafugia/CursorCreate
def _make_image(cursor: AnimatedCursor) -> Image:
    """
    PRIVATE METHOD:
    Make an image from a cursor, representing all of it's frames. Used by save_project to make project art files
    when original files can't be found and copied over, as the cursor was loaded from the clip board as an image
    or dragged and dropped from the internet...

    :param cursor: The cursor to convert to a tiled horizontal image.
    :return: An picture with frames stored horizontally...
    """
    cursor.normalize([(128, 128)])
    im = Image.new("RGBA", (128 * len(cursor), 128), (0, 0, 0, 0))

    for i, (cursor, delay) in enumerate(cursor):
        im.paste(cursor[(128, 128)].image, (128 * i, 0))

    return im
コード例 #6
0
ファイル: cursor_util.py プロジェクト: Terafugia/CursorCreate
def load_cursor_from_cursor(file: BinaryIO) -> AnimatedCursor:
    """
    Loads a cursor from one of the supported cursor formats implemented in this library. Normalizes sizes and
    removes non-square versions of the cursor...

    :param file: The file handler pointing to the SVG data.
    :return: An AnimatedCursor object, representing an animated cursor. Static cursors will only have 1 frame.
    """
    # Currently supported cursor formats...
    ani_cur_readers = format_core.AnimatedCursorStorageFormat.__subclasses__()
    cur_readers = format_core.CursorStorageFormat.__subclasses__()

    file.seek(0)
    magic = file.read(12)
    file.seek(0)

    for reader in ani_cur_readers:
        if (reader.check(magic)):
            ani_cur = reader.read(file)
            ani_cur.normalize(DEFAULT_SIZES)
            ani_cur.remove_non_square_sizes()
            return ani_cur

    for reader in cur_readers:
        if (reader.check(magic)):
            ani_cur = AnimatedCursor([reader.read(file)], [100])
            ani_cur.normalize(DEFAULT_SIZES)
            ani_cur.remove_non_square_sizes()
            return ani_cur

    raise ValueError("Unsupported cursor format.")
コード例 #7
0
    def read(cls, cur_file: BinaryIO) -> AnimatedCursor:
        """
        Read an xcur or X-Org cursor file from the specified file buffer.

        :param cur_file: The file buffer with xcursor data.
        :return: An AnimatedCursor object, non-animated cursors will contain only 1 frame.
        """
        magic_data = cur_file.read(4)

        cls._assert(cls.check(magic_data), "Not a XCursor File!!!")

        header_size = to_int(cur_file.read(4))
        cls._assert(header_size == cls.HEADER_SIZE,
                    f"Header size is not {cls.HEADER_SIZE}!")
        version = to_int(cur_file.read(4))

        # Number of cursors...
        num_toc = to_int(cur_file.read(4))
        # Used to store cursor offsets per size...
        nominal_sizes = {}

        for i in range(num_toc):
            main_type = to_int(cur_file.read(4))

            if main_type == cls.CURSOR_TYPE:
                nominal_size = to_int(cur_file.read(4))
                offset = to_int(cur_file.read(4))

                if nominal_size not in nominal_sizes:
                    nominal_sizes[nominal_size] = [offset]
                else:
                    nominal_sizes[nominal_size].append(offset)

        max_len = max(len(nominal_sizes[size]) for size in nominal_sizes)
        cursors = []
        delays = []

        for i in range(max_len):
            cursor = Cursor()
            sub_delays = []

            for size, offsets in nominal_sizes.items():
                if i < len(offsets):
                    img, x_hot, y_hot, delay = cls._read_chunk(
                        offsets[i], cur_file, size)
                    cursor.add(CursorIcon(img, x_hot, y_hot))
                    sub_delays.append(delay)

            cursors.append(cursor)
            delays.append(max(sub_delays))

        return AnimatedCursor(cursors, delays)
コード例 #8
0
    def write(cls, cursor: AnimatedCursor, out: BinaryIO):
        """
        Write an AnimatedCursor to the specified file in the X-Org cursor format.

        :param cursor: The AnimatedCursor object to write.
        :param out: The file buffer to write the new X-Org Cursor data to.
        """
        cursor = cursor.copy()
        cursor.normalize()

        if len(cursor) == 0:
            return

        num_curs = sum(len(length) for length, delay in cursor)

        out.write(cls.MAGIC)
        out.write(to_bytes(cls.HEADER_SIZE, 4))
        out.write(to_bytes(cls.VERSION, 4))

        out.write(to_bytes(num_curs, 4))
        # The initial offset...
        offset = num_curs * 12 + cls.HEADER_SIZE

        sorted_sizes = sorted(cursor[0][0])

        # Write the Table of contents [type, subtype(size), offset]
        for size in sorted_sizes:
            for sub_cur, delay in cursor:
                out.write(to_bytes(cls.CURSOR_TYPE, 4))
                out.write(to_bytes(int(size[0] * cls.SIZE_SCALING_FACTOR), 4))
                out.write(to_bytes(offset, 4))
                offset += cls.IMG_CHUNK_H_SIZE + (size[0] * size[1] * 4)

        # Write the actual images...
        for size in sorted_sizes:
            for sub_cur, delay in cursor:
                cls._write_chunk(out, sub_cur[size], delay)
コード例 #9
0
ファイル: cursor_util.py プロジェクト: Terafugia/CursorCreate
def load_cursor_from_svg(file: BinaryIO) -> AnimatedCursor:
    """
    Load a cursor from an SVG. SVG is expected to be square. If it is not square, this method will slide
    horizontally across the SVG using a height * height square, loading each following square as next frame of the
    cursor with a delay of 100 and hotspots of (0, 0). Note this method uses CairoSVG library to load and render
    SVGs of various sizes to bitmaps. For info on supported SVG features, look at the docs for the CairoSVG library.

    :param file: The file handler pointing to the SVG data.
    :return: An AnimatedCursor object, representing an animated cursor. Static cursors will only have 1 frame.
    """
    # Convert SVG in memory to PNG, and read that in with PIL to get the default size of the SVG...
    mem_png = BytesIO()
    cairosvg.svg2png(file_obj=file, write_to=mem_png)
    file.seek(0)
    size_ref_img = Image.open(mem_png)

    # Compute height to width ratio an the number of frame(Assumes they are stored horizontally)...
    h_to_w_multiplier = size_ref_img.size[0] / size_ref_img.size[1]
    num_frames = int(h_to_w_multiplier)

    if (num_frames == 0):
        raise ValueError(
            "Image width is smaller then height so this will load as a 0 frame cursor!!!"
        )

    # Build empty animated cursor to start stashing frames in...
    ani_cur = AnimatedCursor([Cursor() for __ in range(num_frames)],
                             [100] * num_frames)

    for sizes in DEFAULT_SIZES:
        # For each default size, resize svg to it and add all frames to the AnimatedCursor object...
        image = BytesIO()
        cairosvg.svg2png(file_obj=file,
                         write_to=image,
                         output_height=sizes[1],
                         output_width=int(sizes[1] * h_to_w_multiplier))
        file.seek(0)
        image = Image.open(image)

        height = image.size[1]
        for i in range(num_frames):
            ani_cur[i][0].add(
                CursorIcon(
                    image.crop((i * height, 0, i * height + height, height)),
                    0, 0))

    return ani_cur
コード例 #10
0
class CursorHotspotWidget(QtWidgets.QWidget):
    VIEW_SIZE = (64, 64)

    userHotspotChange = Signal((int, int))

    def __init__(self,
                 parent=None,
                 cursor: AnimatedCursor = None,
                 frame=0,
                 *args,
                 **kwargs):
        super().__init__(parent, *args, **kwargs)
        self._frame = 0
        self._frame_img = None
        self._pressed = False

        if (cursor is None) or (len(cursor) == 0):
            tmp_img = Image.fromarray(
                np.zeros(self.VIEW_SIZE + (4, ), dtype=np.uint8))
            tmp_cursor = Cursor([CursorIcon(tmp_img, 0, 0)])
            self._cursor = AnimatedCursor([tmp_cursor], [100])
        else:
            self._cursor = cursor
            self._cursor.normalize([self.VIEW_SIZE])

        self.__painter = QtGui.QPainter()
        self.frame = frame
        self.setCursor(QtCore.Qt.CrossCursor)

        self.setMinimumSize(QtCore.QSize(*self.VIEW_SIZE))
        self.setSizePolicy(QtWidgets.QSizePolicy.Fixed,
                           QtWidgets.QSizePolicy.Fixed)
        self.resize(self.minimumSize())

    def paintEvent(self, event: QtGui.QPaintEvent):
        self.__painter.begin(self)

        self.__painter.setPen(QtGui.QColor("black"))
        self.__painter.setBrush(QtGui.QColor("red"))

        self.__painter.drawPixmap(0, 0, self._frame_img)

        hotspot = QtCore.QPoint(*self.hotspot)

        self.__painter.setPen(QtGui.QColor(0, 0, 0, 150))
        self.__painter.setBrush(QtGui.QColor(255, 0, 0, 100))
        self.__painter.drawEllipse(hotspot, 4, 4)

        self.__painter.setPen(QtGui.QColor(0, 0, 255, 255))
        self.__painter.setBrush(QtGui.QColor(0, 0, 255, 255))
        self.__painter.drawEllipse(hotspot, 1, 1)

        self.__painter.end()

    def sizeHint(self) -> QtCore.QSize:
        return QtCore.QSize(*self.VIEW_SIZE)

    def mousePressEvent(self, event: QtGui.QMouseEvent):
        self._pressed = True

    def mouseMoveEvent(self, event: QtGui.QMouseEvent):
        if self._pressed:
            x, y = event.x(), event.y()
            self.hotspot = x, y
            self.userHotspotChange.emit(x, y)

    def mouseReleaseEvent(self, event: QtGui.QMouseEvent):
        if self._pressed:
            self.mouseMoveEvent(event)
            self._pressed = False

    @property
    def frame(self) -> int:
        return self._frame

    @frame.setter
    def frame(self, value: int):
        if 0 <= value < len(self._cursor):
            self._frame = value
            self._frame_img = toqpixmap(
                self._cursor[self._frame][0][self.VIEW_SIZE].image)
            self.update()
        else:
            raise ValueError(
                f"The frame must land within length of the animated cursor!")

    @property
    def hotspot(self) -> Tuple[int, int]:
        return self._cursor[self._frame][0][self.VIEW_SIZE].hotspot

    @hotspot.setter
    def hotspot(self, value: Tuple[int, int]):
        if not (isinstance(value, Tuple) and len(value) == 2):
            raise ValueError("Not a coordinate pair!")

        value = min(max(0, value[0]),
                    self.VIEW_SIZE[0] - 1), min(max(0, value[1]),
                                                self.VIEW_SIZE[1] - 1)

        for size in self._cursor[self._frame][0]:
            x_rat, y_rat = size[0] / self.VIEW_SIZE[0], size[
                1] / self.VIEW_SIZE[1]
            x_hot, y_hot = int(value[0] * x_rat), int(value[1] * y_rat)

            self._cursor[self._frame][0][size].hotspot = x_hot, y_hot

        self.update()

    @property
    def current_cursor(self) -> AnimatedCursor:
        return self._cursor