Skip to content

Commit b1edb4a

Browse files
committed
Added convert_mode param when saving
1 parent 5103075 commit b1edb4a

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

787787

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

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

568+
for mode in ["LA", "RGBA", "P", "I"]:
569+
img = Image.new(mode, (20, 20))
570+
img.save(out, "JPEG", convert_mode=True)
571+
572+
temp_file = str(tmp_path / "temp.jpg")
573+
with Image.open("Tests/images/pil123rgba.png") as img:
574+
img.save(temp_file, convert_mode=True, fill_color="red")
575+
576+
with Image.open(temp_file) as reloaded:
577+
with Image.open("Tests/images/pil123rgba_red.jpg") as target:
578+
assert_image_similar(reloaded, target, 4)
579+
568580
def test_save_tiff_with_dpi(self, tmp_path):
569581
# Arrange
570582
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
@@ -11,6 +11,8 @@
1111
skip_unless_feature,
1212
)
1313

14+
from io import BytesIO
15+
1416
try:
1517
from PIL import _webp
1618

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

89+
def test_save_convert_mode(self):
90+
out = BytesIO()
91+
for mode in ["CMYK", "I", "L", "LA", "P"]:
92+
img = Image.new(mode, (20, 20))
93+
img.save(out, "WEBP", convert_mode=True)
94+
8795
def test_write_rgb(self, tmp_path):
8896
"""
8997
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
@@ -5,7 +5,14 @@
55

66
import PIL
77
import pytest
8-
from PIL import Image, ImageDraw, ImagePalette, ImageShow, UnidentifiedImageError
8+
from PIL import (
9+
Image,
10+
ImageDraw,
11+
ImagePalette,
12+
ImageShow,
13+
TiffImagePlugin,
14+
UnidentifiedImageError,
15+
)
916

1017
from .helper import (
1118
assert_image_equal,
@@ -375,6 +382,14 @@ def test_registered_extensions(self):
375382
for ext in [".cur", ".icns", ".tif", ".tiff"]:
376383
assert ext in extensions
377384

385+
def test_no_convert_mode(self):
386+
self.assertTrue(not hasattr(TiffImagePlugin, "_convert_mode"))
387+
388+
temp_file = self.tempfile("temp.tiff")
389+
390+
im = hopper()
391+
im.save(temp_file, convert_mode=True)
392+
378393
def test_effect_mandelbrot(self):
379394
# Arrange
380395
size = (512, 512)

src/PIL/GifImagePlugin.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,13 @@ def write(self, data):
869869
return fp.data
870870

871871

872+
def _convert_mode(im):
873+
return {
874+
'LA':'P',
875+
'CMYK':'RGB'
876+
}.get(im.mode)
877+
878+
872879
# --------------------------------------------------------------------
873880
# Registry
874881

src/PIL/Image.py

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

2126-
save_all = params.pop("save_all", False)
2127-
self.encoderinfo = params
2128-
self.encoderconfig = ()
2129-
21302126
preinit()
21312127

21322128
ext = os.path.splitext(filename)[1].lower()
@@ -2141,11 +2137,20 @@ def save(self, fp, format=None, **params):
21412137

21422138
if format.upper() not in SAVE:
21432139
init()
2144-
if save_all:
2140+
if params.pop('save_all', False):
21452141
save_handler = SAVE_ALL[format.upper()]
21462142
else:
21472143
save_handler = SAVE[format.upper()]
21482144

2145+
if params.get('convert_mode'):
2146+
plugin = sys.modules[save_handler.__module__]
2147+
converted_im = self._convert_mode(plugin, params)
2148+
if converted_im:
2149+
return converted_im.save(fp, format, **params)
2150+
2151+
self.encoderinfo = params
2152+
self.encoderconfig = ()
2153+
21492154
if open_fp:
21502155
if params.get("append", False):
21512156
# Open also for reading ("+"), because TIFF save_all
@@ -2161,6 +2166,37 @@ def save(self, fp, format=None, **params):
21612166
if open_fp:
21622167
fp.close()
21632168

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

800800

801+
def _convert_mode(im):
802+
mode = im.mode
803+
if mode == 'P':
804+
return 'RGBA' if 'A' in im.im.getpalettemode() else 'RGB'
805+
return {
806+
'RGBA':'RGB',
807+
'LA':'L',
808+
'I':'L'
809+
}.get(mode)
810+
811+
801812
# ---------------------------------------------------------------------
802813
# Registry stuff
803814

src/PIL/PngImagePlugin.py

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

13231323

1324+
def _convert_mode(im):
1325+
return {
1326+
'CMYK':'RGB'
1327+
}.get(im.mode)
1328+
1329+
13241330
# --------------------------------------------------------------------
13251331
# Registry
13261332

0 commit comments

Comments
 (0)