diff --git a/SConstruct b/SConstruct index ec2704776..1a30c0dc8 100644 --- a/SConstruct +++ b/SConstruct @@ -84,6 +84,7 @@ pretty_dep_names = { 'jpeg':'JPEG C library | configure with JPEG_LIBS & JPEG_INCLUDES', 'tiff':'TIFF C library | configure with TIFF_LIBS & TIFF_INCLUDES', 'png':'PNG C library | configure with PNG_LIBS & PNG_INCLUDES', + 'imgquant':'ImageQuant C library | configure with IQ_LIBS & IQ_INCLUDES', 'webp':'WEBP C library | configure with WEBP_LIBS & WEBP_INCLUDES', 'icuuc':'ICU C++ library | configure with ICU_LIBS & ICU_INCLUDES or use ICU_LIB_NAME to specify custom lib name | more info: http://site.icu-project.org/', 'harfbuzz':'HarfBuzz text shaping library | configure with HB_LIBS & HB_INCLUDES', @@ -346,6 +347,9 @@ opts.AddVariables( BoolVariable('PNG', 'Build Mapnik with PNG read and write support', 'True'), PathVariable('PNG_INCLUDES', 'Search path for libpng include files', '/usr/include', PathVariable.PathAccept), PathVariable('PNG_LIBS','Search path for libpng library files','/usr/' + LIBDIR_SCHEMA_DEFAULT, PathVariable.PathAccept), + BoolVariable('IQ', 'Build Mapnik with ImageQuant support', 'True'), + PathVariable('IQ_INCLUDES', 'Search path for libimagequant include files', '/usr/include', PathVariable.PathAccept), + PathVariable('IQ_LIBS','Search path for libimagequant library files','/usr/' + LIBDIR_SCHEMA_DEFAULT, PathVariable.PathAccept), BoolVariable('JPEG', 'Build Mapnik with JPEG read and write support', 'True'), PathVariable('JPEG_INCLUDES', 'Search path for libjpeg include files', '/usr/include', PathVariable.PathAccept), PathVariable('JPEG_LIBS', 'Search path for libjpeg library files', '/usr/' + LIBDIR_SCHEMA_DEFAULT, PathVariable.PathAccept), @@ -1304,6 +1308,15 @@ if not preconfigured: else: env['SKIPPED_DEPS'].extend(['png']) + if env['IQ']: + OPTIONAL_LIBSHEADERS.append(['imagequant', 'libimagequant.h', False,'C','-DHAVE_IQ']) + inc_path = env['%s_INCLUDES' % 'IQ'] + lib_path = env['%s_LIBS' % 'IQ'] + env.AppendUnique(CPPPATH = fix_path(inc_path)) + env.AppendUnique(LIBPATH = fix_path(lib_path)) + else: + env['SKIPPED_DEPS'].extend(['iq']) + if env['WEBP']: OPTIONAL_LIBSHEADERS.append(['webp', 'webp/decode.h', False,'C','-DHAVE_WEBP']) inc_path = env['%s_INCLUDES' % 'WEBP'] @@ -1962,4 +1975,4 @@ if not HELP_REQUESTED: SConscript('utils/mapnik-config/build.py') # write the viewer.ini file - SConscript('demo/viewer/build.py') \ No newline at end of file + SConscript('demo/viewer/build.py') diff --git a/bootstrap.sh b/bootstrap.sh index 5baccd02f..b1926bc86 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -107,6 +107,8 @@ CAIRO_INCLUDES = '${MASON_LINKED_REL}/include' CAIRO_LIBS = '${MASON_LINKED_REL}/lib' SQLITE_INCLUDES = '${MASON_LINKED_REL}/include' SQLITE_LIBS = '${MASON_LINKED_REL}/lib' +IQ_INCLUDES = /usr/local/include +IQ_LIBS = /usr/local/lib BENCHMARK = True CPP_TESTS = True PGSQL2SQLITE = True diff --git a/include/mapnik/png_io.hpp b/include/mapnik/png_io.hpp index 8526b5bec..7f2df798a 100644 --- a/include/mapnik/png_io.hpp +++ b/include/mapnik/png_io.hpp @@ -32,6 +32,8 @@ // zlib #include // for Z_DEFAULT_COMPRESSION +#include "memory_datasource.hpp" +#include "palette.hpp" // boost @@ -39,6 +41,8 @@ extern "C" { #include +// TODO: make this optional +#include "libimagequant.h" } #define MAX_OCTREE_LEVELS 4 @@ -46,22 +50,27 @@ extern "C" namespace mapnik { struct png_options { + + enum quantization_type { HEXTREE = 0, OCTTREE = 1, IMGQUANT = 2 }; + int colors; int compression; int strategy; int trans_mode; + int iq_speed; double gamma; bool paletted; - bool use_hextree; + quantization_type quantization; bool use_miniz; png_options() : colors(256), compression(Z_DEFAULT_COMPRESSION), strategy(Z_DEFAULT_STRATEGY), trans_mode(-1), + iq_speed(3), gamma(-1), paletted(true), - use_hextree(true), + quantization(HEXTREE), use_miniz(false) {} }; @@ -704,6 +713,55 @@ void save_as_png8_pal(T1 & file, save_as_png8(file, image, pal, pal.palette(), pal.alphaTable(), opts); } +// TODO: This only works with rgba8_t image types +template +void save_as_png8_libimagequant(T1 & file, + T2 const& image, + png_options const& opts) +{ + unsigned width = image.width(); + unsigned height = image.height(); + + // TODO: can this be done as a single blob, rather than using rows? + uint32_t *buf[height]; + + // TODO: this won't work on big-endian architectures, liq expects + // data in RGBA byte order + for (size_t y = 0; y < height; ++y) + { + buf[y] = (uint32_t *)image.get_row(y); + } + + liq_attr *attr = liq_attr_create(); + // TODO: set gamma + // TODO: error checking + liq_set_speed(attr, opts.iq_speed); + liq_image *liq_image = liq_image_create_rgba_rows(attr, (void **)buf, width, height, 0); + liq_result *res = liq_quantize_image(attr, liq_image); + + // Store palettized version + image_gray8 reduced_image(width, height); + liq_write_remapped_image(res, liq_image, (void *)reduced_image.data(), width*height*sizeof(gray8_t)); + const liq_palette *liq_pal = liq_get_palette(res); + + std::vector palette; + std::vector alpha; + + for (int i = 0; i < liq_pal->count; ++i) + { + palette.push_back({ liq_pal->entries[i].r, liq_pal->entries[i].g, liq_pal->entries[i].b }); + alpha.push_back(liq_pal->entries[i].a); + } + + // TODO: support 1 & 4 bit packing, not just 8, to reduce the file size, particularly on blank + // or solid tiles + save_as_png(file, palette, reduced_image, width, height, 8, alpha, opts); + + liq_attr_destroy(attr); + liq_image_destroy(liq_image); + liq_result_destroy(res); +} + } #endif // MAPNIK_PNG_IO_HPP diff --git a/src/build.py b/src/build.py index a53f066c4..7d1814648 100644 --- a/src/build.py +++ b/src/build.py @@ -73,6 +73,9 @@ if '-DHAVE_PNG' in env['CPPDEFINES']: lib_env['LIBS'].append('png') enabled_imaging_libraries.append('png_reader.cpp') +if '-DHAVE_IQ' in env['CPPDEFINES']: + lib_env['LIBS'].append('imagequant') + if '-DMAPNIK_USE_PROJ4' in env['CPPDEFINES']: lib_env['LIBS'].append('proj') diff --git a/src/image_util_png.cpp b/src/image_util_png.cpp index 5b2ccc7f2..d80ae269d 100644 --- a/src/image_util_png.cpp +++ b/src/image_util_png.cpp @@ -80,11 +80,23 @@ void handle_png_options(std::string const& type, } else if (t == "m=o") { - opts.use_hextree = false; + opts.quantization = png_options::OCTTREE; } else if (t == "m=h") { - opts.use_hextree = true; + opts.quantization = png_options::HEXTREE; + } + else if (t == "m=iq") + { + opts.quantization = png_options::IMGQUANT; + + } + else if (boost::algorithm::starts_with(t, "iq=")) + { + if (!mapnik::util::string2int(t.substr(3), opts.iq_speed) || opts.iq_speed < 1 || opts.iq_speed > 10) + { + throw ImageWriterException("invalid iq speed parameter: " + t.substr(3)); + } } else if (t == "e=miniz") { @@ -224,14 +236,18 @@ void process_rgba8_png_pal(T const& image, } else if (opts.paletted) { - if (opts.use_hextree) + if (opts.quantization == png_options::HEXTREE) { save_as_png8_hex(stream, image, opts); } - else + else if (opts.quantization == png_options::OCTTREE) { save_as_png8_oct(stream, image, opts); } + else + { + save_as_png8_libimagequant(stream, image, opts); + } } else { @@ -252,14 +268,18 @@ void process_rgba8_png(T const& image, handle_png_options(t, opts); if (opts.paletted) { - if (opts.use_hextree) + if (opts.quantization == png_options::HEXTREE) { save_as_png8_hex(stream, image, opts); } - else + else if (opts.quantization == png_options::OCTTREE) { save_as_png8_oct(stream, image, opts); } + else + { + save_as_png8_libimagequant(stream, image, opts); + } } else {