PHP
Article

Glide: Easy Dynamic on-Demand Image Resizing

By Bruno Skvorc

Glide kayaks image

Glide is an image processing library built on top of Intervention. Its purpose is to facilitate on-demand image processing. That’s a fancy way of saying it creates images as they’re requested if they don’t exist.

For example, you might have a large, high quality portrait for your user profile. But when opening on a mobile device, downloading a 10 MB image is not only wasteful, it is slow. Instead, you could add a media query into your CSS, like so:

@media (min-width: 400px) {
	.profilePic {
		background-image: url('/images/myprofile.jpg');	
	}
}

@media (max-width: 400px) {
    .profilePic {
		background-image: url('/images/myprofile-320.jpg');			
	}
}

This would make all devices under 400px of width load the smaller background image, thus downloading faster. But manually resizing every image you might need in your app is tedious, time consuming, and error prone. That’s where Glide comes in.

Glide can be configured to respond to missing image requests (such as the non-existant myprofile-320.jpg from the example above) by creating them from a predetermined source. In a nutshell, if the requested image doesn’t exist, but its source does, the requested image gets created from it. What’s more, the image can be saved into a cache, so that future requests don’t invoke the library and waste precious CPU resources on re-rendering.

Let’s configure it.

If you’d like to follow along, feel free to use our no-framework application and Homestead Improved for quickly setting up an environment and sample application to test this in.

Bootstrapping

Step one is installing Glide:

composer require league/glide

Then, we need to configure the Glide server. In the NOFW project above, this happens in app/config.php where all the services are configured for future injection via PHP-DI. This is how it’s done:

// ...

    'glide' => function () {
        $server = League\Glide\ServerFactory::create(
            [
                'source' => new League\Flysystem\Filesystem(
                    new League\Flysystem\Adapter\Local(
                        __DIR__ . '/../assets/image'
                    )
                ),
                'cache' => new League\Flysystem\Filesystem(
                    new League\Flysystem\Adapter\Local(
                        __DIR__ . '/../public/static/image'
                    )
                ),
                'driver' => 'gd',
            ]
        );

        return $server;
    },

// ...

Now we can push (or pull) the glide instance into a controller when needed. The source and cache values determine where original images are found, and where generated images should be stored respectively, while the driver key specifies that the built-in PHP GD image manipulation extension should be used to modify the images.

You should alter the image paths to apply to your own.

While these are the main ones, there are many other settings, too – feel free to study them at your own leisure.

Routing

Next, we need to define a route which will get triggered when an image which does not exist is requested. The no-framework application uses fastRoute, so defining the route is as simple as adding the following into app/routes.php:

['GET', '/static/image/{image}', ['MyControllers\Controllers\ImageController', 'renderImage']]

What ever {image} in the route evaluates to will get passed as $image to the renderImage method of ImageController (which we will write next).

Processing

Let’s build the ImageController now:

<?php

namespace MyControllers\Controllers;

use League\Glide\Server;
use Standard\Abstracts\Controller;

class ImageController extends Controller
{
    /**
     * @Inject("glide")
     * @var Server
     */
    private $glide;

    public function renderImage($image)
    {
        $width = $this->getWidth($image);

        $imageSource = str_replace('-'.$width, '', $image);
        $this->glide->outputImage($imageSource, ['w' => $width]);
    }

    /**
     * @param string $imagePath
     * @return int
     */
    private function getWidth(string $imagePath) : int
    {
        $fragments = explode('-', $imagePath);
        return (int)explode('.', $fragments[1])[0];
    }
}

The Glide server instance is automatically injected into the controller with PHP-DI’s @Inject annotation. If this is confusing or interesting, please see the documentation. Then, renderImage is given the image path, calls the internal getWidth method which extracts the width from the image name, and finally resizes and renders the image.

Note that we’re not using $_GET params for passing in resize settings as per Glide docs – this is because I prefer my image URLs to look like real URLs to static resources, so that they can later be used as actual static resources.

This works, but astute readers might notice a problem…

Securing

As it is, the application is vulnerable to mass-image-editing attacks. Someone could write a loop of thousands of different image sizes, slap them onto every image they can harvest from your website, and make your server convert to arbitrary and pointless sizes 24/7. The Glide docs recommend signing the URL so that only authorized URLs pass, but this is both complicated and very difficult to achieve when dealing with external stylesheets. We’d have to pre-process our CSS and regenerate all image URLs inside it – a complex procedure made even more complicated by the fact that we probably have some other, unrelated front end asset compilation steps, too (for that, see here).

The right approach is, instead, to limit the sizes to which images can be resized with either routes or specific rules. Since we only have one route which leads to our images, let’s take the latter approach – allowing only specific sizes to be generated right there in the processing logic.

<?php

namespace MyControllers\Controllers;

use League\Glide\Server;
use Standard\Abstracts\Controller;

class ImageController extends Controller
{
    /**
     * @Inject("glide")
     * @var Server
     */
    private $glide;

    private $allowedWidths = [
        320,
        768,
        992,
        1280,
        1920,
    ];

    public function renderImage($image)
    {
        $width = $this->getWidth($image);

        $imageSource = str_replace('-'.$width, '', $image);
        $this->glide->outputImage($imageSource, ['w' => $width]);
    }

    /**
     * @param string $imagePath
     * @return int
     */
    private function getWidth(string $imagePath) : int
    {
        $fragments = explode('-', $imagePath);

        if (count($fragments) < 2) {
            header(
                ($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0') . ' 500 ' . 'Server Error ImgErr00'
            );
            die("Nope! Image fragments missing.");
        }

        $width = (int)explode('.', $fragments[1])[0];
        if (!in_array($width, $this->allowedWidths)) {
            header(
                ($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0') . ' 500 ' . 'Server Error ImgErr01'
            );
            die("Nope! Disallowed width.");
        }

        return $width;
    }
}

The getWidth method was extended with some primitive checks – the first to make sure the image has a size extension (so no more generating original-size images! – that saves bandwidth, too) – and the second to make sure the size is allowed, by comparing it to the $allowedWidths array property in the controller.

Naturally, this can be extended with heights, sizing mode, and all the other Glide options on offer.

A Note on Saving with Glide

Our images are now being safely resized and rendered.

Unfortunately, Glide is “PHP only” in that it is not possible to have a server point to an image directly after it’s been generated. This is because the generated images always looks like this:

- images/
   - myprofile.jpg/
       lhgn3q489uncdue7b9qdny98xq3

… rather than the requested myprofile-320.jpg. The image name will actually be the folder, and the image will be an anonymous file in that folder. This means that in order to get back the myprofile-320.jpg image, PHP will always have to invoke Glide, which will always have to check if the image exists and then serve it, or generate it if it doesn’t.

Generally, this isn’t a dealbreaker due to the images being served with extra long-lasting headers:

        header('Cache-Control:'.'max-age=31536000, public');
        header('Expires:'.date_create('+1 years')->format('D, d M Y H:i:s').' GMT');

… and the fact that a single PHP request to serve an image before, again, using those super long expiry headers, isn’t that much of an overhead, but is something you might want to keep in mind when planning that next high traffic app. The situation can further be improved by putting a pull zone in front of everything, and even a more powerful caching server like Varnish, but that’s a story for another day.

Alternatively…

To completely bypass the PHP cycle in the subsequent renders, you could do something like this:

    public function renderImage($image)
    {
        $width = $this->getWidth($image);

        $imageSource = str_replace('-'.$width, '', $image);

        $imagePath = $this->glide->makeImage($imageSource, ['w' => $width]);
        $imageBase = $this->glide->getCache()->read($imagePath);
        $this->glide->getCache()->put($image, $imageBase);
        $this->glide->outputImage($imageSource, ['w' => $width]);
    }

makeImage creates and saves the image, but returns only its path. We then read the image’s contents with said path from the cache. Finally, we re-save the image under its originally requested name, and then output like we did before. Thus, only this first call will be expensive (several I/O operations and a conversion), and all future calls to this image URL will go straight to the image – bypassing PHP completely. In fact, if you power down PHP with sudo service php-fpm stop, the image will still load.

Conclusion

In this tutorial, we looked at the ease of use of Glide, an image manipulation package which can generate modified images on-demand, saving you the trouble of having to generate them before deploying your application.

Are you using Glide? An alternative approach perhaps? Let us know!

No Reader comments

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.