From cd07a85dd463915b4018e5882ae28707ed25e275 Mon Sep 17 00:00:00 2001 From: Ruven Date: Thu, 5 Sep 2024 12:49:17 +0200 Subject: [PATCH] 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. --- .github/workflows/c-cpp.yml | 2 +- ChangeLog | 8 ++ README | 4 + configure.ac | 31 ++++- docker/Dockerfile | 1 + docker/Dockerfile.debian | 1 + man/iipsrv.8 | 13 +- src/AVIFCompressor.cc | 240 ++++++++++++++++++++++++++++++++++++ src/AVIFCompressor.h | 173 ++++++++++++++++++++++++++ src/CVT.cc | 6 +- src/Environment.h | 28 +++++ src/IIIF.cc | 20 ++- src/JTL.cc | 6 +- src/Main.cc | 22 +++- src/Makefile.am | 7 +- src/RawTile.h | 2 +- src/Task.cc | 20 ++- src/Task.h | 29 ++--- src/TileManager.cc | 20 ++- src/TileManager.h | 3 + 20 files changed, 605 insertions(+), 31 deletions(-) create mode 100644 src/AVIFCompressor.cc create mode 100644 src/AVIFCompressor.h diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 3ed8cb1e..84b74b4c 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -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: | diff --git a/ChangeLog b/ChangeLog index 09360c6f..1228a818 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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: diff --git a/README b/README index a31fe7a2..c6a45cd9 100644 --- a/README +++ b/README @@ -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. diff --git a/configure.ac b/configure.ac index ce4910e3..2c0d87ad 100644 --- a/configure.ac +++ b/configure.ac @@ -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++]) @@ -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 @@ -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]) diff --git a/docker/Dockerfile b/docker/Dockerfile index 45c99b7a..735387fb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,6 +29,7 @@ RUN apk add --no-cache \ openjpeg-dev \ libpng-dev \ libwebp-dev \ + libavif-dev \ libmemcached-dev \ lighttpd diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 7138b838..dbd4c148 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -36,6 +36,7 @@ apt-get install --no-install-recommends -y \ make \ libpng-dev \ libwebp-dev \ + libavif-dev \ libmemcached-dev \ lighttpd diff --git a/man/iipsrv.8 b/man/iipsrv.8 index 310e6385..cd14bea5 100644 --- a/man/iipsrv.8 +++ b/man/iipsrv.8 @@ -1,4 +1,4 @@ -.TH IIPSRV 8 "May 2023" "Ruven Pillay" +.TH IIPSRV 8 "September 2024" "Ruven Pillay" .SH NAME IIPSRV \- IIPImage Image Server @@ -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 @@ -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 diff --git a/src/AVIFCompressor.cc b/src/AVIFCompressor.cc new file mode 100644 index 00000000..e6d15948 --- /dev/null +++ b/src/AVIFCompressor.cc @@ -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 +#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 +} diff --git a/src/AVIFCompressor.h b/src/AVIFCompressor.h new file mode 100644 index 00000000..d0d7e2be --- /dev/null +++ b/src/AVIFCompressor.h @@ -0,0 +1,173 @@ +/* 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. + +*/ + + +#ifndef _AVIFCOMPRESSOR_H +#define _AVIFCOMPRESSOR_H + + +#include "Compressor.h" +#include + + + +/// Wrapper class to AVIF library: Handles 8 bit and alpha channels +class AVIFCompressor : public Compressor { + + private: + + /// AVIF structures + avifEncoder *encoder; + avifImage *avif; + avifCodecChoice codec; + + + /// Data for simulated strip-based output + RawTile tile; ///< Output data for strip-based output + unsigned int chunk_size; ///< Number of bytes to output per strip + size_t current_chunk; ///< Index of current byte + + + /// Write ICC profile + void writeICCProfile(); + + /// Write XMP metadata + void writeXMPMetadata(); + + + public: + + /// Constructor + /** @param compressionLevel WebP compression level (range 0-100) + */ + AVIFCompressor( int quality ) : Compressor(quality) {}; + + /// Destructor + ~AVIFCompressor(){}; + + + /// Initialize strip based compression + /** For strip based encoding, we need to first initialize with InitCompression, + then compress a single strip at a time using CompressStrip and finally clean up using Finish + @param rawtile RawTile object containing the image to be compressed + @param strip_height height in pixels of the strip we want to compress + */ + void InitCompression( const RawTile& rawtile, unsigned int strip_height ); + + + /// Compress a strip of image data + /** @param source source image data + @param tile_height pixel height of the tile we are compressing + @param output output_buffer + @return size of compressed strip + */ + unsigned int CompressStrip( unsigned char* source, unsigned char* output, unsigned int tile_height ); + + + /// Finish the strip based compression and free memory + /** @param output Output buffer + @return size of output generated + */ + unsigned int Finish( unsigned char* output ); + + + /// Compress an entire buffer of image data at once in one command + /** @param t tile of image data + @return size of compressed data + */ + unsigned int Compress( RawTile& t ); + + + /// Return the WebP header size + inline unsigned int getHeaderSize() const { return header_size; } + + /// Return a pointer to the header itself + inline unsigned char* getHeader() { return header; } + + /// Return the WebP mime type + inline const char* getMimeType() const { return "image/avif"; } + + /// Return the image filename suffix + inline const char* getSuffix() const { return "avif"; } + + /// Get compression type + inline ImageEncoding getImageEncoding() const { return ImageEncoding::AVIF; }; + + + /// Get the current compression level + /** @return compresson level */ + inline int getQuality() const { return Q; } + + + /// Set the compression level + /** @param compression level: 0-100 with (0 = highest compression). -1 = lossless + */ + inline void setQuality( int quality ){ + + // AVIF quality ranges from 0 (best compression - smallest size) to 100 (worst compression - largest size) + this->Q = quality; + + // AVIF compression level + if( Q < -1 ) Q = -1; // Lossless + else if( Q > 100 ) Q = 100; + + } + + + /// Set codec for use during encoding - note that not all may be enabled in libavif + /** @param codec IIPImage's codec option code (0=auto,1=aom,2=rav1e,3=svt) + */ + inline void setCodec( unsigned int codec ) + { + this->codec = AVIFCompressor::getCodecChoice( codec ); + } + + + /// Static function: Convert from our option native system to libavif's codec choices + /** + @param codec IIPImage's codec option code (0=auto,1=aom,2=rav1e,3=svt) + @return libavif codec choice enum + */ + inline static avifCodecChoice getCodecChoice( unsigned int codec ) + { + if( codec == 1 ) return AVIF_CODEC_CHOICE_AOM; + else if( codec == 2 ) return AVIF_CODEC_CHOICE_RAV1E; + else if( codec == 3 ) return AVIF_CODEC_CHOICE_SVT; + else return AVIF_CODEC_CHOICE_AUTO; + } + + + /// Static function: Get codec name from IIPImage codec option code + /** + @param codec IIPImage's codec option code (0=auto,1=aom,2=rav1e,3=svt) + @return code name + */ + inline static const char* getCodecName( unsigned int codec ) + { + avifCodecChoice choice = AVIFCompressor::getCodecChoice( codec ); + const char* name = avifCodecName( choice, AVIF_CODEC_FLAG_CAN_ENCODE ); + if( name == NULL ) return "unsupported codec - will not be able to encode to avif"; + else return name; + } + +}; + +#endif diff --git a/src/CVT.cc b/src/CVT.cc index 4e7dab4c..f99fbf83 100644 --- a/src/CVT.cc +++ b/src/CVT.cc @@ -55,6 +55,9 @@ void CVT::send( Session* session ){ #endif #ifdef HAVE_WEBP else if( session->view->output_format == ImageEncoding::WEBP ) compressor = session->webp; +#endif +#ifdef HAVE_AVIF + else if( session->view->output_format == ImageEncoding::AVIF ) compressor = session->avif; #endif else return; @@ -406,7 +409,8 @@ void CVT::send( Session* session ){ // For PNG and WebP, strip extra bands if we have more than 4 present if( ( (session->view->output_format == ImageEncoding::JPEG) && (complete_image.channels == 2 || complete_image.channels > 3) ) || ( (session->view->output_format == ImageEncoding::PNG) && (complete_image.channels > 4) ) || - ( (session->view->output_format == ImageEncoding::WEBP) && (complete_image.channels > 4) ) ){ + ( (session->view->output_format == ImageEncoding::WEBP) && (complete_image.channels > 4) ) || + ( (session->view->output_format == ImageEncoding::AVIF) && (complete_image.channels > 4) ) ){ int output_channels = (complete_image.channels==2)? 1 : 3; if( session->loglevel >= 5 ) function_timer.start(); diff --git a/src/Environment.h b/src/Environment.h index a3050f37..13a4161a 100644 --- a/src/Environment.h +++ b/src/Environment.h @@ -32,6 +32,8 @@ #define JPEG_QUALITY 75 #define PNG_QUALITY 1 #define WEBP_QUALITY 50 +#define AVIF_QUALITY 50 +#define AVIF_CODEC 0 // 0 = auto, 1 = aom, 2 = rav2e, 3 = svt #define MAX_CVT 5000 #define MAX_LAYERS 0 #define FILESYSTEM_PREFIX "" @@ -157,6 +159,32 @@ class Environment { } + static int getAVIFQuality(){ + const char* envpara = getenv( "AVIF_QUALITY" ); + int quality; + if( envpara ){ + quality = atoi( envpara ); + if( quality > 100 ) quality = 100; + if( quality < -1 ) quality = -1; // Allow -1 = lossless + } + else quality = AVIF_QUALITY; + + return quality; + } + + + static unsigned int getAVIFCodec(){ + unsigned int codec; + const char* envpara = getenv( "AVIF_CODEC" ); + if( envpara ){ + codec = atoi( envpara ); + if( codec > 3 ) codec = 0; + } + else codec = AVIF_CODEC; + return codec; + } + + static int getMaxCVT(){ const char* envpara = getenv( "MAX_CVT" ); int max_CVT; diff --git a/src/IIIF.cc b/src/IIIF.cc index 7c0a57a8..2816388f 100644 --- a/src/IIIF.cc +++ b/src/IIIF.cc @@ -196,6 +196,7 @@ void IIIF::run( Session* session, const string& src ) string host = session->headers["BASE_URL"]; if ( host.length() > 0 ){ string query = (session->headers["QUERY_STRING"]); + // Strip off the "IIIF=" query prefix query = query.substr( 5, query.length() - suffix.length() - 6 ); id = host + query; } @@ -271,9 +272,21 @@ void IIIF::run( Session* session, const string& src ) << " \"maxWidth\" : " << max << "," << endl << " \"maxHeight\" : " << max << "," << endl << " \"extraQualities\": [\"color\",\"gray\",\"bitonal\"]," << endl + +#if defined(HAVE_WEBP) || defined(HAVE_AVIF) + << " \"extraFormats\": [" #ifdef HAVE_WEBP - << " \"extraFormats\": [\"webp\"]," << endl + << "\"webp\"" +#endif +#if defined(HAVE_WEBP) && defined(HAVE_AVIF) + << "," +#endif +#ifdef HAVE_AVIF + << "\"avif\"" #endif + << "]," +#endif + << " \"extraFeatures\": [\"regionByPct\",\"sizeByForcedWh\",\"sizeByWh\",\"sizeAboveFull\",\"sizeUpscaling\",\"rotationBy90s\",\"mirroring\"]"; if( !rights.empty() ){ @@ -287,7 +300,7 @@ void IIIF::run( Session* session, const string& src ) infoStringStream << " \"@id\" : \"" << iiif_id << "\"," << endl << " \"profile\" : [" << endl << " \"" << IIIF_PROTOCOL << "/" << iiif_version << "/" << IIIF_PROFILE << ".json\"," << endl - << " { \"formats\" : [ \"jpg\", \"png\", \"webp\" ]," << endl + << " { \"formats\" : [ \"jpg\", \"png\", \"webp\", \"avif\" ]," << endl << " \"qualities\" : [\"native\",\"color\",\"gray\",\"bitonal\"]," << endl << " \"supports\" : [\"regionByPct\",\"regionSquare\",\"sizeByForcedWh\",\"sizeByWh\",\"sizeAboveFull\",\"sizeUpscaling\",\"rotationBy90s\",\"mirroring\"]," << endl << " \"maxWidth\" : " << max << "," << endl @@ -614,6 +627,9 @@ void IIIF::run( Session* session, const string& src ) #endif #ifdef HAVE_WEBP else if( format == "webp" ) session->view->output_format = ImageEncoding::WEBP; +#endif +#ifdef HAVE_AVIF + else if( format == "avif" ) session->view->output_format = ImageEncoding::AVIF; #endif else throw invalid_argument( "IIIF :: unsupported output format" ); } diff --git a/src/JTL.cc b/src/JTL.cc index 4461323c..2887cb8f 100644 --- a/src/JTL.cc +++ b/src/JTL.cc @@ -82,6 +82,9 @@ void JTL::send( Session* session, int resolution, int tile ){ #endif #ifdef HAVE_WEBP else if( session->view->output_format == ImageEncoding::WEBP ) compressor = session->webp; +#endif + #ifdef HAVE_AVIF + else if( session->view->output_format == ImageEncoding::AVIF ) compressor = session->avif; #endif else compressor = session->jpeg; @@ -330,7 +333,8 @@ void JTL::send( Session* session, int resolution, int tile ){ // For PNG and WebP, strip extra bands if we have more than 4 present if( ( (session->view->output_format == ImageEncoding::JPEG) && (rawtile.channels == 2 || rawtile.channels > 3) ) || ( (session->view->output_format == ImageEncoding::PNG) && (rawtile.channels > 4) ) || - ( (session->view->output_format == ImageEncoding::WEBP) && (rawtile.channels > 4) ) ){ + ( (session->view->output_format == ImageEncoding::WEBP) && (rawtile.channels > 4) ) || + ( (session->view->output_format == ImageEncoding::AVIF) && (rawtile.channels > 4) ) ){ unsigned int bands = (rawtile.channels==2) ? 1 : 3; if( session->loglevel >= 4 ){ diff --git a/src/Main.cc b/src/Main.cc index 6bb3b146..c1cde84c 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -296,6 +296,14 @@ int main( int argc, char *argv[] ) int webp_quality = Environment::getWebPQuality(); + // Get our default AVIF compression level + int avif_quality = Environment::getAVIFQuality(); + + + // Get our requested AVIF codec + unsigned int avif_codec = Environment::getAVIFCodec(); + + // Get our max CVT size int max_CVT = Environment::getMaxCVT(); @@ -391,6 +399,12 @@ int main( int argc, char *argv[] ) logfile << "Setting default WebP compression level to "; if( webp_quality == -1 ) logfile << "lossless" << endl; else logfile << webp_quality << endl; +#endif +#ifdef HAVE_AVIF + logfile << "Setting default AVIF compression level to "; + if( webp_quality == -1 ) logfile << "lossless" << endl; + else logfile << avif_quality << endl; + logfile << "Setting AVIF codec to " << AVIFCompressor::getCodecName( avif_codec ) << endl; #endif logfile << "Setting maximum CVT size to " << max_CVT << endl; logfile << "Setting HTTP Cache-Control header to '" << cache_control << "'" << endl; @@ -623,7 +637,10 @@ int main( int argc, char *argv[] ) #ifdef HAVE_WEBP WebPCompressor webp( webp_quality ); #endif - +#ifdef HAVE_AVIF + AVIFCompressor avif( avif_quality ); + avif.setCodec( avif_codec ); +#endif // View object for use with the CVT command etc View view; @@ -653,6 +670,9 @@ int main( int argc, char *argv[] ) #endif #ifdef HAVE_WEBP session.webp = &webp; +#endif +#ifdef HAVE_AVIF + session.avif = &avif; #endif session.loglevel = loglevel; session.logfile = &logfile; diff --git a/src/Makefile.am b/src/Makefile.am index 09e43e2e..2ccb44ca 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -21,6 +21,10 @@ if ENABLE_WEBP iipsrv_fcgi_LDADD += WebPCompressor.o endif +if ENABLE_AVIF +iipsrv_fcgi_LDADD += AVIFCompressor.o +endif + if ENABLE_MODULES iipsrv_fcgi_LDADD += DSOImage.o endif @@ -29,7 +33,8 @@ EXTRA_iipsrv_fcgi_SOURCES = Main.cc DSOImage.h DSOImage.cc \ KakaduImage.h KakaduImage.cc \ OpenJPEGImage.h OpenJPEGImage.cc \ PNGCompressor.h PNGCompressor.cc \ - WebPCompressor.h WebPCompressor.cc + WebPCompressor.h WebPCompressor.cc \ + AVIFCompressor.h AVIFCompressor.cc iipsrv_fcgi_SOURCES = \ IIPImage.h \ diff --git a/src/RawTile.h b/src/RawTile.h index b81e1c6a..abe3490f 100644 --- a/src/RawTile.h +++ b/src/RawTile.h @@ -34,7 +34,7 @@ enum class ColorSpace { NONE, GREYSCALE, sRGB, CIELAB, BINARY }; /// File format / encoding / compression types -enum class ImageEncoding { UNSUPPORTED, RAW, TIFF, JPEG2000, JPEG, DEFLATE, PNG, WEBP }; +enum class ImageEncoding { UNSUPPORTED, RAW, TIFF, JPEG2000, JPEG, DEFLATE, PNG, WEBP, AVIF }; /// Sample types enum class SampleType { FIXEDPOINT, FLOATINGPOINT }; diff --git a/src/Task.cc b/src/Task.cc index dc13769d..5b353ea7 100644 --- a/src/Task.cc +++ b/src/Task.cc @@ -1,7 +1,7 @@ /* IIP Command Handler Member Functions - Copyright (C) 2006-2022 Ruven Pillay. + Copyright (C) 2006-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 @@ -56,6 +56,9 @@ Task* Task::factory( const string& t ){ #endif #ifdef HAVE_WEBP else if( type == "wtl" ) return new WTL; +#endif +#ifdef HAVE_AVIF + else if( type == "atl" ) return new ATL; #endif else if( type == "jtl" ) return new JTL; else if( type == "jtls" ) return new JTLS; @@ -94,10 +97,10 @@ void QLT::run( Session* session, const string& argument ){ int factor = atoi( argument.c_str() ); // Check the value is realistic - if( factor < 0 || factor > 100 ){ + if( factor < -1 || factor > 100 ){ if( session->loglevel >= 2 ){ *(session->logfile) << "QLT :: Quality factor of " << argument - << " out of bounds. Must be 0-100 for JPEG and 0-9 for PNG" << endl; + << " out of bounds. Must be 0-100 for JPEG, WebP or AVIF and 0-9 for PNG" << endl; } } @@ -108,6 +111,9 @@ void QLT::run( Session* session, const string& argument ){ #ifdef HAVE_WEBP session->webp->setQuality( factor ); #endif +#ifdef HAVE_AVIF + session->avif->setQuality( factor ); +#endif if( session->loglevel >= 2 ) *(session->logfile) << "QLT :: Requested quality is " << factor << endl; } @@ -258,11 +264,17 @@ void CVT::run( Session* session, const string& src ){ if( session->loglevel >= 3 ) *(session->logfile) << "CVT :: PNG output" << endl; } #endif - #ifdef HAVE_WEBP +#ifdef HAVE_WEBP else if( argument == "webp" ){ session->view->output_format = ImageEncoding::WEBP; if( session->loglevel >= 3 ) *(session->logfile) << "CVT :: WebP output" << endl; } +#endif +#ifdef HAVE_AVIF + else if( argument == "avif" ){ + session->view->output_format = ImageEncoding::AVIF; + if( session->loglevel >= 3 ) *(session->logfile) << "CVT :: AVIF output" << endl; + } #endif else{ session->view->output_format = ImageEncoding::JPEG; diff --git a/src/Task.h b/src/Task.h index a892faed..d4df7cc1 100644 --- a/src/Task.h +++ b/src/Task.h @@ -1,5 +1,5 @@ /* - IIP Generic Task Class + IIP Session & Generic Task Classes Copyright (C) 2006-2024 Ruven Pillay @@ -26,7 +26,6 @@ #include "IIPImage.h" #include "IIPResponse.h" -#include "JPEGCompressor.h" #include "View.h" #include "TileManager.h" #include "Timer.h" @@ -35,16 +34,6 @@ #include "Watermark.h" #include "Transforms.h" #include "Logger.h" -#ifdef HAVE_PNG -#include "PNGCompressor.h" -#endif -#ifdef HAVE_WEBP -#include "WebPCompressor.h" -#endif - - -// Define our http header cache max age (24 hours) -#define MAX_AGE 86400 @@ -62,8 +51,6 @@ typedef HASHMAP imageCacheMapType; - - /// Structure to hold our session data struct Session { IIPImage **image; @@ -73,6 +60,9 @@ struct Session { #endif #ifdef HAVE_WEBP WebPCompressor* webp; +#endif +#ifdef HAVE_AVIF + AVIFCompressor* avif; #endif View* view; IIPResponse* response; @@ -268,6 +258,17 @@ class WTL : public JTL { }; +/// WebP Tile Command +class ATL : public JTL { +public: + void run( Session* session, const std::string& argument ){ + // Set our encoding format and call JTL::run + session->view->output_format = ImageEncoding::AVIF; + JTL::run( session, argument ); + }; +}; + + /// JPEG Tile Sequence Command class JTLS : public Task { public: diff --git a/src/TileManager.cc b/src/TileManager.cc index 402997a3..4da31699 100644 --- a/src/TileManager.cc +++ b/src/TileManager.cc @@ -97,6 +97,14 @@ RawTile TileManager::getNewTile( int resolution, int tile, int xangle, int yangl break; + case ImageEncoding::AVIF: + if( loglevel >= 4 ) compression_timer.start(); + compressor->Compress( ttt ); + if( loglevel >= 4 ) *logfile << "TileManager :: AVIF compression time: " + << compression_timer.getTime() << " microseconds" << endl; + break; + + case ImageEncoding::DEFLATE: // No deflate for the time being ;-) if( loglevel >= 4 ) *logfile << "TileManager :: DEFLATE compression requested: Not currently available" << endl; @@ -107,6 +115,7 @@ RawTile TileManager::getNewTile( int resolution, int tile, int xangle, int yangl break; } + } @@ -164,6 +173,14 @@ RawTile TileManager::getTile( int resolution, int tile, int xangle, int yangle, break; + case ImageEncoding::AVIF: + if( (rawtile = tileCache->getTile( image->getImagePath(), resolution, tile, + xangle, yangle, ImageEncoding::AVIF, compressor->getQuality() )) ) break; + if( (rawtile = tileCache->getTile( image->getImagePath(), resolution, tile, + xangle, yangle, ImageEncoding::RAW, 0 )) ) break; + break; + + case ImageEncoding::RAW: if( (rawtile = tileCache->getTile( image->getImagePath(), resolution, tile, xangle, yangle, ImageEncoding::RAW, 0 )) ) break; @@ -183,6 +200,7 @@ RawTile TileManager::getTile( int resolution, int tile, int xangle, int yangle, case ImageEncoding::JPEG: compName = "JPEG"; break; case ImageEncoding::PNG: compName = "PNG"; break; case ImageEncoding::WEBP: compName = "WebP"; break; + case ImageEncoding::AVIF: compName = "AVIF"; break; case ImageEncoding::DEFLATE: compName = "DEFLATE"; break; case ImageEncoding::RAW: compName = "RAW"; break; default: break; @@ -231,7 +249,7 @@ RawTile TileManager::getTile( int resolution, int tile, int xangle, int yangle, // PNG compression can have 8 or 16 bits and alpha channels if( (rawtile->compressionType == ImageEncoding::RAW) && ( ( ctype==ImageEncoding::JPEG && rawtile->bpc==8 && (rawtile->channels==1 || rawtile->channels==3) ) || - ctype==ImageEncoding::PNG || ctype==ImageEncoding::WEBP ) ){ + ctype==ImageEncoding::PNG || ctype==ImageEncoding::WEBP || ctype==ImageEncoding::AVIF ) ){ // Rawtile is a pointer to the cache data, so we need to create a copy of it in case we compress it RawTile ttt( *rawtile ); diff --git a/src/TileManager.h b/src/TileManager.h index cd8cfb46..70b8be87 100644 --- a/src/TileManager.h +++ b/src/TileManager.h @@ -33,6 +33,9 @@ #ifdef HAVE_WEBP #include "WebPCompressor.h" #endif +#ifdef HAVE_AVIF +#include "AVIFCompressor.h" +#endif #include "Cache.h" #include "Timer.h" #include "Watermark.h"