Skip to content

Commit 713dd17

Browse files
authored
Merge pull request #4547 from radarhere/webp
Added method argument to single frame WebP saving
2 parents db7186b + f17f1bc commit 713dd17

3 files changed

Lines changed: 98 additions & 74 deletions

File tree

Tests/test_file_webp.py

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import io
2+
13
import pytest
24
from PIL import Image, WebPImagePlugin
35

@@ -54,72 +56,65 @@ def test_read_rgb(self):
5456
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
5557
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
5658

57-
def test_write_rgb(self, tmp_path):
58-
"""
59-
Can we write a RGB mode file to webp without error.
60-
Does it have the bits we expect?
61-
"""
62-
59+
def _roundtrip(self, tmp_path, mode, epsilon, args={}):
6360
temp_file = str(tmp_path / "temp.webp")
6461

65-
hopper(self.rgb_mode).save(temp_file)
62+
hopper(mode).save(temp_file, **args)
6663
with Image.open(temp_file) as image:
6764
assert image.mode == self.rgb_mode
6865
assert image.size == (128, 128)
6966
assert image.format == "WEBP"
7067
image.load()
7168
image.getdata()
7269

73-
# generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm
74-
assert_image_similar_tofile(
75-
image, "Tests/images/hopper_webp_write.ppm", 12.0
76-
)
70+
if mode == self.rgb_mode:
71+
# generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm
72+
assert_image_similar_tofile(
73+
image, "Tests/images/hopper_webp_write.ppm", 12.0
74+
)
7775

7876
# This test asserts that the images are similar. If the average pixel
7977
# difference between the two images is less than the epsilon value,
8078
# then we're going to accept that it's a reasonable lossy version of
81-
# the image. The old lena images for WebP are showing ~16 on
82-
# Ubuntu, the jpegs are showing ~18.
83-
target = hopper(self.rgb_mode)
84-
assert_image_similar(image, target, 12.0)
79+
# the image.
80+
target = hopper(mode)
81+
if mode != self.rgb_mode:
82+
target = target.convert(self.rgb_mode)
83+
assert_image_similar(image, target, epsilon)
84+
85+
def test_write_rgb(self, tmp_path):
86+
"""
87+
Can we write a RGB mode file to webp without error?
88+
Does it have the bits we expect?
89+
"""
90+
91+
self._roundtrip(tmp_path, self.rgb_mode, 12.5)
92+
93+
def test_write_method(self, tmp_path):
94+
self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6})
95+
96+
buffer_no_args = io.BytesIO()
97+
hopper().save(buffer_no_args, format="WEBP")
98+
99+
buffer_method = io.BytesIO()
100+
hopper().save(buffer_method, format="WEBP", method=6)
101+
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
85102

86103
def test_write_unsupported_mode_L(self, tmp_path):
87104
"""
88105
Saving a black-and-white file to WebP format should work, and be
89106
similar to the original file.
90107
"""
91108

92-
temp_file = str(tmp_path / "temp.webp")
93-
hopper("L").save(temp_file)
94-
with Image.open(temp_file) as image:
95-
assert image.mode == self.rgb_mode
96-
assert image.size == (128, 128)
97-
assert image.format == "WEBP"
98-
99-
image.load()
100-
image.getdata()
101-
target = hopper("L").convert(self.rgb_mode)
102-
103-
assert_image_similar(image, target, 10.0)
109+
self._roundtrip(tmp_path, "L", 10.0)
104110

105111
def test_write_unsupported_mode_P(self, tmp_path):
106112
"""
107113
Saving a palette-based file to WebP format should work, and be
108114
similar to the original file.
109115
"""
110116

111-
temp_file = str(tmp_path / "temp.webp")
112-
hopper("P").save(temp_file)
113-
with Image.open(temp_file) as image:
114-
assert image.mode == self.rgb_mode
115-
assert image.size == (128, 128)
116-
assert image.format == "WEBP"
117-
118-
image.load()
119-
image.getdata()
120-
target = hopper("P").convert(self.rgb_mode)
121-
122-
assert_image_similar(image, target, 50.0)
117+
self._roundtrip(tmp_path, "P", 50.0)
123118

124119
def test_WebPEncode_with_invalid_args(self):
125120
"""

src/PIL/WebPImagePlugin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ def _save(im, fp, filename):
314314
if isinstance(exif, Image.Exif):
315315
exif = exif.tobytes()
316316
xmp = im.encoderinfo.get("xmp", "")
317+
method = im.encoderinfo.get("method", 0)
317318

318319
if im.mode not in _VALID_WEBP_LEGACY_MODES:
319320
alpha = (
@@ -331,6 +332,7 @@ def _save(im, fp, filename):
331332
float(quality),
332333
im.mode,
333334
icc_profile,
335+
method,
334336
exif,
335337
xmp,
336338
)

src/_webp.c

Lines changed: 62 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,7 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
545545
int height;
546546
int lossless;
547547
float quality_factor;
548+
int method;
548549
uint8_t* rgb;
549550
uint8_t* icc_bytes;
550551
uint8_t* exif_bytes;
@@ -556,49 +557,75 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
556557
Py_ssize_t exif_size;
557558
Py_ssize_t xmp_size;
558559
size_t ret_size;
560+
int rgba_mode;
561+
int channels;
562+
int ok;
559563
ImagingSectionCookie cookie;
564+
WebPConfig config;
565+
WebPMemoryWriter writer;
566+
WebPPicture pic;
560567

561-
if (!PyArg_ParseTuple(args, "y#iiifss#s#s#",
568+
if (!PyArg_ParseTuple(args, "y#iiifss#is#s#",
562569
(char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode,
563-
&icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) {
570+
&icc_bytes, &icc_size, &method, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) {
564571
return NULL;
565572
}
566-
if (strcmp(mode, "RGBA")==0){
567-
if (size < width * height * 4){
568-
Py_RETURN_NONE;
569-
}
570-
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
571-
if (lossless) {
572-
ImagingSectionEnter(&cookie);
573-
ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4 * width, &output);
574-
ImagingSectionLeave(&cookie);
575-
} else
576-
#endif
577-
{
578-
ImagingSectionEnter(&cookie);
579-
ret_size = WebPEncodeRGBA(rgb, width, height, 4 * width, quality_factor, &output);
580-
ImagingSectionLeave(&cookie);
581-
}
582-
} else if (strcmp(mode, "RGB")==0){
583-
if (size < width * height * 3){
584-
Py_RETURN_NONE;
585-
}
586-
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
587-
if (lossless) {
588-
ImagingSectionEnter(&cookie);
589-
ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3 * width, &output);
590-
ImagingSectionLeave(&cookie);
591-
} else
592-
#endif
593-
{
594-
ImagingSectionEnter(&cookie);
595-
ret_size = WebPEncodeRGB(rgb, width, height, 3 * width, quality_factor, &output);
596-
ImagingSectionLeave(&cookie);
597-
}
598-
} else {
573+
574+
rgba_mode = strcmp(mode, "RGBA") == 0;
575+
if (!rgba_mode && strcmp(mode, "RGB") != 0) {
599576
Py_RETURN_NONE;
600577
}
601578

579+
channels = rgba_mode ? 4 : 3;
580+
if (size < width * height * channels) {
581+
Py_RETURN_NONE;
582+
}
583+
584+
// Setup config for this frame
585+
if (!WebPConfigInit(&config)) {
586+
PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!");
587+
return NULL;
588+
}
589+
config.lossless = lossless;
590+
config.quality = quality_factor;
591+
config.method = method;
592+
593+
// Validate the config
594+
if (!WebPValidateConfig(&config)) {
595+
PyErr_SetString(PyExc_ValueError, "invalid configuration");
596+
return NULL;
597+
}
598+
599+
if (!WebPPictureInit(&pic)) {
600+
PyErr_SetString(PyExc_ValueError, "could not initialise picture");
601+
return NULL;
602+
}
603+
pic.width = width;
604+
pic.height = height;
605+
pic.use_argb = 1; // Don't convert RGB pixels to YUV
606+
607+
if (rgba_mode) {
608+
WebPPictureImportRGBA(&pic, rgb, channels * width);
609+
} else {
610+
WebPPictureImportRGB(&pic, rgb, channels * width);
611+
}
612+
613+
WebPMemoryWriterInit(&writer);
614+
pic.writer = WebPMemoryWrite;
615+
pic.custom_ptr = &writer;
616+
617+
ImagingSectionEnter(&cookie);
618+
ok = WebPEncode(&config, &pic);
619+
ImagingSectionLeave(&cookie);
620+
621+
WebPPictureFree(&pic);
622+
if (!ok) {
623+
PyErr_SetString(PyExc_ValueError, "encoding error");
624+
return NULL;
625+
}
626+
output = writer.mem;
627+
ret_size = writer.size;
628+
602629
#ifndef HAVE_WEBPMUX
603630
if (ret_size > 0) {
604631
PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size);

0 commit comments

Comments
 (0)