This is an example of a CDN PHP script for WordPress on Akamai/Linode

This is a repeat of the Example for EC2 Linux, using AWS Credentials to access the aws-sdk from a non-AWS platform.

We do not provide SES configurations on this site.


Linode: WordPress Media Library + Custom CDN Script


Please see the EC2 article for details.

mu-plugins scripts

Again, please see the EC2 article for details. One must use one’s own details in the scripts provided here.

jms-media-policy.php (for using multiple domains on one bucket)

<?php
/**
 * Jacous Media Policy — Clean Version
 */


/* ======================================================
   SECTION 1 — IMAGE SIZE REGISTRATION
====================================================== */

add_action('after_setup_theme', function () {

    add_image_size('max_1800', 1800, 1800, false);

}, 5);


/* ======================================================
   SECTION 2 — IMAGE QUALITY
====================================================== */

add_filter('jpeg_quality', function () {
    return 100;
});

add_filter('wp_editor_set_quality', function ($quality, $mime_type) {
    return ($mime_type === 'image/webp') ? 100 : $quality;
}, 10, 2);


/* ======================================================
   SECTION 3 — MIME TYPES
====================================================== */

add_filter('upload_mimes', function($mimes) {

    // Fonts
    $mimes['ttf']   = 'font/ttf';
    $mimes['otf']   = 'font/otf';
    $mimes['woff']  = 'font/woff';
    $mimes['woff2'] = 'font/woff2';
    $mimes['eot']   = 'application/vnd.ms-fontobject';

    // SVG
    $mimes['svg'] = 'image/svg+xml';

    // Audio
    $mimes['mp3']  = 'audio/mpeg';
    $mimes['wav']  = 'audio/wav';
    $mimes['ogg']  = 'audio/ogg';

    // Video
    $mimes['mp4']  = 'video/mp4';
    $mimes['webm'] = 'video/webm';
    $mimes['mov']  = 'video/quicktime';

    return $mimes;

});


jms-media-pipeline.php

--> This example uses a cdn bucket and domain called cdn.multi-domain.com, and then we can re-user the code for each domain under the mu-plugins directories in WordPress.
Hence, domain.com would use https://cdn.multi-domain.com/domain.com/wp-content/uploads/some_image.webp - that dort of thing.
I will also provide for a single domain and cdn/bucket further below.

<?php
/**
 * ======================================================
 * JME MEDIA PIPELINE — JACOUS.COM (FINAL STABLE)
 * Linode + AWS Profile + CloudFront
 * ======================================================
 */


/* ======================================================
   SECTION 1 — CORE FLAGS
====================================================== */

define('JME_ENABLE_WEBP', true);
define('JME_WEBP_ONLY', true);

define('JME_STORE_ORIGINALS', true);

define('JME_ALLOWED_SIZES', ['thumbnail','medium','large']);

define('JME_JPEG_QUALITY', 85);



/* ======================================================
   SECTION 2 — WATERMARK CONFIG
====================================================== */

define('JME_ENABLE_WATERMARK', true);

define('JME_WATERMARK_OPTIONS', [

    /* ======================================================
       LOGO SETTINGS
    ====================================================== */

    'logo' => WP_CONTENT_DIR . '/uploads/logo_white.png',

    // Size mode: fixed (px) OR percent (relative to image width)
    'logo_size_mode' => 'fixed',   // fixed | percent
    'logo_fixed_px'   => 120,
    'logo_percent'    => 0.12,

    'logo_opacity' => 0.5,

    /* ======================================================
       TEXT SETTINGS
    ====================================================== */

    'text' => '© jacous.com',
    'font' => WP_CONTENT_DIR . '/uploads/plex-light.ttf',

    // Font size mode: fixed OR percent of image width
    'font_size_mode' => 'fixed',   // fixed | percent
    'font_fixed_px'   => 28,
    'font_percent'    => 0.025,

    'text_color'   => '#ffffff',
    'text_opacity' => 0.6,

    /* ======================================================
       POSITIONING
    ====================================================== */

    // bottom-left | bottom-right | top-left | top-right | center
    'position' => 'bottom-left',

    // pixel-based outer spacing
    'margin' => 60,

    // spacing between logo and text
    'gap' => 40,

    /* ======================================================
       SHADOW SYSTEM
    ====================================================== */

    // simple | soft | stroke | stroke_shadow
    'shadow_mode' => 'stroke_shadow',

    'shadow_opacity' => 0.45,
    'shadow_offset'  => 2,

]);


/* ======================================================
   SECTION 3 — CDN CONFIG (MULTI-SITE PREFIX)
====================================================== */

define('JME_CDN_ENABLED', true);
define('JME_CDN_UPLOAD', true);
define('JME_CDN_NON_IMAGES', true);

/* YOUR REAL BUCKET */
define('JME_BUCKET', 'cdn.domain.com');

define('JME_REGION', 'ap-southeast-2');

/* PER SITE — CHANGE THIS PER INSTALL */
define('JME_CDN_PREFIX', 'domain.com');

/* CDN URL */
define('JME_CDN_URL', 'https://cdn.multi-domain.com/' . JME_CDN_PREFIX);

/* Site URL */
define('JME_SITE_URL', 'https://domain.com');

/* ======================================================
   SECTION 4 — AWS CLIENT (PROFILE)
====================================================== */

function jme_s3_client() {

    static $client = null;
    if ($client !== null) return $client;

    require_once '/var/www/aws-sdk/vendor/autoload.php';

    $client = new Aws\S3\S3Client([
        'version' => 'latest',
        'region'  => JME_REGION,
        'profile' => 'wordpress',
    ]);

    return $client;
}


/* ======================================================
   SECTION 5 — MIME SAFETY
====================================================== */

add_filter('wp_check_filetype_and_ext', function($data, $file, $filename, $mimes) {

    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    if ($ext === 'svg') {
        $data['ext']  = 'svg';
        $data['type'] = 'image/svg+xml';
        return $data;
    }

    if (in_array($ext, ['ttf','otf','woff','woff2','eot'], true)) {
        $data['ext']  = $ext;
        $data['type'] = $mimes[$ext] ?? 'application/octet-stream';
    }

    return $data;

}, 10, 4);



/* ======================================================
   SECTION 6 — WATERMARK ENGINE
====================================================== */

function jme_apply_watermark_resource($img) {

    if (!JME_ENABLE_WATERMARK) return $img;

    $opts = JME_WATERMARK_OPTIONS;

    imagealphablending($img, true);
    imagesavealpha($img, true);

    $img_w = imagesx($img);
    $img_h = imagesy($img);

    /* ======================================================
       SIZE CALCULATION (EXPLICIT MODES)
    ====================================================== */

    // LOGO SIZE
    if (($opts['logo_size_mode'] ?? 'fixed') === 'percent') {
        $logo_w = intval($img_w * $opts['logo_percent']);
    } else {
        $logo_w = $opts['logo_fixed_px'];
    }

    // FONT SIZE
    if (($opts['font_size_mode'] ?? 'fixed') === 'percent') {
        $font_size = intval($img_w * $opts['font_percent']);
    } else {
        $font_size = $opts['font_fixed_px'];
    }

    /* ======================================================
       LOAD LOGO
    ====================================================== */

    $logo_img = null;
    $logo_h = 0;

    if (!empty($opts['logo']) && file_exists($opts['logo'])) {

        $logo_img = imagecreatefrompng($opts['logo']);

        if ($logo_img) {

            imagealphablending($logo_img, true);
            imagesavealpha($logo_img, true);

            $logo_h = intval(imagesy($logo_img) * $logo_w / imagesx($logo_img));

            $resized = imagecreatetruecolor($logo_w, $logo_h);

            imagealphablending($resized, false);
            imagesavealpha($resized, true);

            $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
            imagefill($resized, 0, 0, $transparent);

            imagecopyresampled(
                $resized,
                $logo_img,
                0, 0, 0, 0,
                $logo_w,
                $logo_h,
                imagesx($logo_img),
                imagesy($logo_img)
            );

            $opacity = max(0, min(1, $opts['logo_opacity']));
            $alpha = 127 * (1 - $opacity);

            imagefilter($resized, IMG_FILTER_COLORIZE, 0, 0, 0, $alpha);

            imagedestroy($logo_img);
            $logo_img = $resized;
        }
    }

    /* ======================================================
       TEXT MEASURE
    ====================================================== */

    $text = $opts['text'];
    $font = $opts['font'];

    $bbox = imagettfbbox($font_size, 0, $font, $text);

    $text_w = abs($bbox[2] - $bbox[0]);
    $text_h = abs($bbox[7] - $bbox[1]);

    /* ======================================================
       LAYOUT CALC
    ====================================================== */

    $margin = $opts['margin'];
    $gap    = $opts['gap'];
    $pos    = $opts['position'];

    $logo_gap = $logo_img ? $gap : 0;

    /* ======================================================
       CORNER LAYOUT (HORIZONTAL)
    ====================================================== */

    if (in_array($pos, ['bottom-left','bottom-right','top-left','top-right'])) {

        $block_w = $logo_w + $logo_gap + $text_w;
        $block_h = max($logo_h, $text_h);

        switch ($pos) {

            case 'bottom-right':
                $x = $img_w - $block_w - $margin;
                $y = $img_h - $block_h - $margin;
                break;

            case 'top-left':
                $x = $margin;
                $y = $margin;
                break;

            case 'top-right':
                $x = $img_w - $block_w - $margin;
                $y = $margin;
                break;

            case 'bottom-left':
            default:
                $x = $margin;
                $y = $img_h - $block_h - $margin;
                break;
        }

        // LOGO
        if ($logo_img) {
            imagecopy($img, $logo_img, $x, $y, 0, 0, $logo_w, $logo_h);
        }

        // TEXT (vertically centered with logo)
        $text_x = $x + $logo_w + $logo_gap;
        $text_y = $y + ($block_h / 2) + ($text_h / 2);
    }

    /* ======================================================
       CENTER LAYOUT (VERTICAL STACK)
    ====================================================== */

    else {

        $block_h = $logo_h + $gap + $text_h;
        $block_w = max($logo_w, $text_w);

        $x = ($img_w - $block_w) / 2;
        $y = ($img_h - $block_h) / 2;

        // LOGO (centered)
        if ($logo_img) {
            $lx = $x + ($block_w - $logo_w) / 2;
            imagecopy($img, $logo_img, $lx, $y, 0, 0, $logo_w, $logo_h);
        }

        // TEXT (below logo)
        $text_x = $x + ($block_w - $text_w) / 2;
        $text_y = $y + $logo_h + $gap + $text_h;
    }

    /* ======================================================
       SHADOW SYSTEM
    ====================================================== */

    $shadow_mode = $opts['shadow_mode'] ?? 'simple';
    $offset = $opts['shadow_offset'] ?? 2;

    list($sr,$sg,$sb) = [0,0,0];
    $sa = 127 * (1 - ($opts['shadow_opacity'] ?? 0.4));

    $shadow = imagecolorallocatealpha($img, $sr,$sg,$sb,$sa);

    $draw_shadow = function($x, $y) use ($img, $font_size, $font, $text, $shadow, $offset, $shadow_mode) {

        if ($shadow_mode === 'simple') {

            imagettftext($img, $font_size, 0,
                $x + $offset,
                $y + $offset,
                $shadow,
                $font,
                $text
            );
        }

        elseif ($shadow_mode === 'soft') {

            imagettftext($img, $font_size, 0, $x+1, $y+1, $shadow, $font, $text);
            imagettftext($img, $font_size, 0, $x+2, $y+2, $shadow, $font, $text);
        }

        elseif ($shadow_mode === 'stroke') {

            foreach ([[-1,0],[1,0],[0,-1],[0,1]] as $o) {
                imagettftext($img, $font_size, 0,
                    $x + $o[0],
                    $y + $o[1],
                    $shadow,
                    $font,
                    $text
                );
            }
        }

        elseif ($shadow_mode === 'stroke_shadow') {

            foreach ([[-1,0],[1,0],[0,-1],[0,1]] as $o) {
                imagettftext($img, $font_size, 0,
                    $x + $o[0],
                    $y + $o[1],
                    $shadow,
                    $font,
                    $text
                );
            }

            imagettftext($img, $font_size, 0,
                $x + $offset,
                $y + $offset,
                $shadow,
                $font,
                $text
            );
        }
    };

    $draw_shadow($text_x, $text_y);

    /* ======================================================
       FINAL TEXT
    ====================================================== */

    list($r,$g,$b) = jme_hex2rgb($opts['text_color']);
    $alpha = 127 * (1 - $opts['text_opacity']);

    $color = imagecolorallocatealpha($img, $r,$g,$b,$alpha);

    imagettftext(
        $img,
        $font_size,
        0,
        $text_x,
        $text_y,
        $color,
        $font,
        $text
    );

    return $img;
}

/* ======================================================
   SECTION 7 — HELPERS
====================================================== */

function jme_hex2rgb($hex){
    $hex=str_replace("#","",$hex);

    return [
        hexdec(substr($hex,0,2)),
        hexdec(substr($hex,2,2)),
        hexdec(substr($hex,4,2))
    ];
}

/* ======================================================
   SECTION 7B — EXPOSE CUSTOM IMAGE SIZES TO UI
====================================================== */

/**
 * 1 — WordPress media UI (Gutenberg + classic)
 */
add_filter('image_size_names_choose', function ($sizes) {
    $sizes['max_1800'] = 'Max 1800px';
    return $sizes;
});


/**
 * 2 — Avada / Fusion Builder support -- ONLY FOR AVADA
 */
add_filter('fusion_image_sizes', function ($sizes) {
    $sizes['max_1800'] = esc_html__('Max 1800px', 'avada');
    return $sizes;
});


/* ======================================================
   SECTION 8 — WEBP PIPELINE (CLEAN + ORIGINAL TRACKING)
====================================================== */

$GLOBALS['jme_last_original'] = null;

add_filter('wp_handle_upload', function($upload) {

    if (!JME_ENABLE_WEBP || empty($upload['file'])) return $upload;

    $file = $upload['file'];
    $ext  = strtolower(pathinfo($file, PATHINFO_EXTENSION));

    if (!in_array($ext, ['jpg','jpeg','png','webp'], true)) {
        return $upload;
    }

    $uploads = wp_upload_dir();

    $original_dir = trailingslashit($uploads['basedir']) . 'original';

    if (!file_exists($original_dir)) {
        wp_mkdir_p($original_dir);
    }

    $orig_name = wp_unique_filename($original_dir, basename($file));
    $original_path = $original_dir . '/' . $orig_name;

    rename($file, $original_path);

    $filename = pathinfo($file, PATHINFO_FILENAME) . '.webp';
    $webp_file = $uploads['path'] . '/' . wp_unique_filename($uploads['path'], $filename);

    switch ($ext) {
        case 'jpg':
        case 'jpeg':
            $img = imagecreatefromjpeg($original_path);
            break;
        case 'png':
            $img = imagecreatefrompng($original_path);
            imagepalettetotruecolor($img);
            imagealphablending($img, false);
            imagesavealpha($img, true);
            break;
        case 'webp':
            $img = imagecreatefromwebp($original_path);
            break;
        default:
            return $upload;
    }

    if (!$img) return $upload;

    imagewebp($img, $webp_file, JME_JPEG_QUALITY);
    imagedestroy($img);

    $GLOBALS['jme_last_original'] = $original_path;

    $upload['file'] = $webp_file;
    $upload['url']  = str_replace($uploads['basedir'], $uploads['baseurl'], $webp_file);
    $upload['type'] = 'image/webp';

    return $upload;

}, 10);


/* SAVE ORIGINAL META */
add_action('add_attachment', function($id) {
    if (!empty($GLOBALS['jme_last_original'])) {
        update_post_meta($id, '_jme_original_file', $GLOBALS['jme_last_original']);
        unset($GLOBALS['jme_last_original']);
    }
});


/* ======================================================
   SECTION 9 — IMAGE SIZES
====================================================== */

add_filter('intermediate_image_sizes_advanced', function($sizes) {
    return array_intersect_key($sizes, array_flip(JME_ALLOWED_SIZES));
});


/* ======================================================
   SECTION 10 — WATERMARK (FULL SIZE ONLY)
====================================================== */

add_filter('wp_generate_attachment_metadata', function($metadata, $id) {

    if (!JME_ENABLE_WATERMARK) return $metadata;

    $file = get_attached_file($id);
    if (!$file || !file_exists($file)) return $metadata;

    if (preg_match('/-\d+x\d+/', $file)) return $metadata;

    $img = imagecreatefromwebp($file);
    if (!$img) return $metadata;

    $img = jme_apply_watermark_resource($img);

    imagewebp($img, $file, JME_JPEG_QUALITY);
    imagedestroy($img);

    return $metadata;

}, 95, 2);


/* ======================================================
   SECTION 11 — CDN UPLOAD (AFTER WATERMARK)
====================================================== */

add_filter('wp_generate_attachment_metadata', function($metadata, $attachment_id) {

    if (!JME_CDN_UPLOAD) return $metadata;

    $main = get_attached_file($attachment_id);
    if (!$main || !file_exists($main)) return $metadata;

    $uploads = wp_upload_dir();
    $base_dir = trailingslashit($uploads['basedir']);

    $upload = function($file) use ($base_dir) {

        if (!file_exists($file)) return;

        $type = wp_check_filetype($file)['type'] ?? 'application/octet-stream';
        $ext  = strtolower(pathinfo($file, PATHINFO_EXTENSION));

        if (strpos($type, 'image/') === 0) {
            if (JME_WEBP_ONLY && $ext !== 'webp') return;
            $cache = 'public, max-age=31536000, immutable';
        } else {
            if (!JME_CDN_NON_IMAGES) return;
            $cache = 'public, max-age=86400';
        }

//        $key = 'wp-content/uploads/' . ltrim(str_replace($base_dir, '', $file), '/');
// For multi-site
         $key = JME_CDN_PREFIX . '/wp-content/uploads/' . ltrim(str_replace($base_dir, '', $file), '/');

        try {
            jme_s3_client()->putObject([
                'Bucket'       => JME_BUCKET,
                'Key'          => $key,
                'SourceFile'   => $file,
                'ContentType'  => $type,
                'CacheControl' => $cache,
            ]);
        } catch (\Exception $e) {
            error_log('JME CDN ERROR: ' . $e->getMessage());
        }
    };

    $upload($main);

    $files = glob(dirname($main) . '/' . pathinfo($main, PATHINFO_FILENAME) . '-*.*');

    foreach ($files as $file) {
        $upload($file);
    }

    return $metadata;

}, 99, 2);


/* ======================================================
   SECTION 12 — URL REWRITE
====================================================== */

add_filter('wp_get_attachment_url', function($url) {

    if (!JME_CDN_ENABLED) return $url;

    return str_replace(
        JME_SITE_URL . '/wp-content/uploads/',
        JME_CDN_URL . '/wp-content/uploads/',
        $url
    );
});


/* ======================================================
   SECTION 13 — DELETE SYNC (FULL CLEANUP)
====================================================== */

add_action('delete_attachment', function($attachment_id) {

    if (!JME_CDN_UPLOAD) return;

    $main = get_attached_file($attachment_id);
    if (!$main) return;

    $uploads  = wp_upload_dir();
    $base_dir = trailingslashit($uploads['basedir']);

    $rm = function($file) use ($base_dir) {

        if (!$file) return;

        $relative = ltrim(str_replace($base_dir, '', $file), '/');
        // $key = 'wp-content/uploads/' . $relative;
        // For multi-site
        $key = JME_CDN_PREFIX . '/wp-content/uploads/' . $relative;

        try {
            jme_s3_client()->deleteObject([
                'Bucket' => JME_BUCKET,
                'Key'    => $key,
            ]);
        } catch (\Exception $e) {
            error_log('JME DELETE ERROR: ' . $e->getMessage());
        }

        if (file_exists($file)) {
            @unlink($file);
        }
    };

    $rm($main);

    $files = glob(dirname($main) . '/' . pathinfo($main, PATHINFO_FILENAME) . '-*.*');

    foreach ((array)$files as $file) {
        $rm($file);
    }

    $original = get_post_meta($attachment_id, '_jme_original_file', true);

    if (!empty($original)) {
        $rm($original);
    }

}, 10);


/* ======================================================
   END OF FILE
====================================================== */

And the same code for a single domain and bucket:

This example is not using the 1800 custom size or adding to Avada's theme. You can modify as required, but entries must match the jms-media-policy.php file.
This is for a single domain, e.g. https://cdn.domain.com and bucket.

<?php
/**
 * ======================================================
 * JME MEDIA PIPELINE — JACOUS.COM (FINAL STABLE)
 * Linode + AWS Profile + CloudFront
 * ======================================================
 */


/* ======================================================
   SECTION 1 — CORE FLAGS
====================================================== */

define('JME_ENABLE_WEBP', true);
define('JME_WEBP_ONLY', true);

define('JME_STORE_ORIGINALS', true);

define('JME_ALLOWED_SIZES', ['thumbnail','medium','large']);

define('JME_JPEG_QUALITY', 85);

/* ======================================================
   SECTION 2 — WATERMARK CONFIG
====================================================== */

define('JME_ENABLE_WATERMARK', true);

define('JME_WATERMARK_OPTIONS', [

    /* ======================================================
       LOGO SETTINGS
    ====================================================== */

    'logo' => WP_CONTENT_DIR . '/uploads/logo_white.png',

    // Size mode: fixed (px) OR percent (relative to image width)
    'logo_size_mode' => 'fixed',   // fixed | percent
    'logo_fixed_px'   => 120,
    'logo_percent'    => 0.12,

    'logo_opacity' => 0.5,

    /* ======================================================
       TEXT SETTINGS
    ====================================================== */

    'text' => '© jacous.com',
    'font' => WP_CONTENT_DIR . '/uploads/plex-light.ttf',

    // Font size mode: fixed OR percent of image width
    'font_size_mode' => 'fixed',   // fixed | percent
    'font_fixed_px'   => 28,
    'font_percent'    => 0.025,

    'text_color'   => '#ffffff',
    'text_opacity' => 0.6,

    /* ======================================================
       POSITIONING
    ====================================================== */

    // bottom-left | bottom-right | top-left | top-right | center
    'position' => 'bottom-left',

    // pixel-based outer spacing
    'margin' => 60,

    // spacing between logo and text
    'gap' => 40,

    /* ======================================================
       SHADOW SYSTEM
    ====================================================== */

    // simple | soft | stroke | stroke_shadow
    'shadow_mode' => 'stroke_shadow',

    'shadow_opacity' => 0.45,
    'shadow_offset'  => 2,

]);

/* ======================================================
   SECTION 3 — CDN CONFIG
====================================================== */

define('JME_CDN_ENABLED', true);
define('JME_CDN_UPLOAD', true);
define('JME_CDN_NON_IMAGES', true);

define('JME_BUCKET', 'cdn.domain.com');
define('JME_REGION', 'ap-southeast-2');

define('JME_CDN_URL', 'https://cdn.domain.com');
define('JME_SITE_URL', 'https://domain.com');


/* ======================================================
   SECTION 4 — AWS CLIENT (PROFILE)
====================================================== */

function jme_s3_client() {

    static $client = null;
    if ($client !== null) return $client;

    require_once '/var/www/aws-sdk/vendor/autoload.php';

    $client = new Aws\S3\S3Client([
        'version' => 'latest',
        'region'  => JME_REGION,
        'profile' => 'wordpress',
    ]);

    return $client;
}


/* ======================================================
   SECTION 5 — MIME SAFETY
====================================================== */

add_filter('wp_check_filetype_and_ext', function($data, $file, $filename, $mimes) {

    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    if ($ext === 'svg') {
        $data['ext']  = 'svg';
        $data['type'] = 'image/svg+xml';
        return $data;
    }

    if (in_array($ext, ['ttf','otf','woff','woff2','eot'], true)) {
        $data['ext']  = $ext;
        $data['type'] = $mimes[$ext] ?? 'application/octet-stream';
    }

    return $data;

}, 10, 4);


/* ======================================================
   SECTION 6 — WATERMARK ENGINE
====================================================== */


/* ======================================================
   SECTION 6 — WATERMARK ENGINE
====================================================== */

function jme_apply_watermark_resource($img) {

    if (!JME_ENABLE_WATERMARK) return $img;

    $opts = JME_WATERMARK_OPTIONS;

    imagealphablending($img, true);
    imagesavealpha($img, true);

    $img_w = imagesx($img);
    $img_h = imagesy($img);

    /* ======================================================
       SIZE CALCULATION (EXPLICIT MODES)
    ====================================================== */

    // LOGO SIZE
    if (($opts['logo_size_mode'] ?? 'fixed') === 'percent') {
        $logo_w = intval($img_w * $opts['logo_percent']);
    } else {
        $logo_w = $opts['logo_fixed_px'];
    }

    // FONT SIZE
    if (($opts['font_size_mode'] ?? 'fixed') === 'percent') {
        $font_size = intval($img_w * $opts['font_percent']);
    } else {
        $font_size = $opts['font_fixed_px'];
    }

    /* ======================================================
       LOAD LOGO
    ====================================================== */

    $logo_img = null;
    $logo_h = 0;

    if (!empty($opts['logo']) && file_exists($opts['logo'])) {

        $logo_img = imagecreatefrompng($opts['logo']);

        if ($logo_img) {

            imagealphablending($logo_img, true);
            imagesavealpha($logo_img, true);

            $logo_h = intval(imagesy($logo_img) * $logo_w / imagesx($logo_img));

            $resized = imagecreatetruecolor($logo_w, $logo_h);

            imagealphablending($resized, false);
            imagesavealpha($resized, true);

            $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
            imagefill($resized, 0, 0, $transparent);

            imagecopyresampled(
                $resized,
                $logo_img,
                0, 0, 0, 0,
                $logo_w,
                $logo_h,
                imagesx($logo_img),
                imagesy($logo_img)
            );

            $opacity = max(0, min(1, $opts['logo_opacity']));
            $alpha = 127 * (1 - $opacity);

            imagefilter($resized, IMG_FILTER_COLORIZE, 0, 0, 0, $alpha);

            imagedestroy($logo_img);
            $logo_img = $resized;
        }
    }

    /* ======================================================
       TEXT MEASURE
    ====================================================== */

    $text = $opts['text'];
    $font = $opts['font'];

    $bbox = imagettfbbox($font_size, 0, $font, $text);

    $text_w = abs($bbox[2] - $bbox[0]);
    $text_h = abs($bbox[7] - $bbox[1]);

    /* ======================================================
       LAYOUT CALC
    ====================================================== */

    $margin = $opts['margin'];
    $gap    = $opts['gap'];
    $pos    = $opts['position'];

    $logo_gap = $logo_img ? $gap : 0;

    /* ======================================================
       CORNER LAYOUT (HORIZONTAL)
    ====================================================== */

    if (in_array($pos, ['bottom-left','bottom-right','top-left','top-right'])) {

        $block_w = $logo_w + $logo_gap + $text_w;
        $block_h = max($logo_h, $text_h);

        switch ($pos) {

            case 'bottom-right':
                $x = $img_w - $block_w - $margin;
                $y = $img_h - $block_h - $margin;
                break;

            case 'top-left':
                $x = $margin;
                $y = $margin;
                break;

            case 'top-right':
                $x = $img_w - $block_w - $margin;
                $y = $margin;
                break;

            case 'bottom-left':
            default:
                $x = $margin;
                $y = $img_h - $block_h - $margin;
                break;
        }

        // LOGO
        if ($logo_img) {
            imagecopy($img, $logo_img, $x, $y, 0, 0, $logo_w, $logo_h);
        }

        // TEXT (vertically centered with logo)
        $text_x = $x + $logo_w + $logo_gap;
        $text_y = $y + ($block_h / 2) + ($text_h / 2);
    }

    /* ======================================================
       CENTER LAYOUT (VERTICAL STACK)
    ====================================================== */

    else {

        $block_h = $logo_h + $gap + $text_h;
        $block_w = max($logo_w, $text_w);

        $x = ($img_w - $block_w) / 2;
        $y = ($img_h - $block_h) / 2;

        // LOGO (centered)
        if ($logo_img) {
            $lx = $x + ($block_w - $logo_w) / 2;
            imagecopy($img, $logo_img, $lx, $y, 0, 0, $logo_w, $logo_h);
        }

        // TEXT (below logo)
        $text_x = $x + ($block_w - $text_w) / 2;
        $text_y = $y + $logo_h + $gap + $text_h;
    }

    /* ======================================================
       SHADOW SYSTEM
    ====================================================== */

    $shadow_mode = $opts['shadow_mode'] ?? 'simple';
    $offset = $opts['shadow_offset'] ?? 2;

    list($sr,$sg,$sb) = [0,0,0];
    $sa = 127 * (1 - ($opts['shadow_opacity'] ?? 0.4));

    $shadow = imagecolorallocatealpha($img, $sr,$sg,$sb,$sa);

    $draw_shadow = function($x, $y) use ($img, $font_size, $font, $text, $shadow, $offset, $shadow_mode) {

        if ($shadow_mode === 'simple') {

            imagettftext($img, $font_size, 0,
                $x + $offset,
                $y + $offset,
                $shadow,
                $font,
                $text
            );
        }

        elseif ($shadow_mode === 'soft') {

            imagettftext($img, $font_size, 0, $x+1, $y+1, $shadow, $font, $text);
            imagettftext($img, $font_size, 0, $x+2, $y+2, $shadow, $font, $text);
        }

        elseif ($shadow_mode === 'stroke') {

            foreach ([[-1,0],[1,0],[0,-1],[0,1]] as $o) {
                imagettftext($img, $font_size, 0,
                    $x + $o[0],
                    $y + $o[1],
                    $shadow,
                    $font,
                    $text
                );
            }
        }

        elseif ($shadow_mode === 'stroke_shadow') {

            foreach ([[-1,0],[1,0],[0,-1],[0,1]] as $o) {
                imagettftext($img, $font_size, 0,
                    $x + $o[0],
                    $y + $o[1],
                    $shadow,
                    $font,
                    $text
                );
            }

            imagettftext($img, $font_size, 0,
                $x + $offset,
                $y + $offset,
                $shadow,
                $font,
                $text
            );
        }
    };

    $draw_shadow($text_x, $text_y);

    /* ======================================================
       FINAL TEXT
    ====================================================== */

    list($r,$g,$b) = jme_hex2rgb($opts['text_color']);
    $alpha = 127 * (1 - $opts['text_opacity']);

    $color = imagecolorallocatealpha($img, $r,$g,$b,$alpha);

    imagettftext(
        $img,
        $font_size,
        0,
        $text_x,
        $text_y,
        $color,
        $font,
        $text
    );

    return $img;
}

/* ======================================================
   SECTION 7 — HELPERS
====================================================== */

function jme_hex2rgb($hex){
    $hex=str_replace("#","",$hex);

    return [
        hexdec(substr($hex,0,2)),
        hexdec(substr($hex,2,2)),
        hexdec(substr($hex,4,2))
    ];
}

/* ======================================================
   SECTION 7B — EXPOSE CUSTOM IMAGE SIZES TO UI
====================================================== */

/**
 * 1 — WordPress media UI (Gutenberg + classic)
 */
add_filter('image_size_names_choose', function ($sizes) {
    $sizes['max_1800'] = 'Max 1800px';
    return $sizes;
});


/**
 * 2 — Avada / Fusion Builder support -- ONLY FOR AVADA
 */
add_filter('fusion_image_sizes', function ($sizes) {
    $sizes['max_1800'] = esc_html__('Max 1800px', 'avada');
    return $sizes;
});



/* ======================================================
   SECTION 8 — WEBP PIPELINE (CLEAN + ORIGINAL TRACKING)
====================================================== */

$GLOBALS['jme_last_original'] = null;

add_filter('wp_handle_upload', function($upload) {

    if (!JME_ENABLE_WEBP || empty($upload['file'])) return $upload;

    $file = $upload['file'];
    $ext  = strtolower(pathinfo($file, PATHINFO_EXTENSION));

    if (!in_array($ext, ['jpg','jpeg','png','webp'], true)) {
        return $upload;
    }

    $uploads = wp_upload_dir();

    $original_dir = trailingslashit($uploads['basedir']) . 'original';

    if (!file_exists($original_dir)) {
        wp_mkdir_p($original_dir);
    }

    $orig_name = wp_unique_filename($original_dir, basename($file));
    $original_path = $original_dir . '/' . $orig_name;

    rename($file, $original_path);

    $filename = pathinfo($file, PATHINFO_FILENAME) . '.webp';
    $webp_file = $uploads['path'] . '/' . wp_unique_filename($uploads['path'], $filename);

    switch ($ext) {
        case 'jpg':
        case 'jpeg':
            $img = imagecreatefromjpeg($original_path);
            break;
        case 'png':
            $img = imagecreatefrompng($original_path);
            imagepalettetotruecolor($img);
            imagealphablending($img, false);
            imagesavealpha($img, true);
            break;
        case 'webp':
            $img = imagecreatefromwebp($original_path);
            break;
        default:
            return $upload;
    }

    if (!$img) return $upload;

    imagewebp($img, $webp_file, JME_JPEG_QUALITY);
    imagedestroy($img);

    $GLOBALS['jme_last_original'] = $original_path;

    $upload['file'] = $webp_file;
    $upload['url']  = str_replace($uploads['basedir'], $uploads['baseurl'], $webp_file);
    $upload['type'] = 'image/webp';

    return $upload;

}, 10);


/* SAVE ORIGINAL META */
add_action('add_attachment', function($id) {
    if (!empty($GLOBALS['jme_last_original'])) {
        update_post_meta($id, '_jme_original_file', $GLOBALS['jme_last_original']);
        unset($GLOBALS['jme_last_original']);
    }
});


/* ======================================================
   SECTION 9 — IMAGE SIZES
====================================================== */

add_filter('intermediate_image_sizes_advanced', function($sizes) {
    return array_intersect_key($sizes, array_flip(JME_ALLOWED_SIZES));
});


/* ======================================================
   SECTION 10 — WATERMARK (FULL SIZE ONLY)
====================================================== */

add_filter('wp_generate_attachment_metadata', function($metadata, $id) {

    if (!JME_ENABLE_WATERMARK) return $metadata;

    $file = get_attached_file($id);
    if (!$file || !file_exists($file)) return $metadata;

    if (preg_match('/-\d+x\d+/', $file)) return $metadata;

    $img = imagecreatefromwebp($file);
    if (!$img) return $metadata;

    $img = jme_apply_watermark_resource($img);

    imagewebp($img, $file, JME_JPEG_QUALITY);
    imagedestroy($img);

    return $metadata;

}, 95, 2);


/* ======================================================
   SECTION 11 — CDN UPLOAD (AFTER WATERMARK)
====================================================== */

add_filter('wp_generate_attachment_metadata', function($metadata, $attachment_id) {

    if (!JME_CDN_UPLOAD) return $metadata;

    $main = get_attached_file($attachment_id);
    if (!$main || !file_exists($main)) return $metadata;

    $uploads = wp_upload_dir();
    $base_dir = trailingslashit($uploads['basedir']);

    $upload = function($file) use ($base_dir) {

        if (!file_exists($file)) return;

        $type = wp_check_filetype($file)['type'] ?? 'application/octet-stream';
        $ext  = strtolower(pathinfo($file, PATHINFO_EXTENSION));

        if (strpos($type, 'image/') === 0) {
            if (JME_WEBP_ONLY && $ext !== 'webp') return;
            $cache = 'public, max-age=31536000, immutable';
        } else {
            if (!JME_CDN_NON_IMAGES) return;
            $cache = 'public, max-age=86400';
        }

        $key = 'wp-content/uploads/' . ltrim(str_replace($base_dir, '', $file), '/');

        try {
            jme_s3_client()->putObject([
                'Bucket'       => JME_BUCKET,
                'Key'          => $key,
                'SourceFile'   => $file,
                'ContentType'  => $type,
                'CacheControl' => $cache,
            ]);
        } catch (\Exception $e) {
            error_log('JME CDN ERROR: ' . $e->getMessage());
        }
    };

    $upload($main);

    $files = glob(dirname($main) . '/' . pathinfo($main, PATHINFO_FILENAME) . '-*.*');

    foreach ($files as $file) {
        $upload($file);
    }

    return $metadata;

}, 99, 2);


/* ======================================================
   SECTION 12 — URL REWRITE
====================================================== */

add_filter('wp_get_attachment_url', function($url) {

    if (!JME_CDN_ENABLED) return $url;

    return str_replace(
        JME_SITE_URL . '/wp-content/uploads/',
        JME_CDN_URL . '/wp-content/uploads/',
        $url
    );
});


/* ======================================================
   SECTION 13 — DELETE SYNC (FULL CLEANUP)
====================================================== */

add_action('delete_attachment', function($attachment_id) {

    if (!JME_CDN_UPLOAD) return;

    $main = get_attached_file($attachment_id);
    if (!$main) return;

    $uploads  = wp_upload_dir();
    $base_dir = trailingslashit($uploads['basedir']);

    $rm = function($file) use ($base_dir) {

        if (!$file) return;

        $relative = ltrim(str_replace($base_dir, '', $file), '/');
        $key = 'wp-content/uploads/' . $relative;

        try {
            jme_s3_client()->deleteObject([
                'Bucket' => JME_BUCKET,
                'Key'    => $key,
            ]);
        } catch (\Exception $e) {
            error_log('JME DELETE ERROR: ' . $e->getMessage());
        }

        if (file_exists($file)) {
            @unlink($file);
        }
    };

    $rm($main);

    $files = glob(dirname($main) . '/' . pathinfo($main, PATHINFO_FILENAME) . '-*.*');

    foreach ((array)$files as $file) {
        $rm($file);
    }

    $original = get_post_meta($attachment_id, '_jme_original_file', true);

    if (!empty($original)) {
        $rm($original);
    }

}, 10);


/* ======================================================
   END OF FILE
====================================================== */

This example uses a /home/nginx user and .aws subdirectory with a wordpress profile.

e.g.

File/Directory permissions are essential.

cd /home/nginx
ls -la

drwx------ 2 nginx nginx 4096 Apr 12 09:24 .aws

cd aws
ls -la

-rw------- 1 nginx nginx  115 Apr 12 09:23 config
-rw------- 1 nginx nginx  217 Apr 12 09:24 credentials

cat config

[profile wordpress]
region = ap-southeast-2
output = json

cat credentials

[wordpress]
aws_access_key_id = AKIA.......
aws_secret_access_key = abcdefg...........


/etc/nginx/nginx.conf would need to comment out or resize:

# client_max_body_size 10M; otherwise Media Library file sizes are restricted to 10MB max. Not good for larger PDF files.

www.conf (for php8.4 and Debian13):

php_admin_flag[allow_url_fopen] = on
php_admin_value[open_basedir] = /var/www:/var/www/html:/tmp:/usr/bin:/usr/local/bin/aws:/usr/share/phpMyAdmin:/home/nginx/.aws

--> Here we see that PHP needs to access phpmyadmin for database management. The other entries are needed.

 

 

AWS Configurations

We are very careful about publishing AWS examples. Read our non-liability statements.

See our article on EC2.

These are example permissions for the cdn.domain.com S3 Bucket:

--> We have used the same IAM user name as the .aws/profile for convenience.
This is the single domain example.
You can always modify the stanzasand check with AI for correctness.



{
    "Version": "2012-10-17",
    "Id": "PolicyForCdnCom",
    "Statement": [
        {
            "Sid": "AllowCloudFrontServicePrincipal",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::cdn.domain.com/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
                }
            }
        },
        {
            "Sid": "AllowWordPressMediaManagement",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT_ID:user/wordpress"
            },
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::cdn.domain.com",
                "arn:aws:s3:::cdn.domain.com/*"
            ]
        }
    ]
}

Here are the IAM User (wordpress) policies:
CDN_CreateInvalidation

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CloudFrontInvalidationAccess",
            "Effect": "Allow",
            "Action": "cloudfront:CreateInvalidation",
            "Resource": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
        }
    ]
}

cdn_multidomain:

--> This example shows use of multi-domains. Edit it to use a single domain.
e.g. s3:prefix would be wp-content/uploads/* without the domain name shown below.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "S3MediaAccess",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::cdn.multi-domain.com/domain.com/wp-content/uploads/*"
        },
        {
            "Sid": "S3ListBucket",
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::cdn.multi-domain.com",
            "Condition": {
                "StringLike": {
                    "s3:prefix": "domain.com/wp-content/uploads/*"
                }
            }
        },
        {
            "Sid": "CloudFrontInvalidation",
            "Effect": "Allow",
            "Action": "cloudfront:CreateInvalidation",
            "Resource": "*"
        }
    ]
}

And, CDN_Wordpress:

If using a single CDN bucket, you would have cdn.domain.com and cdn.domain.com/*

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "WordPressMediaBucketAccess",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": [
                "arn:aws:s3:::cdn.multi-domain.com",
                "arn:aws:s3:::cdn.multi-domain.com/*"
            ]
        }
    ]
}

When testing, failures need to be found in the various error logs, and sent to AI with policy examples. It is best to replace your account ID with ACCOUNT_ID at all times, rather than the actual account number.

An example:

The Media Library uses the URL https://cdn.domain.com/wp-content/uploads/…..

If we switch the PHP flag to false, the URL is https://domain.com/wp-content/uploads/…..

If we switch back, we go back to https://cdn…. which is fine as our defaults always place the media library content into the CDN anyway. There are various scenarios we are not covering, or sync script sot ensure consistency between local and CDN.

Disclaimer: This content is provided as reference only and reflects practical experience at the time of writing. Technology and best practices change, so examples may require modification. No warranty is provided. Always test configurations on a development system before using them in production.