diff --git a/PIL/Image.py b/PIL/Image.py index 87868f8dc..96e871bd7 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 @@ -967,6 +968,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 @@ -981,10 +983,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..5e04ea9b0 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -15,6 +15,14 @@ class TestImageQuantize(PillowTestCase): im = im.quantize(palette=hopper("P")) self.assert_image(im, "P", im.size) + def test_libimagequant_quantize(self): + im = hopper() + + im = im.quantize(100, Image.LIBIMAGEQUANT) + self.assert_image(im, "P", im.size) + + assert len(im.getcolors()) == 100 + def test_octree_quantize(self): im = hopper() diff --git a/libImaging/Quant.c b/libImaging/Quant.c index 5b8a8d994..b66ecd9fa 100644 --- a/libImaging/Quant.c +++ b/libImaging/Quant.c @@ -1484,7 +1484,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) return ImagingError_ModeError(); /* only octree supports RGBA */ - if (!strcmp(im->mode, "RGBA") && mode != 2) + if (!strcmp(im->mode, "RGBA") && mode != 2 && mode != 3) return ImagingError_ModeError(); p = malloc(sizeof(Pixel) * im->xsize * im->ysize); @@ -1503,8 +1503,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 +1519,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 +1575,21 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) withAlpha ); break; + case 3: + if (!strcmp(im->mode, "RGBA")) { + withAlpha = 1; + } + result = quantize_pngquant( + p, + im->xsize, + im->ysize, + colors, + &palette, + &paletteLength, + &newData, + withAlpha + ); + break; default: result = 0; break; diff --git a/libImaging/QuantPngQuant.c b/libImaging/QuantPngQuant.c new file mode 100644 index 000000000..55b697b93 --- /dev/null +++ b/libImaging/QuantPngQuant.c @@ -0,0 +1,141 @@ +/* Copyright (c) 2010 Oliver Tonnhofer , Omniscale +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +*/ + +/* +// This file implements a quantization using libimagequant, a part of pngquant. +*/ + +#include +#include +#include + +#include "QuantPngQuant.h" + +#ifdef HAVE_LIBIMAGEQUANT +#include "libimagequant.h" + +int +quantize_pngquant( + Pixel *pixelData, + uint32_t width, + uint32_t 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; + char *charMatrix = NULL; + char **charMatrixRows = NULL; + *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 (unsigned int 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(char*)); + if (!charMatrixRows) { goto err; } + for (unsigned int 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 (int 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; +} + +#else + +/* Offer dummy implementation */ +int +quantize_pngquant( + Pixel *pixelData, + uint32_t width, + uint32_t height, + uint32_t quantPixels, + Pixel **palette, + uint32_t *paletteLength, + uint32_t **quantizedPixels, + int withAlpha) +{ + return 0; +} + +#endif diff --git a/libImaging/QuantPngQuant.h b/libImaging/QuantPngQuant.h new file mode 100644 index 000000000..dc8dd3cfa --- /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 *, + uint32_t, + uint32_t, + uint32_t, + Pixel **, + uint32_t *, + uint32_t **, + int); + +#endif diff --git a/setup.py b/setup.py index 144c273ee..59ba76df7 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,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 @@ -114,6 +114,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 @@ -122,7 +123,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']) @@ -185,7 +186,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: @@ -478,6 +479,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'): @@ -591,6 +600,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)) @@ -698,6 +710,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"),