Skip to content

Commit

Permalink
Added AVIF output support
Browse files Browse the repository at this point in the history
Both tile and region output are supported for both IIP and IIIF. Individual tiles can be requested using the ATL paramter which works in the same way as PTL for PNG and JTL for JPEG. CVT region requests can be made with CVT=avif. AVIF encoder supports alpha channels and 8 bit images only at the moment (avif supports up to 12 bit).

To compile, iipsrv requires the libavif development headers and libraries, which are automatically detected in ./configure. libavif itself will require at least one encoder codec enabled.

The AVIF_QUALITY startup variable sets the default encoding quality (0=most compression, 100=best quality, default=50), -1 specifies lossless encoding.

The AVIF_CODEC variable allows selection of the libavif codec (0=auto, 1=aom, 2=rav1e, 3=svt, default=auto). The selected codec needs to have been built and working within libavif.
  • Loading branch information
ruven committed Sep 5, 2024
1 parent b4cd473 commit cd07a85
Show file tree
Hide file tree
Showing 20 changed files with 605 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/c-cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v3

- name: Install dependencies
run: sudo apt-get update && sudo apt-get install libtiff-dev libpng-dev libturbojpeg-dev libwebp-dev libmemcached-dev libopenjp2-7-dev
run: sudo apt-get update && sudo apt-get install libtiff-dev libpng-dev libturbojpeg-dev libwebp-dev libavif-dev libmemcached-dev libopenjp2-7-dev

- name: Configure
run: |
Expand Down
8 changes: 8 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
05/09/2024:
- Added convenience function to Rawtile class to duplicate bands for encoders that cannot natively handle
single band monochrome images: simplifies WebP encoding.
- Added AVIF output support. Both tile and region output are supported for both IIP and IIIF. Individual tiles
can be requested using the ATL paramter which works in the same way as PTL for PNG and JTL for JPEG. CVT
region requests can be made with CVT=avif. AVIF encoder supports alpha channels and 8 bit images only at the
moment (avif supports up to 12 bit). To compile, iipsrv requires the libavif development headers and libraries,
which are automatically detected in ./configure. libavif itself will require at least one encoder codec enabled.
The AVIF_QUALITY startup variable sets the default encoding quality (0=most compression, 100=best quality,
default=50), -1 specifies lossless encoding. The AVIF_CODEC variable allows selection of the libavif codec
(0=auto, 1=aom, 2=rav1e, 3=svt, default=auto). The selected codec needs to have been built and working within libavif.


29/06/2024:
Expand Down
4 changes: 4 additions & 0 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,10 @@ PNG_QUALITY: The default PNG quality factor for compression when the client does

WEBP_QUALITY: The default WebP quality factor for compression when the client does not specify one. For lossy compression the value should be between 0 (highest level of compression) and 100 (highest image quality). For lossless compression, set this to -1. The default is lossy compression with a quality factor of 50.

AVIF_QUALITY: The default AVIF quality factor for compression when the client does not specify one. For lossy compression the value should be between 0 (highest level of compression) and 100 (highest image quality). For lossless compression, set this to -1. The default is lossy compression with a quality factor of 50.

AVIF_CODEC: The AVIF codec to be used for encoding. Set to 0 for automatic codec selection, 1 for aom, 2 for rav1e and 3 for svt. The default is 0 (automatic codec selection).

MAX_CVT: Limits the maximum output image dimensions (in pixels) allowable for dynamic image export via the CVT command or for IIIF requests. This prevents huge requests from overloading the server. The default is 5000. If set to -1, no limit is set.

ALLOW_UPSCALING: Determines whether an image may be rendered at a size greater than that of the source image. A value of 0 will prevent upscaling.
Expand Down
31 changes: 29 additions & 2 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ AC_CHECK_HEADERS([chrono])
AC_CHECK_HEADERS([unordered_map])
AC_CHECK_HEADERS([tr1/unordered_map])
AC_CHECK_HEADERS([ext/hash_map])
AC_CHECK_HEADER([thread], [AC_DEFINE(HAVE_STL_THREAD)])
AX_CXX_COMPILE_STDCXX(11)
AC_LANG_POP([C++])

Expand Down Expand Up @@ -190,6 +191,31 @@ else
fi


#************************************************************
# Check for AVIF support
#************************************************************

AVIF=false
AC_ARG_ENABLE( avif,
[ --disable-avif disable AVIF])


if test "x$enable_avuf" == "xno"; then
AC_MSG_RESULT([configure: disabling AVIF support])
AM_CONDITIONAL([ENABLE_AVIF], [false])
else
AC_CHECK_HEADER( [avif/avif.h], [AVIF=true], [AVIF=false] )
AC_SEARCH_LIBS( [avifEncoderAddImage], [avif], [AVIF=true], [AVIF=false] )

if test "x${AVIF}" = xtrue; then
AM_CONDITIONAL([ENABLE_AVIF], [true])
AC_DEFINE(HAVE_AVIF)
else
AM_CONDITIONAL([ENABLE_AVIF], [false])
fi

fi


#************************************************************
# Check for libdl for dynamic library loading
Expand Down Expand Up @@ -422,8 +448,9 @@ Options Enabled:
JPEG2000 : ${JPEG2000_CODEC}
OpenMP : ${OPENMP}
Loggers : ${LOGGING}
PNG Output : ${PNG}
WebP Output : ${WEBP}])
PNG Output : ${PNG}
WebP Output : ${WEBP}
AVIF Output : ${AVIF}])

if [test "x${DEBUG}" = xtrue]; then
AC_MSG_RESULT([ Debug mode : activated])
Expand Down
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ RUN apk add --no-cache \
openjpeg-dev \
libpng-dev \
libwebp-dev \
libavif-dev \
libmemcached-dev \
lighttpd

Expand Down
1 change: 1 addition & 0 deletions docker/Dockerfile.debian
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ apt-get install --no-install-recommends -y \
make \
libpng-dev \
libwebp-dev \
libavif-dev \
libmemcached-dev \
lighttpd

Expand Down
13 changes: 11 additions & 2 deletions man/iipsrv.8
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.TH IIPSRV 8 "May 2023" "Ruven Pillay"
.TH IIPSRV 8 "September 2024" "Ruven Pillay"
.SH NAME

IIPSRV \- IIPImage Image Server
Expand All @@ -9,7 +9,7 @@ images. It is designed to be fast and bandwidth-efficient with low processor and
well as advanced image features such as 8, 16 and 32 bit depths, CIELAB colorimetric images and scientific imagery such as multispectral images.
Source images can be either TIFF (tiled multi-resolution) or JPEG2000 (if enabled).

The image server can also dynamically export images in JPEG, PNG and WebP format and perform basic image processing, such as contrast adjustment, gamma control, conversion from color to greyscale, color twist, region extraction and arbitrary rescaling. The server can also export spectral point or profile data from multispectral data and apply color maps or perform hillshading rendering.
The image server can also dynamically export images in JPEG, PNG, WebP and AVIF format and perform basic image processing, such as contrast adjustment, gamma control, conversion from color to greyscale, color twist, region extraction and arbitrary rescaling. The server can also export spectral point or profile data from multispectral data and apply color maps or perform hillshading rendering.

.SH SYNOPSIS

Expand Down Expand Up @@ -66,7 +66,16 @@ The default is 1.
.IP WEBP_QUALITY
The default WebP quality factor for compression when the client does not specify one.
The value should be between 0 (highest level of compression) and 100 (highest image quality).
A value of -1 can be used to specify lossless encoding.
The default is 50.
.IP AVIF_QUALITY
The default AVIF quality factor for compression when the client does not specify one.
The value should be between 0 (highest level of compression) and 100 (highest image quality).
A value of -1 can be used to specify lossless encoding.
The default is 50.
.IP AVIF_CODEC
The AVIF codec to use for encoding. Integer value. Set 0 for automatic codec selection, 1 for aom, 2 for rav1e, 3 for svt.
Default is 0 (automatic codec selection)
.IP MAX_IMAGE_CACHE_SIZE
Max image cache size to be held in RAM in MB. This is a cache of
the compressed JPEG image tiles requested by the client. The default
Expand Down
240 changes: 240 additions & 0 deletions src/AVIFCompressor.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/* IIP AVIF Compressor Class:
Handles alpha channels, ICC profiles and XMP metadata
Copyright (C) 2024 Ruven Pillay
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/


#include "AVIFCompressor.h"

#if HAVE_STL_THREAD
#include <thread>
#endif

using namespace std;


/// Initialize chunk-based encoding for the CVT handler
void AVIFCompressor::InitCompression( const RawTile& rawtile, unsigned int strip_height ){

// Manually set up the correct width and height for this particular tile and point to the existing data buffer
tile.width = rawtile.width;
tile.height = rawtile.height;
tile.channels = rawtile.channels;
tile.bpc = rawtile.bpc;
tile.data = rawtile.data;
tile.dataLength = rawtile.dataLength;
tile.capacity = rawtile.capacity;
tile.memoryManaged = 0; // We don't want the RawTile destructor to free this memory

// libavif cannot handle strip or region-based encoding, so compress the entire image in one go
this->Compress( tile );

current_chunk = 0;
}



/// libwebp cannot handle line or region-based encoding, so simulate strip-based output using byte chunks
unsigned int AVIFCompressor::CompressStrip( unsigned char* source, unsigned char* output, unsigned int tile_height ){

// Initialize our chunk size only once at the start of the sequence
if( current_chunk == 0 ) chunk_size = (unsigned int)( ( (tile.dataLength*tile_height) + (tile_height/2) ) / tile.height );

// Make sure we don't over-run our allocated memory
if( (current_chunk + chunk_size) > (tile.dataLength - 1) ) chunk_size = tile.dataLength - current_chunk;

// Copy our chunk of data to the given output buffer
if( chunk_size > 0 ){
unsigned char* data = (unsigned char*) tile.data;
memcpy( output, &data[current_chunk], chunk_size );
current_chunk += chunk_size;
}

return chunk_size;
}



unsigned int AVIFCompressor::Finish( unsigned char* output ){

// Output any remaining bytes
if( current_chunk < tile.dataLength-1 ){
unsigned char* data = (unsigned char*) tile.data;
chunk_size = tile.dataLength - current_chunk - 1;
memcpy( output, &data[current_chunk], chunk_size );
return chunk_size;
}

return 0;
}



/// Compress a single tile of data
unsigned int AVIFCompressor::Compress( RawTile& rawtile ){

avifResult OK;
avifRWData output = AVIF_DATA_EMPTY;

// Initialize image structure
avifPixelFormat format = AVIF_PIXEL_FORMAT_YUV420;

#if AVIF_VERSION_MAJOR >= 1
// Use full 4:4:4 sampling for lossless
if( this->Q == -1 ) format = AVIF_PIXEL_FORMAT_YUV444;
#endif

if( rawtile.channels == 1 ) format = AVIF_PIXEL_FORMAT_YUV400;

// Create our image structure
avif = avifImageCreate( (uint32_t) rawtile.width, (uint32_t) rawtile.height, (uint32_t) rawtile.bpc, format );
if( !avif ){
throw string( "AVIFCompressor :: avifImageCreate() error" );
}

avifRGBImage rgb;
avifRGBImageSetDefaults( &rgb, avif );


// Set channel layout
rgb.format = (rawtile.channels==4) ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB;


// Monochrome single band input not directly supported - duplicate to 3 identical bands
if( rawtile.channels == 1 ) rawtile.triplicate();

#if AVIF_VERSION_MAJOR >= 1
rgb.chromaDownsampling = AVIF_CHROMA_DOWNSAMPLING_FASTEST;
#endif
rgb.rowBytes = rawtile.width * rawtile.channels * (rawtile.bpc/8);
rgb.pixels = (uint8_t*) rawtile.data; // rgb.pixels type is uint8_t even for 10 bit AVIF


// Initialize encoder
encoder = avifEncoderCreate();
if( !encoder ){
throw string( "AVIFCompressor :: avifEncoderCreate() error" );
}

// Set our encoder options
encoder->codecChoice = this->codec;
encoder->speed = AVIF_SPEED_FASTEST;


// Auto-tiling and Quality parameter only exists in version 1 onwards
#if AVIF_VERSION_MAJOR >= 1
encoder->autoTiling = true;
if( this->Q == -1 ){
encoder->quality = AVIF_QUALITY_LOSSLESS;
encoder->maxQuantizer = AVIF_QUANTIZER_LOSSLESS;
}
else encoder->quality = this->Q;
#else
encoder->maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY;
#endif

// Set threading concurrency
#if AVIF_VERSION_MAJOR >= 1 && HAVE_STL_THREAD
rgb.maxThreads = std::thread::hardware_concurrency();
encoder->maxThreads = rgb.maxThreads;
#endif


if( (OK=avifImageRGBToYUV( avif, &rgb )) != AVIF_RESULT_OK ){
throw string( "Failed to convert to YUV(A) " + string(avifResultToString(OK)) );
}


// Add ICC profile and XMP metadata to our image
writeICCProfile();
writeXMPMetadata();


if( (OK=avifEncoderAddImage( encoder, avif, 1, AVIF_ADD_IMAGE_FLAG_SINGLE )) != AVIF_RESULT_OK ){
throw string( "AVIFCompressor :: Failed to add image to encoder: " + string(avifResultToString(OK)) );
}

if( (OK=avifEncoderFinish( encoder, &output )) != AVIF_RESULT_OK ){
throw string( "AVIFCompressor :: Failed to finish encode: " + string(avifResultToString(OK)) );
}


// Allocate the appropriate amount of memory if the encoded AVIF is larger than the raw image buffer
if( output.size > rawtile.capacity ){
if( rawtile.memoryManaged ) delete[] (unsigned char*) rawtile.data;
rawtile.data = new unsigned char[output.size];
rawtile.capacity = output.size;
}

// Copy the encoded data back into our rawtile buffer
memcpy( rawtile.data, output.data, output.size );

if( avif ){
avifImageDestroy( avif );
}
if( encoder ){
avifEncoderDestroy( encoder );
}

// Return our compressed tile
rawtile.dataLength = output.size;

// Free our output structure
avifRWDataFree( &output );

rawtile.quality = this->Q;
rawtile.compressionType = ImageEncoding::AVIF;
return rawtile.dataLength;
}



/// Write ICC profile
void AVIFCompressor::writeICCProfile()
{
size_t len = icc.size();
if( len == 0 ) return;

#if AVIF_VERSION_MAJOR < 1
// No return from version < 1
avifImageSetProfileICC( avif, (const uint8_t*) icc.c_str(), len );
#else
if( avifImageSetProfileICC( avif, (const uint8_t*) icc.c_str(), len ) != AVIF_RESULT_OK ){
throw string( "AVIFCompressor :: Error adding ICC profile" );
}
#endif
}



/// Write XMP metadata
void AVIFCompressor::writeXMPMetadata()
{
size_t len = xmp.size();
if( len == 0 ) return;

#if AVIF_VERSION_MAJOR < 1
// No return from version < 1
avifImageSetMetadataXMP( avif, (const uint8_t*) xmp.c_str(), len );
#else
if( avifImageSetMetadataXMP( avif, (const uint8_t*) xmp.c_str(), len ) != AVIF_RESULT_OK ){
throw string( "AVIFCompressor :: Error adding XMP metadata" );
}
#endif
}
Loading

0 comments on commit cd07a85

Please sign in to comment.