Skip to content

Commit da5b2de

Browse files
committed
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.
1 parent b4cd473 commit da5b2de

20 files changed

+606
-32
lines changed

.github/workflows/c-cpp.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- uses: actions/checkout@v3
1414

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

1818
- name: Configure
1919
run: |
@@ -31,7 +31,7 @@ jobs:
3131
- uses: actions/checkout@v3
3232

3333
- name: Install dependencies
34-
run: brew install automake libtool fcgi libtiff openjpeg libmemcached libpng webp libomp
34+
run: brew install automake libtool fcgi libtiff openjpeg libmemcached libpng webp libavif libomp
3535

3636
- name: Configure
3737
run: |

ChangeLog

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

513

614
29/06/2024:

README

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,10 @@ PNG_QUALITY: The default PNG quality factor for compression when the client does
404404

405405
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.
406406

407+
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.
408+
409+
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).
410+
407411
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.
408412

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

configure.ac

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ AC_CHECK_HEADERS([chrono])
7575
AC_CHECK_HEADERS([unordered_map])
7676
AC_CHECK_HEADERS([tr1/unordered_map])
7777
AC_CHECK_HEADERS([ext/hash_map])
78+
AC_CHECK_HEADER([thread], [AC_DEFINE(HAVE_STL_THREAD)])
7879
AX_CXX_COMPILE_STDCXX(11)
7980
AC_LANG_POP([C++])
8081

@@ -190,6 +191,31 @@ else
190191
fi
191192

192193

194+
#************************************************************
195+
# Check for AVIF support
196+
#************************************************************
197+
198+
AVIF=false
199+
AC_ARG_ENABLE( avif,
200+
[ --disable-avif disable AVIF])
201+
202+
203+
if test "x$enable_avuf" == "xno"; then
204+
AC_MSG_RESULT([configure: disabling AVIF support])
205+
AM_CONDITIONAL([ENABLE_AVIF], [false])
206+
else
207+
AC_CHECK_HEADER( [avif/avif.h], [AVIF=true], [AVIF=false] )
208+
AC_SEARCH_LIBS( [avifEncoderAddImage], [avif], [AVIF=true], [AVIF=false] )
209+
210+
if test "x${AVIF}" = xtrue; then
211+
AM_CONDITIONAL([ENABLE_AVIF], [true])
212+
AC_DEFINE(HAVE_AVIF)
213+
else
214+
AM_CONDITIONAL([ENABLE_AVIF], [false])
215+
fi
216+
217+
fi
218+
193219

194220
#************************************************************
195221
# Check for libdl for dynamic library loading
@@ -422,8 +448,9 @@ Options Enabled:
422448
JPEG2000 : ${JPEG2000_CODEC}
423449
OpenMP : ${OPENMP}
424450
Loggers : ${LOGGING}
425-
PNG Output : ${PNG}
426-
WebP Output : ${WEBP}])
451+
PNG Output : ${PNG}
452+
WebP Output : ${WEBP}
453+
AVIF Output : ${AVIF}])
427454

428455
if [test "x${DEBUG}" = xtrue]; then
429456
AC_MSG_RESULT([ Debug mode : activated])

docker/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ RUN apk add --no-cache \
2929
openjpeg-dev \
3030
libpng-dev \
3131
libwebp-dev \
32+
libavif-dev \
3233
libmemcached-dev \
3334
lighttpd
3435

docker/Dockerfile.debian

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ apt-get install --no-install-recommends -y \
3636
make \
3737
libpng-dev \
3838
libwebp-dev \
39+
libavif-dev \
3940
libmemcached-dev \
4041
lighttpd
4142

man/iipsrv.8

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.TH IIPSRV 8 "May 2023" "Ruven Pillay"
1+
.TH IIPSRV 8 "September 2024" "Ruven Pillay"
22
.SH NAME
33

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

12-
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.
12+
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.
1313

1414
.SH SYNOPSIS
1515

@@ -66,7 +66,16 @@ The default is 1.
6666
.IP WEBP_QUALITY
6767
The default WebP quality factor for compression when the client does not specify one.
6868
The value should be between 0 (highest level of compression) and 100 (highest image quality).
69+
A value of -1 can be used to specify lossless encoding.
6970
The default is 50.
71+
.IP AVIF_QUALITY
72+
The default AVIF quality factor for compression when the client does not specify one.
73+
The value should be between 0 (highest level of compression) and 100 (highest image quality).
74+
A value of -1 can be used to specify lossless encoding.
75+
The default is 50.
76+
.IP AVIF_CODEC
77+
The AVIF codec to use for encoding. Integer value. Set 0 for automatic codec selection, 1 for aom, 2 for rav1e, 3 for svt.
78+
Default is 0 (automatic codec selection)
7079
.IP MAX_IMAGE_CACHE_SIZE
7180
Max image cache size to be held in RAM in MB. This is a cache of
7281
the compressed JPEG image tiles requested by the client. The default

src/AVIFCompressor.cc

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/* IIP AVIF Compressor Class:
2+
Handles alpha channels, ICC profiles and XMP metadata
3+
4+
Copyright (C) 2024 Ruven Pillay
5+
6+
This program is free software; you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation; either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program; if not, write to the Free Software Foundation,
18+
Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
19+
20+
*/
21+
22+
23+
#include "AVIFCompressor.h"
24+
25+
#if HAVE_STL_THREAD
26+
#include <thread>
27+
#endif
28+
29+
using namespace std;
30+
31+
32+
/// Initialize chunk-based encoding for the CVT handler
33+
void AVIFCompressor::InitCompression( const RawTile& rawtile, unsigned int strip_height ){
34+
35+
// Manually set up the correct width and height for this particular tile and point to the existing data buffer
36+
tile.width = rawtile.width;
37+
tile.height = rawtile.height;
38+
tile.channels = rawtile.channels;
39+
tile.bpc = rawtile.bpc;
40+
tile.data = rawtile.data;
41+
tile.dataLength = rawtile.dataLength;
42+
tile.capacity = rawtile.capacity;
43+
tile.memoryManaged = 0; // We don't want the RawTile destructor to free this memory
44+
45+
// libavif cannot handle strip or region-based encoding, so compress the entire image in one go
46+
this->Compress( tile );
47+
48+
current_chunk = 0;
49+
}
50+
51+
52+
53+
/// libwebp cannot handle line or region-based encoding, so simulate strip-based output using byte chunks
54+
unsigned int AVIFCompressor::CompressStrip( unsigned char* source, unsigned char* output, unsigned int tile_height ){
55+
56+
// Initialize our chunk size only once at the start of the sequence
57+
if( current_chunk == 0 ) chunk_size = (unsigned int)( ( (tile.dataLength*tile_height) + (tile_height/2) ) / tile.height );
58+
59+
// Make sure we don't over-run our allocated memory
60+
if( (current_chunk + chunk_size) > (tile.dataLength - 1) ) chunk_size = tile.dataLength - current_chunk;
61+
62+
// Copy our chunk of data to the given output buffer
63+
if( chunk_size > 0 ){
64+
unsigned char* data = (unsigned char*) tile.data;
65+
memcpy( output, &data[current_chunk], chunk_size );
66+
current_chunk += chunk_size;
67+
}
68+
69+
return chunk_size;
70+
}
71+
72+
73+
74+
unsigned int AVIFCompressor::Finish( unsigned char* output ){
75+
76+
// Output any remaining bytes
77+
if( current_chunk < tile.dataLength-1 ){
78+
unsigned char* data = (unsigned char*) tile.data;
79+
chunk_size = tile.dataLength - current_chunk - 1;
80+
memcpy( output, &data[current_chunk], chunk_size );
81+
return chunk_size;
82+
}
83+
84+
return 0;
85+
}
86+
87+
88+
89+
/// Compress a single tile of data
90+
unsigned int AVIFCompressor::Compress( RawTile& rawtile ){
91+
92+
avifResult OK;
93+
avifRWData output = AVIF_DATA_EMPTY;
94+
95+
// Initialize image structure
96+
avifPixelFormat format = AVIF_PIXEL_FORMAT_YUV420;
97+
98+
#if AVIF_VERSION_MAJOR >= 1
99+
// Use full 4:4:4 sampling for lossless
100+
if( this->Q == -1 ) format = AVIF_PIXEL_FORMAT_YUV444;
101+
#endif
102+
103+
if( rawtile.channels == 1 ) format = AVIF_PIXEL_FORMAT_YUV400;
104+
105+
// Create our image structure
106+
avif = avifImageCreate( (uint32_t) rawtile.width, (uint32_t) rawtile.height, (uint32_t) rawtile.bpc, format );
107+
if( !avif ){
108+
throw string( "AVIFCompressor :: avifImageCreate() error" );
109+
}
110+
111+
avifRGBImage rgb;
112+
avifRGBImageSetDefaults( &rgb, avif );
113+
114+
115+
// Set channel layout
116+
rgb.format = (rawtile.channels==4) ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB;
117+
118+
119+
// Monochrome single band input not directly supported - duplicate to 3 identical bands
120+
if( rawtile.channels == 1 ) rawtile.triplicate();
121+
122+
#if AVIF_VERSION_MAJOR >= 1
123+
rgb.chromaDownsampling = AVIF_CHROMA_DOWNSAMPLING_FASTEST;
124+
#endif
125+
rgb.rowBytes = rawtile.width * rawtile.channels * (rawtile.bpc/8);
126+
rgb.pixels = (uint8_t*) rawtile.data; // rgb.pixels type is uint8_t even for 10 bit AVIF
127+
128+
129+
// Initialize encoder
130+
encoder = avifEncoderCreate();
131+
if( !encoder ){
132+
throw string( "AVIFCompressor :: avifEncoderCreate() error" );
133+
}
134+
135+
// Set our encoder options
136+
encoder->codecChoice = this->codec;
137+
encoder->speed = AVIF_SPEED_FASTEST;
138+
139+
140+
// Auto-tiling and Quality parameter only exists in version 1 onwards
141+
#if AVIF_VERSION_MAJOR >= 1
142+
encoder->autoTiling = true;
143+
if( this->Q == -1 ){
144+
encoder->quality = AVIF_QUALITY_LOSSLESS;
145+
encoder->maxQuantizer = AVIF_QUANTIZER_LOSSLESS;
146+
}
147+
else encoder->quality = this->Q;
148+
#else
149+
encoder->maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY;
150+
#endif
151+
152+
// Set threading concurrency
153+
#if AVIF_VERSION_MAJOR >= 1 && HAVE_STL_THREAD
154+
rgb.maxThreads = std::thread::hardware_concurrency();
155+
encoder->maxThreads = rgb.maxThreads;
156+
#endif
157+
158+
159+
if( (OK=avifImageRGBToYUV( avif, &rgb )) != AVIF_RESULT_OK ){
160+
throw string( "Failed to convert to YUV(A) " + string(avifResultToString(OK)) );
161+
}
162+
163+
164+
// Add ICC profile and XMP metadata to our image
165+
writeICCProfile();
166+
writeXMPMetadata();
167+
168+
169+
if( (OK=avifEncoderAddImage( encoder, avif, 1, AVIF_ADD_IMAGE_FLAG_SINGLE )) != AVIF_RESULT_OK ){
170+
throw string( "AVIFCompressor :: Failed to add image to encoder: " + string(avifResultToString(OK)) );
171+
}
172+
173+
if( (OK=avifEncoderFinish( encoder, &output )) != AVIF_RESULT_OK ){
174+
throw string( "AVIFCompressor :: Failed to finish encode: " + string(avifResultToString(OK)) );
175+
}
176+
177+
178+
// Allocate the appropriate amount of memory if the encoded AVIF is larger than the raw image buffer
179+
if( output.size > rawtile.capacity ){
180+
if( rawtile.memoryManaged ) delete[] (unsigned char*) rawtile.data;
181+
rawtile.data = new unsigned char[output.size];
182+
rawtile.capacity = output.size;
183+
}
184+
185+
// Copy the encoded data back into our rawtile buffer
186+
memcpy( rawtile.data, output.data, output.size );
187+
188+
if( avif ){
189+
avifImageDestroy( avif );
190+
}
191+
if( encoder ){
192+
avifEncoderDestroy( encoder );
193+
}
194+
195+
// Return our compressed tile
196+
rawtile.dataLength = output.size;
197+
198+
// Free our output structure
199+
avifRWDataFree( &output );
200+
201+
rawtile.quality = this->Q;
202+
rawtile.compressionType = ImageEncoding::AVIF;
203+
return rawtile.dataLength;
204+
}
205+
206+
207+
208+
/// Write ICC profile
209+
void AVIFCompressor::writeICCProfile()
210+
{
211+
size_t len = icc.size();
212+
if( len == 0 ) return;
213+
214+
#if AVIF_VERSION_MAJOR < 1
215+
// No return from version < 1
216+
avifImageSetProfileICC( avif, (const uint8_t*) icc.c_str(), len );
217+
#else
218+
if( avifImageSetProfileICC( avif, (const uint8_t*) icc.c_str(), len ) != AVIF_RESULT_OK ){
219+
throw string( "AVIFCompressor :: Error adding ICC profile" );
220+
}
221+
#endif
222+
}
223+
224+
225+
226+
/// Write XMP metadata
227+
void AVIFCompressor::writeXMPMetadata()
228+
{
229+
size_t len = xmp.size();
230+
if( len == 0 ) return;
231+
232+
#if AVIF_VERSION_MAJOR < 1
233+
// No return from version < 1
234+
avifImageSetMetadataXMP( avif, (const uint8_t*) xmp.c_str(), len );
235+
#else
236+
if( avifImageSetMetadataXMP( avif, (const uint8_t*) xmp.c_str(), len ) != AVIF_RESULT_OK ){
237+
throw string( "AVIFCompressor :: Error adding XMP metadata" );
238+
}
239+
#endif
240+
}

0 commit comments

Comments
 (0)