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)
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 i 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
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
def read(cls, cur_file: BinaryIO) -> Cursor: """ Read a cursor file in the windows .cur format. :param cur_file: The file or file-like object to read from. :return: A Cursor """ magic_header = cur_file.read(4) if (not cls.check(magic_header)): raise SyntaxError("Not a Cur Type File!!!") is_ico = (magic_header == cls.ICO_MAGIC) # Dump the file with the ico header to allow to be read by Pillow Ico reader... data = BytesIO() data.write(cls.ICO_MAGIC) data.write(cur_file.read()) data.seek(0) ico_img = IcoFile(data) cursor = Cursor() for head in ico_img.entry: width = head["width"] height = head["height"] x_hot = 0 if (is_ico) else head["planes"] y_hot = 0 if (is_ico) else head["bpp"] # Check that hotspots are valid... if (not (0 <= x_hot < width)): x_hot = 0 if (not (0 <= y_hot < height)): y_hot = 0 image = ico_img.getimage((width, height)) cursor.add(CursorIcon(image, x_hot, y_hot)) return cursor
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)): self._cursor = AnimatedCursor( [Cursor([CursorIcon(Image.fromarray(np.zeros(self.VIEW_SIZE + (4,), dtype=np.uint8)), 0, 0)])], [100] ) else: self._cursor = cursor self._cursor.normalize([self.VIEW_SIZE]) self.__painter = QtGui.QPainter() self.frame = frame self.setCursor(QtGui.Qt.CrossCursor) self.setMinimumSize(QtCore.QSize(*self.VIEW_SIZE)) self.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) self.resize(self.minimumSize())
def _icon_chunk(header: Dict[str, Any], data: bytes, data_out: Dict[str, Any]): """ Represents .ani icon chunk, which has an identifier of "icon". """ if (header is None): raise SyntaxError("icon chunk became before header!") if (header["is_in_ico"]): # Cursors are stored as either .cur or .ico, use CurFormat to read them... cursor = CurFormat.read(BytesIO(data)) else: # BMP format, load in and then correct the height... c_icon = CursorIcon(BmpImagePlugin.DibImageFile(BytesIO(data)), 0, 0) c_icon.image._size = (c_icon.image.size[0], c_icon.image.size[1] // 2) d, e, o, a = c_icon.image.tile[0] c_icon.image.tile[0] = d, (0, 0) + c_icon.image.size, o, a # Add the image to the cursor list... cursor = Cursor([c_icon]) if ("list" not in data_out): data_out["list"] = [] data_out["list"].append(cursor)