Skip to content

Commit aee5d22

Browse files
committed
Added convert_mode param when saving
1 parent 33dca34 commit aee5d22

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
@@ -786,6 +786,17 @@ def test_save_I(tmp_path):
786786
assert_image_equal(reloaded.convert("L"), im.convert("L"))
787787

788788

789+
def test_save_wrong_modes(self):
790+
out = BytesIO()
791+
for mode in ["CMYK"]:
792+
img = Image.new(mode, (20, 20))
793+
self.assertRaises(ValueError, img.save, out, "GIF")
794+
795+
for mode in ["CMYK", "LA"]:
796+
img = Image.new(mode, (20, 20))
797+
img.save(out, "GIF", convert_mode=True)
798+
799+
789800
def test_getdata():
790801
# Test getheader/getdata against legacy values.
791802
# 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
@@ -558,14 +558,26 @@ def test_save_correct_modes(self):
558558
img = Image.new(mode, (20, 20))
559559
img.save(out, "JPEG")
560560

561-
def test_save_wrong_modes(self):
561+
def test_save_wrong_modes(self, tmp_path):
562562
# ref https://github.com/python-pillow/Pillow/issues/2005
563563
out = BytesIO()
564-
for mode in ["LA", "La", "RGBA", "RGBa", "P"]:
564+
for mode in ["LA", "La", "RGBA", "RGBa", "P", "I"]:
565565
img = Image.new(mode, (20, 20))
566566
with pytest.raises(OSError):
567567
img.save(out, "JPEG")
568568

569+
for mode in ["LA", "RGBA", "P", "I"]:
570+
img = Image.new(mode, (20, 20))
571+
img.save(out, "JPEG", convert_mode=True)
572+
573+
temp_file = str(tmp_path / "temp.jpg")
574+
with Image.open("Tests/images/pil123rgba.png") as img:
575+
img.save(temp_file, convert_mode=True, fill_color="red")
576+
577+
with Image.open(temp_file) as reloaded:
578+
with Image.open("Tests/images/pil123rgba_red.jpg") as target:
579+
assert_image_similar(reloaded, target, 4)
580+
569581
def test_save_tiff_with_dpi(self, tmp_path):
570582
# Arrange
571583
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
@@ -230,6 +230,14 @@ def test_load_transparent_rgb(self):
230230
# image has 876 transparent pixels
231231
assert im.getchannel("A").getcolors()[0][0] == 876
232232

233+
def test_save_CMYK(self):
234+
out = BytesIO()
235+
im = Image.new("CMYK", (20, 20))
236+
with pytest.raises(IOError):
237+
im.save(out, "PNG")
238+
239+
im.save(out, "PNG", convert_mode=True)
240+
233241
def test_save_p_transparent_palette(self, tmp_path):
234242
in_file = "Tests/images/pil123p.png"
235243
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,
@@ -377,6 +384,14 @@ def test_registered_extensions(self):
377384
for ext in [".cur", ".icns", ".tif", ".tiff"]:
378385
assert ext in extensions
379386

387+
def test_no_convert_mode(self):
388+
self.assertTrue(not hasattr(TiffImagePlugin, "_convert_mode"))
389+
390+
temp_file = self.tempfile("temp.tiff")
391+
392+
im = hopper()
393+
im.save(temp_file, convert_mode=True)
394+
380395
def test_effect_mandelbrot(self):
381396
# Arrange
382397
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
@@ -2115,10 +2115,6 @@ def save(self, fp, format=None, **params):
21152115
# may mutate self!
21162116
self._ensure_mutable()
21172117

2118-
save_all = params.pop("save_all", False)
2119-
self.encoderinfo = params
2120-
self.encoderconfig = ()
2121-
21222118
preinit()
21232119

21242120
ext = os.path.splitext(filename)[1].lower()
@@ -2133,11 +2129,20 @@ def save(self, fp, format=None, **params):
21332129

21342130
if format.upper() not in SAVE:
21352131
init()
2136-
if save_all:
2132+
if params.pop('save_all', False):
21372133
save_handler = SAVE_ALL[format.upper()]
21382134
else:
21392135
save_handler = SAVE[format.upper()]
21402136

2137+
if params.get('convert_mode'):
2138+
plugin = sys.modules[save_handler.__module__]
2139+
converted_im = self._convert_mode(plugin, params)
2140+
if converted_im:
2141+
return converted_im.save(fp, format, **params)
2142+
2143+
self.encoderinfo = params
2144+
self.encoderconfig = ()
2145+
21412146
if open_fp:
21422147
if params.get("append", False):
21432148
# Open also for reading ("+"), because TIFF save_all
@@ -2153,6 +2158,37 @@ def save(self, fp, format=None, **params):
21532158
if open_fp:
21542159
fp.close()
21552160

2161+
def _convert_mode(self, plugin, params):
2162+
if not hasattr(plugin, '_convert_mode'):
2163+
return
2164+
new_mode = plugin._convert_mode(self)
2165+
if self.mode == 'LA' and new_mode == 'P':
2166+
alpha = self.getchannel('A')
2167+
# Convert the image into P mode but only use 255 colors
2168+
# in the palette out of 256.
2169+
im = self.convert('L') \
2170+
.convert('P', palette=ADAPTIVE, colors=255)
2171+
# Set all pixel values below 128 to 255, and the rest to 0.
2172+
mask = eval(alpha, lambda px: 255 if px < 128 else 0)
2173+
# Paste the color of index 255 and use alpha as a mask.
2174+
im.paste(255, mask)
2175+
# The transparency index is 255.
2176+
im.info['transparency'] = 255
2177+
return im
2178+
2179+
elif self.mode == 'I':
2180+
im = self.point([i//256 for i in range(65536)], 'L')
2181+
return im.convert(new_mode) if new_mode != 'L' else im
2182+
2183+
elif self.mode in ('RGBA', 'LA') and new_mode in ('RGB', 'L'):
2184+
fill_color = params.get('fill_color', 'white')
2185+
background = new(new_mode, self.size, fill_color)
2186+
background.paste(self, self.getchannel('A'))
2187+
return background
2188+
2189+
elif new_mode:
2190+
return self.convert(new_mode)
2191+
21562192
def seek(self, frame):
21572193
"""
21582194
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
@@ -801,6 +801,17 @@ def jpeg_factory(fp=None, filename=None):
801801
return im
802802

803803

804+
def _convert_mode(im):
805+
mode = im.mode
806+
if mode == 'P':
807+
return 'RGBA' if 'A' in im.im.getpalettemode() else 'RGB'
808+
return {
809+
'RGBA':'RGB',
810+
'LA':'L',
811+
'I':'L'
812+
}.get(mode)
813+
814+
804815
# ---------------------------------------------------------------------
805816
# Registry stuff
806817

src/PIL/PngImagePlugin.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,6 +1355,12 @@ def append(fp, cid, *data):
13551355
return fp.data
13561356

13571357

1358+
def _convert_mode(im):
1359+
return {
1360+
'CMYK':'RGB'
1361+
}.get(im.mode)
1362+
1363+
13581364
# --------------------------------------------------------------------
13591365
# Registry
13601366

0 commit comments

Comments
 (0)