Allow plugins to specify their supported modes

This commit is contained in:
Andrew Murray 2021-11-09 22:09:17 +11:00
parent 8fab24c8ab
commit dac567f42b
8 changed files with 126 additions and 64 deletions

View File

@ -672,8 +672,7 @@ class TestFileJpeg:
img.save(temp_file, convert_mode=True, fill_color="red") img.save(temp_file, convert_mode=True, fill_color="red")
with Image.open(temp_file) as reloaded: with Image.open(temp_file) as reloaded:
with Image.open("Tests/images/pil123rgba_red.jpg") as target: assert_image_similar_tofile(reloaded, "Tests/images/pil123rgba_red.jpg", 4)
assert_image_similar(reloaded, target, 4)
def test_save_tiff_with_dpi(self, tmp_path): def test_save_tiff_with_dpi(self, tmp_path):
# Arrange # Arrange

View File

@ -15,8 +15,6 @@ from .helper import (
skip_unless_feature, skip_unless_feature,
) )
from io import BytesIO
try: try:
from PIL import _webp from PIL import _webp
@ -91,7 +89,7 @@ class TestFileWebp:
assert_image_similar(image, target, epsilon) assert_image_similar(image, target, epsilon)
def test_save_convert_mode(self): def test_save_convert_mode(self):
out = BytesIO() out = io.BytesIO()
for mode in ["CMYK", "I", "L", "LA", "P"]: for mode in ["CMYK", "I", "L", "LA", "P"]:
img = Image.new(mode, (20, 20)) img = Image.new(mode, (20, 20))
img.save(out, "WEBP", convert_mode=True) img.save(out, "WEBP", convert_mode=True)

View File

@ -7,13 +7,7 @@ import warnings
import pytest import pytest
from PIL import ( from PIL import Image, ImageDraw, ImagePalette, TiffImagePlugin, UnidentifiedImageError
Image,
ImageDraw,
ImagePalette,
TiffImagePlugin,
UnidentifiedImageError,
)
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -138,8 +132,6 @@ class TestImage:
im.size = (3, 4) im.size = (3, 4)
def test_invalid_image(self): def test_invalid_image(self):
import io
im = io.BytesIO(b"") im = io.BytesIO(b"")
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):
with Image.open(im): with Image.open(im):
@ -430,14 +422,67 @@ class TestImage:
for ext in [".cur", ".icns", ".tif", ".tiff"]: for ext in [".cur", ".icns", ".tif", ".tiff"]:
assert ext in extensions assert ext in extensions
def test_no_convert_mode(self, tmp_path): def test_supported_modes(self):
assert not hasattr(TiffImagePlugin, "_convert_mode") 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") temp_file = str(tmp_path / "temp.tiff")
im = hopper() im = hopper()
im.save(temp_file, convert_mode=True) 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): def test_effect_mandelbrot(self):
# Arrange # Arrange
size = (512, 512) size = (512, 512)

View File

@ -1022,11 +1022,8 @@ def getdata(im, offset=(0, 0), **params):
return fp.data return fp.data
def _convert_mode(im): def _supported_modes():
return { return ["RGB", "RGBA", "P", "I", "F", "LA", "L", "1"]
'LA':'P',
'CMYK':'RGB'
}.get(im.mode)
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -2277,14 +2277,16 @@ class Image:
if format.upper() not in SAVE: if format.upper() not in SAVE:
init() init()
if params.pop('save_all', False): if params.pop("save_all", False):
save_handler = SAVE_ALL[format.upper()] save_handler = SAVE_ALL[format.upper()]
else: else:
save_handler = SAVE[format.upper()] save_handler = SAVE[format.upper()]
if params.get('convert_mode'): if params.get("convert_mode"):
plugin = sys.modules[save_handler.__module__] plugin = sys.modules[save_handler.__module__]
converted_im = self._convert_mode(plugin, params) if hasattr(plugin, "_supported_modes"):
modes = plugin._supported_modes()
converted_im = self._convert_mode(modes, params)
if converted_im: if converted_im:
return converted_im.save(fp, format, **params) return converted_im.save(fp, format, **params)
@ -2315,32 +2317,57 @@ class Image:
if open_fp: if open_fp:
fp.close() fp.close()
def _convert_mode(self, plugin, params): def _convert_mode(self, modes, params={}):
if not hasattr(plugin, '_convert_mode'): if not modes or self.mode in modes:
return return
new_mode = plugin._convert_mode(self) if self.mode == "P":
if self.mode == 'LA' and new_mode == 'P': preferred_modes = []
alpha = self.getchannel('A') 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 # Convert the image into P mode but only use 255 colors
# in the palette out of 256. # in the palette out of 256.
im = self.convert('L') \ im = self.convert("L").convert("P", palette=Palette.ADAPTIVE, colors=255)
.convert('P', palette=Palette.ADAPTIVE, colors=255)
# Set all pixel values below 128 to 255, and the rest to 0. # Set all pixel values below 128 to 255, and the rest to 0.
mask = eval(alpha, lambda px: 255 if px < 128 else 0) mask = eval(alpha, lambda px: 255 if px < 128 else 0)
# Paste the color of index 255 and use alpha as a mask. # Paste the color of index 255 and use alpha as a mask.
im.paste(255, mask) im.paste(255, mask)
# The transparency index is 255. # The transparency index is 255.
im.info['transparency'] = 255 im.info["transparency"] = 255
return im return im
elif self.mode == 'I': elif self.mode == "I":
im = self.point([i//256 for i in range(65536)], 'L') im = self.point([i // 256 for i in range(65536)], "L")
return im.convert(new_mode) if new_mode != 'L' else im return im.convert(new_mode) if new_mode != "L" else im
elif self.mode in ('RGBA', 'LA') and new_mode in ('RGB', 'L'): elif self.mode in ("RGBA", "LA") and new_mode in ("RGB", "L"):
fill_color = params.get('fill_color', 'white') fill_color = params.get("fill_color", "white")
background = new(new_mode, self.size, fill_color) background = new(new_mode, self.size, fill_color)
background.paste(self, self.getchannel('A')) background.paste(self, self.getchannel("A"))
return background return background
elif new_mode: elif new_mode:

View File

@ -819,15 +819,8 @@ def jpeg_factory(fp=None, filename=None):
return im return im
def _convert_mode(im): def _supported_modes():
mode = im.mode return ["RGB", "CMYK", "YCbCr", "RGBX", "L", "1"]
if mode == 'P':
return 'RGBA' if 'A' in im.im.getpalettemode() else 'RGB'
return {
'RGBA':'RGB',
'LA':'L',
'I':'L'
}.get(mode)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

View File

@ -1420,10 +1420,8 @@ def getchunks(im, **params):
return fp.data return fp.data
def _convert_mode(im): def _supported_modes():
return { return ["RGB", "RGBA", "P", "I", "LA", "L", "1"]
'CMYK':'RGB'
}.get(im.mode)
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -344,17 +344,22 @@ def _save(im, fp, filename):
fp.write(data) fp.write(data)
def _convert_mode(im): def _supported_modes():
mode = im.mode return [
if mode == 'P': "RGB",
return 'RGBA' if 'A' in im.im.getpalettemode() else 'RGB' "RGBA",
return { "RGBa",
# Pillow doesn't support L modes for webp for now. "RGBX",
'L':'RGB', "CMYK",
'LA':'RGBA', "YCbCr",
'I':'RGB', "HSV",
'CMYK':'RGB' "I",
}.get(mode) "F",
"P",
"LA",
"L",
"1",
]
Image.register_open(WebPImageFile.format, WebPImageFile, _accept) Image.register_open(WebPImageFile.format, WebPImageFile, _accept)