Skip to content

Commit 270121f

Browse files
committed
Allow plugins to specify their supported modes
1 parent b1edb4a commit 270121f

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
@@ -11,8 +11,6 @@
1111
skip_unless_feature,
1212
)
1313

14-
from io import BytesIO
15-
1614
try:
1715
from PIL import _webp
1816

@@ -87,7 +85,7 @@ def _roundtrip(self, tmp_path, mode, epsilon, args={}):
8785
assert_image_similar(image, target, epsilon)
8886

8987
def test_save_convert_mode(self):
90-
out = BytesIO()
88+
out = io.BytesIO()
9189
for mode in ["CMYK", "I", "L", "LA", "P"]:
9290
img = Image.new(mode, (20, 20))
9391
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
@@ -101,8 +102,6 @@ def test_width_height(self):
101102
im.size = (3, 4)
102103

103104
def test_invalid_image(self):
104-
import io
105-
106105
im = io.BytesIO(b"")
107106
with pytest.raises(UnidentifiedImageError):
108107
Image.open(im)
@@ -382,14 +381,67 @@ def test_registered_extensions(self):
382381
for ext in [".cur", ".icns", ".tif", ".tiff"]:
383382
assert ext in extensions
384383

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

390426
im = hopper()
391427
im.save(temp_file, convert_mode=True)
392428

429+
def test_convert_mode(self):
430+
for mode, modes in [["P", []], ["P", ["P"]]]: # no modes, same mode
431+
im = Image.new(mode, (100, 100))
432+
assert im._convert_mode(modes) is None
433+
434+
for mode, modes in [
435+
["P", ["RGB"]],
436+
["P", ["L"]], # converting to a non-preferred mode
437+
["LA", ["P"]],
438+
["I", ["L"]],
439+
["RGB", ["L"]],
440+
["RGB", ["CMYK"]],
441+
]:
442+
im = Image.new(mode, (100, 100))
443+
assert im._convert_mode(modes) is not None
444+
393445
def test_effect_mandelbrot(self):
394446
# Arrange
395447
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
@@ -798,15 +798,8 @@ def jpeg_factory(fp=None, filename=None):
798798
return im
799799

800800

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

811804

812805
# ---------------------------------------------------------------------

src/PIL/PngImagePlugin.py

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

13231323

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

13291327

13301328
# --------------------------------------------------------------------

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)