Skip to content

Commit fc1c575

Browse files
committed
Allow plugins to specify their supported modes
1 parent 158a228 commit fc1c575

8 files changed

Lines changed: 130 additions & 59 deletions

File tree

Tests/test_file_gif.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -785,11 +785,12 @@ def test_save_I(tmp_path):
785785
assert_image_equal(reloaded.convert("L"), im.convert("L"))
786786

787787

788-
def test_save_wrong_modes(self):
788+
def test_save_wrong_modes():
789789
out = BytesIO()
790790
for mode in ["CMYK"]:
791791
img = Image.new(mode, (20, 20))
792-
self.assertRaises(ValueError, img.save, out, "GIF")
792+
with pytest.raises(ValueError):
793+
img.save(out, "GIF")
793794

794795
for mode in ["CMYK", "LA"]:
795796
img = Image.new(mode, (20, 20))

Tests/test_file_webp.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
skip_unless_feature,
1111
)
1212

13-
from io import BytesIO
14-
1513
try:
1614
from PIL import _webp
1715

@@ -85,7 +83,7 @@ def _roundtrip(self, tmp_path, mode, epsilon, args={}):
8583
assert_image_similar(image, target, epsilon)
8684

8785
def test_save_convert_mode(self):
88-
out = BytesIO()
86+
out = io.BytesIO()
8987
for mode in ["CMYK", "I", "L", "LA", "P"]:
9088
img = Image.new(mode, (20, 20))
9189
img.save(out, "WEBP", convert_mode=True)

Tests/test_image.py

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import io
22
import os
33
import shutil
4+
import sys
45
import tempfile
56

67
import PIL
@@ -94,8 +95,6 @@ def test_width_height(self):
9495
im.size = (3, 4)
9596

9697
def test_invalid_image(self):
97-
import io
98-
9998
im = io.BytesIO(b"")
10099
with pytest.raises(UnidentifiedImageError):
101100
Image.open(im)
@@ -375,14 +374,67 @@ def test_registered_extensions(self):
375374
for ext in [".cur", ".icns", ".tif", ".tiff"]:
376375
assert ext in extensions
377376

378-
def test_no_convert_mode(self):
379-
self.assertTrue(not hasattr(TiffImagePlugin, "_convert_mode"))
380-
381-
temp_file = self.tempfile("temp.tiff")
377+
def test_supported_modes(self):
378+
for format in Image.MIME.keys():
379+
try:
380+
save_handler = Image.SAVE[format]
381+
except KeyError:
382+
continue
383+
plugin = sys.modules[save_handler.__module__]
384+
if not hasattr(plugin, "_supported_modes"):
385+
continue
386+
387+
# Check that the supported modes list is accurate
388+
supported_modes = plugin._supported_modes()
389+
for mode in [
390+
"1",
391+
"L",
392+
"P",
393+
"RGB",
394+
"RGBA",
395+
"CMYK",
396+
"YCbCr",
397+
"LAB",
398+
"HSV",
399+
"I",
400+
"F",
401+
"LA",
402+
"La",
403+
"RGBX",
404+
"RGBa",
405+
]:
406+
out = io.BytesIO()
407+
im = Image.new(mode, (100, 100))
408+
if mode in supported_modes:
409+
im.save(out, format)
410+
else:
411+
with pytest.raises(Exception):
412+
im.save(out, format)
413+
414+
def test_no_supported_modes_method(self, tmp_path):
415+
assert not hasattr(TiffImagePlugin, "_supported_modes")
416+
417+
temp_file = str(tmp_path / "temp.tiff")
382418

383419
im = hopper()
384420
im.save(temp_file, convert_mode=True)
385421

422+
def test_convert_mode(self):
423+
for mode, modes in [["P", []], ["P", ["P"]]]: # no modes, same mode
424+
im = Image.new(mode, (100, 100))
425+
assert im._convert_mode(modes) is None
426+
427+
for mode, modes in [
428+
["P", ["RGB"]],
429+
["P", ["L"]], # converting to a non-preferred mode
430+
["LA", ["P"]],
431+
["I", ["L"]],
432+
["RGB", ["L"]],
433+
["RGB", ["CMYK"]],
434+
]:
435+
im = Image.new(mode, (100, 100))
436+
assert im._convert_mode(modes) is not None
437+
386438
def test_effect_mandelbrot(self):
387439
# Arrange
388440
size = (512, 512)

src/PIL/GifImagePlugin.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -869,11 +869,8 @@ def write(self, data):
869869
return fp.data
870870

871871

872-
def _convert_mode(im):
873-
return {
874-
'LA':'P',
875-
'CMYK':'RGB'
876-
}.get(im.mode)
872+
def _supported_modes():
873+
return ["RGB", "RGBA", "P", "I", "F", "LA", "L", "1"]
877874

878875

879876
# --------------------------------------------------------------------

src/PIL/Image.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,16 +2137,18 @@ def save(self, fp, format=None, **params):
21372137

21382138
if format.upper() not in SAVE:
21392139
init()
2140-
if params.pop('save_all', False):
2140+
if params.pop("save_all", False):
21412141
save_handler = SAVE_ALL[format.upper()]
21422142
else:
21432143
save_handler = SAVE[format.upper()]
21442144

2145-
if params.get('convert_mode'):
2145+
if params.get("convert_mode"):
21462146
plugin = sys.modules[save_handler.__module__]
2147-
converted_im = self._convert_mode(plugin, params)
2148-
if converted_im:
2149-
return converted_im.save(fp, format, **params)
2147+
if hasattr(plugin, "_supported_modes"):
2148+
modes = plugin._supported_modes()
2149+
converted_im = self._convert_mode(modes, params)
2150+
if converted_im:
2151+
return converted_im.save(fp, format, **params)
21502152

21512153
self.encoderinfo = params
21522154
self.encoderconfig = ()
@@ -2166,32 +2168,57 @@ def save(self, fp, format=None, **params):
21662168
if open_fp:
21672169
fp.close()
21682170

2169-
def _convert_mode(self, plugin, params):
2170-
if not hasattr(plugin, '_convert_mode'):
2171+
def _convert_mode(self, modes, params={}):
2172+
if not modes or self.mode in modes:
21712173
return
2172-
new_mode = plugin._convert_mode(self)
2173-
if self.mode == 'LA' and new_mode == 'P':
2174-
alpha = self.getchannel('A')
2174+
if self.mode == "P":
2175+
preferred_modes = []
2176+
if "A" in self.im.getpalettemode():
2177+
preferred_modes.append("RGBA")
2178+
preferred_modes.append("RGB")
2179+
else:
2180+
preferred_modes = {
2181+
"CMYK": ["RGB"],
2182+
"RGB": ["CMYK"],
2183+
"RGBX": ["RGB"],
2184+
"RGBa": ["RGBA", "RGB"],
2185+
"RGBA": ["RGB"],
2186+
"LA": ["RGBA", "P", "L"],
2187+
"La": ["LA", "L"],
2188+
"L": ["RGB"],
2189+
"F": ["I"],
2190+
"I": ["L", "RGB"],
2191+
"1": ["L"],
2192+
"YCbCr": ["RGB"],
2193+
"LAB": ["RGB"],
2194+
"HSV": ["RGB"],
2195+
}.get(self.mode, [])
2196+
for new_mode in preferred_modes:
2197+
if new_mode in modes:
2198+
break
2199+
else:
2200+
new_mode = modes[0]
2201+
if self.mode == "LA" and new_mode == "P":
2202+
alpha = self.getchannel("A")
21752203
# Convert the image into P mode but only use 255 colors
21762204
# in the palette out of 256.
2177-
im = self.convert('L') \
2178-
.convert('P', palette=ADAPTIVE, colors=255)
2205+
im = self.convert("L").convert("P", palette=ADAPTIVE, colors=255)
21792206
# Set all pixel values below 128 to 255, and the rest to 0.
21802207
mask = eval(alpha, lambda px: 255 if px < 128 else 0)
21812208
# Paste the color of index 255 and use alpha as a mask.
21822209
im.paste(255, mask)
21832210
# The transparency index is 255.
2184-
im.info['transparency'] = 255
2211+
im.info["transparency"] = 255
21852212
return im
21862213

2187-
elif self.mode == 'I':
2188-
im = self.point([i//256 for i in range(65536)], 'L')
2189-
return im.convert(new_mode) if new_mode != 'L' else im
2214+
elif self.mode == "I":
2215+
im = self.point([i // 256 for i in range(65536)], "L")
2216+
return im.convert(new_mode) if new_mode != "L" else im
21902217

2191-
elif self.mode in ('RGBA', 'LA') and new_mode in ('RGB', 'L'):
2192-
fill_color = params.get('fill_color', 'white')
2218+
elif self.mode in ("RGBA", "LA") and new_mode in ("RGB", "L"):
2219+
fill_color = params.get("fill_color", "white")
21932220
background = new(new_mode, self.size, fill_color)
2194-
background.paste(self, self.getchannel('A'))
2221+
background.paste(self, self.getchannel("A"))
21952222
return background
21962223

21972224
elif new_mode:

src/PIL/JpegImagePlugin.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -796,15 +796,8 @@ def jpeg_factory(fp=None, filename=None):
796796
return im
797797

798798

799-
def _convert_mode(im):
800-
mode = im.mode
801-
if mode == 'P':
802-
return 'RGBA' if 'A' in im.im.getpalettemode() else 'RGB'
803-
return {
804-
'RGBA':'RGB',
805-
'LA':'L',
806-
'I':'L'
807-
}.get(mode)
799+
def _supported_modes():
800+
return ["RGB", "CMYK", "YCbCr", "RGBX", "L", "1"]
808801

809802

810803
# ---------------------------------------------------------------------

src/PIL/PngImagePlugin.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,10 +1319,8 @@ def append(fp, cid, *data):
13191319
return fp.data
13201320

13211321

1322-
def _convert_mode(im):
1323-
return {
1324-
'CMYK':'RGB'
1325-
}.get(im.mode)
1322+
def _supported_modes():
1323+
return ["RGB", "RGBA", "P", "I", "LA", "L", "1"]
13261324

13271325

13281326
# --------------------------------------------------------------------

src/PIL/WebPImagePlugin.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -342,17 +342,22 @@ def _save(im, fp, filename):
342342
fp.write(data)
343343

344344

345-
def _convert_mode(im):
346-
mode = im.mode
347-
if mode == 'P':
348-
return 'RGBA' if 'A' in im.im.getpalettemode() else 'RGB'
349-
return {
350-
# Pillow doesn't support L modes for webp for now.
351-
'L':'RGB',
352-
'LA':'RGBA',
353-
'I':'RGB',
354-
'CMYK':'RGB'
355-
}.get(mode)
345+
def _supported_modes():
346+
return [
347+
"RGB",
348+
"RGBA",
349+
"RGBa",
350+
"RGBX",
351+
"CMYK",
352+
"YCbCr",
353+
"HSV",
354+
"I",
355+
"F",
356+
"P",
357+
"LA",
358+
"L",
359+
"1",
360+
]
356361

357362

358363
Image.register_open(WebPImageFile.format, WebPImageFile, _accept)

0 commit comments

Comments
 (0)