diff --git a/Tests/images/pil123rgba_red.jpg b/Tests/images/pil123rgba_red.jpg new file mode 100644 index 00000000000..073a654366d Binary files /dev/null and b/Tests/images/pil123rgba_red.jpg differ diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 90943ac8f69..fff2da930db 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -786,6 +786,17 @@ def test_save_I(tmp_path): assert_image_equal(reloaded.convert("L"), im.convert("L")) +def test_save_wrong_modes(self): + out = BytesIO() + for mode in ["CMYK"]: + img = Image.new(mode, (20, 20)) + self.assertRaises(ValueError, img.save, out, "GIF") + + for mode in ["CMYK", "LA"]: + img = Image.new(mode, (20, 20)) + img.save(out, "GIF", convert_mode=True) + + def test_getdata(): # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 03a2dba51a1..fbf57e8ad45 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -558,14 +558,26 @@ def test_save_correct_modes(self): img = Image.new(mode, (20, 20)) img.save(out, "JPEG") - def test_save_wrong_modes(self): + def test_save_wrong_modes(self, tmp_path): # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() - for mode in ["LA", "La", "RGBA", "RGBa", "P"]: + for mode in ["LA", "La", "RGBA", "RGBa", "P", "I"]: img = Image.new(mode, (20, 20)) with pytest.raises(OSError): img.save(out, "JPEG") + for mode in ["LA", "RGBA", "P", "I"]: + img = Image.new(mode, (20, 20)) + img.save(out, "JPEG", convert_mode=True) + + temp_file = str(tmp_path / "temp.jpg") + with Image.open("Tests/images/pil123rgba.png") as img: + 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) + def test_save_tiff_with_dpi(self, tmp_path): # Arrange outfile = str(tmp_path / "temp.tif") diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index f5993674105..ac342188a38 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -230,6 +230,14 @@ def test_load_transparent_rgb(self): # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 + def test_save_CMYK(self): + out = BytesIO() + im = Image.new("CMYK", (20, 20)) + with pytest.raises(IOError): + im.save(out, "PNG") + + im.save(out, "PNG", convert_mode=True) + def test_save_p_transparent_palette(self, tmp_path): in_file = "Tests/images/pil123p.png" with Image.open(in_file) as im: diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 11fbd9fd579..19c50b4647a 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -12,6 +12,8 @@ skip_unless_feature, ) +from io import BytesIO + try: from PIL import _webp @@ -85,6 +87,12 @@ def _roundtrip(self, tmp_path, mode, epsilon, args={}): target = target.convert(self.rgb_mode) assert_image_similar(image, target, epsilon) + def test_save_convert_mode(self): + out = BytesIO() + for mode in ["CMYK", "I", "L", "LA", "P"]: + img = Image.new(mode, (20, 20)) + img.save(out, "WEBP", convert_mode=True) + def test_write_rgb(self, tmp_path): """ Can we write a RGB mode file to webp without error? diff --git a/Tests/test_image.py b/Tests/test_image.py index cc0edbdd7a7..5e2d50e8f5f 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -6,7 +6,14 @@ import pytest import PIL -from PIL import Image, ImageDraw, ImagePalette, ImageShow, UnidentifiedImageError +from PIL import ( + Image, + ImageDraw, + ImagePalette, + ImageShow, + TiffImagePlugin, + UnidentifiedImageError, +) from .helper import ( assert_image_equal, @@ -377,6 +384,14 @@ def test_registered_extensions(self): for ext in [".cur", ".icns", ".tif", ".tiff"]: assert ext in extensions + def test_no_convert_mode(self): + self.assertTrue(not hasattr(TiffImagePlugin, "_convert_mode")) + + temp_file = self.tempfile("temp.tiff") + + im = hopper() + im.save(temp_file, convert_mode=True) + def test_effect_mandelbrot(self): # Arrange size = (512, 512) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 248c37ba16c..eab0f790468 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -872,6 +872,13 @@ def write(self, data): return fp.data +def _convert_mode(im): + return { + 'LA':'P', + 'CMYK':'RGB' + }.get(im.mode) + + # -------------------------------------------------------------------- # Registry diff --git a/src/PIL/Image.py b/src/PIL/Image.py index db639f5248c..5ce1ad04440 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2115,10 +2115,6 @@ def save(self, fp, format=None, **params): # may mutate self! self._ensure_mutable() - save_all = params.pop("save_all", False) - self.encoderinfo = params - self.encoderconfig = () - preinit() ext = os.path.splitext(filename)[1].lower() @@ -2133,11 +2129,20 @@ def save(self, fp, format=None, **params): if format.upper() not in SAVE: init() - if save_all: + if params.pop('save_all', False): save_handler = SAVE_ALL[format.upper()] else: save_handler = SAVE[format.upper()] + 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) + + self.encoderinfo = params + self.encoderconfig = () + if open_fp: if params.get("append", False): # Open also for reading ("+"), because TIFF save_all @@ -2153,6 +2158,37 @@ def save(self, fp, format=None, **params): if open_fp: fp.close() + def _convert_mode(self, plugin, params): + if not hasattr(plugin, '_convert_mode'): + return + new_mode = plugin._convert_mode(self) + 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=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 + 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 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')) + return background + + elif new_mode: + return self.convert(new_mode) + def seek(self, frame): """ Seeks to the given frame in this sequence file. If you seek diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index e2f7da9aff6..b93b9372251 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -801,6 +801,17 @@ 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) + + # --------------------------------------------------------------------- # Registry stuff diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 0c53a031b5e..40c20bc8910 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1355,6 +1355,12 @@ def append(fp, cid, *data): return fp.data +def _convert_mode(im): + return { + 'CMYK':'RGB' + }.get(im.mode) + + # -------------------------------------------------------------------- # Registry diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 2e9746fa3ff..b76b3de223e 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -342,6 +342,19 @@ 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) + + Image.register_open(WebPImageFile.format, WebPImageFile, _accept) if SUPPORTED: Image.register_save(WebPImageFile.format, _save)