diff --git a/.travis.yml b/.travis.yml index 8b6c9e55d..3c95c001c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,6 +45,9 @@ install: # openjpeg - pushd depends && ./install_openjpeg.sh && popd + # libimagequant + - pushd depends && ./install_imagequant.sh && popd + script: - if [ "$TRAVIS_PYTHON_VERSION" != "nightly" ]; then coverage erase; fi - python setup.py clean diff --git a/PIL/Image.py b/PIL/Image.py index 28a76882b..7479865e9 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -185,6 +185,7 @@ ADAPTIVE = 1 MEDIANCUT = 0 MAXCOVERAGE = 1 FASTOCTREE = 2 +LIBIMAGEQUANT = 3 # categories NORMAL = 0 @@ -961,6 +962,7 @@ class Image(object): :param method: 0 = median cut 1 = maximum coverage 2 = fast octree + 3 = libimagequant :param kmeans: Integer :param palette: Quantize to the :py:class:`PIL.ImagingPalette` palette. :returns: A new image @@ -975,10 +977,11 @@ class Image(object): if self.mode == 'RGBA': method = 2 - if self.mode == 'RGBA' and method != 2: + if self.mode == 'RGBA' and method not in (2, 3): # Caller specified an invalid mode. - raise ValueError('Fast Octree (method == 2) is the ' + - ' only valid method for quantizing RGBA images') + raise ValueError( + 'Fast Octree (method == 2) and libimagequant (method == 3) ' + + 'are the only valid methods for quantizing RGBA images') if palette: # use palette from reference image diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 986a0a6cc..6ecc8dd7d 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -6,31 +6,46 @@ from PIL import Image class TestImageQuantize(PillowTestCase): def test_sanity(self): - im = hopper() + image = hopper() + converted = image.quantize() + self.assert_image(converted, 'P', converted.size) + self.assert_image_similar(converted.convert('RGB'), image, 10) - im = im.quantize() - self.assert_image(im, "P", im.size) + image = hopper() + converted = image.quantize(palette=hopper('P')) + self.assert_image(converted, 'P', converted.size) + self.assert_image_similar(converted.convert('RGB'), image, 60) - im = hopper() - im = im.quantize(palette=hopper("P")) - self.assert_image(im, "P", im.size) + def test_libimagequant_quantize(self): + image = hopper() + try: + converted = image.quantize(100, Image.LIBIMAGEQUANT) + except ValueError as ex: + if 'dependency' in str(ex).lower(): + self.skipTest('libimagequant support not available') + else: + raise + self.assert_image(converted, 'P', converted.size) + self.assert_image_similar(converted.convert('RGB'), image, 15) + assert len(converted.getcolors()) == 100 def test_octree_quantize(self): - im = hopper() - - im = im.quantize(100, Image.FASTOCTREE) - self.assert_image(im, "P", im.size) - - assert len(im.getcolors()) == 100 + image = hopper() + converted = image.quantize(100, Image.FASTOCTREE) + self.assert_image(converted, 'P', converted.size) + self.assert_image_similar(converted.convert('RGB'), image, 20) + assert len(converted.getcolors()) == 100 def test_rgba_quantize(self): - im = hopper('RGBA') - im.quantize() - self.assertRaises(Exception, lambda: im.quantize(method=0)) + image = hopper('RGBA') + image.quantize() + self.assertRaises(Exception, lambda: image.quantize(method=0)) def test_quantize(self): - im = Image.open('Tests/images/caption_6_33_22.png') - im.convert('RGB').quantize().convert('RGB') + image = Image.open('Tests/images/caption_6_33_22.png').convert('RGB') + converted = image.quantize() + self.assert_image(converted, 'P', converted.size) + self.assert_image_similar(converted.convert('RGB'), image, 1) if __name__ == '__main__': diff --git a/depends/README.rst b/depends/README.rst index dd05b86ca..050173679 100644 --- a/depends/README.rst +++ b/depends/README.rst @@ -1,8 +1,8 @@ Depends ======= -``install_openjpeg.sh`` and ``install_webp.sh`` can be used to -download, build & install non-packaged dependencies; useful for +``install_openjpeg.sh``, ``install_webp.sh`` and ``install_imagequant.sh`` can +be used to download, build & install non-packaged dependencies; useful for testing with Travis CI. The other scripts can be used to install all of the dependencies for diff --git a/depends/debian_8.2.sh b/depends/debian_8.2.sh index dd0679a9f..96e6a8e2b 100755 --- a/depends/debian_8.2.sh +++ b/depends/debian_8.2.sh @@ -14,3 +14,4 @@ sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \ python-tk python3-tk ./install_openjpeg.sh +./install_imagequant.sh diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh new file mode 100755 index 000000000..dd497dc3c --- /dev/null +++ b/depends/install_imagequant.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# install libimagequant + +git clone -b 2.6.0 https://github.com/pornel/pngquant + +pushd pngquant + +make -C lib shared +sudo cp lib/libimagequant.so* /usr/lib/ +sudo cp lib/libimagequant.h /usr/include/ + +popd diff --git a/depends/ubuntu_12.04.sh b/depends/ubuntu_12.04.sh index e9b16d2b4..9bfae43b0 100755 --- a/depends/ubuntu_12.04.sh +++ b/depends/ubuntu_12.04.sh @@ -14,3 +14,4 @@ sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \ ./install_openjpeg.sh ./install_webp.sh +./install_imagequant.sh diff --git a/depends/ubuntu_14.04.sh b/depends/ubuntu_14.04.sh index 14b9e7066..a548f74fa 100755 --- a/depends/ubuntu_14.04.sh +++ b/depends/ubuntu_14.04.sh @@ -12,3 +12,4 @@ sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-dev \ python-tk python3-tk ./install_openjpeg.sh +./install_imagequant.sh diff --git a/docs/installation.rst b/docs/installation.rst index 7d4f8735c..022823e7e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -140,6 +140,8 @@ Many of Pillow's features require external libraries: * **libfreetype** provides type related services +* **libimagequant** provides improved color quantization + * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and @@ -190,17 +192,17 @@ Build Options * Build flags: ``--disable-zlib``, ``--disable-jpeg``, ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, ``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, - ``--disable-webpmux``, ``--disable-jpeg2000``. Disable building the - corresponding feature even if the development libraries are present - on the building machine. + ``--disable-webpmux``, ``--disable-jpeg2000``, ``--disable-imagequant``. + Disable building the corresponding feature even if the development + libraries are present on the building machine. * Build flags: ``--enable-zlib``, ``--enable-jpeg``, ``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``, ``--enable-tk``, ``--enable-lcms``, ``--enable-webp``, - ``--enable-webpmux``, ``--enable-jpeg2000``. Require that the - corresponding feature is built. The build will raise an exception if - the libraries are not found. Webpmux (WebP metadata) relies on WebP - support. Tcl and Tk also must be used together. + ``--enable-webpmux``, ``--enable-jpeg2000``, ``--enable-imagequant``. + Require that the corresponding feature is built. The build will raise + an exception if the libraries are not found. Webpmux (WebP metadata) + relies on WebP support. Tcl and Tk also must be used together. * Build flag: ``--disable-platform-guessing``. Skips all of the platform dependent guessing of include and library directories for diff --git a/libImaging/Quant.c b/libImaging/Quant.c index 5b8a8d994..6c122eee2 100644 --- a/libImaging/Quant.c +++ b/libImaging/Quant.c @@ -27,6 +27,7 @@ #include "QuantTypes.h" #include "QuantOctree.h" +#include "QuantPngQuant.h" #include "QuantHash.h" #include "QuantHeap.h" @@ -1483,8 +1484,8 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) strcmp(im->mode, "RGB") != 0 && strcmp(im->mode, "RGBA") !=0) return ImagingError_ModeError(); - /* only octree supports RGBA */ - if (!strcmp(im->mode, "RGBA") && mode != 2) + /* only octree and imagequant supports RGBA */ + if (!strcmp(im->mode, "RGBA") && mode != 2 && mode != 3) return ImagingError_ModeError(); p = malloc(sizeof(Pixel) * im->xsize * im->ysize); @@ -1503,8 +1504,10 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) should be done by a simple copy... */ for (i = y = 0; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++, i++) + for (x = 0; x < im->xsize; x++, i++) { p[i].c.r = p[i].c.g = p[i].c.b = im->image8[y][x]; + p[i].c.a = 255; + } } else if (!strcmp(im->mode, "P")) { /* palette */ @@ -1517,6 +1520,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) p[i].c.r = pp[v*4+0]; p[i].c.g = pp[v*4+1]; p[i].c.b = pp[v*4+2]; + p[i].c.a = pp[v*4+3]; } } else if (!strcmp(im->mode, "RGB") || !strcmp(im->mode, "RGBA")) { @@ -1572,6 +1576,25 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) withAlpha ); break; + case 3: +#ifdef HAVE_LIBIMAGEQUANT + if (!strcmp(im->mode, "RGBA")) { + withAlpha = 1; + } + result = quantize_pngquant( + p, + im->xsize, + im->ysize, + colors, + &palette, + &paletteLength, + &newData, + withAlpha + ); +#else + result = -1; +#endif + break; default: result = 0; break; @@ -1580,7 +1603,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) free(p); ImagingSectionLeave(&cookie); - if (result) { + if (result > 0) { imOut = ImagingNew("P", im->xsize, im->ysize); ImagingSectionEnter(&cookie); @@ -1620,6 +1643,12 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) } else { + if (result == -1) { + return (Imaging) ImagingError_ValueError( + "dependency required by this method was not " + "enabled at compile time"); + } + return (Imaging) ImagingError_ValueError("quantization error"); } diff --git a/libImaging/QuantPngQuant.c b/libImaging/QuantPngQuant.c new file mode 100644 index 000000000..a9a547540 --- /dev/null +++ b/libImaging/QuantPngQuant.c @@ -0,0 +1,110 @@ +/* + * The Python Imaging Library + * $Id$ + * + * quantization using libimagequant, a part of pngquant. + * + * Copyright (c) 2016 Marcin Kurczewski + * + */ + +#include +#include +#include + +#include "QuantPngQuant.h" + +#ifdef HAVE_LIBIMAGEQUANT +#include "libimagequant.h" + +int +quantize_pngquant( + Pixel *pixelData, + int width, + int height, + uint32_t quantPixels, + Pixel **palette, + uint32_t *paletteLength, + uint32_t **quantizedPixels, + int withAlpha) +{ + int result = 0; + liq_image *image = NULL; + liq_attr *attr = NULL; + liq_result *remap = NULL; + unsigned char *charMatrix = NULL; + unsigned char **charMatrixRows = NULL; + unsigned int i, y; + *palette = NULL; + *paletteLength = 0; + *quantizedPixels = NULL; + + /* configure pngquant */ + attr = liq_attr_create(); + if (!attr) { goto err; } + if (quantPixels) { + liq_set_max_colors(attr, quantPixels); + } + + /* prepare input image */ + image = liq_image_create_rgba( + attr, + pixelData, + width, + height, + 0.45455 /* gamma */); + if (!image) { goto err; } + + /* quantize the image */ + remap = liq_quantize_image(attr, image); + if (!remap) { goto err; } + liq_set_output_gamma(remap, 0.45455); + liq_set_dithering_level(remap, 1); + + /* write output palette */ + const liq_palette *l_palette = liq_get_palette(remap); + *paletteLength = l_palette->count; + *palette = malloc(sizeof(Pixel) * l_palette->count); + if (!*palette) { goto err; } + for (i = 0; i < l_palette->count; i++) { + (*palette)[i].c.b = l_palette->entries[i].b; + (*palette)[i].c.g = l_palette->entries[i].g; + (*palette)[i].c.r = l_palette->entries[i].r; + (*palette)[i].c.a = l_palette->entries[i].a; + } + + /* write output pixels (pngquant uses char array) */ + charMatrix = malloc(width * height); + if (!charMatrix) { goto err; } + charMatrixRows = malloc(height * sizeof(unsigned char*)); + if (!charMatrixRows) { goto err; } + for (y = 0; y < height; y++) { + charMatrixRows[y] = &charMatrix[y * width]; + } + if (LIQ_OK != liq_write_remapped_image_rows(remap, image, charMatrixRows)) { + goto err; + } + + /* transcribe output pixels (pillow uses uint32_t array) */ + *quantizedPixels = malloc(sizeof(uint32_t) * width * height); + if (!*quantizedPixels) { goto err; } + for (i = 0; i < width * height; i++) { + (*quantizedPixels)[i] = charMatrix[i]; + } + + result = 1; + +err: + if (attr) liq_attr_destroy(attr); + if (image) liq_image_destroy(image); + if (remap) liq_result_destroy(remap); + free(charMatrix); + free(charMatrixRows); + if (!result) { + free(*quantizedPixels); + free(*palette); + } + return result; +} + +#endif diff --git a/libImaging/QuantPngQuant.h b/libImaging/QuantPngQuant.h new file mode 100644 index 000000000..d539a7a0d --- /dev/null +++ b/libImaging/QuantPngQuant.h @@ -0,0 +1,15 @@ +#ifndef __QUANT_PNGQUANT_H__ +#define __QUANT_PNGQUANT_H__ + +#include "QuantTypes.h" + +int quantize_pngquant(Pixel *, + int, + int, + uint32_t, + Pixel **, + uint32_t *, + uint32_t **, + int); + +#endif diff --git a/setup.py b/setup.py index 247af5508..76e2dfc97 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ _LIB_IMAGING = ( "RankFilter", "RawDecode", "RawEncode", "Storage", "SunRleDecode", "TgaRleDecode", "Unpack", "UnpackYCC", "UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode", "ZipEncode", "TiffDecode", "Incremental", - "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur") + "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur", "QuantPngQuant") DEBUG = False @@ -115,6 +115,7 @@ TCL_ROOT = None JPEG_ROOT = None JPEG2K_ROOT = None ZLIB_ROOT = None +IMAGEQUANT_ROOT = None TIFF_ROOT = None FREETYPE_ROOT = None LCMS_ROOT = None @@ -123,7 +124,7 @@ LCMS_ROOT = None class pil_build_ext(build_ext): class feature: features = ['zlib', 'jpeg', 'tiff', 'freetype', 'tcl', 'tk', 'lcms', - 'webp', 'webpmux', 'jpeg2000'] + 'webp', 'webpmux', 'jpeg2000', 'imagequant'] required = set(['jpeg', 'zlib']) @@ -193,7 +194,7 @@ class pil_build_ext(build_ext): # add configured kits for root in (TCL_ROOT, JPEG_ROOT, JPEG2K_ROOT, TIFF_ROOT, ZLIB_ROOT, - FREETYPE_ROOT, LCMS_ROOT): + FREETYPE_ROOT, LCMS_ROOT, IMAGEQUANT_ROOT): if isinstance(root, type(())): lib_root, include_root = root else: @@ -490,6 +491,14 @@ class pil_build_ext(build_ext): feature.openjpeg_version = '.'.join([str(x) for x in best_version]) + if feature.want('imagequant'): + _dbg('Looking for imagequant') + if _find_include_file(self, 'libimagequant.h'): + if _find_library_file(self, "imagequant"): + feature.imagequant = "imagequant" + elif _find_library_file(self, "libimagequant"): + feature.imagequant = "libimagequant" + if feature.want('tiff'): _dbg('Looking for tiff') if _find_include_file(self, 'tiff.h'): @@ -603,6 +612,9 @@ class pil_build_ext(build_ext): if feature.zlib: libs.append(feature.zlib) defs.append(("HAVE_LIBZ", None)) + if feature.imagequant: + libs.append(feature.imagequant) + defs.append(("HAVE_LIBIMAGEQUANT", None)) if feature.tiff: libs.append(feature.tiff) defs.append(("HAVE_LIBTIFF", None)) @@ -710,6 +722,7 @@ class pil_build_ext(build_ext): (feature.jpeg2000, "OPENJPEG (JPEG2000)", feature.openjpeg_version), (feature.zlib, "ZLIB (PNG/ZIP)"), + (feature.imagequant, "LIBIMAGEQUANT"), (feature.tiff, "LIBTIFF"), (feature.freetype, "FREETYPE2"), (feature.lcms, "LITTLECMS2"), diff --git a/winbuild/README.md b/winbuild/README.md index 918fb2ba3..89d60de62 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -15,4 +15,4 @@ For more extensive info, see the windows build instructions `docs/build.rst`. * `python test.py` runs the tests on Pillow in all the virtual envs. * Currently working with zlib, libjpeg, freetype, and libtiff on Python 2.7, 3.3, and 3.4, both 32 and 64 bit, on a local win7 pro machine and appveyor.com * Webp is built, not detected. -* LCMS and OpenJpeg are not building. +* LCMS, OpenJpeg and libimagequant are not building.