Skip to content

Commit dac567f

Browse files
committed
Allow plugins to specify their supported modes
1 parent 8fab24c commit dac567f

8 files changed

Lines changed: 126 additions & 64 deletions

File tree

Tests/test_file_jpeg.py

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

674674
with Image.open(temp_file) as reloaded:
675-
with Image.open("Tests/images/pil123rgba_red.jpg") as target:
676-
assert_image_similar(reloaded, target, 4)
675+
assert_image_similar_tofile(reloaded, "Tests/images/pil123rgba_red.jpg", 4)
677676

678677
def test_save_tiff_with_dpi(self, tmp_path):
679678
# Arrange

Tests/test_file_webp.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
skip_unless_feature,
1616
)
1717

18-
from io import BytesIO
19-
2018
try:
2119
from PIL import _webp
2220

@@ -91,7 +89,7 @@ def _roundtrip(self, tmp_path, mode, epsilon, args={}):
9189
assert_image_similar(image, target, epsilon)
9290

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

Tests/test_image.py

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,7 @@
77

88
import pytest
99

10-
from PIL import (
11-
Image,
12-
ImageDraw,
13-
ImagePalette,
14-
TiffImagePlugin,
15-
UnidentifiedImageError,
16-
)
10+
from PIL import Image, ImageDraw, ImagePalette, TiffImagePlugin, UnidentifiedImageError
1711

1812
from .helper import (
1913
assert_image_equal,
@@ -138,8 +132,6 @@ def test_width_height(self):
138132
im.size = (3, 4)
139133

140134
def test_invalid_image(self):
141-
import io
142-
143135
im = io.BytesIO(b"")
144136
with pytest.raises(UnidentifiedImageError):
145137
with Image.open(im):
@@ -430,14 +422,67 @@ def test_registered_extensions(self):
430422
for ext in [".cur", ".icns", ".tif", ".tiff"]:
431423
assert ext in extensions
432424

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

436465
temp_file = str(tmp_path / "temp.tiff")
437466

438467
im = hopper()
439468
im.save(temp_file, convert_mode=True)
440469

470+
def test_convert_mode(self):
471+
for mode, modes in [["P", []], ["P", ["P"]]]: # no modes, same mode
472+
im = Image.new(mode, (100, 100))
473+
assert im._convert_mode(modes) is None
474+
475+
for mode, modes in [
476+
["P", ["RGB"]],
477+
["P", ["L"]], # converting to a non-preferred mode
478+
["LA", ["P"]],
479+
["I", ["L"]],
480+
["RGB", ["L"]],
481+
["RGB", ["CMYK"]],
482+
]:
483+
im = Image.new(mode, (100, 100))
484+
assert im._convert_mode(modes) is not None
485+
441486
def test_effect_mandelbrot(self):
442487
# Arrange
443488
size = (512, 512)

src/PIL/GifImagePlugin.py

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

10241024

1025-
def _convert_mode(im):
1026-
return {
1027-
'LA':'P',
1028-
'CMYK':'RGB'
1029-
}.get(im.mode)
1025+
def _supported_modes():
1026+
return ["RGB", "RGBA", "P", "I", "F", "LA", "L", "1"]
10301027

10311028

10321029
# --------------------------------------------------------------------

src/PIL/Image.py

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

22782278
if format.upper() not in SAVE:
22792279
init()
2280-
if params.pop('save_all', False):
2280+
if params.pop("save_all", False):
22812281
save_handler = SAVE_ALL[format.upper()]
22822282
else:
22832283
save_handler = SAVE[format.upper()]
22842284

2285-
if params.get('convert_mode'):
2285+
if params.get("convert_mode"):
22862286
plugin = sys.modules[save_handler.__module__]
2287-
converted_im = self._convert_mode(plugin, params)
2288-
if converted_im:
2289-
return converted_im.save(fp, format, **params)
2287+
if hasattr(plugin, "_supported_modes"):
2288+
modes = plugin._supported_modes()
2289+
converted_im = self._convert_mode(modes, params)
2290+
if converted_im:
2291+
return converted_im.save(fp, format, **params)
22902292

22912293
self.encoderinfo = params
22922294
self.encoderconfig = ()
@@ -2315,32 +2317,57 @@ def save(self, fp, format=None, **params):
23152317
if open_fp:
23162318
fp.close()
23172319

2318-
def _convert_mode(self, plugin, params):
2319-
if not hasattr(plugin, '_convert_mode'):
2320+
def _convert_mode(self, modes, params={}):
2321+
if not modes or self.mode in modes:
23202322
return
2321-
new_mode = plugin._convert_mode(self)
2322-
if self.mode == 'LA' and new_mode == 'P':
2323-
alpha = self.getchannel('A')
2323+
if self.mode == "P":
2324+
preferred_modes = []
2325+
if "A" in self.im.getpalettemode():
2326+
preferred_modes.append("RGBA")
2327+
preferred_modes.append("RGB")
2328+
else:
2329+
preferred_modes = {
2330+
"CMYK": ["RGB"],
2331+
"RGB": ["CMYK"],
2332+
"RGBX": ["RGB"],
2333+
"RGBa": ["RGBA", "RGB"],
2334+
"RGBA": ["RGB"],
2335+
"LA": ["RGBA", "P", "L"],
2336+
"La": ["LA", "L"],
2337+
"L": ["RGB"],
2338+
"F": ["I"],
2339+
"I": ["L", "RGB"],
2340+
"1": ["L"],
2341+
"YCbCr": ["RGB"],
2342+
"LAB": ["RGB"],
2343+
"HSV": ["RGB"],
2344+
}.get(self.mode, [])
2345+
for new_mode in preferred_modes:
2346+
if new_mode in modes:
2347+
break
2348+
else:
2349+
new_mode = modes[0]
2350+
if self.mode == "LA" and new_mode == "P":
2351+
alpha = self.getchannel("A")
23242352
# Convert the image into P mode but only use 255 colors
23252353
# in the palette out of 256.
2326-
im = self.convert('L') \
2327-
.convert('P', palette=Palette.ADAPTIVE, colors=255)
2354+
im = self.convert("L").convert("P", palette=Palette.ADAPTIVE, colors=255)
23282355
# Set all pixel values below 128 to 255, and the rest to 0.
23292356
mask = eval(alpha, lambda px: 255 if px < 128 else 0)
23302357
# Paste the color of index 255 and use alpha as a mask.
23312358
im.paste(255, mask)
23322359
# The transparency index is 255.
2333-
im.info['transparency'] = 255
2360+
im.info["transparency"] = 255
23342361
return im
23352362

2336-
elif self.mode == 'I':
2337-
im = self.point([i//256 for i in range(65536)], 'L')
2338-
return im.convert(new_mode) if new_mode != 'L' else im
2363+
elif self.mode == "I":
2364+
im = self.point([i // 256 for i in range(65536)], "L")
2365+
return im.convert(new_mode) if new_mode != "L" else im
23392366

2340-
elif self.mode in ('RGBA', 'LA') and new_mode in ('RGB', 'L'):
2341-
fill_color = params.get('fill_color', 'white')
2367+
elif self.mode in ("RGBA", "LA") and new_mode in ("RGB", "L"):
2368+
fill_color = params.get("fill_color", "white")
23422369
background = new(new_mode, self.size, fill_color)
2343-
background.paste(self, self.getchannel('A'))
2370+
background.paste(self, self.getchannel("A"))
23442371
return background
23452372

23462373
elif new_mode:

src/PIL/JpegImagePlugin.py

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

821821

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

832825

833826
# ---------------------------------------------------------------------

src/PIL/PngImagePlugin.py

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

14221422

1423-
def _convert_mode(im):
1424-
return {
1425-
'CMYK':'RGB'
1426-
}.get(im.mode)
1423+
def _supported_modes():
1424+
return ["RGB", "RGBA", "P", "I", "LA", "L", "1"]
14271425

14281426

14291427
# --------------------------------------------------------------------

src/PIL/WebPImagePlugin.py

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

346346

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

359364

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

0 commit comments

Comments
 (0)