Series of changes for the tiff encoder:

* Added configuration options for the TIFF encoder. The options are:
   - compression: adobe_deflate(default), deflate, lzw, none
   - zlevel: 0-9
   - scanline: 1,0 (Forces scanline encoding)
 * Fixed bug in encoder with scanline where memory was being modified
 * by TIFFWriteScanline. A buffer is now created for each row to prevent
 * modification of the underlying image_data.
This commit is contained in:
Blake Thompson 2014-12-04 17:11:33 -05:00
parent af4c2620aa
commit bf5c532269
3 changed files with 163 additions and 26 deletions

View file

@ -156,10 +156,23 @@ static int tiff_dummy_map_proc(thandle_t , tdata_t*, toff_t* )
return 0;
}
struct tiff_config
{
tiff_config()
: compression(COMPRESSION_ADOBE_DEFLATE),
zlevel(4),
scanline(false) {}
int compression;
int zlevel;
bool scanline;
};
struct tag_setter : public mapnik::util::static_visitor<>
{
tag_setter(TIFF * output)
: output_(output) {}
tag_setter(TIFF * output, tiff_config & config)
: output_(output),
config_(config) {}
template <typename T>
void operator() (T const&) const
@ -174,28 +187,56 @@ struct tag_setter : public mapnik::util::static_visitor<>
TIFFSetField(output_, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
TIFFSetField(output_, TIFFTAG_BITSPERSAMPLE, 8);
TIFFSetField(output_, TIFFTAG_SAMPLESPERPIXEL, 4);
TIFFSetField(output_, TIFFTAG_PREDICTOR, PREDICTOR_HORIZONTAL);
uint16 extras[] = { EXTRASAMPLE_UNASSALPHA };
TIFFSetField(output_, TIFFTAG_EXTRASAMPLES, 1, extras);
if (config_.compression == COMPRESSION_DEFLATE
|| config_.compression == COMPRESSION_ADOBE_DEFLATE
|| config_.compression == COMPRESSION_LZW)
{
TIFFSetField(output_, TIFFTAG_PREDICTOR, PREDICTOR_HORIZONTAL);
}
}
inline void operator() (image_data_gray32f const&) const
{
TIFFSetField(output_, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
TIFFSetField(output_, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP);
TIFFSetField(output_, TIFFTAG_BITSPERSAMPLE, 32);
TIFFSetField(output_, TIFFTAG_PREDICTOR, PREDICTOR_FLOATINGPOINT);
TIFFSetField(output_, TIFFTAG_SAMPLESPERPIXEL, 1);
if (config_.compression == COMPRESSION_DEFLATE
|| config_.compression == COMPRESSION_ADOBE_DEFLATE
|| config_.compression == COMPRESSION_LZW)
{
TIFFSetField(output_, TIFFTAG_PREDICTOR, PREDICTOR_FLOATINGPOINT);
}
}
inline void operator() (image_data_gray16 const&) const
{
TIFFSetField(output_, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
TIFFSetField(output_, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
TIFFSetField(output_, TIFFTAG_BITSPERSAMPLE, 16);
TIFFSetField(output_, TIFFTAG_PREDICTOR, PREDICTOR_HORIZONTAL);
TIFFSetField(output_, TIFFTAG_SAMPLESPERPIXEL, 1);
if (config_.compression == COMPRESSION_DEFLATE
|| config_.compression == COMPRESSION_ADOBE_DEFLATE
|| config_.compression == COMPRESSION_LZW)
{
TIFFSetField(output_, TIFFTAG_PREDICTOR, PREDICTOR_HORIZONTAL);
}
}
inline void operator() (image_data_gray8 const&) const
{
TIFFSetField(output_, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
TIFFSetField(output_, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
TIFFSetField(output_, TIFFTAG_BITSPERSAMPLE, 8);
TIFFSetField(output_, TIFFTAG_PREDICTOR, PREDICTOR_HORIZONTAL);
TIFFSetField(output_, TIFFTAG_SAMPLESPERPIXEL, 1);
if (config_.compression == COMPRESSION_DEFLATE
|| config_.compression == COMPRESSION_ADOBE_DEFLATE
|| config_.compression == COMPRESSION_LZW)
{
TIFFSetField(output_, TIFFTAG_PREDICTOR, PREDICTOR_HORIZONTAL);
}
}
inline void operator() (image_data_null const&) const
{
@ -205,10 +246,29 @@ struct tag_setter : public mapnik::util::static_visitor<>
private:
TIFF * output_;
tiff_config config_;
};
void set_tiff_config(TIFF* output, tiff_config & config)
{
// Set some constent tiff information that doesn't vary based on type of data
// or image size
TIFFSetField(output, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
// Set the compression for the TIFF
TIFFSetField(output, TIFFTAG_COMPRESSION, config.compression);
if (COMPRESSION_ADOBE_DEFLATE == config.compression || COMPRESSION_DEFLATE == config.compression)
{
// Set the zip level for the compression
// http://en.wikipedia.org/wiki/DEFLATE#Encoder.2Fcompressor
// Changes the time spent trying to compress
TIFFSetField(output, TIFFTAG_ZIPQUALITY, config.zlevel);
}
}
template <typename T1, typename T2>
void save_as_tiff(T1 & file, T2 const& image)
void save_as_tiff(T1 & file, T2 const& image, tiff_config & config)
{
const int width = image.width();
const int height = image.height();
@ -228,43 +288,39 @@ void save_as_tiff(T1 & file, T2 const& image)
throw ImageWriterException("Could not write TIFF");
}
// Set some constent tiff information that doesn't vary based on type of data
// or image size
TIFFSetField(output, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(output, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(output, TIFFTAG_IMAGEDEPTH, 1);
TIFFSetField(output, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
// Set the compression for the TIFF
//TIFFSetField(output, TIFFTAG_COMPRESSION, COMPRESSION_DEFLATE);
TIFFSetField(output, TIFFTAG_COMPRESSION, COMPRESSION_ADOBE_DEFLATE);
set_tiff_config(output, config);
// Set the zip level for the compression
// http://en.wikipedia.org/wiki/DEFLATE#Encoder.2Fcompressor
// Changes the time spent trying to compress
TIFFSetField(output, TIFFTAG_ZIPQUALITY, 4);
// Set tags that vary based on the type of data being provided.
tag_setter set(output);
tag_setter set(output, config);
set(image);
//util::apply_visitor(set, image);
// If the image is greater then 8MB uncompressed, then lets use scanline rather then
// tile. TIFF also requires that all TIFFTAG_TILEWIDTH and TIFF_TILELENGTH all be
// a multiple of 16, if they are not we will use scanline.
if (image.getSize() > 8 * 32 * 1024 * 1024 || width % 16 != 0 || height % 16 != 0)
if (image.getSize() > 8 * 32 * 1024 * 1024
|| width % 16 != 0
|| height % 16 != 0
|| config.scanline)
{
// Process Scanline
TIFFSetField(output, TIFFTAG_ROWSPERSTRIP, 1);
int next_scanline = 0;
typename T2::pixel_type * row = reinterpret_cast<typename T2::pixel_type*>(::operator new(image.getRowSize()));
while (next_scanline < height)
{
typename T2::pixel_type * row = const_cast<typename T2::pixel_type *>(image.getRow(next_scanline));
memcpy(row, image.getRow(next_scanline), image.getRowSize());
//typename T2::pixel_type * row = const_cast<typename T2::pixel_type *>(image.getRow(next_scanline));
TIFFWriteScanline(output, row, next_scanline, 0);
++next_scanline;
}
::operator delete(row);
}
else
{
@ -272,8 +328,11 @@ void save_as_tiff(T1 & file, T2 const& image)
TIFFSetField(output, TIFFTAG_TILELENGTH, height);
TIFFSetField(output, TIFFTAG_TILEDEPTH, 1);
// Process as tiles
typename T2::pixel_type * image_data = const_cast<typename T2::pixel_type *>(image.getData());
typename T2::pixel_type * image_data = reinterpret_cast<typename T2::pixel_type*>(::operator new(image.getSize()));
memcpy(image_data, image.getData(), image.getSize());
//typename T2::pixel_type * image_data = const_cast<typename T2::pixel_type *>(image.getData());
TIFFWriteTile(output, image_data, 0, 0, 0, 0);
::operator delete(image_data);
}
// TODO - handle palette images
// std::vector<mapnik::rgb> const& palette

View file

@ -255,6 +255,82 @@ void handle_png_options(std::string const& type,
}
#endif
#if defined(HAVE_TIFF)
void handle_tiff_options(std::string const& type,
tiff_config & config)
{
if (type == "tiff")
{
return;
}
if (type.length() > 4)
{
boost::char_separator<char> sep(":");
boost::tokenizer< boost::char_separator<char> > tokens(type, sep);
for (auto const& t : tokens)
{
if (t == "tiff")
{
continue;
}
else if (boost::algorithm::starts_with(t, "compression="))
{
std::string val = t.substr(12);
if (!val.empty())
{
if (val == "deflate")
{
config.compression = COMPRESSION_DEFLATE;
}
else if (val == "adobedeflate")
{
config.compression = COMPRESSION_ADOBE_DEFLATE;
}
else if (val == "lzw")
{
config.compression = COMPRESSION_LZW;
}
else if (val == "none")
{
config.compression = COMPRESSION_NONE;
}
else
{
throw ImageWriterException("invalid tiff compression: '" + val + "'");
}
}
}
else if (boost::algorithm::starts_with(t, "zlevel="))
{
std::string val = t.substr(7);
if (!val.empty())
{
if (!mapnik::util::string2int(val,config.zlevel) || config.zlevel < 0 || config.zlevel > 9)
{
throw ImageWriterException("invalid tiff zlevel: '" + val + "'");
}
}
}
else if (boost::algorithm::starts_with(t, "scanline="))
{
std::string val = t.substr(9);
if (!val.empty())
{
if (!mapnik::util::string2bool(val,config.scanline))
{
throw ImageWriterException("invalid tiff scanline: '" + val + "'");
}
}
}
else
{
throw ImageWriterException("unhandled tiff option: " + t);
}
}
}
}
#endif
#if defined(HAVE_WEBP)
void handle_webp_options(std::string const& type,
WebPConfig & config,
@ -621,7 +697,9 @@ void save_to_stream(T const& image,
else if (boost::algorithm::starts_with(t, "tif"))
{
#if defined(HAVE_TIFF)
save_as_tiff(stream, image);
tiff_config config;
handle_tiff_options(t, config);
save_as_tiff(stream, image, config);
#else
throw ImageWriterException("tiff output is not enabled in your build of Mapnik");
#endif

View file

@ -27,10 +27,10 @@ def test_image_premultiply():
#im = mapnik.Image(-40,40)
def test_tiff_round_trip_scanline():
filepath = '/tmp/mapnik-tiff-io.tiff'
filepath = '/tmp/mapnik-tiff-io-scanline.tiff'
im = mapnik.Image(255,267)
im.background = mapnik.Color('rgba(1,2,3,.5)')
im.save(filepath,'tiff')
im.save(filepath,'tiff:zlevel=0:scanline=1:compression=lzw')
im2 = mapnik.Image.open(filepath)
eq_(im.width(),im2.width())
eq_(im.height(),im2.height())
@ -38,7 +38,7 @@ def test_tiff_round_trip_scanline():
eq_(len(im.tostring('tiff')),len(im2.tostring('tiff')))
def test_tiff_round_trip_tiled():
filepath = '/tmp/mapnik-tiff-io.tiff'
filepath = '/tmp/mapnik-tiff-io-tiled.tiff'
im = mapnik.Image(256,256)
im.background = mapnik.Color('rgba(1,2,3,.5)')
im.save(filepath,'tiff')