Skip to content

Commit 484f990

Browse files
committed
Added convert_mode param when saving
1 parent 058b8d3 commit 484f990

11 files changed

Lines changed: 135 additions & 8 deletions

Tests/images/pil123rgba_red.jpg

4.56 KB
Loading

Tests/test_file_gif.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,17 @@ 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):
803+
out = BytesIO()
804+
for mode in ["CMYK"]:
805+
img = Image.new(mode, (20, 20))
806+
self.assertRaises(ValueError, img.save, out, "GIF")
807+
808+
for mode in ["CMYK", "LA"]:
809+
img = Image.new(mode, (20, 20))
810+
img.save(out, "GIF", convert_mode=True)
811+
812+
802813
def test_getdata():
803814
# Test getheader/getdata against legacy values.
804815
# Create a 'P' image with holes in the palette.

Tests/test_file_jpeg.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,14 +626,26 @@ def test_save_correct_modes(self):
626626
img = Image.new(mode, (20, 20))
627627
img.save(out, "JPEG")
628628

629-
def test_save_wrong_modes(self):
629+
def test_save_wrong_modes(self, tmp_path):
630630
# ref https://github.com/python-pillow/Pillow/issues/2005
631631
out = BytesIO()
632-
for mode in ["LA", "La", "RGBA", "RGBa", "P"]:
632+
for mode in ["LA", "La", "RGBA", "RGBa", "P", "I"]:
633633
img = Image.new(mode, (20, 20))
634634
with pytest.raises(OSError):
635635
img.save(out, "JPEG")
636636

637+
for mode in ["LA", "RGBA", "P", "I"]:
638+
img = Image.new(mode, (20, 20))
639+
img.save(out, "JPEG", convert_mode=True)
640+
641+
temp_file = str(tmp_path / "temp.jpg")
642+
with Image.open("Tests/images/pil123rgba.png") as img:
643+
img.save(temp_file, convert_mode=True, fill_color="red")
644+
645+
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)
648+
637649
def test_save_tiff_with_dpi(self, tmp_path):
638650
# Arrange
639651
outfile = str(tmp_path / "temp.tif")

Tests/test_file_png.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,14 @@ def test_load_transparent_rgb(self):
231231
# image has 876 transparent pixels
232232
assert im.getchannel("A").getcolors()[0][0] == 876
233233

234+
def test_save_CMYK(self):
235+
out = BytesIO()
236+
im = Image.new("CMYK", (20, 20))
237+
with pytest.raises(IOError):
238+
im.save(out, "PNG")
239+
240+
im.save(out, "PNG", convert_mode=True)
241+
234242
def test_save_p_transparent_palette(self, tmp_path):
235243
in_file = "Tests/images/pil123p.png"
236244
with Image.open(in_file) as im:

Tests/test_file_webp.py

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

15+
from io import BytesIO
16+
1517
try:
1618
from PIL import _webp
1719

@@ -85,6 +87,12 @@ def _roundtrip(self, tmp_path, mode, epsilon, args={}):
8587
target = target.convert(self.rgb_mode)
8688
assert_image_similar(image, target, epsilon)
8789

90+
def test_save_convert_mode(self):
91+
out = BytesIO()
92+
for mode in ["CMYK", "I", "L", "LA", "P"]:
93+
img = Image.new(mode, (20, 20))
94+
img.save(out, "WEBP", convert_mode=True)
95+
8896
def test_write_rgb(self, tmp_path):
8997
"""
9098
Can we write a RGB mode file to webp without error?

Tests/test_image.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
import pytest
77

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

1118
from .helper import (
1219
assert_image_equal,
@@ -402,6 +409,14 @@ def test_registered_extensions(self):
402409
for ext in [".cur", ".icns", ".tif", ".tiff"]:
403410
assert ext in extensions
404411

412+
def test_no_convert_mode(self):
413+
self.assertTrue(not hasattr(TiffImagePlugin, "_convert_mode"))
414+
415+
temp_file = self.tempfile("temp.tiff")
416+
417+
im = hopper()
418+
im.save(temp_file, convert_mode=True)
419+
405420
def test_effect_mandelbrot(self):
406421
# Arrange
407422
size = (512, 512)

src/PIL/GifImagePlugin.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,13 @@ 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)
880+
881+
875882
# --------------------------------------------------------------------
876883
# Registry
877884

src/PIL/Image.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2133,10 +2133,6 @@ def save(self, fp, format=None, **params):
21332133
# may mutate self!
21342134
self._ensure_mutable()
21352135

2136-
save_all = params.pop("save_all", False)
2137-
self.encoderinfo = params
2138-
self.encoderconfig = ()
2139-
21402136
preinit()
21412137

21422138
ext = os.path.splitext(filename)[1].lower()
@@ -2151,11 +2147,20 @@ def save(self, fp, format=None, **params):
21512147

21522148
if format.upper() not in SAVE:
21532149
init()
2154-
if save_all:
2150+
if params.pop('save_all', False):
21552151
save_handler = SAVE_ALL[format.upper()]
21562152
else:
21572153
save_handler = SAVE[format.upper()]
21582154

2155+
if params.get('convert_mode'):
2156+
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)
2160+
2161+
self.encoderinfo = params
2162+
self.encoderconfig = ()
2163+
21592164
if open_fp:
21602165
if params.get("append", False):
21612166
# Open also for reading ("+"), because TIFF save_all
@@ -2171,6 +2176,37 @@ def save(self, fp, format=None, **params):
21712176
if open_fp:
21722177
fp.close()
21732178

2179+
def _convert_mode(self, plugin, params):
2180+
if not hasattr(plugin, '_convert_mode'):
2181+
return
2182+
new_mode = plugin._convert_mode(self)
2183+
if self.mode == 'LA' and new_mode == 'P':
2184+
alpha = self.getchannel('A')
2185+
# Convert the image into P mode but only use 255 colors
2186+
# in the palette out of 256.
2187+
im = self.convert('L') \
2188+
.convert('P', palette=ADAPTIVE, colors=255)
2189+
# Set all pixel values below 128 to 255, and the rest to 0.
2190+
mask = eval(alpha, lambda px: 255 if px < 128 else 0)
2191+
# Paste the color of index 255 and use alpha as a mask.
2192+
im.paste(255, mask)
2193+
# The transparency index is 255.
2194+
im.info['transparency'] = 255
2195+
return im
2196+
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
2200+
2201+
elif self.mode in ('RGBA', 'LA') and new_mode in ('RGB', 'L'):
2202+
fill_color = params.get('fill_color', 'white')
2203+
background = new(new_mode, self.size, fill_color)
2204+
background.paste(self, self.getchannel('A'))
2205+
return background
2206+
2207+
elif new_mode:
2208+
return self.convert(new_mode)
2209+
21742210
def seek(self, frame):
21752211
"""
21762212
Seeks to the given frame in this sequence file. If you seek

src/PIL/JpegImagePlugin.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,17 @@ 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)
806+
807+
797808
# ---------------------------------------------------------------------
798809
# Registry stuff
799810

src/PIL/PngImagePlugin.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,6 +1383,12 @@ 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)
1390+
1391+
13861392
# --------------------------------------------------------------------
13871393
# Registry
13881394

0 commit comments

Comments
 (0)