0
\$\begingroup\$

I'm looking for feedback on a WordPress plugin I developed for automatically converting images to WebP and AVIF formats. The goal is to optimize image loading performance.

The plugin has the following key features:

  • Automatically converts images on upload to WebP and AVIF formats using predefined quality settings. Uses imagecreatefromjpeg/png/gif and imagewebp/imageavif functions.

  • Adds custom fields in media library for per-image quality control of JPEG, WebP, and AVIF.

  • Bulk conversion utility to convert all existing media library images. Shows progress bar during conversion.

  • Option to bulk delete all WebP/AVIF images.

  • Fallback to original image formats if WebP/AVIF is not supported by the browser.

    Uses best practices like namespace organization, OOP structure, and exception handling.

Main files:

public/class-web-avif-converter.php - Main plugin class
public/src/hooks/* - WordPress hooks for handling uploads and edits
public/src/generator-utils.php - Functions for image conversion
public/src/utils/* - Helper utilities

The full code is pasted below. I would greatly appreciate any feedback on:

  • Overall code structure and organization
  • Code quality, best practices
  • Performance optimizations or improvements
  • Any bugs or issues you see
  • Any other suggestions!

class-web-avif-converter.php

<?php

/**
 * The plugin bootstrap file
 *
 * This file is read by WordPress to generate the plugin information in the plugin
 * admin area. This file also includes all of the dependencies used by the plugin,
 * registers the activation and deactivation functions, and defines a function
 * that starts the plugin.
 *
 * @link              https://rekurencja.com/
 * @since             1.0.0
 * @package           Web_Avif_Converter
 *
 * @wordpress-plugin
 * Plugin Name:       WebP & Avif Converter
 * Plugin URI:        https://images.rekurencja.com
 * Description:       Fast and simple plugin to automatically convert and serve WebP & AVIF images.
 * Version:           1.0.0
 * Author:            Rekurencja
 * Author URI:        https://rekurencja.com/
 * License:           GPL-2.0+
 * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain:       web-avif-converter
 * Domain Path:       /languages
 */

// If this file is called directly, abort.
if (!defined('WPINC')) {
    die;
}

define('PHP_REQUIRED_VERSION', '8.1.0');
define('PHP_VERSION_OK', version_compare(phpversion(), PHP_REQUIRED_VERSION, '>='));
define('DEFAULT_QUALITY', 82);

require plugin_dir_path(__FILE__) . 'public/src/utils/utils.php';
require plugin_dir_path(__FILE__) . 'public/src/hooks/activation_hooks.php';
require plugin_dir_path(__FILE__) . 'public/src/hooks/attachment_hooks.php';
require plugin_dir_path(__FILE__) . 'public/src/utils/generator-utils.php';
// exceptions
require plugin_dir_path(__FILE__) . 'public/src/exceptions/exceptions.php';

// Include admin.php for admin page functionality
if (is_admin()) {
    require plugin_dir_path(__FILE__) . 'public/src/admin.php';
}

register_activation_hook(__FILE__, 'WebAvifConverter\Hooks\activate_web_avif_converter');
register_deactivation_hook(__FILE__, 'WebAvifConverter\Hooks\deactivate_web_avif_converter');

add_filter('attachment_fields_to_edit', 'add_image_quality_field', 10, 2);
add_filter('manage_media_columns', 'add_image_quality_column');
add_action('manage_media_custom_column', 'display_image_quality_column', 10, 2);


add_filter('jpeg_quality', 'get_jpeg_quality');
function get_jpeg_quality($quality)
{
    return get_option('wac_quality_jpeg', 82);
}


use WebAvifConverter\Hooks;
use WebAvifConverter\Utils;

function add_image_quality_column($columns)
{
    $columns['quality_webp'] = 'WebP Quality';
    $columns['quality_avif'] = 'Avif Quality';
    $columns['quality_jpeg'] = 'Jpeg Quality';

    return $columns;
}

function add_image_quality_field($form_fields, $post)
{
    // wp_die( print_r( $form_fields ));
    $form_fields['quality_webp'] = generate_quality_field($post->ID, 'quality_webp', 'Quality of WEBP');
    $form_fields['quality_avif'] = generate_quality_field($post->ID, 'quality_avif', 'Quality of AVIF');
    $form_fields['quality_jpeg'] = generate_quality_field($post->ID, 'quality_jpeg', 'Quality of JPEG');
    return $form_fields;
}

function determine_image_extension($image_url)
{
    $extension = pathinfo($image_url, PATHINFO_EXTENSION);
    return strtolower($extension);
}

function generate_quality_field($post_id, $field_name, $field_label)
{
    // get post metadata
    $metadata = wp_get_attachment_metadata($post_id);
    // echo $post_id;
    // wp_die(print_r($metadata));
    if (isset(wp_get_attachment_metadata($post_id)[$field_name])) {
        $field_value = wp_get_attachment_metadata($post_id)[$field_name];
        $placeholder_value = $field_value;
    } else {
        $field_value = '';
        $placeholder_value = 'Quality is not set';
    }

    $input_html = "<input type='number' min='0' max='100' step='1' ";
    $input_html .= "name='attachments[$post_id][$field_name]' ";
    $input_html .= "value='" . esc_attr($field_value ? $field_value : '') . "' ";
    $input_html .= "placeholder='" . esc_attr($placeholder_value) . "'/>";

    $field = array(
        'label' => $field_label,
        'input' => 'html',
        'helps' => 'Enter a value between 1 and 100 for image quality (100 = best quality, 1 = worst quality)',
        'html' => $input_html,
    );

    return $field;
}


function display_image_quality_column($column_name, $attachment_id)
{
    $allowed_columns = ['quality_avif', 'quality_webp', 'quality_jpeg'];

    if (in_array($column_name, $allowed_columns)) {
        $attachment_metadata = wp_get_attachment_metadata($attachment_id);

        if (isset($attachment_metadata[$column_name])) {
            echo $attachment_metadata[$column_name];
        } else {
            echo 'N/A';
        }
    }
}

add_action('admin_menu', 'webp_avif_bulk_convert_menu');
function webp_avif_bulk_convert_menu()
{
    add_submenu_page(
        'options-general.php',
        'WebP & Avif Converter',
        'WebP & Avif Converter',
        'manage_options',
        'webp_avif_bulk_convert',
        'webp_avif_bulk_convert_page'
    );
}

function get_images_size_info()
{
    // loop through all images
    $images = get_posts(
        array(
            'post_type' => 'attachment',
            'post_mime_type' => 'image',
            'numberposts' => -1,
        )
    );

    $images_size_info = [
        "total_original_size" => 0,
        "total_avif_size" => 0,
        "total_webp_size" => 0,
    ];

    echo "<div class='summary-wrapper'>";

    foreach ($images as $image) {
        try {
            // Determine the extension
            $extension = determine_image_extension($image->guid);

            // Handle unsupported extensions
            if ($extension === 'svg' || $extension === 'gif') {
                throw new FilesizeUnavailableException($extension, 'Unsupported extension: ' . $extension);
            }

            $metadata = wp_get_attachment_metadata($image->ID);

            $cur_image_size_info = [
                "original_size" => isset($metadata['filesize']) ? $metadata['filesize'] : 0,
                "avif_size" => 0,
                "webp_size" => 0,
            ];

            if (isset($metadata['sizes'])) {
                foreach ($metadata['sizes'] as $size_name => $size) {
                    $cur_image_size_info["original_size"] += isset($size['filesize']) ? $size['filesize'] : 0;
                }
            }

            if (isset($metadata['sizes_avif'])) {
                foreach ($metadata['sizes_avif'] as $size_name => $size) {
                    $cur_image_size_info["avif_size"] += isset($size['filesize']) ? $size['filesize'] : 0;
                }
            }

            if (isset($metadata['sizes_webp'])) {
                foreach ($metadata['sizes_webp'] as $size_name => $size) {
                    $cur_image_size_info["webp_size"] += isset($size['filesize']) ? $size['filesize'] : 0;
                }
            }

            if (!isset($metadata['sizes'])) {
                throw new FilesizeUnavailableException('Unknown', 'Filesize information not available for this file.');
            }

            $images_size_info['total_original_size'] += $cur_image_size_info['original_size'];
            $images_size_info['total_avif_size'] += $cur_image_size_info['avif_size'];
            $images_size_info['total_webp_size'] += $cur_image_size_info['webp_size'];

            echo "<div class='summary-item'>";
            echo "<img src='" . $image->guid . "' alt='Image ID: " . $image->ID . "' width='150px'>";
            echo "<h4>Image ID: " . $image->ID . "</h4>";
            echo "<p>Image Title: <b>" . $image->post_title . "</b></p>";
            echo "<p>Original size: " . size_format($cur_image_size_info['original_size']) . " </p>";
            echo "<p>Avif size: " . size_format($cur_image_size_info['avif_size']) . "</p>";
            echo "<p>Webp size: " . size_format($cur_image_size_info['webp_size']) . "</p>";
            echo "</div>";
        } catch (FilesizeUnavailableException $e) {
            echo "Filesize information not available for image ID " . $image->ID . ". " . $e->getMessage();
        } catch (Exception $e) {
            echo "An error occurred: " . $e->getMessage();
        }
    }
    


    echo "</div>";
    echo "<div class='total-summary-item'>";
    echo "<h4>Total Summary:</h4>";
    echo "<p>Total Original Image Size: " . size_format($images_size_info['total_original_size']) . "</p>";
    echo "<p>Total AVIF Image Size: " . size_format($images_size_info['total_avif_size']) . "</p>";
    echo "<p>Total WebP Image Size: " . size_format($images_size_info['total_webp_size']) . "</p>";
    echo "</div>";
    return $images_size_info;
}



function webp_avif_bulk_convert_page()
{
    if (isset($_POST['submit'])) {

        $quality_webp = isset($_POST['quality_webp']) ? intval($_POST['quality_webp']) : DEFAULT_QUALITY;
        $quality_avif = isset($_POST['quality_avif']) ? intval($_POST['quality_avif']) : DEFAULT_QUALITY;
        $quality_jpeg = isset($_POST['quality_jpeg']) ? intval($_POST['quality_jpeg']) : DEFAULT_QUALITY;

        if ($_POST['submit'] === 'Set') {
            echo '<div class="success alert conversion-wrapper" id="alert" [class.visible]="isVisible">';
            echo '<div class="content">';
            update_option('wac_quality_webp', $quality_webp);
            update_option('wac_quality_avif', $quality_avif);
            update_option('wac_quality_jpeg', $quality_jpeg);

            if (isset($_POST['regenerate']) && $_POST['regenerate'] === 'on') {
                echo '<span class="closebtn icon" onclick="this.parentElement.style.display=\'none\';"></span>';
                echo '<div class="progress-bar" id="progress-bar" style="width: 0%">0%;
                </div>';
                Hooks\update_all_attachments_quality($quality_webp, $quality_avif, $quality_jpeg);

                echo '<p class="notification notification-good">
                    All images have been converted to WebP and Avif formats.
                </p>';
            }
        } else if ($_POST['submit'] === 'Delete') {
            echo '<div class="success alert conversion-wrapper" id="alert" [class.visible]="isVisible">';
            echo '<div class="content">';
            Hooks\delete_all_attachments_avif_and_webp();
            echo '<p class="notification notification-bad">
                    All WebP and Avif images have been deleted.
                </p>';
        }

        echo '</div>';
        echo '</div>';
    }

    ?>
<div class="image-conversion-wrapper">
    <h1 class="conversion-heading">WebP & Avif Converter </h1>

    <form method="post">
        <?php $isPhpVersionOk = true; ?> <!-- Define PHP_VERSION_OK -->
        <fieldset <?php echo $isPhpVersionOk ? '' : 'disabled' ?>>
            <p class="conversion-description">Tool for WebP and Avif format conversion of all images in the
                uploads/media library directory. <br>
                <b>Choose quality</b> - choose the quality of the converted images (the lower the quality, the smaller
                the size) <br>
                <b>Delete function</b> - deletes all WebP and Avif images from the uploads/media library directory.
                <br>
                <b>Convert function</b> - converts all images in the uploads/media library directory to WebP and Avif
                formats. <br>
                <b>Note: </b> This tool will not convert images that are not in the uploads/media library directory.<br>
                <?php echo Utils\get_php_version_info(); ?>
            </p>
            <div class="conversion-options">
                <label class="option-label">Quality of WEBP: (0 - 100%)</label>
                <input type="number" name="quality_webp" value="<?php echo get_option('wac_quality_webp', DEFAULT_QUALITY); ?>" min="0" max="100">

                <label class="option-label">Quality of AVIF: (0 - 100%)</label>
                <input type="number" name="quality_avif" value="<?php echo get_option('wac_quality_avif', DEFAULT_QUALITY); ?>" min="0" max="100">

                <label class="option-label">Quality of JPEG: (0 - 100%)</label>
                <input type="number" name="quality_jpeg" value="<?php echo get_option('wac_quality_jpeg', DEFAULT_QUALITY); ?>" min="0" max="100">

                <input type="checkbox" name="regenerate" id="regenerate" checked>
                <label for="regenerate" class="option-label">
                    Update qualities of existing images
                </label>
                
                <div class="toggle-summary_w">
                <button id="toggle-summary">Show / Hide Podsumowanie</button>
                </div>
                <div class="conversion-summary-toggle">
                    <?php
                    // You need to define the get_images_size_info function or include the file that contains it.
                    get_images_size_info();
                    ?>
                </div>
            </div>
            
            <div class="conversion-buttons">
                <input type="submit" name="submit" value="Set" class="convert-button">
                <input type="submit" name="submit" value="Delete" class="delete-button">
            </div>
        </fieldset>
    </form>
</div>

<?php
}

/**
 * Runs the main functionality of the plugin.
 */
function run_web_avif_converter()
{
    // The 'Web_Avif_Converter' class code is included in the 'includes/class-web-avif-converter.php' file.
    require plugin_dir_path(__FILE__) . 'includes/class-web-avif-converter.php';
    // echo print_r
    $plugin = new Web_Avif_Converter();
    $plugin->run();
}
run_web_avif_converter();

attachment_hooks.php

<?php

namespace WebAvifConverter\Hooks;

use WebAvifConverter\Utils;
use WebAvifConverter\GeneratorUtils;

// function p($a, $die = 0){
    // echo '<pre>';
    // echo print_r($a);
    // echo '</pre>';
    // if ($die) { wp_die(); }
// }


apply_filters('jepg_quality', get_option('wac_quality_jpeg', DEFAULT_QUALITY));

add_filter('wp_generate_attachment_metadata', 'WebAvifConverter\Hooks\convert_images_on_generate_attachment_metadata', 10, 2);
function convert_images_on_generate_attachment_metadata($metadata, $attachment_id)
{
    if (!isset($metadata['file'])) { return $metadata; }

    $extension = pathinfo($metadata['file'], PATHINFO_EXTENSION);
    if (!in_array($extension, ['jpg', 'jpeg', 'png'])) { return $metadata; }
    
    $metadata['quality_jpeg'] = get_option('wac_quality_jpeg', DEFAULT_QUALITY);

    wp_update_attachment_metadata($attachment_id, $metadata);

    // Generating JPEGs with adjusted quality is handled by jepg_quality filter
    GeneratorUtils\generate_webp_sizes($attachment_id, get_option('wac_quality_webp', DEFAULT_QUALITY));
    GeneratorUtils\generate_avif_sizes($attachment_id, get_option('wac_quality_avif', DEFAULT_QUALITY));

    return wp_get_attachment_metadata($attachment_id);
}



add_action('edit_attachment', 'WebAvifConverter\Hooks\save_image_quality_metadata');
function save_image_quality_metadata($attachment_id)
{   
    $metadata = wp_get_attachment_metadata($attachment_id);

    $filed_names = ['quality_jpeg', 'quality_avif', 'quality_webp' ];

    foreach ($filed_names as $filed_name) {
        if (empty($_REQUEST['attachments'][$attachment_id][$filed_name])) { continue; }

        $quality = intval($_REQUEST['attachments'][$attachment_id][$filed_name]);

        if ($quality < 1 || $quality > 100 || isset($metadata[$filed_name]) && $quality === $metadata[$filed_name]) { continue; }

        if ($filed_name === 'quality_jpeg') { GeneratorUtils\generate_jpeg_sizes($attachment_id, $quality); }
        if ($filed_name === 'quality_webp') { GeneratorUtils\generate_webp_sizes($attachment_id, $quality); }
        if ($filed_name === 'quality_avif') { GeneratorUtils\generate_avif_sizes($attachment_id, $quality); }
    }   
}

function update_attachment_quality($attachment_id, $quality_webp = DEFAULT_QUALITY, $quality_avif = DEFAULT_QUALITY, $quality_jpeg = DEFAULT_QUALITY)
{
    $metadata = wp_get_attachment_metadata($attachment_id);

    if (empty($metadata) || !isset($metadata['file']) || !isset($metadata['sizes'])) {
        return; // No metadata found for this attachment.
    }

    if(!isset($metadata['quality_jpeg']) || intval($metadata['quality_jpeg']) !== $quality_jpeg){
        
        GeneratorUtils\generate_jpeg_sizes($attachment_id, $quality_jpeg);
    }

    if(!isset($metadata['quality_webp']) || intval($metadata['quality_webp']) !== $quality_webp){
        

        
        GeneratorUtils\generate_webp_sizes($attachment_id, $quality_webp);
    }
    
    if(!isset($metadata['quality_avif']) || intval($metadata['quality_avif']) !== $quality_avif){
        GeneratorUtils\generate_avif_sizes($attachment_id, $quality_avif);
    }
}

function update_all_attachments_quality($quality_webp, $quality_avif, $quality_jpeg)
{
    Utils\update_progress_bar(0);

    $attachments = get_posts(array(
        'post_type' => 'attachment',
        'numberposts' => -1,
        'post_status' => null,
        'exclude' => get_post_thumbnail_id()
    ));

    $total_attachments = count($attachments);
    $progress = 0;
    
    foreach ($attachments as $attachment) {
        $progress++;
        $percent = intval($progress / $total_attachments * 100);
        Utils\update_progress_bar($percent);
        update_attachment_quality($attachment->ID, $quality_webp, $quality_avif, $quality_jpeg);
    }
}

add_filter('delete_attachment', 'WebAvifConverter\Hooks\delete_attachment_files');
function delete_attachment_files($attachment_id)
{
    $metadata = wp_get_attachment_metadata($attachment_id);
    if (empty($metadata)) { return; }

    $upload_dir = wp_upload_dir()['basedir'];

    if (array_key_exists('file_webp', $metadata)) {
        Utils\safe_unlink($upload_dir . '/' . $metadata['file_webp']);
        unset($metadata['file_webp']);
    }

    if (array_key_exists('file_avif', $metadata)) {
        Utils\safe_unlink($upload_dir . '/' . $metadata['file_avif']);
        unset($metadata['file_avif']);
    }

    $attachment_subdir = dirname($metadata['file']);
    $attachment_files_dir = $upload_dir . '/' . $attachment_subdir . '/';

    if (array_key_exists('sizes_avif', $metadata)) {
        foreach ($metadata['sizes_avif'] as $size) {
            Utils\safe_unlink($attachment_files_dir . $size['file']);
        }
        unset($metadata['sizes_avif']);
    }
    
    if (array_key_exists('quality_webp', $metadata)){
        unset($metadata['quality_webp']);
    }
    
    if (array_key_exists('quality_avif', $metadata)){


        unset($metadata['quality_avif']);
    }

    if (array_key_exists('sizes_webp', $metadata)) {
        foreach ($metadata['sizes_webp'] as $size) {
            Utils\safe_unlink($attachment_files_dir . $size['file']);
        }
        unset($metadata['sizes_webp']);
    }wp_update_attachment_metadata($attachment_id, $metadata);
}

function delete_all_attachments_avif_and_webp()
{
    Utils\update_progress_bar(0);

    $attachments = get_posts(array(
        'post_type' => 'attachment',
        'numberposts' => -1,
        'post_mime_type' => 'image',
        'exclude' => get_post_thumbnail_id()
    ));

    $total_attachments = count($attachments);
    $progress = 0;

    foreach ($attachments as $attachment) {
        delete_attachment_files($attachment->ID);       
        $progress++;
        $percent = intval($progress / $total_attachments * 100) . "%";
        Utils\update_progress_bar($percent);
    }
}

generator-utils.php

<?php

namespace WebAvifConverter\GeneratorUtils;

/**
 * Convert image to WebP format
 *
 * @param string $path Path to the image file.
 * @param int $quality Image quality (0 to 100, default is 80).
 *
 * @return string|void Returns the relative path to the converted WebP image or void if unable to create an image resource.
 */

 function p($a, $die = 0){
    echo '<pre>';
    echo print_r($a);
    echo '</pre>';
    if ($die) { wp_die(); }
}

function generate_jpeg(string $path, int $quality = 80){
    $output_path = dirname($path);
    $extension = pathinfo($path, PATHINFO_EXTENSION);
    $filename = basename($path);

    $image = null;
    if ($extension === 'jpeg' || $extension === 'jpg') {
        $image = imagecreatefromjpeg($path);
    } elseif ($extension === 'png') {
        $image = imagecreatefrompng($path);
    } elseif ($extension === 'gif') {
        $image = imagecreatefromgif($path);
    } else {
        return; // Unsupported format.
    }

    if (!$image) {
        return; // Unable to create image resource, possibly unsupported format.
    }

    imagejpeg($image, $output_path . '/' . $filename , $quality);
    imagedestroy($image);

    $directories = explode('/', dirname($path));
    $upload_subdir = implode('/', array_slice($directories, -2));
    return $upload_subdir . '/' . $filename;
}

function generate_webp(string $path, int $quality = 80)
{
    $output_path = dirname($path);
    $extension = pathinfo($path, PATHINFO_EXTENSION);
    $filename = basename($path, '.' . $extension);

    $image = null;
    if ($extension === 'jpeg' || $extension === 'jpg') {
        $image = imagecreatefromjpeg($path);
    } elseif ($extension === 'png') {
        $image = imagecreatefrompng($path);
    } elseif ($extension === 'gif') {
        $image = imagecreatefromgif($path);
    }

    if (!$image) {
        return; // Unable to create image resource, possibly unsupported format.
    }

    imagewebp($image, $output_path . '/' . $filename . '.webp', $quality);
    imagedestroy($image);

    $directories = explode('/', dirname($path));
    $upload_subdir = implode('/', array_slice($directories, -2));
    return $upload_subdir . '/' . $filename . '.webp';
}

/**
 * Convert image to AVIF format
 *
 * @param string $path Path to the image file.
 * @param int $quality Image quality (0 to 100, default is 80).
 *
 * @return string|void Returns the relative path to the converted AVIF image or void if unable to create an image resource.
 */
function generate_avif(string $path, int $quality = 80)
{
    $output_path = dirname($path);
    $extension = pathinfo($path, PATHINFO_EXTENSION);
    $filename = basename($path, '.' . $extension);

    $image = null;
    if ($extension === 'jpeg' || $extension === 'jpg') {
        $image = imagecreatefromjpeg($path);
    } elseif ($extension === 'png') {
        $image = imagecreatefrompng($path);
    } elseif ($extension === 'gif') {
        $image = imagecreatefromgif($path);
    } else {
        return; // Unsupported format.
    }

    if (!$image) {
        return; // Unable to create image resource, possibly unsupported format.
    }

    imageavif($image, $output_path . '/' . $filename . '.avif', $quality);
    imagedestroy($image);

    $directories = explode('/', dirname($path));
    $upload_subdir = implode('/', array_slice($directories, -2));
    return $upload_subdir . '/' . $filename . '.avif';
}



function generate_jpeg_sizes($attachment_id, $quality){

    $metadata = wp_get_attachment_metadata($attachment_id);
    if (empty($metadata)) { return; }
    
    $original_image_path = wp_get_upload_dir()['basedir'] . '/' . $metadata['file'];
    $image_subdir_path = wp_get_upload_dir()['basedir'] . '/' . dirname($metadata['file']);

    $metadata['quality_jpeg'] = $quality;

    // skip svg files path info
    if (pathinfo($original_image_path, PATHINFO_EXTENSION) === 'svg') {
        return;
    }

    // p($metadata['sizes'], 1);
    foreach($metadata['sizes'] as $size_name => $size){
        $subsize_path  = $image_subdir_path . '/' . $size['file'];
        $image = wp_get_image_editor($subsize_path);
        $image->set_quality( $quality );
        $saved = $image->save($subsize_path);

        $metadata['sizes'][$size_name]['filesize'] = filesize($subsize_path);
    }

    wp_update_attachment_metadata($attachment_id, $metadata);
};


function generate_webp_sizes($attachment_id, $quality){
    $metadata = wp_get_attachment_metadata($attachment_id);
    if (empty($metadata)) { return; }
    
    $image_subdir_path = wp_get_upload_dir()['basedir'] . '/' . dirname($metadata['file']);

    $metadata['quality_webp'] = $quality;
    $metadata['sizes_webp'] = [];

    $metadata['file_webp'] = generate_webp(
        wp_get_upload_dir()['basedir'] . '/' . $metadata['file'],
        $quality
    );
    $metadata['webp_filesize'] = filesize(wp_get_upload_dir()['basedir'] . '/'. $metadata['file_webp']);

    $original_image_path = wp_get_upload_dir()['basedir'] . '/' . $metadata['file'];

    // skip svg files path info
    if (pathinfo($original_image_path, PATHINFO_EXTENSION) === 'svg') {
        return;
    }

    foreach($metadata['sizes'] as $size_name => $size){
        $original_subsize_path = $image_subdir_path . '/' . $size['file'];
        
        //create or overwrite the subsize image
        $subsize_subpath = generate_webp($original_subsize_path, $quality);

        $metadata['sizes_webp'][$size_name] = [
            'file' => basename($subsize_subpath),
            'width' => $size['width'],
            'height' => $size['height'],
            'mime-type' => 'image/webp',
            'filesize' => filesize(wp_get_upload_dir()['basedir'] . '/' . $subsize_subpath)
        ];
    }

    wp_update_attachment_metadata($attachment_id, $metadata);
};


function generate_avif_sizes($attachment_id, $quality){
    $metadata = wp_get_attachment_metadata($attachment_id);
    if (empty($metadata)) { return; }
    
    $image_subdir_path = wp_get_upload_dir()['basedir'] . '/' . dirname($metadata['file']);

    $metadata['quality_avif'] = $quality;
    $metadata['sizes_avif'] = [];

    $metadata['file_avif'] = generate_avif(
        wp_get_upload_dir()['basedir'] . '/' . $metadata['file'],
        $quality
    );
    $metadata['avif_filesize'] = filesize(wp_get_upload_dir()['basedir'] . '/'. $metadata['file_avif']);

    $original_image_path = wp_get_upload_dir()['basedir'] . '/' . $metadata['file'];

    // skip svg files path info
    if (pathinfo($original_image_path, PATHINFO_EXTENSION) === 'svg') {
        return;
    }

    foreach($metadata['sizes'] as $size_name => $size){
        $original_subsize_path = $image_subdir_path . '/' . $size['file'];

        //create or overwrite the subsize image
        $subsize_subpath = generate_avif($original_subsize_path, $quality);

        $metadata['sizes_avif'][$size_name] = [
            'file' => basename($subsize_subpath),
            'width' => $size['width'],
            'height' => $size['height'],
            'mime-type' => 'image/avif',
            'filesize' => filesize(wp_get_upload_dir()['basedir'] . '/' . $subsize_subpath)
        ];
    }

    wp_update_attachment_metadata($attachment_id, $metadata);
};

utils.php

<?php

namespace WebAvifConverter\Utils;

/**
 * Helper function for deleting files.
 */
function safe_unlink($path)
{
    if (file_exists($path)) {
        unlink($path);
    }
}

function update_progress_bar($percent) {
    $percent = strval($percent);
    echo "<script>
            var progressBar = document.getElementById('progress-bar');
            progressBar.style.width = '$percent%';
            progressBar.innerHTML = '$percent%';
    </script>";
    ob_flush();
    flush();
}

function get_php_version_info()
{
    if(version_compare(phpversion(), PHP_REQUIRED_VERSION, '>=')){
        return 'PHP Version is: <span class="php-version-good">' . phpversion() . '</span><b> version >= ' . PHP_REQUIRED_VERSION . '</b> is required.';
    } else {
        return 'PHP Version: <span class="php-version-bad">' . phpversion() . '</span> is too low <.>version >= ' . PHP_REQUIRED_VERSION . '</b> is required.';
    }
}
\$\endgroup\$
2
  • 2
    \$\begingroup\$ The full code has multiple files combined in one block, right? It may be helpful to have the files separated with headers above each one, like in this example. \$\endgroup\$ Oct 5, 2023 at 15:36
  • 1
    \$\begingroup\$ @SᴀᴍOnᴇᴌᴀ Done, thanks for tip \$\endgroup\$ Oct 6, 2023 at 7:46

1 Answer 1

2
+50
\$\begingroup\$

Foreword

I haven't use Wordpress in quite some time and when I did I only really added a plugin to log data in an external database. I recently installed Wordpress locally but didn't get it running so I honestly haven't had a chance to add the converter and try it out.

General feedback

The code is not too difficult to read. Two of the files contain a docblocks. The first one is for the bootstrap the file. There appear to be four docblocks above functions. Only two of them list parameters and possible return values. If the goal is to distribute/share the plugin then it would likely be best to add docblocks above all functions describing what the functions achieve as well as parameters, return values, possible exceptions, etc.

It may or may not have been a goal to have the code in line with a style guide but it mostly adheres to PSR-12. For class-web-avif-converter.php there are only a few warnings - e.g. using else if instead of elseif, and lines longer than 120 characters. For attachment_hooks there are numerous other warnings and the same is true for generator-utils.php.

The description lists the main files:

public/class-web-avif-converter.php - Main plugin class
public/src/hooks/* - WordPress hooks for handling uploads and edits
public/src/generator-utils.php - Functions for image conversion
public/src/utils/* - Helper utilities

I was expecting to see a class definition in public/class-web-avif-converter.php but apparently there is no class defined there. I do see it has this comment near the end of the file within function run_web_avif_converter():

// The 'Web_Avif_Converter' class code is included in the 'includes/class-web-avif-converter.php' file.

Perhaps a more appropriate name for public/class-web-avif-converter.php would be public/web-avif-converter-plugin.php

Some of the code is repetitive - e.g. the functions generate_jpeg(), generate_webp() and generate_avif() all achieve very similar things and have many repeated (or at least similar). One could abstract some of the common functionality out to separate functions.

There don't appear to be unit/feature/end-to-end tests. If those exist then it would be good to include those in sub-sequent posts. If those do not exist then it would be good to create and maintain those. I haven't gone through it but I see there is a handbook page for plugin integration tests on the wordpress.org site.

Suggestions

Add type declarations to function arguments and return values

Type declarations can help ensure that functions are called with the correct type of arguments and also contribute to the readability and documentation. There do appear to be some type declarations for some functions in generator-utils.php though most functions in other files have no type declarations.

Use the null coalescing operator to simplify logic

For example - in display_image_quality_column():

if (isset($attachment_metadata[$column_name])) {
     echo $attachment_metadata[$column_name];
} else {
    echo 'N/A';
}

Can be simplified to the following using the null coalescing operator:

    echo $attachment_metadata[$column_name] ?? 'N/A';

Obviously there is one less function call and if the function was called numerous times there may be a noticeable change in performance but it may not be much.

In the screenshot below is a comparison of using the null-coalescing operator on the left compared to using isset() with a ternary operator. Obviously it is a trivial snippet of code but it shows there are fewer number of operations using the null-coalescing operator.

sampleComparisonOfVLDOutput

Similarly in the foreach in get_images_size_info() is this assignment:

$cur_image_size_info = [
    "original_size" => isset($metadata['filesize']) ? $metadata['filesize'] : 0,
    "avif_size" => 0,
    "webp_size" => 0,
];

The ternary can be simplified to using the null-coalescing operator:

$cur_image_size_info = [
    "original_size" => $metadata['filesize'] ?? 0,
    "avif_size" => 0,
    "webp_size" => 0,
];

The same also applies to the four lines inside the conditional loops that add file size values to the elements in cur_image_size_info.

Utilize array index inside foreach instead of updating counter

In function update_all_attachments_quality() there is an increment of counter.

$progress = 0;

foreach ($attachments as $attachment) {
    $progress++;
    $percent = intval($progress / $total_attachments * 100);
    Utils\update_progress_bar($percent);
    update_attachment_quality($attachment->ID, $quality_webp, $quality_avif, $quality_jpeg);
}

The increment of $progress could be switched to pre-fix increment and consolidated in the line where $percent is assigned:

     $percent = intval(++$progress / $total_attachments * 100);

However, $progress can be eliminated by using the second form of foreach:

foreach ($attachments as $index => $attachment) {
    $percent = intval(($index + 1)/ $total_attachments * 100);
    Utils\update_progress_bar($percent);
    update_attachment_quality($attachment->ID, $quality_webp, $quality_avif, $quality_jpeg);
}

The same applies to delete_all_attachments_avif_and_webp().

Use short echo tags to simplify output within HTML

The short echo tag <?= can be used in place of <?php echo. It appears there are five places that occurs - mostly in webp_avif_bulk_convert_page().

else following a conditional return can be removed

In get_php_version_info() the else keyword can be removed which allows for the indentation level of the last return to be decreased.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.