Skip to content

Commit bf70856

Browse files
committed
Allow plugins to specify their supported modes
1 parent 85e8890 commit bf70856

8 files changed

Lines changed: 126 additions & 57 deletions

File tree

Tests/test_file_jpeg.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,8 +664,7 @@ def test_save_wrong_modes(self, tmp_path):
664664
img.save(temp_file, convert_mode=True, fill_color="red")
665665

666666
with Image.open(temp_file) as reloaded:
667-
with Image.open("Tests/images/pil123rgba_red.jpg") as target:
668-
assert_image_similar(reloaded, target, 4)
667+
assert_image_similar_tofile(reloaded, "Tests/images/pil123rgba_red.jpg", 4)
669668

670669
def test_save_tiff_with_dpi(self, tmp_path):
671670
# Arrange

Tests/test_file_webp.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
skip_unless_feature,
1414
)
1515

16-
from io import BytesIO
17-
1816
try:
1917
from PIL import _webp
2018

@@ -89,7 +87,7 @@ def _roundtrip(self, tmp_path, mode, epsilon, args={}):
8987
assert_image_similar(image, target, epsilon)
9088

9189
def test_save_convert_mode(self):
92-
out = BytesIO()
90+
out = io.BytesIO()
9391
for mode in ["CMYK", "I", "L", "LA", "P"]:
9492
img = Image.new(mode, (20, 20))
9593
img.save(out, "WEBP", convert_mode=True)

Tests/test_image.py

Lines changed: 56 additions & 4 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 pytest
@@ -125,8 +126,6 @@ def test_width_height(self):
125126
im.size = (3, 4)
126127

127128
def test_invalid_image(self):
128-
import io
129-
130129
im = io.BytesIO(b"")
131130
with pytest.raises(UnidentifiedImageError):
132131
with Image.open(im):
@@ -413,14 +412,67 @@ def test_registered_extensions(self):
413412
for ext in [".cur", ".icns", ".tif", ".tiff"]:
414413
assert ext in extensions
415414

416-
def test_no_convert_mode(self, tmp_path):
417-
assert not hasattr(TiffImagePlugin, "_convert_mode")
415+
def test_supported_modes(self):
416+
for format in Image.MIME.keys():
417+
try:
418+
save_handler = Image.SAVE[format]
419+
except KeyError:
420+
continue
421+
plugin = sys.modules[save_handler.__module__]
422+
if not hasattr(plugin, "_supported_modes"):
423+
continue
424+
425+
# Check that the supported modes list is accurate
426+
supported_modes = plugin._supported_modes()
427+
for mode in [
428+
"1",
429+
"L",
430+
"P",
431+
"RGB",
432+
"RGBA",
433+
"CMYK",
434+
"YCbCr",
435+
"LAB",
436+
"HSV",
437+
"I",
438+
"F",
439+
"LA",
440+
"La",
441+
"RGBX",
442+
"RGBa",
443+
]:
444+
out = io.BytesIO()
445+
im = Image.new(mode, (100, 100))
446+
if mode in supported_modes:
447+
im.save(out, format)
448+
else:
449+
with pytest.raises(Exception):
450+
im.save(out, format)
451+
452+
def test_no_supported_modes_method(self, tmp_path):
453+
assert not hasattr(TiffImagePlugin, "_supported_modes")
418454

419455
temp_file = str(tmp_path / "temp.tiff")
420456

421457
im = hopper()
422458
im.save(temp_file, convert_mode=True)
423459

460+
def test_convert_mode(self):
461+
for mode, modes in [["P", []], ["P", ["P"]]]: # no modes, same mode
462+
im = Image.new(mode, (100, 100))
463+
assert im._convert_mode(modes) is None
464+
465+
for mode, modes in [
466+
["P", ["RGB"]],
467+
["P", ["L"]], # converting to a non-preferred mode
468+
["LA", ["P"]],
469+
["I", ["L"]],
470+
["RGB", ["L"]],
471+
["RGB", ["CMYK"]],
472+
]:
473+
im = Image.new(mode, (100, 100))
474+
assert im._convert_mode(modes) is not None
475+
424476
def test_effect_mandelbrot(self):
425477
# Arrange
426478
size = (512, 512)

src/PIL/GifImagePlugin.py

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

912912

913-
def _convert_mode(im):
914-
return {
915-
'LA':'P',
916-
'CMYK':'RGB'
917-
}.get(im.mode)
913+
def _supported_modes():
914+
return ["RGB", "RGBA", "P", "I", "F", "LA", "L", "1"]
918915

919916

920917
# --------------------------------------------------------------------

src/PIL/Image.py

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

21912191
if format.upper() not in SAVE:
21922192
init()
2193-
if params.pop('save_all', False):
2193+
if params.pop("save_all", False):
21942194
save_handler = SAVE_ALL[format.upper()]
21952195
else:
21962196
save_handler = SAVE[format.upper()]
21972197

2198-
if params.get('convert_mode'):
2198+
if params.get("convert_mode"):
21992199
plugin = sys.modules[save_handler.__module__]
2200-
converted_im = self._convert_mode(plugin, params)
2201-
if converted_im:
2202-
return converted_im.save(fp, format, **params)
2200+
if hasattr(plugin, "_supported_modes"):
2201+
modes = plugin._supported_modes()
2202+
converted_im = self._convert_mode(modes, params)
2203+
if converted_im:
2204+
return converted_im.save(fp, format, **params)
22032205

22042206
self.encoderinfo = params
22052207
self.encoderconfig = ()
@@ -2219,32 +2221,57 @@ def save(self, fp, format=None, **params):
22192221
if open_fp:
22202222
fp.close()
22212223

2222-
def _convert_mode(self, plugin, params):
2223-
if not hasattr(plugin, '_convert_mode'):
2224+
def _convert_mode(self, modes, params={}):
2225+
if not modes or self.mode in modes:
22242226
return
2225-
new_mode = plugin._convert_mode(self)
2226-
if self.mode == 'LA' and new_mode == 'P':
2227-
alpha = self.getchannel('A')
2227+
if self.mode == "P":
2228+
preferred_modes = []
2229+
if "A" in self.im.getpalettemode():
2230+
preferred_modes.append("RGBA")
2231+
preferred_modes.append("RGB")
2232+
else:
2233+
preferred_modes = {
2234+
"CMYK": ["RGB"],
2235+
"RGB": ["CMYK"],
2236+
"RGBX": ["RGB"],
2237+
"RGBa": ["RGBA", "RGB"],
2238+
"RGBA": ["RGB"],
2239+
"LA": ["RGBA", "P", "L"],
2240+
"La": ["LA", "L"],
2241+
"L": ["RGB"],
2242+
"F": ["I"],
2243+
"I": ["L", "RGB"],
2244+
"1": ["L"],
2245+
"YCbCr": ["RGB"],
2246+
"LAB": ["RGB"],
2247+
"HSV": ["RGB"],
2248+
}.get(self.mode, [])
2249+
for new_mode in preferred_modes:
2250+
if new_mode in modes:
2251+
break
2252+
else:
2253+
new_mode = modes[0]
2254+
if self.mode == "LA" and new_mode == "P":
2255+
alpha = self.getchannel("A")
22282256
# Convert the image into P mode but only use 255 colors
22292257
# in the palette out of 256.
2230-
im = self.convert('L') \
2231-
.convert('P', palette=ADAPTIVE, colors=255)
2258+
im = self.convert("L").convert("P", palette=ADAPTIVE, colors=255)
22322259
# Set all pixel values below 128 to 255, and the rest to 0.
22332260
mask = eval(alpha, lambda px: 255 if px < 128 else 0)
22342261
# Paste the color of index 255 and use alpha as a mask.
22352262
im.paste(255, mask)
22362263
# The transparency index is 255.
2237-
im.info['transparency'] = 255
2264+
im.info["transparency"] = 255
22382265
return im
22392266

2240-
elif self.mode == 'I':
2241-
im = self.point([i//256 for i in range(65536)], 'L')
2242-
return im.convert(new_mode) if new_mode != 'L' else im
2267+
elif self.mode == "I":
2268+
im = self.point([i // 256 for i in range(65536)], "L")
2269+
return im.convert(new_mode) if new_mode != "L" else im
22432270

2244-
elif self.mode in ('RGBA', 'LA') and new_mode in ('RGB', 'L'):
2245-
fill_color = params.get('fill_color', 'white')
2271+
elif self.mode in ("RGBA", "LA") and new_mode in ("RGB", "L"):
2272+
fill_color = params.get("fill_color", "white")
22462273
background = new(new_mode, self.size, fill_color)
2247-
background.paste(self, self.getchannel('A'))
2274+
background.paste(self, self.getchannel("A"))
22482275
return background
22492276

22502277
elif new_mode:

src/PIL/JpegImagePlugin.py

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

817817

818-
def _convert_mode(im):
819-
mode = im.mode
820-
if mode == 'P':
821-
return 'RGBA' if 'A' in im.im.getpalettemode() else 'RGB'
822-
return {
823-
'RGBA':'RGB',
824-
'LA':'L',
825-
'I':'L'
826-
}.get(mode)
818+
def _supported_modes():
819+
return ["RGB", "CMYK", "YCbCr", "RGBX", "L", "1"]
827820

828821

829822
# ---------------------------------------------------------------------

src/PIL/PngImagePlugin.py

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

13961396

1397-
def _convert_mode(im):
1398-
return {
1399-
'CMYK':'RGB'
1400-
}.get(im.mode)
1397+
def _supported_modes():
1398+
return ["RGB", "RGBA", "P", "I", "LA", "L", "1"]
14011399

14021400

14031401
# --------------------------------------------------------------------

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)