Merge branch 'master' of https://github.com/etienned/Pillow into etienned-master

This commit is contained in:
Alex Clark 2013-03-08 08:36:53 -05:00
commit b03abf2e58
5 changed files with 425 additions and 4 deletions

View File

@ -36,6 +36,7 @@ __version__ = "0.6"
import array, struct import array, struct
from PIL import Image, ImageFile, _binary from PIL import Image, ImageFile, _binary
from JpegPresets import presets
i8 = _binary.i8 i8 = _binary.i8
o8 = _binary.o8 o8 = _binary.o8
@ -416,6 +417,31 @@ RAWMODE = {
"YCbCr": "YCbCr", "YCbCr": "YCbCr",
} }
zigzag_index = ( 0, 1, 5, 6, 14, 15, 27, 28,
2, 4, 7, 13, 16, 26, 29, 42,
3, 8, 12, 17, 25, 30, 41, 43,
9, 11, 18, 24, 31, 40, 44, 53,
10, 19, 23, 32, 39, 45, 52, 54,
20, 22, 33, 38, 46, 51, 55, 60,
21, 34, 37, 47, 50, 56, 59, 61,
35, 36, 48, 49, 57, 58, 62, 63)
samplings = {
(1, 1, 1, 1, 1, 1): 0,
(2, 1, 1, 1, 1, 1): 1,
(2, 2, 1, 1, 1, 1): 2,
}
def convert_dict_qtables(qtables):
qtables = [qtables[key] for key in xrange(len(qtables)) if qtables.has_key(key)]
for idx, table in enumerate(qtables):
qtables[idx] = [table[i] for i in zigzag_index]
return qtables
def get_sampling(im):
sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
return samplings.get(sampling, -1)
def _save(im, fp, filename): def _save(im, fp, filename):
try: try:
@ -427,13 +453,72 @@ def _save(im, fp, filename):
dpi = info.get("dpi", (0, 0)) dpi = info.get("dpi", (0, 0))
quality = info.get("quality", 0)
subsampling = info.get("subsampling", -1) subsampling = info.get("subsampling", -1)
qtables = info.get("qtables")
if quality == "keep":
quality = 0
subsampling = "keep"
qtables = "keep"
elif quality in presets:
preset = presets[quality]
quality = 0
subsampling = preset.get('subsampling', -1)
qtables = preset.get('quantization')
elif not isinstance(quality, int):
raise ValueError("Invalid quality setting")
else:
if subsampling in presets:
subsampling = presets[subsampling].get('subsampling', -1)
if qtables in presets:
qtables = presets[qtables].get('quantization')
if subsampling == "4:4:4": if subsampling == "4:4:4":
subsampling = 0 subsampling = 0
elif subsampling == "4:2:2": elif subsampling == "4:2:2":
subsampling = 1 subsampling = 1
elif subsampling == "4:1:1": elif subsampling == "4:1:1":
subsampling = 2 subsampling = 2
elif subsampling == "keep":
if im.format != "JPEG":
raise ValueError("Cannot use 'keep' when original image is not a JPEG")
subsampling = get_sampling(im)
def validate_qtables(qtables):
if qtables is None:
return qtables
if isinstance(qtables, basestring):
try:
lines = [int(num) for line in qtables.splitlines()
for num in line.split('#', 1)[0].split()]
except ValueError:
raise ValueError("Invalid quantization table")
else:
qtables = [lines[s:s+64] for s in xrange(0, len(lines), 64)]
if isinstance(qtables, (tuple, list, dict)):
if isinstance(qtables, dict):
qtables = convert_dict_qtables(qtables)
elif isinstance(qtables, tuple):
qtables = list(qtables)
if not (0 < len(qtables) < 5):
raise ValueError("None or too many quantization tables")
for idx, table in enumerate(qtables):
try:
if len(table) != 64:
raise
table = array.array('b', table)
except TypeError:
raise ValueError("Invalid quantization table")
else:
qtables[idx] = list(table)
return qtables
if qtables == "keep":
if im.format != "JPEG":
raise ValueError("Cannot use 'keep' when original image is not a JPEG")
qtables = getattr(im, "quantization", None)
qtables = validate_qtables(qtables)
extra = b"" extra = b""
@ -454,7 +539,7 @@ def _save(im, fp, filename):
# get keyword arguments # get keyword arguments
im.encoderconfig = ( im.encoderconfig = (
info.get("quality", 0), quality,
# "progressive" is the official name, but older documentation # "progressive" is the official name, but older documentation
# says "progression" # says "progression"
# FIXME: issue a warning if the wrong form is used (post-1.1.7) # FIXME: issue a warning if the wrong form is used (post-1.1.7)
@ -464,6 +549,7 @@ def _save(im, fp, filename):
info.get("streamtype", 0), info.get("streamtype", 0),
dpi[0], dpi[1], dpi[0], dpi[1],
subsampling, subsampling,
qtables,
extra, extra,
info.get("exif", b"") info.get("exif", b"")
) )

250
PIL/JpegPresets.py Normal file
View File

@ -0,0 +1,250 @@
"""
JPEG quality settings equivalent to the Photoshop settings.
More presets can be added to the presets dict if needed.
Can be use when saving JPEG file.
To apply the preset, specify:
- quality=preset name
To apply only the quantization table:
- qtables=preset name
To apply only the subsampling setting:
- subsampling=preset name
Example:
im.save("image_name.jpg", quality="web_high")
Subsampling
-----------
Subsampling is the practice of encoding images by implementing less resolution
for chroma information than for luma information.
(ref.: http://en.wikipedia.org/wiki/Chroma_subsampling)
Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and
4:1:1 (or 4:2:0?).
You can get the subsampling of a JPEG with the
`JpegImagePlugin.get_subsampling(im)` function.
Quantization tables
-------------------
They are values use by the DCT (Discrete cosine transform) to remove
*unnecessary* information from the image (the lossy part of the compression).
(ref.: http://en.wikipedia.org/wiki/Quantization_matrix#Quantization_matrices,
http://en.wikipedia.org/wiki/JPEG#Quantization)
You can get the quantization tables of a JPEG with:
im.quantization
This will return a dict with a number of arrays. You can pass this dict directly
as the qtables argument when saving a JPEG.
The tables format between im.quantization and quantization in presets differ in
3 ways:
1. The base container of the preset is a list with sublists instead of dict.
dict[0] -> list[0], dict[1] -> list[1], ...
2. Each table in a preset is a list instead of an array.
3. The zigzag order is remove in the preset (needed by libjpeg >= 6a).
You can convert the dict format to the preset format with the
`JpegImagePlugin.convert_dict_qtables(dict_qtables)` function.
Libjpeg ref.: http://www.jpegcameras.com/libjpeg/libjpeg-3.html
"""
presets = {
'web_low': {'subsampling': 2, # "4:1:1"
'quantization': [
[20, 16, 25, 39, 50, 46, 62, 68,
16, 18, 23, 38, 38, 53, 65, 68,
25, 23, 31, 38, 53, 65, 68, 68,
39, 38, 38, 53, 65, 68, 68, 68,
50, 38, 53, 65, 68, 68, 68, 68,
46, 53, 65, 68, 68, 68, 68, 68,
62, 65, 68, 68, 68, 68, 68, 68,
68, 68, 68, 68, 68, 68, 68, 68],
[21, 25, 32, 38, 54, 68, 68, 68,
25, 28, 24, 38, 54, 68, 68, 68,
32, 24, 32, 43, 66, 68, 68, 68,
38, 38, 43, 53, 68, 68, 68, 68,
54, 54, 66, 68, 68, 68, 68, 68,
68, 68, 68, 68, 68, 68, 68, 68,
68, 68, 68, 68, 68, 68, 68, 68,
68, 68, 68, 68, 68, 68, 68, 68]
]},
'web_medium': {'subsampling': 2, # "4:1:1"
'quantization': [
[16, 11, 11, 16, 23, 27, 31, 30,
11, 12, 12, 15, 20, 23, 23, 30,
11, 12, 13, 16, 23, 26, 35, 47,
16, 15, 16, 23, 26, 37, 47, 64,
23, 20, 23, 26, 39, 51, 64, 64,
27, 23, 26, 37, 51, 64, 64, 64,
31, 23, 35, 47, 64, 64, 64, 64,
30, 30, 47, 64, 64, 64, 64, 64],
[17, 15, 17, 21, 20, 26, 38, 48,
15, 19, 18, 17, 20, 26, 35, 43,
17, 18, 20, 22, 26, 30, 46, 53,
21, 17, 22, 28, 30, 39, 53, 64,
20, 20, 26, 30, 39, 48, 64, 64,
26, 26, 30, 39, 48, 63, 64, 64,
38, 35, 46, 53, 64, 64, 64, 64,
48, 43, 53, 64, 64, 64, 64, 64]
]},
'web_high': {'subsampling': 0, # "4:4:4"
'quantization': [
[ 6, 4, 4, 6, 9, 11, 12, 16,
4, 5, 5, 6, 8, 10, 12, 12,
4, 5, 5, 6, 10, 12, 14, 19,
6, 6, 6, 11, 12, 15, 19, 28,
9, 8, 10, 12, 16, 20, 27, 31,
11, 10, 12, 15, 20, 27, 31, 31,
12, 12, 14, 19, 27, 31, 31, 31,
16, 12, 19, 28, 31, 31, 31, 31],
[ 7, 7, 13, 24, 26, 31, 31, 31,
7, 12, 16, 21, 31, 31, 31, 31,
13, 16, 17, 31, 31, 31, 31, 31,
24, 21, 31, 31, 31, 31, 31, 31,
26, 31, 31, 31, 31, 31, 31, 31,
31, 31, 31, 31, 31, 31, 31, 31,
31, 31, 31, 31, 31, 31, 31, 31,
31, 31, 31, 31, 31, 31, 31, 31]
]},
'web_very_high': {'subsampling': 0, # "4:4:4"
'quantization': [
[ 2, 2, 2, 2, 3, 4, 5, 6,
2, 2, 2, 2, 3, 4, 5, 6,
2, 2, 2, 2, 4, 5, 7, 9,
2, 2, 2, 4, 5, 7, 9, 12,
3, 3, 4, 5, 8, 10, 12, 12,
4, 4, 5, 7, 10, 12, 12, 12,
5, 5, 7, 9, 12, 12, 12, 12,
6, 6, 9, 12, 12, 12, 12, 12],
[ 3, 3, 5, 9, 13, 15, 15, 15,
3, 4, 6, 11, 14, 12, 12, 12,
5, 6, 9, 14, 12, 12, 12, 12,
9, 11, 14, 12, 12, 12, 12, 12,
13, 14, 12, 12, 12, 12, 12, 12,
15, 12, 12, 12, 12, 12, 12, 12,
15, 12, 12, 12, 12, 12, 12, 12,
15, 12, 12, 12, 12, 12, 12, 12]
]},
'web_maximum': {'subsampling': 0, # "4:4:4"
'quantization': [
[ 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 2,
1, 1, 1, 1, 1, 1, 2, 2,
1, 1, 1, 1, 1, 2, 2, 3,
1, 1, 1, 1, 2, 2, 3, 3,
1, 1, 1, 2, 2, 3, 3, 3,
1, 1, 2, 2, 3, 3, 3, 3],
[ 1, 1, 1, 2, 2, 3, 3, 3,
1, 1, 1, 2, 3, 3, 3, 3,
1, 1, 1, 3, 3, 3, 3, 3,
2, 2, 3, 3, 3, 3, 3, 3,
2, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3]
]},
'low': {'subsampling': 2, # "4:1:1"
'quantization': [
[18, 14, 14, 21, 30, 35, 34, 17,
14, 16, 16, 19, 26, 23, 12, 12,
14, 16, 17, 21, 23, 12, 12, 12,
21, 19, 21, 23, 12, 12, 12, 12,
30, 26, 23, 12, 12, 12, 12, 12,
35, 23, 12, 12, 12, 12, 12, 12,
34, 12, 12, 12, 12, 12, 12, 12,
17, 12, 12, 12, 12, 12, 12, 12],
[20, 19, 22, 27, 20, 20, 17, 17,
19, 25, 23, 14, 14, 12, 12, 12,
22, 23, 14, 14, 12, 12, 12, 12,
27, 14, 14, 12, 12, 12, 12, 12,
20, 14, 12, 12, 12, 12, 12, 12,
20, 12, 12, 12, 12, 12, 12, 12,
17, 12, 12, 12, 12, 12, 12, 12,
17, 12, 12, 12, 12, 12, 12, 12]
]},
'medium': {'subsampling': 2, # "4:1:1"
'quantization': [
[12, 8, 8, 12, 17, 21, 24, 17,
8, 9, 9, 11, 15, 19, 12, 12,
8, 9, 10, 12, 19, 12, 12, 12,
12, 11, 12, 21, 12, 12, 12, 12,
17, 15, 19, 12, 12, 12, 12, 12,
21, 19, 12, 12, 12, 12, 12, 12,
24, 12, 12, 12, 12, 12, 12, 12,
17, 12, 12, 12, 12, 12, 12, 12],
[13, 11, 13, 16, 20, 20, 17, 17,
11, 14, 14, 14, 14, 12, 12, 12,
13, 14, 14, 14, 12, 12, 12, 12,
16, 14, 14, 12, 12, 12, 12, 12,
20, 14, 12, 12, 12, 12, 12, 12,
20, 12, 12, 12, 12, 12, 12, 12,
17, 12, 12, 12, 12, 12, 12, 12,
17, 12, 12, 12, 12, 12, 12, 12]
]},
'high': {'subsampling': 0, # "4:4:4"
'quantization': [
[ 6, 4, 4, 6, 9, 11, 12, 16,
4, 5, 5, 6, 8, 10, 12, 12,
4, 5, 5, 6, 10, 12, 12, 12,
6, 6, 6, 11, 12, 12, 12, 12,
9, 8, 10, 12, 12, 12, 12, 12,
11, 10, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12,
16, 12, 12, 12, 12, 12, 12, 12],
[ 7, 7, 13, 24, 20, 20, 17, 17,
7, 12, 16, 14, 14, 12, 12, 12,
13, 16, 14, 14, 12, 12, 12, 12,
24, 14, 14, 12, 12, 12, 12, 12,
20, 14, 12, 12, 12, 12, 12, 12,
20, 12, 12, 12, 12, 12, 12, 12,
17, 12, 12, 12, 12, 12, 12, 12,
17, 12, 12, 12, 12, 12, 12, 12]
]},
'maximum': {'subsampling': 0, # "4:4:4"
'quantization': [
[ 2, 2, 2, 2, 3, 4, 5, 6,
2, 2, 2, 2, 3, 4, 5, 6,
2, 2, 2, 2, 4, 5, 7, 9,
2, 2, 2, 4, 5, 7, 9, 12,
3, 3, 4, 5, 8, 10, 12, 12,
4, 4, 5, 7, 10, 12, 12, 12,
5, 5, 7, 9, 12, 12, 12, 12,
6, 6, 9, 12, 12, 12, 12, 12],
[ 3, 3, 5, 9, 13, 15, 15, 15,
3, 4, 6, 10, 14, 12, 12, 12,
5, 6, 9, 14, 12, 12, 12, 12,
9, 10, 14, 12, 12, 12, 12, 12,
13, 14, 12, 12, 12, 12, 12, 12,
15, 12, 12, 12, 12, 12, 12, 12,
15, 12, 12, 12, 12, 12, 12, 12,
15, 12, 12, 12, 12, 12, 12, 12]
]},
}

View File

@ -506,6 +506,69 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args)
#include "Jpeg.h" #include "Jpeg.h"
static unsigned int** get_qtables_arrays(PyObject* qtables) {
PyObject* tables;
PyObject* table;
PyObject* table_data;
int i, j, num_tables;
unsigned int **qarrays;
if (qtables == Py_None) {
return NULL;
}
if (!PySequence_Check(qtables)) {
PyErr_SetString(PyExc_ValueError, "Invalid quantization tables");
return NULL;
}
tables = PySequence_Fast(qtables, "expected a sequence");
num_tables = PySequence_Size(qtables);
if (num_tables < 2 || num_tables > NUM_QUANT_TBLS) {
PyErr_SetString(PyExc_ValueError, "Not a valid numbers of quantization tables. Should be between 2 and 4.");
return NULL;
}
qarrays = (unsigned int**) PyMem_Malloc(num_tables * sizeof(unsigned int));
if (!qarrays) {
Py_DECREF(tables);
PyErr_NoMemory();
return NULL;
}
for (i = 0; i < num_tables; i++) {
table = PySequence_Fast_GET_ITEM(tables, i);
if (!PySequence_Check(table)) {
Py_DECREF(tables);
PyErr_SetString(PyExc_ValueError, "Invalid quantization tables");
return NULL;
}
if (PySequence_Size(table) != DCTSIZE2) {
Py_DECREF(tables);
PyErr_SetString(PyExc_ValueError, "Invalid quantization tables");
return NULL;
}
table_data = PySequence_Fast(table, "expected a sequence");
qarrays[i] = (unsigned int*) PyMem_Malloc(DCTSIZE2 * sizeof(unsigned int));
if (!qarrays[i]) {
Py_DECREF(tables);
PyErr_NoMemory();
return NULL;
}
for (j = 0; j < DCTSIZE2; j++) {
qarrays[i][j] = PyInt_AS_LONG(PySequence_Fast_GET_ITEM(table_data, j));
}
}
Py_DECREF(tables);
if (PyErr_Occurred()) {
PyMem_Free(qarrays);
qarrays = NULL;
}
return qarrays;
}
PyObject* PyObject*
PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) PyImaging_JpegEncoderNew(PyObject* self, PyObject* args)
{ {
@ -520,15 +583,17 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args)
int streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ int streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
int xdpi = 0, ydpi = 0; int xdpi = 0, ydpi = 0;
int subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ int subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
PyObject* qtables;
unsigned int **qarrays = NULL;
char* extra = NULL; char* extra = NULL;
int extra_size; int extra_size;
char* rawExif = NULL; char* rawExif = NULL;
int rawExifLen = 0; int rawExifLen = 0;
if (!PyArg_ParseTuple(args, "ss|iiiiiiii"PY_ARG_BYTES_LENGTH""PY_ARG_BYTES_LENGTH, if (!PyArg_ParseTuple(args, "ss|iiiiiiiiO"PY_ARG_BYTES_LENGTH""PY_ARG_BYTES_LENGTH,
&mode, &rawmode, &quality, &mode, &rawmode, &quality,
&progressive, &smooth, &optimize, &streamtype, &progressive, &smooth, &optimize, &streamtype,
&xdpi, &ydpi, &subsampling, &extra, &extra_size, &xdpi, &ydpi, &subsampling, &qtables, &extra, &extra_size,
&rawExif, &rawExifLen)) &rawExif, &rawExifLen))
return NULL; return NULL;
@ -539,6 +604,8 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args)
if (get_packer(encoder, mode, rawmode) < 0) if (get_packer(encoder, mode, rawmode) < 0)
return NULL; return NULL;
qarrays = get_qtables_arrays(qtables);
if (extra && extra_size > 0) { if (extra && extra_size > 0) {
char* p = malloc(extra_size); char* p = malloc(extra_size);
if (!p) if (!p)
@ -560,6 +627,7 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args)
encoder->encode = ImagingJpegEncode; encoder->encode = ImagingJpegEncode;
((JPEGENCODERSTATE*)encoder->state.context)->quality = quality; ((JPEGENCODERSTATE*)encoder->state.context)->quality = quality;
((JPEGENCODERSTATE*)encoder->state.context)->qtables = qarrays;
((JPEGENCODERSTATE*)encoder->state.context)->subsampling = subsampling; ((JPEGENCODERSTATE*)encoder->state.context)->subsampling = subsampling;
((JPEGENCODERSTATE*)encoder->state.context)->progressive = progressive; ((JPEGENCODERSTATE*)encoder->state.context)->progressive = progressive;
((JPEGENCODERSTATE*)encoder->state.context)->smooth = smooth; ((JPEGENCODERSTATE*)encoder->state.context)->smooth = smooth;

View File

@ -88,6 +88,9 @@ typedef struct {
/* Chroma Subsampling (-1=default, 0=none, 1=medium, 2=high) */ /* Chroma Subsampling (-1=default, 0=none, 1=medium, 2=high) */
int subsampling; int subsampling;
/* Custom quantization tables () */
unsigned int **qtables;
/* Extra data (to be injected after header) */ /* Extra data (to be injected after header) */
char* extra; int extra_size; char* extra; int extra_size;

View File

@ -143,8 +143,22 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes)
/* Compressor configuration */ /* Compressor configuration */
jpeg_set_defaults(&context->cinfo); jpeg_set_defaults(&context->cinfo);
if (context->quality > 0)
/* Use custom quantization tables */
if (context->qtables) {
int i;
int quality = 100;
if (context->quality > 0) {
quality = context->quality;
}
for (i = 0; i < sizeof(context->qtables)/sizeof(unsigned int); i++) {
// TODO: Should add support for none baseline
jpeg_add_quant_table(&context->cinfo, i, context->qtables[i],
quality, TRUE);
}
} else if (context->quality > 0) {
jpeg_set_quality(&context->cinfo, context->quality, 1); jpeg_set_quality(&context->cinfo, context->quality, 1);
}
/* Set subsampling options */ /* Set subsampling options */
switch (context->subsampling) switch (context->subsampling)