webp: add decoder

- support incremental decoding
- support Image.draft
- support reading Exif/ICC profile data even if Mux API is not available
This commit is contained in:
Benoit Pierre 2015-02-25 10:03:02 +01:00
parent 2ae69f1dc2
commit 76c8dda681
9 changed files with 459 additions and 141 deletions

View File

@ -1,19 +1,11 @@
from PIL import Image
from PIL import ImageFile
from io import BytesIO
from PIL import _webp
from PIL import Image, ImageFile, _webp
from PIL._binary import i8, i32le
import os
_VALID_WEBP_MODES = {
"RGB": True,
"RGBA": True,
}
_VALID_WEBP_MODES = ("RGB", "RGBA")
_VP8_MODES_BY_IDENTIFIER = {
b"VP8 ": "RGB",
b"VP8X": "RGBA",
b"VP8L": "RGBA", # lossless
}
_VP8_MODES_BY_IDENTIFIER = (b"VP8 ", b"VP8X", b"VP8L")
def _accept(prefix):
@ -23,6 +15,63 @@ def _accept(prefix):
return is_riff_file_format and is_webp_file and is_valid_vp8_mode
# VP8X/VP8L/VP8 chunk parsing routines.
# Arguments:
# - chunk_size: RIFF chunk size
# - fp: file
# Return:
# - mode: RGB/RGBA
# - size: width/height
# - skip: how many bytes to skip to next chunk
def _parse_vp8x(chunk_size, fp):
if chunk_size < 10:
raise SyntaxError("bad VP8X chunk")
vp8x = fp.read(10)
if 10 != len(vp8x):
raise SyntaxError("bad VP8X chunk")
flags = i8(vp8x[0])
if (flags & 0b10000):
mode = 'RGBA'
else:
mode = 'RGB'
width = (i8(vp8x[4]) | (i8(vp8x[5]) << 8) | (i8(vp8x[6]) << 16)) + 1
height = (i8(vp8x[7]) | (i8(vp8x[8]) << 8) | (i8(vp8x[9]) << 16)) + 1
return mode, (width, height), chunk_size - 10
def _parse_vp8l(chunk_size, fp):
if chunk_size < 5:
raise SyntaxError("bad VP8L chunk")
vp8l = fp.read(5)
if 5 != len(vp8l):
raise SyntaxError("bad VP8L chunk")
vp8l = [i8(b) for b in vp8l]
# Check signature.
if 0x2f != vp8l[0] or 0 != (vp8l[4] >> 5):
raise SyntaxError("bad VP8L chunk")
if (vp8l[4] & 0b10000):
mode = 'RGBA'
else:
mode = 'RGB'
# 14 bits for each...
width = (vp8l[1] | ((vp8l[2] & 0b111111) << 8)) + 1
height = ((vp8l[2] >> 6) | (vp8l[3] << 2) | ((vp8l[4] & 0b1111) << 10)) + 1
return mode, (width, height), chunk_size - 5
def _parse_vp8(chunk_size, fp):
if chunk_size < 10:
raise SyntaxError("bad VP8 chunk")
vp8 = fp.read(10)
if 10 != len(vp8):
raise SyntaxError("bad VP8 chunk")
# Check signature.
if 0x9d != i8(vp8[3]) or 0x01 != i8(vp8[4]) or 0x2a != i8(vp8[5]):
raise SyntaxError("bad VP8 chunk")
mode = 'RGB'
width = (i8(vp8[6]) | (i8(vp8[7]) << 8)) & 0x3fff
height = (i8(vp8[8]) | (i8(vp8[9]) << 8)) & 0x3fff
return mode, (width, height), chunk_size - 10
class WebPImageFile(ImageFile.ImageFile):
@ -30,17 +79,102 @@ class WebPImageFile(ImageFile.ImageFile):
format_description = "WebP image"
def _open(self):
data, width, height, self.mode, icc_profile, exif = \
_webp.WebPDecode(self.fp.read())
if icc_profile:
self.info["icc_profile"] = icc_profile
if exif:
self.info["exif"] = exif
header = self.fp.read(12)
if 12 != len(header):
raise SyntaxError("not a WebP file")
self.size = width, height
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
if b'RIFF' != header[0:4] or b'WEBP' != header[8:12]:
raise SyntaxError("not a WebP file")
mode = None
size = None
lossy = None
first_chunk = True
while True:
chunk_header = self.fp.read(8)
if 8 != len(chunk_header):
if first_chunk:
raise SyntaxError("not a WebP file")
break
chunk_fourcc = chunk_header[0:4]
chunk_size = i32le(chunk_header[4:8])
if first_chunk:
if chunk_fourcc not in _VP8_MODES_BY_IDENTIFIER:
raise SyntaxError("not a WebP file")
if b'VP8X' == chunk_fourcc:
if first_chunk:
mode, size, chunk_size = _parse_vp8x(chunk_size, self.fp)
elif b'VP8L' == chunk_fourcc:
if first_chunk:
mode, size, chunk_size = _parse_vp8l(chunk_size, self.fp)
lossy = False
elif b'VP8 ' == chunk_fourcc:
if first_chunk:
mode, size, chunk_size = _parse_vp8(chunk_size, self.fp)
lossy = True
elif b'ALPH' == chunk_fourcc:
mode = 'RGBA'
elif b'EXIF' == chunk_fourcc:
exif = self.fp.read(chunk_size)
if chunk_size != len(exif):
raise SyntaxError("bad EXIF chunk")
self.info["exif"] = exif
chunk_size = 0
elif b'ICCP' == chunk_fourcc:
icc_profile = self.fp.read(chunk_size)
if chunk_size != len(icc_profile):
raise SyntaxError("bad ICCP chunk")
self.info["icc_profile"] = icc_profile
chunk_size = 0
if chunk_size > 0:
# Skip to next chunk.
pos = self.fp.tell()
self.fp.seek(chunk_size, os.SEEK_CUR)
if self.fp.tell() != (pos + chunk_size):
raise SyntaxError("not a WebP file")
first_chunk = False
if None in (mode, size, lossy):
raise SyntaxError("not a WebP file")
self.mode = mode
self.size = size
self.tile = [('webp', (0, 0) + size, 0,
# Decoder params: rawmode, has_alpha, width, height.
(mode, 1 if 'RGBA' == mode else 0, size[0], size[1]))]
def draft(self, mode, size):
if 1 != len(self.tile):
return
d, e, o, a = self.tile[0]
if mode in _VALID_WEBP_MODES:
a = mode, a[1], a[2], a[3]
self.mode = mode
if size:
e = e[0], e[1], size[0], size[1]
self.size = size
self.tile = [(d, e, o, a)]
return self
def _getexif(self):
from PIL.JpegImagePlugin import _getexif

View File

@ -0,0 +1,56 @@
from helper import unittest, PillowTestCase, fromstring, tostring
from PIL import Image
CODECS = dir(Image.core)
FILENAME = "Tests/images/hopper.webp"
DATA = tostring(Image.open(FILENAME).resize((512, 512)), "WEBP")
ALPHA_FILENAME = "Tests/images/transparent.webp"
ALPHA_DATA = tostring(Image.open(ALPHA_FILENAME).resize((512, 512)), "WEBP")
def draft(mode, size):
im = fromstring(DATA)
im.draft(mode, size)
return im
def alpha_draft(mode, size):
im = fromstring(ALPHA_DATA)
im.draft(mode, size)
return im
class TestImageDraft(PillowTestCase):
def setUp(self):
if "webp_decoder" not in CODECS:
self.skipTest("WebP support not available")
def test_size(self):
# Upscaling/downscaling to any size is supported.
self.assertEqual(draft("RGB", (1024, 1024)).size, (1024, 1024))
self.assertEqual(draft("RGB", (512, 512)).size, (512, 512))
self.assertEqual(draft("RGB", (256, 256)).size, (256, 256))
self.assertEqual(draft("RGB", (128, 128)).size, (128, 128))
self.assertEqual(draft("RGB", (64, 64)).size, (64, 64))
self.assertEqual(draft("RGB", (32, 32)).size, (32, 32))
def test_mode(self):
# Decoder only support RGB/RGBA output.
self.assertEqual(draft("1", (512, 512)).mode, "RGB")
self.assertEqual(draft("L", (512, 512)).mode, "RGB")
self.assertEqual(draft("RGB", (512, 512)).mode, "RGB")
self.assertEqual(draft("RGBA", (512, 512)).mode, "RGBA")
self.assertEqual(draft("YCbCr", (512, 512)).mode, "RGB")
self.assertEqual(alpha_draft("1", (512, 512)).mode, "RGBA")
self.assertEqual(alpha_draft("L", (512, 512)).mode, "RGBA")
self.assertEqual(alpha_draft("RGB", (512, 512)).mode, "RGB")
self.assertEqual(alpha_draft("RGBA", (512, 512)).mode, "RGBA")
self.assertEqual(alpha_draft("YCbCr", (512, 512)).mode, "RGBA")
if __name__ == '__main__':
unittest.main()
# End of file

View File

@ -3343,6 +3343,7 @@ extern PyObject* PyImaging_BitDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_FliDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_GifDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_HexDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_WebPDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_JpegDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_Jpeg2KDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_TiffLzwDecoderNew(PyObject* self, PyObject* args);
@ -3411,6 +3412,9 @@ static PyMethodDef functions[] = {
{"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, 1},
{"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, 1},
{"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, 1}, /* EPS=HEX! */
#ifdef HAVE_LIBWEBP
{"webp_decoder", (PyCFunction)PyImaging_WebPDecoderNew, 1},
#endif
#ifdef HAVE_LIBJPEG
{"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, 1},
{"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, 1},

113
_webp.c
View File

@ -9,7 +9,7 @@
#include <webp/mux.h>
#endif
PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
static PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
{
int width;
int height;
@ -132,113 +132,9 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args)
Py_RETURN_NONE;
}
PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args)
{
PyBytesObject *webp_string;
const uint8_t *webp;
Py_ssize_t size;
PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, *exif = NULL;
WebPDecoderConfig config;
VP8StatusCode vp8_status_code = VP8_STATUS_OK;
char* mode = "RGB";
if (!PyArg_ParseTuple(args, "S", &webp_string)) {
Py_RETURN_NONE;
}
if (!WebPInitDecoderConfig(&config)) {
Py_RETURN_NONE;
}
PyBytes_AsStringAndSize((PyObject *) webp_string, (char**)&webp, &size);
vp8_status_code = WebPGetFeatures(webp, size, &config.input);
if (vp8_status_code == VP8_STATUS_OK) {
// If we don't set it, we don't get alpha.
// Initialized to MODE_RGB
if (config.input.has_alpha) {
config.output.colorspace = MODE_RGBA;
mode = "RGBA";
}
#ifndef HAVE_WEBPMUX
vp8_status_code = WebPDecode(webp, size, &config);
#else
{
int copy_data = 0;
WebPData data = { webp, size };
WebPMuxFrameInfo image;
WebPData icc_profile_data = {0};
WebPData exif_data = {0};
WebPMux* mux = WebPMuxCreate(&data, copy_data);
if (NULL == mux)
goto end;
if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image))
{
WebPMuxDelete(mux);
goto end;
}
webp = image.bitstream.bytes;
size = image.bitstream.size;
vp8_status_code = WebPDecode(webp, size, &config);
if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data))
icc_profile = PyBytes_FromStringAndSize((const char*)icc_profile_data.bytes, icc_profile_data.size);
if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data))
exif = PyBytes_FromStringAndSize((const char*)exif_data.bytes, exif_data.size);
WebPDataClear(&image.bitstream);
WebPMuxDelete(mux);
}
#endif
}
if (vp8_status_code != VP8_STATUS_OK)
goto end;
if (config.output.colorspace < MODE_YUV) {
bytes = PyBytes_FromStringAndSize((char *)config.output.u.RGBA.rgba,
config.output.u.RGBA.size);
} else {
// Skipping YUV for now. Need Test Images.
// UNDONE -- unclear if we'll ever get here if we set mode_rgb*
bytes = PyBytes_FromStringAndSize((char *)config.output.u.YUVA.y,
config.output.u.YUVA.y_size);
}
#if PY_VERSION_HEX >= 0x03000000
pymode = PyUnicode_FromString(mode);
#else
pymode = PyString_FromString(mode);
#endif
ret = Py_BuildValue("SiiSSS", bytes, config.output.width,
config.output.height, pymode,
NULL == icc_profile ? Py_None : icc_profile,
NULL == exif ? Py_None : exif);
end:
WebPFreeDecBuffer(&config.output);
Py_XDECREF(bytes);
Py_XDECREF(pymode);
Py_XDECREF(icc_profile);
Py_XDECREF(exif);
if (Py_None == ret)
Py_RETURN_NONE;
return ret;
}
// Return the decoder's version number, packed in hexadecimal using 8bits for
// each of major/minor/revision. E.g: v2.5.7 is 0x020507.
PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){
static PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){
return Py_BuildValue("i", WebPGetDecoderVersion());
}
@ -246,20 +142,19 @@ PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){
* The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well.
* Files that are valid with 0.3 are reported as being invalid.
*/
PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){
static PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){
return Py_BuildValue("i", WebPGetDecoderVersion()==0x0103);
}
static PyMethodDef webpMethods[] =
{
{"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"},
{"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"},
{"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_VARARGS, "WebPVersion"},
{"WebPDecoderBuggyAlpha", WebPDecoderBuggyAlpha_wrapper, METH_VARARGS, "WebPDecoderBuggyAlpha"},
{NULL, NULL}
};
void addMuxFlagToModule(PyObject* m) {
static void addMuxFlagToModule(PyObject* m) {
#ifdef HAVE_WEBPMUX
PyModule_AddObject(m, "HAVE_WEBPMUX", Py_True);
#else

View File

@ -722,6 +722,54 @@ PyImaging_ZipDecoderNew(PyObject* self, PyObject* args)
#endif
/* -------------------------------------------------------------------- */
/* WebP */
/* -------------------------------------------------------------------- */
#ifdef HAVE_LIBWEBP
#include "WebP.h"
PyObject*
PyImaging_WebPDecoderNew(PyObject* self, PyObject* args)
{
ImagingDecoderObject* decoder;
WEBPSTATE* context;
char* mode;
char* rawmode; /* what we wan't from the decoder */
int has_alpha;
int width, height;
if (!PyArg_ParseTuple(args, "ssiii",
&mode, &rawmode,
&has_alpha, &width, &height))
return NULL;
decoder = PyImaging_DecoderNew(sizeof(WEBPSTATE));
if (decoder == NULL)
return NULL;
if (get_unpacker(decoder, mode, rawmode) < 0)
return NULL;
decoder->decode = ImagingWebPDecode;
decoder->cleanup = ImagingWebPDecodeCleanup;
context = (WEBPSTATE*)decoder->state.context;
strncpy(context->rawmode, rawmode, IMAGING_MODE_LENGTH - 1);
context->rawmode[IMAGING_MODE_LENGTH - 1] = '\0';
context->has_alpha = has_alpha;
context->width = width;
context->height = height;
context->decoder = NULL;
return (PyObject*) decoder;
}
#endif
/* -------------------------------------------------------------------- */
/* JPEG */
/* -------------------------------------------------------------------- */

View File

@ -419,6 +419,11 @@ extern int ImagingGifEncode(Imaging im, ImagingCodecState state,
UINT8* buffer, int bytes);
extern int ImagingHexDecode(Imaging im, ImagingCodecState state,
UINT8* buffer, int bytes);
#ifdef HAVE_LIBWEBP
extern int ImagingWebPDecode(Imaging im, ImagingCodecState state,
UINT8* buffer, int bytes);
extern int ImagingWebPDecodeCleanup(ImagingCodecState state);
#endif
#ifdef HAVE_LIBJPEG
extern int ImagingJpegDecode(Imaging im, ImagingCodecState state,
UINT8* buffer, int bytes);

31
libImaging/WebP.h Normal file
View File

@ -0,0 +1,31 @@
/*
* The Python Imaging Library.
*
* declarations for the WebP codec interface.
*/
#include <webp/encode.h>
#include <webp/decode.h>
#include <webp/types.h>
/* -------------------------------------------------------------------- */
/* Decoder */
typedef struct {
/* CONFIGURATION */
/* Decoder output mode (input to the shuffler). */
char rawmode[IMAGING_MODE_LENGTH];
/* Original image information. */
int has_alpha;
int width, height;
/* PRIVATE CONTEXT (set by decoder) */
WebPDecoderConfig config;
WebPIDecoder *decoder;
} WEBPSTATE;

145
libImaging/WebPDecode.c Normal file
View File

@ -0,0 +1,145 @@
/*
* The Python Imaging Library.
*
* decoder for WebP image data.
*/
#ifdef HAVE_LIBWEBP
#include "Imaging.h"
#include "WebP.h"
#include <assert.h>
static int _vp8_status_to_codec_status(VP8StatusCode code)
{
switch (code)
{
case VP8_STATUS_OK:
return 0;
case VP8_STATUS_OUT_OF_MEMORY:
return IMAGING_CODEC_MEMORY;
case VP8_STATUS_BITSTREAM_ERROR:
case VP8_STATUS_NOT_ENOUGH_DATA:
case VP8_STATUS_SUSPENDED:
return IMAGING_CODEC_BROKEN;
case VP8_STATUS_INVALID_PARAM:
case VP8_STATUS_UNSUPPORTED_FEATURE:
return IMAGING_CODEC_CONFIG;
default:
case VP8_STATUS_USER_ABORT:
return IMAGING_CODEC_UNKNOWN;
}
}
/* -------------------------------------------------------------------- */
/* Decoder */
/* -------------------------------------------------------------------- */
int ImagingWebPDecode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes)
{
WEBPSTATE *context = (WEBPSTATE *)state->context;
VP8StatusCode vp8_status_code;
if (!state->state)
{
WebPDecoderConfig *config = &context->config;
if (!WebPInitDecoderConfig(config))
{
/* Mismatched version. */
state->errcode = IMAGING_CODEC_CONFIG;
return -1;
}
if (0 == strcmp("RGBA", context->rawmode))
config->output.colorspace = MODE_RGBA;
else
config->output.colorspace = MODE_RGB;
if (state->xsize != context->width || state->ysize != context->height)
{
config->options.scaled_width = state->xsize;
config->options.scaled_height = state->ysize;
config->options.use_scaling = 1;
}
context->decoder = WebPIDecode(NULL, 0, config);
if (NULL == context->decoder)
{
state->errcode = _vp8_status_to_codec_status(vp8_status_code);
return -1;
}
state->state = 1;
}
/* Consume the buffer, decoding as much as possible. */
vp8_status_code = WebPIAppend(context->decoder, buf, bytes);
if (VP8_STATUS_NOT_ENOUGH_DATA != vp8_status_code &&
VP8_STATUS_SUSPENDED != vp8_status_code &&
VP8_STATUS_OK != vp8_status_code)
{
state->errcode = _vp8_status_to_codec_status(vp8_status_code);
return -1;
}
if (VP8_STATUS_NOT_ENOUGH_DATA != vp8_status_code)
{
/* Check progress, and unpack available data. */
const uint8_t *rgba;
int last_y;
int width;
int height;
int stride;
rgba = WebPIDecGetRGB(context->decoder, &last_y, &width, &height, &stride);
if (NULL != rgba)
{
assert(width == state->xsize);
assert(height == state->ysize);
assert(last_y <= state->ysize);
for (; state->y < last_y; ++state->y)
{
assert(state->y < state->ysize);
state->shuffle((UINT8*) im->image[state->y + state->yoff] +
state->xoff * im->pixelsize,
rgba + state->y * stride, state->xsize);
}
}
}
if (VP8_STATUS_OK == vp8_status_code)
{
/* We're finished! */
state->errcode = IMAGING_CODEC_END;
return -1;
}
/* Return number of bytes consumed. */
return bytes;
}
/* -------------------------------------------------------------------- */
/* Cleanup */
/* -------------------------------------------------------------------- */
int ImagingWebPDecodeCleanup(ImagingCodecState state)
{
WEBPSTATE* context = (WEBPSTATE*) state->context;
if (NULL != context->decoder)
{
WebPFreeDecBuffer(&context->config.output);
WebPIDelete(context->decoder);
context->decoder = NULL;
}
return -1;
}
#endif

View File

@ -37,7 +37,7 @@ _LIB_IMAGING = (
"RankFilter", "RawDecode", "RawEncode", "Storage", "SunRleDecode",
"TgaRleDecode", "Unpack", "UnpackYCC", "UnsharpMask", "XbmDecode",
"XbmEncode", "ZipDecode", "ZipEncode", "TiffDecode", "Incremental",
"Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur")
"Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur", "WebPDecode")
def _add_directory(path, dir, where=None):
@ -535,6 +535,14 @@ class pil_build_ext(build_ext):
libs.extend(["kernel32", "user32", "gdi32"])
if struct.unpack("h", "\0\1".encode('ascii'))[0] == 1:
defs.append(("WORDS_BIGENDIAN", None))
if os.path.isfile("_webp.c") and feature.webp:
webp_defs = [("HAVE_LIBWEBP", None)]
webp_libs = [feature.webp]
if feature.webpmux:
webp_defs.append(("HAVE_WEBPMUX", None))
webp_libs.append(feature.webpmux)
libs.extend(webp_libs)
defs.extend(webp_defs)
exts = [(Extension(
"PIL._imaging", files, libraries=libs, define_macros=defs))]
@ -564,16 +572,8 @@ class pil_build_ext(build_ext):
libraries=["lcms2"] + extra))
if os.path.isfile("_webp.c") and feature.webp:
libs = [feature.webp]
defs = []
if feature.webpmux:
defs.append(("HAVE_WEBPMUX", None))
libs.append(feature.webpmux)
libs.append(feature.webpmux.replace('pmux','pdemux'))
exts.append(Extension(
"PIL._webp", ["_webp.c"], libraries=libs, define_macros=defs))
"PIL._webp", ["_webp.c"], libraries=webp_libs, define_macros=webp_defs))
if sys.platform == "darwin":
# locate Tcl/Tk frameworks