diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 9f755e8c6..9086c9b0e 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -672,8 +672,7 @@ class TestFileJpeg: img.save(temp_file, convert_mode=True, fill_color="red") with Image.open(temp_file) as reloaded: - with Image.open("Tests/images/pil123rgba_red.jpg") as target: - assert_image_similar(reloaded, target, 4) + assert_image_similar_tofile(reloaded, "Tests/images/pil123rgba_red.jpg", 4) def test_save_tiff_with_dpi(self, tmp_path): # Arrange diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 339580bde..e87cc8503 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -15,8 +15,6 @@ from .helper import ( skip_unless_feature, ) -from io import BytesIO - try: from PIL import _webp @@ -91,7 +89,7 @@ class TestFileWebp: assert_image_similar(image, target, epsilon) def test_save_convert_mode(self): - out = BytesIO() + out = io.BytesIO() for mode in ["CMYK", "I", "L", "LA", "P"]: img = Image.new(mode, (20, 20)) img.save(out, "WEBP", convert_mode=True) diff --git a/Tests/test_image.py b/Tests/test_image.py index 91ecd3c4f..b9d9d7451 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,13 +7,7 @@ import warnings import pytest -from PIL import ( - Image, - ImageDraw, - ImagePalette, - TiffImagePlugin, - UnidentifiedImageError, -) +from PIL import Image, ImageDraw, ImagePalette, TiffImagePlugin, UnidentifiedImageError from .helper import ( assert_image_equal, @@ -138,8 +132,6 @@ class TestImage: im.size = (3, 4) def test_invalid_image(self): - import io - im = io.BytesIO(b"") with pytest.raises(UnidentifiedImageError): with Image.open(im): @@ -430,14 +422,67 @@ class TestImage: for ext in [".cur", ".icns", ".tif", ".tiff"]: assert ext in extensions - def test_no_convert_mode(self, tmp_path): - assert not hasattr(TiffImagePlugin, "_convert_mode") + def test_supported_modes(self): + for format in Image.MIME.keys(): + try: + save_handler = Image.SAVE[format] + except KeyError: + continue + plugin = sys.modules[save_handler.__module__] + if not hasattr(plugin, "_supported_modes"): + continue + + # Check that the supported modes list is accurate + supported_modes = plugin._supported_modes() + for mode in [ + "1", + "L", + "P", + "RGB", + "RGBA", + "CMYK", + "YCbCr", + "LAB", + "HSV", + "I", + "F", + "LA", + "La", + "RGBX", + "RGBa", + ]: + out = io.BytesIO() + im = Image.new(mode, (100, 100)) + if mode in supported_modes: + im.save(out, format) + else: + with pytest.raises(Exception): + im.save(out, format) + + def test_no_supported_modes_method(self, tmp_path): + assert not hasattr(TiffImagePlugin, "_supported_modes") temp_file = str(tmp_path / "temp.tiff") im = hopper() im.save(temp_file, convert_mode=True) + def test_convert_mode(self): + for mode, modes in [["P", []], ["P", ["P"]]]: # no modes, same mode + im = Image.new(mode, (100, 100)) + assert im._convert_mode(modes) is None + + for mode, modes in [ + ["P", ["RGB"]], + ["P", ["L"]], # converting to a non-preferred mode + ["LA", ["P"]], + ["I", ["L"]], + ["RGB", ["L"]], + ["RGB", ["CMYK"]], + ]: + im = Image.new(mode, (100, 100)) + assert im._convert_mode(modes) is not None + def test_effect_mandelbrot(self): # Arrange size = (512, 512) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 4de387509..e1d2e82cb 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -1022,11 +1022,8 @@ def getdata(im, offset=(0, 0), **params): return fp.data -def _convert_mode(im): - return { - 'LA':'P', - 'CMYK':'RGB' - }.get(im.mode) +def _supported_modes(): + return ["RGB", "RGBA", "P", "I", "F", "LA", "L", "1"] # -------------------------------------------------------------------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 266044d49..ffe6df0e3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2277,16 +2277,18 @@ class Image: if format.upper() not in SAVE: init() - if params.pop('save_all', False): + if params.pop("save_all", False): save_handler = SAVE_ALL[format.upper()] else: save_handler = SAVE[format.upper()] - if params.get('convert_mode'): + if params.get("convert_mode"): plugin = sys.modules[save_handler.__module__] - converted_im = self._convert_mode(plugin, params) - if converted_im: - return converted_im.save(fp, format, **params) + if hasattr(plugin, "_supported_modes"): + modes = plugin._supported_modes() + converted_im = self._convert_mode(modes, params) + if converted_im: + return converted_im.save(fp, format, **params) self.encoderinfo = params self.encoderconfig = () @@ -2315,32 +2317,57 @@ class Image: if open_fp: fp.close() - def _convert_mode(self, plugin, params): - if not hasattr(plugin, '_convert_mode'): + def _convert_mode(self, modes, params={}): + if not modes or self.mode in modes: return - new_mode = plugin._convert_mode(self) - if self.mode == 'LA' and new_mode == 'P': - alpha = self.getchannel('A') + if self.mode == "P": + preferred_modes = [] + if "A" in self.im.getpalettemode(): + preferred_modes.append("RGBA") + preferred_modes.append("RGB") + else: + preferred_modes = { + "CMYK": ["RGB"], + "RGB": ["CMYK"], + "RGBX": ["RGB"], + "RGBa": ["RGBA", "RGB"], + "RGBA": ["RGB"], + "LA": ["RGBA", "P", "L"], + "La": ["LA", "L"], + "L": ["RGB"], + "F": ["I"], + "I": ["L", "RGB"], + "1": ["L"], + "YCbCr": ["RGB"], + "LAB": ["RGB"], + "HSV": ["RGB"], + }.get(self.mode, []) + for new_mode in preferred_modes: + if new_mode in modes: + break + else: + new_mode = modes[0] + if self.mode == "LA" and new_mode == "P": + alpha = self.getchannel("A") # Convert the image into P mode but only use 255 colors # in the palette out of 256. - im = self.convert('L') \ - .convert('P', palette=Palette.ADAPTIVE, colors=255) + im = self.convert("L").convert("P", palette=Palette.ADAPTIVE, colors=255) # Set all pixel values below 128 to 255, and the rest to 0. mask = eval(alpha, lambda px: 255 if px < 128 else 0) # Paste the color of index 255 and use alpha as a mask. im.paste(255, mask) # The transparency index is 255. - im.info['transparency'] = 255 + im.info["transparency"] = 255 return im - elif self.mode == 'I': - im = self.point([i//256 for i in range(65536)], 'L') - return im.convert(new_mode) if new_mode != 'L' else im + elif self.mode == "I": + im = self.point([i // 256 for i in range(65536)], "L") + return im.convert(new_mode) if new_mode != "L" else im - elif self.mode in ('RGBA', 'LA') and new_mode in ('RGB', 'L'): - fill_color = params.get('fill_color', 'white') + elif self.mode in ("RGBA", "LA") and new_mode in ("RGB", "L"): + fill_color = params.get("fill_color", "white") background = new(new_mode, self.size, fill_color) - background.paste(self, self.getchannel('A')) + background.paste(self, self.getchannel("A")) return background elif new_mode: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 618b9996a..ebc56ebf3 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -819,15 +819,8 @@ def jpeg_factory(fp=None, filename=None): return im -def _convert_mode(im): - mode = im.mode - if mode == 'P': - return 'RGBA' if 'A' in im.im.getpalettemode() else 'RGB' - return { - 'RGBA':'RGB', - 'LA':'L', - 'I':'L' - }.get(mode) +def _supported_modes(): + return ["RGB", "CMYK", "YCbCr", "RGBX", "L", "1"] # --------------------------------------------------------------------- diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 740727bbb..4c3e7143b 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1420,10 +1420,8 @@ def getchunks(im, **params): return fp.data -def _convert_mode(im): - return { - 'CMYK':'RGB' - }.get(im.mode) +def _supported_modes(): + return ["RGB", "RGBA", "P", "I", "LA", "L", "1"] # -------------------------------------------------------------------- diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index d81bc99a4..665896f55 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -344,17 +344,22 @@ def _save(im, fp, filename): fp.write(data) -def _convert_mode(im): - mode = im.mode - if mode == 'P': - return 'RGBA' if 'A' in im.im.getpalettemode() else 'RGB' - return { - # Pillow doesn't support L modes for webp for now. - 'L':'RGB', - 'LA':'RGBA', - 'I':'RGB', - 'CMYK':'RGB' - }.get(mode) +def _supported_modes(): + return [ + "RGB", + "RGBA", + "RGBa", + "RGBX", + "CMYK", + "YCbCr", + "HSV", + "I", + "F", + "P", + "LA", + "L", + "1", + ] Image.register_open(WebPImageFile.format, WebPImageFile, _accept)