Skip to main content

4. Implementing an image resizing solution yourself

If you are going to implement your own image resizing solution, the first thing to understand is the basic infrastructure and execution flow.

Infrastructure

The basic flow is thus:

image.png

Your website is going to involve uploading to cloud storage even if you don't build your own image resizing solution, so the only parts you particularly need to do here are implementing your cloud resizing function, and understanding how to use the API for your serverless code provider to invoke that function.

Invoking serverless code functions

For most providers, there is some API method of doing this. Here I will cover AWS Lambda since I'm familiar with it, but some of the principles likely apply to other providers as well.

Your webserver needs to be able to authenticate itself to AWS Lambda, usually in initialising the API client it will use. In PHP initialising the Lambda client looks like this:

$lambdaAdminClient = new Aws\Lambda\LambdaClient([
	'credentials' => [
		'key' => '<the Access Key for the IAM role you want to use to invoke the function>',
		'secret' => '<the Secret Key for the IAM role you want to use to invoke the function>',
	],
	'region' => '<the region of your function, e.g. eu-west-3>',
	'version' => '<the API version to use for AWS Lambda - these are dates specified in the documentation, e.g. 2015-03-31>',
]);

It's best to create an IAM user whose specific job is for running Lambda functions, rather than e.g. using some 'superuser' IAM role with a huge amount of permissions. Firstly because it's security best practice not to have unnecessary permissions, but also so that the user gets an error if you unintentionally make it do something it isn't supposed to do.

Do not store these credentials on the webserver itself! This shouldn't need to be said, but I'm saying it anyway. For AWS, The Parameter Store within the Systems Manager service is a good option for securely storing credentials; you could also store them in your database server. Unless it's impossible to avoid, which it usually isn't, you should not keep any credentials on the webservers themselves in case a directory containing them was inadvertently exposed to the Internet.

Example Permissions Policy for the IAM User

Here is an example policy - see the notes for important details to pay attention to.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "lambda:CreateFunction",
                "lambda:UpdateFunctionCode",
                "iam:PassRole",
                "lambda:InvokeFunction",
                "lambda:GetLayerVersion",
                "lambda:UpdateFunctionConfiguration",
                "lambda:DeleteFunction"
            ],
            "Resource": [
                "arn:aws:iam::<your AWS account number>:role/<the name of the Lambda execution role that the function is executed with>",
                "arn:aws:lambda:<the AWS region of your Lambda function>:<your AWS account number>:function:<the name/s of the function this user is allowed to execute>",
                "arn:aws:lambda:<the AWS region of your Lambda function>:<your AWS account number>:layer:*:*"
            ]
        }
    ]
}
Notes
  • The function name you specify in the Resources array can include wildcards. I would suggest naming functions with some prefix, e.g. "Your-Website-FunctionName" so that you can put "Your-Website-*" in this field, letting your user run only functions that have that prefix and not any others.

  • The Lambda execution role is not the same as this IAM user - it is its own Role entity in the IAM console. You need to give this role its own set of permissions to do its job, which is covered later.

  • The iam:PassRole permission is necessary here, because under the hood your IAM user cannot execute the Lambda function itself, even though it has the permissions to do so. It has to be able to "pass" to the Lambda execution role you specified in the Resources array.

The Lambda "execution role"

Whenever your Lambda function is run, it runs with the permissions of a specific role, the execution role specified for that function. Here's an example from Deserted Chateau's test environment.

zwLimage.png

This role is entirely separate from the IAM user your webserver uses to request the Lambda function be run. You can modify the role the function uses either after creating it, or when creating it. You will need to modify the permissions of the role you use in the IAM console, under Access Management -> Roles.

Generally speaking, your Lambda execution role is going to need more than one permissions policy (it's messy to try and put all permissions you need in one policy).

Example Basic Permissions Policy for the Lambda Execution Role

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ec2:CreateNetworkInterface",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "logs:PutLogEvents",
            "Resource": "arn:aws:logs:*:<your AWS account number>:log-group:*:log-stream:*"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:CreateLogGroup"
            ],
            "Resource": "arn:aws:logs:*:<your AWS account number>:log-group:*"
        }
    ]
}
Notes
  • You only need the EC2 network interface permissions if your execution role is going to be used for invoking, or creating, functions that will run within a VPC (it doesn't do much harm to put them in there, anyway). Without those permissions, functions that need to run in a VPC won't work.

Example Additional Permissions Policy for the Lambda Execution Role

This policy is specifically so our execution role can perform S3 operations (as it needs to be able to get the image files from S3 to resize them, and then put the new resized versions back into S3).

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::<AWS bucket name 1>/*",
                "arn:aws:s3:::<AWS bucket name 1>",
                "arn:aws:s3:::<AWS bucket name 2>/*",
                "arn:aws:s3:::<AWS bucket name 2>",
                "arn:aws:s3:::<AWS bucket name 3>/*",
                "arn:aws:s3:::<AWS bucket name 3>"
            ]
        }
    ]
}
Notes
  • For every bucket your Lambda execution role needs permissions to, you need to add two lines in the Resource area. One - with the /* wildcard - gives access to all items in the bucket, the second gives permissions to the root of the bucket.

Now, we can start looking at the cloud function itself, which will perform the resizing.

Serverless code function for performing image resizing and re-encoding

Before writing the code itself, we first need to understand some critical concepts: performance, image quality, encoding, and image dimensions.

Performance

The speed of your image resizing code is crucial. The longer it takes, the longer a user has to wait before you can finish processing their images and show their artwork or any other image file.

For best security practices, you also want to avoid showing the user the original image file they uploaded in the submission form. Doing so requires that image to be publicly available to the Internet, even for a brief period, which isn't ideal. Instead, you should resize the image to a small size (e.g. 640x480 or similar) and show the user that, preventing their full-size upload from ever being made publicly visible. As a result, being able to do this quickly is very important.

For best performance, you need to use a highly performant library, which usually means using one that relies on libvips internally. sharp is an excellent option, but there are likely others. You will also need to understand what encoders to use, particularly when working with JPEGs.

Image quality (particularly important for JPEGs)

For JPEGs in particular, quality is not just a parameter you enter into the encoder, and it is not e.g. a linear estimate of quality. JPG quality values are on an arbitrary scale that is roughly logarithmic; going from 70 -> 80% quality is not the same increase as going from 80 -> 90% quality.

For JPEGs, the higher you go with quality, the less additional visual quality is gained, but a larger filesize increase is gained. For example, moving from 60 -> 70% quality results in fairly little change in filesize but a noticeable improvement in quality; moving from 90% -> 95% quality results in almost no visual improvement but a significant increase in filesize.

For non-art websites, fairly low values like 70-80% are chosen, as a tradeoff between quality and filesize (which will impact your bandwidth costs and page loading times). For an art site, quality is more important, and so I would recommend 90% quality for images that will be viewed at large sizes and 80% for thumbnails of 640x480 or smaller, where the slightly lower visual quality is less noticeable, and where the bandwidth costs are most pronounced due to the large number of such images on a given page.

In addition to the quality parameter, JPEGs also have chroma subsampling, which reduces the range of colours in an image to reduce filesize. In most web applications, this is fine and frequently is not noticeable or significant, but for an art website this is not a good thing. By default it is normally set to 4:2:0, which reduces colour ranges somewhat; you want to set this to 4:4:4 for the purposes of an art website, which disables chroma subsampling.

Image encoders

Taking JPEGs as an example, a JPEG is not a format that is made in one prescribed way; only the result has a standard form. There are several encoders that can save an image as a JPEG file; older encoders tend to run a bit faster, but result in higher filesizes for the same quality than newer encoders.

I would recommend using the MozJPEG encoder, which achieves file sizes comparable to Google's WebP format, with no reduction in quality compared to older JPG encoders or WebP. In addition, for an art website this is even more preferable, as artists are not big fans of WebP (it is not well supported by many art programs, and even some image viewing software). The sharp library has an option for using MozJPEG when saving JPEG images; I highly recommend you use it.

Image dimensions

When resizing artworks proactively (i.e. not lazily, on-demand, as a CDN solution does), you need to make sure you have the image in a few different sizes for displaying efficiently. For an art website, you may have a 4K version of the image for subscribed users, a 1080p version for normally viewing an artwork on its own in a details page, a 480p version for thumbnails in galleries, and perhaps a 240p version as a preview image in notifications areas.

Your image resizing code should not do all of these resizes in one function invocation. Instead, your webserver should call the function one time for each size it needs to make, and wait for all results to finish. Since these can all be done in parallel, the whole operation only takes as long as the longest resize operation (typically the 4K one if it is required). Since the larger resizes take more time, avoid resizing an image to e.g. 4K if it's not already bigger than 4K - while you can tell sharp or other libraries to not increase an image's size, it's still better to avoid performing any resize in the first place so that you don't unnecessarily make the user wait (and consume Lambda execution time).

For an example GitHub repository, see here: https://github.com/antsstyle/sample-imageresizing-code .