Image resize on upload v13

I’m struggling with some people in the business uploading full-sized 7Mb photos to our site. I’ve found the ImageResizer (ImageResizer | Umbraco Marketplace) plugin, which works well on local, but it does not affect images uploaded to Umbraco Cloud.

Is there a neat way to make all image uploads be resized on width to at most 1920 pixels wide? or to restrict by file size (but not blocking things like large pdfs)

On v13 you can create crops either in the code or in the back office then called them as named crops in the code. This make use of ImageSharp which is part of v13.
However, after v13 this will no longer be the case due to licencing changes.

1 Like

You shouldn’t need another package to do this and should be able to achieve it using a combination of INotificationAsyncHandler<MediaSavingNotification> and ImageSharp which ships with Umbraco.

You could cancel the save operation when people exceed a given threshold for file size or dimensions. or you can resize it for them. This is what I am doing at the moment in v13.

    /// <summary>
    /// Checks the upload size and permissions to upload the filetype.
    /// </summary>
    /// <param name="mediaItem">The <see cref="IMedia"/> item being saved.</param>
    /// <param name="notification">The <see cref="MediaSavingNotification"/>.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    private async Task CheckUploadSizeAndPermissions(IMedia? mediaItem, MediaSavingNotification notification,
        CancellationToken cancellationToken)
    {
        if (mediaItem == null) return;

        IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
        
        if(currentUser == null)
        {
            notification.CancelOperation(new EventMessage("Media Upload Permissions",
                "Unable to detect a valid current user  .", EventMessageType.Error));
            return;
        }
        
        IFileSystem mediaFileSystem = _mediaFileManager.FileSystem;
        int currentWidth = mediaItem.GetValue<int>(Conventions.Media.Width);
        string fileType = mediaItem.GetValue<string>(Conventions.Media.Extension);

        //Determine if this is a PNG file
        var isPng = fileType.Equals("png", StringComparison.OrdinalIgnoreCase);
        IEnumerable<string> supportedTypes = _contentSettings.Imaging.ImageFileTypes.ToList();
        string imgFileProp = mediaItem.GetValue<string>(Conventions.Media.File);

        // Get the relative file path
        var filePath = JsonConvert.DeserializeObject<ImageCropperValue>(imgFileProp).Src;

        // Get the full file system path
        string originalFilePath = mediaFileSystem.GetFullPath(filePath);

        // Make sure it's an image.
        string extension = Path.GetExtension(originalFilePath).Substring(1);

        if (extension.Equals("gif", StringComparison.InvariantCultureIgnoreCase) &&
            currentUser.Groups.All(g => g.Alias != Constants.UserGroups.Example))
        {
            notification.CancelOperation(new EventMessage("Media Upload Permissions",
                "You do not have permission to upload a file of this type.", EventMessageType.Error));
        }
    
        // If it is a supported file type and has a width greater than 4000px we need to resize it 
        if (supportedTypes.InvariantContains(extension) && currentWidth > 4000)
        {
            var resizedImage = await CreateResizedImage(mediaItem, originalFilePath, originalFilePath, 4000, 0, cancellationToken);
            if (resizedImage == null)
            {
                notification.Messages.Add(new EventMessage("Image Resize", "Unable to resize image",
                    EventMessageType.Warning));
                _logger.LogWarning($"There was a problem resizing image {mediaItem.Name} ({mediaItem.Id})");
            }
            else
            {
                notification.Messages.Add(new EventMessage("Image Resized",
                    "Your image was resized to the maximum width of 4000 pixels wide.",
                    EventMessageType.Info));
            }
        }
    }

And here is the method that performs the resizing:

    /// <summary>
    /// Creates a resized version of the image at the size specified in the parameters.
    /// </summary>
    /// <param name="mediaItem">The media item to resize.</param>
    /// <param name="originalFilePath">The full path of the original file.</param>
    /// <param name="newFilePath">The full path of the new file.</param>
    /// <param name="width">The new image width.</param>
    /// <param name="height">The new image height.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A resized image object of type Image or null if there was an error resizing the image.</returns>
    private async Task<Image?> CreateResizedImage(IMedia mediaItem, string originalFilePath, string newFilePath,
        int width, int height, CancellationToken cancellationToken)
    {
        using Image img = await Image.LoadAsync(originalFilePath, cancellationToken);
        var newImage = img;

        try
        {
            if (img.Width > width || img.Height > height)
            {
                img.Mutate(x => x.Resize(new ResizeOptions
                {
                    Mode = ResizeMode.Max,
                    Size = new Size(width, height),
                    Position = AnchorPositionMode.Center
                }));

                // Determine encoder based on file extension
                IImageEncoder encoder = GetEncoderForFilePath(newFilePath);

                await using var outputStream = File.Create(newFilePath);
                await img.SaveAsync(outputStream, encoder, cancellationToken);
            
                newImage = img;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred while resizing the image.");
            return null;
        }

        mediaItem.SetValue(Conventions.Media.Width, newImage.Width);
        mediaItem.SetValue(Conventions.Media.Height, newImage.Height);

        try
        {
            mediaItem.SetValue(Conventions.Media.Bytes, _mediaFileManager.FileSystem.GetSize(originalFilePath));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting new resized image file size");
        }

        return newImage;
    }

Hopefully that helps.

1 Like

Have you got any further details on that? I’m a bit out of the loop and that potentially has quite an impact on a project I am working on that will likely be upgraded to the next LTS version.

1 Like

afaik you have to add it as package
[Breaking change]: Separating ImageSharp and SQL Server dependencies · Issue #5 · umbraco/Announcements

2 Likes

Thanks! I’ll take a look today!

I seem to be getting an error on save of an image and I wonder if I’m constructing this incorrectly, your code is great and seems to promise exactly what I would like to implement but I’m not sure how to build the notification properly, I was getting build errors for ContentSettings and now that that is resolved it falls over at the save, could be it can’t access the allowed file extensions or something. Probably just my lack of umbraco knowledge shining through

You need to ensure that you pass in the ContentSettings in your constructor.

public class MediaSavingAsyncNotificationHandler : INotificationAsyncHandler<MediaSavingNotification>
{
    private readonly ILogger<MediaSavingAsyncNotificationHandler> _logger;
    private readonly MediaFileManager _mediaFileManager;
    private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
    private readonly ContentSettings _contentSettings;

    public MediaSavingAsyncNotificationHandler(ILogger<MediaSavingAsyncNotificationHandler> logger,
        MediaFileManager mediaFileManager, IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
        IOptions<ContentSettings> contentSettings)
    {
        _logger = logger;
        _mediaFileManager = mediaFileManager;
        _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
        _contentSettings = contentSettings.Value;
    }
    ...
}

If you are getting another error then you need to share it or it’s just a guessing game.

1 Like

Oh! I see what I had wrong. Gimme a sec to test and see if the error persists, if it does I’ll share it!

It’s working! Thanks so much. I’ll likely have to make some tweaks to bits and pieces, but your solution is excellent!

I’ll add a credit in the comments as well :slight_smile:

1 Like

Sorry I’m getting one error.

It’s working on local but on the dev environment on cloud it’s not quite acting right.

I get this error:
image

Is this something where umbraco cloud has a slightly different variant of a service or something? I think because of Azure blob I need to use file streams maybe.

Just a final update so anyone else on cloud can get this to work. You need to rely on relative paths as it’s in cloud storage not in the media folders.

You also need to use

_mediaFileManager.FileSystem.OpenFile

as the stream for Image.LoadAsync

then use new MemoryStream() as the steam for img.SaveAsync

It took me ages but this now works consistently and for multiple media types

2 Likes