Merge pull request #1889 from rr-/libpngquant

Add libimagequant support in quantize()
This commit is contained in:
wiredfool 2016-05-26 21:12:25 +01:00
commit 3657bc10a6
14 changed files with 242 additions and 37 deletions

View File

@ -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

View File

@ -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

View File

@ -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__':

View File

@ -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

View File

@ -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

12
depends/install_imagequant.sh Executable file
View File

@ -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

View File

@ -14,3 +14,4 @@ sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \
./install_openjpeg.sh
./install_webp.sh
./install_imagequant.sh

View File

@ -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

View File

@ -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

View File

@ -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");
}

110
libImaging/QuantPngQuant.c Normal file
View File

@ -0,0 +1,110 @@
/*
* The Python Imaging Library
* $Id$
*
* quantization using libimagequant, a part of pngquant.
*
* Copyright (c) 2016 Marcin Kurczewski <rr-@sakuya.pl>
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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

View File

@ -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

View File

@ -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"),

View File

@ -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.