Skip to content

Commit bd015e8

Browse files
committed
Allow plugins to specify their supported modes
1 parent 1fff671 commit bd015e8

8 files changed

Lines changed: 131 additions & 58 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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from io import BytesIO
2+
13
import pytest
24
from PIL import Image, WebPImagePlugin
35

@@ -8,8 +10,6 @@
810
skip_unless_feature,
911
)
1012

11-
from io import BytesIO
12-
1313
try:
1414
from PIL import _webp
1515

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
@@ -2113,16 +2113,18 @@ def save(self, fp, format=None, **params):
21132113

21142114
if format.upper() not in SAVE:
21152115
init()
2116-
if params.pop('save_all', False):
2116+
if params.pop("save_all", False):
21172117
save_handler = SAVE_ALL[format.upper()]
21182118
else:
21192119
save_handler = SAVE[format.upper()]
21202120

2121-
if params.get('convert_mode'):
2121+
if params.get("convert_mode"):
21222122
plugin = sys.modules[save_handler.__module__]
2123-
converted_im = self._convert_mode(plugin, params)
2124-
if converted_im:
2125-
return converted_im.save(fp, format, **params)
2123+
if hasattr(plugin, "_supported_modes"):
2124+
modes = plugin._supported_modes()
2125+
converted_im = self._convert_mode(modes, params)
2126+
if converted_im:
2127+
return converted_im.save(fp, format, **params)
21262128

21272129
self.encoderinfo = params
21282130
self.encoderconfig = ()
@@ -2142,32 +2144,57 @@ def save(self, fp, format=None, **params):
21422144
if open_fp:
21432145
fp.close()
21442146

2145-
def _convert_mode(self, plugin, params):
2146-
if not hasattr(plugin, '_convert_mode'):
2147+
def _convert_mode(self, modes, params={}):
2148+
if not modes or self.mode in modes:
21472149
return
2148-
new_mode = plugin._convert_mode(self)
2149-
if self.mode == 'LA' and new_mode == 'P':
2150-
alpha = self.getchannel('A')
2150+
if self.mode == "P":
2151+
preferred_modes = []
2152+
if "A" in self.im.getpalettemode():
2153+
preferred_modes.append("RGBA")
2154+
preferred_modes.append("RGB")
2155+
else:
2156+
preferred_modes = {
2157+
"CMYK": ["RGB"],
2158+
"RGB": ["CMYK"],
2159+
"RGBX": ["RGB"],
2160+
"RGBa": ["RGBA", "RGB"],
2161+
"RGBA": ["RGB"],
2162+
"LA": ["RGBA", "P", "L"],
2163+
"La": ["LA", "L"],
2164+
"L": ["RGB"],
2165+
"F": ["I"],
2166+
"I": ["L", "RGB"],
2167+
"1": ["L"],
2168+
"YCbCr": ["RGB"],
2169+
"LAB": ["RGB"],
2170+
"HSV": ["RGB"],
2171+
}.get(self.mode, [])
2172+
for new_mode in preferred_modes:
2173+
if new_mode in modes:
2174+
break
2175+
else:
2176+
new_mode = modes[0]
2177+
if self.mode == "LA" and new_mode == "P":
2178+
alpha = self.getchannel("A")
21512179
# Convert the image into P mode but only use 255 colors
21522180
# in the palette out of 256.
2153-
im = self.convert('L') \
2154-
.convert('P', palette=ADAPTIVE, colors=255)
2181+
im = self.convert("L").convert("P", palette=ADAPTIVE, colors=255)
21552182
# Set all pixel values below 128 to 255, and the rest to 0.
21562183
mask = eval(alpha, lambda px: 255 if px < 128 else 0)
21572184
# Paste the color of index 255 and use alpha as a mask.
21582185
im.paste(255, mask)
21592186
# The transparency index is 255.
2160-
im.info['transparency'] = 255
2187+
im.info["transparency"] = 255
21612188
return im
21622189

2163-
elif self.mode == 'I':
2164-
im = self.point([i//256 for i in range(65536)], 'L')
2165-
return im.convert(new_mode) if new_mode != 'L' else im
2190+
elif self.mode == "I":
2191+
im = self.point([i // 256 for i in range(65536)], "L")
2192+
return im.convert(new_mode) if new_mode != "L" else im
21662193

2167-
elif self.mode in ('RGBA', 'LA') and new_mode in ('RGB', 'L'):
2168-
fill_color = params.get('fill_color', 'white')
2194+
elif self.mode in ("RGBA", "LA") and new_mode in ("RGB", "L"):
2195+
fill_color = params.get("fill_color", "white")
21692196
background = new(new_mode, self.size, fill_color)
2170-
background.paste(self, self.getchannel('A'))
2197+
background.paste(self, self.getchannel("A"))
21712198
return background
21722199

21732200
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
@@ -1328,10 +1328,8 @@ def append(fp, cid, *data):
13281328
return fp.data
13291329

13301330

1331-
def _convert_mode(im):
1332-
return {
1333-
'CMYK':'RGB'
1334-
}.get(im.mode)
1331+
def _supported_modes():
1332+
return ["RGB", "RGBA", "P", "I", "LA", "L", "1"]
13351333

13361334

13371335
# --------------------------------------------------------------------

src/PIL/WebPImagePlugin.py

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

353353

354-
def _convert_mode(im):
355-
mode = im.mode
356-
if mode == 'P':
357-
return 'RGBA' if 'A' in im.im.getpalettemode() else 'RGB'
358-
return {
359-
# Pillow doesn't support L modes for webp for now.
360-
'L':'RGB',
361-
'LA':'RGBA',
362-
'I':'RGB',
363-
'CMYK':'RGB'
364-
}.get(mode)
354+
def _supported_modes():
355+
return [
356+
"RGB",
357+
"RGBA",
358+
"RGBa",
359+
"RGBX",
360+
"CMYK",
361+
"YCbCr",
362+
"HSV",
363+
"I",
364+
"F",
365+
"P",
366+
"LA",
367+
"L",
368+
"1",
369+
]
365370

366371

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

0 commit comments

Comments
 (0)