diff --git a/Tests/images/pil123rgba_red.jpg b/Tests/images/pil123rgba_red.jpg new file mode 100644 index 000000000..073a65436 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 dffd1006f..f908c4413 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -997,6 +997,18 @@ def test_save_I(tmp_path): assert_image_equal(reloaded.convert("L"), im.convert("L")) +def test_save_wrong_modes(): + out = BytesIO() + for mode in ["CMYK"]: + img = Image.new(mode, (20, 20)) + with pytest.raises(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 203065802..9f755e8c6 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -655,14 +655,26 @@ class TestFileJpeg: 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 bb2b0d119..d92634d76 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -238,6 +238,14 @@ class TestFilePng: # 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 c69e13a89..339580bde 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -15,6 +15,8 @@ from .helper import ( skip_unless_feature, ) +from io import BytesIO + try: from PIL import _webp @@ -88,6 +90,12 @@ class TestFileWebp: 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 07cf6eb92..91ecd3c4f 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,7 +7,13 @@ import warnings import pytest -from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError +from PIL import ( + Image, + ImageDraw, + ImagePalette, + TiffImagePlugin, + UnidentifiedImageError, +) from .helper import ( assert_image_equal, @@ -424,6 +430,14 @@ 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") + + temp_file = str(tmp_path / "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 b798bb969..4de387509 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -1022,6 +1022,13 @@ def getdata(im, offset=(0, 0), **params): 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 2e8279583..266044d49 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2263,10 +2263,6 @@ class Image: # 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() @@ -2281,11 +2277,20 @@ class Image: 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 = () + created = False if open_fp: created = not os.path.exists(filename) @@ -2310,6 +2315,37 @@ class Image: 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=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 93741ec6e..618b9996a 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -819,6 +819,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 53525e22e..740727bbb 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1420,6 +1420,12 @@ def getchunks(im, **params): 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 7dd3f5272..d81bc99a4 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -344,6 +344,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)