Skip to content

Commit ccdc9cd

Browse files
committed
Allow plugins to specify their supported modes
1 parent 484f990 commit ccdc9cd

9 files changed

Lines changed: 131 additions & 61 deletions

Tests/test_file_gif.py

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

801801

802-
def test_save_wrong_modes(self):
802+
def test_save_wrong_modes():
803803
out = BytesIO()
804804
for mode in ["CMYK"]:
805805
img = Image.new(mode, (20, 20))
806-
self.assertRaises(ValueError, img.save, out, "GIF")
806+
with pytest.raises(ValueError):
807+
img.save(out, "GIF")
807808

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

Tests/test_file_jpeg.py

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

645645
with Image.open(temp_file) as reloaded:
646-
with Image.open("Tests/images/pil123rgba_red.jpg") as target:
647-
assert_image_similar(reloaded, target, 4)
646+
assert_image_similar_tofile(reloaded, "Tests/images/pil123rgba_red.jpg", 4)
648647

649648
def test_save_tiff_with_dpi(self, tmp_path):
650649
# Arrange

Tests/test_file_webp.py

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

15-
from io import BytesIO
16-
1715
try:
1816
from PIL import _webp
1917

@@ -88,7 +86,7 @@ def _roundtrip(self, tmp_path, mode, epsilon, args={}):
8886
assert_image_similar(image, target, epsilon)
8987

9088
def test_save_convert_mode(self):
91-
out = BytesIO()
89+
out = io.BytesIO()
9290
for mode in ["CMYK", "I", "L", "LA", "P"]:
9391
img = Image.new(mode, (20, 20))
9492
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 pytest
@@ -126,8 +127,6 @@ def test_width_height(self):
126127
im.size = (3, 4)
127128

128129
def test_invalid_image(self):
129-
import io
130-
131130
im = io.BytesIO(b"")
132131
with pytest.raises(UnidentifiedImageError):
133132
with Image.open(im):
@@ -409,14 +408,67 @@ def test_registered_extensions(self):
409408
for ext in [".cur", ".icns", ".tif", ".tiff"]:
410409
assert ext in extensions
411410

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

417453
im = hopper()
418454
im.save(temp_file, convert_mode=True)
419455

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

src/PIL/GifImagePlugin.py

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

874874

875-
def _convert_mode(im):
876-
return {
877-
'LA':'P',
878-
'CMYK':'RGB'
879-
}.get(im.mode)
875+
def _supported_modes():
876+
return ["RGB", "RGBA", "P", "I", "F", "LA", "L", "1"]
880877

881878

882879
# --------------------------------------------------------------------

src/PIL/Image.py

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

21482148
if format.upper() not in SAVE:
21492149
init()
2150-
if params.pop('save_all', False):
2150+
if params.pop("save_all", False):
21512151
save_handler = SAVE_ALL[format.upper()]
21522152
else:
21532153
save_handler = SAVE[format.upper()]
21542154

2155-
if params.get('convert_mode'):
2155+
if params.get("convert_mode"):
21562156
plugin = sys.modules[save_handler.__module__]
2157-
converted_im = self._convert_mode(plugin, params)
2158-
if converted_im:
2159-
return converted_im.save(fp, format, **params)
2157+
if hasattr(plugin, "_supported_modes"):
2158+
modes = plugin._supported_modes()
2159+
converted_im = self._convert_mode(modes, params)
2160+
if converted_im:
2161+
return converted_im.save(fp, format, **params)
21602162

21612163
self.encoderinfo = params
21622164
self.encoderconfig = ()
@@ -2176,32 +2178,57 @@ def save(self, fp, format=None, **params):
21762178
if open_fp:
21772179
fp.close()
21782180

2179-
def _convert_mode(self, plugin, params):
2180-
if not hasattr(plugin, '_convert_mode'):
2181+
def _convert_mode(self, modes, params={}):
2182+
if not modes or self.mode in modes:
21812183
return
2182-
new_mode = plugin._convert_mode(self)
2183-
if self.mode == 'LA' and new_mode == 'P':
2184-
alpha = self.getchannel('A')
2184+
if self.mode == "P":
2185+
preferred_modes = []
2186+
if "A" in self.im.getpalettemode():
2187+
preferred_modes.append("RGBA")
2188+
preferred_modes.append("RGB")
2189+
else:
2190+
preferred_modes = {
2191+
"CMYK": ["RGB"],
2192+
"RGB": ["CMYK"],
2193+
"RGBX": ["RGB"],
2194+
"RGBa": ["RGBA", "RGB"],
2195+
"RGBA": ["RGB"],
2196+
"LA": ["RGBA", "P", "L"],
2197+
"La": ["LA", "L"],
2198+
"L": ["RGB"],
2199+
"F": ["I"],
2200+
"I": ["L", "RGB"],
2201+
"1": ["L"],
2202+
"YCbCr": ["RGB"],
2203+
"LAB": ["RGB"],
2204+
"HSV": ["RGB"],
2205+
}.get(self.mode, [])
2206+
for new_mode in preferred_modes:
2207+
if new_mode in modes:
2208+
break
2209+
else:
2210+
new_mode = modes[0]
2211+
if self.mode == "LA" and new_mode == "P":
2212+
alpha = self.getchannel("A")
21852213
# Convert the image into P mode but only use 255 colors
21862214
# in the palette out of 256.
2187-
im = self.convert('L') \
2188-
.convert('P', palette=ADAPTIVE, colors=255)
2215+
im = self.convert("L").convert("P", palette=ADAPTIVE, colors=255)
21892216
# Set all pixel values below 128 to 255, and the rest to 0.
21902217
mask = eval(alpha, lambda px: 255 if px < 128 else 0)
21912218
# Paste the color of index 255 and use alpha as a mask.
21922219
im.paste(255, mask)
21932220
# The transparency index is 255.
2194-
im.info['transparency'] = 255
2221+
im.info["transparency"] = 255
21952222
return im
21962223

2197-
elif self.mode == 'I':
2198-
im = self.point([i//256 for i in range(65536)], 'L')
2199-
return im.convert(new_mode) if new_mode != 'L' else im
2224+
elif self.mode == "I":
2225+
im = self.point([i // 256 for i in range(65536)], "L")
2226+
return im.convert(new_mode) if new_mode != "L" else im
22002227

2201-
elif self.mode in ('RGBA', 'LA') and new_mode in ('RGB', 'L'):
2202-
fill_color = params.get('fill_color', 'white')
2228+
elif self.mode in ("RGBA", "LA") and new_mode in ("RGB", "L"):
2229+
fill_color = params.get("fill_color", "white")
22032230
background = new(new_mode, self.size, fill_color)
2204-
background.paste(self, self.getchannel('A'))
2231+
background.paste(self, self.getchannel("A"))
22052232
return background
22062233

22072234
elif new_mode:

src/PIL/JpegImagePlugin.py

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

796796

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

807800

808801
# ---------------------------------------------------------------------

src/PIL/PngImagePlugin.py

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

13851385

1386-
def _convert_mode(im):
1387-
return {
1388-
'CMYK':'RGB'
1389-
}.get(im.mode)
1386+
def _supported_modes():
1387+
return ["RGB", "RGBA", "P", "I", "LA", "L", "1"]
13901388

13911389

13921390
# --------------------------------------------------------------------

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)