diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 592b301de..e6a745eda 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -209,13 +209,7 @@ class BmpImageFile(ImageFile.ImageFile): else: raise OSError("Unsupported BMP bitfields layout") elif file_info["compression"] == self.RAW: - try: - self.is_cur - except: - self.is_cur = False - - if file_info["bits"] == 32 and self.is_cur: - raw_mode, self.mode = "BGRA", "RGBA" + pass elif file_info["compression"] == self.RLE8: decoder_name = "bmp_rle" else: diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index e96e18d16..f7b73ff6b 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -15,14 +15,103 @@ # # See the README file for information on usage and redistribution. # -from . import BmpImagePlugin, Image +import warnings +from io import BytesIO +from math import ceil, log + +from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin, IcoImagePlugin from ._binary import i16le as i16 from ._binary import i32le as i32 +from ._binary import o8 +from ._binary import o16le as o16 +from ._binary import o32le as o32 # # -------------------------------------------------------------------- +def _save(im, fp, filename): + fp.write(b"\0\0\2\0") + bmp = im.encoderinfo.get("bitmap_format") == "bmp" + sizes = im.encoderinfo.get( + "sizes", + [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], + ) + hotspots = im.encoderinfo.get( + "hotspots", + [(0, 0) for i in range(len(sizes))] + ) + if len(hotspots) != len(sizes): + raise ValueError("Number of hotspots must be equal to number of cursor sizes") + + frames = [] + provided_ims = [im] + im.encoderinfo.get("append_images", []) + width, height = im.size + for size in sorted(set(sizes)): + if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256: + continue + + for provided_im in provided_ims: + if provided_im.size != size: + continue + frames.append(provided_im) + if bmp: + bits = BmpImagePlugin.SAVE[provided_im.mode][1] + bits_used = [bits] + for other_im in provided_ims: + if other_im.size != size: + continue + bits = BmpImagePlugin.SAVE[other_im.mode][1] + if bits not in bits_used: + # Another image has been supplied for this size + # with a different bit depth + frames.append(other_im) + bits_used.append(bits) + break + else: + # TODO: invent a more convenient method for proportional scalings + frame = provided_im.copy() + frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) + frames.append(frame) + fp.write(o16(len(frames))) # idCount(2) + offset = fp.tell() + len(frames) * 16 + for hotspot, frame in zip(hotspots, frames): + width, height = frame.size + # 0 means 256 + fp.write(o8(width if width < 256 else 0)) # bWidth(1) + fp.write(o8(height if height < 256 else 0)) # bHeight(1) + + bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0) + fp.write(o8(colors)) # bColorCount(1) + fp.write(b"\0") # bReserved(1) + fp.write(o16(hotspot[0])) # x_hotspot(2) + fp.write(o16(hotspot[1])) # y_hotspot(2) + + image_io = BytesIO() + if bmp: + frame.save(image_io, "dib") + + if bits != 32: + and_mask = Image.new("1", size) + ImageFile._save( + and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] + ) + else: + frame.save(image_io, "png") + image_io.seek(0) + image_bytes = image_io.read() + if bmp: + image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:] + bytes_len = len(image_bytes) + fp.write(o32(bytes_len)) # dwBytesInRes(4) + fp.write(o32(offset)) # dwImageOffset(4) + current = fp.tell() + fp.seek(offset) + fp.write(image_bytes) + offset = offset + bytes_len + fp.seek(current) + + def _accept(prefix): return prefix[:4] == b"\0\0\2\0" @@ -30,47 +119,69 @@ def _accept(prefix): ## # Image plugin for Windows Cursor files. +class CurFile(IcoImagePlugin.IcoFile): + def __init__(self, buf): + """ + Parse image from file-like object containing cur file data + """ -class CurImageFile(BmpImagePlugin.BmpImageFile): + # check if CUR + s = buf.read(6) + if not _accept(s): + raise SyntaxError("not a CUR file") + + self.buf = buf + self.entry = [] + + # Number of items in file + self.nb_items = i16(s, 4) + + # Get headers for each item + for _ in range(self.nb_items): + s = buf.read(16) + + icon_header = { + "width": s[0], + "height": s[1], + "nb_color": s[2], # No. of colors in image (0 if >=8bpp) + "reserved": s[3], + "x_hotspot": i16(s, 4), + "y_hotspot": i16(s, 6), + "size": i32(s, 8), + "offset": i32(s, 12), + } + + # See Wikipedia + for j in ("width", "height"): + if not icon_header[j]: + icon_header[j] = 256 + + # cursor files have transparency, hence 32 bpp + icon_header["bpp"] = 32 + icon_header["dim"] = (icon_header["width"], icon_header["height"]) + icon_header["square"] = icon_header["width"] * icon_header["height"] + + self.entry.append(icon_header) + + self.entry = sorted(self.entry, key=lambda x: x["square"]) + self.entry.reverse() + + +class CurImageFile(IcoImagePlugin.IcoImageFile): format = "CUR" format_description = "Windows Cursor" def _open(self): - - offset = self.fp.tell() - - # check magic - s = self.fp.read(6) - if not _accept(s): - raise SyntaxError("not a CUR file") - self.is_cur = True - - # pick the largest cursor in the file - m = b"" - for i in range(i16(s, 4)): - s = self.fp.read(16) - if not m: - m = s - elif s[0] > m[0] and s[1] > m[1]: - m = s - if not m: - raise TypeError("No cursors were found") - - # load as bitmap - self._bitmap(i32(m, 12) + offset) - - # patch up the bitmap height - self._size = self.size[0], self.size[1] // 2 - d, e, o, a = self.tile[0] - self.tile[0] = d, (0, 0) + self.size, o, a - - return + self.ico = CurFile(self.fp) + self.info["sizes"] = self.ico.sizes() + self.size = self.ico.entry[0]["dim"] + self.load() # # -------------------------------------------------------------------- Image.register_open(CurImageFile.format, CurImageFile, _accept) - +Image.register_save(CurImageFile.format, _save) Image.register_extension(CurImageFile.format, ".cur")