0Wordpress LogoAutomatically generate and serve avif images with wordpress

I have spent quite a lot of time optimising the performance of this site, and as part of ongoing work I monitor new technologies that could help improve the speed for visitors. I’ve been tracking the adoption of next-generation image formats for a while and with support by web browsers now fairly widespread it was time to figure out how to make use of these new formats in wordpress.

There are multiple steps required which I have broken down into headings below

Enable support in nginx

The first step is to enable your web server (in my case nginx) to recognise the mime types of the new formats. To do this you need to edit mime.types which is likely to be found at /etc/nginx/mime.types. I added the following section

image/heic                                       heic;
image/heif                                       heif;
image/heic-sequence                              heics;
image/heif-sequence                              heifs;
image/avif                                       avif;
image/avif-sequence                              avis;
image/jxl                                        jxl;

Automatically serve files where they are available

The next step is to tell nginx to serve files automatically whenever they exist (and to fall back to older formats where a new format file doesn’t exist)
To do this first edit the main nginx config file (usually /etc/nginx/nginx.conf) and add the following section inside the http{} section of the config

map $http_accept $img_ext
{
   ~image/jxl   '.jxl';
   ~image/avif  '.avif';
   ~image/webp  '.webp';
   default      '';
}

Note that I am only trying to serve jxl (JPEG-XL) or avif files, you could add more options (in order of preference!) if you wish.
Next, you need to add the following under the server{} section of the nginx config (which may be in the main config file or may be in a separate config file depending how you’ve set up your nginx config structure)

location ~* ^.+\.(png|jpg|jpeg)$
{
   add_header Vary Accept;
   try_files $uri$img_ext $uri =404;
}

Now nginx will look for image.jpg.jxl and then image.jpg.avif and then image.jpg.webp and finally image.jpg when it is asked for image.jpg by any browsers that support the newer formats. Next we need to enable them in wordpress

enable next-gen formats in wordpress

Add the following code to your functions.php (ideally do this in a child theme so that when you update your theme your changes don’t get over-written)

/***************************************************\
* Enable SVG support and other modern image formats *
\***************************************************/
function cc_mime_types( $mimes )
{
   $mimes['svg'] = 'image/svg+xml';
   $mimes['webp'] = 'image/webp';
   $mimes['heic'] = 'image/heic';
   $mimes['heif'] = 'image/heif';
   $mimes['heics'] = 'image/heic-sequence';
   $mimes['heifs'] = 'image/heif-sequence';
   $mimes['avif'] = 'image/avif';
   $mimes['avis'] = 'image/avif-sequence';
   $mimes['jxl'] = 'image/jxl';
   return $mimes;
}
add_filter( 'upload_mimes', 'cc_mime_types' );

Note that I also enabled support for SVG images at the same time

Now you can actually upload next gen format images and use them directly in wordpress if you want to, but I don’t recommend this as many older browsers don’t support them yet – and we’ve already set up nginx to serve them intelligently so we should make use of that. What we want to do is to automatically generate the new formats when we upload images (there are plugins that do this for webp already, but nothing that does it for jxl or avif yet).

Install libheif

This step does require access to the command line of your web host, which is straight-forward if you run a VPS, but may not be so simple if you’re on shared hosting in which case you might need to ask your host for support
Run the following bash commands (these are selected for Centos 8, other distributions may be a little different)

dnf -y install --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-8.noarch.rpm
dnf -y install libheif

Add custom function to wordpress functions.php to compress all uploaded images (and their thumbnails) to avif format

As with the earlier functions.php step I recommend adding this in a child theme

/*****************************\
* Convert png and jpg to avif *
\*****************************/
add_filter( 'wp_generate_attachment_metadata', 'jps_compress_img', 10, 2 );
function jps_compress_img( $metadata, $attachment_id )
{
   // get filepath from id
   $filepath= get_attached_file($attachment_id);

   // is the file a png or jpeg
   if(pathinfo($filepath, PATHINFO_EXTENSION)=="jpg" || pathinfo($filepath, PATHINFO_EXTENSION)=="jpeg" || pathinfo($filepath, PATHINFO_EXTENSION)=="png")
   {
      // If so, run compression of the main file to avif
      shell_exec("heif-enc $filepath -o $filepath.avif -q 50 -A");
      $parts = preg_split('~/(?=[^/]*$)~', $filepath);

      // Then run compression of the thumbnails to avif
      $thumbpaths = $metadata['sizes'];
      foreach ($thumbpaths as $key => $thumb)
      {
          $thumbpath= $thumb['file'];
          $thumbfullpath= $parts[0] . "/" . $thumbpath;
          shell_exec("heif-enc $thumbfullpath -o $thumbfullpath.avif -q 50 -A");
      }
   }
   // give back what we got
   return $metadata;
}

//dont forget to delete the avifs if the attachments are deleted
function jps_delete_avif($attachment_id)
{
   $all_images= get_intermediate_image_sizes($attachment_id);
   foreach($all_images as $each_img)
   {
      $each_img_det= wp_get_attachment_image_src($attach_id,$each_img);
      $each_img_path= ABSPATH.'wp-content'.substr($each_img_det[0],strpos($each_img_det[0],"/uploads/")).'.avif';
      shell_exec("rm -f $each_img_path");
   }
}
add_action( 'delete_attachment', 'jps_delete_avif' );

At present that is it – all images you upload will be converted to avif copies (with the originals retained). You can regenerate all of your images to create the avif files by using a plugin. Note – I haven’t yet set up JPEG-XL compression as support for it isn’t available in mainstream browsers yet (although it is in prerelease browsers so it is coming very soon).

Whilst I’m covering next gen formats and optimisation though I have one final tip – To compress SVG images with gzip (or even better zopfli and brotli). To do that requires another custom function…

Bonus: SVG compression

/*************************************************\
* Compress SVG images more with brotli and zopfli *
\*************************************************/
add_filter( 'wp_generate_attachment_metadata', 'jps_compress_vectors', 10, 2 );
function jps_compress_vectors( $metadata, $attachment_id )
{
    // get filepath from id
    $filepath= get_attached_file($attachment_id);

    // is the file an svg
    if(pathinfo($filepath, PATHINFO_EXTENSION)=="svg")
    {
       // If so, run compression of it using zopfli and brotli
       shell_exec("zopfli --gzip $filepath");
       shell_exec("brotli --best --output=$filepath.br $filepath");
    }

    // give back what we got
    return $metadata;
}

Note you will need gzip_static and brotli_static enabled in your nginx config.

Self-compile for a newer version

I found that on Centos the latest version of heif-enc is 1.7 which has quite a few bugs when creating avifs. So I opted to compile my own newer (1.12) build and use that instead. Doing so was a little complicated as it required the aom codec as a shared library. To do so run the following bash commands. Make sure to run them as a normal user, not as the root user. Also note some of these commands may not be strictly needed, it took me a while to get it working and I just made a note of what worked – I am by no means a linux expert!

dnf install x265 x265-devel svt-av1
export CXXFLAGS="$CXXFLAGS -fPIC"
cd ~

git clone https://aomedia.googlesource.com/aom
mkdir -p aom_build
cd aom_build
cmake ~/aom -DBUILD_SHARED_LIBS=true
make
sudo make install
cp ./libaom.so.3 /usr/bin/local/libaom.so.3

cd ~
export PKG_CONFIG_PATH=~/aom_build/
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/aom_build/
export LD_LIBRARY_PATH

git clone --recurse-submodules --recursive https://github.com/strukturag/libheif.git
cd libheif
./autogen.sh
./configure
make
sudo make install

LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/bin/
export LD_LIBRARY_PATH

Once you’ve done that, make sure it works by running php -a and running the following command

shell_exec("/usr/local/bin/heif-enc -A");

You should get a full output, not just a 1 line error. Assuming that works ok you can modify your functions.php so that each of the shell-exec references points to /usr/local/bin/heif-enc instead of just heif-enc

/*****************************\
* Convert png and jpg to avif *
\*****************************/
add_filter( 'wp_generate_attachment_metadata', 'jps_compress_img', 10, 2 );
function jps_compress_img( $metadata, $attachment_id )
{
   // get filepath from id
   $filepath= get_attached_file($attachment_id);

   // is the file a png or jpeg
   if(pathinfo($filepath, PATHINFO_EXTENSION)=="jpg" || pathinfo($filepath, PATHINFO_EXTENSION)=="jpeg" || pathinfo($filepath, PATHINFO_EXTENSION)=="png")
   {
      // If so, run compression of the main file to avif
      shell_exec("/usr/local/bin/heif-enc $filepath -o $filepath.avif -q 50 -A");
      $parts = preg_split('~/(?=[^/]*$)~', $filepath);

      // Then run compression of the thumbnails to avif
      $thumbpaths = $metadata['sizes'];
      foreach ($thumbpaths as $key => $thumb)
      {
          $thumbpath= $thumb['file'];
          $thumbfullpath= $parts[0] . "/" . $thumbpath;
          shell_exec("/usr/local/bin/heif-enc $thumbfullpath -o $thumbfullpath.avif -q 50 -A");
      }
   }
   // give back what we got
   return $metadata;
}

What do you think? Drop us a comment below! If you would like to subscribe please use the subscribe link on the menu at the top right. You can also share this with your friends by using the social links below. Cheers.

Leave a Reply